ハツェの真時代傾向璋

興味を持ったことを書いていく鱗片的な場所から先の未来の場

Persistence入門 - PlayerObject編

こんばんは。ハツェです。
前回に続き、Persistenceの第二回です。
動作環境は、Unity : 2022.3.22f1、VRCSDK : 3.7.2-persistence-beta.1です。

目次


前回

前回は、Persistenceに関する基礎知識の紹介をしました。
hatuxes.hatenablog.jp

前提知識

同期に関する内容を扱うため、必要な方は先に下記をご覧下さい。
hatuxes.hatenablog.jp

今回の概要

今回は、PersistenceのPlayerObjectを使ったデータの保存と復元例について紹介します。

PlayerObjectの作成方法

PlayerObjectを用いてUserDataを作る方法は以下の通りです。

  1. 一番親のGameObjectに VRCPlayerObject コンポーネントをアタッチする
  2. VRCPlayerObject コンポーネントをアタッチしたGameObjectより子の中で、データを保存しておきたいGameObjectに VRCEnablePersistence コンポーネントをアタッチする
  3. VRCEnablePersistence コンポーネントをアタッチしたGameObjectにUdon Behaviourがアタッチされていれば、保存するデータをそのUdonの同期変数として記載する

PlayerObjectの取得方法

PlayerObjectは実行時にテンプレートから各プレイヤーごとに複製されるため、プレイヤーの数だけPlayerObjectが存在することになります。
また、実行時にテンプレート本体は非アクティブ化されます。

そのため、Inspectorからドラック&ドロップする形ではテンプレートへの参照となり、保存も復元も行われなくなるため、実行時にPlayerObjectの参照を得る必要があります。

PlayerObjectの参照については、Networking.GetPlayerObjects(player) を用いることで取得することが出来ます。
この関数は、引数に入れたプレイヤーのPlayerObjectをテンプレートの数だけ配列として返すため、自分のPlayerObjectはもちろん、他人のPlayerObjectを取得することも可能です。

GameObject[] playerObjectList = Networking.GetPlayerObjects(player);


そして、取得したPlayerObjectから目的のオブジェクトやスクリプトの参照を見つけてくることで目的のセーブデータを取得します。

実例紹介

ここからは、「再Joinしてもカウントが保存されるカウントアップ装置」の作成例を示しながらPlayerObjectについて紹介していきます。

下準備

まず、受け取ったカウント数をテキストとして表示するUdonを用意しておきます。
ここは、特にPersistenceとは関係ありません。

using UdonSharp;
using UnityEngine;
using UnityEngine.UI;
using VRC.SDKBase;

[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class PushCountViewer : UdonSharpBehaviour
{
    [SerializeField]
    private Text _pushCountText;


    public void SetPushCount(int count)
    {
        if (!Utilities.IsValid(_pushCountText))
        {
            return;
        }

        _pushCountText.text = count.ToString();
    }
}

User Dataを用意する

次に、User Dataを用意します。
Player ObjectにするGameObjectを用意し、一番親のGameObjectに VRC Player Object をアタッチします。

その後、VRC Player Object をアタッチしたGameObjectの子にUser Data用のGameObjectを用意し、VRC Enable Persistence とUser Data用のUdonをアタッチします。

そして、アタッチするスクリプトを以下の通りにします。
ここでは、どこからでもこのUserDataが取得出来るようにした上で、値を更新しただけで自動的にセーブするような記述にしています。

using UdonSharp;
using UnityEngine;
using VRC.SDKBase;

[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class PushCountUserData : UdonSharpBehaviour
{
    // 保存するデータ
    [UdonSynced]
    private int _pushCount;


    public static PushCountUserData GetInstance(VRCPlayerApi player)
    {
        GameObject[] playerObjectList = Networking.GetPlayerObjects(player);
        if (!Utilities.IsValid(playerObjectList))
        {
            return null;
        }

        foreach (GameObject playerObject in playerObjectList)
        {
            if (!Utilities.IsValid(playerObject))
            {
                continue;
            }

            PushCountUserData foundScript = playerObject.GetComponentInChildren<PushCountUserData>();
            if (!Utilities.IsValid(foundScript))
            {
                continue;
            }

            return foundScript;
        }

        return null;
    }


    public int GetPushCount()
    {
        return _pushCount;
    }

    public void SetPushCount(int count)
    {
        _pushCount = count;

         // 同期変数の変更を同期させることで自動的にUserDataが更新される
        RequestSerialization();
    }
}

カウントアップの処理を作る

これまで用意してきたものを使って、実際のカウントアップ処理を作っていきます。
インタラクトしたタイミングでセーブデータを取得し、データを更新して保存するようにしました。

using UdonSharp;
using UnityEngine;
using VRC.SDKBase;

[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class PlayerObjectCountupCube : UdonSharpBehaviour
{
    [SerializeField]
    private PushCountViewer _viewer;


    public override void Interact()
    {
        base.Interact();
        PushCountUserData userData = PushCountUserData.GetInstance(Networking.LocalPlayer);
        if (!userData)
        {
            return;
        }

        // カウントアップ
        int pushCount = userData.GetPushCount();
        pushCount++;

        // セーブデータに反映
        userData.SetPushCount(pushCount);
        
        // テキスト更新
        UpdatePushCount();
    }

    public override void OnPlayerRestored(VRCPlayerApi player)
    {
        base.OnPlayerRestored(player);
        if (player.isLocal)
        {
            UpdatePushCount();
        }
    }


    private void UpdatePushCount()
    {
        if (!Utilities.IsValid(_viewer))
        {
            return;
        }

        PushCountUserData userData = PushCountUserData.GetInstance(Networking.LocalPlayer);
        if (!Utilities.IsValid(userData))
        {
            return;
        }

        _viewer.SetPushCount(userData.GetPushCount());
    }
}

実行

以上のものを実行すると以下の通りになります。
Cubeをインタラクトすることで数字がカウントアップされ、再度Joinすると保存された数値から再開されていることが確認出来るかと思います。

OwnerのUserDataを表示するようにしてみる

ここから、Cubeを押すとOwnerが譲渡されて、そのOwnerのカウント値が表示されるようにしてみます。
先ほどとの相違点としては、「自身ではなくOwnerのUserDataを取得するようにしたこと」と「インタラクト時にOwnerを変更するようにしたこと」の2点です。

using UdonSharp;
using UnityEngine;
using UnityEngine.UI;
using VRC.SDKBase;
using VRC.Udon.Common.Interfaces;

[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class OwnerPlayerObjectCountupCube : UdonSharpBehaviour
{
    [SerializeField]
    private PushCountViewer _viewer;

    [SerializeField]
    private Text _ownerText;


    public override void Interact()
    {
        base.Interact();
        PushCountUserData userData = PushCountUserData.GetInstance(Networking.LocalPlayer);
        if (!userData)
        {
            return;
        }

         // カウントアップ
        int pushCount = userData.GetPushCount();
        pushCount++;

        // セーブデータに反映
        userData.SetPushCount(pushCount);

        // Ownerを自分に譲渡して、全員にテキストを更新を命じる
        Networking.SetOwner(Networking.LocalPlayer, this.gameObject);
        SendCustomNetworkEvent(NetworkEventTarget.All, nameof(UpdateAllText));
    }

    public override void OnPlayerRestored(VRCPlayerApi player)
    {
        base.OnPlayerRestored(player);
        if (player.isLocal)
        {
            UpdateAllText();
        }
    }


    public void UpdateAllText()
    {
        UpdatePushCount();
        UpdateOwnerText();
    }


    private void UpdatePushCount()
    {
        if (!Utilities.IsValid(_viewer))
        {
            return;
        }

        VRCPlayerApi owner = Networking.GetOwner(this.gameObject);
        if (!Utilities.IsValid(owner))
        {
            return;
        }

        PushCountUserData userData = PushCountUserData.GetInstance(owner);
        if (!Utilities.IsValid(userData))
        {
            return;
        }

        _viewer.SetPushCount(userData.GetPushCount());
    }

    private void UpdateOwnerText()
    {
        if (!Utilities.IsValid(_ownerText))
        {
            return;
        }

        VRCPlayerApi owner = Networking.GetOwner(this.gameObject);
        if (!Utilities.IsValid(owner))
        {
            _ownerText.text = "Owner is invalid.";
            return;
        }

        _ownerText.text = $"{owner.displayName} is owner!";
    }
}

実行

改変したものを実行する以下の通りになります。
自分がインタラクトしている時は自身のセーブされた数値が、他人がインタラクトしている時は他人のセーブされた数値が表示されています。

あとがき

お疲れ様でした。
今回は、PlayerObjectを使ってデータの保存と復元方法について紹介しました。
他人のセーブデータも取得出来るのは驚きました。色々出来そうですね。
それと、PlayerObject自体はセーブ機能以外にも使い道がありそうな気がします。
今後の展開も楽しみです。

次回

次回は、Player Dataを使った例を紹介する予定です。