はじめに
様々な言語で「デザインパターン」の本が世の中にありますが、筆者個人の経験では
いまいちピンとこない例
いまいちピンとこないコード
で説明されてることが多く、
結局これっていつ使うの?
という疑問に答えるには仕事仲間等との議論をしないと
辿り着けないことが多々ありました。
そこで特に「ゲーム開発ではどう使うか?」にフォーカスを当てて、実践的な例を交えて
デザインパターンの説明の需要があると思い記事を作りました。
デザインパターンを学ぶ理由
デザインパターンを学ぶ理由としては
- 車輪の再発明の防止
- 長文で読みにくいコード(可読性の低いコード)を減らす
- コードを疎結合にして変更に強くなる(変更時のコスト・変更箇所を減らす)
- モジュールとして使いまわせるように、コードの再利用性を高める
といった効果を期待できます。
対象読者
Unity 全くの初心者(インストールしただけで触ったことがないような方)はお断りです。
最低限以下のことは理解・経験を積んでおくことが必須になります。
- MonoBehaviour 継承クラスでコードを書いたことがある
- C# のピュアクラスを用いた自作クラスを作ったことがある
- クラスの継承という概念は知っている
そのため、脱・初心者
中級者へのステップアップ
として デザインパターンを学ぶ
のが良いと思います。
デザパタ記事リンク
生成系
構造系
様態・ふるまい系
- Chain of Responsibility パターン
- Command パターン
- Interpreter パターン
- Iterator パターン
- Mediator パターン
- Memento パターン
- Observer パターン
- State パターン
- Strategy パターン
- TemplateMethod パターン
- Visitor パターン
Facade パターンについて
Facade(ファサード, 読み方が特殊なのはフランス語由来だからです)とは、建築デザイン系の用語が元となっており、建物の正面のことを示します。
これを転じてFacadeパターンとは、他のクラスなどからの APIの窓口
となって、内部処理をいい感じに行い
結果を返す
ような、いわゆる 図書館の司書さん
や ホテルのコンシェルジュ
のような役割です。
最近で言えば、 Hey, Siri
, OK, Google
, Alexa
とスマートスピーカーに呼びかける方も多くなってきたと思います。これらのAIはまさにFacadeパターンそのもので、各種複雑な処理を依頼する窓口になってくれており、内部で通信処理など全て行った後結果を音声で返してくれますね。
本来であれば 電気を消す
, 音楽をかける
等のAPIを叩かないと動かないのですが、音声信号を入力として渡すことで、あとはよしなにやってくれますね。
Facadeパターンの重要な役目の一つとして 簡素化された窓口(API)を用意する
ところがポイントです。
もちろん低レベルのレイヤーへアクセスするためのインターフェースは 公開しても良い
です。
あくまでも 従来の複雑なインターフェースを実装・公開
+ 簡素化された窓口を用意
しているのがFacadeパターンの特徴です。
Facadeパターンのメリット・デメリット
一見するとメリットが多いFacadeパターンですが、 簡素化した窓口
にも用意する数によってはデメリットにもなりえます。
メリット
- 複雑なインターフェースを考慮しないで済むAPIがある
- 設計などを気にしなくても利用可能
デメリット
-
わからないから
という理由でAPIを増やされがちになる (設計力の敗北) - せっかく共通化の目的で
インターフェース
を切っているのに叩かれない場合がある - Facade専用の簡素化されたAPIは固有のものが多く、保守性にかける
- メソッドを安易に生やしがちになりやすいため、メソッド数が肥大化しやすい(可読性が下がる恐れがある)
ゲームにおける Facade
ほとんどの場合 Singletonパターン と合わせて使われることが多いです。
SoundManager
やら APIManager
やら Resource(Addressable)Manager
やら SaveDataManager
...。
ここでは例えとして SoundManager
を考えてみます。
通常、音を再生するときは Play()
のAPIをコールしますが、ちゃんと音の再生フローを考えると以下のようになります。
リソース管理周りの処理が結構複雑で、これをそれぞれのDownload/Load APIを順番にコールさせるのは使用者にとって酷です。
SoundManager はあくまでも Play()
というAPIだけ用意してあげて、その中身の処理はSoundManager側が頑張って対応します。
適切に処理が分割されていれば、各処理はLoader/Downloaderに依頼してその結果を元に再生準備を行うように作るでしょう。
もし、Play()という窓口がなければ愚直にやると以下のような形になります。
AudioSource audioSource;
public async UniTask<bool> PlayAsync(int soundId, CancellationToken token)
{
if( SoundManager.Instance.Loader.TryGetLoadedResources( soundId, out AudioClip clip))
{
this.audioSource.clip = clip;
this.audioSource.Play();
return;
}
if( !SoundManager.Instance.Loader.TryGetResources(soundId, out AudioClip clip))
{
try
{
var clip = await SoundManager.Instance.Downloader.GetResourceAsync(soundId, token);
SoundManager.Instance.Loader.Register(soundId, clip);
this.audioSource.clip = clip;
this.audioSource.Play();
}
catch (OperationCanceledException ex)
{
}
catch (Exception e)
{
Debug.LogError(e);
}
}
else
{
this.audioSource.clip = clip;
this.audioSource.Play();
}
}
これをSoundManagerの外側で毎回書くのは辛いですし、リソースの取得周りのインターフェースを公開されていたとしても、どう扱うのか?は上記のフローチャートを頭に入っている人しかちゃんと実装できません。
そのため、この処理をとりまとめた SoundManager.Instance.Play(int soundId)
や SoundManager.Instance.PlayAsync(int soundId, CancellationToken token)
のような窓口を作ってあげると他の開発者に優しいシステムになります。
まとめ
FacadeパターンはSingletonパターンと一緒に利用されることが非常に多いデザインパターンです。
便利なメソッドを公開することで簡単に使える反面、保守性が下がるメソッドが増えがちになるため、公開するメソッド数は肥大化しないように気をつけないといけません。