パフォーマンスのヒント

パフォーマンスは、アプリケーションにマルチプレイヤー・コンポーネントを流動的かつシームレスに統合するために欠くことができない部分です。
そのため、ここではPhotonで開発する際に覚えておくべきヒントのリストをまとめました。

サービスを定期的に呼び出す

クライアントのライブラリは、アプリのロジックにトリガーされた場合にのみ任意のメッセージを送信するように構築されています。このようにして、クライアントは複数の操作を集約してネットワークのオーバーヘッドを回避することができます。

データの送信をトリガーするには、メインループで LoadBalancingClient.Service() または PhotonPeer.SendOutgoingCommands() を頻繁に呼び出す必要があります。まだキューに入っているデータがある場合、boolの戻り値はtrueです。その場合は、再度SendOutgoingCommandsを呼び出してください (ただし連続して3回まで)。

ServiceとSendOutgoingCommandsは、接続を維持するために重要な確認応答とpingも送信します。どちらも呼び出しの間の長い休止は避けた方が良いでしょう。特に、ロード中にもかかわらずServiceが呼び出されるようにしてください。

この問題は、見落とされると特定や再現が難しくなります。C#ライブラリにはConnectionHandlerがあり解決に役立ちます。

ローカルのラグを避けるには、ゲームループがネットワークのアップデートを書いた後にSendOutgoingCommandsを呼び出します。

アップデート vs トラフィック

1秒間ごとのアップデート数を増やすと、ゲームがより流動的かつ最新のものとなります。
一方で、トラフィックが劇的に増加する場合があります。また、ランダムなラグやロスを避けることはできないのでアップデートの受信者は常に重要な値を補間できるようにしなければなりません。

以下の点に留意してください。呼び出した操作の多くは他のプレイヤーにイベントを作成するため、1秒あたりのアップデートの送信回数が少ない方が処理が速い可能性があります。

トラフィックの最適化

トラフィックの問題を避けるには、送信量を減らします。
送信量を減らすには、いくつかの方法があります:

必要以上に送信しない

必要な分だけ通信してください。
関連する値のみを送信し、それらの値から出来る限り派生させてください。

状況に応じて、送信するものを最適化してください。
何を送信するべきか、また送信頻度を考慮するようにしましょう。
重要でないデータは同期によって強制的に再計算される場合を除き、同期されたデータ、またはゲームの進行内容にもとづいて受信側で再計算されるべきではありません。

例:

  • RTSでは、発生時に複数のユニット向けに「オーダー」を送信できます。
    これは1秒間に10回の頻度で各ユニットに位置、回転、速度を送信するよりもはるかに効率的です。

1500 archersを参照してください。

  • シューティングゲームでは、発射は位置と方向として送信してください。
    銃弾は通常、直線で飛びます。このため、100ミリ秒ごとにそれぞれの位置を送信する必要はありません。
    銃弾が何かに当たった場合、または銃弾が「非常に多くの」ユニットを通過した後には、銃弾をクリーンアップすることができます。

  • アニメーションは送信しないでください。通常は、プレイヤーの入力やアクションからすべてのアニメーションを派生することができます。
    アニメーションを送信すると遅延が発生する可能性が十分にあり、プレイが遅延すると非常に不自然な印象を与えます。

  • デルタ圧縮を使用してください。前回の送信から変更があった値のみを送信してください。
    受信側での値を平滑化するため、データ補間を使用してください。
    この方法は無理に同期をおこなうよりも望ましく、トラフィックを軽減します。

送信量を抑える

やり取りする型とデータ構造を最適化してください。

例:

  • 小さな整数の場合には、intではなくbyteを使用してください。可能であれば、floatではなくintを使用してください。
  • stringのやり取りを避け、なるべくenumやbyteを使用してください。
  • 送信されるものが明確でない限り、カスタムの型をやり取りしないでください。

静的なデータ、またはよりサイズの大きなデータをダウンロードするには、別のサービスを使用してください(例:マップ)。

Photonはコンテンツ配信システムとして構築されていません。
HTTPベースのコンテンツシステムを使用したほうがコストを抑えられ、管理も容易です。
最大転送単位(MTU)よりも大きなものはすべて分割され、複数の信頼性の高いパッケージとして送信されます(完全なメッセージへと再構成する必要があります)。

送信頻度を抑える

  • 送信レートを下げてください。できれば、10未満に下げることを推奨します。
    この設定は、当然のことながらゲームプレイに応じて異なります。
    この変更はトラフィックに大きく影響します。

ユーザーのアクティビティや、やり取りするデータにもとづいて、適応送信レートや動的送信レートを使用できます。この設定もトラフィックに大きく影響します。

  • 可能な場合には信頼性を低くして送信してください。
    新たなアップデートをすぐに送信する必要がある場合、通常は信頼性の低いメッセージを使用できます。
    信頼性の低いメッセージは、繰り返しを発生しません。
    例:FPSでは、通常プレイヤーの位置は信頼性を低くして送信されます。

データの作成と消費

「トラフィック」のトピックに関連した問題として、受信側の末端で消費可能な量のデータしか生成されない問題が挙げられます。
パフォーマンスやフレームレートが受信イベントに追い付いていけない場合、それらは実行される前に古くなってしまいます。

最悪のケースでは、一方の側が受信側の末端を壊すほど多くのデータを生成します。
開発中には、クライアントの待ち行列の長さに注意してください。

信頼性の低いコマンドの実行を制限

クライアントが受信メッセージをしばらくディスパッチしなくても(例:ローディング中に)、クライアントはすべての受信とバッファリングをおこないます。
他のプレイヤーのアクティビティーに応じて、クライアントは追いつくまでに膨大な処理をおこなう必要があります。

単純化するため、クライアントは信頼性の低いメッセージを特定の長さに自動的にカットします。
結果的に最新メッセージをより早く取得でき、アップデートされていない箇所は最新のメッセージにすぐに置換されます。

この制限はデフォルトが20の(PUN内も同じ)LoadbalancingPeer.LimitOfUnreliableCommands経由で設定されています。

データグラムのサイズ

データグラムのコンテンツのサイズは、1,200バイトに制限されています。

1,200バイトにはヘッダーからのすべてのオーバーヘッド(「バイナリプロトコル」を参照してください)、サイズおよび型の情報(「Photonでのシリアル化」を参照してください)が含まれます。これによって、実際の純粋なペイロード数は若干減少します。
データがどのように構築されたかによって異なりますが、純粋なペイロードデータが1KB未満の場合には単独のデータグラムにおさまると考えて問題ありません。

1,200バイトよりも大きなオペレーションやイベントは分割され、複数のコマンドで送信されます。
これらは自動的に信頼性が高くなり、受信側はすべてのフラグメントを受信後にこれらの大きなデータチャンクを再構成して、ディスパッチできます。

大きなデータ「ストリーム」は、ディスパッチされる前に多くのパッケージから再構成する必要があるため、レイテンシーに大きく影響します。
これらは個別のチャネルに送信可能なため、(低い)チャネル番号の「ライブ」位置アップデートには影響しません。

プールしたByteArraySliceで割当を減少

デフォルトで、C# SDKのPhotonクライアントはbyte[]およびArraySegment<byte>byte[]としてシリアル化します。
受信側では、このシリアル化により同じ長さの新しいbyte[]が割り当てられ、OnEventコールバックへパスされます。
ByteArraySliceはこれらのオプションに対するnon-alloc・non-boxingの代替案です。

ByteArraySliceは、再利用可能なクラスであることを除いてArraySegment<byte>によく似たbyte[]のラッパークラスです。
ByteArraySliceボクシングから割当を作成せずに、クラスとして(Photonメッセージすべてがキャストする先の)オブジェクトへのキャストになれます。

ByteArraySliceのフィールド・プロパティは以下の通りです。

  • Buffer: ラップされたbyte[]配列。
  • Offset: トランスポートが読み込む開始バイト。
  • Count: オフセットを越えて書き込まれたバイト数。

シリアライゼーション方法

2通りの方法があります。以下で説明します。

ByteArraySlicePoolから取得

C#

void Serialization()
{
    // Get a pooled Slice.
    var pool = loadBalancingClient.LoadBalancingPeer.ByteArraySlicePool;
    var slice = pool.Acquire(256);

    // Write your serialization to the byte[] Buffer.
    // Set Count to the number of bytes written.
    slice.Count = MySerialization(slice.Buffer);

    loadBalancingClient.OpRaiseEvent(MSG_ID, slice, opts, sendOpts);

    // The ByteArraySlice that was Acquired is automatically returned to the pool
    // inside of the OpRaiseEvent
}

ByteArraySliceを修正する

C#

private ByteArraySlice slice = new ByteArraySlice(new byte[1024]);

void Serialization()
{
    // Write your serialization to the byte[] Buffer.
    // Set Count to the number of bytes written.
    slice.Count = MySerialization(slice.Buffer);

    loadBalancingClient.OpRaiseEvent(MSG_ID, slice, opts, sendOpts);
}

非シリアライゼーション使用法

デフォルトで、byte[]データは new byte[x]に非シリアル化されます。
LoadBalancingPeer.UseByteArraySlicePoolForEvents = trueを設定してnon-allocコンジットを有効にする必要があります。
有効になると、受信オブジェクトはbyte[]ではなくByteArraySliceにキャストされるようになります。

C#

// By default byte arrays arrive as byte[]
// UseByateArraySlicePoolForEvents must be enabled to use this feature
private static void EnableByteArraySlicePooling()
{
    loadBalancingPeer.UseByteArraySlicePoolForEvents = true;
}

private void OnEvent(EventData photonEvent)
{
    // Rather than casting to byte[], we now cast to ByteArraySlice
    ByteArraySlice slice = photonEvent.CustomData as ByteArraySlice;

    // Read in the contents of the byte[] Buffer
    // Your custom deserialization code for byte[] will go here.
    Deserialize(slice.Buffer, slice.Count);

    // Be sure to release the slice back to the pool
    slice.Release();
}

EventDataの再利用

C#クライアントは、OnEvent(EventData ev)を介してイベントを受信します。デフォルトでは、各EventDataは新しいインスタンスであるため、ガベージコレクターに余分な作業が発生します。

多くの場合、EventDataを再利用することで容易にオーバーヘッドを回避できます。これは PhotonPeer.ReuseEventInstance設定で有効にできます。

Back to top