Chapter 04

🔄 同期1 : オブジェクト同期

o8que
o8que
2021.04.13に更新

ネットワークオブジェクトはオブジェクト同期(Object Synchronization)によって、定期的にデータの送受信を行って、自由な値を同期することができます。

オブジェクト同期の基礎

ネットワークオブジェクトにIPunObservableインターフェースを実装したスクリプトを追加すると、そのスクリプトはPhotonViewの監視対象コンポーネント(Observed Components)となって、IPunObservable.OnPhotonSerializeView()が定期的に呼ばれるようになります。

IPunObservable.OnPhotonSerializeView()では、ストリーム(PhotonStream)からPhotonで通信できるデータ型の値を読み書きしていきます。自身が管理するネットワークオブジェクトならデータをストリームに書き込んで送信する処理、他プレイヤーが管理するネットワークオブジェクトなら受信したストリームを読み込んで同期する処理を行います。どちらの処理を行うべきかはstream.IsWritingで判定できます。

using Photon.Pun;
using UnityEngine;

// IPunObservableインターフェースを実装して、PhotonViewの監視対象コンポーネントにする
public class SampleTransformView : MonoBehaviourPunCallbacks, IPunObservable
{
    void IPunObservable.OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) {
        if (stream.IsWriting) {
            // Transformの値をストリームに書き込んで送信する
            stream.SendNext(transform.localPosition);
            stream.SendNext(transform.localRotation);
            stream.SendNext(transform.localScale);
        } else {
            // 受信したストリームを読み込んでTransformの値を更新する
            transform.localPosition = (Vector3)stream.ReceiveNext();
            transform.localRotation = (Quaternion)stream.ReceiveNext();
            transform.localScale = (Vector3)stream.ReceiveNext();
        }
    }
}

既製コンポーネント

PUN2では、オブジェクトの座標やアニメーションなどの同期を簡単に行えるようにするための、オブジェクト同期を利用して作成されたコンポーネントがあらかじめ用意されています。これを使用することで、オブジェクト同期を行うスクリプトなどを独自に実装せずに同期処理を済ませることができます。

PhotonTransformView

PhotonTransformViewコンポーネントはTransformの値(座標・回転・スケール)の同期を行うスクリプトです。このコンポーネントをネットワークオブジェクトに追加するだけで、Transformの値を同期させることができます。

PhotonTransformViewClassic

PhotonTransformViewClassicコンポーネントは、古いバージョンのPhotonTransformViewを移植したものです。PhotonTransformViewは細かい調整なしでいい感じに座標を同期できるように作られていますが、逆に細かい調整ができないという不満点も上がっていました。PhotonTransformViewの挙動に満足がいかず、より細かく調整したい場合に使ってみると良いでしょう。

PhotonRigidbodyView(PhotonRigidbody2DView)

PhotonRigidbodyViewコンポーネントでRigidbodyの同期を行うことができます。このコンポーネントを追加すると、Rigidbodyの値(座標・回転)が同期されるようになります。PhotonTransformViewと併用する場合は、座標や回転の値を二重に同期しないようにしておかないと、動きがカクついたり無駄な通信負荷が発生してしまう可能性があるので注意しましょう。

また、Rigidbody2Dの同期を行うPhotonRigidbody2DViewコンポーネントも用意されています。

PhotonAnimatorView

PhotonAnimatorViewコンポーネントでAnimatorのアニメーションパラメーターを同期できます。インスペクターには、対応するAnimatorのアニメーションパラメーターのリストが自動的に表示されます。

🎮 アバターのスタミナゲージを表示しよう

チュートリアルで作成したサンプルプロジェクトのアバターに、スタミナのパラメーターを持たせて、オブジェクト同期でスタミナゲージの表示を同期してみましょう。スタミナは、移動中は減少していき、静止すると回復していくものとします。

スタミナゲージのUIを作成しよう

最初に、アバターのゲームオブジェクトの子要素として、スタミナゲージのUIを作成しましょう。





スタミナゲージを同期しよう

アバターを操作するスクリプトにスタミナのパラメーター(currentStamina)を追加して、入力の有無で増減させる処理を実装します。その後に、スクリプトのクラスにIPunObservableインターフェースを実装して、スタミナの値をIPunObservable.OnPhotonSerializeView()で送受信して同期させましょう。

AvatarController.cs
using Photon.Pun;
using UnityEngine;
using UnityEngine.UI;

public class AvatarController : MonoBehaviourPunCallbacks, IPunObservable
{
    private const float MaxStamina = 6f;

    [SerializeField]
    private Image staminaBar = default;

    private float currentStamina = MaxStamina;

    private void Update() {
        if (photonView.IsMine) {
            var input = new Vector3(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"), 0f);
            if (input.sqrMagnitude > 0f) {
                // 入力があったら、スタミナを減少させる
                currentStamina = Mathf.Max(0f, currentStamina - Time.deltaTime);
                transform.Translate(6f * Time.deltaTime * input.normalized);
            } else {
                // 入力がなかったら、スタミナを回復させる
                currentStamina = Mathf.Min(currentStamina + Time.deltaTime * 2, MaxStamina);
            }
        }

        // スタミナをゲージに反映する
        staminaBar.fillAmount = currentStamina / MaxStamina;
    }

    void IPunObservable.OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) {
        if (stream.IsWriting) {
            // 自身のアバターのスタミナを送信する
            stream.SendNext(currentStamina);
        } else {
            // 他プレイヤーのアバターのスタミナを受信する
            currentStamina = (float)stream.ReceiveNext();
        }
    }
}

インスペクターから、先ほど作成したUIの参照をstaminaBarにアタッチするのも忘れないようにしましょう。

プロジェクトをビルドして、スタミナゲージの表示が同期されていれば成功です。

このサンプルでは、スタミナが少なくなったり0になったりしても何も起こりません。スタミナが減少したら移動速度が落ちるような処理の追加を試してみるのも面白いでしょう。

🌶 オブジェクト同期の頻度を調整する

オブジェクト同期はデータを定期的に送信しますが、その頻度によって、同期の精度や処理コスト、通信量やその負荷などが大きく変わってきます。PUN2ではPhotonNetworkから簡単に送信頻度を調整することができます。

PhotonNetwork.SendRate = 20; // 1秒間にメッセージ送信を行う回数
PhotonNetwork.SerializationRate = 10; // 1秒間にオブジェクト同期を行う回数

本書では便宜上、IPunObservable.OnPhotonSerializeView()でPhotonStreamに値を書き込むことを「データを送信する」と書いていますが、厳密には「送信データを作成する」が正しいです。Photonには、複数の送信データを可能な限りまとめて送信することで、通信を最適化する仕組みが備わっています。SerializationRateの間隔で作成された送信データは、SendRateの間隔でまとめて送信されます。つまり、データが作成されてから送信されるまでには若干の遅延があるということです。SendRateを上げることで、この遅延を最小に抑えることができますが、複数の送信データがバラバラに送られるようになり通信量が増える可能性があります。

🌶 オブジェクト同期の一時停止

オブジェクト同期で不要なデータを送信しないようにすることで、通信量を削減できます。

PhotonStreamに書き込まない

空のPhotonStreamは送信されません。例えば、フラグを使って書き込みを行わないようにするだけで、オブジェクト同期のデータ送信を一時停止することが可能です。

private bool isSyncing = true; // 同期フラグ

void IPunObservable.OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) {
    if (stream.IsWriting) {
        // 同期フラグが立っている場合のみ送信を行う
        if (isSyncing) {
            stream.SendNext(transform.position);
        }
    } else {
        transform.position = (Vector3)stream.ReceiveNext();
    }
}

PhotonViewの監視オプションを設定する

PhotonViewの監視オプションを「Reliable Delta Compressed」か「Unreliable On Change」に設定すると、監視対象の値が更新された場合にのみデータを送信するようになります。

値が更新されたとみなす閾値を調整する

PhotonViewの監視対象の値が更新されたとみなす閾値は、PhotonNetworkから調整できます。

PhotonNetwork.PrecisionForVectorSynchronization = 0.00001f;
PhotonNetwork.PrecisionForQuaternionSynchronization = 1f;
PhotonNetwork.PrecisionForFloatSynchronization = 0.01f;

しかし、これはデータ型の単位でしか設定することができないので、正直使いづらい所があります。同じような処理を独自で実装してしまう方が楽なことが多いです。

private Vector3 lastPosition = transform.position;

void IPunObservable.OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) {
    if (stream.IsWriting) {
        // 前回に送信した座標から、一定の距離以上移動した場合のみ現在の座標を送信する
        if (Vector3.Distance(transform.position, lastPosition) > 0.01f) {
            stream.SendNext(transform.position);
            lastPosition = transform.position;
        }
    } else {
        transform.position = (Vector3)stream.ReceiveNext();
    }
}