LoginSignup
16
16

【Unity】Unityで学ぶデザインパターン01: AbstractFactory パターン【デザパタ】

Last updated at Posted at 2023-03-03

はじめに

様々な言語で「デザインパターン」の本が世の中にありますが、筆者個人の経験では
いまいちピンとこない例 いまいちピンとこないコード で説明されてることが多く、
結局これっていつ使うの? という疑問に答えるには仕事仲間等との議論をしないと
辿り着けないことが多々ありました。

そこで特に「ゲーム開発ではどう使うか?」にフォーカスを当てて、実践的な例を交えて
デザインパターンの説明の需要があると思い記事を作りました。

デザインパターンを学ぶ理由

デザインパターンを学ぶ理由としては

  1. 車輪の再発明の防止
  2. 長文で読みにくいコード(可読性の低いコード)を減らす
  3. コードを疎結合にして変更に強くなる(変更時のコスト・変更箇所を減らす)
  4. モジュールとして使いまわせるように、コードの再利用性を高める
    といった効果を期待できます。

対象読者

Unity 全くの初心者(インストールしただけで触ったことがないような方)はお断りです。
最低限以下のことは理解・経験を積んでおくことが必須になります。

  • MonoBehaviour 継承クラスでコードを書いたことがある
  • C# のピュアクラスを用いた自作クラスを作ったことがある
  • クラスの継承という概念は知っている

そのため、脱・初心者 中級者へのステップアップ として デザインパターンを学ぶ のが良いと思います。

デザパタ記事リンク

生成系

  1. AbstractFactory パターン(本記事)
  2. Builder パターン
  3. FactoryMethod パターン
  4. Prototype パターン
  5. Singleton パターン

構造系

  1. Adapter パターン
  2. Bridge パターン
  3. Composite パターン
  4. Decorator パターン
  5. Facade パターン
  6. Flyweight パターン
  7. Proxy パターン

様態・ふるまい系

  1. Chain of Responsibility パターン
  2. Command パターン
  3. Interpreter パターン
  4. Iterator パターン
  5. Mediator パターン
  6. Memento パターン
  7. Observer パターン
  8. State パターン
  9. Strategy パターン
  10. TemplateMethod パターン
  11. Visitor パターン

AbstractFactory パターンについて

AbstractFactory(アブストラクトファクトリー)パターンとは 同一のものを複数回、生成がリクエストされるようなケース で用いられるデザインパターンです。名称が長いので Abstract(抽象)を省略して Factoryパターンを呼称する場合もありますが、別記事で説明するFactoryMethod パターンとどちらを指すのか?という問題もあるので、省略するときはご注意ください。

さて、名前の通り「抽象的な工場」ですが、これは 工場の抽象クラスを作ると便利 程度の認識で大丈夫です。
具体的なFactory(SimpleFactory などとも呼ばれたりする) を考えても良いのですが、もう少し使いまわせる仕組みとして1段階抽象化レベルを上げて「汎用的な工場に必要な仕組み」を考えた方が再利用性が高いコードを作ることが可能になります。

ゲームにおける AbstractFactoryパターン

ではゲームにおける 工場 とは何でしょうか? Satisfactory のような工場シミュレーションゲームそのものではありません(もちろん内部実装ではAbstractFactoryパターンを使っているとは思いますが)。

いくつか実例を考えてみましょう。

ゲームカテゴリ 利用例
(弾幕)シューティングゲーム 自機の弾の生成役(スポナー)
アクションゲーム 無限湧きする敵のスポナー
戦略シミュレーション, RTS ユニット作成時のスポナー

このように考えるといわゆる スポナー と呼ばれる機能= 工場 と考えて良いでしょう。

工場の特色

工場の特色としては以下のとおりです。

  • 生成リクエストが来たら納品物を工場内で作成→納品
  • リクエスト側は生成過程を見ることが出来ない(仮に工場見学できたとしても生成過程に口出しは出来ない)

AbstractFactory パターンを使わない実装

NonFactoryEnemy.cs
using System.Collections.Generic;
using UnityEditor;

public class Enemy : MonoBehaviour
{
    /* parameters */
    public string Name{get;set;}
    public int Hp{get;set;}
    public int MaxHp{get;set;}
    public int Mp{get;set;}
    public int MaxMp{get;set;}
    public int Atk{get;set;}
    public int Def{get;set;}
    public int Speed{get;set;}
    public Skill EnemySkill{get;set;}
}

public class GameCycle : MonoBahaviour
{
    const int MAX_ENEMY_COUNT = 10;
    List<Enemy> enemys = new List<Enemy>(MAX_ENEMY_COUNT); // Listのキャパシティは要件に応じて設定しようね
    void Update()
    {
        if(enemys.Count < MAX_ENEMY_COUNT )
        {
            var e = (enemys.Count % 2) == 0 
                        ? Instantiate<Enemy>(SKILLED_PREFAB_PATH)
                        : Instantiate<Enemy>(NON_SKILL_PREFAB_PATH);
            e.Name = "Enemy_"+enemys.Count;
            e.Hp = 10;
            e.MaxHp = 10;
            e.Mp = 1;
            e.MaxHp = 1;
            e.Atk = 2;
            e.Def = 1;
            e.Speed =1;
            e.EnemySkill = (enemys.Count % 2) == 0 ? new EnemySkill_Fire() : null ;

            enemys.Add(e);
        }
    }
}

このように敵が一定数以下になったら敵を生成するコードを考えると、パラメータ設定をゲームのメインサイクルに書くことになります。
また、EnemySkill の部分ですが、今回はSkill クラスということで、プレイヤーSkill と敵用Skill が同じ派生元の場合、Enemy.EnemySkill の部分に最悪プレイヤー用Skill を設定することも可能です。

上記からわかるとおり、以下のような課題があります。

  • パラメータ設定を逐一設定するコードを書かないといけない
  • 間違った入力が混入する可能性がある

AbstractFactory を使った実装

今回の件を考えると工場は Spawn() という生成用メソッド(生成API) があると言えるでしょう。

つまりは以下のような形です。

これを継承して「スキルを持った敵」と「スキルを持たない敵」の生成クラスを考えてみましょう。
クラス図を作ると以下のような形です。

Spawner.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class BaseSpawnParameter
{
}
public abstract class BaseSpawnObject : MonoBehaviour
{
}

public abstract class BaseSpawner
{
    public abstract BaseSpawnObject Spawner(TParam p) where TParam:BaseSpawnParameter
}

Enemy.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Assertions;

public class Enemy : BaseSpawnObject
{
    /* parameters */
    public string Name{get;set;}
    public int Hp{get;set;}
    public int MaxHp{get;set;}
    public int Mp{get;set;}
    public int MaxMp{get;set;}
    public int Atk{get;set;}
    public int Def{get;set;}
    public int Speed{get;set;}
    public Skill EnemySkill{get;set;}
}
public class EnemySpawnParam : BaseSpawnParameter
{
    /* parameters */
    public string Name{get;set;}
    public int Hp{get;set;}
    public int MaxHp{get;set;}
    public int Mp{get;set;}
    public int MaxMp{get;set;}
    public int Atk{get;set;}
    public int Def{get;set;}
    public int Speed{get;set;}
    public Skill EnemySkill{get;set;}
}

public class NonSkillEnemySpawner : BaseSpawner
{
    private static readonly PREFAB_PATH = "Resources/NonSkillEnemy.prefab";

    public override BaseSpawnObject Spawner(TParam p) where TParam:BaseSpawnParameter
    {
        var enemyParam = p as EnemySpawnParam;
        var e = GameObject.Instantiate<Enemy>(PREFAB_PATH);
        Assert.IsNotNull(e, "[NonSkillEnemySpawner] e is NULL");
        e.Name = enemyParam.Name;
        e.Hp = enemyParam.Hp;
        e.MaxHp = enemyParam.MaxHp;
        e.Mp = enemyParam.Mp;
        e.MaxMp = enemyParam.MaxMp;
        e.Atk = enemyParam.Atk;
        e.Def = enemyParam.Def;
        e.Speed = enemyParam.Speed;
        e.EnemySkill = null;
        return e;
    }
}

public class SkilledEnemySpawner : BaseSpawner
{
    private static readonly PREFAB_PATH = "Resources/SkilledEnemy.prefab";

    public override BaseSpawnObject Spawner(TParam p) where TParam:BaseSpawnParameter
    {
        var enemyParam = p as EnemySpawnParam;
        var e = GameObject.Instantiate<Enemy>(PREFAB_PATH);
        Assert.IsNotNull(e, "[NonSkillEnemySpawner] e is NULL");
        e.Name = enemyParam.Name;
        e.Hp = enemyParam.Hp;
        e.MaxHp = enemyParam.MaxHp;
        e.Mp = enemyParam.Mp;
        e.MaxMp = enemyParam.MaxMp;
        e.Atk = enemyParam.Atk;
        e.Def = enemyParam.Def;
        e.Speed = enemyParam.Speed;
        e.EnemySkill = enemyParam.Skill;
        return e;
    }
}
Main.cs
public class GameCycle : MonoBahaviour
{
    const int MAX_ENEMY_COUNT = 10;
    NonSkillEnemySpawner nonSkillEnemySpawner = new NonSkillEnemySpawner();
    SkilledEnemySpawner skillEnemySpawner = new SkilledEnemySpawner();
    EnemySpawnParam spawnParam = new EnemySpawnParam();
    List<Enemy> enemys = new List<Enemy>(MAX_ENEMY_COUNT); // Listのキャパシティは要件に応じて設定しようね

    void Awake()
    {
            spawnParam.Hp = 10;
            spawnParam.MaxHp = 10;
            spawnParam.Mp = 1;
            spawnParam.MaxHp = 1;
            spawnParam.Atk = 2;
            spawnParam.Def = 1;
            spawnParam.Speed =1;
            spawnParam.EnemySkill = new EnemySkill_Fire();
    }
    void Update()
    {
     
        if(enemys.Count < MAX_ENEMY_COUNT )
        {
            spawnParam.Name = "Enemy_"+enemys.Count;
            var e = GetSpawner(enemys.Count).Spawn(spawnParam);
            enemys.Add(e);
        }
    }

    BaseSpawner GetSpawner(int count)
    {
        if( count == 0 ) return skillEnemySpawner;
        else return  nonSkillEnemySpawner
    }
}

このように工場自体を分割すれば、スキル無しの敵にスキルが誤って設定されることは防止できます。
さらに、使用するPrefab を取り違えるような間違いもかなり防げるようになっています。

ゲームロジックを書くメインのコードが非常にスッキリしました。

確かにFactoryパターンであるSpawner を作る手間はあります。
しかし、派生系を沢山作る場合に、if文でどのパラメータを書き換えるかを考えるよりも、どのスポナー(Fatory) を使うかを考えるだけ(GetSpawner() のところのみ) に修正コストを減らすことが可能なため、バリエーションが多くなりがちなところで強力に効果を発揮します。

まとめ

AbstractFactory パターンを使うと単に同じObjectを複数生成するときに、生成したい側は生成関数を呼ぶだけ 生成担当側にロジックを分離 することが出来ます。

特にゲームにおいては ObjectPool と組み合わせてObjectの再生成 に使われることが多く、「弾」や「敵」の生成などにクリティカルに使われるデザインパターンであることがわかりました。

16
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
16