VRCで遊園地ワールドを作った話(ギミック解説)

真面目な記事6年ぶりに書くので初投稿です。

 

先日第3回目のしまぱん集会が開催されました。

センシティブな画像がついてるとX埋め込めないらしい

https://x.com/ran_VRC/status/1827204185034498054

しまぱん集会はらんさんと一緒に企画しており、大体3~4ヶ月に1回ペースで開催しています。

集会ワールドを毎回新しく作っており、前回の集会はしまぱんのミュージアムワールドでした。

しまぱんミュージアム – VRChat

 

前回の集会あいさつの際に「次は遊園地ワールドで開催したいと思います~」ということを突然言い出した結果、4ヶ月間にも及ぶ遊園地ワールド作成が始まりました。

目次

 

出来上がったもの

しまぱんらんど – VRChat

遊園地のアセットはこちらを使用しました。

海外のアセットだからなのか、基本的にそういうものなのかわかりませんが基本的にパーツ分けが大雑把な事が多いです。例えば左側にあるジェットコースターは1つのオブジェクトのため、コースを変えることは不可能なのはまだしも、乗り場や入口の床まで一緒にくっついています。

ミュージアムワールドで使用したアセットの時は、壁、床、天井が1つのオブジェクトだったので建物の拡張性に乏しかったり、Unityに持ってきた際に商品画像のライティングとぜんぜん違う!みたいなこともあり、毎回ワールドのアセット選びの難しさを感じています。

 

今までの集会ワールドはモノを置くだけで完成していましたが、今回は遊園地ワールドという都合上、乗り物に乗れて、VRC内で遊具が動く必要がありました。

Udonをこねたことが無い人間が4ヶ月で作ったギミックなので多分もっと良い実装方法があるものがあるかもしれませんが、しまぱんらんどはこうやって動いていますという解説記事を書きたかったので書いています。

(一応C言語でご飯を食べている人間なのでしっちゃかめっちゃかにはなっていないはず…とりあえず動けば良いで実装している部分もあるかもだけど)

それでは各遊具とギミックの説明をしていきます。

 

フリーフォール

上に打ち上げられて落ちてくるやつ。

昇ったときにワールド内のランダムなユーザーのパンツを見れるギミック付き。

処理の流れとしては

1.Sit判定に座る

2.座った時、スタート用オブジェクトと縞パンモニターを表示

3.スタート用オブジェクトをインタラクトしたら、SendCustomNetworkEventで全員が上下移動をするアニメーションを開始

4.降りたらスタート用オブジェクトと縞パンモニターを非表示

 

下準備

・AnimatorControllerを新規作成し、boolのパラメーターがtrueになったら「download 0」という状態でアニメーションが再生され、falseになったら待機状態になるアニメーションを設定します。

→アニメーションの組み方まで説明しきれないため、既に組んだものをこちらに用意してあります。

再生したいアニメーションをdownload 0のMotionに入れるだけでOK

・フリーフォールのオブジェクトにAnimatorを追加して、作成したAnimatorControllerを適用します。

使用するプログラムは3つで、座ったらオブジェクトの表示を切り替えるプログラム、フリーフォールを動かすアニメーション再生プログラム、ランダムなプレイヤーの足元にカメラが追従するプログラムです。フリーフォールをするだけならカメラのプログラムは不要です。

 

最初のギミック説明なのでどうやってUdonのプログラムをVRC内に持ってくるかの説明をここでします。

UnityのProjectの任意の場所で右クリックをして、Create→VRChat→Udon→Udon C# Program Assetを実行します。

U#アイコンができるので、任意の名前にします

作成したファイルを選択し、InspectorのCreate Scriptを実行します

#アイコンのファイルが作成されます(これがUdonのプログラム部分)

ダブルクリックしてファイルを開くとプログラムが表示されるので、そこにこのあと紹介するプログラムを貼り付ければギミックが動くはずです。

注意点として、Udonのプログラムのファイル名(白背景に#が書いてある方)と、Udonのプログラムの7行目あたりにある

public class XXXXXXXXX : UdonSharpBehaviour

のXXXXXXXXXの部分の名前が一致していないとエラーになります。

そのためファイル名を合わせるか、プログラム側のXXXXXXXXを合わせる必要があります。

 

プログラムを書いたら、プログラムを適用したいオブジェクトにAdd ComponentでUdon Behaviourを追加します。

追加されたUdon BehaviourのProgram Sourceに作成したプログラムを適用させます。

成功すると上記のようにプログラムの内容が表示されます。

 

Synchronization Methodというのが、同期処理をいつするかという内容になっています。

今回紹介するプログラムはデフォルトのContinuousで良いものと、Manualを指定する必要があるものがありますが、Manualを指定する必要があるものは自動的にManualになるようにプログラム側に記載しているため、今回紹介するギミックでは基本的に操作不要です。

このあたりの細かい内容を知りたい場合はUdon Synchronization Method[検索]してください。

座ったらオブジェクトの表示を切り替えるプログラム

このプログラムは椅子(VRCChair3)に適用します。VRCChair3ではない場合、VRC Station (Script)が適用されているオブジェクトに追加します。

適用するとこんな感じ

Target Objectsの数字にオブジェクト数を入力し、切り替えたいオブジェクトをElementに指定するだけです。

座ると表示して、降りると非表示にする簡単なプログラムです。これで縞パン追従カメラのオブジェクトと、開始ボタンのCubeの表示を切り替えます。

if (player.isLocal)  にて自分が座ったときだけ処理するようにしています。

 

↓フリーフォール本体のプログラム

このプログラムは開始ボタンのオブジェクトに適用します。

適用するとこんな感じ

Target AnimatorにAnimatorを設定したオブジェクト

Animation Triggerにパラメーター名(サンプルのままならDownload bool)

Target Udon Behaviourに後述するパンツ追従ギミックを適用したオブジェクトを適用します。

 

プログラムの内容としてはインタラクトされたときにInteract()内の処理の

SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, “PlayRide”);

にてワールド内の全ユーザーにPlayRide関数を実行させ、パラメーターをtrueにすることでフリーフォールのアニメーションを再生させます。

targetUdonBehaviour.SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, “AssignRandomUser”);

は後述するパンツ追従カメラにてランダムなプレイヤーを選ぶ処理を実行します。

複数回実行されても大丈夫なように再生終了まで複数回実行されないようにしていますが、今見たら無くても大丈夫かも。

 

↓足元追従カメラのプログラム

適用するとこんな感じ

パンツを映すスクリーンはこちらに置いています。

WorldScreen.prefabに対してScreen 1.matを適用するだけで動きます。

FollowCameraにカメラオブジェクト

Camera Materialにカメラの映像を出力するマテリアル(Screen 1.mat)を指定します。

 

カメラのRotationはあらかじめ上を向くようにしておきます。(プログラムではRotationのXを操作していません)

 

プログラムの内容としては

開始時にワールド内のランダムなプレイヤーを選択(AssignRandomUser)して1920x1080のRenderTextureを作成し、0.1秒おきにカメラの位置をプレイヤーの足元に移動して映像を出力します。

足元へのPosition移動は

プレイヤーの頭座標を取得して、X,Z座標はそのまま(Xだけ好みで微調整してる)

アバターの高さを頭のY座標 – 足元のY座標で求めて、

Y座標をプレイヤーの頭座標 – アバターの高さ + 0.2をすることで、プレイヤーの足元から0.2上に来るようにしています。

この0.2は、まめひなたのような低身長アバターでもパンツが見える値にしました。

 

Positionだけの移動だと、プレイヤーの向きによってパンツが見えない事が多かったので、

カメラをRotationのYとZをプレイヤーの向きに追従するようにしました。

座標設定後、CaptureToRenderTexture()でカメラの映像を出力しています。

縞パンを見ることに対する執念がすごい(自画自賛)

 

 

ジェットコースター

無難なジェットコースターです。下り坂で自動で記念写真を撮るギミック付き。

ジェットコースターって結構いろんな遊園地ワールドに実装されているので、その手のワールドのクレジットとか見たらあるやろ~って思っていたのですが、全然なくてどうやって実装されているのか謎でした。

Boothでジェットコースターギミックがないかな~と探していたところ、電車のギミックがあったため下記を参考に作成しました。(現在公開停止中)

UdonTrainSystemサンプル【Udon】https://booth.pm/ja/items/3294648

 

作者に確認したところ、数年前に作ったもので動作する保証がないため公開を取り下げたとのことで、改変したものを配布しても構わないとの回答を頂いたため、ジェットコースターギミックに修正したものをUnityPackageにて公開することにしました。

https://github.com/hirachon/VRC-Gimmick/blob/main/Jetcoaster.unitypackage

Roller Coaster.prefabを置けばそのままジェットコースターが動きます。

コースの設定はDollyTrack_SampleのCinemachine Smooth Pathで設定します

他のワールドがどう実装しているかは定かではないですが、Cinemachine Smooth PathというUnityの標準機能で移動ルートを設定することができて、VRC内でもこの機能が動くためレールの設定はこれで行われています。

ジェットコースターのレールに合わせて140箇所くらい設定しました。

DollyCart_Sampleで各種設定が行えます

PathLenge_A:DollyTrack_SampleのCinemachine Smooth Pathに表示されているPath Lengthを入力します。コースを編集すると増減するのでその度に修正が必要です。

FastMax_Speed:後半の最高速度です

SlowMax_Speed:前半の最高速度です

StopTime:前半と後半の間の停止時間

SpeedUp_Time:最高速度になるまでにかかる加速時間

SpeedDown_Time:停止するときに何秒前から減速を開始するかの時間

ManualMode:元々OFFだと自動発進、ONだと手動発進でしたが、OFFの自動発進は一切テストしてません。(多分OFFだと動かない)

StationPosition:2地点指定し、1つ目は0、2つ目は下り坂前の停止したいChinemachine Dolly Cartのpositionを指定します。

DollyCart_Sample内のChinemachine Dolly CartのPositionを操作するとどの値のときにどこに居るのかが確認できるので、これで停止したい地点を探します。

ShatterPosition:記念写真を撮るタイミングをChinemachine Dolly Cartのposition値で指定します。

SlowCartSound:前半の移動中サウンドをAudioSouceで指定します。(音源は付属していません)

FastCartSound:後半の移動中サウンドをAudioSouceで指定します。(音源は付属していません)

targetCamera:記念写真を撮るカメラを指定します。

CameraMaterial:カメラのマテリアルを指定します。

CameraFOV / CameraPosition / CameraRotation:カメラのFOV / Position / Rotateを指定します。カメラに設定されている設定値ではなく、ここで設定した値で撮影されます。理由は後述。

texturePropertyName:変更不要。

 

カメラのFOV、Position、Rotationをわざわざ指定している理由ですが、原因は不明ですがデスクトップモードとVRモードで撮影される映像の距離感が異なることが集会当日のテスト中に発覚し、FOVかPositionが違いそうな画像になっていたため、撮影前に固定値を入れることで対応するパワープレイを行いました。(Rotationはおまけ)

移動速度に応じて移動音の音量を調整したり、開始タイミングを統一するために開始時に1秒後の時間を共有→1秒後に各々開始処理とか結構ちゃんと作り込みました。

プログラム内容を見たい人はUnityPackageをインポートしてTrain_base.csを見てください。

 

観覧車

普通の観覧車です。同期が本当に大変で集会当日まで調整してた。

どうにもならなかったから同期の参考のために2,000円で売ってる観覧車を買ったらアニメーションを同期させていたのでUdonじゃなくてアニメーションで回転させたほうが良いのかなと思いつつ、結局アニメーションにしてもうまく動かなかったりとか本当に踏んだり蹴ったりしてた。(後述してるけど多分VRC Object Syncが悪さしてたのでアニメーション方式でもいけたはず)

観覧車って実はこうやって回転してる

ゴンドラが付いてる枠は右回転、ゴンドラ自体は逆の左回転をすると、ゴンドラの向きを維持したまま回転してくれるので、枠とゴンドラをPositionとRotateを同期する必要があります。

度重なるデバッグとどっかに飛んでいく観覧車を眺める日々

結局試行錯誤の末に出来たものだからこれが一番良いとは思ってないけど、観覧車の乗り降りに支障が出るようなズレはない程度には同期できているはず。

結構いろいろやった割にはプログラム自体は割とシンプル。

処理の流れとしては、

①オブジェクトのオーナーが0.2秒に1回、観覧車とゴンドラのPositionとRotateを[UdonSynced]の変数に更新

②インスタンスに参加した人は、参加してから10秒後に[UdonSynced]の変数の位置を適用して同期する。

③後は各々同期せず回転し続けるだけ。

なので、インスタンスオーナーに対しておおよそ0.2秒程度の位置の遅れは許容しているのと、実は開始10秒間は全然同期してないし、1回同期したら同期はそれでおしまい。

 

ポイントは、

・入った直後だと、UdonSyncedの変数の値は同期されておらず0になっているので、入ってから10秒後に同期させた。(入った後にRequestSerializationが実行されて初めて適用されるのかな)

・定期的に位置の同期を行うと乗っているときにガタガタするので、1回位置を同期したらそれ以降は一切同期処理はしない

・位置同期処理を個別に行う(Udon Behiviarをオブジェクト毎に配置の意味)するとどうしても同期タイミングに差が出て、観覧車の間隔が詰まったり離れたり最悪重なってしまうので、1箇所で全部の同期が必要な回転オブジェクトを同期するようにした。(処理時間は測ってないけど、これでも最初に同期したオブジェクトと最後に同期したオブジェクトのタイミングに多少差があるように感じる。26個あるしまあしゃーないか)

・VRC Object Syncがついたオブジェクトが同期対象に関わっていると、ガッタガタになる(これに気がつくのに時間がかかった)

・オブジェクトのPositionの取得には、PositionとlocalPositionがあり、後者を使用する必要があった(親オブジェクトの座標との関係があり、Positionだとだめだった)

ちなみに設定画面はこうなる

同期が必要な回転オブジェクトを全て1箇所で管理するので、オブジェクトの指定と回転量の指定がオブジェクト数分出てくる。

ちなみにこの同期回転処理を利用して空中ブランコも作れます。

 

バイキング

左右に揺れる船に乗るやつです。

Sit判定でアバターのスケールが10倍になって船に対してパンツを見せつけるユーザー参加型のギミック付き。

船が左右に揺れるのは、アニメーションをループで常に再生させているだけなので割愛しますが、申し訳程度のアニメーションの同期を入れています。

↓アニメーションの同期プログラム

適用した結果

Anim:Animatorの設定されたオブジェクトを指定

SyncTime:何秒間隔で同期するか

やっつけで作ったのであまりテストしてないですが、アニメーションの再生位置の同期をしています。

観覧車と同じで1回同期したらそれで終了でも良かったかも。どうしても同期がちゃんとしているかの確認って1人でやりきれないので確認が難しいね。(一応8クライアントまではUnityから起動できるけど、同時に起動するから異なるタイミングでJoinしてきたときの挙動とかが見れない)

 

↓座ったらデカくなる椅子

VRCChair3に追加するだけで座ったらサイズ10倍、降りたらもとに戻ります。

座ったときにアニメーションさせる方法は、VRCChair3のAnimator ControllerにEntry→Motionを指定したStateのAnimator Controllerを適用するだけで多分いけます。(うろ覚え)

多分VRC Stationに座ったらアニメーションする方法については解説している記事がたくさんあるのと思うので最悪これで動かなくても調べればなんとかなるはず。VRCChair3内のSeatの座標を移動すれば座った後任意の位置に移動させれるので、船のところに移動させれば完成です。

 

まとめ

とりあえず30人くらい人が集まっても問題なく集会が完了できてよかった。

遊園地ギミックを作った苦労を知ってほしい自己顕示欲を満たしたいだけの記事でした。

次はどんな集会テーマになるんやろね。