ゲーム制作   Unity ソフトウエア 

Unity ソフトウエアでゲーム制作#1モグラたたき編(15.ゲームマネージャーとシングルトン)

キャッチ画像モグラたたきゲームマネージャーと7シングルトン

本投稿は2024年9月時点の内容になります。アップデートにより変更となる場合があります。また環境によって違いがあると思いますのであくまで参考として、ご了承ください。

様々な書籍、ブログや動画を参考にさせていただきました。多すぎて一つ一つはtお紹介できませんが感謝です。

初心者の自分がUnity ソフトウエアでゲームを作ってみました。とりあえずシンプルなものということでモグラたたきに挑戦です。ゲーム作ってみるかという感じになったときに、いいタイミングで某ゲームのイベントシナリオ内ミニゲームにモグラたたきが実装されていたのでUIとかエフェクトとか、諸々の仕様をぱくって参考にして作ってみましたよ。様々なHowToの中の選択肢のひとつとして、同じ初心者さんの参考になればよいです。

\ チェック /

ゲームマネージャーGameManagerとシングルトンパターンについて

敵のスポーンがほぼ完成したのでいよいよGameManagerでの全体管理を考えていきます。あわせてGameManagerの話題とセットで語られることの多いシングルトンについても初心者的に解説してみます。初心者の解説なので用語など間違えてるかもしれません。ご了承を。

本記事のポイント

  • シングルトンパターンとは
  • 汎用的なジェネリックシングルトン解説
  • GameManagerの実装

シングルトンパターン

シングルトンパターンとは

Singleton パターン(シングルトン・パターン)とは、オブジェクト指向のコンピュータプログラムにおける、デザインパターンの1つである。GoF (Gang of Four)によって定義された。Singleton パターンとは、そのクラスのインスタンスが1つしか生成されないことを保証するデザインパターンのことである。

Wikipedia-Singleton パターン

Unity プロジェクトだとゲームを通して一つしか存在しないことを保証するデザインパターンて感じですかね。

シングルトンのメリットとか使いどころ

  • デザインパターンとしてゲームを通して一つしか存在しないことを保証すること
  • 使いどころは、ゲーム全体でひとつだけの方がいいもの
  1. ゲーム全体を管理する「GameManager」 Unity プロジェクトでシングルトンといえば必ず使用例で上がってくる
  2. 「SoundManager」(特にBGM) シーンをまたいで途切れることなくBGMを流したい 2つのBGMが同時に流れると都合が悪いなど、DontDestroyOnLoadと一つであることが良い場合が多い
  3. プレイヤーキャラ ステージ(シーン)をまたいでも同一であることが保証されている方が良い場合のある
  • staticでグローバルな存在なのでシーンに関わらずどこからでもアクセスできる

シングルトンのデメリット

  • デザインパターンとは関係なくstaticな存在として便利だから使ってしまう
  • 上記のように便利なので、ついつい使って肥大化する
  • 全体への影響が見えにくく大きい グローバルな変数をみんなで見にいって、さらに変更なんかするとあっちでもこっちでも影響が出たりする
  • 複数のインスタンスが必要になったときに対応が困難
  • なので単体でのテストが難しい

今回は学習のためシングルトンパターンを使ってみますが、あくまでデザインパターンの無理して使う必要はないです。別にUnity 公式が推奨してるわけでもないです。どちらかというとアンチな意見の方が多いみたいです。

ちなみに全体の影響が見えにくいは、今回使ってみて結構痛感したかもです。難しいですね。

ジェネリックシングルトン

ジェネリックシングルトンです。簡単に言うとシングルトンをいろいろなクラスで継承できるようにしたものです。

ジェネリックとは

  1. 型をパラメータとして渡せるようにして汎用性をもたせたもの
  2. 使ったことなさそうで実は身近なジェネリック。List<T>これです。Listって中にintやらfloatやらGameObjectまで入れられますよね。そういうことです。

ジェネリックシングルトン実装

  1. 「Singleton」スクリプトを作成します。
  2. 以下コードです

というか

ほぼ 公式のebook から拝借したものです。ただし二重チェックのところとか少しあやしいので変更してます。

class Singleton
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//Unity プロジェクトにおけるジェネリックシングルトン
//Unityではとくにシーンに依存しないデータの保持や
//プロジェクト上、唯一である方が好ましいときに使う

//シングルトン:単一生成を保証するデザインパターン

//ジェネリック:型をパラメータとして渡せるようにして汎用性をもたせたもの
//このためGamemaneger、SoundManager など型が変わっても汎用的に使える
//ただしwhere:で制約されている
//これはジェネリック内で使用されているメソッドなどの利用を保証するため
//今回はComponentクラス(MonoBehaviourの継承元)
public class Singleton<T> : MonoBehaviour where T : Component
{
    //T型のメンバ変数
    //staticなのでアプリケーションに1つのみ生成される
    //(同じ場所に生成される)
    //privateなので外部からはアクセスできない
    //自分の中だけで管理できる
    //単一生成を保証その1
    private static T instance;

    //ダブルチェックロッキングのためのインスタンス
    private static readonly object _lock = new object();

    //プロパティ
    public static T Instance
    {
        //遅延インスタンス化
        get
        {
            //ダブルチェックロッキングとかいうやつ
            //スレッドセーフ(お守り程度かも?)
            if (instance == null)
            {
                instance = (T)FindObjectOfType(typeof(T));
                lock (_lock)
                {
                    SetupInstance();
                }
            }
            return instance;
        }
    }

    //シーンの一番最初、オブジェクトのインスタンス化のときに1度だけ呼ばれる
    //virtualなので継承先で使うときはoverrideできる
    public virtual void Awake()
    {
        //単一生成を保証その2
        //二重生成のチェック
        RemoveDuplicates();
    }

    private static void SetupInstance()
    {
        //アクセスされたときにまだインスタンスを持ってなかったら
        //再度チェック
        instance = (T)FindObjectOfType(typeof(T));
        if (instance == null)
        {
            //確認用ログ
            Debug.Log("There is No" + typeof(T).Name);

            //ゲームオブジェクトを作りTと同じ名前をつける
            GameObject gameObj = new GameObject();        
            gameObj.name = typeof(T).Name;
            //Tをコンポーネントに追加したゲームオブジェクトをメンバ変数instanceに代入
            instance = gameObj.AddComponent<T>();
            //このゲームオブジェクトはシーンをまたいでも破棄されない
            DontDestroyOnLoad(gameObj);
        }
    }
    
    //二重生成のチェック
    private void RemoveDuplicates()
    {
        //フィールドinstanceがnullなら
        if (instance == null)
        {
            //自分をインスタンスをinstanceに代入
            instance = this as T;
            //このゲームオブジェクト(Tがアタッチされているゲームオブジェクト)はシーンをまたいでも破棄されない
            DontDestroyOnLoad(gameObject);
        }
        else if(instance && instance!=this)
        {
            //instancenullがnullでない、かつ自身でないなら、このゲームオブジェクト破棄
            Destroy(gameObject);
        }
    }
}

スクリプトざっくり解説

  1. (16)where T : Component(MonoBehaviourの継承元) ジェネリックの制約。 FindObjectOfTypeやAddComponentの使用を保証している MonoBehaviourクラスを継承したシングルトンクラスが作れる
  2. (24)インスタンスを入れるprivateでstaticなフィールド privateなので外からアクセスできない クラスの中だけで管理できる 単一生成の保証その1
  3. (38-48)instance のプロパティ
  4. (33-46)遅延インスタンス化。プロパティにアクセスがあるまで自身によるインスタンス生成を遅らせる。ここがいまいちわからない。たぶん理想はAwake(後述)の中でinstanceをもらいたい。なので自身によるインスタンスの生成を(58-76)を、getされるタイミングまでギリギリ遅らせるよ という処理 て感じでしょうか?あってるかなぁ? 
  5. (27,40)公式からの変更点 ある程度スレッドセーフにした方がいいらしいので、ダブルチェックロッキングていうのを使ってます。
  6. (51-56)Awake Unity 公式だと MonoBehaviourの関数 スクリプトのインスタンスがロードされたときに呼び出されます。具体的には①シーンロードで、このスクリプトを含むアクティブなゲームオブジェクトが初期化されたとき、②アクティブでなかったゲームオブジェクトがアクティブになったとき、③ゲームオブジェクトがInstantiateされたとき 一番最初に呼び出されます。ここで二重生成のチェックをしてinstanceをもらう ※ちなみにvirtualになっているので継承先でもAwakeを使いたいときはoverrideしてbase.AwakeすればOKです。
  7. (79-94)二重チェックでinstanceを設定。
  8. (89)公式からの変更点 elseから else if で条件を変更 公式のままだと自分を破棄して、そして誰もいなくなった になることがあったので

ジェネリックシングルトンを継承したGameManagerを作る

GameManagerの作成

ジェネリックシングルトン実装

  1. 「GameManager」スクリプトを作成します。
  2. ヒエラルキー上に「GameManager」の空のオブジェクトを作成してアタッチします。
  3. 以下コードです
class GameManager
public class GameManager : Singleton<GameManager> 
{

}

スクリプトざっくり解説

  1. (1)ジェネリックシングルトンの継承はこれだけでOKです。
  2. 外部からフィールドやメソッドにはシングルトンのInstanceプロパティを使ってGameManager.Instance.フィールド、 GameManager.Instance.メソッド でどこからでもアクセスできます。

GameManagerの実装

GameManagerの実装例です。今回はゲームの状態と時間の管理をしていきます。

class GameManager
public class GameManager : Singleton<GameManager> 

{
    public GameState CurrentGameState {  get; set; }
    public float PlayTime {  get; private set; }
    EnemyManager EnemyManager { get;  set; }

    public void SetEnemyManager(EnemyManager enemyManager)
    {
        this.enemyManager = enemyManager;
    }
    
    public void UpdateCurrentState(GameState newGameState) 
    { 
        CurrentGameState = newGameState;
        switch (newGameState)
        {
            case GameState.gamestart:
                Gamestart();
                break;
                
            case GameState.activate:
                ActivateGame();
                Time.timeScale = 1.0f;
                break;
                
            case GameState.active:
                Time.timeScale = 1.0f;
                break;

            case GameState.pause:
                Pause();
                Time.timeScale = 0f;
                break;

            case GameState.resume:
                Resume();
                Time.timeScale = 1.0f;
            break;

            case GameState.gameover:
                Time.timeScale = 0f;
            break;

            default:
                throw new ArgumentOutOfRangeException
                    (nameof(newGameState),newGameState, $"UpdateCurrentStateにて予期せぬステート{newGameState}が指定されました");

        }

    }

    private void Gamestart()
    {
        UpdateCurrentState(GameState.activate);
    }

    private void ActivateGame()
    {
        EnemyManager.StartRepeatSpawnEnemy();
        UpdateCurrentState(GameState.active);
    }

    private void Pause()
    {

    }

    private void Resume()
    {

    }

    // Start is called before the first frame update
    void Start()
    {
        UpdateCurrentState(GameState.gamestart);
    }

    // Update is called once per frame
    void Update()
    {
        if (CurrentGameState == GameState.active)
        {
            PlayTime += Time.deltaTime;
        }
    }
    private void OnEnable()
    {
        CurrentGameState = GameState.none;
    }
}

public enum GameState
{
    none,
    home,
    gamestart,
    activate,
    active,
    pause,
    resume,
    gameover,
    loadsscene,
}

スクリプトざっくり解説

  1. (84-95)ゲームの状態を表す列挙型
  2. (4)現在のゲームの状態を表すプロパティ
  3. (5)ゲーム内の時間のプロパティ
  4. (6)EnemyManagerをセットするプロパティ
  5. (8-11)EnemyManagerをセットするメソッド
  6. (13-51)ゲームの状態を変更するときに実行 switchで各状態に対する処理を行う。各メッソドと時間の管理 「timescale」は時間の経過スケール 1で通常、1以下でスロー 0でストップ
  7. (45-47)switchに含まれていない状態があったときにコンソールに表示
  8. (53-72)実行するメソッド 仮「gamestart」から「Activate」で「EnemyManager」のスポーン開始を実行
  9. (83-86)プレイ時間を取得 
  10. (85)「deltaTime」前のフレームから今のフレームまでの秒時間 毎フレーム呼ばれる「Update」と組み合わせて使うことが多い

「EnemyManager」の変更点

class EnemyManager
    private void OnEnable()
    {
        Spawns=false;
        GameManager.Instance.SetEnemyManager(this);
    }

スクリプトざっくり解説

  1. (4)初期化で「GameManager」に自身を渡しています。

ひとまず「GameManager」からスポーンさせてみました。スポーンに関しては前回解説しています。今後少しずつ実装、変更を加えていきます。

まとめ

まとめ

  • Singleton パターン(シングルトン・パターン)とは、そのクラスのインスタンスが1つしか生成されないことを保証するデザインパターンのことである。
  • シングルトンは「GameManager」などxxxManagerに使われることが多い。
  • ジェネリックとは型をパラメータとして渡せるようにして汎用性をもたせたもの。
  • ジェネリックシングルトンクラスを作って継承させると便利。
ユニティちゃん公式ホームページへ
ユニティちゃん公式ホームページ
ユニティちゃんライセンス
ユニティちゃんの画像、素材、ライセンスロゴはユニティちゃんライセンス条項を元に使用しています

\ Unityのスクリプトを書くのに役立ちます /

もっと早く教えてほしかった!Unity C#入門

MARU

マケイヌ的おすすめ度

わかりやすい度

目指せ脱初心者

〇おすすめポイント

ボリューム大でC#学習にもよく使う関数のチェックにもOK

×よくないポイント

始めたばかりの人にはちょっと難しい

おすすめ記事

 

プロフィール

マケイヌ

人生のメインストリームから外れた40代の♂。

90年代オルタナにはまり、文字通りメインストリームから逸脱。 その後もたびたび人生から逃亡。

心が動いた作品の紹介や 自分のちいさな経験、HowToを発信できればと日々模索中。

1年後までにイラストと写真のポートフォリオをつくりたい。

記:2019年12月

▼プロフィールはこちら

Follow me

アーカイヴ