Unityをあまり使ったことがない初心者が、VRChatのワールドを作ってみました。どのくらい初心者かというと、作り始めた当初はRigidbodyとColliderの違いがわからないくらい、と言ったら椅子から転げ落ちますかね(桂三枝のように)。でもなんとか公開にこぎ着けました!

DATA CENTER
https://vrchat.com/home/world/wrld_fd08cccd-0a17-49bf-a4f5-51b4f4ec12bb

Comminity Labsの検索オプションをオンにして、DATA CENTERで検索すると出てくると思います。

制作に使用したソフト Unity

ゲーム開発とかもやってみたいし、Unity使えるようになりたいなぁと思って、参考書買って一通り本の通りにやって、スマホでアプリも動かせて、ちょっと上達した気分になりました。よし、何か作ろう!と思ってUnityを開いたところで、そこから一歩も進めないんですよね。何をどうしたらいいのかさっぱりわからない。

まずはVRChatのワールドの基礎の基礎から始めることにしました。VRChatのSDKをインポートして、Planeを配置しただけのワールド。でもOculus Questで見たときに感動しました。自分で作ったVR空間に入れるのが素晴らしい!

最初もかなり苦戦しましたね。SDKもバージョンが2と3があって、検索するとSDK2の情報ばかり出てきてSDK3の情報が多くありません。Uniyのバージョン縛りも罠でした。最新のUnity使えばいいだろう→だめ。Unity Hubからダウンロードできる旧バージョン→だめ。まさかマイナーバージョンまで完全一致じゃないといけないとは。

アセットの配置

さすがに3Dモデルのデザインから作るのは無理なので、無料で配布されているものや、販売されているアセットを使用しました。主にBOOTHUnity Asset Storeのものを使ってます。

はじめに床や壁などの建物の形を作り、その上にラックや空調機器などのアセットを配置していきました。

大量のアセットが配置されるので、ヒエラルキーウィンドウは最初から細かく階層化して作りました。大きく分けて建物、建物内の設置物、ライト、動くもの&動かせるもの、サウンド、操作用のコントロールパネルなど。

持ったり動かせるもの

アセットを配置しただけでも見ることはできますが、やはり何か触れるような体験が欲しいところです。そこでいくつかのアイテムは持ったり、投げたり(?)できるようにしました。VRChatのSDKにはこうゆう機能が用意されていて、VRC Pickupというコンポーネントを追加するだけでできます。素晴らしい!

これはねこなべやさんの即売会セットというパイプ椅子の3Dモデルですが、背もたれのところを持てるようにしています。またColliderがこれだけだと床を貫通してしまうので、底面にもColliderを入れています。

子オブジェクトにVRC Stationのコンポーネントを入れて、座れるようにしてみました。こうゆうやり方が正しいのかはよくわかってません。

出し入れできるラックのコライダー

サーバーラックには2種類あって、サーバーを出し入れできるラックと、設置済みのラックがあります。全部出し入れできるようにすると負荷がすごいことになってしまうので、出し入れできるのは一部のみで、他は飾りなのです。ラックのColliderは左右と奥の側面、一番下の段に設定しました。

ただこのままではサーバーが落ちてきてしまうので、Collider付きの棚板も設置しました。本当はラックレール(サーバーを引き出すためのスライドの金具)の位置に合わせて設定したかったのですが、そうすると衝突判定がとてもシビアになってしまいます。

サーバー本体にもColliderが乗ってるので、ラックのColliderとぶつかってしまって、ぴったり合わせないと入りません。しかしVRChat上の操作でそこまで厳密に動かすことは難しく、そこまでリアルさを出す必要もありません。そこである程度スペースを開けて起きやすいようにしてみました。

これでもまだ入りづらい感じがあるので、サーバーの後方部分は三角形のColliderにして、斜め方向からでも設置しやすいした方がいいかもしれませんね。(あまり削ると上に乗せたときに落ちてしまう問題も)

四角以外のColliderの作り方知らないので、覚えたら修正します!

サーバーを光らせたい!

実際に動いているサーバーのように、LEDがチカチカと光ったらかっこいいですよね~。まず光らせるにはどうしたらいいのか考えました。

・Point Lightを使って発光する
・Emissionを使う

最初に思い付いたPoint Lightを使うというのは、滅茶苦茶重いらしいので途中からSpot Lightにしましたが、どっちにしても光の処理はとても重いらしく、多用は避けた方がいいとのことでした。

ほかに方法が無いかと調べてみると、Emissionという光る色の指定(環境の光の影響を受けない=暗いところでも一定の明るさで見える)があるのを知り、これでLEDが表現できそうです。サーバーのアセットにも元々Emissionの設定があり、LEDが光っているような表現はできるのですが、私はこれを点滅させたいと考えました。

LEDをチカチカさせる

点灯時と消灯時の色のマテリアルを作り、LEDを模した球体(Sphere)に適用させました。問題はどうやってチカチカさせるかです。

最初に考えたのが、ON時とOFF時の2つのオブジェクトを作り、アニメーション機能を使って交互にActive状態を切り替えていくという方法でした。しかしこのままではすべてのLEDが均等に点滅してしまって、いまいち現実感がありません。点滅がばらけるとよりそれっぽくなります。

うどん美味しいよね

(※この画像はLEDのものではなく、アニメーションの操作とサウンドを再生するUdonです)

Udon?うどん?なんの話?みんなUdon、Udonて言ってて、最初Udonのことを聞いたときは何のことかよくわかりませんでした。どうやらコードを書かずに配線を繋いでプログラミングしていく、Scratchみたいな機能があるようです。

ところがこれがくせ者。まず、それぞれの配置されているノードを、メニューからどう辿っていって、何を選択すれば出てくるのかがわからない。見た目は簡単そうに見えるけど、すごく難しい。ものすごく難しい。1行もプログラム書かずに済むなんて便利だなぁ~なんて考えてましたが、C#で書いた方が早いと気付きました。Udonを理解するには、Unity C#の理解が前提です。

C# うどん? U# Udonsharp

UnityにはC#で記述するスクリプトが使用できます。とはいえ私はC言語が大の苦手。Cが苦手なのにC#なんて。C#はよく知らない言語だったのですが、C言語というよりは、Javaっぽい感じです。読みやすくて、慣れれば扱いやすそうです。

Unity上では動くのに、VRChat上では動かない

UnityのPlayボタンで動かしたときは正常にスクリプトが動作するのに、VRChat上では動かないという現象にハマりました。いろいろと調べてみると、どうやらVRChatではUnityのC#は動かず、UdonかUdonsharpを使うとのこと。調べ方が悪いのかもしれませんが、この辺ちゃんと書いてる人少ない気がしました。

UdonsharpもほとんどUnityのC#と同じ書き方で、大部分はそのままコピーして移植できました。

RTX 3080で12fpsだと!?

話がだいぶ遠回りになってしまいましたが、やっとLEDをチカチカさせるUdonsharpのプログラムができました。LEDのON/OFFするタイミングを、ランダム関数を使ってばらけさせます。これでいざ実行してみたところ、カクカクで重すぎて全然だめ。

それもそのはず。1つのLEDにそれぞれスクリプトがついたものが、ワールド内に2600個あります。1秒間に2600個ものスクリプトが同時動き、1秒の間に何回も2600個ものオブジェクトがActive/Deactiveするという鬼のような状況。やばいですね!(by ペコリーヌ)

根本的に考え方を変えなくてはいけなそうです。そこでLEDを1個1個光らせるのではなく、まとめて光らせることにしました。発光パターンもランダムではなく、速度に応じた3種類。サーバーも、持ち運べるサーバーと、ラック内に収まっているサーバーで分けることにしました。

モデルの結合

サーバーのLEDは、点滅する緑LEDが8個、点灯のみの緑LEDが8個、点滅する赤LEDが1個あります。これをグループ分けします。緑点滅を2グループにして、それぞれのグループのモデルを結合しました。結合にはこちらのスクリプトを使わせていただきました。

以下の箇所を若干修正しました。
//var mat = mesh.renderer.sharedMaterial;
var mat = mesh.GetComponent<MeshRenderer>().sharedMaterial;

こうやってまとめることで、負荷を減らせるのではないかと考えました。個々のLEDに付いていたスクリプトも、サーバー1台で1つにします。サーバーが設置済みのラックにつても同じやり方です。

サーバー設置済みのラックは動かす必要がないので、全てのLEDをまとめました。これでかなりの負荷削減ができました。

LEDをチカチカさせるUdonsharpスクリプト

スクリプト: Server_SV_LEDs.cs
それぞれの紐づけはこのようになってます。

上から順に、Enableは点滅の有効/無効化(Oculus Questでのビルド時に止無効にする)、マテリアル(緑と赤のON/OFFそれぞれ用)、点滅させる緑LEDのオブジェクト数とそれぞれの緑オブジェクト、赤オブジェクトです。

このスクリプトは指定したオブジェクトに対して、交互にON用とOFF用のマテリアルの入れ替えをしています。点滅間隔を最初にランダムにすることで、負荷を抑えつつも、それぞれが独立して点滅して見えるような効果を狙ってます。

サーバーの電源をON/OFFさせたい

サーバーを持ち運ぶにはVRC Pickupを使うと簡単に実現できますが、ここで別のアクションを加えることができます。サーバーなので電源を入れるアクションを追加してみました。AutoHoldをYesにするとUseが使えるようになります。今回は起動音(ピッ)を鳴らして、LEDの点滅を開始するようにしてみました。

スクリプト: Pickup_Use_Toggle_with_Sound.cs

Useを押したときに OnPickupUseDown() が呼ばれるので、電源がOFF状態の場合はサウンドを鳴らし、指定したLEDのオブジェクトのActive状態を反転させます。サウンドはスクリプトのあるGameObject上に付けたAudio Sourceを再生しているだけです。

Networking.なんちゃら、のところは同期用の命令です。同期については後ほど説明します。

ドアの開閉

ラックのドアも開閉できるようにしましょう。ラックの開閉はアニメーションを使います。閉じている状態、閉→開、開→閉の3パターンのアニメーションを用意しました。AnimatorではdooropenというBool型の変数を使い、スクリプトからこの変数を変更することで、ドアの開閉を制御しています。

スクリプト: Door_Animation_Switch.cs

プログラムは長々とありますが、やりたいことはアニメーターの変数を変える doorAnimator.SetBool(valhash, nowstate); が目的です。これ以外のところは同期関連の処理で、同期については後ほど説明します。

このスクリプトはラックのドアだけでなく、奥にあるフェンスで囲われた区画の扉にも使われています。アニメーションを変えるだけで、いろいろと使いまわしができそうです。

荷物を運べる台車

データセンターといえばサーバーの搬入!台車は欠かせません。ということで今回はSILVER VRCHAT FBXさんの台車を使用させていただきました。取っ手のところにColliderを設定して、VRC Pickupコンポーネントを追加しました。

次に台車が床を貫通しないようにColliderを底面に設定し、サーバーを乗せて実際に運んでみたのですが、台車を動かすと上に乗っているサーバーが勢いで落ちてしまいます。

試行錯誤した結果、最終的には前後左右をColliderで囲う方式にしました。こうするとサーバーが動いてもColliderで当たって落ちなくなります。

囲いは見えない方がいいと思ったのですが、そうするとどこに当たり判定があるのか見えないため、うっかりぶつけて台車を倒してしまいます。そこで薄い色を付けて、当たり判定がある箇所を見えるようにしてみました。

なお、台車を持つ人と、中にラックを置いた人が別の場合、荷物が落ちることがあります。同期の話になりますが、おそらくそれぞれのオーナーが異なるため同期タイミングがずれて、移動している間に落ちてしまうのかもしれません。

サウンドの再生

データセンターと言えばあの音。ゴーーー、シャーーー。心が落ち着く癒しのサウンドです。とはいえ人によってはそうでない場合もあるので、音量をコントロールできるようにしてみました。使用したのは生チョコ教団さんのヨドコロちゃんハプティックコントローラー

スライダーで音量を変更できるというスグレモノです!今回はサーバー音と空調音で2つに分けました。お好みのサウンドに調整できます。

ちょっとハマったのが、Audio SourceのPriorityの数字。複数のサウンドがあった場合、これが同じだと片方しか音が聞こえないことがあったので、同時に再生する可能性があるサウンドはPriorityを変えています。

踏み台のON/OFF

小さいアバターだと音量のスライダーやボタンに届かない可能性があるので、踏み台を出現させるようにしました。

VRC PickupのProximityを大きくすれば、遠くからでも操作ができるのですが、そうすると他の場所にも反応してしまって、目的のところがなかなか触れないということが起こります。(ワールド散策していてよくありますよね、それ!!)そのため、0.2mまで近づかないと反応しないようにしました。

スクリプト: Humidai_OnOff.cs

これは単純にオブジェクトをオンオフしているだけの、汎用的なスクリプトです。

同期対応

VRChatには同期という概念があります。最初ワールドのインスタンスを作成すると、VRChatのサーバー上にワールドのデータが読み込まれて、その中でプレイヤーが動いているのだと思ってました。でも実際は各自のローカル環境上でワールドが立ち上がっていて、相手のアバターやオブジェクトの座標がリアルタイムに反映されているのだそうです。
同期についてはこのページの説明が参考になります。

たとえば相手がサーバーを持ち上げて移動させると、同じワールドにいる人のローカル上のワールドの中のオブジェクトの座標情報も変わって、動いているように見えるということです。これを自動的にやってくれるのがVRC Object Syncというコンポーネントで、VRC PickupとVRC Object Syncを付けておくだけで、簡単に同期してくれます。

アニメーションの同期

VRC Object Syncを使えば簡単に同期ができますが、アニメーションの同期はできません。このワールドではアニメーションを使ってドアの開閉をしています。そこでドアの開閉状態を保持する変数の同期と、ドアの開閉を行うメソッドの実行を行うプログラムを作りました。

スクリプト: Door_Animation_Switch.cs

先ほど出てきた長いスクリプトです。まずはドアが閉じているのか、開いているのか、ドアの状態を全てのプレイヤー間で同じにしなければなりません。これがその同期用の変数です。
[UdonSynced(UdonSyncMode.None)] private bool nowstate;

ドアの開閉ボタンを押すと Interact() が呼ばれます。触れた人をそのオブジェクトのオーナーにします。同期変数nowstateに、ドアの”変更後の”状態を入れます。そしたら RequestSerialization(); で変数の同期を実行します。

Synchronization MethodをデフォルトのContinuousにしておけば、 RequestSerialization() を使わなくても自動的に同期してくれるのですが、どうやらContinuousは優先度が低いようで、取りこぼしが発生します。そうするとある人はドアを開けて中に入っていったのに、周りで見ている人にはドアの中に突っ込んで行ってるように見えてしまいます。ここをマニュアル同期にすることで、そういった取りこぼしが起こりにくくしています。

変数の同期とメソッドの同期実行は同時にできない

ドアを開けるには open_the_door() を実行します。同じワールド内にいる他の人のドアも開けるところが、
SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, nameof(open_the_door));
の行ですが、どうやら変数の同期と、メソッドの同期は同時にはできないらしく、正常に動きませんでした。

そこでこちらの記事を参考にして、遅延実行させるようにしました。同期変数が同期した後、wait = 10; にして一旦終了します。Update()は毎フレームごとに呼ばれるので、waitをデクリメントさせて0になったら SendCustomNetworkEvent() を実行するというものです。たしか1秒が60フレームなので、0.6秒だともしかしたら足らないかもしれません。

後から入ってきた人も同期できるようにする

これで同じワールドにいる人同士は同期ができるようになりましたが、問題は後から入ってきた人です。後から入ってきた人はもちろん初期状態ですから、先に来た人とは見え方が違います。同期変数は自動的に同期されるようなので、後はドアを開けるというメソッドを実行するだけでよさそうです。

新規プレイヤーが入出すると OnPlayerJoined() が呼ばれます。ここに open_the_door() を入れてあげただけです。

軽量化

元々このワールドは120MBほどありました。ダウンロード時間長そうですね。いったい何がこんなに容量を食ってるのか調べてみたら、大部分がテクスチャでした。そりゃあ2048とか4096の画像がガンガン入ってたら重いわな。

ということでテクスチャのサイズを256~1024に変更しました。サーバーの文字盤はくっきり見えるようにしたかったのでそこは細かくし、壁などは粗くしました。あと圧縮も有効にしたところ、最終的には18MBくらいにまでなりました。

Oculus Quest対応

Oculus Quest 2めっちゃ売れてるみたいですね。私が使ってるVRゴーグルもOculus Quest 2です。VRChatにはPC用と、Quest対応のワールドがあって、Quest対応のワールドは少ないようです。でもせっかく作ったのだからQuestでもプレイできるようにしたい。ということでワールドの軽量化をしてみました。

Quest用のワールドは容量に制限があったり、Questの処理能力にも限界があります。しかし何を消せば軽くなるのかわかりません。わからないので、1つ1つ消していってどこで軽くなるかを調べることにしました。元データを壊すといけないので、Unityのプロジェクトのフォルダを複製して作業しました。

調査の結果、LEDが重いことがわかりました。うん、そんな気がしてた。LEDの点滅をやめたものの、LEDが存在するだけでも重く、最終的にはLEDを削除することにしました。Quest版ではLEDは点滅しませんが、なんとか動くレベルまでにはなりました。

Quest版で削減したこと
・LEDを点滅をやめた
・LEDの実装をやめた(元々の3DモデルにもLEDの描写はある)
・Post Processingを無効にする(入ってても効かないみたい)
・中身固定のラックをStaticにする(PCだと容量肥大化してしまったけど)
・テクスチャの縮小

ただ実際にOculus Quest 2でプレイしてみると、いたるところがチカチカとしてしまってます。モデルの境界部分が光に反射しているのに、アンチエイリアスがかからずにドットが見えてしまっているような感じになってます。なにがいけないのかよくわかりません。

同期が合わない謎現象が発生!

左のドアを開けたら、右のドアが開くという、ドリフのような現象が起こりました。笑 PC同士だと同期するのに、Questだと別のものが同期してしまいます。ダンボール箱を投げたらイスが吹っ飛ぶ感じです。

原因はQuest版の軽量化のためにLEDを削除したことでした。LEDを削除すると、LEDに実装されたUdonsharpの同期変数もなくなりますが、これが悪さをしていたようです。

これは推測ですが、VRChatはワールド内でオブジェクトの同期をとるときに、「どのオブジェクトが」という形で同期をしているわけではなさそうです。同じワールドなのに同期変数を削除してしまうと、「何番目のオブジェクト」という順番が変わってしまい、別のものが同期されてしまうのではないかと思われます。なので同期変数は削除はせずオブジェクトの表示をしない、という形にしました。

こんなイメージ?
期待していた処理 { “hoge”=>123, “huga”=>456 }
実際の処理 [ 123, 456 ]

ライティングさっぱりわからん

ベイク?焼くの?何それ美味しそう。私の技術では無理でした。本当は蛍光灯に付けたSpot Lightの光で影がつくようにしたかったのですが、ベイクしたら全く意図しないような見え方になってしまいました。なんかやたら暗いし、パラメーターもたくさんあって何をどうしたらいいのかさっぱりわかりません。

そこで「Unityライティング入門」みたいなブログをいくつか読んでみたのですが、何を言ってるのか全くわからなくて諦めました。実際ワールドをめぐってると、室内で影のついてるワールドはあまり多くない感じです。ライトは処理が重いそうですが、Directional Lightは軽いらしいので、Directional Lightの影なしで室内を照らすことにしました。

何はともあれ完成!

DATA CENTER
https://vrchat.com/home/world/wrld_fd08cccd-0a17-49bf-a4f5-51b4f4ec12bb

わからないことだらけで始めましたが、なんとか動くものが作れたので、Community Labsに公開することができました。まだCommunity Labの表示を有効にしてないと検索には出てきませんが、そのうち普通に表示されるようになると思います。

使用させていただいたアセット

メタルラック(VRChat想定)2倍製作所さん
Lura’s Switch 仮想狐のデザイン工房さん
ボタン(スイッチ) 白百合めしべの道具箱さん
UPS 晴嵐獣工さん
ヨドコロちゃんハプティックコントローラー 生チョコ教団さん
ダンボール lyohm(リオム)さん
ノートPC れおの小物店さん
即売会セット ねこなべやさん
台車 SILVER VRCHAT FBXさん
鉄のドア fassfaceさん
モデルの結合 tsubaki_t1さん

以下、Unity Asset Storeより
Control Panels Set 3D Casterさん
Chainlink Fences Kobra Game Studiosさん
High Quality Server Room Pack SanuRenuArtさん
Server pack VP.Studio 3dさん
Server Room Kit FastTrackStudioさん

LINEで送る
Pocket