はじめに
様々な言語で「デザインパターン」の本が世の中にありますが、筆者個人の経験では
いまいちピンとこない例
いまいちピンとこないコード
で説明されてることが多く、
結局これっていつ使うの?
という疑問に答えるには仕事仲間等との議論をしないと
辿り着けないことが多々ありました。
そこで特に「ゲーム開発ではどう使うか?」にフォーカスを当てて、実践的な例を交えて
デザインパターンの説明の需要があると思い記事を作りました。
デザインパターンを学ぶ理由
デザインパターンを学ぶ理由としては
- 車輪の再発明の防止
- 長文で読みにくいコード(可読性の低いコード)を減らす
- コードを疎結合にして変更に強くなる(変更時のコスト・変更箇所を減らす)
- モジュールとして使いまわせるように、コードの再利用性を高める
といった効果を期待できます。
対象読者
Unity 全くの初心者(インストールしただけで触ったことがないような方)はお断りです。
最低限以下のことは理解・経験を積んでおくことが必須になります。
- MonoBehaviour 継承クラスでコードを書いたことがある
- C# のピュアクラスを用いた自作クラスを作ったことがある
- クラスの継承という概念は知っている
そのため、脱・初心者
中級者へのステップアップ
として デザインパターンを学ぶ
のが良いと思います。
デザパタ記事リンク
生成系
構造系
様態・ふるまい系
- Chain of Responsibility パターン
- Command パターン
- Interpreter パターン
- Iterator パターン
- Mediator パターン
- Memento パターン
- Observer パターン
- State パターン
- Strategy パターン
- TemplateMethod パターン
- Visitor パターン
AbstractFactory パターンについて
AbstractFactory(アブストラクトファクトリー)パターンとは 同一のものを複数回、生成がリクエストされるようなケース
で用いられるデザインパターンです。名称が長いので Abstract(抽象)を省略して Factoryパターンを呼称する場合もありますが、別記事で説明するFactoryMethod パターンとどちらを指すのか?という問題もあるので、省略するときはご注意ください。
さて、名前の通り「抽象的な工場」ですが、これは 工場の抽象クラスを作ると便利
程度の認識で大丈夫です。
具体的なFactory(SimpleFactory などとも呼ばれたりする) を考えても良いのですが、もう少し使いまわせる仕組みとして1段階抽象化レベルを上げて「汎用的な工場に必要な仕組み」を考えた方が再利用性が高いコードを作ることが可能になります。
ゲームにおける AbstractFactoryパターン
ではゲームにおける 工場
とは何でしょうか? Satisfactory のような工場シミュレーションゲームそのものではありません(もちろん内部実装ではAbstractFactoryパターンを使っているとは思いますが)。
いくつか実例を考えてみましょう。
ゲームカテゴリ | 利用例 |
---|---|
(弾幕)シューティングゲーム | 自機の弾の生成役(スポナー) |
アクションゲーム | 無限湧きする敵のスポナー |
戦略シミュレーション, RTS | ユニット作成時のスポナー |
このように考えるといわゆる スポナー
と呼ばれる機能= 工場
と考えて良いでしょう。
工場の特色
工場の特色としては以下のとおりです。
- 生成リクエストが来たら納品物を工場内で作成→納品
- リクエスト側は生成過程を見ることが出来ない(仮に工場見学できたとしても生成過程に口出しは出来ない)
AbstractFactory パターンを使わない実装
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) があると言えるでしょう。
つまりは以下のような形です。
これを継承して「スキルを持った敵」と「スキルを持たない敵」の生成クラスを考えてみましょう。
クラス図を作ると以下のような形です。
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
}
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;
}
}
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の再生成
に使われることが多く、「弾」や「敵」の生成などにクリティカルに使われるデザインパターンであることがわかりました。