Photon Plugins FAQs
Contents
配置
Photon是否支持多個插件?
每個應用程序一次只能配置一個插件組件(DLL)或插件factory。 在這個DLL中,您可以有任意多的插件。 在創建房間時,只有一個插件會被加載和事件化。 房間和插件的事件之間是1對1的關係--所以每個房間都有自己的插件事件。
<PluginSettings Enabled="true">
<Plugins>
<Plugin …/>
<Plugin …/>
</Plugins>
</PluginSettings>
如何在創建房間時選擇使用哪個插件?
我們的插件模型使用一個factory模式。 插件是按名稱按需事件化的。
客戶端創建房間時,要求使用roomOptions.Plugins
進行插件設置。
roomOptions.Plugins
是string[]
類型,其中第一個字符串(roomOptions.Plugins[0]
)應該是一個插件名稱,將被傳遞給factory。
例如:
roomOptions.Plugins = new string[] { "NameOfYourPlugin" };
或
roomOptions.Plugins = new string[] { "NameOfOtherPlugin" };
或
如果客戶端沒有發送任何東西,伺服器將使用默認值(當沒有配置時)或插件factory在創建時返回的任何東西(如果配置了)。
在factory中只需使用名稱來加載相應的插件,如下所示:
public class PluginFactory : IPluginFactory
{
public IGamePlugin Create(IPluginHost gameHost, string pluginName, Dictionary<string, string> config, out string errorMsg)
{
var plugin = new DefaultPlugin(); // default
switch(pluginName){
case "Default":
// name not allowed, throw error
break;
case "NameOfYourPlugin":
plugin = new NameOfYourPlugin();
break;
case "NameOfOtherPlugin":
plugin = new NameOfOtherPlugin();
break;
default:
//plugin = new DefaultPlugin();
break;
}
if (plugin.SetupInstance(gameHost, config, out errorMsg))
{
return plugin;
}
return null;
}
}
如果PluginFactory.Create
中返回的插件名稱與客戶要求的不一致。
那麼該插件將被卸載,客戶端的創建或加入操作將以 PluginMismatch (32757)
的錯誤失敗。
我想在運行時從磁盤讀取文件。插件可以訪問文件系統嗎?我需要從外部伺服器下載文件嗎?
您可以在上傳插件所必需的文件的同時上傳額外的文件。
然後可以使用typeof(yourplugin).Assembly.Location
檢索文件的路徑。
依賴模組(DLLs)是否會與主插件的DLL一起自動加載?為什麼我得到一個System.TypeLoadException
?
使用外部模組應該通過引用插件解決方案中的DLLs或項目來執行。 您需要確保所有引用的依賴模塊被部署到與插件DLL相同的目錄,以便它們被加載和鏈接。
Callbacks
當玩家試圖進入一個房間時,會調用哪些方法?
為了回答這個問題,我們需要了解有四種不同的情況,包括玩家試圖通過創建、加入或重新加入一個房間。
一般來說,JoinRoom
和 CreateRoom
操作的結構非常相似。
把它們看作是一個具有不同 JoinMode
值的邏輯加入操作可能會有幫助:
- 創建:
OpCreateRoom
:如果房間已經存在,則返回錯誤。 - 加入:
OpJoinRoom
(JoinMode.Default
或未設置),OpJoinRandomRoom
:如果房間不存在則返回錯誤。 - CreateIfNotExists:
OpJoinRoom
(JoinMode.CreateIfNotExist
): 如果房間不存在就創建房間。 - RejoinOnly:
OpJoinRoom
(JoinMode.RejoinOnly
): 如果actor不存在於房間,則返回錯誤。
如果房間在內存中,2)到4)將觸發BeforeJoin
,假設您調用{ICallInfo}.Continue()
,OnJoin
將被調用。
OnCreateGame
在房間創建後,一旦插件被設置好,在actor被添加到房間之前就會被調用。
這可以由1)、3)和4)觸發。
後者只有在保存和加載狀態被正確處理的情況下才會發生。
這發生在玩家要求重新加入,並從伺服器內存中刪除房間的時候。
Photon將創建它並設置插件。
然後,該插件將從數據庫或外部服務中獲取序列化的狀態,並調用SetSerializedGameState
。
該狀態將包含一個actor的列表,所有的actor都處於非活動狀態。
重新加入會重新激活重新加入的角色。
如何在插件回調中獲得actor編號?
下面是如何在插件掛鉤中獲得actor編號:
1. OnCreateGame, Info.IsJoin == False:
ActorNr = 1:遊戲的第一個actor,也就是創建游戲的人,其actor編號將永遠被設置為1。
2. OnCreateGame, Info.IsJoin == True:
A. Before Info.Continue();
通過UserId從非活動actor的列表中獲取actor編號。如果房間是通過加載其遊戲狀態而被 "重新創建 "的。 那麼您需要從加載的房間狀態的ActorList上循環找到遊戲狀態中的actor編號,並與UserId進行比較。 (UserId可能不可用,這將使重新加入失敗,這需要用CheckUserOnJoin和每個actor的唯一UserId創建房間)
// load State from webservice or database or re-construct it
if (this.PluginHost.SetGameState(state))
{
int actorNr = 0;
foreach (var actor in PluginHost.GameActorsInactive)
{
if (actor.UserId == info.UserId)
{
actorNr = actor.ActorNr;
break;
}
}
if (actorNr == 0)
{
if (!asyncJoin)
{
// error, join will fail with
// ErrorCode.JoinFailedWithRejoinerNotFound = 32748, // 0x7FFF - 19,
}
else
{
actorNr = PluginHost.GetSerializableGameState().ActorCounter + 1;
}
}
}
否則,如果您在JoinRoom操作中從客戶端發送 "正確的 "ActorNr,您可以從info.Request.ActorNr
取得。
B. After Info.Continue();
通過UserId從活動actor的列表中獲取ActorNr。
int actorNr;
foreach (var actor in PluginHost.GameActorsActive)
{
if (actor.UserId == info.UserId)
{
actorNr = actor.ActorNr;
break;
}
}
3. BeforeJoin
A. Before Info.Continue();
int actorNr = 0;
switch (info.Request.JoinMode)
{
case JoinModeConstants.JoinOnly:
case JoinModeConstants.CreateIfNotExists:
actorNr = PluginHost.GetSerializableGameState().ActorCounter + 1;
break;
case JoinModeConstants.RejoinOnly:
foreach (var actor in PluginHost.GameActorsInactive)
{
if (actor.UserId == info.UserId)
{
actorNr = actor.ActorNr;
break;
}
}
if (actorNr == 0)
{
// error, join will fail with
// ErrorCode.JoinFailedWithRejoinerNotFound = 32748, // 0x7FFF - 19,
}
break;
case JoinModeConstants.RejoinOrJoin:
foreach (var actor in PluginHost.GameActorsInactive)
{
if (actor.UserId == info.UserId)
{
actorNr = actor.ActorNr;
break;
}
}
if (actorNr == 0)
{
actorNr = PluginHost.GetSerializableGameState().ActorCounter + 1;
}
break;
}
否則,如果您在JoinRoom操作中從客戶端發送了 "正確的 "ActorNr,您可以從info.Request.ActorNr
取得。
B. After Info.Continue();
通過UserId從活動actor的列表中獲取ActorNr。
int actorNr;
foreach (var actor in this.PluginHost.GameActorsActive)
{
if (actor.UserId == info.UserId)
{
actorNr = actor.ActorNr;
break;
}
}
4. OnJoin, OnRaiseEvent, BeforeSetProperties, OnSetProperties, OnLeave:
使用可用的info.ActorNr
。
5. BeforeCloseGame, OnCloseGame:
無法獲得ActorNr,因為掛鉤是由伺服器觸發的,而不是由客戶端操作。
是否可以從伺服器上創建房間?
不,不可能這樣做。
是否可以從伺服器上刪除房間?
不,不可能這樣做。
什麼是最好的方法來理解插件事件中的數據?
一般來說,事件的ICallInfo
回調參數應該暴露出它的需求。
對於大多數情況,實際的操作請求參數可以通過{ICallInfo}.OperationRequest
(或Request
)屬性來檢索。
UseStrictMode
的功能?
該插件的理念是,在我們處理傳入請求之前或之后,您可以鉤住 "正常"的Photon流程。
最初我們並沒有強制要求您必須調用任何東西。這基本上等於取消了對收到的請求的默認處理。
這使得開發者有責任在不使用任何默認回調邏輯的情況下從頭開始執行任何需求。
這造成了一些意想不到的問題。取消處理並不總是開發者想要做的。
所以我們引入了嚴格模式--期望由開發者來決定。
現在您被期望只調用Continue
、Fail'或
Cancel`一次(任何一次)。
當覆蓋PluginBase
的回調方法時,我是否需要調用base.XXX()
?
所有在PluginBase
中回調方法在最後都包括對Continue()
的調用。
我們的想法是,通過繼承PluginBase
,您只需要覆蓋您感興趣的方法。
有時您只需要在base.XXX()
之後或之前添加額外的代碼,有時您需要完全改變默認行為。
在第一種情況下,您不應該添加任何對ICallInfo
處理方法的調用。
在第二種情況下,您不應該調用base.XXX()
,從頭開始做您自己的方法執行,然後添加對可用的ICallInfo
處理方法之一的調用。
不過有一個例外,那就是PluginBase.OnLeave
。
這個方法將處理MasterClient的變化,以防當前的客戶要離開。
為什麼ILeaveGameCallInfo.ActorNr == -1
在OnLeave
中出現?
這可能發生在客戶端未能進入房間的情況下(創建或加入)。
基本上ILeaveGameCallInfo.ActorNr == -1
意味著客戶端一開始就沒有被添加到房間,也沒有被賦予一個actor號碼。
OnLeave(ILeaveGameCallInfo.ActorNr == -1)
是在OnCreateGame
或BeforeJoin
中調用info.Continue()
的結果。
當OnLeave'被觸發時,伺服器不會在房間裡為相應的客戶找到一個actor,這就是為什麼ActorNr的值默認為-1(可以被理解為 "沒有找到actor")。
ILeaveGameCallInfo.Reason,在這裡,應該總是3:
LeaveReason.ServerDisconnect`。
這裡有一個樣本插件,可以檢測並記錄ILeaveGameCallInfo.ActorNr == -1
:
using System.Collections.Generic;
using System.Linq;
namespace Photon.Plugins.Samples
{
public class JoinFailureDetection : PluginBase
{
private static readonly Dictionary<string, ICallInfo> pendingJoin = new Dictionary<string, ICallInfo>();
public override void OnCreateGame(ICreateGameCallInfo info)
{
this.PluginHost.LogInfo(string.Format("OnCreateGame UserId={0} GameId={1}", info.UserId, info.Request.GameId));
this.AddPendingJoin(info.UserId, info);
info.Continue(); // in case of join failure this will call OnLeave inside w/ ActorNr == -1
this.CheckJoinSuccess(info.UserId, info);
}
public override void BeforeJoin(IBeforeJoinGameCallInfo info)
{
this.PluginHost.LogInfo(string.Format("BeforeJoin UserId={0} GameId={1}", info.UserId, info.Request.GameId));
this.AddPendingJoin(info.UserId, info);
info.Continue(); // in case of join failure this will call OnLeave inside w/ ActorNr == -1
this.CheckJoinSuccess(info.UserId, info);
}
public override void OnLeave(ILeaveGameCallInfo info)
{
this.PluginHost.LogInfo(string.Format("OnLeave UserId={0} GameId={1}", info.UserId, this.PluginHost.GameId));
this.CheckJoinFailure(info);
info.Continue();
}
private void AddPendingJoin(string userId, ICallInfo info)
{
if (string.IsNullOrEmpty(userId))
{
this.PluginHost.LogError("UserId is null or empty");
}
else
{
this.PluginHost.LogInfo(string.Format("User with ID={0} is trying to enter room {1}, ({2})", userId, this.PluginHost.GameId, info.GetType()));
pendingJoin[userId] = info;
}
}
private void CheckJoinFailure(ILeaveGameCallInfo info)
{
if (info.ActorNr == -1)
{
this.PluginHost.LogWarning(string.Format("ILeaveGameCallInfo.ActorNr == -1, UserId = {0} Reason = {1} ({2}) GameId = {3} Details = {4}", info.UserId, info.Reason, LeaveReason.ToString(info.Reason), this.PluginHost.GameId, info.Details));
}
if (string.IsNullOrEmpty(info.UserId))
{
this.PluginHost.LogError("ILeaveGameCallInfo.UserId is null or empty");
return;
}
if (pendingJoin.ContainsKey(info.UserId))
{
this.PluginHost.LogError(string.Format("User with ID={0} failed to enter room {1}, removing pending request {2}", info.UserId, this.PluginHost.GameId, pendingJoin[info.UserId]));
pendingJoin.Remove(info.UserId);
}
else
{
this.PluginHost.LogError(string.Format("no previous pending join for UserID = {0} GameId = {1}", info.UserId, this.PluginHost.GameId));
}
}
private void CheckJoinSuccess(string userId, ICallInfo info)
{
if (info.IsSucceeded)
{
if (string.IsNullOrEmpty(userId))
{
this.PluginHost.LogError("userId is null or empty");
return;
}
if (this.PluginHost.GameActorsActive.Any(u => userId.Equals(u.UserId)))
{
if (pendingJoin.ContainsKey(userId))
{
this.PluginHost.LogInfo(string.Format("User with ID={0} succeeded to enter room {1}, removing pending request {2}", userId, this.PluginHost.GameId, pendingJoin[userId]));
pendingJoin.Remove(userId);
}
else
{
this.PluginHost.LogDebug(string.Format("no previous pending join for UserID = {0} GameId = {1}", userId, this.PluginHost.GameId));
}
}
}
}
public override void OnCloseGame(ICloseGameCallInfo info)
{
info.Continue();
foreach (var pair in pendingJoin)
{
this.PluginHost.LogError(string.Format("Room {0} is being removed, unexpected leftover pending join UserID = {1} ICallInfo = {2}", this.PluginHost.GameId, pair.Key, pair.Value));
}
}
}
}
一些加入失敗的例子(您可以看一下創建或加入房間操作的客戶端錯誤代碼)。
- 一個用戶試圖重新加入一個房間,而那裡沒有相同UserID的非活動actor。 客戶端將得到錯誤代碼JoinFailedWithRejoinerNotFound = 32748。
- 一個用戶試圖加入一個房間,而那裡有一個具有相同UserID的活動actor。 客戶端將得到錯誤代碼JoinFailedFoundActiveJoiner = 32746。
- 一個用戶試圖加入一個房間,而那裡有一個非活動的actor,有相同的UserID。 客戶端將得到錯誤代碼JoinFailedFoundInactiveJoiner = 32749。
如果ILeaveGameCallInfo.ActorNr == -1
不是加入失敗的結果,那麼它就是一個異常,所以要麼在自定義用戶插件代碼中存在異常,要麼在內部伺服器代碼中。
在任何情況下,從插件方面,您都可以及早發現這個問題並記錄追蹤。
另外ILeaveGameCallInfo.ActorNr
應該在ILeaveGameCallInfo.Continue
前後發生變化,您仍然可以檢測到這一點並報告它。
您可以在代碼中通過執行ReportError
方法來捕獲異常,或者如果從PluginBase.Case擴展,則重寫它。
事件
HttpForward
屬性和WebFlags
類的功能?
這些是與WebHooks相關的功能。
WebFlags
是在WebHooks v1.2插件中引入的。請到本頁閱讀更多關於WebFlags的信息。
HttpForward
是一個屬性,表示相應的webflag的值。
雖然它們最初是為WebHooks和WebRPC制作的,但您可能想在您的插件中使用它們。
如何從插件發送事件?
PluginHost.BroadcastEvent
被用於這個目的。
它的工作方式與您從客戶端發送事件的方式相同,主要區別是:
您可以將發送者的編號設置為0,以代表伺服器。
了解更多信息,請閱讀插件手冊中的 "從插件發送事件"部分。
為什麼PluginHost.BroadcastEvent
在OnJoin
回調中被調用時,從未成功發送事件到客戶端,而在OnRaiseEvent
中則不同?
在OnRaiseEvent
中,客戶端已經加入,所以它能夠接收事件。
在OnJoin
中,除非使用{ICallInfo}.Continue()
處理請求,否則客戶端並沒有完全加入。
因此,如果在{ICallInfo}.Continue()
之後調用PluginHost.BroadcastEvent
,事件應該被目標客戶端收到。
插件掛鉤的工作方式是:您通過調用{ICallInfo}.Continue()
來觸發Photon的正常處理。
在這種情況下,在連接完全完成後發送一個事件更有意義。
為什麼客戶端不能接收從插件發送的事件數據?
這是一個已知的問題。
客戶端期望事件具有某種預定義的結構,具有已知的關鍵代碼。
245代表事件數據,254代表actor編號。
要解決這個問題,只需對您從插件發送事件數據的方式做一個小小的改變。
代替(Dictionary<byte,object>)eventData
發送new Dictionary<byte,object>(){{245,eventData},{254,senderActorNr}}
。
插件是否支持自定義操作?
不,插件不支持自定義操作。 Photon插件只為一組操作(Create, Join, SetProperties, RaiseEvent 和 Leave)提供回調。 您不能攔截在自我的Photon伺服器中添加的自定義操作,也不能使用Plugins SDK擴展新的操作。
然而,您可以通過交換雙向事件來達到同樣的效果。
- 通過調用
LoadBalancingClient.OpRaiseEvent
從客戶端到插件。 - 從插件到客戶端,調用
PluginHost.BroadcastEvent
。
遊戲狀態
如何區分 "活躍 "用戶和 "非活躍 "用戶?
一旦玩家斷開連接,Photon通常會進行清理,actor也會被移除,但您可以在創建房間時在客戶端的CreateOptions
中定義一個PlayerTTL
。
如果它是嚴格意義上的正數,Photon在清理之前會等待這個時間(以毫秒為單位)。
同時,該actor被認為是不活躍的,可以重新加入遊戲。如果成功地做到了這一點,該玩家就再次活躍起來。
如果您想保存遊戲狀態並允許玩家繼續遊戲,或者例如RTS遊戲,玩家可能因為連接不良而斷開連接,但在短時間內(幾分鐘)允許他們返回,那麼這個功能就很有用。
PluginHost.GameActorsActive
包含房間內的所有actor(加入),PluginHost.GameActorsInActive
包含所有離開房間的actor(沒有放棄)。
是否可以從一個插件中使actor離開房間?如果是的話,怎麼做?
是的,這是有可能的。在插件類中,您應該調用PluginHost.RemoveActor(int actorNr, string reasonDetails)
。
您可以通過調用接受三個參數的方法重載來設置原因。
PluginHost.RemoveActor(int actorNr, byte reason, string reasonDetails)
。
如何保持房間的狀態?
要保存一個房間的狀態
- 調用
PluginHost.GetGameState
並獲得一個SerializableGamestate
。 - 序列化該狀態(例如JSON)。
- 將狀態保存到您的數據存儲中。
要加載一個房間狀態:
- 從您的數據存儲中檢索狀態。
- 將您的狀態解凍。
- 調用
PluginHost.SetGameState
。
Note: 您只允許在調用{ICallInfo}.Continue()
之前,在OnCreateGame
中調用PluginHost.SetGameState
。
我在SerializableGameState
裡面找不到一些自定義房間屬性?只有大廳屬性,這是為什麼?
在可序列化的遊戲狀態中,我們在設計上不提供對所有自定義屬性的訪問。 只有那些您與大廳共享的屬性才會被暴露出來,這只是為了 "查看"的目的。 所有的屬性組合都包含在二進制數組中。 這允許序列化為JSON,並且在反序列化時不會丟失類型信息。 這個功能主要是為保存/加載場景設計的。 我們可能會在未來改變這種行為。
如何從插件訪問房間屬性(MaxPlayers
, IsVisible
, IsOpen
, Etc.)?
所有的房間屬性都可以從PluginHost.GameProperties
中訪問,它的類型是Hashtable
。
這些屬性包括 "本地"或 "已知"的屬性。
這些屬性被列在Photon.Hive.Plugin.GameParameters
中。
PluginHost.GameProperties
也包含自定義房間屬性。
那些應該有字符串鍵,由您來處理。
另一方面,只有大廳可見的自定義屬性被存儲在PluginHost.CustomGameProperties
中,它是一個Dictionary<string, object>
。
這個屬性應該被看作是只讀的。
您可以從插件中訪問(讀和寫)房間和actor的屬性,如下所示:
Some helper methods to get properties:
private bool TryGetRoomProperty<T>(byte key, out T property)
{
property = default;
if (this.PluginHost.GameProperties.ContainsKey(key))
{
property = (T)this.PluginHost.GameProperties[key];
}
return false;
}
private bool TryGetCustomRoomProperty<T>(string key, out T property)
{
property = default;
if (this.PluginHost.GameProperties.ContainsKey(key))
{
property = (T)this.PluginHost.GameProperties[key];
}
return false;
}
private bool TryGetActorByNumber(int actorNr, out IActor actor)
{
actor = this.PluginHost.GameActors.FirstOrDefault(a => a.ActorNr == actorNr);
return actor == default;
}
private bool TryGetActorProperty<T>(int actorNr, byte key, out T property)
{
property = default;
if (this.TryGetActorByNumber(actorNr, out IActor actor) && actor.Properties.TryGetValue(key, out object temp))
{
property = (T)temp;
}
return false;
}
private bool TryGetCustomActorProperty<T>(int actorNr, string key, out T property)
{
property = default;
if (this.TryGetActorByNumber(actorNr, out IActor actor) && actor.Properties.TryGetValue(key, out object temp))
{
property = (T)temp;
}
return false;
}
Writing examples:
PluginHost.SetProperties(actorNr: 0, properties: new Hashtable { { "map", "america" } }, expected: null, broadcast: false); // actor=0 for Room properties
PluginHost.SetProperties(actorNr: 1, properties: new Hashtable { { "health", 100 } }, expected: null, broadcast: true);
線程
關於.NET插件組件的線程要求的更多細節
Photon主機是自由線程的嗎?任何線程都可以在任何時間訪問任何房間嗎?
我們有一個消息傳遞架構:你的插件一次只能被一個線程調用。 但是,由於我們使用線程池,每次調用的線程可能不一樣。
編寫插件有什麼線程安全問題嗎?
通常我們可以假設所有對插件的調用都是串行化的(實際上是在一個線程上/不一定在同一個物理線程上)。
企業雲端
與企業雲端的運行環境有關的問題。
如何配置插件?
對於Photon企業雲端:
您應該進入Photon儀表盤,然后進入應用程序的管理頁面,添加一個新的插件。
在那裡,您應該點擊頁面底部的 "創建一個新的插件 "按鈕。
現在您可以通過添加鍵/值項來配置該插件。
AssemblyName
, Version
, Path
和Type
是必須的。
制作Photon插件的流程是什麼?
制作Photon插件的流程很簡單:
- 下載所需的SDK和伺服器二進制文件。
- 編碼並建立一個插件組件。
- 部署和測試。
- 上傳。
- 配置。
Photon插件的環境設置可以是:
- 開發:本地機器。
- 測試:在您的本地網絡中。
- Staging: 在我們的雲端中的獨立AppId。
- 生產:在我們的雲端的實時AppId。
插件的上傳是否自動化?
是的,我們為企業客戶提供PowerShell腳本,幫助他們管理他們的私有雲端。 請查看插件上傳在線指南,了解全部細節。
如何監控插件的性能?
我們追蹤一堆計數器,這些計數器在我們的板上都有。 此外,我們可以為您添加自定義的計數器,或者您也可以集成一個外部的計數器工具(例如,New Relic)。 如果您需要這些服務,需要安排一個咨詢協議。
是否有辦法擁有任何插件的日誌?
在我們的伺服器上訪問日誌文件是不允許的。 所以應該使用外部服務來獲取日誌或警報。 我們建議使用Logentries或Papertrail。 所以我們要求我們的企業客戶與我們聯系,用他們選擇的日誌服務配置他們的私有雲端。 如果您想使用Logentries提供配置的日誌令牌。 如果您想使用Papertrail提供您的自定義url與端口 。
我如何獲得Logentries令牌?
創建一個Logentries賬戶,並遵循以下步驟:
- 選擇 "日誌"/"添加新日誌"。
- 選擇 "庫"/".NET"。
- 為您的日誌集輸入一個名稱。
- 單擊 "創建日誌令牌"。
- 單擊 "完成並查看日誌"。
- 選擇您的新日誌集,選擇 "設置"標簽。您現在可以查看令牌了。
- 將該令牌通過電子郵件發送給我們。
如何獲得Papertrail URL?
創建一個Papertrail賬戶,並遵循以下步驟:
- 添加 "系統"
- 在下一頁的頂部,您會看到類似 "您的日誌將轉到logs6.papertrailapp.com:12345並出現在事件中"。
- 將該網址通過電子郵件發送給我們。
推廣新的插件版本的推薦方式是什麼?
另一種方式,但更進階技術。
您可以建立一個擁有核心伺服器更新循環的插件DLL,而實際的遊戲邏輯則來自另一個將被明確加載的DLL。 核心插件DLL不應該被更新,而遊戲邏輯應該在每個版本中有一個以上的DLL。 核心插件DLL將根據客戶端的版本加載適當的遊戲邏輯DLL。 這就像將伺服器端的代碼與客戶端的代碼進行映射,以達到完全兼容。 這樣就可以執行插件的版本兼容更新。 這樣,當一個新的插件版本推出時,您不需要強制客戶端更新。 您可以把遊戲邏輯DLLs放在每個版本的獨立文件夾裡,或者放在同一個文件夾裡,但每個版本的名稱不同。
我們可以在插件中使用靜態字段嗎?
其他
如何檢索插件執行 RemoveActor
時給出的 reason
?
目前,我們不向客戶端發送原因(代碼byte
或消息string
)。
您可以使用一個自定義的事件,在斷開連接前通知客戶端。
例如,您可以發送一個帶有原因的自定義事件,並在200ms後使用一個定時器安排RemoveActor
。
您可以使用以下輔助方法:
private const int RemoveActorEventCode = 199;
private const int RemoveActorTimerDelay = 200;
private void RemoveActor(ICallInfo callInfo, int actorNr, string reason)
{
this.PluginHost.BroadcastEvent(new List<int> { actorNr }, 0, RemoveActorEventCode,
new Dictionary<byte, object> { { 254, 0 }, { 245, reason }}, 0);
this.PluginHost.CreateOneTimeTimer(callInfo, () => this.PluginHost.RemoveActor(actorNr, reason),
RemoveActorTimerDelay);
}
private void RemoveActor(ICallInfo callInfo, int actorNr, byte reasonCode, string reason)
{
this.PluginHost.BroadcastEvent(new List<int> { actorNr }, 0, RemoveActorEventCode,
new Dictionary<byte, object> { { 254, 0 }, { 245, new { reasonCode, reason } }}, 0);
this.PluginHost.CreateOneTimeTimer(callInfo, () => this.PluginHost.RemoveActor(actorNr, reason),
RemoveActorTimerDelay);
}
private void RemoveActor(int actorNr, string reason)
{
this.PluginHost.BroadcastEvent(new List<int> { actorNr }, 0, RemoveActorEventCode,
new Dictionary<byte, object> { { 254, 0 }, { 245, reason }}, 0);
var fiber = this.PluginHost.GetRoomFiber();
fiber.CreateOneTimeTimer(() => this.PluginHost.RemoveActor(actorNr, reason), RemoveActorTimerDelay);
}
private void RemoveActor(int actorNr, byte reasonCode, string reason)
{
this.PluginHost.BroadcastEvent(new List<int> { actorNr }, 0, RemoveActorEventCode,
new Dictionary<byte, object> { { 254, 0 }, { 245, new { reasonCode, reason } }}, 0);
var fiber = this.PluginHost.GetRoomFiber();
fiber.CreateOneTimeTimer(() => this.PluginHost.RemoveActor(actorNr, reasonCode, reason), RemoveActorTimerDelay);
}
Client SDKs 支持通過注冊自定義類型來擴展序列化。我如何在伺服器上對這些類型進行反序列化?
您可以像在客戶端SDK中一樣注冊自定義類型,如下所示:
PluginHost.TryRegisterType(type: typeof (CustomPluginType), typeCode: 1, serializeFunction: SerializeFunction, deserializeFunction: DeserializeFunction);
更多信息請閱讀插件手冊的 "自定義類型 "部分。
對插件.DLL的文件大小有限制嗎?
沒有,但我們認為它不應該那麼大。
如何在Photon插件中獲得PUN的PhotonNetwork.ServerTimestamp
?
使用 Environment.TickCount
.