これは Cluster Creator Kit Advent Calendar 2020 1日目の記事です。
こんにちは、はとりんです🐦
クラスターのソフトウェアエンジニアで、 Creator Kit の開発とかをしているハトです。
今年のアドベントカレンダーは Creator Kit だけで25枠が埋まってとてもうれしいです。
あとは無事完走できるといいなあと思っているので、よろしくおねがいします。
さて、この記事では、たびたびクリエイターの皆さんを悩ませてしまっている Creator Kit の同期について書いていきます。
前半は cluster がどのようにワールドの状態を同期しているかの概要を解説していきます。かなりの上級者向けになってしまっているので、よくわからなければ読み飛ばしてもらっても大丈夫です。
後半は、いくつかのケース別に同期に関する問題とその解決方法について書いていきます。こちらは Creator Kit の Item や Trigger, Gimmick を使ったことがある方であれば読める内容になっているかと思います。
Creator Kit の同期に関する考え方
Creator Kit は、クリエイターが同期のことを意識せずにマルチプレイゲームを作成、公開できることを目指して開発しています。なので、普通に作っていたらよしなに同期される、というのが Creator Kit の基本的な動作です。
一方で、現在その理想を完全に実現できているわけではなく、すべてを一朝一夕に解決できるものでもないため、この記事を公開するに至っています。しかし、このような記事が一日でも早く廃れることを目指して、日々開発を進めているということはお伝えしておきます!
また以下の内容は、 2020/12/1 現在の実装を解説するものであり、予告なく変更することに注意してください。長期的にサポートされる仕様は https://docs.cluster.mu/creatorkit/ です。
ワールドの状態の同期
Item の Ownership
すべての Item には Owner がいます。
ワールドに一人しか Player がいないとき、すべての Item の Owner はその Player です。
Create Item Gimmick によって作られた Item は、その Create Item Gimmick がついている Item の Owner が Owner になります。
Item の Owner は変わることがあります。
- Interact Item Trigger を設定した Item を使った時、使った人に
- Grabbable Item を掴んだ時、掴んだ人に
- Item の Owner である Player が退室したり、その Player の通信が途切れた時、そのワールドにいる誰かがランダムに
Ownership はクライアント(ここでは各プレイヤーのPCやスマホで動作している cluster アプリを指します)がサーバーに要求することで割り振られ、各 Item の Owner がどの Player であるかの情報は定期的にサーバーから通知されます。この頻度は毎秒数回です。
Interact や Grab したとき、そのクライアントはサーバーに要求すると同時に、仮に Owner を持っているものとして振る舞います。それから数秒以内にサーバーから Ownership が割り当てられなかった場合は、 Owner として振る舞うのをやめます。
Item の Owner である Player が退室したり、継続的に通信が途切れた時は、概ね数秒以内にワールドにいる他の Player が Ownership を要求します。よって、数秒間 Owner が不在の Item は存在しうるということになります。
アイテムの位置の同期
Item の Owner が位置をサーバーに送り、サーバーはワールドにいる全クライアントに転送します。
物理挙動の計算も Item の Owner のみが行います。 rigidbody の Is Kinematic が false である Movable Item は、 Owner である場合そのまま動作しますが、 Owner でない場合には Is Kinematic は true となり、受信した位置を(補間しながら)反映するだけです(このために起きている衝突に関する問題については後述します)。
送信頻度は秒間数回です。
アイテムやプレイヤー、ワールドの状態の同期
状態とはつまり Trigger によって変化し、Gimmick が反映することでワールドに変化を発生させるものです。
各クライアントは、 Player 自身や自分が Owner の Item の状態の変更(Trigger の発火や Operation の実行結果など)をサーバーに送ります。サーバーはそれらを統合し、最新の状態を定期的に各クライアントに通知します。
クライアントからサーバーへの送信頻度は最大で秒間数回です(変化があった際には即時送信しますが、連続的に変化があった場合には一定時間(1秒未満)待ってからその間の変更をまとめて送信します)。サーバーからクライアントへの同期頻度も秒間数回です。その間サーバーが受け取った状態の変更はまとめられて、同じ Target : Key への変更は最後にサーバーが受け取ったものが採用されます。
これは、連続して送った Signal は間引かれたり、複数クライアントが同時に同じ Target : Key の状態を変更したときにはいずれかの変更が採用されない可能性があることを示しています。
1ワールド(サーバー)が持つことのできる状態の数には上限があります。上限を越えたことを検知したクライアントは、以降のサーバーへの状態更新の送信を停止します。
Signal の実体はタイムスタンプです (Set Text Gimmick で Signal を受け取ると時刻を表示できるのはこのためです)。したがって Signal とは「何らかの時間を状態として表現する」 Parameter Type であると言えそうです。つまり、 Signal の Trigger もやはり状態(ほとんどは、最終更新時刻)の変更であるということです。
状態の変更
Item Trigger (Interact Item Trigger などの ~Item Trgger で終わる Trigger) や Item Operation (Item Logic などの Item~ で始まる Operation) を実行し、対象の状態を変更するのは Owner です。
Global Operation (Global Logic などの Global~ で始まる Operation) も、 Item Operation とほぼ同様です。Global Operation を実行するのはいずれかのクライアントであり、これは Item の Owner と同じような仕組みで割り当てられています。
Player Trigger (現在は On Join Player Trigger のみ) や Player Operation (Player Logic などの Player~ で始まる Operation)を実行し、対象の状態を変更するのは各 Player のクライアントです。
また先程、状態の変更の同期頻度について触れましたが、実行したクライアント上では即時反映します。
状態の反映
Item Gimmick (Add Continuous Force Item Gimmick のような ~Item Gimmick で終わる Gimmick) や Item Operation は、基本的に Item の Owner のみが実行しています。
Item の位置や物理挙動に影響を与えるものはすべて Item Gimmick です。他に、 Item の生成 (Create Item Gimmick) と破棄 (Destroy Item Gimmick) がありますが、これらは少し特殊なので後述します。
Trigger の実行者と Item Gimmick や Item Operation (がついている Item) の Owner が同じである場合は、 Gimmick や Operation は安定して即時実行されます。それ以外の場合では、遅延や欠損することがあります。また Owner を取得しつつ Trigger を発火するケースでは、新旧 Owner で多重実行されることがあります。
Player Gimmick (Respawn Player Gimmick のような ~Player Gimmick で終わる Gimmick) や Player Operation や、 Target に LocalPlayer を指定した Global Gimmick (Item Gimmick と Player Gimmick 以外はすべて Global Gimmick) は、各クライアントが自分自身の Player について実行します。よって、 Trigger の実行者が Player 自身であれば安定して即座に実行されます。それ以外の場合は、遅延や欠損することがあります。
Global Gimmick は、各クライアントでそれぞれに実行します。よって Trigger の実行者から見たときには即時実行されます。それ以外のクライアントでは、遅延や欠損することがあります。
Global Gimmick は、各クライアントが実行する性質上、実行タイミングの違いによって状態のズレが発生することがあります。これは特に Set Animator Value Gimmick で Animation や Animator Controller の作りによっては顕著になります。
Signal によって実行される Gimmick の一部では、 Signal は数秒で期限切れになります。これは結果が大きくズレるのを防いだり、過去の影響を受けないようにする、 Owner 引き継ぎ時の多重実行を防ぐなどいくつか理由があります。
これによって、その数秒の間通信が途絶えていたクライアントで反映が欠損することがあります。
アイテムの生成
Create Item Gimmick で生成された Item は、位置情報が送られて来た際に Ownership の存在が確認できた場合、各クライアントでも生成します。つまり、最新の Ownership 情報が同期されるまでの時間遅延します。
また Item Gimmick の反映の説明と重複しますが、 Owner を取得しつつ Trigger を発火するケースでは、新旧 Owner が多重実行することで Item が2つ生成されてしまう場合があります。
アイテムの破棄
Item の破棄は、 その Item がワールドに始めから存在する Item か、 Create Item Gimmick によって生成された Item かによって少し違います。
ワールドに始めから存在する Item の場合は、 Destroy Item Gimmick を各クライントが実行し、破棄されます。
Create Item Gimmick によって生成された Item は、 Destroy Item Gimmick を実行するのは Owner だけです。Owner は Ownership の破棄をサーバーに知らせ、各クライアントはその破棄された後の Ownership 情報を受け取ったタイミングで Item を破棄します。
ケース別 同期に関する問題と解決
ここからは、よく起きる問題から逆引きして、回避策の有無を見ていきましょう。
物理的な衝突の影響を与えられたり与えられなかったりする
Item 同士の Owner が違うと、物理的な衝突が期待したとおりに動作しない場合があります。
物理的な衝突に強く依存したゲームデザインは避けてもらうのが無難です。
野球のように、投げる人と打つ人がいるような仕組みは不向きと言えます。
ラグビーのように、掴んだ人が投げるようなルールであれば比較的きれいに動きそうです。
On Collide Item Trigger が発火しない、遅延する
ワールドに一人しかいない状況でも発生する場合は、まず Collider の設定を確認してください。
https://docs.cluster.mu/creatorkit/trigger-components/on-collide-item-trigger/
Item 同士の Owner、 Playerと Item の Owner が違う場合に、遅延や発火しない問題が発生することがあります。
たとえば、跳ねているボールが Owner 以外から見たときに毎回地面に着くとは限りません。
「特定の設定で Movable Item と On Collide Item Trigger がついている Item の Owner が別だと発火しない」不具合を認識しています (On Collide Item Trigger の側に Is Kinematic がオフで Constraint を全てオンにした Rigidbody を設定することで発火するようになる場合があります)。
人によってアイテムの位置がズレる
Movable Item がついていることを確認してください。
位置の同期に0.2秒ほど遅延があるのは仕様です。
人によってアイテム生成の反映が遅い
同期頻度を上げる改善を予定しています。
高速な銃弾などは、他人に見えるようになる前に着弾・消滅する可能性があります。
銃と弾を1対1でワールドに用意しておくような仕組みで改善できるかもしれません。ハトソード https://booth.pm/ja/items/2162556 は実装例として参考になるかもしれません。
ネストした Item は生成できないため、ワールドにはじめから設置しておく必要があるなど、制約は強くなります
オブジェクトの移動速度を遅くすることも検討してみてください。
高速な移動は、当たり判定に失敗する可能性も高めます。
人によって状態の反映が遅い
同期頻度を上げる改善を予定しています。
今のインターネットではがんばっても遅延は0にはならないので、カイラル通信(ゲーム『DEATH STRANDING』に登場するインターネットのようなもの。超大容量の情報をゼロ時間で送受信できる)の登場に期待してください。
Trigger を1回しか押してないのに Gimmick が2回実行される
Owner が移る際に、自分目線ではなるべく即時実行するために、逆に他クライアントと重複して実行してしまう場合があります。
Owner が移るような操作と分けることで発生しにくくなることが期待できます。
たとえば、 Interact する Item と Create Item Gimmick がついている Item を分けることで、 Create Item Gimmick がついている Item の Owner は移らないようにするなどが考えられます。
代わりに遅延は発生しやすくなります。
Trigger を2回押したのに Gimmick が1回しか実行されない
Trigger と Gimmck の Owner が違う場合には、同期頻度でまとめられるのが原因です。
同期頻度を上げることでの改善を予定しています。
関連する Item の Owner を即時反映させたい Player に取得させる(Interact させるなど)ような工夫をすることで、発生しにくくできるかもしれません。
Timer が実行されない時がある
Signal の期限切れが原因です。
実装の改善を検討しています。
一時的にオブジェクトを非表示にし、数秒後に表示するなどは Animation で実装することを検討してみてください。
定期的に呼び出しを行うための Timer で、クライアントが一時的に停止する(アプリがバックグラウンドになるなどで)ことを想定したい場合は、定期よりも長い Timer を二重三重にかけることで、1つ目が欠損した場合にも復活できる可能性があります(目覚ましのスヌーズみたいな感じです)。
Owner が移った際に Timer の実行が漏れることを想定したい場合は、 On Receive Ownership Item Trigger を使うことで解消できる可能性があります。
Animator の状態が人によってズレる
Set Animator Value Gimmick は作り方次第で状態のズレが起きやすい Gimmick です。
すべての遷移を Any State からにする、 Signal (Trigger) は一時的変化に使うことでズレは起きにくくなります。
例えば、一時的に表示される勝利演出のようなものには Signal (Trigger) は便利です。
何らかの操作によってその後持続的に表示を切り替えたいものには Bool の使用が推奨です。
しばらく遊んでると同期が止まる
状態の数の上限に達している可能性が高いです。
実装の改善を検討しています。
Trigger の数を減らす、Item が無限に増えにくいようにするなどの対策が考えられます。
さいごに
この記事が、クリエイターの皆さんの助け、あるいは “誰かを助けることの助け” になれば幸いです。
途中にも書きましたが、この記事の冒頭に『非推奨』とでっかく書ける日が訪れるよう、開発も引き続きがんばっていきます。
明日は御楠ルナさんの『ワールドのアップロードエラーで悩まされた話』です。
今日とは打って変わってとてもやさしそうな記事の予感があり、バランスが取れていて大変助かりますね。楽しみです!