VRスクワットジムの制作記 ②
第2回です。
前回はこちら:
導線を考える
導線設計はこのようにします。ワールドに入ると操作説明のポスターが見えます。
そしてポスターの前に立つと敷居越しにメイン部屋が見え、操作説明のポスターと実物を見ながら読めるようにします。そしてメイン部屋の鏡の前で運動し、運動に疲れた人は後ろのほうへ移動します。
そこにはVRAAのポータルがあるので、十分に運動したと思ったグループは別の面白いワールドへと旅立つことができます。
この導線を実装するにあたって、1つ技術的な制約がありました。スクワット計測器は頭の高さによって計測するので、高低差を作ると計測できません。床が真っ平らであれば、鏡の前から離れてどこにいても計測できますが、仮に入り口に高さをつけたとすると入り口付近では計測できませんという注意書きが必要になり、分かりやすさが削がれてしまいます。なので床はすべて真っ平らにしましたが、しかしVRChatの人々は高い場所に移動したがる傾向にあります。そんなVRChatterの要求を満たすため、観葉植物と鏡、ポスターの上に限り乗れるようにしました。
不安定な足場に乗るという、ちょっとしたアスレチック要素が雑談における良いスパイスになっています。そしてそのような不安定な足場でスクワットするというのはどう見ても正しいやり方ではないので、その上でのスクワットが計測器でカウントされないというのは直感的に納得できて説明不要なので採用しました。
VRAAに応募する
スクワットジムはワールドコンテストであるVRAA02に出展しています。この文章を書いている段階ではまだ審査を受けていませんが、フレンドと運動する楽しさと一体感をきっと見ていただけると思います。
新登場のUdon
スクワットジムの機能は Udon Sharp で実装されていて、705行のコードから成ります。長いです。
ちゃんとしたプログラミング言語であればもっと簡潔に書けるだろうと思いますが、Udonの機能の貧弱さゆえにどうしても長く複雑になってしまいます。こんな機能があれば簡単になるけれどまだないなあ、と思うものがたくさんあります。関数に引数を渡せない、Synced変数の同期頻度を調整できない、Invokeがない、Listがないなど、様々な不足があります。
といってもSDK2 の時代に比べればずっと良くなりました。かの時代にもシェーダー芸や VRC Trigger芸、Animation芸がありましたが、難易度が高いわりに複雑なことはできませんでした。新登場のUdonは未熟とはいえ、プログラムを組む手段があるかないかでは全然違います。さらに有志の方が作ったUdonSharpの存在も大きいです。
Udonノードで組むか、UdonSharpで組むか
現在はUdonを組むといえばUdonSharpで組む派とデフォルトのノードで組む派の2つに分かれています。私はスクワットジムの簡単な試作品をUdonノードで作り、そのあとUdonSharpで組み直し、そこでUdonSharpのほうが良いと判断してから本格的な機能を組んでいきました。
私は圧倒的にUdonSharpをオススメします。ノードの何がダメかというと、仮に私のスクワットジムのUdonSharpコードをノードに書き換えるとしたら、ノード数は1,000を超えるでしょう。これをチマチマと配置して線を繋げて整頓して、デバッグしてまた線を繋げ直して、などとやっていたら永遠に終わりません。対してUdonSharpはテキストで書くので、線を繋げる必要はありません。散らばったノードを整理することもありません。また入力補助であるインテリセンスを利用できるので素早く入力でき、迷いません。
1,000のノードを繋げるより、700行のコードを入力補助付きで書くほうが何倍も高速です。プログラミングのために作られたエディタを使い、その機能の恩恵に預かることは生産性で大きな違いを生みます。もしあなたがノードでUdonをこねていたら、悪いことは言いません、今すぐUdonSharpに移ってコードを書きましょう。
そもそも、Udonのノードは分かりやすくありません。入門者の方はノードに接続された矢印や線を見て、テキストよりも流れが分かりそうだと誤解しがちです。しかしそれは罠です。線と線はスパゲッティのようにからまり、処理のかたまりはバラバラになり、そのノードは要求するプログラミングの知識量を軽減してはくれません。変数、型とは何か。メソッド、引数、戻り値とは。配列、オブジェクト、トランスフォーム、コンポーネントとはどのような概念か。そういった難しいことを回避するか簡単に済ますような機能はUdonにはありません。分からないままに作り始めたら、きっとバグに悩むことになるでしょう。
残念ながら、ノードもUdonSharpも、プログラミングがまったく初めてという人には優しくありません。もしノードを書けるほどにプログラミングを理解したなら、UdonSharpも分かると思います。こちらのほうが効率的に書けるので、初めからUdonSharpで書いたほうが良いと思います。
Udon実装上の工夫
Udonの機能不足は工夫とゴリ押しで補っていきます。Synced変数は同期する頻度が多すぎて、あまり使うとネットワーク帯域を圧迫してしまいます。なのでSynced変数は最小限にし、頻度を低く抑えたSendCustomNetworkEventでなるべく動作するようにします。他のプレイヤーのスクワット回数は見えますが計測器の上下の針は自分にしか見えないのはその制約のためです。
次に負荷軽減のため、毎フレーム実行するような処理があったら1秒に1度程度で済ませられないか検討します。1秒に1度実行といえばInvokeRepeat関数を使いたいところですが、まだないのでUpdate関数で自分で時間を管理します。軽量化のために1秒のラグを許容できるという状況はスクワットジムでもたくさんあります。例えば目標ボタンが押されてから目標数値が反映されるまで、プレイヤーが去ってからプレイヤー数の表示が減るまで、目標を達成してからおめでとうボイスと花火が出るまで、1秒程度のラグがありますがユーザー体験にはほぼ影響がありません。ここで処理の頻度を抑えることで軽量化しています。
逆に、一瞬のラグも許容したくない箇所もあります。あるユーザーがスクワットして計測器の針を超えた瞬間、自分の回数表示は即座に増えなければなりません。グループ合計の回数表示も同様です。反映まで0.5秒の間があるようでは、自分が数字を増やしたという実感を得られません。
自分がOwnerの変数についてはUpdate関数で増やせば良いので簡単ですが、問題はグループ合計の回数表示でした。グループ回数は自分がOwnerとは限らず、他の人と同期しながら増えるものなので、自分が1回スクワットしても反映まで0.5秒の同期ラグがありました。これを解決するために、同じような変数でSyncedとLocalの2種類を用意します。int groupReps_Synced, int groupReps_Local の2つある、といった感じです。SyncedはOwnerから同期された数、Localは同期を待たずに自分だけ即反映される数、といった風に使い分け、毎フレーム2つの変数を比較して整合性を取っています。
ワールド公開、集会の開催
ワールド公開後、幸いにも好評を頂きました。
先導者Pさんがスクワットジム集会を主催してくださり、熱気に満ちた集会になりました。フレンドからの声援と集会の熱気で夢中になって何百回とスクワットする方も多く、記録はグループ合計で5,000回というとんでもない数になりました。
「今までの人生でもっともスクワットした日だ」という声をたくさんいただきました。
運動に関して最高レベルのモチベーションを引き出すことができたというのは本当に嬉しいことです。VRの民の健康に寄与できたということ、楽しませることができたということ、価値のあるものを作り出せたという経験を得られたのはありがたいことです。スクワットジムに来ていただいた方、本当にありがとうございました。
この記事が気に入ったらサポートをしてみませんか?