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を作る方法は以下の通りです。
- 一番親のGameObjectに
VRCPlayerObject
コンポーネントをアタッチする VRCPlayerObject
コンポーネントをアタッチしたGameObjectより子の中で、データを保存しておきたいGameObjectにVRCEnablePersistence
コンポーネントをアタッチする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を使った例を紹介する予定です。
Persistence入門 - 基礎知識編
こんばんは。ハツェです。
久しぶりに面白い機能が追加されるので、紹介しようかなと思います。
動作環境は、Unity : 2022.3.22f1、VRCSDK : 3.7.2-persistence-beta.1です。
目次
Persistenceとは
Persistenceとは、VRChatでデータを保存してそれを復元することが出来る機能です。
データはローカルに保存されずにVRChat上のサーバーにアップロードされる形で保存されるため、同一アカウントであれば別クライアントでも同じワールドにJoinすることでデータを引き継ぐことが出来ます。
しかし、現状はワールドインスタンス間でのデータ共有機能はないみたいです。今後実装されるといいですね。
また、公式ドキュメントではアップロードするデータのことを「User Data」と呼んでいるため、本記事でもこれに倣っていこうと思います。
vrc-persistence-docs.netlify.app
Persistenceの必要性
後述するデータ強制削除不可の性質上、本当にPersistenceが必要かどうかは考えてから導入することをおすすめします。
もし、一度Persistenceを導入した場合、以降の開発ではセーブデータの互換性が取れるようにしていかなければなりません。ある程度の品質を求める場合に限りますが...
ですので、Persistemceを使わなくても大丈夫そうな場合には、その方法を用いた方が良いかもしれません。
User Dataの消去
Udon側からUser Dataを消去する方法はありませんが、ユーザー自身がUser Dataを消す方法は幾つか存在しています。
全User Dataを消去する場合
クライアントからは、メインメニューの設定タブを開き、デバッグ情報ページにある「Reset All User Data」を押すことで全てのワールドのUser Dataを消去することが出来ます。
Webからは、画面上部の歯車アイコンを押し、User Data欄の「Reset All User Data」を押すことで全てのワールドのUser Dataを消去することが出来ます。
特定のワールドのUser Dataだけ消去する場合
クライアントからは、そのワールドにJoinしていない状態でワールドページを開き、「Reset User Data」を押すことでそのワールドのUser Dataを消去出来ます。
Webからは、そのワールドのページを開き、「Reset User Data」を押すことでそのワールドのUser Dataを消去出来ます。
Persistenceの種類
Persistenceには、二種類のAPIが存在します。
以下、それぞれについて紹介しますが、なるべくPlayer Objectを使用することを推奨します。
- Player Data
- Player Object
Player Data
Player Dataとは、キーバリュー型でUser Dataを構築し、それをVRChatのサーバーに保存するような仕組みのAPIです。
Player Data自体は、Udon Behaviourからは独立しているため、どのUdonからも利用可能ですが、データが送られてくるタイミングは RequestSerialization
になっています。
また、Player Dataに保存出来るデータの型は、下記公式ドキュメントに記載されているものになります。
vrc-persistence-docs.netlify.app
キーとそのデータを追加するだけで保存出来るため、簡単に保存データを追加出来るというのがメリットです。
しかし、キーが重複した場合にデータが意図せず上書きされてしまったり、キーが削除出来ないという性質上、使用しなくなったキーが大量発生してしまったりするデメリットもあるため、アップデートを逐一行う規模の大きいワールド制作や頒布物、販売物への利用には適していません。
とりあえず、自分のワールドで保存ギミックを軽く扱うぐらいの場合にのみ使用することをお勧めします。
Player Object
Player Objectとは、GameObjectの状態そのものをVRChatのサーバーに保存するような仕組みのAPIです。
そのため、GameObjectにアタッチしているUdonの同期変数はもちろん、VRCObjectSync
のようなコンポーネントの同期状態も保存出来るようです。
Player Objectは、NetworkIDをキーにしているようなので、別のGameObjectにしたりNetworkIDを意図的に別の値にしない限り、データは保存され続けることになります。
そのため、Player Dataで問題になる「キーの重複」が頒布物のインポート等で起こることがなく、ワールドをアップデートしていく過程においても比較的保全性があるため、基本的にはこちらを使用することを推奨します。
データのアップロード制限
現状は、Player Dataで100KB、Player Objectで100KBまでそれぞれアップロードすることが出来ます。
これを超えた容量をアップロードしようとした場合、アップロードだけは失敗するため、次回以降のJoin時にデータが復元出来なくなります。
また、容量を超えたアップロードをした際、現状は特にエラー等が吐き出されたりしないため、自前で容量チェックする必要がありそうです。
容量系のイベントを追加してほしいというCannyは上がっているので、投票しておくといいかも...?
feedback.vrchat.com
データの復元と保存タイミング
データが復元するタイミングは、ユーザーがインスタンスにJoinした後の OnPlayerRestored
イベント発生時です。
復元させるデータ量によっては、Start
や OnPlayerJoined
よりも後に OnPlayerRestored
が発生する可能性もあるため、保存データを扱う際は OnPlayerRestored
以降で扱うようにする必要があります。
また、データが保存されるタイミングは、Player DataはキーにデータをSetした時、Player Objectでは同期変数の値を更新して再同期させた時になります。
ワールドJoin時は自動でデータを復元してくれますが、Left時には自動でデータを保存してくれないため、注意が必要です。
次回
基礎知識としては以上になります。
次回は、Player Objectを使った例を紹介していきます。
hatuxes.hatenablog.jp