デザインパターンとは?
デザインパターンとは、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
などする必要がありますが、敵キャラの生成時に外からnew
やInstantiate
を呼ばなくても良くなります。
新しく敵クラスを追加する場合もFactory
クラス内に追記するだけで対応出来ます。
注意点
Factory
内に生成処理を追記していくため、Factory
クラスが肥大化しやすくなります。
うまく分割したり、ScriptableObject
などを使用して生成処理をデータ化するなど考える必要があるかもしれません。
具体的な対策例:
- カテゴリ別の
Factory
分割: 敵、アイテム、UIなどでFactory
を分ける ScriptableObject
やjsonでのデータ駆動: 生成ルールをコード外で管理する- オブジェクトプールとの組み合わせ: パフォーマンスを考慮した実装
パフォーマンスへの配慮:
多数のオブジェクトを頻繁に生成・破棄する場合は、Factory
とオブジェクトプールを組み合わせることで、GCの負荷を減らすことができます。
まとめ
デザインパターンのファクトリメソッドパターンについて簡単に解説しました。
ファクトリメソッドパターンを使うことで、インスタンス生成の責務を集約でき、保守性が向上します。
一方生成する対象が増えるとFactory
クラスが肥大化する可能性があるので注意が必要です。
ゲーム中の敵キャラ、弾丸、アイテム、エフェクトなど、
生成する種類の多いオブジェクトを生成する際に役に立つので
是非活用してみてください。
📣おしらせ!
Unity Asset Storeで Fresh Assets – Fall edition が開催中です。
50%OFFでセール中です。