徳島ゲーム開発ごっこ 技術ブログ

ゲームを作るために役に立ったり立たなかったりする技術を学んでいきます!

【Unity】ゲーム中に常時必要なGameObjectがどのシーンから始めても存在するようにしてみよう

 ゲームジャム中の会話にて

「では僕の方で、音の挙動を制御するサウンドマネージャー的なものを作っておくよ。」
「サウンドマネージャーですか。それなら後でもよくありませんか。」
「おや、どうしてだい?」
「だって、そういうゲーム中でずっと必要なものって、全シーンに置かなくちゃいけないじゃないですか。まだ全てのシーンが揃っていませんし後でもよくないですか。」
「いや、全てのシーンに置く必要はないかな。ゲーム開始時に生成し、DontDestroyOnLoad()を使えば、
シーンが切り替わっても存在し続けるよ。」
「でもそうすると、必ずタイトルシーンから実行しなくちゃいけないじゃないですか。ゲームシーンのテストが面倒になります。」

 ふむ、なるほど。
 「ゲーム開始時に生成する」という点を、「ゲーム開始時のシーンに必要なものを配置しておく」と捉えられてしまったか。
 そうじゃないんだ。
 今回僕が使うのはこれなんだ。
 RuntimeInitializeOnLoadMethod
 個人制作時によく使うインチキ技さ。

RuntimeInitializeOnLoadMethodは別にインチキではありません。


この記事にはUnity2017.3.1f1を使用しています。

ああ、こんなときあるよね

 さてさて、音を管理するコンポーネントやシーンを管理するコンポーネントなど、ゲーム実行中は常に存在しておかねばならないものってあるよね。
 最終的にゲームが完成したら、必ず最初に始まるシーンってものがあるわけだから、そこに常時存在する必要のあるコンポーネントを配置してDontDestroyOnLoad()を使って、シーンが切り替わっても消えないようにしてあげればいいよね。

f:id:urahimono:20180211164057p:plain

 でもゲーム制作中はどうだろう。
 制作中にテストする場合でも、わざわざ最初のシーンから実行しなくちゃいけないのは面倒くさいよね。
  メニューのシーンを作成中は、そのメニューから実行してテストしたいだろうし、終盤のステージをテストにするために、ステージ1からテストしていかなくてはいけないのは、あまりに時間がかかりすぎる。

f:id:urahimono:20180211164140p:plain

 ということは、常に必要なコンポーネント各種は、全てのシーンに配置しなくちゃいけないのかな。
 でもそれって結構面倒くさくない?
 新しいシーンを作るときには、その必要なコンポーネントやGameObjectを配置しておかなくちゃいけないし、必要なものの数や種類に変更があったら、全てのシーンに対して修正してあげなくちゃいけない。

f:id:urahimono:20180211164154p:plain
f:id:urahimono:20180211164205p:plain

 更に同じコンポーネントやGameObjectでもシーンを切り替え時には破棄されてしまうし、DontDestroyOnLoad()を使う場合でも、処理を誤ってしまうと、同じものが複数存在する可能性も出て
 きてしまう。

f:id:urahimono:20180211164214p:plain

 いろいろと面倒なことが多そうだ。

うん、そんなときにはRuntimeInitializeOnLoadMethod

 お悩みのあなたにこれ、RuntimeInitializeOnLoadMethod
docs.unity3d.com

 このアトリビュートは、あの聖書(テラシュールブログ先生)で記事になったこともある由緒正しきアトリビュートなんだ。
tsubakit1.hateblo.jp

 このアトリビュートを指定した関数は、ゲームまたはエディタ実行時の最初のシーンを読み込む前or後に呼ばれるようになるんだ。
 そのため、エディタから開始する際のシーンが、タイトルだろうが、リザルトであろうが、ステージ2であろうが、どのシーンだろうが開始時に共通の処理を呼ぶことが可能になるんだ。

シーンの読み込み前か後かの設定

 RuntimeInitializeOnLoadMethodRuntimeInitializeLoadTypeを設定することで、シーンの読み込み前か後を指定できるよ。
 試してみよう。

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

f:id:urahimono:20180211164231p:plain

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

f:id:urahimono:20180211164250p:plain

 これならば、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を使う場合には、きちんとルールなどを決めてから使ったほうがいいでしょうねー。