デザインパターンとは?

デザインパターンとは、Unityに限らずソフトウェア開発全般において使われる、設計の定石です。

「こういう問題に対して、こう設計すればうまくいく」という経験則から生まれたテンプレートのようなもので、

プログラミング言語に依存せず幅広く活用する事が出来ます。

ただ言語によっては、デザインパターンをより簡単に実装出来たり、逆に複雑になる場合もあります。

ファクトリメソッド(Factory Method)とは?

ファクトリメソッドパターンとは、インスタンスを生成する際にサブクラスに任せることで、

柔軟で拡張性のあるオブジェクト生成を可能にするパターンです。

通常、インスタンスを作る際に以下の様にnewを使うと思います。

var player = new Player();

ファクトリメソッドパターンでは、newを直接書かず、生成用のクラスに任せます。

それによってオブジェクト生成のルールを柔軟に切り替えられるようにします。

ファクトリメソッドパターンの活用例

Factory Methodパターンは、ゲーム開発の様々な場面で活用できます。

従来のnewによる直接生成の問題点:

  • オブジェクト生成のロジックがコード全体に散在する
  • 新しい種類を追加する際に複数箇所の修正が必要
  • テスト時にモックオブジェクトへの差し替えが困難

Factory Methodパターンのメリット:

  • 生成ロジックが一箇所に集約される
  • 新しい種類の追加が簡単
  • 設定ファイルやScriptableObjectによる外部設定が可能
  • 単体テストでのモック化が容易

ゲーム開発での具体的な活用場面:

  • 敵キャラ生成: 敵の種類、難易度、ステージに応じた敵の生成
  • アイテム生成: レアリティや種類に応じたアイテムの生成
  • UI要素生成: ダイアログ、ポップアップ、メニューの種類別生成
  • エフェクト生成: 攻撃、回復、環境エフェクトの種類別生成
  • 弾丸・projectile生成: 武器の種類に応じた弾丸の生成

実装の例

ゲームの敵キャラを作成するケースについて考えてみます。

まず敵の共通のインターフェイスを定義します。

public interface IEnemy
{
}

インターフェイスを継承して敵キャラ毎のクラスを用意します。

// ゴブリン
public class Goblin : IEnemy
{
}

// ゾンビ
public class Zombie : IEnemy
{
}

// スケルトン
public class Skeleton : IEnemy
{
}

enumを用意し、enumを使ってインスタンスを生成するファクトリクラスを作成します。

public enum EnemyType
{
	Goblin,
	Zombie,
	Skeleton
}

public class EnemyFactory
{
	public static IEnemy Create(EnemyType type)
	{
    return type switch
    {
        EnemyType.Goblin => new Goblin(),
        EnemyType.Zombie => new Zombie(),
        EnemyType.Skeleton => new Skeleton(),
    };
	}
}

以下の様に使用します。

var enemy = EnemyFactory.Create(EnemyType.Zombie);

より実践的なUnityでの実装例

実際のUnityプロジェクトでは、MonoBehaviourを継承したクラスやPrefabを扱うことが多いため、より実用的な実装例を見てみましょう。

敵キャラのMonoBehaviour実装

// 敵キャラの基底クラス
public abstract class Enemy : MonoBehaviour, IEnemy
{
    [SerializeField] protected int _health = 100;
    [SerializeField] protected float _speed = 2f;
    
    public abstract void Initialize();
    public abstract void Attack();
}

// 具体的な敵キャラの実装
public class Goblin : Enemy
{
    public override void Initialize()
    {
        _health = 50;
        _speed = 3f;
    }
    
    public override void Attack()
    {
        // ゴブリンの攻撃処理
    }
}

public class Zombie : Enemy
{
    public override void Initialize()
    {
        _health = 80;
        _speed = 1f;
    }
    
    public override void Attack()
    {
        // ゾンビの攻撃処理
    }
}

Prefabを使ったFactoryの実装

[CreateAssetMenu(fileName = "EnemyFactory", menuName = "Factory/EnemyFactory")]
public class EnemyFactory : ScriptableObject
{
    [System.Serializable]
    public class EnemyData
    {
        public EnemyType type;
        public GameObject prefab;
        public int spawnCost;
    }
    
    [SerializeField] private EnemyData[] _enemyDataArray;
    
    public GameObject CreateEnemy(EnemyType type, Vector3 position, Quaternion rotation)
    {
        var enemyData = System.Array.Find(_enemyDataArray, data => data.type == type);
        
        if (enemyData?.prefab == null)
        {
            Debug.LogError($"Enemy prefab not found for type: {type}");
            return null;
        }
        
        var enemyObject = Instantiate(enemyData.prefab, position, rotation);
        var enemyComponent = enemyObject.GetComponent<Enemy>();
        enemyComponent?.Initialize();
        
        return enemyObject;
    }
    
    public int GetSpawnCost(EnemyType type)
    {
        var enemyData = System.Array.Find(_enemyDataArray, data => data.type == type);
        return enemyData?.spawnCost ?? 0;
    }
}

使用例

public class EnemySpawner : MonoBehaviour
{
    [SerializeField] private EnemyFactory _enemyFactory;
    [SerializeField] private Transform _spawnPoint;
    
    public void SpawnEnemy(EnemyType type)
    {
        if (_enemyFactory != null && _spawnPoint != null)
        {
            var enemy = _enemyFactory.CreateEnemy(type, _spawnPoint.position, _spawnPoint.rotation);
            if (enemy != null)
            {
                Debug.Log($"Spawned {type} at {spawnPoint.position}");
            }
        }
    }
    
    // ランダムな敵を生成
    public void SpawnRandomEnemy()
    {
        var randomType = (EnemyType)Random.Range(0, System.Enum.GetValues(typeof(EnemyType)).Length);
        SpawnEnemy(randomType);
    }
}

この実装では、以下のような利点があります:

  • ScriptableObjectによる設定の外部化: Inspector上でPrefabや設定値を変更可能
  • エラーハンドリング: 存在しない敵タイプに対する適切な処理
  • 拡張性: 新しい敵タイプを追加する際は、ScriptableObjectに設定を追加するだけ
  • 再利用性: 異なるシーンやステージで同じFactoryを再利用可能

Unityで使用する場合はInstantiateなどする必要がありますが、敵キャラの生成時に外からnewInstantiateを呼ばなくても良くなります。

新しく敵クラスを追加する場合もFactoryクラス内に追記するだけで対応出来ます。

注意点

Factory内に生成処理を追記していくため、Factoryクラスが肥大化しやすくなります。

うまく分割したり、ScriptableObjectなどを使用して生成処理をデータ化するなど考える必要があるかもしれません。

具体的な対策例:

  • カテゴリ別のFactory分割: 敵、アイテム、UIなどでFactoryを分ける
  • ScriptableObjectやjsonでのデータ駆動: 生成ルールをコード外で管理する
  • オブジェクトプールとの組み合わせ: パフォーマンスを考慮した実装

パフォーマンスへの配慮:

多数のオブジェクトを頻繁に生成・破棄する場合は、Factoryとオブジェクトプールを組み合わせることで、GCの負荷を減らすことができます。

まとめ

デザインパターンのファクトリメソッドパターンについて簡単に解説しました。

ファクトリメソッドパターンを使うことで、インスタンス生成の責務を集約でき、保守性が向上します。

一方生成する対象が増えるとFactoryクラスが肥大化する可能性があるので注意が必要です。

ゲーム中の敵キャラ、弾丸、アイテム、エフェクトなど、

生成する種類の多いオブジェクトを生成する際に役に立つので

是非活用してみてください。

📣おしらせ!

Unity Asset Storeで Fresh Assets – Fall edition が開催中です。

50%OFFでセール中です。

🔗関連ページ