はじめに
様々な言語で「デザインパターン」の本が世の中にありますが、筆者個人の経験では
いまいちピンとこない例
いまいちピンとこないコード
で説明されてることが多く、
結局これっていつ使うの?
という疑問に答えるには仕事仲間等との議論をしないと
辿り着けないことが多々ありました。
そこで特に「ゲーム開発ではどう使うか?」にフォーカスを当てて、実践的な例を交えて
デザインパターンの説明の需要があると思い記事を作りました。
デザインパターンを学ぶ理由
デザインパターンを学ぶ理由としては
- 車輪の再発明の防止
- 長文で読みにくいコード(可読性の低いコード)を減らす
- コードを疎結合にして変更に強くなる(変更時のコスト・変更箇所を減らす)
- モジュールとして使いまわせるように、コードの再利用性を高める
といった効果を期待できます。
対象読者
Unity 全くの初心者(インストールしただけで触ったことがないような方)はお断りです。
最低限以下のことは理解・経験を積んでおくことが必須になります。
- MonoBehaviour 継承クラスでコードを書いたことがある
- C# のピュアクラスを用いた自作クラスを作ったことがある
- クラスの継承という概念は知っている
そのため、脱・初心者
中級者へのステップアップ
として デザインパターンを学ぶ
のが良いと思います。
デザパタ記事リンク
生成系
構造系
様態・ふるまい系
- Chain of Responsibility パターン
- Command パターン
- Interpreter パターン
- Iterator パターン
- Mediator パターン
- Memento パターン
- Observer パターン
- State パターン
- Strategy パターン(本記事)
- TemplateMethod パターン
- Visitor パターン
Strategy パターンについて
戦略・戦術
を意味するStrategy。
ゲームでは RealTimeStrategy(RTS) などゲームジャンルの一つとして利用されているような用語なので、初耳・初見という人はそこまで多くはない(ことを願います)と思います。
何が戦略なのかというと、「可読性・再利用性を高める戦略」を考えます。
具体的には アルゴリズム
と その呼び出し方
を クラスでまとめて管理(=カプセル化)
してしまおうというものです。
Bridge パターンとの違い
Builder パターンはざっくり言うと パーツ(モジュール)の換装
を容易に行うためのデザインパターンでした。パーツ間で共通のインターフェース(または基底クラス)をもつことで、そのあたりの実現を行なっていました。
ただ、このパーツですが、敵AIを差し替えるという意味では アルゴリズムの換装
もBridgeパターンに入るのでは?と考えることもできます。
結論から言ってしまうと、 言語によってはBridge パターンもStrategyパターンも、 やってることが共通インターフェース越しにメソッドを呼ぶことで、実際に行われる処理を変更しやすくする
だけな場合もあります。
また、初心者への説明として 構造に関するデザインパターン
と ふるまいのデザインパターン
だから という説明は抽象化レベルが非常に高いため筆者個人としてはあまり推奨しません。
強いていえば 初期化時の構成を柔軟に変更できる
のが Bridgeパターン
で、 ランタイムでも柔軟に変更可能
なのが Strategyパターン
という説明もできます。しかし、ゲームでは「戦闘中に状況に応じて行動を変更するのは極々当たり前」な話であるし、ランタイムでパーツ換装なんてことも普通にあります。
このように考えると、インターフェースを使って柔軟に変更できるのは全部 Strategy パターン
でよくね? という発想が出てくるのは当然かと思います。
よくある例として 比較方法を提供する ICompareを実装したクラスがいくつかあり、それらを切り替えるのが Strategyパターンとして紹介されます。
public inteface ICompare
{
int compare(Character a, Character b);
}
public class HPComare:ICompare{
int ICompare.compare(Character a, Character b)
{
if(a == null || b == null )
{
if( a == null && b == null )return 0;//比較不可能なのでそのまま
return a == null ? -1 : 1;
}
return a.HP == b.HP ? 0
: a.HP < b.HP ? -1 : 1;
}
}
public class AtkComare:ICompare{
int ICompare.compare(Character a, Character b)
{
if(a == null || b == null )
{
if( a == null && b == null )return 0;//比較不可能なのでそのまま
return a == null ? -1 : 1;
}
return a.ATK == b.ATK ? 0
: a.ATK < b.ATK ? -1 : 1;
}
}
public class MainLogic
{
Character MyChara;
Character SubChara;
void Update()
{
ICompare comparator = new HPComare();
// ICompare comparator = new AtkComare();
int result = comparator.compare(MyChara, SubChara);
if(result < 0)
{
SubChara.Guard();
}
else if(result > 0)
{
MyChara.CureSelf();
}
}
}
よりゲームっぽい事例を書きましたがUML的には以下の通りで、関係性はBridgeパターンと同じです。
まとめ
処理の差し替えが可能というのは非常に恩恵が大きく、バリエーションが増えた時の対策にもなります。
特にスキルなどは共通処理にしやすく、ロジック変更のみで対応可能なため、Strategyパターンと相性が良いです。
一応思想としては「構造の整理」としてのBridgeパターンと「アルゴリズムの差し替え」のStrategyパターンがありますが、UML的にはほぼ同じなため、突き詰めれば全部Strartegyパターンとみなすことも可能です。
Unity、C#においては分離の粒度に応じて使い分けると良いでしょう。