Unityでシューティングゲームでよく使われるデザインパターン

Unityでシューティングゲームを作る際によく使われるデザインパターンには、以下のようなものがあります。

1. Object Poolパターン

Object Poolパターンは、頻繁に生成と破棄が行われるオブジェクトを事前に生成し、再利用することで、パフォーマンスを向上させるためのパターンです。シューティングゲームでは、敵や弾など、頻繁に生成されるオブジェクトをObject Poolで管理することで、メモリの消費や処理負荷を減らすことができます。

以下は、Object Poolパターンの例です。

public class ObjectPool : MonoBehaviour
{
    public GameObject objectPrefab;
    public int poolSize;

    private List<GameObject> pool = new List<GameObject>();

    private void Start()
    {
        for (int i = 0; i < poolSize; i++)
        {
            GameObject obj = Instantiate(objectPrefab, Vector3.zero, Quaternion.identity);
            obj.SetActive(false);
            pool.Add(obj);
        }
    }

    public GameObject GetObject()
    {
        foreach (GameObject obj in pool)
        {
            if (!obj.activeInHierarchy)
            {
                obj.SetActive(true);
                return obj;
            }
        }

        GameObject newObj = Instantiate(objectPrefab, Vector3.zero, Quaternion.identity);
        newObj.SetActive(true);
        pool.Add(newObj);
        return newObj;
    }

    public void ReleaseObject(GameObject obj)
    {
        obj.SetActive(false);
    }
}

この例では、ObjectPoolクラスがObject Pool、objectPrefabが再利用するオブジェクトのプレハブ、poolSizeがプールするオブジェクトの数です。Start()メソッドで、指定された数のオブジェクトを生成し、非アクティブにします。GetObject()メソッドでは、再利用可能なオブジェクトを検索し、アクティブにして返します。再利用可能なオブジェクトがない場合は、新しいオブジェクトを生成してプールに追加します。ReleaseObject()メソッドでは、オブジェクトを非アクティブにします。

2. Singletonパターン

Singletonパターンは、アプリケーション内で唯一のインスタンスを保証するためのパターンです。シューティングゲームでは、GameManagerやInputManagerなどのシステム管理クラスに使用されます。

以下は、Singletonパターンの例です。

public class GameManager : MonoBehaviour
{
    private static GameManager instance;

    public static GameManager Instance
    {
        get
        {
            if (instance == null)
            {
                instance = FindObjectOfType<GameManager>();
            }
            return instance;
        }
    }

    private void Awake()
    {
        if (instance != null && instance != this)
        {
            Destroy(gameObject);
        }
        else
        {
            instance = this;
        }

        DontDestroyOnLoad(gameObject);
    }

    public void GameOver()
    {
        // ゲームオーバー時の処理
    }
}

この例では、GameManagerクラスがシングルトンで、instance変数が唯一のインスタンスを保持します。Instanceプロパティで、instance変数がnullの場合に、シーン内からGameManagerを検索して取得し、返します。Awakeメソッドで、instance変数がnullの場合に、自身をinstanceに代入します。既にインスタンスが存在する場合は、自身を破棄します。DontDestroyOnLoad()メソッドで、シーンを切り替えた際にGameManagerが破棄されないように設定します。

3. Commandパターン

Commandパターンは、操作をオブジェクトとしてカプセル化することで、実行履歴の保存ややり直し、アンドゥの実現などを容易にするパターンです。シューティングゲームでは、プレイヤーの攻撃やアイテム使用などの操作をCommandパターンで実装することができます。

以下は、Commandパターンの例です。

public interface ICommand
{
    void Execute();
    void Undo();
}
public class AttackCommand : ICommand
{
    private GameObject player;
    private GameObject enemy;

    public AttackCommand(GameObject player, GameObject enemy)
    {
        this.player = player;
        this.enemy = enemy;
    }

    public void Execute()
    {
        // プレイヤーの攻撃処理
    }

    public void Undo()
    {
        // プレイヤーの攻撃を取り消す処理
    }
}

public class InputHandler : MonoBehaviour
{
    public GameObject player;
    public GameObject enemy;

    private Stack<ICommand> commandStack = new Stack<ICommand>();

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            ICommand command = new AttackCommand(player, enemy);
            command.Execute();
            commandStack.Push(command);
        }
        else if (Input.GetKeyDown(KeyCode.Z))
        {
            if (commandStack.Count > 0)
            {
                ICommand command = commandStack.Pop();
                command.Undo();
            }
        }
    }
}

この例では、ICommandインターフェースがCommandパターンを表し、Execute()メソッドとUndo()メソッドが定義されています。AttackCommandクラスは、プレイヤーの攻撃操作をカプセル化したコマンドで、playerenemyを受け取っています。InputHandlerクラスは、プレイヤーの入力を処理し、スペースキーで攻撃コマンドを実行し、Zキーで直前の操作を取り消すコマンドを実行します。実行されたコマンドは、commandStackに追加されます。

4. Observerパターン

Observerパターンは、オブジェクトの状態変化を他のオブジェクトに通知することで、オブジェクト間の依存関係を疎にするパターンです。シューティングゲームでは、敵の出現や撃破、プレイヤーの残り体力など、様々な状態の変化をObserverパターンで管理することができます。

以下は、Observerパターンの例です。

public interface IObserver
{
    void UpdateObserver();
}

public class EnemySpawner : MonoBehaviour
{
    public GameObject enemyPrefab;

    private List<IObserver> observers = new List<IObserver>();

    private void Start()
    {
        SpawnEnemy();
    }

    private void SpawnEnemy()
    {
        Instantiate(enemyPrefab, transform.position, transform.rotation);
        NotifyObservers();
    }

    public void AddObserver(IObserver observer)
    {
        observers.Add(observer);
    }

    public void RemoveObserver(IObserver observer)
    {
        observers.Remove(observer);
    }

    private void NotifyObservers()
    {
        foreach (IObserver observer in observers)
        {
            observer.UpdateObserver();
        }
    }
}

public class UIManager : MonoBehaviour, IObserver
{
    public Text healthText;

    private int health = 100;

    private void Start()
    {
        GameObject spawner = GameObject.Find("EnemySpawner");
        if (spawner != null)
        {
            EnemySpawner enemySpawner = spawner.GetComponent<EnemySpawner>();
            enemySpawner.AddObserver(this);
        }
    }

    public void UpdateObserver()
    {
        health -= 10;
        healthText.text = "Health: " + health;
    }
}

この例では、IObserverインターフェースがObserverパターンを表し、UpdateObserver()メソッドが定義されています。EnemySpawnerクラスは、敵の出現を管理し、AddObserver()メソッドでObserverを追加し、RemoveObserver()メソッドでObserverを削除します。敵を出現させると、NotifyObservers()メソッドでObserverに通知します。

UIManagerクラスは、プレイヤーの体力を表示するUIを管理し、UpdateObserver()メソッドで体力を減らします。Start()メソッドで、EnemySpawnerを検索して、自身をObserverとして追加します。

以上が、シューティングゲームでよく使われるデザインパターンの一部です。他にも、Template MethodパターンやStateパターンなど、様々なパターンがあります。適切なパターンを選択することで、シューティングゲームの開発を効率化することができます。例えば、AIの行動パターンを管理するStateパターンを使うことで、複雑な状態遷移を簡単に扱うことができます。

5. Factory Methodパターン

Factory Methodパターンは、インスタンスの生成をサブクラスに任せることで、オブジェクトの生成を簡単にするパターンです。シューティングゲームでは、敵やアイテムの生成などでFactory Methodパターンを使うことができます。

以下は、Factory Methodパターンの例です。

public abstract class EnemyFactory
{
    public abstract GameObject CreateEnemy();
}

public class EasyEnemyFactory : EnemyFactory
{
    public override GameObject CreateEnemy()
    {
        return Resources.Load<GameObject>("EasyEnemy");
    }
}

public class MediumEnemyFactory : EnemyFactory
{
    public override GameObject CreateEnemy()
    {
        return Resources.Load<GameObject>("MediumEnemy");
    }
}

public class HardEnemyFactory : EnemyFactory
{
    public override GameObject CreateEnemy()
    {
        return Resources.Load<GameObject>("HardEnemy");
    }
}

public class EnemySpawner : MonoBehaviour
{
    public EnemyFactory enemyFactory;

    private void Start()
    {
        SpawnEnemy();
    }

    private void SpawnEnemy()
    {
        GameObject enemyPrefab = enemyFactory.CreateEnemy();
        Instantiate(enemyPrefab, transform.position, transform.rotation);
    }
}

この例では、EnemyFactoryクラスがFactory Methodパターンを表し、CreateEnemy()メソッドが定義されています。EasyEnemyFactoryMediumEnemyFactoryHardEnemyFactoryクラスは、CreateEnemy()メソッドをオーバーライドして、それぞれ異なる敵のPrefabを返します。

EnemySpawnerクラスは、敵の出現を管理し、SpawnEnemy()メソッドでEnemyFactoryから敵のPrefabを生成して、インスタンスを作成します。EnemyFactoryの具体的な実装は、EnemySpawnerから注入されます。

以上が、シューティングゲームでよく使われるデザインパターンの一部です。デザインパターンは、様々なシーンで使われる汎用的な解決策を提供してくれます。適切なパターンを選択することで、より効率的に、より品質の高いシューティングゲームを開発することができます。