🛠️

【Photon Fusion】位置の同期 自作編

2023/08/16に公開

はじめに

Photon Fusionには、NetworkTransformなどのネットワークオブジェクトの位置を同期するコンポーネントがいくつか用意されています。

https://zenn.dev/photon_japan/articles/0ff7a5b9171666/

ただ、Fusionで用意されたコンポーネントだけではうまくいかないケース(サポートされていない機能が必要になる・ゲームに合わせてより細かい調整や最適化を行いたい等)の場合には、独自に同期コンポーネントを実装することになるでしょう。この記事では具体的な例を通して、同期の独自実装方法について説明します。

開発環境

  • Unity 2022.2.13f1
  • Fusion SDK 1.1.8 F Build 725

同期の独自実装の例

同期の独自実装は、同期したい値をネットワークプロパティ(Networked Properties)にして通信することが基本になるでしょう。実装の大体の手順は以下のような流れになります。

  1. 同期したい値をネットワークプロパティで定義する
  2. FixedUpdateNetwork()でネットワークプロパティの値を更新する
  3. Render()でネットワークプロパティの値を反映する

独自実装した同期コンポーネントでスナップショット補間(Snapshot Interpolation)を適用したい場合は、Fusionに用意されているインターポレーター(Interpolator)を活用することで、スムーズな補間がシンプルに実装できるでしょう。

https://doc.photonengine.com/ja-jp/fusion/v1/manual/network-object/network-behaviour
https://doc.photonengine.com/ja-jp/fusion/v1/manual/interpolator

スケールの同期

NetworkTransformはスケール(transform.localScale)を同期できないので、スケールをネットワークプロパティで同期するスクリプトを実装してみましょう。

スケールの変化をスムーズに補間して表示するため、スケールのネットワークプロパティのインターポレーター(Interpolator<Vector3>)を、NetworkBehaviourGetInterpolator<Vector3>()から取得して使用しましょう。

NetworkTransformScale.cs
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)をネットワークプロパティで同期するスクリプトを実装してみましょう。

NetworkTransformLocal.cs
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は入れ子で使用しても同期ズレは発生しない

エルミート補間による位置の同期

インターポレーターを応用すれば、スナップショット補間で任意の補間アルゴリズムが適用できます。ここでは、エルミート曲線(始点(p1)とその速度(v1)、終点(p2)とその速度(v2)から、始点と終点の間を繋ぐ曲線)の補間で位置を同期するスクリプトを実装してみましょう。

まず以下のような、エルミート補間の計算処理を行う静的クラスを定義しておきます。

https://t-pot.com/program/2_3rdcurve/index.html

CubicHermiteSpline.cs
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キーワードを使用してネットワークプロパティを定義すると、構造体のコピーではなく参照が取得できる仕組みがあるのでそれを利用できます。

https://doc.photonengine.com/ja-jp/fusion/v1/manual/inetworkstruct

NetworkPositionVelocity.cs
using Fusion;
using UnityEngine;

public struct NetworkPositionVelocity : INetworkStruct
{
    [Networked]
    public Vector3 Position { get; set; }
    [Networked]
    public Vector3 Velocity { get; set; }
}

通常のインターポレーター(Interpolator<T>)に対応していない型の補間にはRawInterpolatorを使用できます。RawInterpolatorTryGetStruct<NetworkPositionVelocity>から開始値・終了値・補間係数が取得できるので、それらを使ってエルミート補間の計算処理を行いましょう。

CustomNetworkTransform.cs
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の読み書きを行う必要があります。

  1. クライアントはResimulationループ段階で、各ネットワークプロパティが最新のサーバーのスナップショットにロールバックした後、かつFixedUpdateNetwork()が実行される前に、ネットワークプロパティの値をtransformに反映する
  2. ホストはForwardループ段階で、FixedUpdateNetwork()が実行された後、かつForwardループが完了してスナップショットがクライアントに送信される前に、transformの値をネットワークプロパティに反映する

上記に対応するコールバックとして、BeforeAllTicksAfterAllTicksが利用できます。スクリプトにIBeforeAllTicksIAfterAllTicksインターフェースを実装するとコールバックが受け取れるようになるので、そこでtransformを読み書きする処理を追加しましょう。

https://doc.photonengine.com/ja-jp/fusion/v1/manual/execution-order

NetworkTransformLocal.cs
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;
        }
    }
}
Photon運営事務局TechBlog

Discussion