推しアイデア
UnityとWebの奇跡の融合!? Web側で送信されたコメントがUnityで弾として発射されちゃう! 君のコメントで推しをゲームクリアに導こう。
UnityとWebの奇跡の融合!? Web側で送信されたコメントがUnityで弾として発射されちゃう! 君のコメントで推しをゲームクリアに導こう。
配信者になりたいなって思った瞬間、ないですか? でも視聴してくれてもコメントが盛り上がらないんじゃ... そんな不安を一気に解消、コメントが絶対に盛り上がるゲームを作っちゃえばいいんでは??
UnityとWebの接続をWebSocketで実現しています。それによりコメントが来たタイミングでリアルタイムで反映されます。 Unity側にはゲームが重くならないようにいろんなデザインパターンを取り入れています。
ストリーマーシューター
配信弾幕ゲーム
このゲームにおけるプレイヤーは、シューティングゲームをプレイする配信者側と、その配信者を応援するためにコメントを打つ視聴者側の2種類に別れる。
配信者側はシューティングをプレイし、敵の弾を避けるながら自らが放つ弾を当てなければならないが、この弾を放つという仕組みはプレイヤーに委ねられているわけではなく、視聴者側の入力するコメントから行われている。
視聴者側がコメントを打ち込むことにより、それが配信者側のゲームに反映され、弾が放たれるのだ。
そう、このゲームは推しをゲームクリアに導くため、オタクたちが立ち上がる配信協力型ゲームなのである
ゲームとWebの関係は以下の通り。
アーキテクチャ図
一言で言えば、一つのインスタンスを使い回すデザインパターン。 インスタンスを生成する、削除するという行為そのものは決して重くはないが、これが何度も続くようであれば話は変わってくる。
今回のゲームにおいては「弾幕シューティング」と銘打っているため、敵もプレイヤーもたくさん弾を撃つ。 このような仕組みで生成、削除を何度も使うとメモリも食えば、処理もだんだん重くなって処理落ち、ということになりかねない。
ということで、一つのインスタンスに構成要素を定義し、再定義して仕舞えば再利用できるような形にした。
もし現在利用可能なインスタンスがあればそれを利用し、なければ生成する、というような形。
具体的には以下のような感じになる。
ここで暗くなっているのはUnity上では非表示になっており、ゲームプレイ中は消えたように感じるが、実際には画面上に残っており、また別のところで発射の処理を行う時に位置の移動、発射処理等を行うことで、さも今生成したかのように振る舞うことができる。
今回のゲームにおいては、プレイヤーの弾、エネミーの弾、エネミーそのものなどのゲーム中に大量に生成されるものにこの処理が適応されている。
(M)Model (V)View (P)Presenter で構成されるオブジェクト
わかりやすいコードはこちら BulletModel.cs BulletView.cs BulletPresenter.cs
Modelにはそのオブジェクトのデータの実態を持つ。 BulletModelにおいては、弾のコメントデータや、そのオブジェクトが利用可能かどうかを示すboolなどを持っている。
ViewにはそのオブジェクトのUIに関する処理を持つ。 BulletViewにおいては、弾の表示を行うコンポーネントに対してコメントの注入を行ったり、表示非表示を管理する。
PresenterにはそのオブジェクトのModelとViewの繋ぎこみを行う。 コードを参照して貰えばわかるが、ModelとViewはそれぞれの存在を知らない。 そのため、それぞれをつなげる役割であるPresenterが必要である。BulletPresenterの理解に関しては後述のR3を参考にしてほしい。
あくまで行っているのはPresenterがModelの状態を観測し、変更があればViewのメソッドを発火させるという処理。 すなわち、Modelに何かしら変更があってもViewには影響がないし、Viewに変更があってもModelには影響がない。 別のViewを挟もうが、別のModelの情報を取ってこようが、極論問題ないのである。ModelとViewはお互いを知らないので。
これにより責務分離、拡張性の高いコード等々良いことが多くあります。
今回のゲームでは表示機能を持つオブジェクトに関しては概ねMVPパターンの実装を行っている。(Model、Viewと直接書いていないものもある)
今回使用したUnity 6は、Unityの最新バージョンです。 去年出たばかり、去年使った時はプレビュー版だったのにLTSになってびっくり。
今までと何が違うの? 第一にレンダリングパフォーマンス。 HD レンダーパイプラインに大きくパフォーマンス強化が入ったことで、CPU フレーム時間を最大 50%も削減できる。
第二にライティングの強化。 一言で言えば、ライティング周りで機能拡張があったことによって昼夜の切り替えだったり照明周りのライティングがしやすくなったことで細かい表現の調整ができるようになったとのこと。
そして個人的に面白いなと思っているのがマルチプラットフォームの充実。 元々Unity自体はいろんなプラットフォームに対してビルドが可能だったのがさらに便利に。 特にAndroid、iOSはWeb上でアプリを動かすことができるようになっため、ウェブビューにアプリを乗せて実行する、なんてこともできるようです。
イベント管理を行うことのできるライブラリです。 元々UnityにおいてRx系といえばUnirxというくらいには蔓延っていましたが、2020年を最後にUniRxの更新がストップ。それからしばらく音沙汰がありませんでしたが、今年に入ってからUniRxの製作者neuecさん主導でCysharpが新しいライブラリR3を開発しました。 その目的は既存のRxの再定義、再実装。現在、非同期処理のベストプラクティスはasync/awaitですが、それに比べるとRxは価値が低下。実装自体も古いC#で実装していたため、機能面でもかなり劣っていました。async/awaitとの共存や、最新のC#の環境に合わせるためにR3は作られました。
例えばどんなことが変わったの? 1番面白い変更はObservableの定義について。 元々、Observable はSystem定義のインタフェースに依存していましたが、R3ではSystem定義のインタフェースに依存していない独自の抽象クラスObservableとして定義されています。
RxとR3のObservableを比較すると、変更されているのはOnErrorとOnCompletedの挙動です。 元々Rxでは例外が発生した時はOnErrorを発行し、そのままObservableは動作を停止するように作られていました。
(たかだか)例外一個出ただけでイベントが止まると困る場面が出てきます。破棄されたObservableを再構築するのもパフォーマンス的にはいかがなものかと、そういった問題を解決するために、OnErrorで動作を停止するのはなく、例外を通知しObservable自体の寿命は関係しないOnErrorResumeが追加されました。
また、既存のOnErrorとOnCompletedは統合されてOnCompletedになり、停止理由が送ることが可能になっています。
個人的にこのことを表す1番好きな一文が**「すべてのObservableは「完了」する」**元々の実装を考えるとかなり大胆な変更がされていることがよくわかりますね。
これ以外にも今までを振り返ってよりよく実装され直している部分が多々あります。 使用感的にはあまりUniRxとは違いは大きく感じませんが、より深いところを見てみるとなかなかに面白いです。
クラスAは自身の変数を変更したときにクラスBを参照して、クラスBのメソッドを発火する必要があります。 この時クラスBはクラスAのことを知る必要はありません。
クラスAが自身の変数を変更したという通知をクラスBが受け取ることでクラスBが自身の処理を発火させます。 この時クラスAはクラスBのことを知る必要はありません。
例の場合、変更を行う、変更から処理を行うの関係が1 : 1の関係なので、いまいちピンと来ないかも知れませんが、変更から処理を行うクラスがもしも複数個あった場合、変更を行うクラスはそれだけたくさんのクラスを参照することになります。
依存性の逆転を行うことで、それぞれの変更から処理を行うクラスは変更を行うクラス1つを参照すれば良いため、擬似的に1 : 1の関係を生み出すことができます。
今回は、データとUIを完全にわけ、データ間でのやりとりやUIの表示を行いやすくしました
高品質なアニメーションを行うことのできるライブラリです。 例えば三角形を上に移動させたい時は以下のコードを1行書くだけで動画のように動きます。
this.gameObject.transform.DOMove(Vector3.up * 2, 3);
動きながら回転は以下のように
this.gameObject.transform.DOMove(Vector3.up * 2, 3); this.gameObject.transform.DORotate(new Vector3(0, 0, 360), 3, RotateMode.FastBeyond360);
ちょっと力を貯めて
this.gameObject.transform.DOMove(Vector3.up * 2, 3).SetEase(Ease.InBack); this.gameObject.transform.DORotate(new Vector3(0, 0, 360), 3, RotateMode.FastBeyond360).SetEase(Ease.InBack);
ゲーム側に通信の責任を持たせないよう、バックエンドサーバに情報を集めてからゲーム側に情報を送っている。
Goの標準機能であるGoroutineは並列処理をシンプルに安全に、かつパフォーマンス高く実装できる機能である。
今回、視聴者と配信者の複数のコネクションを同時に扱わなければならないため、各コネクションをGoroutineで並列処理させて実現した。
複数あるWebSocketのコネクションを簡潔に、スケーラブルに実装するためにSender、Hub、Receiverというクラスを用いて実現した。
ウェブクライアントとのコネクションを管理するSender、仲介役を行い、情報を集計、監視するHub、ゲームクライアントとのコネクションを管理するReceiverという役割を持っている。
Senderは視聴者の数いるので、複数のコネクションが行われる。そのため、各コネクションに対してSenderインスタンスを生成し各視聴者のコネクションを管理している。
Receiverはゲームクライアントとの状態を管理している。Hubから送られてきたメッセージをソケットを通してゲームクライアントに送信している。Receiverとのコネクションを管理している。
Hubは複数のSenderや単一のReceiverに関する情報を監視したり、メッセージをReceiverに送ったりすることを行っている。SenderやReceiverがコネクションを確立すると、Hubに登録される。
それぞれのクラスにI/O用のチャネルを持たせチャネルを監視し、イベントが起きたときに発火するようにしている。
複数のゲーム部屋を実現するためにHubManagerによるHubの管理を行った。配信者から部屋作成のリクエストが来たとき、Hubのインスタンスが生成し、生成されたHubをHubManagerに登録することでHubManagerによる管理を行っている。
生成されたが使用されていない部屋を残し続けないように、JanitorによるHubの使用具合の監視を行っている。使用されていない部屋を定期的に検出し、5分以上アクティブではなかった場合削除する。
本番でバックエンドサーバが処理落ちしてしまうことがないように、今回ハッカソンに参加している方々40名 + メンターの方々約10名の50名が3秒あたり約10文字のコメントをしたと想定して負荷テストを行った。
結果、Render上にデプロイしたバックエンドサーバでは正常に処理を行うことができた。
ユーザがコメントを送信できるように、WebSocketを用いてコメントを送信する機能、ログイン、ログアウト機能などを制作しました。
WebSocketは常に監視しています
部屋番号が間違っていた場合にトップページに遷移するのはもちろん
ゲームが終了した(WebSocketが切断された)ときもトップページに遷移します
現在のログイン状況を様々な場面で取得しています
トップページに名前が表示されたり
通常ヘッダーにはログインボタンが表示されていますが
ログイン中はログアウトボタンに変わります
(ここのデザインちょっとお気に入りです。
ログインボタンは背景色付きですがログアウトボタンは縁取りだけにしています)
今回行いたかったのは、ゲームとWebの融合でした。 特にメンバーの過半数がどちらかといえばWebという状況で、最も最適なゲームを作れたと思います。
単純にUnityだけで完結するゲームを作る、Webだけで完結するアプリを作る、のではなく、その垣根を超えて関わり合えるゲームを作れたのはとても良かったと考えています。