Unity(UniRx)で蚊を倒すゲームを作った話

遊びでUnityで蚊を倒すゲームを作ってみました。大学の学祭のときにでもみんなに遊んでもらおうと画策してます。ゲームの紹介と技術的なことを書きます。

遊び方

  1. でてきた蚊をたたいてたおそう!マウスを動かして左クリック!
  2. 蚊を倒すとスコアが上がるよ。たくさんたおそう!はずすと命中率が下がってスコアがさがるから気をつけよう。
  3. 制限時間は90秒!時間内にたくさんの蚊をたおそう!

ゲームの様子

プレー中の様子を動画にしました。

ダウンロード

GitHubのリリースにWindows・Mac向けのビルド済みファイルを置いてあります。遊んでミテネ。

8q/mosquito-game/releases

技術的な話

UniRxを使って

せっかくUniRxを覚えたので全面的にUniRxを使って作りました。UniRxのおかげでこれまでのUnityで作ったゲームより圧倒的コンポーネント間の疎結合が実現できたなあと感じています。カーソルを例に考えてみます。

今回のゲームではマウスのクリックひとつに

  1. 「パン」が生成される
  2. 蚊に当たると蚊が消える
  3. 命中率の再計算を行う
  4. 再計算した命中率をUIに反映させる

といった複数の処理が発生します。これをUniRxを用いずに愚直に実現するカーソルのビヘイビアを書くと、

BadCursorBehaviour.cs

public class BadCursorBehaviour : MonoBehaviour
{
    [SerializeField]
    private GameObject pan;
    
    void Update()
    {
        if(Input.GetMouseButtonDown(0))
        {
            //1. 「パン」を生成する
            Instantiate(pan); 
            
            //2. 蚊に当たったら蚊を消す処理
            // some code ...

            //3. 命中率を再計算する処理
            // some code ...

            //4. 再計算した命中率をUIに反映させる
            // some code ...
        }
    }
}

と、こんな感じでカーソルのビヘイビアなのにカーソルに関係ない処理がモリモリ増えていきます。そうすると別クラスに切り出すというのは考えられるのですが、結局カーソルが各クラスのメソッドを呼び出す形になるので、クラス間の依存関係が複雑になってきます。そもそもカーソルがUIの更新を行うのかというところも疑問ですし、カーソルが「パン」のオブジェクトのことを知っておかなければいけないというのも気に食いません。

では、UniRxを使ってイベント駆動にしてみます。カーソルは「クリックをした」というイベントを発行するにとどまり、各処理はそのイベントを観測次第実行するという風にしてみます。

EventManager.cs

public class EventManager : SingletonMonoBehaviour<EventManager>
{
    public Subject<Unit> MouseClick {get; private set;} = new Subject<Unit>();
}

CursorBehaviour.cs

public class CursorBehaviour : MonoBehaviour
{
    private EventManager eventManager;
    
    void Start()
    {
        eventManager = EventManager.Instance;
    }
    
    void Update()
    {
        if(Input.GetMouseButtonDown(0))
        {
            // クリックのイベントを発行
            eventManager.MouseClick.OnNext(Unit.Default); 
        }
    }
}

PanGenerater.cs

public class PanGenerater : MonoBehaviour
{
    [SerializeField]
    private GameObject pan;

    void Start()
    {
        var eventManager = EventManager.Instance;

        // クリックイベントの監視の登録
        // Subscribeでイベントが発生したときの動作を登録する。
        eventManager.MouseClick
            .Subscribe(_ =>
            {
                // 「パン」を生成する。
                Instantiate(pan);
            });
    }
}

と、このようにEventManagerでイベントを管理して、EventManager経由でイベントを発行・通知するようにしました。するとカーソルがカーソルに関係のない処理を呼び出していたものが、役割に応じた各ビヘイビアに処理ごと切り出すことができました。例では「パン」を生成するコードのみを切り出しましたが、その他処理も同様に切り出せます。クラス間の関係の見通しがよくなったと思いませんか?最高。

名前空間の切り分け

Unityで自動生成されるコードには名前空間が振られない(グローバルな名前空間)ですね。最初は気にせず開発していたのですがこんなことがありました。ゲームシーンでの蚊のビヘイビアとして「MosquitoBehaviour」クラスを作っていました。タイトルシーンでも蚊を動かす必要があったのですが、ゲームシーンでのそれとは動作が違うので新たにビヘイビアを作る必要がありました。クラス名として「MosquitoBehaviour」は既に使われているので「TitleMosquitoBehaviour」か?長くない?そもそもタイトルシーンの方をそういう風に命名するならゲームシーンの方も「GameMosquitoBehaviour」に直す?じゃあカーソルとかのビヘイビアも今後別のシーンで使われることを見越して「GameCursorBehavior」にするのか?とキリがなくなってしまいます。そういうときはシーンごとに名前空間を切り分けてやるとうまくいくんじゃないかと思いました。

namespace MosquitoGame.GameScene
{
    public class MosquitoBehavoiur : MonoBehaviour
    {
        // ゲームシーンの蚊のビヘイビア
    }
}

作ったゲームでは以下のようにフォルダ分けして名前空間も分けました。こうすることでゲームシーンでもタイトルシーンでも余計な制約なくクラス名がつけられるようになりました。

Assets/Scripts
├── Editor //拡張エディタ, MosquitoGame.Editor
├── GameScene //ゲームシーン, MosquitoGame.GameScene
├── RankingScene //ランキングシーン, MosquitoGame.RankingScene
├── TitleScene //タイトルシーン, MosquitoGame.TitleScene
└── Utils //シーンを跨いで使う汎用的なスクリプト, MosquitoGame.Utils