This document is about: SERVER 5
SWITCH TO

このページは編集中です。更新が保留になっている可能性があります。

Photonプラグイン FAQ

Photonプラグインは、Enterprise CloudまたはセルフホスティングされたPhoton Server v4でのみ利用可能です。

設定

Photonは複数プラグインに対応していますか?

一つのアプリケーションには一度に一つのプラグインアセンブリ(DLL)またはプラグインファクトリしか設定できません。
このDLLでは、設定できるプラグインに上限はありません。
ルーム作成時には1つのプラグインのみが読み込まれ、インスタンス化されます。
ルームとプラグインのインスタンスには1対1の関係があります。したがって、各ルームにはの独自のプラグインのインスタンスがあります。

以下の設定は許可されていません:

<PluginSettings Enabled="true">
    <Plugins>
        <Plugin …/>
        <Plugin …/>
    </Plugins>
</PluginSettings>

ルームを作成時に使用するプラグインの選択方法は?

プラグインモデルはファクトリパターンを使用しています。
プラグインは、オンデマンドで名前でインスタンス化されます。

クライアントはroomOptions.Pluginsを使用してプラグインの設定をリクエストし、ルームを作成します。
roomOptions.Pluginsstring[]型で、最初の文字列(roomOptions.Plugins[0])はファクトリに渡されるプラグイン名です。

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

クライアントが何も送信しない場合、サーバーはデフォルト(何も設定されていない場合)を使用します。設定されている場合は、プラグインファクトリが作成時に返すものを使用します。

以下のように、ファクトリでは名前を使用して対応するプラグインを読み込みます。

C#

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で返されたプラグインの名前がクライアントがリクエストしたものと一致しない場合、
プラグインはアンロードされ、クライアントのcreateまたはjoin操作は失敗し、「PluginMismatch(32757)」エラーが発生します。

実行時にディスクからファイルを読み取ろうとしています。プラグインはファイルシステムにアクセスできますか?外部サーバーからファイルをダウンロードする必要がありますか?

プラグインに必要なファイルや、他のファイルをアップロードできます。
ファイルへのパスは typeof(yourplugin).Assembly.Locationで取得できます。

依存モジュール(DLL)は、メインプラグインDLLと共に自動的に読み込まれますか? System.TypeLoadExceptionが発生するのはなぜですか?

外部モジュールは、プラグインソリューションでDLLやプロジェクトを参照することで動作します。
参照するすべての依存モジュールを読み込み、リンクできるようにするには、プラグインDLLと同じディレクトリにデプロイする必要があります。

コールバック

プレイヤーがルームに入ろうとする際、どのようなメソッドが呼び出されますか?

まず、作成、入室、再入室によりプレイヤーがルームに入る際に4つのシナリオが存在することを理解する必要があります。

基本的に、JoinRoom操作とCreateRoom操作の構造は非常に似ています。
異なるJoinMode値を持つ1つの論理的なJoin操作と考えると良いかも知れません。

  1. Create: OpCreateRoom:ルームが既に存在する場合はエラーが返されます。
  2. Join: OpJoinRoom JoinMode.Defaultまたは未設定)、 OpJoinRandomRoom:ルームが存在しない場合はエラーが返されます。
  3. CreateIfNotExists: OpJoinRoom (JoinMode.CreateIfNotExist):ルームが存在しない場合は作成します。
  4. RejoinOnly: OpJoinRoom (JoinMode.RejoinOnly): アクターがまだルームに存在しない場合はエラーが発生します。

ルームがメモリ内にある場合、2)〜4)はBeforeJoinをトリガーし、{ICallInfo}.Continue()を呼び出すと、 OnJoin が呼び出されます。

OnCreateGameは、プラグインがセットアップされ、アクターがルームに追加される直前に、ルームの作成直後に呼び出されます。
これは、1)、3)、および4)によってトリガーすることができます。
後者は、状態の保存と読み込みが適切に処理されている場合にのみ発生します。
これは、プレイヤーがサーバーのメモリから削除されたルームへの再参加をリクエストし​​たときに発生します。
Photonは、それでも作成を行いプラグインを設定します。
プラグインは、データベースまたは外部サービスから直列化された状態を取得し、SetSerializedGameStateを呼び出します。
状態には、インアクティブ状態にある全てのアクターのリストが含まれます。
再入室により、再入室するアクターは再アクティブ化されます。

プラグインコールバックでアクター番号を取得する方法は?

ActorNrは以下のようにプラグインフックで取得されます。

1. OnCreateGame, info.IsJoin == false:

ActorNr = 1: ゲームの作成者でもある最初のアクター。アクター番号は常に1に設定されます。

2. OnCreateGame, info.IsJoin == true:
a. before info.Continue();

インアクティブなアクターのリストからUserIdでActorNrを取得します。ゲームの状態を読み込むことでルームが「再作成」される場合は、
読み込まれたルームの状態からActorListをループし、UserIdと比較して、ゲームの状態でアクター番号を見つける必要があります。
(UserIdが利用できない場合があります。その場合、再入室は失敗します。その場合は、CheckUserOnJoinとアクターごとの一意のUserIdでルームを作成する必要があります。)

C#

// 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でActorNrを取得します。

C#

int actorNr;
foreach (var actor in PluginHost.GameActorsActive)
{
    if (actor.UserId == info.UserId)
    {
        actorNr = actor.ActorNr;
        break;
    }
}
3. BeforeJoin
a. before info.Continue();

C#

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でActorNrを取得します。

C#

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フローにフックすることです。

最初は、必ずしも何かを呼び出す必要はありませんでした。これは、本質的に、受信したリクエストのデフォルト処理をキャンセルすることと同じでした。
そのため、開発者は、デフォルトのコールバックロジックを使用せずに、一から必要なものを実装する必要がありました。
これにより予期しない問題が発生していました。開発者が必ずしもキャンセルを望んでいたとは限りません。
そこで、strictモードを導入し、開発者による決定を求めるようにしました。
現在はContinueFail、またはCancelを1度のみ呼び出すようになりました(いずれかひとつ)。

PluginBaseのコールバックメソッドをオーバーライドする場合、base.XXX() を呼び出す必要がありますか?

PluginBase内のすべてのコールバックメソッドには、最後に Continue()の呼び出しが含まれます。
PluginBaseから継承することで、インタレストのあるメソッドのみをオーバーライドできます。
base.XXX()の前後どちらかにコードを追加するだけでよい場合もあれば、デフォルトの動作を完全に変更しないといけない場合もあります。
最初のケースでは、 ICallInfo処理メソッドへの呼び出しを追加するべきではありません。
2つ目のケースでは、base.XXX()を呼び出さず、一からメソッドの独自の実装を行い、利用可能なICallInfo処理メソッドの1つに呼び出しを追加します。
ただし、例外があります。それはPluginBase.OnLeaveです。
このメソッドは、現在のものが離れる場合に備えてMasterClientの変更を処理します。

なぜOnLeaveILeaveGameCallInfo.ActorNr == -1があるのでしょうか?

クライアントがルームへの入室に失敗した場合に発生します(createまたはjoin)。
基本的にILeaveGameCallInfo.ActorNr == -1は、そもそもルームにクライアントが追加されず、アクラ―番号が付与されなかったことを意味します。
OnLeave(ILeaveGameCallInfo.ActorNr == -1)OnCreateGameまたはBeforeJoin内でのinfo.Continue()呼び出しの結果です。
OnLeaveがトリガーされると、サーバーは各クライアントのルーム内のアクターを見つけることができません。そのため、デフォルトのActorNr値が-1になっています("actor not found(アクターが見つかりません)"と読めます)。
ここではILeaveGameCallInfo.Reasonは、常に3となります: LeaveReason.ServerDisconnect.

以下はILeaveGameCallInfo.ActorNr == -1を検知しログするサンプルプラグインです:

C#

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));
            }
        }
    }
}

join失敗の事例(Create roomまたはJoin room操作についてはクライアントエラーコードを参照してください):

  • 同じUserIDを持つ無効なアクターがいない状態でルームに再入室を試みる。
    クライアントはエラーコードJoinFailedWithRejoinerNotFound = 32748を受け取る。
  • 同じUserIDを持つ無効なアクターがいない状態でルームに入室を試みる。
    クライアントはエラーコードJoinFailedFoundActiveJoiner = 32746を受け取る。
  • 同じUserIDを持つ無効なアクターがいる状態でルームに入室を試みる。
    クライアントはエラーコードJoinFailedFoundInactiveJoiner = 32749を受け取る。

ILeaveGameCallInfo.ActorNr == -1がjoin失敗の結果ではない場合、これは変則となり、カスタムユーザープラグインコードもしくは内部のサーバーコードに例外があります。
どちらにしても、プラグイン側から早い段階で見つけてスタックトレースを取得できます。
また、ILeaveGameCallInfo.Continueの前後でILeaveGameCallInfo.ActorNrは変更しますが、この検知と報告は依然として行うことができます。
コード内の例外はReportErrorメソッドの実装や、PluginBaseから拡張している場合はオーバーライドで取得できます。

イベント

HttpForwardプロパティと WebFlagsクラスは何を行いますか?

これらはWebHooks関連の機能です。
WebFlagsはWebHooks v1.2プラグインで導入されました。WebFlagsの詳細はこちらを参照してください。
HttpForwardは、対応するwebflagの値を示すプロパティです。

元々はWebHooksとWebRPC用に作成されたものですが、プラグインで使用することもできます。

プラグインからイベントを送信する方法は?

この目的にはPluginHost.BroadcastEventを使用する必要があります。
クライアントからイベントを送信する場合と同じように
機能しますが、主な違いは、
サーバーを表すために送信者アクター番号を0に設定できることです。
詳細は、プラグインマニュアルの「プラグインからのイベントの送信」セクションを参照してください。

OnRaiseEventから行う場合とは異なり、 PluginHost.BroadcastEventOnJoinコールバック内で呼び出すとクライアントへのイベント送信に成功しません。なぜですか?

OnRaiseEventでは、クライアントはすでに参加しているため、イベントを受信できます。
OnJoinでは、リクエストが{ICallInfo}.Continue()を使用して処理しない限り、クライアントは完全には参加しません。

したがって、 {ICallInfo}.Continue()の後に PluginHost.BroadcastEventを呼び出すと、イベントはターゲットクライアントによって受信されます。

プラグインフックは次のように機能します:{ICallInfo}.Continue()の呼び出しでPhotonの通常の処理をトリガーします。
この場合、参加が完全に完了した後にイベントを送信する方が理にかなっています。

プラグインから送信されたイベントデータをクライアントが受信できないのはなぜですか?

これは既知の問題です。
クライアントは、イベントがあらかじめ定義された特定の構造と既知のキーコードを有することを想定しています。
イベントデータの場合は245、アクター番号の場合は254です。
これを修正するには、プラグインからイベントデータを送信する方法を少し変更するだけです。
(Dictionary<byte,object>)eventDataの代わりにnew Dictionary<byte,object>(){{245,eventData},{254,senderActorNr}}を送信します。

プラグインはカスタム操作に対応していますか?

いいえ、プラグインはカスタム操作に対応していません。
Photonプラグインは、一連の操作( "Create"、 "Join"、 "SetProperties"、 "RaiseEvent"および "Leave")に対してのみコールバックを提供しています。
セルフホスティングされたPhoton Serverに追加したカスタム操作をインターセプトしたり、プラグインSDKを使用して新しい操作を拡張することはできません。

ただし、2方向のイベントを交換することで同じ結果を得ることができます。

  • LoadBalancingClient.OpRaiseEventを呼び出してクライアントからプラグインへ。
  • PluginHost.BroadcastEventを呼び出して、プラグインからクライアントへ。

ゲームの状態

「アクティブ」ユーザーと「インアクティブ」ユーザーの違いは何ですか?

通常、プレイヤーが切断されると、Photonはクリーンアップされ、アクターは削除されますが、ルームを作成するときにクライアントのCreateOptionsPlayerTTL を定義できます。
厳密な整数の場合、Photonはその時間(ミリ秒単位で定義)待機してからクリーンアップします。
その間、アクターはインアクティブと見なされ、ゲームに再び参加できます。成功すると、プレイヤーは
再びアクティブになります。

接続の不良によりプレイヤーが切断されても短い時間内(数分)であれば戻ることのできるようなRTSゲームなどの場合に、ゲームの状態を保存してプレイヤーを継続させるために、この機能を使用することができます。

PluginHost.GameActorsActiveにはルーム内のすべてのアクター(参加)が含まれ、PluginHost.GameActorsInActiveにはルームを離れた(放棄せずに)すべてのアクターが含まれます。

プラグインを利用して、アクターをルームから退室させることはできますか?その方法は?

はい、可能です。 プラグインクラスから、PluginHost.RemoveActor(int actorNr, string reasonDetails)を呼び出します。
次の3つのパラメーターを受け入れるオーバーロードメソッドを呼び出すことで、理由を設定できます:
PluginHost.RemoveActor(int actorNr, byte reason, string reasonDetails).

ルームの状態を維持する方法は?

ルームの状態を保存するには:

  1. PluginHost.GetGameStateを呼び出してSerializableGamestateを取得します。
  2. 状態を直列化します(たとえば、JSON)。
  3. 状態をデータストアに保存します。

ルームの状態を読み込むには:

  1. データストアから状態を取得します。
  2. 状態を非シリアライズ化します。
  3. PluginHost.SetGameStateを呼び出します。

注意: PluginHost.SetGameStateを呼び出すことが許可されているのはOnCreateGameで、{ICallInfo}.Continue()を呼び出す前のみです。

SerializableGameStateの中に無いカスタムルームプロパティがあります。ロビーのプロパティのみなのはなぜですか?

直列化可能なゲームの状態では、設計によりすべてのカスタムプロパティへのアクセスは提供していません。
ロビーと共有しているもののみが公開され、「観覧」のためだけです。
組み合わせたすべてのプロパティは、バイナリ配列に含まれます。
これにより、JSONに直列化することができ、逆直列化の際に型情報が失われることを防ぎます。
この機能は、主に保存/読み込み用に設計されたものです。
この動作は今後変更される可能性があります。

プラグインからルームプロパティ( MaxPlayers, IsVisible, IsOpenなど)にアクセスする方法は?

全てのルームプロパティには、 Hashtableタイプの PluginBase.PluginHost.GamePropertiesからアクセスできます。
これらのプロパティには、「ネイティブ」または「よく知られている」プロパティが含まれます。
これらは Photon.Hive.Plugin.GameParameters に記載されています。

PluginHost.GamePropertiesには、カスタムルームプロパティも含まれます。
キー/値が含まれ、ユーザーが処理する必要があります。

一方、ロビーに表示されるカスタムプロパティのみが、Dictionary<string, object>である PluginBase.PluginHost.CustomGamePropertiesに保存されます。
このプロパティは readonly として扱われるべきです。

以下のように、プラグインからルームとアクターのプロパティにアクセス(読み込みおよび書き込み)することができます:

プロパティを取得するためのいくつかのヘルパーメソッド。

C#

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;
}

書き方の例:*

C#

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 Serverでいくつのスレッドが実行されていますか?

スレッドの利用は以下のように分けられます:

  1. ネイティブ - 9個のスレッド
  2. 管理 - .NETデフォルト設定を使用する.NET ThreadPoolにもとづく。

この設定には非常に厳密にテストをおこなってきましたので、広範な負荷プロファイルに対して問題なく動作します。

管理スレッドの使用は、状況によって異なります(.NET Windows Performanceカウンターに記載されたとおりです):

a) 1個~12個まで:通常のPhoton Cloud Realtime負荷に対応
b) 35個以上:プラグイン間の通信(ロッキング)をともなうプラグインを実行するカスタマークラウドが例で、より高いコンテンションの原因となります(弊社のコードとは対照的です)。

備考:必要に応じて.NET ThreadPool設定は調整できます。
これまでデフォルトの設定で適切な成果が得られていますが、各バージョンに応じて調整が必要な可能性があります。

Photonのホストはフリースレッドですか?すべてのスレッドがいつでもすべてのルームにアクセスできますか?

メッセージパッシングアーキテクチャがあります:プラグインは一度に1つのスレッドにのみ呼び出されます。
ただし、スレッドプールを使用するため、各呼び出しのスレッドが同じだとは限りません。

プラグインを書き込み際、スレッドでの安全性の問題点はありますか?

基本的に、プラグインへのすべての呼び出しは直列化されます(実質的に1つのスレッド上で、必ずしも同じ物理スレッド上である必要はありません)。

Enterprise Cloud

Enterprise Cloudのランタイム環境に関する質問。

プラグインの設定方法は?

Photon Enterprise Cloudの場合:
Photonダッシュボードからアプリケーションの管理ページに移動して、新しいプラグインを追加します。
ページの下部にある[Create a new Plugin]ボタンをクリックします。
キー/値エントリを追加してプラグインを設定できます。
AssemblyNameVersionPath、およびTypeは必須です。

Photonプラグインを作成するパイプラインプロセスは何ですか?

以下の通り、Photonプラグインを作成するパイプラインプロセスは簡単です。

  1. 必要なSDKとサーバーバイナリをダウンロードします。
  2. プラグインアセンブリをコーディングしてビルドします。
  3. 展開してテストします。
  4. アップロードします。
  5. 設定します。

Photonプラグインの環境設定は次のとおりです:

  • 開発:ローカルマシン。
  • テスト:ローカルネットワーク内。
  • ステージング:クラウドで個別のAppId。
  • 本番:クラウド内の本番AppId。

プラグインのアップロードは自動化されていますか?

はい。Enterpriseのお客様がプライベートクラウドを管理できるように、PowerShellスクリプトを提供しています。
詳細は、プラグインアップロードオンラインガイド をご覧ください。

プラグインのパフォーマンスを監視する方法は?

ダッシュボードで利用できる複数のカウンターをトラッキングしています。
さらに、カスタムのカウンターを追加することも、カウンター用の外部ツールを統合することもできます (例: New Relic)
これらのサービスを使用する場合は、コンサルティング契約を手配する必要があります。

プラグインのログを取得する方法はありますか?

サーバー上のログファイルへのアクセスは許可されていません。
そのため、ログまたはアラートには外部サービスを使用する必要があります。
おすすめはLogentries またはPapertrailです。
Enterpriseのお客様はご相談ください。ご希望のロギングサービスをプライベートクラウドを設定します。
Logentries の使用をご希望の場合は設定されたログトークンをご連絡ください。
Papertrail の使用をご希望の場合はポートを含むカスタムURLをご連絡ください。

Logentriesトークンを取得するには?

Logentriesアカウントを作成して次の手順に従ってください:

  1. "Logs"/"Add New Log"を選択します。
  2. "Libraries"/".NET"を選択します。
  3. ログセットの名前を入力します。
  4. "Create Log Token"をクリックします。
  5. "Finish & View Log"をクリックします。
  6. 新しいログセットを選択し、"Setting"タブを選択します。 これでトークンを表示できます。
  7. メールで弊社にトークンを送ってください。

Papertrail URLを取得するには?

Papertrailアカウントを作成して次の手順に従ってください:

  1. "System"を追加します。
  2. 次のページの上部に、"Your logs will go to logs6.papertrailapp.com:12345 and appear in Events."というようなメッセージが表示されます。
  3. そのURLを弊社にメールで送ってください。

新しいプラグインバージョンをリリースする場合の推奨方法は何ですか?

現在、Photonのプラグインはサイド・バイ・サイドのアセンブリバージョニングのみをサポートしています:AppIDごとに1つのプラグインDLLバージョンです。

新しいプラグインバージョンを展開するには、以下の2つの方法を推奨します:

A. 「互換性のある」プラグインの展開:新たなクライアントバージョンは不要です。

  1. 新しいバージョンのプラグインアセンブリをアップロードします。
  2. AppIDをステージングする際:新たなバージョンが予期されたとおりに動作している点を確認してください(推奨)。
  3. 新たなプラグインアセンブリバージョンを使用するため、本番のAppID設定を更新します。

B. 「互換性のない」プラグインの展開:新たなクライアントバージョンが必要です。

  1. プラグインアセンブリの新たなバージョンをアップロードします。
  2. 新たな本番のAppIDを設定します。
  3. 新たなプラグインアセンブリバージョンを使用するため、新たな本番のAppIDを設定します。

利用可能なさらに高度な手法:

実際のゲームロジックは明示的に読み込まれる別のDLLのものですが、コアサーバー更新ループを持つプラグインDLLを構築できます。
ゲームロジックにはバージョンごとに複数のDLLが必要ですが、コアプラグインDLLは更新しないでください。
コアプラグインDLLは、クライアントバージョンに基づいて適切なゲームロジックDLLを読み込みます。
完全な互換性のためにサーバー側コードをクライアント側コードにマッピングするようなものです。
プラグインのバージョン互換性のある更新が可能になります。
これで、新しいプラグインバージョンがリリースされるときに、クライアントの更新を強制する必要がなくなります。
ゲームロジックDLLは、バージョンごとに別々のフォルダーに配置することも、同じフォルダー内にバージョンごとに異なる名前で配置することもできます。

プラグイン内で静的フィールドを使用できますか?

ルーム間やアプリケーション間で、同じプラグインが共有されます。
同じプラグインクラスの静的フィールドも、同様に共有されます。
静的フィールドの使用を回避できない場合、2つのアプリケーションで同じプラグインアセンブリの使用を回避するための方法は以下のとおりです:

  1. 異なる2つのプラグイン名の下に、同じプラグインファイルをアップロードします:

    a- 名前Xでプラグインアーカイブをアップロード
    b- 名前Yでプラグインアーカイブをアップロード

  2. 2つのアプリケーションに、「Path」以外は同じ設定を適用:

    a- プラグインXを使用するようアプリAを設定:"{customerName}\X"
    b- プラグインYを使用するようアプリBを設定:"{customerName}\X"

その他

プラグインがRemoveActorを実行するときに指定されたreasonを取得する方法は?

現在、intまたはstringの理由はクライアントに送信されません。
カスタムイベントを使用して、切断する前にクライアントに通知できます。
たとえば、カスタムイベントを理由と一緒に送信し、タイマーを使用して200ms後にRemoveActorを設定できます。

これらのヘルパーメソッドを利用することができます。

C#

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);
}

クライアントSDKは、カスタム型を登録することにより、直列化の拡張に対応しています。サーバーでこれらの型を非直列化する方法は?

次のように、クライアントSDKと同じようにカスタム型を登録できます:

C#

PluginHost.TryRegisterType(type: typeof (CustomPluginType), typeCode: 1, serializeFunction: SerializeFunction, deserializeFunction: DeserializeFunction);

詳細は、プラグインマニュアルの "Custom Type" セクションをご覧ください。

プラグイン.DLLのファイルサイズに制限はありますか?

ありませんが、あまり大きくするべきではありません。

PhotonプラグインでPUNのPhotonNetwork.ServerTimestampを取得する方法は?

Environment.TickCountを使用します。

Back to top