はじめに
様々な言語で「デザインパターン」の本が世の中にありますが、筆者個人の経験では
いまいちピンとこない例
いまいちピンとこないコード
で説明されてることが多く、
結局これっていつ使うの?
という疑問に答えるには仕事仲間等との議論をしないと
辿り着けないことが多々ありました。
そこで特に「ゲーム開発ではどう使うか?」にフォーカスを当てて、実践的な例を交えて
デザインパターンの説明の需要があると思い記事を作りました。
デザインパターンを学ぶ理由
デザインパターンを学ぶ理由としては
- 車輪の再発明の防止
- 長文で読みにくいコード(可読性の低いコード)を減らす
- コードを疎結合にして変更に強くなる(変更時のコスト・変更箇所を減らす)
- モジュールとして使いまわせるように、コードの再利用性を高める
といった効果を期待できます。
対象読者
Unity 全くの初心者(インストールしただけで触ったことがないような方)はお断りです。
最低限以下のことは理解・経験を積んでおくことが必須になります。
- MonoBehaviour 継承クラスでコードを書いたことがある
- C# のピュアクラスを用いた自作クラスを作ったことがある
- クラスの継承という概念は知っている
そのため、脱・初心者
中級者へのステップアップ
として デザインパターンを学ぶ
のが良いと思います。
デザパタ記事リンク
生成系
構造系
様態・ふるまい系
- Chain of Responsibility パターン
- Command パターン
- Interpreter パターン
- Iterator パターン
- Mediator パターン
- Memento パターン
- Observer パターン
- State パターン
- Strategy パターン
- TemplateMethod パターン
- Visitor パターン
Singleton パターンについて
ちょっとゲーム開発を行ったことがある人であれば見たことあるくらい利用頻度が高いデザインパターンの一つです。
Singleton とは数学的には 要素が1個だけの集合
というときに使われる単語で、その名の通りインスタンスが1個しか存在しない(2個以上は存在させない)ようにするデザインパターンです。
ゲームにおける使い方
ユーザーデータとかマスターデータと呼ばれるゲームの設定ファイルからデータを参照したい場合にSingleton で作ったクラス経由がデータをロードしておいて、その他のクラスからはデータにアクセスするときにSingleton クラスを経由してアクセスする事が一番多いかと思います。
あとは、後述するFacadeパターンの実現の方法として使われます。
特に機能の基盤的な存在(サーバーとの通信基盤、サウンド基盤等)ではFacadeパターン + Singleton パターンは強力に力を発揮する事が出来ます。
Unity での実現方法
基本的な方針としては以下を満たせば作れます。
- private でstatic な自分自身と同じ型のメンバ変数を持っている
- クラスのインスタンス生成タイミング(Awake() やコンストラクタ) で1.のインスタンス存在チェック
- 存在しなければメンバ変数にインスタンス自身を設定
- 既に存在すれば、自身のインスタンスを即座に破棄
MonoBehaviour 継承Singleton
MonoBehaviour を継承しつつ、ユニークにしか存在しないクラスを作りたい場合があります。
もう少し具体的にはComponent としてくっつけておいて、Scene内では1つか存在させない、又はアプリ内に1つしか存在させないようにしたい場合です。
MonoBehaviour を継承しておくと Singleton なのに [SerializeField] で参照を持たせておく事が可能
Inspector 拡張でパラメータを確認することが可能
GetComponent等GameObject が必要なコードに対して制約がない
等、いろいろなメリットがあります。
さて、この MonoBehaviour継承Singletonの作り方はさまざまな技術ブログ等で紹介されていますが、実はいくつか課題があるので筆者なりの作り方を紹介します。
using System;
using UnityEngine;
using UnityEngine.Assertions;
/// <summary>
/// MonoBehaviour継承したSingleton
/// </summary>
/// <typeparam name="TYpe">自身のクラス</typeparam>
public abstract class SingletonMonoBehaviour<TYpe> : MonoBehaviour, IDisposable where TYpe : MonoBehaviour
{
private static TYpe instance;
public static TYpe Instance
{
get
{
Assert.IsNotNull(instance,"There is no object attached " + typeof(TYpe).Name );
return instance;
}
}
/// <summary>
/// 存在チェック
/// </summary>
/// <returns>True:存在, False:インスタンスが無い</returns>
public static bool IsExist() { return instance != null;}
private void Awake()
{
if (instance != null && instance.gameObject != null)
{
Destroy(this.gameObject);
return;
}
instance = this as TYpe;
OnAwakeProcess();
}
/// <summary>
/// 派生先でも初期化処理を書くためのAPI
/// </summary>
protected virtual void OnAwakeProcess(){}
/// <summary>
/// Destroy時処理
/// 派生先で実行漏れが無いように意図的にPrivate
/// </summary>
private void OnDestroy()
{
// 自身以外のインスタンスが作成→即時破棄されるときに間違って実行されないようにブロック
if(instance != (this as TYpe)) return;
OnDestroyProcess();
Dispose();
}
/// <summary>
/// Destroy時処理
/// </summary>
protected virtual void OnDestroyProcess(){
}
public virtual void Dispose()
{
if(IsExist()) instance = null;
}
}
using System;
using UnityEngine;
using UnityEngine.Assertions;
public class SoundManager : SingletonMonoBehaviour<SoundManager>
{
(中略)
}
ポイント
- 他の技術ブログ等ではInstance のGetプロパティで FindObjectType() でオブジェクトをとってきてる事がありますが、Find系の関数は基本的に「重い」ので特に
モバイル環境などでは使用禁止レベル
な事が多いです。そのため、ちゃんとAwake で存在チェックをして、Getプロパティは単純にSingleton インスタンスのGetterに留めています。 - Awake/OnDestroy は派生先で上書きされて規定動作が実行されないようにするため意図的にPrivateにしてます
- その代わりprotected virtual な初期化用/終了用メソッドを用意してあげています
- もしアプリ内で唯一のInstance しか認めたく無い場合には、OnAwakeProcess のところに
DontDestroyOnLoad(this)
を追記してあげれば問題ないです
ピュアクラスのSingleton
Singleton は常にMonoBehaviour を継承する必要はありません。
ピュアクラスでも当然Singleton なクラスが必要な場合もあります。
以下にピュアクラスの場合のサンプルを載せておきます。
using System;
/// <summary>
/// ピュアクラスのSingletonクラス
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class Singleton<T> : IDisposable where T : class, new()
{
private static T instance = null;
/// <summary>
/// Instanceの糖衣構文
/// </summary>
public static T I => Instance;
public static T Instance
{
get
{
CreateInstance();
return instance;
}
}
/// <summary>
/// Instance 生成
/// </summary>
public static void CreateInstance()
{
if (instance == null)
{
instance = new T();
}
}
/// <summary>
/// Instanceが存在するかどうか
/// </summary>
/// <returns></returns>
public static bool IsExists()
{
return instance != null;
}
/// <summary>
/// 破棄処理
/// </summary>
public virtual void Dispose()
{
instance= null;
}
}
Singleton のアンチパターン
Facade パターンと合わせることで強力なSingleton パターンですが、どのクラスからでもアクセス可能になる反面、なんでもかんでもSingleton に任せてしまうことがあります。
特に設計に詳しくなかったり、納期が厳しく忙殺されていると、とりあえずSingleton にメソッドやプロパティを追加
することで、Singleton クラスが1000行、10000行とどんどん増えていってしまいます。
このような可読性もへったくれもないコードになると運用・保守のコストだけでも開発の時間をほとんどを持っていかれてしまいます。そのため、 適切に機能をモジュールとして切り出す
, 無駄にSingleton に機能を追加しない
ことで、Singleton クラスの肥大化を防ぐようにしないと特に運用系コンテンツ作成時に苦労することになります。
解決法として DIの導入
という方もいますが、「DI」と「コードの肥大化」は全くの別問題です。
DIは XXXManager.Instance.~ のようにクラスへの依存が問題を解決できます。しかしDIを使ったところで、Singleton クラスの肥大化は改善されません。
そのため、何千行もいかないように常に クラスの責務が適切か?
ということを考えるのが一番の解決方法になります。
まとめ
他のComponent やクラスとやりとりするのに便利なSingleton クラスですが、使い方によっては長文スパゲティコードを簡単に作れてしまう弊害もあります。
また、Find 系APIを叩く場合、使い方によっては激重コードになるので実装方法も考えて実装していきましょう。