#はじめに
はじめまして、NeosVRで声がガビガビになりがちな犬の__あむ(Rabbuttz)__といいます。この度はNeosVRのAdventCalenderに僭越ながら参加させていただきます。
この記事はNeosVRを始めてまだ間もない初心者が解説する記事です。
結構難しい内容ですが、なるべく自分と同じような初心者でも理解できるように書きました。とてもほんわかと書いたので、不正確な部分も多々あります。そのため、なんとなく「そうなんだ~」程度に読んでいただければ幸いです(間違いがあればご指摘をお願いします)。また、この記事は「NeosVRの操作はほぼ完ぺきでLogiXもだんだんわかってきたよ~」という人向けの記事です(DevToolTipやLogiXTipの使い方はだいたい理解している人向け)。「LogiXって一体なんだよ~~!!!!」っていう人は、他の方が書いている初心者向けの記事を先に読むといいかも!
#簡単なゲームを作ってみよう!
今回は上下に動くBoxをタイミングよく止めるゲームを作りたいと思います。
ということで次の写真のように、EmptyObjectの下にBoxを作り、写真のようにテキトーにLogiXをつないでみましょう。(つないでいるSlotのノードはBoxです。)
Pulseを押すとBoxが止まり、止まったBoxのローカル座標が表示されます。止めた瞬間のBoxのY座標が0.9以上であればあなたの勝ちです。たのしいね。
一人で遊ぶ分にはこれで構いません。では早速このBoxを持ってフレンドのセッションにJoinし、一緒に遊んでみましょう。
#おいおい、どうなってんだ…
作ったこのゲームでChameleonさんに遊んでもらいましたが、ここでトラブル発生です。私の画面では確かにFalseと表示されているのに、Chameleonさんの画面ではTrue(You win)と表示されているというのです。
あれ…?ほんとだ…。たしかにChameleonさんの画面ではTrueと表示されています。でも私の画面ではFalse(You lose)と出ています。しかも表示されている座標も異なっています。いったいなぜなのでしょうか?
作ったLogiXを見直してみましょう。たしかにパッと見では何も間違っているとは思えません。しかし、このLogiXには(複数人でプレイする場合において)正しくない部分があります。
え?何が間違いなのかよくわからない?
安心してください。ここからゆっくりと説明していきます。
#事前知識:NeosVRで同期はどのように実現されているのか
ここで一つ、NeosVRで他のユーザーとの同期がどのように行われているのかを簡単に理解しておきましょう。NeosVRはP2P通信というものが用いられています。NeosVRではセッションを立てた人がHostUserとなり、HostUserに対して他のUserがアクセスをしているということになります。そのため、HostUserがセッションを閉じると同じセッションにいた他の人ははじき出されますし、HostUserがフリーズすればワールドの様々な機能もフリーズします(例えばInspectorが開かなくなったり、自分以外のUserの動きが止まったり声が聞こえなくなったりなど)。そして、他のUserが行った計算などは一度HostUserに送られて、HostUserがその計算結果を他のUserに送って同期しています。(タブン)
そして、__その「計算」を誰が行い、どのタイミングで行われ、それは同期されるのかどうか__というのが注意しなければいけない点です。
おおまかに、そして誤解を恐れずに言うと、NeosVRにはPulseとDriveの二つの処理方法があると言えます。
Pulse
Pulseが流れているLogiXノードの入力までの計算を、そのPulseを流したUserがPulseを流した瞬間(1フレーム内)に行います。
・Pulseを流すUserが自分以外の場合は遅延が発生し、少しカクつくことがあります。
・Pulseを流したのが自分であれば、遅延は発生しません。
・Hostを介して全てのUserに計算結果が送信され、同期されます。
・計算するタイミングが明示的です。
Drive
Pulseを使わずに表示するDisplayノードや、そのままInterfaceノードに繋いだりする場合はDriveしている状態となり、ローカルで(それぞれのUserのPCで)計算をしています。
・ここでの計算結果は他のUserに送信されず、同期されません。
・ローカルで処理するため、どの人から見ても動きはヌルヌルで計算結果が反映されるまでの遅延は発生しません。
・計算するタイミングははっきりしていません。
さらに図で説明しましょう。
一つ目はPulseをHostUserが流した場合。
PulseをHostUserが押すと、HostUserで1+1が計算され、その結果がFloatにWriteされます。そしてその結果が他のUserにも送信されて同期が行われます。
そして次に、HostUser以外のUserがPulseを流した場合。
PulseをHostUser以外のUserが押すと、そのUserが1+1の計算を行い、その結果がFloatにWriteされ、この計算結果がHostUserに送られて、その後自分とHostUser以外のUserに送られて同期されます。
そして次が、そもそもPulseを使わないDriveの場合です。(Driveの計算タイミングなどはよくわかんないけどイイ感じにやってくれてるらしいです。たぶん入力の値が変化したときとか。それゆえに計算結果がずれることがあります。)
Driveを使うと計算はそれぞれのPCでローカルに行われます。そしてその計算結果はそれぞれのUserのPCのみで表示され、他のUserに送信されません(同期されません)。今回はたまたま同じ値が出ていますが、場合によってはみんな違う値が見えている可能性があります。
とっっっっっっってもざっくりと書きましたが、だいたいこのような感じです。Pulseを使うとそのPulseを流した人の計算結果がすべてのユーザーに同期さますが、Pulseを使わない場合、つまりDriveする場合はそれぞれのUserで計算を行い、それぞれの計算結果がそれぞれのPCで表示されます。
そのため、Driveを使うと場合によっては同期が保証されません。自分には1+1=2と表示されていても、もしかしたらPCがバグって他の人は1+1=3と表示されているかもしれないし、PCの時計がずれていればUtcNowもそれぞれ違う値が表示されます。
#その値は誰が計算しているのか?同期されているのか?
勘のいいひとはここで先ほどのLogiXの誤りに気付いたでしょう。
PositionはDriveされてそれぞれのPCで計算した値が入っています。そして、Pulseが流れてBooleanLatchのBoolが変化するタイミングが通信の遅延などによってUser毎にずれたり、そもそもTの値がUserによって少しずつ違ったりするせいで、Userごとに計算結果が異なります。
このようにWriteノードを追加して、その入力に計算結果を同期させたいノードの終端をつなげれば、Pulseを押した人がそこまでの全ての計算を行って評価するため、Writeで書き込まれたFloatは全員に同期されて誰が見ても同じ値が入ります。(※Float以降で表示している値はDriveされているといえますが、参照しているWriteから書き込まれた値(Float)は同期されている値であり、誰から見ても全く同じであるため、今回の場合は同期が保証されているといっても過言ではありません。)
ちなみにこの状態だとPulseが流れて止まったBoxの位置までは同期されないので、Updateで特定の人(例えば一番近い人など)が毎フレームPulseを流して常にBoxの位置を他のUserと同期させるという方法もあります(こっちのほうがいいと思います)
また、NeosVRのWizardことrheniumさんから伝授した技として、特定のUserを引数としたUpdateをWriteに繋いで値を同期し、通信によるカクつきを補完するためにSmoothLerpを通してPositionをDriveするという方法があります。値が完全に同期されているとは言えませんが、見た目的にはスムーズに動いて違和感がないのでお勧めです。
#処理をするUserを意識するのはとっても大切
ここまで読んで、ある程度理解が進んだと思います。そして、__Pulseが流れているノードの入力までの処理はそのPulseを流した人が行っている__というのを意識すれば、いろいろわかってくることがあります。
例えばボタンを押したUserを取得したいとき、次のようにLogiXをつなげばいいのですが、なぜこのようにLogiXをつなぐとボタンを押した人が取得できるのかというのが、先ほどの話を踏まえると簡単に理解ができます。
LocalUserには自分自身のUserが入っています。Pulseを使わずにそのまま表示(つまりDrive)すると、LocalUserの値はそれぞれのユーザーのPCによってローカルに処理されるため、Displayにはそれぞれ自分の名前が入っているように見えます。
ではこれをWriteノードに繋いでみるとどうなるでしょうか?Writeノードの入力につながれたLocalUserはPulseを押したユーザーが評価を行うため、書き込まれるLocalUserはPulseを押したUserになるのです。
ほかにも、特定のUserにしか見えないオブジェクトを作りたいときは次のようにDriveを使うことで実現できるということも理解できるのではないでしょうか。Activeの値に直接ノードをつないでDriveすることで、値がローカルに処理されてそれぞれのUserによってActiveの状態を変更することができます。(今回のLogiXではボタンを押した人だけがBoxを見ることができます)
逆に、次のようにPulseを使ってSetSlotActiveSelfでオブジェクトのActiveの値をローカルに変更しようとしても、結局Pulseによって他の人とBoxのActiveが同期されてしまうので、やはりUserごとに見える/見えないという処理はDriveを使わないと実現不可能であることが理解できると思います。(まだ同期について理解が足りなかったころ、私はここで躓きました)
どうでしょう、かなり理解が進んだのではないでしょうか。正直言って、ここらへんの話はとても難しいです。そのため、読んで理解するだけでなく、たくさんおもちゃやツールを作って体で覚えていく必要があります。ここを理解するとグッとLogiXでできることが増えるので根気強く理解していきましょう。
#最後に
結局私が言いたかったのは、__その計算を誰が行い、どのタイミングで行われ、それは同期されるのかどうかを意識してLogiXを組もう__ということです。また、これを理解することでLogiX力(?)は何倍にもアップします。あきらめずにいろんな人に質問をして理解していきましょうね!もし質問があれば聞いてください🖖できる範囲でお答えします!
最後になりましたが、記事作成に当たって協力していただいた皆さん、AdventCalenderを企画してくださったPiacereさん、ほんとうにありがとうございました!改めて理解が深まって、よい機会となりました。
NeosVRの未来に栄光あれ~~!!
P.S. ProbablePrimeさんの動画おすすめです→https://www.youtube.com/user/ahref?pbjreload=102