このブログのカテゴリ(記事分類)
あまり綺麗に区分けできていませんが、次のように記事を分類しています。
技術的要素の解説を中心とするもの:
- component VRChat SDK で提供されているコンポーネントの解説
- trigger VRC_Trigger で扱われているトリガーの解説
- stdassets Unity Standard Asset の解説
- unity (VRChat と直接は関係のない) Unity の解説
記事の情報提供のスタイルによるもの:
- guide VRChat の使い方や用語などの解説
- recipe 「こういう構成はこう作れますよ」方式の作り方紹介。解説には重点を置かない
- howto 「~をするには、こうすればよい」の問題解決指向な記事
- tips ちょっとした「こうすると、こうできる」な内容
- demo 機能のデモンストレーション。(guide, mywork との使い分けが微妙。廃止?)
- data 資料集的な、記載されているデータの値に意味がある記事
筆者自身の活動に関するもの:
補足:
- あまり厳密には付けられていません。試行錯誤中です。
- 記事の URL の最初の部分が記事の主分類のつもりです。
- 主分類にはしないが「それだとも言えなくもない」場合には、 カテゴリ一覧で見る人の便利のためにタグ付的な感覚でさらにカテゴリを付与しています。
Persistence / アイディア雑多メモ、利用案と技術的考慮ポイント
この記事では前回の記事を踏まえ、 仮公開された closed beta 段階の Persistence の仕様と、そこから各所で起きた会話をもとに、 取り留めなく「こういうことが出来るかも」というアイディアを並べつつ、その技術的側面を雑多に検討してみます。
- ワールドの環境設定
- ゲームワールドでのスコアやコイン
- インフレ問題
- スコア・ランキング
- あてのないデータ伝搬
- カードゲームのデッキ
- ステージ調光
- 家具を配置する、部屋自体を組み立てる
- シーンオブジェクト か Player Object か
- Player Objects と Player Data の比較
- パブリックな共通使用されるキー
- コラボレーションワーク
- 書いている途中の絵
- 保存容量
- 容量が共通である問題
- Player Object の late joiner での動き
- クロスプラットフォームでの注意
- ワールド間での復活の呪文方式
- 新しいワールドを作り、跡地を残してエクスポート
- 自前シリアライズとバックアップとしての Player Data
- 終わりに
(この小さい文字の個所は、本筋から離れる話です)
この記事は幾つかの目的を念頭に書いています。
- いくらか具体的な利用シーンを提示することで、仕様の理解の助けとする
- 仕様に関して誤解が起きそうな個所について、補足の説明をする
- アイディアの種をまいて、誰かに良きものを作ってもらいたい(という魂胆)
- ユーザー観点での価値の話と実装技術の話とを雑に行き来して隙間を埋めてみて、機能的に不足していることを探り VRChat 開発にフィードバックにつなげる
これから作られるものについての話なので「こうだろう」「こうかもしれない」という調子に全体的になってしまいました。 実際にどうなるかは開発の行方と利用の状況次第で変わってくるので、そこはどうそ話半分で読んでください。
ワールドの環境設定
Persistence の想像しやすい活用例は、ワールドに設置した鏡やソファーのコライダーのオン・オフといった環境設定でしょう。 それらが初期値が自分の好みと異なる場合は、ワールドに入るたびに今は切り替えていると思いますが、Persistence を使うと前回訪れた状態に復元できるようになります。
こういった各プレイヤー毎の(つまりローカルな)設定と、Persistence の「プレイヤー毎の記録」というのは素直に対応づけられるでしょう。
インスタンスに人が入ってくるたびに、次々とミラーのスイッチへ寄っていく姿はそのうち見られなくなるのかもしれません。
ゲームワールドでのスコアやコイン
「ゲームワールドでのゲーム後の各自の記録」といったデータも分かりやすい応用例でしょう。 そこで扱うデータの種類には
- 成績:ハイスコア、クリアタイム
- 進行度:訪れた場所、実績(アチーブメント)
- 資源量:コイン(通貨)、累計撃退数
といったものがありそうです。
ユーザー視点で言うとこれは「やりこみ要素」であり、ワールド・クリエータ視点では「再訪してもらう切っ掛け作り」になりそうです。
インフレ問題
ゲーム内コインを保存できるようにした時の課題に関して「定期リセット」や「上位にリスクを負わせる」といったアイディアが話されています:
強者を強者のまま固定させると退屈だからワールドで1位の人にはなんらかの方法で都落ちリスクが発生するとバランスがいいかもしれない。強制的にダイスを握らせられるとか、定期的に青の甲羅が飛んでくるとか(狂気の実装) https://t.co/CbqJiT0u0U
— るら/VRC (@Lu_Ra_999) 2024年9月20日
こういった事を考えると、獲得したコインなどのように「時間経過で概ね増えていくように作られている量の記録」は、 長期的な「楽しさ」の観点ではそのまま安易に記録しない方がいいのかもしれません。
一般に様々なゲームにおいて言われている事だと思いますが、次第次第に増える量は「長くプレイされる中で希少性が薄れていくインフレ問題」が起きます。 特にオンラインゲームでは開始時期が異なる人が同じ場所に集うので、扱いが難しくなりがちです。
(現実の経済では時間経過と共に新規参入者が取得する量も増加することである程度調整される訳ですが。 ゲームの場合は行為から得られる報酬が往々にして固定的なので、何から別途の積極的な調整機構が必要になるということなのだと思います。)
スコア・ランキング
クリアタイムやスコアがあるならば、他プレイヤーの成績と比べてランキングを掲示したくなるでしょう。 プレイヤー各自が Persistence に保持している過去のベスト・スコアは、ワールド内の他プレイヤーからも参照できるので 「今のインスタンス内でのランキング」を作るのは容易です。
一方、Persistence の仕様は個々のプレイヤー毎にデータを保存するものなので、 「そのワールドを訪れたすべてのプレイヤーでのグローバルなランキング」というのは作れません。 (外部にサーバを作って、そこに送り込むようなことをすれば可能ではありましょう。それは従来と同じです)
ところで。「同じインスタンスに居た他プレイヤーのスコア」は、コピーして自分が保持するデータの一部として記録しておけます。 それを使うようにすれば「出会った事のあるプレイヤーの記録での中での、自分の最新記録の順位という形のランキング」を作るのは難しくないでしょう。
さらに。その「出会った事のあるプレイヤーの記録」を他プレイヤーが参照すれば、 「自分が出会ってはいないプレイヤー」も含めたランキングが作れます。 さらにそれをコピーするようにして他者を介して記録を受け取るということを繰り返せば、 「真にグローバルではないが、より広い範囲でのランキング」というのは Persistence 機能の範囲内で作れそうです。
あてのないデータ伝搬
前述の「他者を介したデータの伝搬」というのは、ちょっと面白げなアイディアです。
プレイヤー視点では「他者を介してデータを受け取れる」ということをになるわけですが、 これをデータ視点で捉えると「偶然インスタンスに居合わせた人の繋がりを乗り継いで、誰かの元にたどり着くことが出来る」とも理解できます。
イメージとしては「ワールドに行ってみると紙切れに誰が書いたのか何か書いてあって、それを見たり書き加えたり出来て、ふたたび放流したものがまた誰かの元にたどり着く」 といった感じのことが作れそうです。 どこに到達できるかは全く分からないので実用性は全くないけど、何か楽し気な可能性を感じます。
ワールド内での表現もたとえば、掲示されている絵、置いてあるノートを手に取って開いたページ、海岸に堕ちている小瓶の中の紙片、などなどいろいろありえそう。
技術観点の検討ポイントの思い付きメモ:
- データ伝達の寿命をどうするか、
- コピー(ないし再放流)が自動的か選択的に人が介入するか、
- コピー派生数を制限するか、
- データが常に見えるかどうか(あるメッセージについては自分は運び屋で見えない、というパターンも面白そう)
カードゲームのデッキ
既に述べたゲームのスコアは遊んだ後のデータであるのに対して、 カードゲームにおけるデッキ構築のように、ゲームの事前準備を保存しておける機能も考えられます。
「事前の準備を整えること自体を遊びの構成要素にする」 「ゲームの過程で武器やパーツを取得出来て、それをカスタマイズして保存して再訪時に使えたり、さらにワールド内で交換出来るようにする」 といったことが出来そうです。
より凝った武器カスタマイズのようにプレイヤーの自由度を高めようとすると、 これは結果的に「ある種の編集保存機能」を作ることになるのかもしれません。
ステージ調光
既存のワールドで、VRChat 内で音楽ライブや演劇の舞台でライトの演出をすることがあると思います。
現実のステージの光の演出は「演目の場面ごとに光を照らすパターン(どのライトを、どこに向け、強さや色や絞りをどうするかなどを調整した状態)をあらかじめ作っておき、 演技進行状況に応じてタイミングを合わせてそれらを使う」という事をしています。 Persistent の保存により同様の事を「ワールド自体を更新することなく、演目に合わせて作り出す」というように出来そうです。
DJイベントを行うようなワールドを作っている人から「長居しているとワールドに作りこんだ光の演出に飽きてしまう」という話を聞いたことがあります。 「ワールド自体を更新することなく演出を変えられる」というのはそういった場面で役に立つかもしれません。
ところでこれも前述のワールドに組み込まれた「ある種の編集保存機能」の別の形と理解できるでしょう。 より抽象化して言うと「ワールドにある仕組みを使って『作品』を作り保存して発表する」というワールドの形(スタイル)ということなのかもしれません。 (これまでも「音楽のリズムパターンを作って遊ぶ」といったものはありました。それをさらに広げていく感じと言えましょうか。)
家具を配置する、部屋自体を組み立てる
「ワールドの環境設定を保存できる」というところから膨らませてみると「ワールドに置いた小物や家具の位置を復元する」という応用が想定できます。 インテリアをアレンジするようなイメージです。 また、壁や扉や階段自体を配置できるようにすれば、部屋自体を組み立てるようなものも作れるでしょう。
VRChat が以前投稿していた次の動画の4秒からの部分は「壁やドアを配置して、入りなおして再構成されている」ことをデモしています。
Persistence is coming soon!
— VRChat (@VRChat) 2024年3月28日
What will you make in #VRChat ? pic.twitter.com/OFeNPjbcZk
この手の応用では「『Persisntence は各プレイヤー毎の記録である』というのをどう扱うか」という検討課題がありそうです。
素朴に「Player Objects として位置保存される家具」として作ると、 複数のプレイヤーがインスタンスに入ってくると「物の位置が重なってしまう、あるいはコライダーが入っていれば吹っ飛ぶ」という事が起きてしまいます。 またそうではなく「家具をシーンオブジェクトとして作る」と物の重なりは発生しなくなりますが「誰の記録を使って復元するのか」という課題が生まれます。
この解決には幾つかのスタイル(ないしパターン)があるでしょう。
完全ローカル
- シーンオブジェクト、かつ位置同期しないローカルなものとする
- 自分が見ているのは自分の記録に従った配置。プレイヤーは互いに違う様子を見ている
- 各自が好きなようにはできるけど、少し寂しいし面白味は少なそう
各自のスペースに復元
- 個々のプレイヤーに空間(ワールド内の部屋やブース)が割り当てられて、そこに限って Player Object として作ったモノが置けるようにする
- つまり、プレイヤー毎に場所があてがわれるので、他者と空間的にかぶる問題が解決される、という案
- 各自が自分の部屋をワールドの中に持っているようなイメージ
- プレイヤーがスペースを越えて移動しようとした時には「移動を許さない」か「その場合に限って復元時には初期位置に戻す」といった事が必要そう
インスタンス開始時にのみ復元
- インスタンスを開いた後に最初に入ったプレイヤーが持っている記録に従って復元する
- イメージとしては「その人の部屋に行く」というような感じ
- 保存についても選択肢がありそう
- 自分が復元した時にのみに保存する(他人の部屋での体験は保存されない)
- 常に保存(復元されなかった自分の記録は上書きされる。最後に体験したそのワールドのインスタンスの様子を持っておく、感じ)
- UI を配置して明示的に保存操作をする(半固定的に使う場合にはこれでもよさそう)
明示的にユーザーが復元操作をする
- 前記の「インスタンス開始時にのみ」のバリエーションとして、明示的な復元操作を備え付ける案
シーンオブジェクト か Player Object か
前述の「物の配置」の応用を考えてみると、 「復元対象が全てのプレイヤーが共通に扱うシーンオブジェクトなのか、個々のプレイヤーに割り当てられた Player Object か」によって、 全体としてどのように受け止められるものになるのかが、だいぶ異なったものになりそうです。
またここから「記録が個々のプレイヤーごとである」からといって、必ずしも「対応するモノが Player Object になるというわけではない」ということも分かると思います。
そういったことは「ユーザーにどのように見える(ないし体験する)ものに仕立て上げるか」次第であって、作りによって異なるものになるでしょう。
Player Objects と Player Data の比較
Player Objects と比べた時の Player Data の特徴は
- 実装に必要な手数が少なく、お手軽である
- データの読み書きは関数を呼ぶだけで行えて、他の準備はいらない
- システムが用意している物なので、失われない
- 対応する同期変数が無くなっただけでデータが失われる Player Object と違い、読み込まれなくなってもデータは存在し続ける
- 全データの取り出し(ないしトラバース)を書くのが容易
- 事前にキーを知らなくても
OnPlayerDataUpdated
によって取り出しが出来ます。一方 Player Objects はGetPlayerObjects
で取り出せますが、その中から保存データのみを取り出すのは容易ではありません。
- 事前にキーを知らなくても
でしょう。欠点は何より
- キーの衝突によってデータが失われる事故が起こりうる
です。
汎用性のあるギミックの場合は、 ドキュメントにも書かれているように Player Data ではなくて Player Objects を使うのがお勧めです。
特に同じギミックをシーンに複数置くことがありうる場合、Player Data を使っている場合では何も工夫しないと同じキーで読み書きしてしまうので個別動作が出来ません。 キーの値を外部から設定できるようにしたり、エディタ拡張で辻褄を合わせるようにすることも可能ではありましょうが、 ワールド作成者に適切に運用してもらうという難しさを背負い込むことになります。
Player Objects ならば同じU#クラスを使っていても network ID によって個別に識別されるので、重複問題は発生しません。
なお、記録の対象が「シーン上に一つしか存在しえないもの」ならば「あえて Player Data を使う」という選択はありかもしれません。
パブリックな共通使用されるキー
(前項の「あえて Player Data を使う」から繋がる発想)
キーの衝突問題(別に作られた Udon スクリプトでも、同じキーなら同じ部分を読み書きしてしまう)を逆手に取れば 「キーの文字列と型に何を使っているかを公開して、別々に作られた Udon が共通のデータを読み書きする」ということが可能でしょう。
特定のキーを共有読み書きメモリとして、ある意味で「Udon の間での疑似的な通信」を作り出せます。
いまひとつ現実的な応用が思いつかないので、これは仮想的な単純な応用ですが例えば「ワールドの中の家電(を模したもの)の電源スイッチを集中制御するスイッチ」が考えられます。
- 個々の家電は、電源の状態で動作ランプが点灯するなど何か異なる表現をする(と仮定して)
- 別々の作者が作ったそれらがワールド内に複数ある
- 集中制御するスイッチがある。それが「電源」というデータに On や Off を書き込む
- 家電の方はそれを参照することで一斉に On/Off の表現が変化する
キーを決めておく以外は、事前に準備が特に必要なく組み合わせ動作が可能になります。
これは Persistence の「保存する」という所は使わずに Player Data の「共通に読み書きする基盤」のみを使うということになります。 まあ、意味があるのかないのか分からないアイディアレベルの話です。
コラボレーションワーク
前述の「家具を配置する、部屋自体を組み立てる」の応用を一般化して考えてみると、それは 「複数のプレイヤーでワールド内で何かを協力して組み上げる。その途中の、あるいは完成した作品を保存する」というジャンル(?)になりそうです。
繰り返しになりますが、この場合もまた「データはユーザごとで保存」という仕様に対して、 「各人が個別の範囲のみを受け持って次回の時に誰かいなくても成立するようにする」「互いの成果をコピーしあう」といった仕様の検討と工夫が必要になるでしょう。
ところで。
このアイディアは面白そうです(これはゲームワールドのたぐいを想定していそうですが)。ロックしたときにいたプレイヤーが全員集まらないと開かない扉
— ねむり木 (@Name1ess_Newbie) 2024年9月20日
つまり「誰かいなくても成立するように」ではなく「全てのメンバーが居ないと作業が再開できない」ようにする逆転の発想もありかもしれません。
書いている途中の絵
前述の「作業の途中を保存する」と言えば、 もっと素直に「ペンで絵を描いている途中の状態を保存する」という応用がありそうです。
作業の途中でワールドから抜けることが出来るようになれば、大作を作るのが容易になるかもしれません。
保存容量
Persistence で保存できる容量は、 それぞれのワールドそれぞれのプレイヤー毎にシステムにより圧縮された状態で Player Data と Player Object それぞれについて、最大 100 KB という事になっています。
これは実地に基づかない完全なる想像ですが、この値は 「設定値を保存するようなものでは十分だろう」 「画像やペンの線を保存する応用のような、データ量がいくらでも増えうるものでは、まあ、厳しいだろう」という気がします。
あるギミックについて容量が問題になってきた場合には「同期の必要性と保存の必要性を吟味して、それぞれにUdonBehaviour を分離する」ということも必要になるかもしれません。
容量が共通である問題
前述の容量の制限は「総量」であって「ギミックごとではない」というのが問題を起こすかもしれません。
いま想像できる問題シナリオは例えば 「ワールド作成者が、他の人が作ったギミックを組み込んでいって、容量制限に出くわした時に、いったいどれが原因になっているのか分からない」 という状況です。 ギミック作成者は自分が作っている物がどのくらいの容量を消費するのかは分かっているでしょう。 しかし、それを利用しているだけのワールド作成者は必ずしもそうではありません。
また現在、消費容量を的確に知る API は提供されていません。 このため、仮に保存失敗のエラーをギミックが上手く表示できたとしても、それはたまたま最後に容量を越えた保存を試みた時に失敗するのであって、 原因となっている容量を大幅に消費しているモノが分かるわけではありません。
そもそも VRChat 開発チームがここの所、すなわち「実態として分業が起きているので、ワールド作成者は必ずしも詳しくない」という事情をケアしてくれるかが心配です。 今のところ気遣っている感じがありません。
Player Object の late joiner での動き
後からワールドに入ったプレイヤー(late joiner)の下で、join 時に Player Object がどう振る舞うのかが若干分かりにくそうなので解説を加えます。
この場合は join した時に自分(=local player)の分だけでなく、既にいる全員分の Player Object のインスタンスが一斉に作られることになります。
もう少し詳しく見ると: 既にワールドに居るプレイヤーが同期処理したデータはサーバに保存されています。 late joinner の下では、自分を含めその時に居る全員のプレイヤー分の Player Object が生成されます。 そこに前回保存したデータがサーバから初回の同期として流し込まれます。
従来の同期でも「late joinner に対してサーバが owner の代理で前回同期のデータを保持(キャッシュ)していて、そこから送り込む」 という動作をしていました。つまり Player Object についても生成以外は同じ動きになっていて、従来の機構の自然な拡張になっています。
クロスプラットフォームでの注意
PC、Android, iOS の各プラットフォーム間で、
Player Object の保存される部分(つまり VRCEnablePersistence
以下)の同期するコンポーネントを異なる構成のものに差し変えると、
「一旦別プラットフォームで入ったら記録が消えてしまった」という現象を起こしてしまうので注意が必要です。
(これはあまり無い事だとは思いますが、念の為に注意喚起として記しておきます)
ワールド間での復活の呪文方式
前の記事に書いたように、さしあたりワールドの間での記録を共有する機能は提供されません。
一方、Persistence が導入される前でもゲームの進行などを保存する方法として、 いわゆる「復活の呪文」方式すなわち「ワールド内のUIでテキストが提示され、それをコピーしてどこかにとっておき、あとで戻ってきた時にそれを入力すると状態が復帰される」 という仕組みを備えているワールドがありました。
ワールドの間での記録の共有は、これに習って 「ユーザーに文字列をコピーしてもらい、ワールドを移動して移動先でペーストしてもらう」 という方法で実現するしかなさそうです。
想像するに、これには、個々のギミックが提供して「同じギミックを備える別ワールドに設定を移植する」といった状況と、 「同じ作者が別のワールドにデータを引き継ぐ」(ゲームワールドで別の章に行く、のような感じ)といった状況があるかと思います。
新しいワールドを作り、跡地を残してエクスポート
前記の「復活の呪文によるワールド間移植」の極端な(?)状況として、 「ワールドを大きく作り変えたくなった場合の最終的な手段」としての活用がありえます。
すなわち「ワールドを公開した後に少しづつ作り変えていったが大きく作り変えたくなったので、別ワールドとして作る。 ユーザーが持っているデータを殺さないために、古い方は跡地として残して設定の取り出し(エクスポート、復活の呪文への書き出し)の機能だけを残して、 新しいワールドへのポータルを置く」という状況です。
自前シリアライズとバックアップとしての Player Data
繰り返し述べましたが Player Data には Player Object と比べると「システムが用意している物なので明示的に消さない限り安定的に存在し続ける」という、 目立たないながら重要な性質があるように思います。
一方で「Player Object には、長期間改変しつつ維持したいワールドのメンテナンスにおける扱いの難しさがある」という点、 さらに容量問題、保存スロット機能の実装(一般のゲームで複数のセーブを別途持てるようなやつ)、 ワールド間共有といった課題を考慮すると、 先に書いた「複雑な事をしようとすると結局、復活の呪文方式の export/import からは結局逃れられないのか?」というようにも思えてきます。
ところで。それらをミックスすると「Player Dataを安定したバックアップとして使えばよいのでは?」というアイディアが浮かびます。
つまり
- Player Object に対して、独自のシリアライズ・デシリアライズ処理を作る
- 適度なタイミングでシリアライズし Player Data にバックアップとして書き込む
- 操作に誤ってデータを失ってしまった時など、前記バックアップをデシリアライズして Player Object を復活させる
という事が出来るのでは、と。
「自前でシリアライズ・デシリアライズする」というのはなんだか本末転倒感がとても酷いわけですが、そこはツールの助けなどでカバー出来うるでしょう (もしもその方式が広まるのであればツールなどが整備され個々人の手間は問題にはならないでしょう)。 またこのシリアライズ・デシリアライズは、保存スロット機能の実装、コピーペーストを介したワールド間共有にそのまま使えるので、 これはその意味でのメリットもある(別の言い方をすると手間に対する価値の回収もできる)ものになる可能性があるように思います。
終わりに
以上、Persistence が導入されることで起こり得そうなあれこれを考えてみました。
Persistence は「同期変数を拡張することで、同期のついでに保存を行う」というやや奇抜な設計ではありますが、 仕組みとしては従来の仕組みと破綻なく馴染んでいて上手く行っているように思えます。
一方で「一旦公開したワールドを変更して行った時に起こりうること」、 特に「ギミック作者とワールド作者が別で組合されていった時に起こること」といった、 開発の営みを補助する部分が足りていないようにも感じます。
実際に我々ユーザーが使って(作って)いく中で、それら問題を見付けて解決したり、開発への課題のフィードバックをしていく必要がありそうだと思います。
次に記事を書くのであれば、そういった課題への解決策パターン(プログラムの組み方、メンテナンスの仕方、対処方法)などを収集してみると良いのかなと考えています。