見出し画像

VRChatのワールドを作ったので小技を書いていく

BilliardCityという、でっかいビリヤードのワールドを作りました。
紹介ツイート

特にすごいギミックがある訳ではありませんが、他の人に参考になるかもしれない小技がいくつかあるので紹介してみます。

キュー

・キューはアニメーションで動かしていますが、Unityのアニメーションは「毎フレームワープ」のような挙動なので、実は高速なアニメーションで衝突させても他のオブジェクトには速度が伝わりません。物をきれいに弾き飛ばすには、何かしら(Unity内の)物理的な操作をするコンポーネントが必要です。今回はその中でも一番シンプルなFixedJointというものを使いました。

画像1

ただし、この方法でもあまりに速すぎる場合はうまくいかないようです。また、検証はしていませんが、処理落ちするとうまくいかなくなったりするかもしれません……もっといい方法を募集中です。

・キューは手を離してからしばらく経つと元の位置へ戻るようになっています。これは、アニメーションからアニメーションイベントでVRC_SceneResetPositionのResetPositionを呼ぶことで行っています。
ただし、この時他人目線でのみキューの中身(↑でFixedPositionで動かしてたやつ)の向きがずれてしまうようなので、本体をResetPositionした後に中身も再度ResetPositionをしています。アニメーションイベントは自分自身にしか影響しないので、子に働きかけるためにButtonのイベントを経由させるというテクを使っています。

画像2

(安全のため本体のResetPositionから0.5秒開けてますが、そんなにいらないかも)

ボール全般

・リセットなどボールに対して影響を与えるイベントがあるので、ボールは作り直しではなく使いまわしています。実はテーブルの下には見えない箱があって、ポケットインしたボールはそこにTeleportしています。通常の落下死判定は使えないので、ポケットの底あたりの高さに専用のBoxColliderを仕込んであります。(プレイヤーのリスポーン用のコライダーも同様にあります)

・ボールは当然他の人からも同じ位置に見えてほしいので、VRC_ObjectSyncで同期しています。しかし、物理的に離れた人もいる都合上全く同じ動きをさせることはできないため、Authorityという(ライブラリ内部の)設定値によって「誰の世界の動きを採用するか」が決められています。Authorityを持っている人は自然にプレイできますが、そうでない人の世界ではボールがガクガク震えたり、そもそも打ったことが反映されなかったりします。これでは困るので、Authorityを適切に設定する必要があります。
AuthorityはPickupするなどでも変更されますが、今回はすべてのボールに設定する必要があるため、TakeOwnershipというイベントを使います(OwnershipとAuthorityは厳密には違いますが、基本的には連動するので同じものだと考えて読んでください。気になる人は直接聞いてね)。このワールドでは「打つ予定の人が」「打つ前に」TakeOwnershipを発行する必要があります。しかし、直接そんな未来予知機能は実装できないので、その時行われるであろうプレイヤーの動きを予想してギミックを実装する必要があります。このワールドでは「キューを持った人のLocalでのみ出現するコライダーがキューの少し先にあり、それが手球に当たった時」にこのイベントを呼んでいます(OwnerLocalみたいなTriggerがあればもうちょっと楽なんですが)。

画像3

キューを手球に近づけたときに「カカカッ」と音が鳴ったことがあると思いますが、あれはAuthorityが移動してボールの物理が再計算された結果ですね。

手球

・手球はファウルのときに好きな位置に置けるようにするため、Pickupが設定されています。置きたい場所に正しく置けるようにするため、2つ工夫をしています。
まず、持ったときにも位置関係が分かりやすいように、持ってる間だけ半透明になるようにしています。この時にも影ははっきり出したいので、(不透明・半透明の2つではなく)不透明影なし・半透明影なし・影のみの3つのオブジェクトを用意しています。(気づいた人いるのかな)
また、手球を離した時に転がっていかないようにイベントを仕込んでいます。OnDropの0.01秒後にSetVelocity/SetAngularVelocityで速度/角速度を0になるように設定しています。

リセットボタン

・リセットボタンは安全のため二段階式(ボタンを押すとUIが出て、UI内のボタンを押すとやっとリセットする)になっています。それ自体に問題はないのですが、UIを消した時に中のボタンなどが選択状態だった場合、シーン上の他のUIが選択状態になってしまいます(おそらくシーン上で一番上のSelectable?このワールドでは音量スライダーだった)。そのため、リセットボタンを押した後しばらくすると意図せずBGMの音量が変わる操作をしてしまうというバグがありました。それを回避するため、SelectableなUIの表示・非表示をいじる場合は、GameObject.SetActiveで変えるのではなく他の方法(例えばScaleを0にするなど)でやるのが安全だと思います。

VRC_Trigger全般

・BroadcastTypeはLocal/AlwaysBufferOne/AlwaysUnbufferedの3種類しか使っていません。自分だけで起こるものはLocal、全員で起こる状態変化(後から入った人でも再現する)はAlwaysBufferOne、全員で起こる一時的なイベント(後から入った人では再現しない)はAlwaysUnbufferedです。
(とはいえこのあたりの使い分けはネットワークが関わる設計に慣れてないと難しいかもです……)

・1つのオブジェクトに2つ以上VRC_Triggerをつけたり、1つのVRC_Triggerに同じTriggerTypeのイベントをつけたりできるはずです。できるはずですが、ものによってはなぜかうまくいかない時があるようなので、そういう時はCustomTriggerを使って回避しています。1つのTriggerに異なるBroadcastTypeのイベントを設定したいときは、まずLocalでActivateCustomTriggerを必要な種類分発行し、実際に起こしたいActionはTriggerTypeがCustomのイベントを作ってそこ経由でBroadcastしています。
 (このバグの詳細な条件やより良い解決法も募集中です)

その他ワールドの設計に関して (という言い訳置き場)

Q: そのルールはおかしいのでは?
A: ワールド内に書いてあるとおり、狙った方向に飛ばすのがとにかく難しいので、ファウル系は減らしています。また、長いと読んでもらえない・覚えられないという事情もあります。
(本当はもっと減らしたいが、ルールを知ってる人と齟齬が起きない程度には忠実にしたつもり。ターン進行など)

Q: なぜ壁があるの?
A: ボールが吹き飛びやすく、外に出てしまった後卓上に戻すのが大変だからです(重力を増やしてマシにはしていますが)。Respawnだとその位置にあった他のボールを吹き飛ばしてしまうので、手球のようなPickupを付ける必要があり、16倍の戻す場所ともろもろのイベント設定、そしてプレイヤーの手間が必要です。そもそもルールも挙動も違うので、多少厳密さを諦めてでも手軽にワイワイできる方が大事かなと思いこうしました。

Q: どうして威力調整できないの?
A: これも↑と同じで、狙った方向に飛ばしづらいなど他にも問題が山積みで、複雑性が増えるがあまりプレイの助けにならないからです。とにかく適当にプレイできるようにして、ミスショットはゲラゲラ笑いましょう。
~というのは半分ぐらい名目で、本当はシステムと見た目でやることが多くて面倒だったからです。~

Q: 地味じゃない?
A: ポケットインしたときに壁を大きく変化させたりエフェクト出したりしたかったんですが、力尽きました……
まわりにビルを生やすのも試したのですが、あんまり好みの見た目にならなかったので止めました。開放感大事。あと、位置が分かりづらくなってしまうので卓上に影を落とせないというのもあります。

Q: 途中入室した時うるさい
A: すみませんすみません完全にサボりです…… 結果表示の球の表示はBufferOne、アニメーションと音はUnbufferedとに分けるべきですね。

この記事が気に入ったらサポートをしてみませんか?