ゲームジャム中の会話にて
「では僕の方で、音の挙動を制御するサウンドマネージャー的なものを作っておくよ。」
「サウンドマネージャーですか。それなら後でもよくありませんか。」
「おや、どうしてだい?」
「だって、そういうゲーム中でずっと必要なものって、全シーンに置かなくちゃいけないじゃないですか。まだ全てのシーンが揃っていませんし後でもよくないですか。」
「いや、全てのシーンに置く必要はないかな。ゲーム開始時に生成し、DontDestroyOnLoad()
を使えば、
シーンが切り替わっても存在し続けるよ。」
「でもそうすると、必ずタイトルシーンから実行しなくちゃいけないじゃないですか。ゲームシーンのテストが面倒になります。」
ふむ、なるほど。
「ゲーム開始時に生成する」という点を、「ゲーム開始時のシーンに必要なものを配置しておく」と捉えられてしまったか。
そうじゃないんだ。
今回僕が使うのはこれなんだ。
RuntimeInitializeOnLoadMethod
。
個人制作時によく使うインチキ技さ。
※ RuntimeInitializeOnLoadMethod
は別にインチキではありません。
この記事にはUnity2017.3.1f1を使用しています。
ああ、こんなときあるよね
さてさて、音を管理するコンポーネントやシーンを管理するコンポーネントなど、ゲーム実行中は常に存在しておかねばならないものってあるよね。
最終的にゲームが完成したら、必ず最初に始まるシーンってものがあるわけだから、そこに常時存在する必要のあるコンポーネントを配置してDontDestroyOnLoad()
を使って、シーンが切り替わっても消えないようにしてあげればいいよね。
でもゲーム制作中はどうだろう。
制作中にテストする場合でも、わざわざ最初のシーンから実行しなくちゃいけないのは面倒くさいよね。
メニューのシーンを作成中は、そのメニューから実行してテストしたいだろうし、終盤のステージをテストにするために、ステージ1からテストしていかなくてはいけないのは、あまりに時間がかかりすぎる。
ということは、常に必要なコンポーネント各種は、全てのシーンに配置しなくちゃいけないのかな。
でもそれって結構面倒くさくない?
新しいシーンを作るときには、その必要なコンポーネントやGameObject
を配置しておかなくちゃいけないし、必要なものの数や種類に変更があったら、全てのシーンに対して修正してあげなくちゃいけない。
更に同じコンポーネントやGameObject
でもシーンを切り替え時には破棄されてしまうし、DontDestroyOnLoad()
を使う場合でも、処理を誤ってしまうと、同じものが複数存在する可能性も出て
きてしまう。
いろいろと面倒なことが多そうだ。
うん、そんなときにはRuntimeInitializeOnLoadMethod
お悩みのあなたにこれ、RuntimeInitializeOnLoadMethod
。
docs.unity3d.com
このアトリビュートは、あの聖書(テラシュールブログ先生)で記事になったこともある由緒正しきアトリビュートなんだ。
tsubakit1.hateblo.jp
このアトリビュートを指定した関数は、ゲームまたはエディタ実行時の最初のシーンを読み込む前or後に呼ばれるようになるんだ。
そのため、エディタから開始する際のシーンが、タイトルだろうが、リザルトであろうが、ステージ2であろうが、どのシーンだろうが開始時に共通の処理を呼ぶことが可能になるんだ。
シーンの読み込み前か後かの設定
RuntimeInitializeOnLoadMethod
にRuntimeInitializeLoadType
を設定することで、シーンの読み込み前か後を指定できるよ。
試してみよう。
RuntimeInitializer.cs
using UnityEngine; public class RuntimeInitializer { [RuntimeInitializeOnLoadMethod( RuntimeInitializeLoadType.BeforeSceneLoad )] private static void InitializeBeforeSceneLoad() { Debug.LogFormat( "RuntimeInitializer::InitializeBeforeSceneLoad() called." ); } [RuntimeInitializeOnLoadMethod( RuntimeInitializeLoadType.AfterSceneLoad )] private static void InitializeAfterSceneLoad() { Debug.LogFormat( "RuntimeInitializer::InitializeAfterSceneLoad() called." ); } } // class RuntimeInitializer
TestComponent.cs
using UnityEngine; public class TestComponent : MonoBehaviour { private void Awake() { Debug.Log( "TestComponent::Awake()" ); } private void Start() { Debug.Log( "TestComponent::Awake()" ); } } // class TestComponent
RuntimeInitializer::InitializeBeforeSceneLoad()
TestComponent::Awake()
RuntimeInitializer::InitializeAfterSceneLoad()
TestComponent::Start()
このとおり。
RuntimeInitializeLoadType.BeforeSceneLoad
ならシーンに配置してあるコンポーネントのAwake()
より前に呼ばれ、RuntimeInitializeLoadType.AfterSceneLoad
なら後に呼ばれる。
アトリビュートを付ける関数の条件
RuntimeInitializeOnLoadMethod
を指定する関数はstatic
な関数なら何でもいいんだ。
そのため、関数があるクラスがMonoBehaviour
を継承していようといなかろうと、static
であろうと関係ない。
関数がpublic
であろうがprivate
であろうが、それも関係ない。
static
な関数であればそれでいいんだ。
そのため、以下の書き方のどれでもOK。
TestComponent.cs
using UnityEngine; public class TestComponent : MonoBehaviour { // MonoBehaviourを継承していても呼ばれる [RuntimeInitializeOnLoadMethod( RuntimeInitializeLoadType.BeforeSceneLoad )] private static void AAA() { } } // class TestComponent public class TestClass { // 普通のクラスでも呼ばれる [RuntimeInitializeOnLoadMethod( RuntimeInitializeLoadType.BeforeSceneLoad )] private static void BBB() { } } // class TestClass public static class TestStaticClass { // staticクラスでも呼ばれる [RuntimeInitializeOnLoadMethod( RuntimeInitializeLoadType.BeforeSceneLoad )] private static void CCC() { } } // class TestStaticClass public struct TestStruct { // 構造体でも呼ばれる [RuntimeInitializeOnLoadMethod( RuntimeInitializeLoadType.BeforeSceneLoad )] private static void DDD() { } } // class TestStruct
さあ、使ってみよう
では実際にRuntimeInitializeOnLoadMethod
を使って、ゲーム中に常に必要なコンポーネントを作っていこう。
もしコンポーネントが、エディタ上で設定するようなパラメータが無いのなら、新しくGameObject
を生成して、コンポーネントをアタッチしてあげるだけでいいね。
RuntimeInitializer.cs
using UnityEngine; public class RuntimeInitializer { [RuntimeInitializeOnLoadMethod( RuntimeInitializeLoadType.BeforeSceneLoad )] private static void InitializeBeforeSceneLoad() { // ゲーム中に常に存在するオブジェクトを生成、およびシーンの変更時にも破棄されないようにする。 var manager = new GameObject( "Manager", typeof( TestManager ) ); GameObject.DontDestroyOnLoad( manager ); } } // class RuntimeInitializer
でもエディタ上で設定したパラメータを使うコンポーネントの場合はどうしようか。
設定したコンポーネントを持つGameObject
をResourcesフォルダにいれて、Resources.Load()
で読み込んであげよう。
これなら、設定したパラメータが生きるはずだ。
RuntimeInitializer.cs
using UnityEngine; public class RuntimeInitializer { [RuntimeInitializeOnLoadMethod( RuntimeInitializeLoadType.BeforeSceneLoad )] private static void InitializeBeforeSceneLoad() { // ゲーム中に常に存在するオブジェクトを読み込み、およびシーンの変更時にも破棄されないようにする。 var manager = GameObject.Instantiate( Resources.Load( "Manager" ) ); GameObject.DontDestroyOnLoad( manager ); } } // class RuntimeInitializer
じゃあ、こんなGameObject
がいっぱい増えた場合は、どうだろうか。
スクリプト上の処理をその分増やしてあげれば解決するけれど、ものが増えるたびにスクリプトを書き直さねばならないのはイマイチだ。
だがstatic
な関数である以上、専用のSerializeField
のプロパティも用意できそうにない。
……いや、必要なパラメータだけ別のオブジェクトとして持っておけばいいじゃないか。
ScriptableObject
としてね。
www.urablog.xyz
RuntimeInitializer.cs
using UnityEngine; public class RuntimeInitializer { [RuntimeInitializeOnLoadMethod( RuntimeInitializeLoadType.BeforeSceneLoad )] private static void InitializeBeforeSceneLoad() { // ScriptableObjectテーブルから情報を取得し設定する。 var table = Resources.Load( "InitializerTable" ) as InitializerTable; if( table != null ) { foreach( var objData in table.Objects ) { var obj = GameObject.Instantiate( objData.Object ); if( objData.DontDestroy ) { GameObject.DontDestroyOnLoad( obj ); } } } } } // class RuntimeInitializer
これならば、GameObject
の生成だけでなく、初期設定用のパラメータを追加してRuntimeInitializeOnLoadMethod
の関数内で処理することも可能になりそうだ。
でも、こんなことには気を付けて
いろいろと便利な使い方が考えられるRuntimeInitializeOnLoadMethod
ですが、危険性もあるから使用には注意が必要ですよー。
知らない人はビックリする
基本的にUnityでは、Hierarchy
上にあるコンポーネントが処理を行いますよね。
そのため、ランタイム時にGameObject
が作成される場合には、Hierarchy
上にあるコンポーネントが何かしら処理していると考えるのが一般的です。
だけど、RuntimeInitializeOnLoadMethod
が指定された関数は、今まで見てきた通り、Hierarchy
上に無かろうと動作することが出来ます。
RuntimeInitializeOnLoadMethod
の存在を知らない人にとって、この動作は驚くべき挙動になります。
作った覚えのないGameObject
が、突然現れる恐怖!
Hierarchy
上にあるコンポーネントを調べたところで、どこにもそんな処理は記述されていません。
「これUnityのバグなんじゃね」と思われること請け合いです。
特にRuntimeInitializeOnLoadMethod
を使ってバグを出した日には目も当てられない。
シーンが動かなくなったら、当然そのシーンに何か問題があると考えるはずです。
だけど、原因がRuntimeInitializeOnLoadMethod
で指定した関数内の処理だった場合には、シーン内のコンポーネントを探せどもバグが見つかることはありません。
チームで作業している際にこんなバグを生み出そうものなら、晒上げられること必至です。
「きっとこれ、Unityのバグだよ」とUnityのせいにして誤魔化すしかありません。
複数設定している場合の実行順は不定
複数の関数にRuntimeInitializeOnLoadMethod
が指定されている場合、呼び出される順番は不定です。
どれから呼ばれるかはわかりません。
同じくアトリビュートであるPostProcessBuildAttribute
などは、引数で優先順位を決めることが出来るんだけど、RuntimeInitializeOnLoadMethod
は出来ないみたいなのです。
もちろん関数単位で指定するため、MonoBehaviour
の実行順を指定するScript Execution Orderでも解決できそうにないですね。
便利ではあるのですが、クセがすごいRuntimeInitializeOnLoadMethod
。
個人制作やゲームジャムのような短期的なもの場合にはまだしも、チームで長期的なプロジェクにてRuntimeInitializeOnLoadMethod
を使う場合には、きちんとルールなどを決めてから使ったほうがいいでしょうねー。