5 - プロパティの変更
概要
ネットワークプロパティを定義すると、Fusionはget
とset
スタブをネットワーク上の状態にアクセスするカスタムコードに置き換えます。つまりアプリケーションでは、これらのメソッドのプロパティの値を変更したり、ローカルでのみ動作する別のセッターメソッドを作成することはできないということです。
この問題を解消するために、Fusionでは[Networked]
属性に引数を提供しています。ここで、プロパティが変更されるごとに呼び出される静的なコールバックメソッドを指定することができます。
これは、状態の変化に応じたローカルのビジュアルエフェクトの生成や、ゲームプレイロジックに直接影響を与えないタスクを実行するのに便利です。重要な注意点ですが、プロパティの変更は再シミュレーション時に何度(正確には、予測で一回、予測が不正確だった場合にもう一回で計二回)も行われます。また、ネットワーク上の状態が送信されるよりも早く値が変更された後に元の値に戻された(またはパケットがドロップした)場合は、プロパティの変更が完全にスキップされることがあります。
OnChanged
コールバックは、UnityのUpdate()
サイクルの中で(厳密には、Update()
が呼ばれる前に)実行されますが、これはシミュレーションの中ではなく、単純にUnityのフレーム間の変更に反応します。これが、値の変更が検出されないもう一つの理由になります。一つのフレーム内で複数のスナップショットが処理されて、最後のスナップショットがネットワークプロパティの元の値を返す場合は、変更は検出されません。
RPC等ではなくコールバックを使用する主なメリットは、RPCがゲームの状態が変更された時点より後に実行される可能性があるのに対して、コールバックは値が変更されたティックに直ちに実行されるという点です。
変更のリスナーを追加する
プロパティ変更のコールバックは、[Networked]
属性のOnChanged
パラメーターで定義されて、Changed<T>
型のパラメーターを一つ持つ静的メソッドの名前を渡します。T
はNetworkBehaviour
を継承したクラスの型です。
コールバックは静的メソッドである必要があります。静的メソッドであることでFusionは、オブジェクトの各インスタンスにデリゲートを割り当てるかわりに、型に対して一つのデリゲートを持たせることができます。
ここから、ボールが発射された時にキューブを白色にして、その後に青色にフェードさせていく例を示します。
エフェクトを発生させるために、ホストはネットワーク上の変数の真偽値を操作します。一つのティックでプレイヤーからはボールが二つ以上スポーンしないようになっているので、ボールをスポーンするたびに真偽値を変更することでOnChanged
コールバックを呼び出すことができます。
コードを記述する前に、この仕様は上手くいかない可能性があることに留意してください。前述したように、値が非常に頻繁に変更される場合には、その変更が検出されない場合があります。この問題を避ける方法の一つは、NetworkBool
をbyte
かint
に置き換えて、呼び出しのたびに値をインクリメントさせてください。結局、どれだけビジュアルエフェクトを優先するか、どれだけ帯域幅を消費するかの問題になります。
これらを踏まえて、Player
クラスを開き、新しいプロパティとコールバックのシンプルな実装を追加します。以下を参照してください。
C#
[Networked(OnChanged = nameof(OnBallSpawned))]
public NetworkBool spawned { get; set; }
public static void OnBallSpawned(Changed<Player> changed)
{
changed.Behaviour.material.color = Color.white;
}
これは明らかにPlayer
に、キューブのメッシュの色を変更するためのマテリアルプロパティがあることを前提としています。ここに新しいプロパティを追加しましょう。
C#
private Material _material;
Material material
{
get
{
if(_material==null)
_material = GetComponentInChildren<MeshRenderer>().material;
return _material;
}
}
色はRender()
の更新で、現在の色から青色に線形補間されます。これがUpdate()
ではなくRender()
で行われているのは、Render()
がFixedUpdateNetwork()
の後に実行されることが保証されているためです。また、Runner.DeltaTime
ではなくTime.deltaTime
が使われているのは、Render()
がFusionのシミュレーションの一部ではなくUnity標準のライフサイクルで実行されるからです。
C#
public override void Render()
{
material.color = Color.Lerp(material.color, Color.blue, Time.deltaTime );
}
後のコードは、Runner.Spawn()
を呼び出した後にspawned
プロパティをトグルして、コールバックを呼び出すためのものです。
C#
Runner.Spawn(_prefabBall, transform.position+_forward, Quaternion.LookRotation(_forward));
spawned = !spawned;
Spawn()
が呼び出される個所が二つあることに留意してください。その両方の箇所でspawned
をトグルします。
理由
Q: 単純に、スポーンの呼び出し時に即座に色を設定すればいいのではないでしょうか?
ホストや入力権限のあるクライアントでは正しく動作しますが、プロキシ(入力権限も状態権限も持たないクライアント)では動作しないためです。
Q: 色がネットワークプロパティになっているなら、単純に全てのクライアントがRender()
でローカルにプロパティの値を更新すれば、OnChanged
は必要ないのではないでしょうか?
確かにうまくはいきますが、頻繁に値が更新されるため不要な通信が発生します。一般的に、ビジュアルエフェクトは状態権限を持つプレイヤーによって引き起こされ、各クライアント上で独立して実行されるべきです。誰もが派手なエフェクトを好みますが、そのエフェクトの火花がどちらの方向に飛んでいるかは誰も気にしていないでしょう。
以前の状態にアクセスする
このシンプルな例では、変更したプロパティの実際の値には触れませんでしたが、コールバックに渡されるChanged<T>
はNetworkBehaviour
の参照を持っていて、プロパティが変更された時のティックの全てのネットワークプロパティにアクセスすることができます。
わかりにくいかもしれませんが、以前の状態をロードするメソッドによって、効率的に前のティックの全てのネットワークプロパティを取得することができます。
C#
var newValue = changed.Behaviour.someNetworkedProperty;
changed.LoadOld();
var oldValue = changed.Behaviour.someNetworkedProperty;
同様に、アプリケーションはLoadNew()
で再度新しい状態をロードすることができます。処理を終了する時に、状態を元にリセットすべきかを気にする必要はありません。ここでの状態は、個々のコールバックのコンテキストでのみ有効だからです。