はじめに
様々な言語で「デザインパターン」の本が世の中にありますが、筆者個人の経験では
いまいちピンとこない例
いまいちピンとこないコード
で説明されてることが多く、
結局これっていつ使うの?
という疑問に答えるには仕事仲間等との議論をしないと
辿り着けないことが多々ありました。
そこで特に「ゲーム開発ではどう使うか?」にフォーカスを当てて、実践的な例を交えて
デザインパターンの説明の需要があると思い記事を作りました。
デザインパターンを学ぶ理由
デザインパターンを学ぶ理由としては
- 車輪の再発明の防止
- 長文で読みにくいコード(可読性の低いコード)を減らす
- コードを疎結合にして変更に強くなる(変更時のコスト・変更箇所を減らす)
- モジュールとして使いまわせるように、コードの再利用性を高める
といった効果を期待できます。
対象読者
Unity 全くの初心者(インストールしただけで触ったことがないような方)はお断りです。
最低限以下のことは理解・経験を積んでおくことが必須になります。
- MonoBehaviour 継承クラスでコードを書いたことがある
- C# のピュアクラスを用いた自作クラスを作ったことがある
- クラスの継承という概念は知っている
そのため、脱・初心者
中級者へのステップアップ
として デザインパターンを学ぶ
のが良いと思います。
デザパタ記事リンク
生成系
構造系
様態・ふるまい系
- Chain of Responsibility パターン
- Command パターン
- Interpreter パターン
- Iterator パターン
- Mediator パターン
- Memento パターン
- Observer パターン
- State パターン
- Strategy パターン
- TemplateMethod パターン
- Visitor パターン
Composite パターンについて
Composite パターンとはその名の通り 複合
という意味です。
端的に言うと、Compositeパターンなクラスとは 共通のインターフェース
を実装し、クラス内部で 共通のインターフェースのList
を抱えているようなクラスです。
このCompositeクラスのメリットは 外側からは 1つのObjectに見える/1つのObjectとして操作する
ところです。
内部には複数のObjectを保持しているので、CompositeパターンのObjectでAPIがコールされると内部のリストに対して同じAPIをfor文で実行します。
つまり、 まとめて処理を実行したい
, 複数のObjectをとりまとめた結果を得たい
ような場合には打ってつけのデザインパターンです。
ゲームでの利用例
UniRx
Unity でいえば有名なリアクティブプログラミングのライブラリである UniRx では CompositeDisposable
というクラスで実装・利用されます。
これはIDisposable をとりまとめたクラスです。
最後にIDisposable実装ObjectそれぞれにDispose()をコールする時、コール忘れが起きるとメモリリーク等の原因になるため、このクラスを利用して登録しておくことで、Dispose処理は CompositeDisposable のクラス1個に対して発行するだけで、内部に登録してあるObject全てをDisposeしてくれるため、破棄処理が複雑にならずに済みます。
スキルの実装
RPGやヒーローシューター等一部のゲームではキャラクターに「スキル」を持たせることがあります。
このスキルですが、下位レベルのものだと単一の効果ですが、上位レベルのスキルだと効果が複数あるものはよくある話です。
この 効果が複数あるスキル
を作るためにCompositeパターンはほぼ必須といっても過言ではありません。
具体例として回復スキルを考えてみましょう。
回復スキルとして HPを回復するスキル
と 状態異常を治すスキル
に分かれると思います。
ここでHPを全回復スキルと状態異常を完全治癒するスキルがあった時に、新たに HPも状態異常も完全回復するスキル
を作りたいとプランナーに言われたとします。
このときにスキルを新規で0から実装する場合、 HPを全回復スキル
と 状態異常を完全治癒するスキル
と同じ処理を書かなければいけません。これでは車輪の再発明で、重複コードも増え、アプリ容量の増加にもつながります(スマホアプリでは容量限界がシビアです)。この時にCompositeパターンで作るとコードの重複を防げます。
実際のコード例を見ていきましょう。
public interface ISKill
{
void Invoke(Character[] targets);
}
public class CompositeSkill : ISkill, ICollection<ISkill>
{
protected readonly object _gate = new object();
protected List<ISkill> list = new List<ISkill>();
#region ===== ISkill =====
void ISkill.Invoke(Character[] targets)
{
if(targets == null) return;
foreach(var skill in list)
{
skill.Invoke(targets);
}
}
#endregion //) ===== ISkill =====
#region ===== ICollection =====
public int Count => list.Count;
public bool IsReadOnly => false;
public void Add(IAudioLoader item){list.Add(item);}
public void Clear(){list.Clear();}
public bool Contains(IAudioLoader item)
{
return list.Contains(item);
}
public void CopyTo(IAudioLoader[] array, int arrayIndex)
{
int startIndex = Mathf.Min(arrayIndex, array.Length - 1);
for (int i =startIndex; i < array.Length; i++)
{
Add(array[i]);
}
}
public bool Remove(IAudioLoader item)
{
return list.Remove(item);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public IEnumerator<IAudioLoader> GetEnumerator()
{
var res = new List<IAudioLoader>();
lock (_gate)
{
foreach (var d in list)
{
if (d != null) res.Add(d);
}
}
return res.GetEnumerator();
}
#endregion //) ===== ICollection =====
}
/// <summary>
/// HP完全回復スキル
/// </summary>
public class FullHealSkill : ISkill
{
void ISkill.Invoke(Character[] targets)
{
if(targets == null) return;
foreach(var target in targets)
{
target.Heal(100.0f);
}
}
}
/// <summary>
/// 状態異常回復スキル
/// </summary>
public class FullCure : ISkill
{
void ISkill.Invoke(Character[] targets)
{
if(targets == null) return;
foreach(var target in targets)
{
target.ClearDebuffAll();
}
}
}
/// <summary>
/// 完全回復スキル
/// </summary>
public class PerfectHeal : CompositeSkill
{
public PerfectHeal()
{
// 完全回復 = HP全回復 + 全状態異常回復
list.Add(new FullHealSkill());
list.Add(new FullCure());
}
}
このようにCompositeパターンを用意すると、組み合わせで表現できるスキルの実装が非常にシンプルになります。
特に、複合スキルについては、複合元のスキルが独立して動くように実装されていれば、実装工数がほとんど0に近い状態まで減らすことができます。
クラス図でいうと以下のようになります。
まとめ
Compositeパターンはゲーム系でもとても有益なデザインパターンです。
特に複雑なものを作成・管理する時に威力を発揮します。
実際の開発現場では理解がほぼ必須といっても過言でないくらい重要なので、もし知らなかった場合は覚えておくと良いと思います。