Programming in VRChat

VRChat でのプログラミングについて調べたことの書き溜め

Persistence(パーシステンス、ワールドの保存機能) / 後編:クリエーター向け解説

(2024/10/11)記事公開後の Open Beta 開始時変化点 この記事の執筆後に開発の進行に伴い、Open Beta 開始時点で以下の変更がされています。

  • Web ページに加えて、にクライアントにもデータを消去する機能が提供されました
    • 全体: メインメニュー > 設定 > デバッグ情報 > User Data > Reset All User Data
    • 特定のワールド: メインメニュー > ワールドの詳細 > アクション > Reset User Data
      • そのワールドの中に居る間は使えません。「動かしながら初期化する」という動作は行えません。
  • Player Object のテンプレートとなっているオブジェクトに Udon からアクセスできるように変更されました。
    • このオブジェクトへの実行時の変更により発生する結果は Udon を書く人に任されています。不用意に変更した場合にシステムが動作保証しない状況や奇妙な挙動は発生しうります。
    • 次の項目 FindComponentInPlayerObjects の為に変更されました(つまり変更ではなく参照の目的)。
  • Networking.FindComponentInPlayerObjects(VRCPlayerApi target, Component referenceComponent) が追加されました。
    • 指定したプレイヤーに対して生成された Player Object の中から、テンプレートの中の Component に相当するコピー生成されたコンポーネントを返します。
    • 個々の Player Object の外から変化を与えたい時の実装がしやすくなりました。
  • ドキュメントの公開位置が https://vrc-beta-docs.netlify.app/worlds/udon/persistence/ になり、内容も説明の追加などがされています。(以下の記事本体はまだ古い方を指しているので注意してください)

注意:この記事は現在(2022/9/20)Closed Beta 段階での情報を元に、公開されたドキュメントを元に執筆しています。リリースされるまでには名称などの表記や APIシグネチャや機能の詳細は変化する可能性があります。 また、執筆時点でドキュメントはリリース版の所には配置されていませんが、記事では配置が予想される URL を仮置きしています。 Closed Beta でのドキュメントは https://vrc-persistence-docs.netlify.app/worlds/udon/persistence/ に置かれています。 Developer Update - 19 September 2024 で URL が公開されました。)

前編では VRChat の Persistence 機能について、一般のユーザー視点での説明をしました。 この後編ではワールドクリエーターやワールドに設置するギミックを作る人を対象として、コンテンツを作成するための情報提供を行います。

この記事はベータ版にあわせて手早く知識を広めるために 「これを読めば使えるようになる」というような手引書ではなく 「要点や注意事項を記したまとめ」を目指して簡素な書き方をしています。 完全な理解のためには公式のドキュメントを合わせて参照してください。 (Closed Beta 期間中の知見を踏まえ「これを読むと公式ドキュメントを理解しやすくなるメモ」を目標にしています)

外面的な機能概要

一般ユーザーにとっての Persistence の機能と特徴は次のようにまとめられます。 前編で述べているのでそちらを参照してください。

  • ワールドにデータ保存・復元機能が提供出来るようになる
  • この機能の利用の仕方(何が、どう保存・復元されるか)は、ワールドの作りに依存する
  • データは、ユーザアカウント毎、かつ、ワールド毎に管理され、サーバに保存される
  • 保存されているデータの削除は VRChat Web サイトで行える

削除ページについて注意:削除ボタンは、ワールドの作者が設定などを行うページではなく利用者として見るページの方に設置されています。

リソース

以下に関連資料などを示します。

  • 公式ドキュメント (執筆メモ:TBD ベータ公開されたらリンクを確認)
    • Persistence
    • ClientSim (Persistence をシミュレートして動作状況を確認する機能が追加されました)
  • サンプルプログラム
    • SDK にサンプルをインポートする「Example Central」というものが作られました。ここからサンプルを Unity プロジェクトにインポートできます。
    • 場所: Unity メニュー > VRChat SDK > Example Central
    • (「Example Central」自体は Persistence に限らず、サンプルへのアクセスを良くするための改善として導入されたもののようです。 表示されるサンプルには Creator Economy に関するものも含まれています。)

実装概要と特徴

この節では、クリエーター(プログラマー)視点での Persistence の実態、特徴、注意点などを記します。

従来のネットワーク同期の仕組みの上に構築されている

「保存と復元の機能」と聞くと、それぞれを提供する save/load メソッドのようなものを想像するかもしれません。しかし現在の実装はそのような形ではなく「自分がオーナーになっているオブジェクトの同期変数を、サーバーを経由してリモートへ送信をするついでにサーバ側で保存する」というような形を取っています。

Udon 同期の通信を行うサーバーには「late joiner のために最終情報を保持していて、プレイヤーが join してきたらそこから送り出す」という機構があります。 Persistence はそこを拡張するような形、つまり他のプレイヤーに同期変数の値が配られるのと同様に、過去の自分から値が送り込まれてくるようになったものと理解できます。「保存が同期と結びついている」というのはユニークな、あるいは少々癖のある設計上の特徴ですが、自然な拡張でもあるように思います。)

なおワールド間のデータの共有手段はさしあたり提供されません。これは同期変数の全体の構成(どの UdonBehaviour が何と言う名前の変数を持っているか)という、ワールド毎に異なる状況に保存形式が依存しているためかもしれません。

(ワールド間でのデータの移行ないしは共有についての質疑で「今は提供されない(Not at this time)」「Not yet, but I agree that it would be cool and useful!」といった表現で回答しているので、将来課題と認識していると捉えておきましょう。データの共有が出来れば続きもののゲームワールドとか、疑似的にユーザーの持ち物(インベントリ)を実装できるので夢が広がるのですが。)

明示的に保存機能に対応して作る必要がある

既存のワールドでデータ(つまり同期変数の値)が自動的に保存されるようにはなりません。 明示的に Persistence 機能を使うようにワールドを作る必要があります。

具体的には、Persistence のために新規に導入されたコンポーネントをシーンに置いたり、Udon スクリプトで保存復元操作を組む必要があります。

(ワールドは従来のまま、ワールドのインスタンスが保存される方式も 検討されたようですが、そのアイディアは問題があって破棄 したようです)

二系統の API が提供される

Persistence は「Player Data」と「Player Object」という二系統の仕組みとして提供されます。 それぞれ利点と欠点があるので状況に応じてどちらを使うかを選びます。 一つのワールドの中で同時に両方を使う事も出来ます。

Player Data はシーン内の全ての UdonBehaviour からアクセスできる簡素な key-value のテーブル(マップ)を提供します。

Player Object は、テンプレートからコピー生成されて個々のプレイヤーに自動的に割り当てられるオブジェクトです。その中の同期変数の値が保存復元できます。

Player Object を使って Player Data 相当のものは作れるはずなので、Player Data はいわば「お手軽便利機能」として用意されています。

居合わせる他プレイヤーからデータが見える

現在の実装では Persistence のデータは、インスタンスに同時に居合わせる他のプレイヤー(のクライアントで実行される Udon)から参照(値を取得)できます。従来の同期変数と同様に、書き込んだり復元された値は居合わせたリモートプレイヤーの元にも届いて読めます

もちろん、他プレイヤーのデータへの書き込みは出来ません。これはオーナーになっていないオブジェクトの同期変数は書き換えできないことと同じ(仕組みとしては、それそのもの)です。

他プレイヤーから読めないようにするということも検討はしたとのことです。 複雑になるので取り止めたという事なのかもしれません。

補足:用語 Persistence

英単語 persist は「粘り強く続く」という動詞です。形容詞形は persistent です。

名詞形 persistence の計算機科学の専門用語としての意味は「終了してもデータが保存される性質」を表します。 日本語では、これは通常は「永続性」と表現されます。 この記事では英語表記のまま Persistence と記します。

参考:

執筆メモ: 一般ユーザー向けの用語は「User Data」にするようです。 Developer Update - 19 September 2024 では「Persistent User Data」という表記を表題に用いています。 Closed Beta で当初はユーザ向けの用語は Player Progress(プレイヤー・プログレス)でした。 2022/9/20 現在で、まだ古い「Player Progress」表記がドキュメントの中に残っています。

Player Data

この節では Persistence で提供される、より簡便に使える仕組みである Player Data について解説します。

Player Data 概要

  • 公式ドキュメント Player Data
  • Player Data は key-value store(キー・バリュー・ストア)」を個々のプレイヤーに対して一つ提供します。
    • key-value store は一般に、マップ、dictionary、連想配列などとも呼ばれる物で、名前(ないし鍵、key)を指定して値(value)を読み書きできる仕組みです。
    • 参考:Wikipedia キーバリュー型データベース (この記事は抽象化した学術的な(?)説明に寄っているので、かえって分かりにくいかもしれないですが…)
  • key
    • key の型は文字列で、任意の文字列が使用できます。
  • value
    • value の型は同期変数で使えるものとほぼ同じです。下に一覧します。 従来の同期変数で使える型と比べて、 配列は byte[] のみ、charVRCUrl は含まれない、という違いがあります。 charVRCUrl のサポートは追加されるかもしれません。執筆メモ:最新状況確認)
    • value の型は動的に決まる方式です。すなわち:
      • 書き込みでは、以前の value の型に影響されず利用可能な任意の型の値を指定できます。型のチェックは無く、以前と異なる型で書き込んでもエラーなどは起きません。
      • 読み出し時には型を意識する必要があります。型が異なると読みだせなかった旨が返される API と、その型のデフォルト値を返す API とがあります(後掲)。

value に使える型一覧:

算術型: byte, sbyte, short, ushort, int, uint, long, ulong, float, double
その他 C# の基礎的な型: bool, string, byte[]
Unity の型: Quaternion, Vector2, Vector3, Vector4, Color, Color32
  • Player Data はシーン内のどの UdonBehaviour からも読み書きができます。よって、一般のプログラミングにおけるグローバル変数のような位置づけになり、同様の利点と欠点とを持ちます。すなわち:
    • key の文字列と value の型さえ合わせておけば、その他の準備の必要無く、任意の UdonBehaviour の間で値のやり取りが出来ます。
    • 書くことが手軽な一方で、注意して使わないと「どこでいつ書かれたのか分からない」という困り事が起きる可能性があります。 例えば意図せず key の文字列が一致してしまって、間違って上書きするというようなことがありえます。

Player Data API

Player Data へのアクセスには、Udon スクリプトで以下の API を使います。

  • 書き込みメソッド
    • static 関数 PlayerData.SetString(string key, string value) などを使います。型ごとにメソッドがあります(左記は String の場合)
    • 書き込みの対象はローカルプレイヤーの Player Data です。
    • この書き込みをするだけで自動的にサーバに保存されます。 同期変数での RequestSerialization のような送出(書き込み)を引き起こすためのメソッドは Player Data には在りません(Set の内部で自動的に実行されている)。
  • 読み出しメソッド
    • 値が存在するかと同時に型の整合性の確認をしつつ読みだす PlayerData.TryGetString(VRCPlayerApi player, string key, out string value) などと、
    • 値が無い時あるいは型が整合しない場合は、型ごとのデフォルト値を返す簡易なメソッド PlayerData.GetString(VRCPlayerApi player, string key) などがあります。
    • (書き込みとは異なり)他のプレイヤーのデータも読み出しできるので、引数に player を指定します。
  • 問い合わせメソッド
    • Player Data の状態を確認する以下のメソッドがあります。
    • 指定した key (と value)を持っているか?: bool PlayerData.HasKey(VRCPlayerApi player, string key)
    • 指定した key の value の型は何か?: Type PlayerData.GetType(VRCPlayerApi player, string key)
    • 指定した key の value は指定した型か?: bool PlayerData.IsType(VRCPlayerApi player, string key, Type t)
  • 準備完了イベント OnPlayerRestored
    • シグネチャ public override void OnPlayerRestored(VRCPlayerApi player)
    • Persistence 機構の準備が整ったことを通知するイベントです。既にサーバにデータが保存されていた場合、その読み込みが完了したことが保証されます。復元するデータが無い場合でも呼び出されます
    • このイベントより前に書き込みを行うと、サーバから復元されるデータによって上書きが起きてしまいます。
    • このイベントは Player Object と共通であり、Persistence の仕組み全体として準備が整ったことを通知するものになっています。
    • 全てのプレイヤーについて呼び出しが発生します。
      • 自分(local player)の join 時に、「自分に関する情報が復元された」ことを伝えるのは当然として、 既にインスタンスに居た全てのプレイヤーについても(各プレイヤーを引数として人数分繰り返し)呼び出されます (蛇足:「local player のクライアントにとって、他者の Persistence の情報が整った」ことを表します。 「他者が他者の立場で整った」ではなく)
      • 自分の後からプレイヤーが join してくると、そのプレイヤー(late joiner)について呼ばれます。
      • 一つの UdonBehaviour に対して何度も呼び出されるので、必要に応じて引数の player を使って適切な処理を行うようにする必要があります。
    • 初回の join の場合や、そもそもワールド保存機能を使っていない場合には復元(restore)するデータは無いわけですが、 場合でもこのイベントは発生します。その意味ではメソッド名に含まれる「restored」は少々不適切です。 persistence ready などでも良かったのでしょうが、分かりやすさを優先したという事のようです。
  • 情報更新イベント OnPlayerDataUpdated
    • シグネチャ public override void OnPlayerDataUpdated(VRCPlayerApi player, PlayerData.Info[] infos)
    • Player Data に更新があった時に発生するイベントです。(なぜか「変化が無い」という通知タイプもありますが)
    • 書き込みが行われると、その当人も含め全員に配信されます。(現在の実装では、書き込んだ値が保持している値と同じ場合には発生しません)
    • 引数の PlayerData.Info から、更新が起きた key と、更新状態(復元された、追加された、など)が得られます。 同時に複数のkeyでの更新が発生しうるので引数 infos は配列になっています。

Player Data 補足事項

  • key-value のうちの一つづつを個々に削除する機能は提供されていません
    • State.Removed が定義されていますが、削除 API が現在は存在しないので実際には発生しない(はず)です。
  • Player Data ではシーン中の全ての UdonBehaviour が一つの key-value store を扱います。 すると、配布するギミックなどの場合に「作者の異なるスクリプトで key に同じ文字列を使ってしまい、互いに読み書きの邪魔をして正しく動かない」ということが発生しうります。 これを回避するには、キー文字列を長くして衝突しないようにします(固有名詞を含むようにするなどして)。 そうしたところで衝突回避の保証は何もないわけですが、これは Player Data のお手軽さとのトレードオフです。
  • Player Data という呼称は Player Object との対比で付けられているようです。 Data は「Object には関連付いていないデータ」程度の意味らしく、実質的にあまり意味は無く特段気にしなくてよいと思われます。

Player Object

Persistence で提供される、より一般的な状況を扱える仕組みである Player Object について解説します。

Player Object 概要

  • 公式ドキュメント Player Object
  • Player Object は、シーンに置いたオブジェクト(とその配下のオブジェクト)をテンプレートとして、プレイヤーがインスタンスに入るたびにコピーを作製して割り当てられるオブジェクト(あるいはそれを実現する機構)です。
  • Player Object の内部に配置された UdonBehaviour の同期変数が、Persistence の保存復元対象データとして指定出来ます。また VRCObjectSync などのシステムが提供するコンポーネントの同期状態も保存復元の対象に指定できます。
  • (Player Object という名前は「プレイヤー毎のオブジェクト」の意味なのでしょう)

Player Object の構成と動作

Player Object のテンプレートの構成の仕方と、実行時の動作について述べます。

  • VRCPlayerObject を追加しテンプレートを作る
    • シーンに置いた GameObject に VRCPlayerObject コンポーネントを追加すると、オブジェクトツリーのそれ以下の部分が Player Object のテンプレートとしてシステムに認識されます。
    • シーンには複数の Player Object のテンプレートを置くことが出来ます。
  • Player Object は実行時にテンプレートから生成される
    • テンプレートになった GameObject それ自体は inactive にされて、まずはシーン上に一つも存在しない状況からワールドは始まります。
    • プレイヤーがワールドに入ってくると(joinすると)そのたびにテンプレートからコピー(clone)が生成されます。生成されたオブジェクトはそのプレイヤーのデータを扱う Player Object としてあてがわれ、オーナーはそのプレイヤーに自動的に設定されます。
  • 保存復元したい部分には VRCEnablePersistence を追加する
    • Player Object 中の保存したい部分には VRCEnablePersistence コンポーネントを付けます。その配下の GameObject が Persistence による保存復元の対象になります。
    • VRCEnablePersistence を置く階層は VRCPlayerObject と異なっていても構いません。コピー生成される UdonBehaviour のうち、一部分だけを保存復元の対象にすることができます。
  • 補足
    • VRCPlayerObjectVRCEnablePersistenceコンポーネントには現在の実装では設定項目などはありません。システムにどう扱ってほしいかをコンポーネントの存在によって示す“マーク”の役割になっています。

Player Object API

  • 準備完了イベント OnPlayerRestored
    • シグネチャ public override void OnPlayerRestored(VRCPlayerApi player)
    • このイベントは Player Data と共通で、Persistence の仕組み全体として準備が整ったことを通知するものになっています。詳しいことはPlayer Data での説明を参照してください。
    • OnPlayerRestored は準備が完了したことを意味しますが、この準備を開始させる(言い換えると復元、オブジェクトのインスタンス生成)のための API は存在しません。プレイヤーが join すると自動で準備が開始されます。
  • 読み書きは従来の同期変数と同じ
    • 保存・復元される変数の定義やアクセスの方法は、従来の同期変数と同じです。すなわち:
  • 全オブジェクト取得 GetPlayerObjects
    • Networking.GetPlayerObjects で、指定したプレイヤーに割り当てられた Player Object が全て取得できます。
      • シグネチャ public static GameObject[] GetPlayerObjects(VRCPlayerApi target)
      • (執筆メモ:更新されたドキュメントをリンクする)
  • 復元動作の判定 isFromStorage
    • サーバーから復元動作として値が渡ってきたことは、デシリアライズのコールバック public override void OnDeserialization(DeserializationResult result) で引き渡されるDeserializationResult を調べることで判定できます。復元時には public readonly bool isFromStoragetrue になります。
    • local player の Player Object では常に自分がオーナーなので OnDeserialization はこの復元動作でしか発生しませんが、他のプレイヤーにとっては初期の復元としてサーバから送られて、そのあとはそのプレイヤーから送られてくるので isFromStorage が変化する事が役に立つことがあります。

Player Object インスタンス生成にまつわる補足

  • テンプレート内への参照は無効、外への参照は有効

    前述したように VRCPlayerObject コンポーネントを追加したシーン上の GameObject はテンプレートして扱われ、Persistence のシステムが取り扱うようになります。

    シーンオブジェクトから、テンプレート内への参照: テンプレートであるオブジェクト(および配下のオブジェクトやコンポーネント)への参照を、 シーンオブジェクトで保持して使うことはプログラムの上では可能ですが、その動作は保証されません。 言い換えると、実行時にテンプレートのオブジェクトはシステム側管轄下に置かれ、ユーザーのプログラムからは活用できません。

    テンプレート内から、外(シーンオブジェクト)への参照: 一方で逆向き参照の使用に制限はありません。 つまりシーン上のオブジェクトやコンポーネントを指す参照をテンプレートの内に保持しておき、 実行時にコピー生成された Player Object から使えます。

    テンプレート内での参照: 当然ながら、テンプレート内のオブジェクト間の参照に制限はありません(そうでないと複数生成される部品が作れませんから)。 コピー生成された Player Object ごとに、それぞれの中で参照関係が保たれます。

  • オーナーは固定される

    Player Object として生成された GameObject のオーナーは、それを生成させることになったプレイヤーに設定され、以降は変更できません。 Start が呼ばれた時点でオーナーは確定しています。(2024/8時点注意:不具合が残っているかもしれません)

  • 生成位置

    Player Object のインスタンス生成は、テンプレートの親に対する相対位置が再現されます。ビルド時のテンプレートのシーン上の位置(ワールド座標)は使われません。つまり、テンプレートの親を動かせば Player Object の出現位置は変化させられます。(ドキュメントされているので書きましたが、Player Object の中で動かせばどのようにもできるので、あまり活用する場面は無いような気はします

  • プレイヤー毎のオブジェクト確保の手段としての利用

    Player Object には動作の最初に「プレイヤー毎にオブジェクトを生成する」ということが行われますが、 この動作は Persistence の保存機能を全く使わなくても利用できます (言い方を変えると VRCEnablePersistence コンポーネントの指定は全く含まなくても Player Object として扱われます)。

    これは例えば、ゲームワールドがプレイヤー毎の競争要素があった場合に必要になったりする 「プレイヤー毎に割り当てつつ同期する GameObject を確保する」という機構が、 「Persistence の提供と共に、ある意味おまけ的に Udon に実装された」と言えるでしょう。

    現在の Udon には「インスタンス生成したオブジェクトの中の UdonBehaviour はネットワーク同期を扱えない」という制約があるために、 そのような同期するオブジェクトが必要な場合は従来は 「対応可能な最大人数分のオブジェクトをシーンにあらかじめ確保しておき、そこから振り出す仕組み」 を用意する必要がありました(いわゆる object pool を用意する必要がありました)。 Player Object はこの面倒事を解消してくれるでしょう。

Player Object 利用パターン紹介

  • ローカルプレイヤーの Player Object を特定して使う

ローカルプレイヤーに対して生成された Player Object を特定して、参照を確保する方法は幾つか考えられます。 ここではその中から簡単に使えて多くの場合に具合がよさそうな方法を検討してみます。

まず当然ですが、Player Object の中に設置した UdonBehaviour にとっては Component.gameObject 自体が Player Object (もしくはその配下の)GameObject を指します。 Persistence のドキュメントでは Networking.GetPlayerObjects を使って GameObject を得る方法が紹介されていますが、 シーンの中に複数の Player Object がある場合には戻り値のとして得られた複数のオブジェクトから、 それぞれが何の役割を持ったオブジェクトなのか(どの Udon スクリプトが付加されたオブジェクトなのか)を特定する必要が生じます。 このためスクリプトの側から Component.gameObject によって参照を得る方が簡単な実装になるでしょう。

次にローカルプレイヤーの物であることを判別することを考えます。 Start の時点ですでにオブジェクトのオーナーは決まっているので、 その時点で Networking.GetOwnerNetworking.LocalPlayer を返すかを確認するのが、 プログラムの最も早い段階でローカルプレイヤーの Player Object を特定する方法です。

ただし、このユーザが既に保存されたデータを持っていた場合には、同期変数はStart の後に保存されていた値で書き換えられます。 この意味で Start の時点では初期化は完了していません。 一般に、何か動作を始めるのには、準備が整ったことを伝える OnPlayerRestored を待つ必要があります。

この OnPlayerRestored は他のプレイヤーの Persistence の準備が整った時、 特に既にワールドに居るプレイヤーについても、ローカルプレイヤーに対して生成された Player Object に対して呼び出されます。 それら呼び出しは自身の初期化に関するものではないので初期化処理を行ってはいけません(でないと複数回の初期化をしてしまう)。

以上を踏まえると、無駄なくシンプルな実装は次のようになるでしょう。

    public override void OnPlayerRestored(VRCPlayerApi player)
    {
        // ローカルプレイヤーの Player Object を特定する:
        // 他プレイヤーの OnPlayerRestored は対象外(1)
        // また自身のものではない Player Data に伝えられる OnPlayerRestored は対象外(2)になる
        // ド・モルガンの法則で、否定と論理和をひっくり返して、以下
        if (player.isLocal // 1
            && (Networking.GetOwner(gameObject) == Networking.LocalPlayer)) // 2
        {
            // gameObject や this を使ってなんやかんやする
            // 保存された値を使って自身を初期化したり、
            // シーンオブジェクトに対してこのオブジェクトを登録するなど
        }
    }
  • シーンオブジェクトからローカルプレイヤーの Player Object を呼び出す

典型的な Player Object の利用形態として「Player Object を操作する UI をシーンに置き、操作する」というのがあるでしょう。 この時、シーン上のオブジェクトである UI コンポーネントイベントハンドラからは Player Object の参照をビルド時には特定できないので、 実行時にこれらの間をつなげる必要があります。

実装するパターンとして以下の形が考えられます。 (分かる人向け:つまりは Proxy パターンを構成するのが有用だろう、という話です)

  UI component event handler          // 例えば Button の OnClick()
     => proxy_object.SomeMethod()     // シーン上のオブジェクトとして置いたもの
       => player_object.SomeMethod()  // ローカルプレイヤーに対して生成された Player Object

proxy_objectplayer_object は、それぞれのスクリプトをアタッチした UdonBehaviour です。 UI component から proxy_object への呼び出しは、通常のシーン上オブジェクトの呼び出しです。

proxy_object から player_object への呼び出しの為の参照は実行時に得る必要があります。 これは前述の「ローカルプレイヤーの Player Object を特定して使う」で紹介した方法を使って、 player_object が自身を検出して proxy_object に登録することで参照を初期化できます。

proxy_object には、UI から呼び出したい player_object のメソッドと同じ名称のメソッドを作ります。 そのメソッドの中身は上述の player_object への参照を使って同名メソッドを呼び出すだけにします。 proxy_object のプログラムはなるべく定型的に“橋渡し”の為の内容だけを書いて、複雑な事は player_object 側に書くようにすることをお勧めします。

(蛇足:両者は相互依存関係になり一般にそれは望ましくないことですが、設計的には不可分の一体のものと考えれば良いでしょう)

  • おまけ:VRCObjectSync + VRCEnablePersistence

VRCObjectSync を含む Player Object を作り、さらに VRCEnablePersistence を加えると、Udon を書かずに「位置同期して、次回join時に復元されるオブジェクト」を作れます。

これだけでは実用的な意味はあまりありませんが、既存の同期の仕組みの上に Persistence の保存が実現されていることが、 よりはっきりと理解できるかもしれません。同期機能を持つシステム提供のコンポーネントは同様に振る舞うと予想されます。

ちなみに Player Object のオーナーは他者に移せないので、 上記の物にさらに VRCPickup を付けて持てるようにすると「各自、自分は持てるが他のプレイヤーは持てない物」になります。

ClientSim によるサポート

ClientSim は Persistence のシミュレーション動作をサポートします。

  • ClientSim のプレイヤーのスポーン操作に従い動作します。
  • データはファイルに保存されます。ファイルは Unity プロジェクト直下の ClientSimStorage フォルダの中に格納されます。(Assets の中ではないので注意)
  • Player Data の内容を確認する画面があります Unity メニュー > VRChat SDK > ClientSim Player Data
  • Player Object が保持しているデータは専用のコンポーネントで確認できます

注意:ClientSim のシミュレート動作の実装はまだ完全ではないかもしれないです。 (執筆メモ:オープンベータになり次第、最新状況を確認する)

補遺:制約事項

Persistence を使用する上で設けられている、制限・制約について述べます。(参考: 公式ドキュメントの記載

  • 容量制限
    • 一つのワールドで保存できる容量には制限があります
    • Player Data と Player Object それぞれで、圧縮した状態で 100KBです。(圧縮はシステム側が行います)
    • 制限を超えた場合はログにその旨のメッセージが表示されます。 (執筆メモ:スクショを貼り、文字列をここにコピー) (ログのみではつらいので、プログラムで扱えるように提案がされています)
    • Player Object ごとではなくワールド全体なので、ワールドギミックの prefab を様々入れている場合には注意が必要です。 特に、制限を超えて期待するように動かなくなったギミックが容量を取っているわけではなく、たまたま最後に保存を行ったモノがそのようになります。 このため、原因を特定するのが難しくなる状況があるかもしれません。
  • 保存タイミングの制約
    • プレイヤーがワールドを離れようとし始めると、Persistence の保存はもう行えません。
    • 特に OnPlayerLeft の中では保存を行えません。
  • local test
    • (執筆メモ:ローカルテストにおける動作について最新情報を反映する。Closed Beta 中でローカルテストは不完全だったので詳細を書いていません。ドキュメントには動作させようとしている旨が書かれてはいます)