同期とステート

ゲームでは全てのプレイヤーを同期し、プレイヤー間で同じ状態を保つことが基本です。 他のプレイヤーが誰なのか、彼らが何をしているのか、どこにいるのか、そして彼らのゲームの世界がどのように見えているのかを把握する必要があります。

PUN(およびPhoton全般)では、アップデートと状態の維持のためにツールを数種類用意しています。 このページでは、そのツールのオプションと使うべきタイミングについて説明しています。

Contents

オブジェクトの同期

PUNを使用すると、簡単にGameObjectを「ネットワーク対応」させることができます。 PhotonViewコンポーネントを割り当てると、オブジェクトは位置、回転、そしてその他の値をリモート複製と同期できるようになります。 Transformや、(より一般的には)スクリプトの1つのようにコンポーネントを「監視」するには、PhotonViewを設定する必要があります。

データを同期するには、4つのオプションがあります。

  • オフ: 同期は発生せず、なにも送受信されません
  • 高信頼データの圧縮:データに変更がない場合にはnullを送信する内部最適化メカニズムによって、データの受信が保証されます。このメカニズムを機能させるには、ストリームを明確な.sendnext()コマンドとともに追加してください。
  • 低信頼: データは順番に受信されますが、一部のアップデートは欠損します。欠損する場合に、遅延はありません。
  • 低信頼OnChange: データは順番に受信されますが、一部のアップデートは欠損します。アップデートが最後の情報を繰り返す場合、Photon Viewは次の変更までアップデートの送信を一時停止します。

ここにあるデモの多くでオブジェクト同期を活用しています。 OnPhotonSerializeView()を実装してPhotonViewの監視されたコンポーネントになるスクリプトもあります。 OnPhotonSerializeView()では、位置とその他の値はストリームに書き込まれ、そこから読み込まれます。 この機能を活用するには、スクリプトにIPunObservableインターフェースを実装しなけれななりません。

Back To Top

リモートプロシージャコール(RPC)

ルームにいる全クライアントから呼び出しが可能になるように、メソッドをマークします。 属性[PunRPC]を使用して'ChangeColorToRed()'を実装した場合、 リモートプレイヤーはphotonView.RPC("ChangeColorToRed", RpcTarget.All);を呼び出すことで ゲームオブジェクトの色を赤に変更できます。

呼び出しは常にGameObjectの特定のPhotonViewを対象にしています。 このため、'ChangeColorToRed()'を呼び出した際、そのPhotonViewのあるGameObject上でのみ実行されます。 特定のオブジェクトにのみ影響を与えたい場合は、とても便利です。

もちろん、実際にターゲットのいないメソッドの場合は、「ダミー」としてシーンの中に空のGameObjectを入れることもできます。 例えば、RPCを使用して特定のGameObjectに関係のないチャット機能を実装することも可能です。

RPCは「バッファリング」することができます。 サーバーはその呼び出しを記憶して、RPCが呼び出された後に参加したクライアントに送信します。 これにより、動作を保存してPhotonNetwork.Instantiate()の代替を実装することができます。 手動でインスタンス化を参照してください。 欠点は、気を付けていないとバッファがどんどん膨れ上がってしまうことです。

Back To Top

カスタムプロパティ

Photonのカスタムプロパティは、必要に応じて入力できるキー値のハッシュテーブルで構成されています。 値はクライアントにおいて同期およびキャッシュされるので、使用する前にフェッチする必要がありません。 変更はSetCustomProperties()によって他のプレイヤーにプッシュされます。

これはどのように役に立つのでしょうか?通常、ルームとプレイヤーはGameObjectに関係のない属性を持っています。 その属性とは、最新のマップ、プレイヤーのキャラクターの色などです。(2d jump and runを思い出してください) これらの情報はオブジェクトの同期もしくはRPCを通して送信されますが、カスタムプロパティを使用したほうが便利な場合があります。

プレイヤーのカスタムプロパティを設定するには、 Player.SetCustomProperties(Hashtable propsToSet)を使用し、追加およびアップデートのためのキー値を含めます。 ローカルプレイヤーオブジェクト用のショートカットはPhotonNetwork.LocalPlayerです。 同様に、PhotonNetwork.CurrentRoom.SetCustomProperties(Hashtable propsToSet)を使用して入室しているルームのアップデートを行います。

アップデートの配布には少し時間がかかりますが、それに応じて全てのクライアントがCurrentRoom.CustomPropertiesPlayer.CustomPropertiesをアップデートします。 プロパティが変更された場合のコールバックとして、PUNは OnRoomPropertiesUpdate(Hashtable propertiesThatChanged)もしくはOnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProps) それぞれ呼び出します。

プロパティの設定は新しいルームを作成したときにも行えます。 ルームのプロパティは、マッチメイキングに使用できるので、ルーム作成時に設定できるのはとても便利です。 参加可能なルームをフィルタリングするためにプロパティ・ハッシュテーブルを使用する JoinRandomRoom()オーバーロードがあります。 ルームを作成する場合、 RoomOptions.CustomRoomPropertiesForLobby を設定してロビーでフィルタリング可能なルームプロパティを定義してください。

マッチメイキングに関するドキュメントでは マッチメイキングにカスタムプロパティを使用する方法を説明しています。

Back To Top

プロパティの確認と交換(CAS)

SetCustomPropertiesを使用すると、サーバーは通常、任意のクライアントから新しい値を受け入れます。これは、少し複雑になる場合があります。

例えば、ルーム内での固有のアイテムを誰が拾ったのかを保管するためにプロパティを使用できます。 つまり、プロパティのキーがアイテムで、値がそれを拾った者を定義します。 どのクライアントも、そのactorNumberにプロパティをいつでも設定することができます。 全員がほぼ同時にそれを行うと、最後の SetCustomProperties呼び出しが アイテムを獲得(最終値を設定)します。 この方法は反直感的で、好ましくありません。

SetCustomPropertiesには任意のexpectedValuesパラメータがあり、条件として使用できます。 expectedValuesを使用すると、現在のキー値がexpectedValues内のものと一致した場合のみ、サーバーがプロパティをアップデートします。 期限の切れたexpectedValuesのアップデートは無視されます。(結果としてクライアントはエラーを取得し、他の者は失敗したアップデートには気づきません。)

この例では、固有のアイテムを受け取るオーナーをexpectedValuesに含めることができます。 全員がアイテムを取ろうとしても、他のすべてのアップデート要求のexpectedValuesに期限切れのオーナーが含まれているので、最初の要求のみが成功します。

SetCustomPropertiesexpectedValuesを条件として使用することを、 確認と交換 (CAS)と呼びます。 これは、並行性の問題を回避するのに便利ですが、他にもクリエイティブな方法で使用することができます。

注:CASを使うと SetCustomProperties は失敗する可能性があるので、全クライアントはカスタムプロパティのアップデートをサーバーから送信するイベントによってのみ行います。これは新しい値を設定しようとするクライアントも含まれます。これは、CASを使用しないで値を設定する場合とは異なるタイミングで行われます。

CASを使用した初期化(つまり、新しいプロパティの初回作成)はサポートされていないことを知っておく必要があります。 また、現在、CASでのSetProperties障害のコールバックはありません。 これについて通知を受け取りたい場合は、MonoBehaviourに追加するコードの例を次に示します。

    private void OnEnable()
    {
        PhotonNetwork.NetworkingClient.OpResponseReceived += NetworkingClientOnOpResponseReceived;
    }

    private void OnDisable()
    {
        PhotonNetwork.NetworkingClient.OpResponseReceived -= NetworkingClientOnOpResponseReceived;
    }

    private void NetworkingClientOnOpResponseReceived(OperationResponse opResponse)
    {
        if (opResponse.OperationCode == OperationCode.SetProperties &&
            opResponse.ReturnCode == ErrorCode.InvalidOperation)
        {
            // cas failure
        }
    }

Back To Top

同期、RPC、プロパティを最大限活用する

どの同期メソッドが最適化を決定するのに、アップデートが必要な頻度と、値の「履歴」が要求されるかどうかを確認することをお勧めします。

Back To Top

頻度の高いアップデート(位置、キャラクターの状態)

頻繁に行われるアップデートについては、Object Synchronizationを使用します。 任意のアップデート数に対して、ストリームに何も書かないことにより、独自のスクリプトでアップデートをスキップすることができます。

キャラクターの位置は頻繁に変わります。各アップデートは有効ですが、新しい情報にすぐに置き換わっていくものです。 PhotonViewは「Unreliable」もしくは「Unreliable On Change」を送信するように設定できます。 「Unreliable」は、キャラクターが動いていない場合でも決まった頻度でアップデートを送信します。 「Unreliable On Change」ではGameObject (キャラクターやユニット)が止まっているときにはアップデートを送信しません。

Back To Top

不定期のアップデート(プレイヤーのアクション)

キャラクターの装備を変えたり、ツールを使用したり、ゲームのターンを終了することは全て頻度の低いアクションです。 ユーザー入力に基づいているのでRPCを送信するのが最適でしょう。

オブジェクトの同期を使用するかどうかの区別は明確なものではありません。 それでもオブジェクトの同期を実行する場合は、頻度の高いアップデートと「並行して」行うほうが理にかなっているでしょう。 例として、キャラクターの位置を送信する場合、「ジャンプ」の状態を送信するために簡単に値を追加できます。 これは独立したRPCにする必要はありません!

オブジェクト同期とは異なり、RPCはバッファリングされる可能性があります。 バッファリングされるRPCは後から参加するプレイヤーに送信されます。 これは動作を次々に再生する必要がある場合に便利です。 例えば、参加しているクライアントは、誰かがどうやってシーンにツールを配置して、誰がその道具をアップグレードしたかを再生できます。 後者は、最初の動作に依存します。

バッファリングされたRPCを新しいプレイヤーに送信するには、トラフィックを必要とします。つまり、クライアントは「実演中の」ゲームプレイに入る前に各自アクションを再生および適用する必要があるということです。 これによって問題が生じる場合があり、過剰なバッファリングは弱いクライアントを壊す可能性があります。このためバッファリングは注意して使用する必要があります。

注: RPCはすぐには送信されません。 詳細は こちらを確認してください。

Back To Top

頻度の低いアップデートや状態(ドアの開閉、地図、キャラクター装備)

頻度の低いアップデートはCustom Propertiesに保管するのが最適です。

バッファリングされたRPCとは異なり、Hashtableプロパティにはは現在のキー値のみが含まれています。 ドアの状態を「オープン」または「クローズ」する場合に優れています。プレイヤーは、以前どのようにドアが「オープン」「クローズ」されたかは 気にしませn。

上記のRPCの例では、誰かがシーンにツールを配置し、それがアップグレードされました。 少ないアクションにRPCを使用するのは問題ありません。修正が多い場合は、プロパティの単一の値に現在の状態を集約する方が簡単です。 複数の「防衛+10」のアップデートは多くのRPCではなく、単一の値に簡単に保存することができます。

繰り返しになりますが、カスタムプロパティとRPCの使用に明確な区分はありません。

カスタムプロパティを使用する適切な例として、ルームの「開始時刻」の保存があります。 ゲームが始まったら、プロパティとして PhotonNetwork.Timeを保存します。 この値は、ルーム内のすべてのクライアントで(ほぼ)同じで、開始時間を使用するとクライアントはゲームが実行されている時間の長さや誰のターンかを計算することができます。 また、各ターンの開始時間を保存することもできます。 これはゲームを一時停止できる場合に適しています。

ドキュメントのトップへ戻る