This document is about: SERVER 5
SWITCH TO

Photon Plugins Manual

Photon Plugins are available only for Enterprise Cloud or self-hosted Photon Server.

要了解Photon服務器插件SDK v5的新內容,請參見這裡

關於最佳方案和常見問題,請參見 Photon Plugins FAQ

簡介

插件API的設計接近於核心流程(創建房間、加入、離開等)。

  1. 保持高度的靈活性:允許在核心流程處理之前和之後挂鉤。
  2. 最大限度地減少破壞流程的機會:在客戶端和伺服器上快速提供錯誤。
  3. 允許使用無鎖代碼:插件事件將永遠不會得到超過一次的調用,框架提供了一個HTTP客戶端和集成在底層Photon消息傳遞架構(Fiber)的計時器。
  4. 降低復雜性,增加易用性:見"最小插件"

概念

為了添加自定義的服務器邏輯,您可以將你的代碼注入預定義的Photon服務器掛鉤中。 目前Photon伺服器隻支持遊戲伺服器插件,因為掛鉤是由房間事件觸發。

根據定義,Photon插件有一個獨特的名字,並執行這些事件回調。 自定義插件被編譯成一個DLL文件,稱為plugins assembly。 然后,將所需文件 "部署 "到您的Photon伺服器上或上傳到您的企業雲端。

在每個房間創建時,配置好的插件程序會在運行時動態加載。 然后根據factory pattern創建一個插件實例。

觸發插件創建的房間被稱為 "主 "遊戲。 後者可以直接從插件中訪問,它們都共享相同的執行時間。

Webhooks是Photon插件的一個好例子。 Webhooks 1.2的源代碼可以在插件SDK中找到。 我們鼓勵您去研究它。

基本流程

The Photon hooking mechanism relies on a 6 steps flow:

  1. 攔截挂鉤的調用
    當回調被觸發時,主機將控制權轉移給插件。
  2. [可選]修改調用信息
    訪問和修改客戶端/服務器發送的請求,在其被處理之前。
  3. [可選] 注入自定義代碼
    在處理調用之前與主機進行交互(例如,發出HTTP請求,查詢房間/actor,設置計時器,等等)。
  4. 處理挂鉤調用
    決定如何並處理請求(見"ICallInfo處理方法")。
  5. [可選]注入自定義代碼
    一旦處理完畢,客戶端/伺服器發送的請求就會 "只讀取 "使用。 然而,即使在處理之后,插件仍然可以與主機交互。
  6. 返回
    插件將控制權返回給主機。

入門

最小的插件

A "Step by Step Guide" is available for beginners.

插件

推薦的制作插件的簡單方法是擴展PluginBase類,而不是直接執行所有IGamePlugin方法。 這樣您就可以隻覆蓋您需要的方法。

要獲得一個最小的插件執行,您只需要覆蓋PluginBase.Name屬性。 它是識別一個插件的標誌。

"Default" and "ErrorPlugin" are reserved names used internally and should not be used as a custom plugin name.

C#

namespace MyCompany.MyProject.HivePlugin
{
  public class CustomPlugin : PluginBase
  {
    public override string Name
    {
        get
        {
            return "CustomPlugin"; // anything other than "Default" or "ErrorPlugin"
        }
    }
  }
}

The Factory

一個插件factory類預計將作為插件組件的一部分被執行。 它負責為每個房間創建一個插件事件。 為了簡單起見,在下面的片段中factory默認返回一個CustomPlugin的事件,而不檢查客戶要求的插件名稱。

C#

namespace MyCompany.MyProject.HivePlugin
{
  public class PluginFactory : PluginFactoryBase
  {
    public override IGamePlugin Create(string pluginName)
    {
        return new CustomPlugin();
    }
  }
}

配置

企業雲端配置

To add a new plugin:

  1. 進入Photon產品類型中的dashboard

  2. 進入其中一個Photon應用程序的管理頁面。

  3. 點擊頁面底部的 "創建新插件 "按鈕。

  4. 通過添加字符串類型的鍵/值來配置該插件。 配置是通過定義字符串的鍵/值對完成的。 每個字符串允許的最大長度是256個字符。 需要的設置是:

    • AssemblyName: 上傳的包含插件的DLL文件的全名。
    • Version: 插件的版本。使用Exit Games提供的PowerShell範本上傳輸插件文件時,使用或返回相同的版本字符串。
    • Path: 組裝文件的路徑。它應該有以下格式:"{customerName}}{pluginName}"。
    • 類型: 使用的PluginFactory類的全名。它有以下格式:"{plugins namespace}.{pluginfactory class name}"。

自有服務器配置

在您的GameServer應用程序的 "plugin.config "文件("deploy\LoadBalancing\GameServer\bin\plugin.config")中添加或修改以下XML節點。 <Plugin.../>元素的默認屬性在下面的例子中呈現。 只有 "版本 "是不需要的。 您可以添加其他可選的配置鍵/值對,傳遞給插件代碼。 您可以通過改變<PluginSettings>元素中的Enabled屬性值來輕鬆激活或停用插件。

XML

<Configuration>
  <PluginSettings Enabled="true">
    <Plugins>
      <Plugin
          Name="{pluginName}"
          Version="{pluginVersion}"
          AssemblyName="{pluginDllFileName}.dll"
          Type="{pluginNameSpace}.{pluginFactoryClassName}"
      />
    </Plugins>
  </PluginSettings>
</Configuration>

插件DLL、其依賴性和其他構建所需的文件必須根據插件名稱的配置值放在 "deploy\plugins{pluginName}{pluginVersion}\bin"文件夾中。

至少應該有這兩個文件:

  • "deploy\plugins\{pluginName}\{pluginVersion}\bin\{pluginDllFileName}.dll"
  • "deploy\plugins\{pluginName}\{pluginVersion}\bin\PhotonHivePlugin.dll"

如果不使用 "版本 "配置鍵或其值為空,則路徑將是 "deploy\plugins{pluginName}\\bin"。 而由於"\\"is treated as"\",預期的路徑是 "deploy\plugins{pluginName}\bin"。

所以至少應該有這兩個文件:

  • "deploy\plugins\{pluginName}\bin\{pluginDllFileName}.dll"
  • "deploy\plugins\{pluginName}\bin\PhotonHivePlugin.dll"

Plugin Factory

我們的插件模型使用factory設計模式。 插件是按名稱,需求事件化的。

一個Photon插件組件可能包含多個插件類和一個活躍的"PluginFactory"。 雖然這是可能的,但制作一個以上的 "PluginFactory "並沒有什麼特別的用處。 相反,編寫多個插件類可能非常有用。 例如,您可以為每個游戲類型(或模式或難度等)配備一個插件。 這個factory也可以用來提供多個插件版本。 閱讀更多關於這個用例

客戶端創建房間,要求使用roomOptions.Plugins進行插件設置。 roomOptions.Pluginsstring[]類型,其中第一個字符串(roomOptions.Plugins[0])應該是一個插件名稱,將被傳遞給factory。

例如: roomOptions.Plugins = new string[] { "NameOfYourPlugin" };
roomOptions.Plugins = new string[] { "NameOfOtherPlugin" };

如果客戶端沒有發送任何東西,伺服器將使用默認值(當沒有配置時)或插件factory在創建時返回的任何東西(如果配置了)。

如果PluginFactory.Create中返回的插件名稱與客戶端請求的插件不一致,那麼該插件將被卸載,客戶端的創建或加入操作將以PluginMismatch (32757)錯誤而失敗。
另外,目前,roomOptions.Plugins最多應該包含一個元素(插件名稱字符串)。 如果超過一個元素被發送(roomOptions.Plugins.Length>1),那麼將收到一個PluginMismatch錯誤,房間加入或創建將失敗。

在factory中隻需使用名稱來加載相應的插件,如下所示:

C#

using System.Collections.Generic;

public class PluginFactory : IPluginFactory
{

    public IGamePlugin Create(IPluginHost gameHost, string pluginName, Dictionary<string, string> config, out string errorMsg)
    {
        IGamePlugin 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;
    }
}

ICallInfo流程方法

該插件的概念是,在伺服器處理傳入的請求之前或之後,您可以鉤住 "正常"的Photon流程。 開發者需要根據請求類型決定使用4種可用的方法之一來執行:

  1. Continue ():用來恢復默認的Photon處理。
  2. Cancel():用於無聲取消流程,即沒有任何錯誤或其他通知給客戶。 它相當於跳過了進一步的處理:
    • OnRaiseEvent內調用,將忽略傳入的事件。
    • BeforeSetProperties內調用將取消屬性的改變。
  3. Fail(string msg, Dictionary<byte,object> errorData): 用於取消進一步的處理,向客戶端返回一個錯誤響應。 從客戶端,您可以在OperationResponse.DebugMessage中獲得msg參數,在OperationResponse.Parameters中獲得errorData

Notes:

  • 插件應該默認啟用嚴格模式(UseStrictMode = true)。 嚴格模式意味著在每個插件的回調中都要調用ICallInfo處理方法之一。 不調用任何可用的方法將拋出一個異常回應。 閱讀更多這裡
  • IGamePluginPluginBase執行的所有回調都會調用{ICallInfo}.Continue()
  • Continue(), Fail()Cancel() 期望只被調用一次。再次調用它們中的任何一個都會拋出一個異常回應。
  • Cancel()只能在OnRaiseEventBeforeSetProperties中調用。
  • 所有執行ICallInfo的類在可用時都會公開客戶端的原始操作請求。 您可以從{ICallInfo}.OperationRequest(或Request)屬性中獲得操作代碼和參數。
  • 所有執行ICallInfo的類都包括輔助屬性,以告知您操作請求的處理CallStatus
    • IsNew: 表示該請求是否沒有被處理,也沒有被推遲。
    • IsProcessed: 表明請求是否已經被處理(即調用了 "繼續 "或 "取消 "或 "失敗 "方法)。
    • IsSucceeded: 表示請求是否被成功處理(即調用了`Continue'方法)。
    • IsCanceled: 表明請求是否被取消(即調用了`Cancel'方法)。
    • IsPaused: 表示請求是否在內部暫停(例如,發送了同步HTTP請求)。
    • IsDeferred: 表示該請求是否被內部推遲(例如,發送了異步HTTP請求)。
    • IsFailed: 表示請求是否 "失敗"(即調用了Fail方法)。

插件回調

Photon伺服器有9個預定義的掛鉤。 您不需要在代碼中明確注冊這些掛鉤。 默認情況下,任何插件類都可以攔截所有9個事件。 然而,您應該執行您需要的那些。 我們建議擴展PluginBase並重寫所需的回調。

所有的核心事件回調都有一個特定的ICallInfo契約。 大多數回調是由客戶端操作直接觸發的。 客戶端發送的操作請求會在ICallInfo.Request中提供(如果有的話)。

OnCreateGame(ICreateGameCallInfo info)

Precondition: 客戶端調用OpCreateRoomOpJoinOrCreateRoomOpJoinRoom,而在Photon伺服器內存中找不到房間。

Processing Method Processing Result
Continue
  • CreateGame operation response is sent to the client with ReturnCode == ErrorCode.Ok.
  • Join event is sent back to client unless SuppressRoomEvents == false.
  • If ICreateGameCallInfo.BroadcastActorProperties == true player custom properties, if any, will be included in the event parameters.
  • If initialized for the first time; room options and initial properties are assigned to the room state, ActorList should contain first actor with its default properties (UserId and NickName). If request contains custom actor properties they should also be added to the actor entry in the list.
  • If loaded, room state should be the same as it was before last removal from Photon Servers memory unless it was altered.
Fail CreateGame operation response is sent to the client with ReturnCode == ErrorCode.PluginReportedError.
Cancel N/A
  • Notes:
    • 在處理請求之前,房間狀態沒有被初始化,包含默認值。 這是唯一可以從外部加載房間狀態的情況,通過調用IPluginHost.SetGameState解析並分配給房間。
    • 在處理請求之前,任何對PluginHost.SetPropertiesPluginHost.BroadcastEvent的調用將被忽略。
    • 您可以使用ICreateGameCallInfo.IsJoinICreateGameCallInfo.CreateIfNotExists來了解操作請求的類型。
Operation method IsJoin CreateIfNotExist
OpCreateRoom false false
OpJoinRoom true false
OpJoinOrCreateRoom true true

BeforeJoin(IBeforeJoinGameCallInfo info)

Precondition: 客戶端調用 OpJoinRoom OpJoinOrCreateRoom OpJoinRandomRoom 並且房間在Photon服務器的內存中。

Processing Method Processing Result
Continue triggers the OnJoin callback.
Fail JoinGame operation response is sent to the client with ReturnCode == ErrorCode.PluginReportedError.
Cancel N/A
  • Notes:
  • 在處理IBeforeJoinGameCallInfo之前,如果您調用PluginHost.BroadcastEvent,不要期望加入的角色會收到該事件,除非您緩存了它。

OnJoin(IJoinGameCallInfo info)

Precondition: IBeforeJoinGameCallInfo.Continue()BeforeJoin中被調用。

Processing Method Processing Result
Continue
  • If the join is allowed, joining actor is added to ActorList with its default properties (UserId and NickName).
  • If request contains custom actor properties they should be set also.
  • JoinGame operation response is sent back to the client with ReturnCode == ErrorCode.Ok.
  • If IJoinGameCallInfo.PublishUserId == true, UserId of other actors will be sent back to client in the operation response.
  • Join event is broadcasted unless SuppressRoomEvents == false.
  • If IJoinGameCallInfo.BroadcastActorProperties == true player custom properties, if any, will be included in the event parameters.
Fail
  • JoinGame operation response is sent to the client with ReturnCode == ErrorCode.PluginReportedError.
  • Adding of actor is reverted.
Cancel N/A

OnLeave(ILeaveGameCallInfo info)

Precondition: 客戶端調用OpLeave,對等體斷開連接或PlayerTTL過期。(見Actors life cycle)。

Processing Method Processing Result
Continue
  • If triggered by OpLeave operation, its response is sent to the client with ReturnCode == ErrorCode.Ok.
  • Leave event is sent to any other actor unless SuppressRoomEvents == false.
  • If ILeaveGameCallInfo.IsInactive == true:
    • The actor is marked as inactive.
    • DeactivationTime is added to its properties.
  • If ILeaveGameCallInfo.IsInactive == false:
    • The actor and its properties are removed from ActorList.
    • The relative cached events could also be removed if DeleteCacheOnLeave == true.
    • If the ActiveActorList becomes empty, a BeforeCloseGame call will follow after EmptyRoomTTL milliseconds.
Fail If triggered by OpLeave operation, its response is sent to the client with ReturnCode == ErrorCode.PluginReportedError.
Cancel N/A
  • Notes:
    • PlayerTTL可以在創建房間時設置。
    • 如果你調用PluginHost.BroadcastEvent,不要指望離開的actor會收到該事件。

OnRaiseEvent(IRaiseEventCallInfo info)

Precondition: 客戶端調用OpRaiseEvent

Processing Method Processing Result
Continue
  • The room state's events cache could be updated if a caching option is used.
  • The custom event is sent according to its parameters.
Fail RaiseEvent operation response is sent to the client with ReturnCode == ErrorCode.PluginReportedError.
Cancel silently skips processing.
  • Notes:
  • 如果IRaiseEventCallInfo被成功處理,沒有操作響應被送回給客戶端。

BeforeSetProperties(IBeforeSetPropertiesCallInfo info)

Precondition: 客戶端調用OpSetProperties

Processing Method Processing Result
Continue
  • Room or actor properties are updated.
  • SetProperties operation response is sent back to the client with ReturnCode == ErrorCode.Ok.
  • PropertiesChanged event is broadcasted.
  • triggers OnSetProperties.
Fail SetProperties operation response is sent to the client with ReturnCode == ErrorCode.PluginReportedError.
Cancel silently skips processing.
  • Notes:
  • 為了知道要改變的屬性是屬於房間還是屬於actor,您可以檢查IBeforeSetPropertiesCallInfo.Request.ActorNumber的值。 如果它是0,那麼房間的屬性即將被更新。 否則就是目標actor號碼,其屬性需要被更新。
  • 注意不要把前面提到的ActorNumberIBeforeSetPropertiesCallInfo.ActorNr混在一起。 後者指的是提出操作請求。

OnSetProperties(ISetPropertiesCallInfo info)

Precondition: IBeforeSetPropertiesCallInfo.Continue()BeforeSetProperties中被調用。

Processing Method Processing Result
Continue nil.
Fail only logs failure.
Cancel N/A

BeforeCloseGame(IBeforeCloseGameCallInfo info)

Precondition: 所有對象斷開連接。

Processing Method Processing Result
Continue triggers OnCloseGame.
Fail only logs failure.
Cancel N/A
  • Notes:
  • EmptyRoomTTL可以由客戶在創建房間時設置。
  • 任何對PluginHost.BroadcastEvent的調用都將被忽略,除非它改變了房間事件的緩存。

OnCloseGame(ICloseGameCallInfo info)

Precondition: IBeforeCloseGameCallInfo.Continue()BeforeCloseGame中被調用,並且EmptyRoomTTL已經過期。

Processing Method Processing Result
Continue the room is removed from Photon Servers memory and plugin instance is unloaded.
Fail only logs failure.
Cancel N/A
  • Notes:
  • 在處理ICloseGameCallInfo之前,您可以選擇保存房間狀態或永遠失去它。 在webhooks中,這可以在房間裡仍然有至少一個不活動的actor時進行。

進階概念

Actors 生命周期

Peer <-> Actor <-> Room

Actor是一個房間裡的玩家。 一旦一個玩家進入一個房間,無論是通過創建還是加入,他/她將被一個actor所代表。 Actor首先由其ActorNr定義,然後使用UserIdNickName。 它也可以有自定義的屬性。 如果玩家第一次進入房間,他/她將在該房間內獲得一個actor號碼,其他玩家無法獲得。 同時,對於每個新的玩家,一個actor將被添加到房間的ActorsList中。

如果一個玩家永遠離開了這個房間,相應的actor將從該列表中刪除。 然而,如果房間的選項允許,玩家可以離開房間,然後再回來。 在這種情況下,當玩家離開時,相應的actor將被標記為不活動。 事件的時間戳將被保存在DeactivationTimeactor屬性中。

您可以限制actor在房間裡保持不活動的時間。 這個持續時間可以在創建房間時通過設置PlayerTTL選項來定義,需要的數值是毫秒。 如果該值為負值或等於最大int值,actor可以無限期地保持不活動。 否則,一旦PlayerTTL在他們的DeactivationTime之後,不活動的actor將被從房間中移除。 在這之前,玩家可以重新加入房間。 如果他/她決定再次暫時離開房間,一個新的DeactivationTime將被計算,倒計時將被重置。 所以對重新加入的次數沒有限制。

Photon插件SDK提供了一種方法,可以在任何給定的時刻使用以下屬性之一獲得所有actor。

  • IPluginHost.GameActors包含一個房間內的所有actor(活躍和不活躍)。
  • IPluginHost.GameActorsActive包含當前加入房間的所有actor。
  • IPluginHost.GameActorsInActive包含所有離開房間(沒有放棄)的actor。

從插件中發送事件

您可以使用Photon插件SDK在房間內發送自定義事件。 自定義事件的類型和內容應該由其代碼來定義。 您的事件代碼應該保持在200以下。

這方面有兩個重載方法。雖然名稱 "BroadcastEvent " 表明事件將被廣播,但您可以根據過濾器做一個多播,甚至發送至單個actor:

  • 發送給一組Actor:

C#

void IPluginHost.BroadcastEvent(byte target, int senderActor, byte targetGroup, byte evCode, Dictionary<byte, object> data, byte cacheOp, SendParameters sendParameters = null);

您可以將target參數設置為以下值:

  • 0 (ReciverGroup.All):所有活動的actor。 targetGroup參數被忽略。
  • 1 (ReciverGroup.Others):所有活動的actor,除了actor號等於senderActor的actor。 targetGroup參數被忽略。 如果senderActor等於0,行為等同於target被設置為0ReciverGroup.All)。
  • 2 (ReciverGroup.Group):只有訂閱了用targetGroup參數指定的組團活動actor。
The ReciverGroup enum and values should not be confused with the ReceiverGroup enum and values in Photon's C# client SDKs including PUN.
  • 使用actor編號發送至特定的actor列表:

C#

void IPluginHost.BroadcastEvent(IList<int> recieverActors, int senderActor, byte evCode, Dictionary<byte, object> data, byte cacheOp, SendParameters sendParameters = null);

可以使用這兩種方法來更新房間的事件緩存。 您可以使用cacheOp參數定義緩存選項。 雖然IPluginHost.Broadcastevent方法不支持所有的緩存操作。 所有高於6的cacheOp'值不被接受,BroadcastEvent'調用將失敗。 一般來說,使用IPluginHost.ExecuteCacheOperation方法來與房間的事件緩存進行交互。 閱讀更多關於 "Photon事件緩存"

由於Photon事件需要一個actor號碼作為事件的來源(發送者),您有兩個選擇。

  • 冒充actor:將 "senderActor "參數設置為加入房間的actor的編號(必須是一個活躍的actor)。
  • 發送一個 "授權的 "或 "全局的 "房間事件:將senderActor參數設置為0。 由於actor編號0從未被分配給一個玩家。 所以它可以用來表示事件起源不是一個客戶端。

我們推薦您選擇從PluginBase中擴展您的插件類,您可能想使用下面的輔助方法來向所有加入房間的actor廣播事件:

C#

protected void BroadcastEvent(byte code, Dictionary<byte, object> data)
In order to be able to retrieve event data sent from plugins and the sender actor number from clients without changing client code, send the data in the expected events structure as follows new Dictionary<byte, object>(){ { 245, eventData },{ 254,senderActorNr } } instead of (Dictionary<byte, object>)eventData. You could use one of the following helper or wrapper methods:

C#

public void RaiseEvent(byte eventCode, object eventData,
    byte receiverGroup = ReciverGroup.All,
    int senderActorNumber = 0,
    byte cachingOption = CacheOperations.DoNotCache,
    byte interestGroup = 0,
    SendParameters sendParams = default(SendParameters))
{
    Dictionary<byte, object> parameters = new Dictionary<byte, object>();
    parameters.Add(245, eventData);
    parameters.Add(254, senderActorNumber);
    PluginHost.BroadcastEvent(receiverGroup, senderActorNumber, interestGroup, eventCode, parameters, cachingOption, sendParams);
}

public void RaiseEvent(byte eventCode, object eventData, IList<int> targetActorsNumbers,
    int senderActorNumber = 0,
    byte cachingOption = CacheOperations.DoNotCache,
    SendParameters sendParams = default(SendParameters))
{
    Dictionary<byte, object> parameters = new Dictionary<byte, object>();
    parameters.Add(245, eventData);
    parameters.Add(254, senderActorNumber);
    PluginHost.BroadcastEvent(targetActorsNumbers, senderActorNumber, eventCode, parameters, cachingOption, sendParams);
}
```</div>

### 外向HTTP調用

`HttpRequest` 是一個用於構造HTTP請求的輔助類。
使用此類,您可以設置URL和HTTP方法(默認為 "GET"),所需的`Accept`和`ContentType`標頭。
這些屬性的值應該被[HttpWebRequest](https://msdn.microsoft.com/en-us/library/system.net.httpwebrequest(v=vs.110).aspx)所支持。
此外,您可以將其他自定義HTTP標頭指定為一個`IDictionary<string, string>`並將其分配給`HttpRequest.CustomHeaders`。
您也可以通過先將請求數據轉換為`MemoryStream`對象,然后將其賦值給`HttpRequest.DataStream`來添加請求數據。
參見[Post JSON](#json)的例子,了解更多關於如何執行。

`HttpRequest`類也應該持有對響應回調的引用,它應該有以下簽名:
`public delegate void HttpRequestCallback(IHttpResponse response, object userState)`。

一旦請求對象被設置好,您可以通過調用`IPluginHost.HttpRequest(HttpRequest request, ICallInfo info)`來發送它。

<div class="alert alert-info">
What's worth noting, is that there are two properties that are exclusive to Photon and important in the plugins logic:  
<ul>
<li> <code>Async</code>: a flag that indicates if the normal processing of the room logic should be interrupted until the response is received or not.
This should be set to false if the room logic depends on the HTTP response.
<li> <code>UserState</code>: an object that will be saved by Photon Server for each request and sent back in the response's callback.
</ul>
</div>

`HttpRequest`類也應該持有對響應回調的引用,它應該有以下簽名:
`public delegate void HttpRequestCallback(IHttpResponse response, object userState)`.

一旦請求對象被設置完成,您可以通過調用 `IPluginHost.HttpRequest(HttpRequest request, ICallInfo info)`來發送它。

<div class="alert alert-info">
If you send a HTTP Request in synchronous mode, it does not make sense to process the ICallInfo object right away without waiting for the HTTP response.
You would be blocking the room fiber for nothing.
We recommend that you process the plugin call only until the response callback is triggered.
</div>

#### 非同步HTTP請求

您可以延遲處理一個掛鉤,直到收到一個HTTP響應:
```csharp
HttpRequest request = new HttpRequest()
{
  Callback = OnHttpResponse,
  Url = yourCustomUrl,
  Async = true,
  UserObject = yourOptionalUserObject
};
PluginHost.HttpRequest(request, info);

當收到響應時,如果需要,您應該決定如何處理ICallInfo

C#

private void OnHttpResponse(IHttpResponse response, object userState)
{
  ICallInfo info = response.CallInfo;
  ProcessCallInfoIfNeeded(info);

處理HTTP響應

在響應回調中,首先要檢查的是IHttpResponse.Status。 它可以採取以下HttpRequestQueueResult值之一。

  • Success (0):端點返回一個成功的HTTP狀態代碼。(即2xx代碼)。
  • RequestTimeout (1):該端點沒有及時返回響應。
  • QueueTimeout (2):請求在HttpRequestQueue中超時。 當一個請求排隊時,計時器就開始了。當之前的查詢花費了太多時間時,它就會超時。
  • Offline (3):應用程序各自的HttpRequestQueue處於離線模式。 在10秒內不應該進行HttpRequest,這是HttpRequestQueue重新連接的時間。
  • QueueFull (4)HttpRequestQueue已經達到了各自應用程序的某個閾值。
  • Error (5):請求的URL不能被解析,或者主機名不能被解析,或者端點無法到達。 如果端點返回一個錯誤的HTTP狀態代碼,也可能發生這種情況。(例如:400:BAD REQUEST)

如果結果不是 Success (0),您可以通過以下屬性獲得更多關於出錯的細節:

  • Reason: 錯誤的可讀形式。 當IHttpResponse.Status等於HttpRequestQueueResult.Error時很有用。
  • WebStatus:包含WebExceptionStatus的代碼。 這表明任何最終的WebException
  • HttpCode:包含返回的HTTP狀態代碼。

下面是一個關於如何在代碼中執行的例子:

C#

private void OnHttpResponse(IHttpResponse response, object userState)
{
  switch(response.Status)
    {
    case HttpRequestQueueResult.Success:
      // on success logic
      break;
    case HttpRequestQueueResult.Error:
      if (response.HttpCode <= 0)
      {
        PluginHost.BroadcastErrorInfoEvent(
          string.Format("Error on web service level: WebExceptionStatus={0} Reason={1}",
          (WebExceptionStatus)response.WebStatus, response.Reason));
      }
      else
      {
        PluginHost.BroadcastErrorInfoEvent(
          string.Format("Error on endpoint level: HttpCode={0} Reason={1}",
          response.HttpCode, response.Reason));
      }
      break;
    default:
      PluginHost.BroadcastErrorInfoEvent(
        string.Format("Error on HttpQueue level: {0}", response.Status));
      break;
  }
}

下面是一個關於如何在代碼中執行的例子:

為了方便使用,Photon插件SDK提供了兩種從HTTP響應中獲取數據的方法。 在執行IHttpResponse的類中暴露了兩個屬性。

  • ResponseData: 響應體的字節數。如果收到的數據不是文本的,它就會很有用。
  • ResponseText: 響應體的UTF8字符串版本。如果收到的數據是文本的,它可能很有用。

響應類也持有對相應的HttpRequest的引用,以防以後需要。 它可以在IHttpResponse.Request中使用。

計時器

計時器是可以設置的對象,其目的是在特定的時間段後調用一個方法。 一旦計時器被創建,倒計時就會自動開始。 它是安排或推遲插件執行代碼的最佳方式。

Photon插件SDK根據不同的使用情況提供兩種不同的計時器:

一次性計時器

一次性計時器是指在一定時間內觸發一個方法。 為了創建這樣的計時器,您需要使用以下方法: object CreateOneTimeTimer(ICallInfo callInfo, Action callback, int dueTimeMs);
您不需要停止這種計時器,除非您想在預定動作發生之前取消它。 若是如此,您應該使用void IPluginHost.StopTimer(object Timer)

Example: delaying SetProperties

C#

public override void BeforeSetProperties(IBeforeSetPropertiesCallInfo info)
{
  PluginHost.CreateOneTimeTimer(
                info,
                () => info.Continue(),
                1000);
}

重復計時器

一個重復的計時器會定期調用一個方法。 您可以定義第一個回調的執行時間和下面連續執行的時間間隔。 為了創建這樣的計時器,您需要使用以下方法: 對象CreateTimer(Action callback, int dueTimeMs, int intervalMs); 這種計時器將一直調用相應的方法,只要它在運行,並且插件在加載(房間沒有關閉)。 它可以在任何時候使用void IPluginHost.StopTimer(object Timer)來停止。

Example: scheduled events

C#

private object timer;
public override void OnCreateGame(ICreateGameCallInfo info)
{
  info.Continue();
  timer = PluginHost.CreateTimer(
        ScheduledEvent,
        1000,
        2000);
}
private void ScheduledEvent()
{
  BroadcastEvent(1, new Dictionary<byte, string>() { { 245, "Time is up" } });
}
public override void BeforeCloseGame(IBeforeCloseGameCallInfo info)
{
  PluginHost.StopTimer(timer);
  info.Continue();
}

自定義類型

如果您希望Photon支持您的自定義類的序列化,那麼您需要注冊它們的類型。 您需要為每個類型手動分配一個代碼(字節),並提供該類的字段和屬性的序列化和序列化方法。 注冊新類型的代碼也應該用在客戶端。 然後為了完成注冊,您需要調用以下方法:

C#

bool IPluginHost.TryRegisterType(Type type, byte typeCode, Func<object, byte[]> serializeFunction, Func<byte[], object> deserializeFunction);

Example: Registering CustomPluginType

下面是自定義類型注冊的例子:

C#

class CustomPluginType
{
  public int intField;
  public byte byteField;
  public string stringField;
}
The registration of custom types should be done by both ends. Meaning the Photon client should also register the custom type with the same code and serialization methods.

序列化方法應該將自定義類型的對象轉換成字節數組。 請注意,您應該先將該對象轉換為預期的類型(CustomPluginType)。

C#

private byte[] SerializeCustomPluginType(object o)
{
  CustomPluginType customObject = o as CustomPluginType;
  if (customObject == null) { return null; }
  using (var s = new MemoryStream())
    {
    using (var bw = new BinaryWriter(s))
    {
      bw.Write(customObject.intField);
      bw.Write(customObject.byteField);
      bw.Write(customObject.stringField);
      return s.ToArray();
    }
  }
}

解除方法應做反向執行。 它將自定義類型的對象從字節數組中構造回來。

C#

private object DeserializeCustomPluginType(byte[] bytes)
{
  CustomPluginType customObject = new CustomPluginType();
  using (var s = new MemoryStream(bytes))
    {
    using (var br = new BinaryReader(s))
    {
      customObject.intField = br.ReadInt32();
      customObject.byteField = br.ReadByte();
      customObject.stringField = br.ReadString();
    }
  }
  return customobject;
}

最後,我們需要注冊CustomPluginType。 一旦插件在SetupInstance中被初始化,我們就可以這樣做:

C#

public override bool SetupInstance(IPluginHost host, Dictionary<string, string> config, out string errorMsg)
{
  host.TryRegisterType(typeof(CustomPluginType), 1,
    SerializeCustomPluginType,
    DeserializeCustomPluginType);
  return base.SetupInstance(host, config, out errorMsg);
}

記錄

為每個插件實例創建一個新的IPluginLogger對象,用它來記錄插件的一切:

C#

public const string PluginName = "MyPlugin";

private IPluginLogger pluginLogger;

public override bool SetupInstance(IPluginHost host, Dictionary<string, string> config, out string errorMsg)
{
    pluginLogger = host.CreateLogger(PluginName);
    // ...
}

// ...

this.pluginLogger.LogDebug("debug");
this.pluginLogger.LogWarning("warning");
this.pluginLogger.LogError("error");
this.pluginLogger.LogFatal("fatal");

當設置日志記錄器的名稱(傳遞給IPluginHost.CreateLogger(loggerName))時,我們在日志記錄器的名稱前面加上Plugin.。 例如,如果您設置MyPlugin.MyClass,它將被記錄為Plugin.MyPlugin.MyClass。 上面的代碼片段將在日誌級別被設置為最大級別的情況下生成這些日誌條目:

Unknown

2020-01-31 17:10:07,394 [1] DEBUG  Plugin.MyPLugin - debug
2020-01-31 17:10:07,901 [1] WARN  Plugin.MyPLugin - warning
2020-01-31 17:10:08,152 [1] ERROR  Plugin.MyPLugin - error
2020-01-31 17:10:08,724 [1] FATAL  Plugin.MyPLugin - fatal

企業雲端日誌配置

由於不允許訪問我們伺服器上的日誌文件,所以應該使用外部服務來獲取日誌或警報。 我們推薦使用LogentriesPapertrail。 所以我們要求我們的企業客戶與我們聯系,用他們選擇的日誌服務配置他們的私有雲端。 如果您想使用Logentries提供配置的日誌令牌。 如果您想使用Papertrail提供您的自定義URL與端口

自有伺服器的日誌配置

默認情況下,日誌輸出可以在 "GSGame.log "文件中與GameServer日誌條目一起被檢索。 要使用單獨的日誌文件,請在GameServer的log4net配置文件("log4net.config")中添加以下片段:

XML

<!-- "plugin" log file appender -->
<appender name="PluginLogFileAppender" type="log4net.Appender.RollingFileAppender">
    <file type="log4net.Util.PatternString" value="%property{Photon:ApplicationLogPath}\\Plugins.log" />
    <param name="AppendToFile" value="true" />
    <param name="MaxSizeRollBackups" value="20" />
    <param name="MaximumFileSize" value="10MB" />
    <param name="RollingStyle" value="Size" />
    <param name="LockingModel" type="log4net.Appender.FileAppender+MinimalLock" />
    <layout type="log4net.Layout.PatternLayout">
        <param name="ConversionPattern" value="%d [%t] %-5p %c - %m%n" />
    </layout>
</appender>
<!-- CUSTOM PLUGINS:  -->
<logger name="Photon.Hive.HiveGame.HiveHostGame.Plugin" additivity="false">
    <level value="DEBUG" />
    <appender-ref ref="PluginLogFileAppender" />
</logger>

企業雲端中的插件版本管理

Currently Photon Plugins support only side-by-side assembly versioning: one plugin DLL version per AppId.

Here are two methods we recommend for rolling out new plugins versions:

A. "Compatible" plugins deploy: does not require new client version

  1. Upload new version of plugins assembly.
  2. On a staging AppId: test to verify that new version works as expected. (recommended)
  3. Update production AppId configuration to use new plugins assembly version.

B. "Incompatible" plugins deploy: requires new client version

  1. Upload new version of plugins assembly.
  2. Setup a new production AppId.
  3. Configure the new production AppId to use new plugins assembly version.

PUN特定的插件

如果您使用PUN作為一個客戶端SDK,並想執行一個與之配合的伺服器端插件,您應該知道以下幾點:

  • PUN注冊了一些額外的自定義類型,主要是Unity基本類。 如果您想從插件中處理這些,您也需從插件中注冊同樣的自定義類型。 您可以在PUN包內的 "CustomTypes.cs "類中找到所有這些自定義類型和如何注冊它們。 如果不注冊一些自定義類型,可能會導致錯誤或意外的行為。 您應該執行IGamePlugin.OnUnknownTypeIGamePlugin.ReportError以獲得此類問題的通知。

  • 所有PUN的進階和獨特的功能都在內部使用RaiseEvent。 每個功能都使用一個或多個事件代碼和特殊的事件數據結構。 要獲得PUN保留的事件列表,請看PUN包內 "PunClasses.cs "文件中的 "PunEvent "類。 例如,為了攔截OnSerializeView的調用,您需要執行 "OnRaiseEvent "回調並捕捉相應類型的事件代碼,在此例子中是 "SendSerialize = 201"。 要知道每個事件的預期內容,看看它的事件數據是如何在PUN的代碼中構建的,或者從插件內部的傳入事件中檢查它。

  • 要在Photon插件中獲得PUN的PhotonNetwork.ServerTimestamp,請使用Environment.TickCount

AuthCookie

也稱為安全數據,是由設置為認証提供者的網絡服務返回的可選JSON對象。 這個對象將不能從客戶端訪問。 更多信息請訪問自定義認証文檔頁面

從插件中,您可以按以下方式訪問AuthCookie。

  • ICallInfo.AuthCookie:獲取觸發掛鉤的當前角色的AuthCookie。 然而,在OnBeforeCloseGameOnCloseGame中,IBeforeCloseGameCallInfo.AuthCookieICloseGameCallInfo.AuthCookie分別不會有任何值,因為這些是在用戶之外觸發的。 例如:

    C#

    public void OnCreateGame(ICreateGameCallInfo info)
    {
        Dictionary<string, object> authCookie = info.AuthCookie;
    
  • IActor.Secure: 獲取任何活動actor的AuthCookie。 例如:

    C#

    foreach (var actor in this.PluginHost.GameActorsActive)
    {
        var authCookie = actor.Secure as Dictionary<string, object>;
    }
    
  • 使用void IActor.UpdateSecure(string key, object value)更新每個角色的安全認証cookie。

Back to top