テラシュールブログ

旧テラシュールウェアブログUnity記事。主にUnityのTipsやAR・VR、ニコニコ動画についてのメモを残します。

もっと楽なシングルトンの実装

以前にUnityのSingleton実装方法を紹介したけど、もっと良さそうな方法があったので紹介。と言っても、実は既に紹介していたりするけど、使い方までは分かってなかったので。

[Unity3D]シングルトンなオブジェクトを作る
http://terasur.blog.fc2.com/blog-entry-203.html

改良版を作りました。

Unityで少しだけ高速なシングルトン



■Awakeに依存していた過去のシングルトン
過去のシングルトンはAwakeの実行に依存する形で設計されており、Awakeのタイミングで呼び出す事ができなかった。例えばAwakeに処理を配置するとInstanceを設定する前に参照してしまい、最悪ぬるぽ(ガ)が発生していた。

それを避けるためにAwakeでコールしたい場合はインスペクターに参照値を書いておく感じの事が必要だったが、これが異なるプレハブを参照していた場合、プレハブ化したタイミングで参照が剥がれたりする。一昨日のゲームジャムではこの問題に悩まされる事もあった。

それともう一つ、
単純にコードを書くのが面倒くさかった。基本的にコピペで良いとは言え、Awake前後にSingletonのコードがゴチャゴチャとあるのは見栄えが良くない。そして同じコードを何度も書くのは気分が良くない。



■コードを楽に設定しようとする。が、ダメ…
コードを何度も書くのが面倒なんで、ジェネリックで何とかすることにした。このコードは失敗作だが、かなり勉強になったので晒しておこうと思う。

やっている事は単純で、ジェネリックを使用してインスタンスの型を取得する。SetInstanceを可能な限り早いタイミング(結局Awake)でコールする感じ。(単純なジェネリックはFind系で探すことができなかった為、Awakeでコールする形で実装している)

public class SingletonMonoBehaviour<T> : MonoBehaviour {

public static T Instance{ get; private set; }
protected bool SetInstance(T instance)
{
if( Instance == null)
{
Instance = instance;
return true;
}else{
Destroy(this);
return false;
}
}
}

使い方はこんなかんじになる。

public class SingletonTest : SingletonMonoBehaviour<SingletonTest> {
void Awake()
{
SetInstance(this);
}
}

さて、このコードは問題がある。
実に単純な問題なのだが、実はこのコードはジェネリックを使っているせいか、シーン切り替えしてもInstanceの参照を綺麗にしてくれない。お陰でオブジェクトは消えたがインスタンスが消えていないといった面倒な問題が発生する。Destroy時にnullを入れてやろうとも思ったが、ジェネリックなのでnullが入らない。
それに、やはりAwake以前で参照を再設定したい。

■Awakeに対応するSingleton
で、今日の本題。
最近似たようなコードを見た覚えがあったので、色々と探ってみると@warapuriさんが翻訳してくれた「Unity開発に関する50のTips 〜ベストプラクティス〜」にて、完成形と思わしきコードが。

Unity開発に関する50のTips 〜ベストプラクティス〜(No hack, no work)
http://warapuri.tumblr.com/post/28972633000/unity-50-tips

ということで早速参考に。ただインスタンスを削除してシングルトンにする仕組みが抜けていたので追加。(削除の実装は実装側がやるべきとの判断かもしれないが)
コードはこんな感じ。
public class SingletonMonoBehaviour<T> : MonoBehaviour where T : MonoBehaviour
{
private static T instance;
public static T Instance {
get {
if (instance == null) {
instance = (T)FindObjectOfType (typeof(T));

if (instance == null) {
Debug.LogError (typeof(T) + "is nothing");
}
}

return instance;
}
}

protected void Awake()
{
CheckInstance();
}

protected bool CheckInstance()
{
if( this == Instance){ return true;}
Destroy(this);
return false;
}
}
で、使い方はこんなかんじで行う。
public class SingletonTest : SingletonMonoBehaviour<SingletonTest>
{
}
これの良い所は、Findで参照を行なっているのでAwakeのタイミングで参照することも可能なところ。インスタンスの削除はAwakeのタイミングで行う事になるけど、参照関連の整理はAwke以前で行うため初期化周りがかなり楽になる。

Awakeが親クラスで実装しているので不要だが、Awakeを使いたい場合はbase.Awake()で親クラスのAwakeを呼んでやる必要がある。

public class SingletonTest : SingletonMonoBehaviour<SingletonTest>
{
void Awake()
{
base.Awake();
}
}
ちなみにオブジェクトを破棄したい場合は、OnDestroyでgameObjectを破棄するような設定にしておくと吉。