見出し画像

VRChatでにんげんタワーバトル風のゲームワールドを作成した話

はじめに

「VRChatアドベンドカレンダー2020」 7日目をやらせていただきますHyakuashiです。

VRChatに関連する記事ならなんでもokです。

とのことだったので、自分が作ったワールドについて解説をさせていただこうと思います。

というわけで作りました。

『AvatarTowerBattle』

下記URLから飛べば実際に遊ぶことができます。
(VRChatのアップデートで壊れていなければですが)

https://vrchat.com/home/launch?worldId=wrld_fa594071-2cbd-4128-950f-08085f1284c8

このワールドにはパk…オマージュ元があります。
それがこちら「にんげんタワーバトル」

言わずと知れた神ゲー「どうぶつタワーバトル」(説明略)の派生である「にんげんタワーバトル」をVRChatで再現したいという願いから『AvatarTowerBattle』は作られました。

今回はこのワールドをどういう風に作っていったかを書き残していきたいと思います。

※ Unity や VRChat 特有の用語について解説は入れませんのであしからず。

環境

・VRChat 2020.3.3p2
・Unity 2018.4.20f1
・VRCSDK 2020.05.06.12.14
・Udon 2020.04.30.18.46
・UdonSharp v0.18.2

ワールド構想(同期編)

(自分が)一番最初に考えるのは「同期」です。
同期について考えるには下記の格言を心に刻んでおきましょう。

ゲームワールドの主な同期へのアプローチとして
 1. 一切同期させず個々のローカル上でのみ進行する
 2. マスターのみが根幹の処理を行い、結果を各クライアントに送信する
  3. クライアントが各々処理を行い、結果を送受信する
があると思います。

それぞれ一長一短があり、
 1. は破綻が発生しません。ただし、各々の進行状況が互いにわからず体験としては微妙になりがちです。
 2. は破綻が発生しにくく、各クライアントで同じような景色が得られやすいです。しかし、マスターに負荷がかかりやすく、マスター以外からはラグによりレスポンス速度が低下しがちです。
 3. は各々の視点でのレスポンスが速くなります。ですが、各々から見た結果を送信しあうために送受信の頻度が高い場合に整合性を保つのは非常に困難です。加えてDebugが地獄。

今回作成するワールドでは、
 ・複数人で遊ぶこと
 ・レスポンス速度は重要でないこと
以上から 2. の同期方法を選択しました。

また、同期させるものは極力少ない方が好ましいので、この時点で同期させるものを洗い出して検討しておきます。
 ・ゲームの開始、終了
 ・アバターのIK、座標、向き
 ・アバターの画像を確定するタイミング
 ・落ちてくるアバター画像の座標と向き
これらが同期されていれば最低限今回のワールドの体験は損なわれないでしょう。

今回のワールド同期の例ですと、
 ・任意の人がゲーム開始ボタンを押します。(SendCustomNetworkEvent)
 ・クライアントがアバターでポーズを取ります。(VRChatのアバター同期に依存)
 ・マスター上でのタイマーが0になるとマスター上でのアバター画像の更新が停止し、更新停止の信号が各クライアントに飛びます。(SendCustomNetworkEvent)
 ・マスター上でのみコライダーが作成され、物理演算を行っている間の座標と向きは各クライアントに同期されています。(UdonBehaviorのSynchronize Position)
 ・任意の人がゲーム終了ボタンを押します。(SendCustomNetworkEvent)

これらの信号が短い期間で頻繁に送受信されることはあまり考えられないので大きな同期ずれが発生しずらいはずです。

同期について紙幅を割いても仕方がないので、このくらいにしておきます。

ワールド構想(ゲームフロー編)

実装に移る前に一度ゲーム全体の流れを書き出しておくと無駄な処理を見つけやすいと思います。

今回のアバタータワーバトルではこんな感じです。

1.ゲーム開始
2.n番目の画像が指定の位置へ(ステート1)
3.時間になるまで待つ(ステート2)
4.時間になったのでコライダーを生成して落下(ステート3)
5.すべての画像が静止するまで待つ(ステート4)
6.n+1が有ればn+1の画像をステート1に、無ければクリア(ステート5)
7.ゲーム終了(すべての画像をステート0に)

本当はゲームフローを書き出して、同期する必要がありそうなものを検討し、ゲームフローを修正し、と、行ったり来たりで最初からバシッと決まりません。
各画像のステートやコライダーなんかは、いかにも同期が要りそうですが、重要なのは画像の位置と向きだけで他はマスターだけが持っていれば良いというのは具体的な絵が頭に出来てきてからでないと気が付きにくいです。

実際に作ってから気が付くと結構気が滅入るので、先に書き出しながらこねくり回すと結果的に楽ができます。

つぎはメインコンテンツである画像に応じたコライダー生成についてです。

画像に応じたコライダー生成1

まずはどうぶつタワーバトルにの実装について考察してみましょう。
(実際の実装については”画像に応じたコライダー生成3”で解説します。飛ばして読んでも差し支えありません)

Unityは賢いもので透過したSpriteにPolygonCollider2Dを割り当ててやると勝手に形状に即したコライダーを自動生成してくれます。
自動で作成されたコライダーが気に食わなければ、手動で修正できますのでこうして微調整を行ったものが、どうぶつタワーバトルの動物たちでしょう。

http://tsubakit1.hateblo.jp/entry/2017/08/22/233000

上記の記事がPolygonCollider2Dの挙動として分かりやすいものになっていると思います。

画像に応じたコライダー生成2

さて、今回は「にんげんタワーバトル」のようにその場で撮影した画像をもとにコライダーを作らなければなりません。

RenderTextureをもとにSpriteを生成できれば、”画像に応じたコライダー生成1”の手法が使えそうです。というわけで調べてみると下記の記事がありました。

http://tsubakit1.hateblo.jp/entry/2016/06/06/221919

意気揚々と実装してみると

System.Exception: Method is not exposed to Udon: UnityEngine.Sprite Create(UnityEngine.Texture2D, UnityEngine.Rect, UnityEngine.Vector2), Udon signature:

現時点でのUdonではSprite.Createはサポートしていませんとのこと。(tex2Dを毎回生成するのも、ラグと負荷の観点からかなり考え物)

ならばと毎回の生成を諦めて、用意しておいたSpriteに情報を流し込みPolygonCollider2DをAddComponentしてやれば自動でコライダーを良い感じに付与してくれるはず。

System.Exception: Method is not exposed to Udon: T AddComponent[T](), Udon signature:

回避策は色々ありそうですが、今回はSpriteにすることとPolygonCollider2DをAddComponentすること自体を諦めました。

画像に応じたコライダー生成3

前置きが長くなりましたが、ここからが実際の実装の話です。

賢いUnity君に自動でコライダーを付けてもらおうとしましたが上手くいかないので、まじめに画像からコライダーを付けるスクリプトを書いていくこととします。

良いやり方がないかなと調べてみると下記がhitしました。

https://imagingsolution.blog.fc2.com/blog-entry-198.html

非常にわかりやすいブログですので、一読されることをお勧めします。
これ以上に分かりやすい説明は自分には難しいので輪郭追跡処理のアルゴリズムの詳細については割愛させていただくとして

アルゴリズム通りに開始地点の探査とそこから輪郭探査を行った際のピクセルの座標を記録しておき、PolygonCollider2D.pointに流し込んでみます。

画像1

完璧ですね。

ところが、いざ実際に積み上げていくと3個目あたりからfpsの低下が顕著となってしまい、まともに遊べません。
それもそのはず、このコライダーの頂点数は上記の画像で700越えでした。
そこで先ほどのブログに戻ってみると、「チェインコード」という概念が存在します。これが本来どういう働きをするのかは不勉強なため分かりませんが、チェインコードで同じ数字が連続する箇所は記録しなくてよいことが、ちょっと考えれば判明します。
ということで、1つ前と同じ方向に進む場合は新たに頂点を記録しないことで約6分の1の頂点数にすることに成功しました。

実際に動かしてみても問題なく動いてくれました。
めでたしめでたし

余談ですが、コライダーを作成するためのRenderTextureは256*256とし、実際の見た目上の画像には1024*1024のRenderTexureを割り当てたカメラを使用することで、処理の軽減を図っています。

蛇足

・注意書きが読まれません(泣)

上の手法では最初に到達した色付きピクセルの部分について境界探索するため、画像自体がひとつなぎになっていないとコライダーが生成されない部分ができてしまいます。
それを防ぐために負荷の高い処理を入れるか迷いましたが、注意文として「被写体が非連続」であることは非推奨だと書くことでお茶を濁しました。
しかしながら、用意しておいた説明や注意書きが読まれることは稀であり、理解されないことがほとんどでした。
動線の構成や文章力、表示されるタイミングの工夫などが今後の課題でしょうか。

・綴りを間違える(泣)

『AvatarTowerBattle』というタイトルですが、日本語入力をサポートしていないVRChat内での検索を容易にするため英語表記にしました。
ところが「Avater」「Towar」「Buttle」といった誤字が散見され、作者にとっても遊ぶ側にとっても混乱を招く結果になってしましました。

【追記】UnityEditor上でのテストを簡単に行う方法

いちいちVRChatを起動してワールドをテストするのは非常に不便です。

そこでVRChatのUdonSharpでの開発時に便利なTipsとして
Networking.LocalPlayerを#if !UNITY_EDITOR,#endifで囲ってやると便利です。

#if !UNITY_EDITOR

       localPlayer = Networking.LocalPlayer;
       ismaster = localPlayer.isMaster;
       if (ismaster)//マスターだけが行うべき処理
       
#endif

       {
           //処理内容
       }

上のように書いてやるとマスターで行われる処理がUnityEditor上でもエラーなしで確認できます。

#if UNITY_EDITOR

       Hoge();
       
#else

       SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, "Hoge");

#endif 

また、SendCustomNetworkEventは上記のような書き方でEditor上で実行してくれるようになります。

画像2

他には、画像のようにTagがEditorOnlyのアバターPrefabをワールド内に置いておくと、アップロード時には勝手に消えてくれるため、UnityEditor上でのアバターがいる状態でのテストや確認がとても楽になります。

雑感

以上です。
Unityを触り始めて6年(?)になりますが、ずっと趣味でしか触っていないのと、ブログを書くこと自体が初めてだったので、だいぶ読みにくいものになってしまったのではないかと不安です。

今回「アドベントカレンダー」という良い機会をいただけたことで新しく挑戦できました。
がとーしょこらさん、ありがとうございます。

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