Udonのワールドを作った時の同期の話

概要

ワールド名:Ride Around ShootingGame を作った際に、同期関係のものに関して焦点をあてて工夫した点などを記事化したものです。

ワールドは以下 https://vrchat.com/home/launch?worldId=wrld_7c9e20ea-fb8c-4a1c-9cb8-ff17d560056b

同期した内容としては以下の内容です。 - 他人から見た他の人の操縦している砲身の向き 及び弾の同期 - スコアの同期 - 敵のクールタイムの同期

[注意] ワールドを一度体験していただいてからこの記事を読んでいただくと理解しやすい可能性があります。 また同期の関係上、2人以上でワールドを遊んでいただくとわかりやすいです。

目次

  • 開発環境
  • 前提知識や前提条件
  • 同期についての少しだけ補足
  • オーナーという概念について
  • 他人から見た他の人の操縦している砲身の向き 及び弾の同期
  • スコアの同期
  • 敵のクールタイムの同期

開発環境

Unity2018.4.20f1 VRCSDK3-WORLD-2020.10.28 15.57_Public(なお作成時点ではすでに古い) U# v0.18.8 

前提知識 や 前提条件

-前提知識- - プログラムを多少書いたことがある。 - Udonの同期が、UdonSyncを使用しない場合、値を送る事が出来ないという事を知っている

-前提条件- - [UdonSync]が常に同期されるものである可能性が高いため、極力使用しない作り方である

これは以下の記事の内容から来ている考えです。

Udon開発する上での注意点[Unity] https://qiita.com/toRisouP/items/16bd06aa303a1bb1a747

同期について少しだけ補足

これから同期という言葉が飛び交いますが、同期とはいったいなんなのでしょうか? 同期とは、他の人の世界に影響を及ぼすといったイメージを持っていただければ、おおまか間違えません。 同期がどういったものかわかる問題ないという人は、この項目を飛ばして頂いて問題ありません。

よくワールドとかで実装されているミラー(Local)などは、自分の世界だけしか影響を受けないもので、これは「同期されていない状態」でローカルといいます。  一方、ミラー(Global)といったものは、自分とか相手が触ったものに対して、「触った」という事を「通知」されて、「他の人も同様に同じ状態になる」といったものです。  この通知によって、他の人も同様の状態になることが「同期」で、Globalとかの言葉でついているIntarctのテキストとして使われている事が多いです。

基本的には、どんなゲームワールドもゲーム開始とかは、なんらかの手段で「同期」がされており、そのおかげで「ほぼ」同じタイミングで、ゲームを開始できます。

念のため、「ほぼ」とつけたのは、同期=その瞬間相手も同じ状態になるというイメージを持つ人が多いかもしれませんが、そうではありません。 多かれ少なかれ自分、もしくは相手の通信状況によっては、それなりの遅れが存在してしまうためです。 AさんからBさんに対して、PickUpできるObjectを持ったとします。 これはAさんからBさんに対して、Objectを持ったという事を通知して同期されなければ、Bさんからみた時にAさんがObjectを持っていない状態になります。

オーナーという概念について

同期関係を考える時にこの「オーナー」は知らなければいけません。 VRCには「オーナー」という概念が存在します。 オーナーは最初にインスタンスに入った人の事を指し、それ以外の人はオーナーではない状態です。 また、オーナーがインスタンスから抜けた場合、時系列的に入って来た人が次のオーナーになる性質を持っています。 (ただし、プログラム等で、強制的に変える事は可能) またこれらは、GameObjectごとに対してのオーナーを持っています。

この「オーナー」は、個人的には、値とかの同期を取る時の基準というイメージです。 例えば、そのオーナーである人以外の人が同期の取られている数値Aを1だと言っていたとしても、オーナーである人が数値Aを0だと言ったら、それは0として扱うみたいイメージに近いと個人的には考えています。

そのため、何かの値として最初に更新をかける場合、それをオーナーで処理する必要があります。 先ほどの例だと、数値Aを1だと言っている人が、最初に更新をかける必要があるので、その人をオーナーに変えてから数値Aを更新する必要があるといったものです。

他人から見た他の人の操縦している砲身の向き 及び弾の同期

以下の記事にて、詳細が記載されていますので、こちらも合わせて参照ください。

基本的には、PickUpObject自体は常に同期を取られているものと、持った人がオーナーになった上で、PickUpObjectとPickUpObjectを持った、落としたの状態の2つだけ同期を行っています。

位置同期はPickUpObject

ゲーム開始の同期

このワールドでは、ゲーム開始のためのStartのオブジェクトを触ったタイミングで、通知されるだけです。

通知を受けた際に、椅子に座らされ、それと同時にアニメーションを再生しています。 厳密には、押した人とそれ以外の人では、誤差のレベルで位置がずれている事があるかもしれません。 しかしながら通知を受けた際に、アニメーションを動かしているので、それほどずれを感じないはずです。

スコアの同期

このゲームでは、4人で争えるように、スコアでのランキングなどを表示しています。 このスコアも、同期あるいは、それをカバーする何かをしなければ基本的には、点数が同じ数値になる事はありません。

では、SendCustomNetWorkを使用して、プレイヤー何番が、何点を取ったという事だけ取っています。それ以外は同期を取っていません。 つまり、スコアの合計などは、それぞれのローカルで作られている点数です。 まぁ、何番の人が何点を取ったというものが取られていれば、基本的には、同じ点数になるため、スコア自体を同期とらなくていいという考えです。

ただ、SendCustomNetWorkは少し曲者で、「数値」を相手に送る手段がないため、基本的には関数呼び出しを行うという方式しかとれないです。 そのため、「誰が」「何点」取ったというものを、関数で識別する必要があります。

その方法とは、Targetとなっているオブジェクトを的にあたるようと、点数用のオブジェクトに分けた上で、点数用のオブジェクトのオブジェクト名を点数とした形式にしています。

また、通知方法を図で表すと以下の内容になります。

f:id:herie270714:20210201003756p:plain
スコアの取得のされ方の図

誰がというものは、各乗る場所によって、名前とひもづけているので 「誰が」は「何番の」という言い換えを行う事ができます。

これで、「誰が」「何点」という部分を指定できるので、以下の形式の文字列でメソッド名を統一する事で、指定したメソッドを呼び出す事ができます。

例えば、プレイヤー1の人が、1点を取った場合のメソッドとしては以下のようなメソッド名になります。 Player1GetPoint1

このPlayer〇GetPoint〇の〇の部分をプレイヤー番号と、獲得した点数といったものが入ります。

メソッドを作成する部分の処理としては以下のようになります。

string methodName = string.format("Player{0}Point:{1}",playerNo,getPoint)

なので、プレイヤー数 × 獲得できるポイント種類数 の関数を用意する必要があります。

あとで調べてわかったんですが、キャストを使用したGetComponentを使用すれば、ローカル上では、数値を取る事ができたみたいです。しかしながら、同期の段階で、数値を送るもしくは確実に同じスコアになる部分が怪しいので、どの道同じアプローチになってたと思います。

敵のクールタイムの同期

敵の的には、クールタイムというのが実装されています。 これは、一度的にあててしまうと、連続的に点数を稼ぐ事ができてしまうため、1-2秒あたりのクールタイムを設けています。(的によってはもっと設けている可能性もあります)

クールタイムの同期はどのようにしているかというと、「当たった」という通知だけを行っています。これは「弾を打った」の同期に近い部分があります。

最後に

同期関係については、VRCの現状難しい部分や、安直に同期させた結果処理的に不安定になったりという事が多々起こりえるというのは、色んなワールドをめぐっているあなた方ならわかる事かと思います。

新しいゲームワールドを作成する上で、何か参考になる事があれば、幸いです。