【Photon Fusion】位置の同期 自作編
はじめに
Photon Fusionには、NetworkTransform
などのネットワークオブジェクトの位置を同期するコンポーネントがいくつか用意されています。
ただ、Fusionで用意されたコンポーネントだけではうまくいかないケース(サポートされていない機能が必要になる・ゲームに合わせてより細かい調整や最適化を行いたい等)の場合には、独自に同期コンポーネントを実装することになるでしょう。この記事では具体的な例を通して、同期の独自実装方法について説明します。
開発環境
- Unity 2022.2.13f1
- Fusion SDK 1.1.8 F Build 725
同期の独自実装の例
同期の独自実装は、同期したい値をネットワークプロパティ(Networked Properties)にして通信することが基本になるでしょう。実装の大体の手順は以下のような流れになります。
- 同期したい値をネットワークプロパティで定義する
-
FixedUpdateNetwork()
でネットワークプロパティの値を更新する -
Render()
でネットワークプロパティの値を反映する
独自実装した同期コンポーネントでスナップショット補間(Snapshot Interpolation)を適用したい場合は、Fusionに用意されているインターポレーター(Interpolator)を活用することで、スムーズな補間がシンプルに実装できるでしょう。
スケールの同期
NetworkTransform
はスケール(transform.localScale
)を同期できないので、スケールをネットワークプロパティで同期するスクリプトを実装してみましょう。
スケールの変化をスムーズに補間して表示するため、スケールのネットワークプロパティのインターポレーター(Interpolator<Vector3>
)を、NetworkBehaviour
のGetInterpolator<Vector3>()
から取得して使用しましょう。
using Fusion;
using UnityEngine;
public class NetworkTransformScale : NetworkBehaviour
{
// スケールのネットワークプロパティを定義する
[Networked]
private Vector3 LocalScale { get; set; }
private Interpolator<Vector3> interpolator;
public override void Spawned() {
LocalScale = transform.localScale;
// スケール(LocalScale)のインターポレーターを取得する
interpolator = GetInterpolator<Vector3>(nameof(LocalScale));
}
public override void FixedUpdateNetwork() {
// 一例として、方向キーの左右でスケール(LocalScale)を更新できるようにする
if (GetInput(out NetworkInputData input)) {
float delta = input.Direction.x * Runner.DeltaTime;
LocalScale += new Vector3(delta, delta, delta) * 3f;
}
}
public override void Render() {
// Interpolatorで自動的に補間されたスケール(LocalScale)をtransformに反映する
transform.localScale = interpolator.Value;
}
}
ローカル座標系の位置の同期
NetworkTransform
はローカル座標系での値を同期することはできません。ローカル座標系の位置(transform.localPosition
)と回転(transform.localRotation
)をネットワークプロパティで同期するスクリプトを実装してみましょう。
using Fusion;
using UnityEngine;
public class NetworkTransformLocal : NetworkBehaviour
{
// ローカル座標系の位置と回転のネットワークプロパティを定義する
[Networked]
private Vector3 LocalPosition { get; set; }
[Networked]
private Quaternion LocalRotation { get; set; }
private Interpolator<Vector3> positionInterpolator;
private Interpolator<Quaternion> rotationInterpolator;
public override void Spawned() {
LocalPosition = transform.localPosition;
LocalRotation = transform.localRotation;
// 各ネットワークプロパティのインターポレーターを取得する
positionInterpolator = GetInterpolator<Vector3>(nameof(LocalPosition));
rotationInterpolator = GetInterpolator<Quaternion>(nameof(LocalRotation));
}
public override void FixedUpdateNetwork() {
// 一例として、方向キーで位置と回転を更新できるようにする
if (GetInput(out NetworkInputData input)) {
var delta = input.Direction * Runner.DeltaTime;
LocalPosition += new Vector3(delta.x * 2f, 0f, 0f);
LocalRotation *= Quaternion.Euler(0f, delta.z * 30f, 0f);
}
}
public override unsafe void Render() {
// Interpolatorで自動的に補間された位置と回転をtransformに反映する
transform.SetLocalPositionAndRotation(positionInterpolator.Value, rotationInterpolator.Value);
}
}
、ここで実装したNetworkTransform
はワールド座標系の値を同期していることが原因で、入れ子にして使用すると同期ズレが起こる問題がありますがNetworkTransformLocal
はローカル座標系の値を同期しているので、入れ子にして使用しても問題なく動作させることができます。
NetworkTransformLocal
は入れ子で使用しても同期ズレは発生しない
エルミート補間による位置の同期
インターポレーターを応用すれば、スナップショット補間で任意の補間アルゴリズムが適用できます。ここでは、エルミート曲線(始点(
まず以下のような、エルミート補間の計算処理を行う静的クラスを定義しておきます。
using UnityEngine;
public static class CubicHermiteSpline
{
public static float Interpolate(float p1, float p2, float v1, float v2, float t) {
float a = 2f * p1 - 2f * p2 + v1 + v2;
float b = -3f * p1 + 3f * p2 - 2f * v1 - v2;
return t * (t * (t * a + b) + v1) + p1;
}
public static Vector3 Interpolate(Vector3 p1, Vector3 p2, Vector3 v1, Vector3 v2, float t) {
return new Vector3(
Interpolate(p1.x, p2.x, v1.x, v2.x, t),
Interpolate(p1.y, p2.y, v1.y, v2.y, t),
Interpolate(p1.z, p2.z, v1.z, v2.z, t)
);
}
}
位置と速度はNetworkStruct
(ネットワークプロパティやインターポレーターなどで扱える構造体)で作成します。構造体は値型なので、通常はネットワークプロパティから取得した構造体のフィールドに値を代入しても変更は反映されない(Unityでtransform.position.x
に直接値を代入して変更できないのと同じ)ですが、C#のref
キーワードを使用してネットワークプロパティを定義すると、構造体のコピーではなく参照が取得できる仕組みがあるのでそれを利用できます。
using Fusion;
using UnityEngine;
public struct NetworkPositionVelocity : INetworkStruct
{
[Networked]
public Vector3 Position { get; set; }
[Networked]
public Vector3 Velocity { get; set; }
}
通常のインターポレーター(Interpolator<T>
)に対応していない型の補間にはRawInterpolator
を使用できます。RawInterpolator
のTryGetStruct<NetworkPositionVelocity>
から開始値・終了値・補間係数が取得できるので、それらを使ってエルミート補間の計算処理を行いましょう。
using Fusion;
using UnityEngine;
public class CustomNetworkTransform : NetworkBehaviour
{
// 位置と速度のネットワークプロパティを定義する
[Networked]
private ref NetworkPositionVelocity PositionVelocity
=> ref MakeRef<NetworkPositionVelocity>();
private RawInterpolator interpolator;
public override void Spawned() {
PositionVelocity.Position = transform.position;
// 位置と速度のインターポレーターを取得する
interpolator = GetInterpolator(nameof(PositionVelocity));
}
public override void FixedUpdateNetwork() {
// 一例として、方向キーで位置と速度の値を更新できるようにする
if (GetInput(out NetworkInputData input)) {
var delta = input.Direction * Runner.DeltaTime;
PositionVelocity.Velocity += delta * 0.2f;
}
PositionVelocity.Position += PositionVelocity.Velocity;
}
public override void Render() {
// RawInterpolatorから取得した値でエルミート補間を行ってtransformに反映する
if (interpolator.TryGetStruct<NetworkPositionVelocity>(out var from, out var to, out var alpha)) {
transform.position = CubicHermiteSpline.Interpolate(
from.Position, to.Position, from.Velocity, to.Velocity, alpha
);
}
}
}
汎用的な同期コンポーネントの実装
これまでの同期コンポーネントの実装例では、transform
の値を直接更新するのではなく、ネットワークプロパティ側の値を更新して、その値をtransform
に反映して同期するような形になっていました。この方法は実装をシンプルにまとめられるメリットがありますが、実用的には、transform
を更新するスクリプトと、そのtransform
を自動的に同期するスクリプトに分けて実装したい(NetworkTransform
と同じように使いたい)ケースもあるでしょう。
transform
を自動的に同期するコンポーネントを実装する場合には、ネットワークシミュレーションループを良く理解して、適切なタイミングでtransform
の読み書きを行う必要があります。
-
クライアントはResimulationループ段階で、各ネットワークプロパティが最新のサーバーのスナップショットにロールバックした後、かつ
FixedUpdateNetwork()
が実行される前に、ネットワークプロパティの値をtransform
に反映する -
ホストはForwardループ段階で、
FixedUpdateNetwork()
が実行された後、かつForwardループが完了してスナップショットがクライアントに送信される前に、transform
の値をネットワークプロパティに反映する
上記に対応するコールバックとして、BeforeAllTicks
とAfterAllTicks
が利用できます。スクリプトにIBeforeAllTicks
とIAfterAllTicks
インターフェースを実装するとコールバックが受け取れるようになるので、そこでtransform
を読み書きする処理を追加しましょう。
using Fusion;
using UnityEngine;
// IBeforeAllTicksとIAfterAllTicksインターフェースを実装する
public class NetworkTransformLocal : NetworkBehaviour, IBeforeAllTicks, IAfterAllTicks
{
[Networked]
private Vector3 LocalPosition { get; set; }
[Networked]
private Quaternion LocalRotation { get; set; }
[Networked]
private Vector3 LocalScale { get; set; }
private Interpolator<Vector3> localPositionInterpolator;
private Interpolator<Quaternion> localRotationInterpolator;
private Interpolator<Vector3> localScaleInterpolator;
public override void Spawned() {
LocalPosition = transform.localPosition;
LocalRotation = transform.localRotation;
LocalScale = transform.localScale;
localPositionInterpolator = GetInterpolator<Vector3>(nameof(LocalPosition));
localRotationInterpolator = GetInterpolator<Quaternion>(nameof(LocalRotation));
localScaleInterpolator = GetInterpolator<Vector3>(nameof(LocalScale));
}
// FixedUpdateNetwork()が実行される前に、ネットワークプロパティの値をtransformに反映する
void IBeforeAllTicks.BeforeAllTicks(bool resimulation, int tickCount) {
transform.SetLocalPositionAndRotation(LocalPosition, LocalRotation);
transform.localScale = LocalScale;
}
// FixeUpdateNetwork()が実行された後に、transformの値をネットワークプロパティに反映する
void IAfterAllTicks.AfterAllTicks(bool resimulation, int tickCount) {
LocalPosition = transform.localPosition;
LocalRotation = transform.localRotation;
LocalScale = transform.localScale;
}
public override void Render() {
// 位置と回転のInterpolatorの値が取得できるなら、transformに反映する
if (localPositionInterpolator.TryValue is {} position && localRotationInterpolator.TryValue is {} rotation) {
transform.SetLocalPositionAndRotation(position, rotation);
}
// スケールのInterpolatorの値が取得できるなら、transformに反映する
if (localScaleInterpolator.TryValue is {} scale) {
transform.localScale = scale;
}
}
}
Discussion