ゲーム化!tomo_manaのブログ

ゲーム化!tomo-manaのブログ

Unityでゲームを作る方法について紹介しています

Unity学習#33 シーンによるプロジェクトの分割#3 (Unity 2019.4.4f1)

今回は、シーンによるプロジェクトの分割の3回目で、第31回から作ってきたサービス(シーンの切り替えを仲介する機能)を使って、シーンを切り替えるメッセージを送ります。


「シーンによるプロジェクトの分割」と呼んでいるのは、ゲーム自体を複数のシーンに分けて作業性を高め、分割・結合を簡単にできるようにすることです。ゲーム全体を一つのマネージャー(ゲーム全体のデータを管理する構造体)に管理させるだけでなく、全てのシーンが揃っていなくても、最低限のシーンだけでテストができるようにします。

前回までの流れ

#31でシーン同士の関係を記述する「サービス」を定義し、#32でシーン同士が親子関係、つまりプロジェクト内の全シーンがツリー状に繋がるようにしました。

f:id:tomo_mana:20210328014102p:plain
シーンのツリー構造

ツリーにしたかったのは、以下の理由からでした。
(1)全てのシーンがマネージャーにアクセスできるようにする
(2)もしマネージャーが不在でも、担当者レベル、シーンレベルでテストをする時に、上位シーンがマネージャーの代理応答をできるようにする

f:id:tomo_mana:20210423074336p:plain
代理応答の例

(参考)
#31:シーン間の関わりを記述するコンポーネント「サービス」
tomo-mana.hatenablog.com

#32:サービスに親子関係を持たせる
tomo-mana.hatenablog.com

今回やりたいこと

今回は、このツリーのどこかにいるシーンに、切り替えメッセージを送る処理を実装します。
この時、シーンは切り替え先のシーンだけを意識し、実際はマネージャーを経由して目的のシーンに切り替えます。

シーンの切り替えを仲介する機能(サービス)は、親子関係を持っています。サービスを持つシーン同士は、以下のように一人の親と複数の子を持ちます。ちょうどGameObjectとTransformの関係に似ています。

f:id:tomo_mana:20210415220526p:plain
サービス同士の親子関係

各サービスが自分が受信したいメッセージを定義します。このツリー状のプロジェクトでは、メッセージはプロジェクト全体で一意(ユニーク)である必要があります。メッセージが一意であることで、送り先を指定しなくても送り先を特定することができます。詳しくは以下にまとめてあります。
tomo-mana.hatenablog.com


サービスはメッセージをブロードキャストで飛ばします。サービスはメッセージを受信できるサービスを前方一致で見つけ、そのサービスをアクティブにして、メッセージの伝達を終わります。

f:id:tomo_mana:20210423075505p:plain
ブロードキャスト

この手続きは、以下の4つのステップに分かれます。
(1) サービスが作られたか(存在するか)
(2) 親(マネージャー)の探索
(3) 親(マネージャー)から子にメッセージを届ける
(4) シーン間メッセージ

サービスが作られたか(存在するか)

 前回は、シーンがロードされたかどうかを、rootObjectが取得できるかで判断しました。サービスを取得する時はこの方が都合が良かったのですが、途中でシーンをアンロードしなければ、サービスがいるものとしてアクセスできる方が楽です。
 そのため、前回追加したinitializedフラグを、boolから状態変数(列挙体)に変えます。初期化されたか、だけでなく、初期化の結果作ったか(Exist)、作らなかったか(Not Exist)。この判断のため、初期化処理を少し変更します。
 

親(マネージャー)の探索

 次に、親を探索します。サービスの親は一人しかいないので、親が居れば自分はマネージャーでなく、居なければ自分がマネージャーです。

親(マネージャー)から子にメッセージを届ける

 親(マネージャー)から子にメッセージを送るには、木構造再帰的に探索します。
 親から子に届けるメッセージは、システム内で一意になるようにするので、今回は前方一致にします。(見つけたら終わり。後方一致は最後まで検索する)
 ※前回の図に番号を振った木構造の図

 ここまでの機能で、システムで最初にアクティブにするシーンを決定できます。システムで親(=マネージャー)になったシーンが、システム内で最初のシーンになりたい人(FirstScene)をアクティブにします。最初のシーンになりたい人の受信メッセージ一覧に"FirstScene"を追加します。なお、この実装が、(3)の単体テストも兼ねます。

シーン間メッセージ

 親の探索と、親から子どもにメッセージを送る機能を実現したら、シーン間メッセージはこの組み合わせで実現できます。
 今回は、(4)のテストも兼ねて、マネージャーまたはフィールドから起動して、フィールドでエンカウントしたらバトルにメッセージを送ってみます。

例えば、フィールドシーンのエンカウント処理(第28回)に以下を追加します。

// シーンは、ゲームオブジェクトのどの階層からでも以下の1文で取得できます。
Scene scene = gameObject.scene;
// サービスの取得用に、以下のインターフェースを用意します。
SceneMessageService service = SceneMessageService.GetService( scene );
// サービス間メッセージ(メッセージは文字列で指定)
if( service != null ){
    SceneMessageService.SendMessage( service, "Encount" );
}

上の4点の修正をまとめると、以下になります。

コード

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

// (1) 存在するか?
public enum RegistState {
    NOT_INITIALIZED,
    EXIST,
    NOT_EXIST,
};

// シーン登録
[System.Serializable]
public class SceneRegistration {
    
    // シーン名
    public string name;
    
    // ロードされていなければロードする
    public bool forceLoad;
    
    // シーン(保持)
    [System.NonSerialized]
    public Scene scene;
    
    // サービス(保持)
    [System.NonSerialized]
    public SceneMessageService service;
    
    // 初期化完了
    public RegistState initialized = RegistState.NOT_INITIALIZED;
    
    public bool Initialized()
    {
        if( initialized != RegistState.NOT_INITIALIZED ){
            return true;
        }
        return false;
    }
    
    public bool Exist()
    {
        if( initialized == RegistState.EXIST ){
            return true;
        }
        return false;
    }
}

// 受信可能なメッセージ一覧
[System.Serializable]
public class SceneMessage {
    public string message;
}

public class SceneMessageService : MonoBehaviour
{
    [SerializeField]
    public SceneRegistration sceneParent;
    [SerializeField]
    public SceneRegistration[] sceneChildren;
    
    public enum ServiceState {
        NOT_INITIALIZED,
        SCENE_INITIALIZED,
        MANAGER_FOUND,
        FIRST_SCENE_SELECTED,
    };
    ServiceState initialized = ServiceState.NOT_INITIALIZED;
    
    [SerializeField]
    public SceneMessage[] messages;
    
    public bool isParent = false;
    
    void Start()
    {
        // 強制起動
        if( NeedToLoad(sceneParent) ){
            SceneManager.LoadSceneAsync( sceneParent.name, LoadSceneMode.Additive );
        }
        foreach( SceneRegistration sceneChild in sceneChildren ){
            if( NeedToLoad(sceneChild) ){
                SceneManager.LoadSceneAsync( sceneChild.name, LoadSceneMode.Additive );
            }
        }
    }
    
    bool InitializeService()
    {
        bool r = true;
        if( !sceneParent.Initialized() ){
            r = false;
            GetService( sceneParent );
        }
        foreach( SceneRegistration sceneChild in sceneChildren ){
            if( !sceneChild.Initialized() ){
                r = false;
                GetService( sceneChild );
            }
        }
        return r;
    }
    
    void Update()
    {
        switch( this.initialized ){
        case ServiceState.NOT_INITIALIZED:
            if( InitializeService() ){
                ServiceLog();
                this.initialized = ServiceState.SCENE_INITIALIZED;
            }
            break;
        case ServiceState.SCENE_INITIALIZED:
            if( FindParent() ){
                this.initialized = ServiceState.MANAGER_FOUND;
            }
            break;
        case ServiceState.MANAGER_FOUND:
            if( isParent ){
                if( !FindChild(this, "FirstScene") ){
                    SceneManager.SetActiveScene(this.gameObject.scene);
                }
            }
            this.initialized = ServiceState.FIRST_SCENE_SELECTED;
            break;
        default:
            break;
        }
    }
    
    bool NeedToLoad(SceneRegistration r)
    {
        if( !string.IsNullOrEmpty( r.name ) ){
            if( !(r.name == SceneManager.GetSceneByName( r.name ).name) ){
                if( r.forceLoad ){
                    return true;
                }
            }
        }
        return false;
    }
    
    List<GameObject> rootObjects = new List<GameObject>();
    
    void GetService(SceneRegistration r)
    {
        if( !string.IsNullOrEmpty( r.name ) ){
            r.scene = SceneManager.GetSceneByName( r.name );
            if( r.scene.name == r.name ){
                r.scene.GetRootGameObjects( rootObjects );
                if( rootObjects != null ){
                    foreach( GameObject obj in rootObjects ){
                        SceneMessageService[] s = obj.GetComponents<SceneMessageService>();
                        if( s.Length > 0 ){
                            r.service = s[0];
                            r.initialized = RegistState.EXIST;
                            break;
                        }
                    }
                }
            } else {
                if(!r.forceLoad){
                    r.initialized = RegistState.NOT_EXIST;
                }
            }
        } else {
            r.initialized = RegistState.NOT_EXIST;
        }
    }
    
    void ServiceLog()
    {
        string[] s = new string[3]{
            "--", "--", "--"
        };
        if( Loaded(sceneParent) ){
            s[0] = sceneParent.service.gameObject.scene.name;
        }
        for(int i = 0; i < sceneChildren.Length; i++){
            if( Loaded(sceneChildren[i]) ){
                s[i+1] = sceneChildren[i].service.gameObject.scene.name;
            }
        }
        Debug.Log($"me:{gameObject.scene.name}, p:{s[0]}, c0:{s[1]}, c1:{s[2]}");
    }
    
    bool Loaded(SceneRegistration r)
    {
        if( !string.IsNullOrEmpty( r.name ) ){
            if( (r.name == SceneManager.GetSceneByName( r.name ).name) ){
                return true;
            }
        }
        return false;
    }
    // (2) 親の選出
    bool FindParent()
    {
        bool find = false;
        
        SceneMessageService sv = this;
        
        while( find == false ){
            if( sv.sceneParent.Initialized() ){
                if( sv.sceneParent.Exist() ){
                    sv = sv.sceneParent.service;
                } else {
                    sv.isParent = true;
                    find = true;
                }
            } else {
                // また今度
                break;
            }
        }
        return find;
    }
    // (3) 子にメッセージを送る
    static bool FindChild(SceneMessageService sv, string message)
    {
        if( sv != null ){
            if( Dispatch(sv, message) ){
                return true;
            } else {
                foreach( SceneRegistration child in sv.sceneChildren ){
                    if( FindChild( child.service, message ) ){
                        return true;
                    }
                }
            }
        }
        return false;
    }
    static bool Dispatch(SceneMessageService sv, string message)
    {
        if( sv == null ){
            return false;
        }
        if( sv.messages == null ){
            return false;
        }
        foreach( SceneMessage myMes in sv.messages ){
            if( message == myMes.message ){
                SceneManager.SetActiveScene( sv.gameObject.scene );
                return true;
            }
        }
        return false;
    }
    // (4) シーン間メッセージ
    public static SceneMessageService GetService( Scene scene )
    {
        List<GameObject> rootObjects = new List<GameObject>();
        
        scene.GetRootGameObjects( rootObjects );
        foreach( GameObject obj in rootObjects ){
            SceneMessageService[] s = obj.GetComponents<SceneMessageService>();
            if( s.Length > 0 ){
                return s[0];
            }
        }
        return null;
    }
    public static bool SendMessage( SceneMessageService sv, string message )
    {
        if( sv = FindParent( sv ) ){
            if( FindChild( sv, message ) ){
                return true;
            }
        }
        return false;
    }
    static SceneMessageService FindParent( SceneMessageService sv )
    {
        if( sv != null ){
            if( sv.isParent || !sv.sceneParent.Exist() ){
                return sv;
            } else {
                return FindParent(sv.sceneParent.service);
            }
        }
        return null;
    }    
}

動作テスト

(1) 初期化が完了するとEXIST、NOT EXISTになっているのが確認できます。
f:id:tomo_mana:20210420000359p:plain
f:id:tomo_mana:20210420000422p:plain

(2) 親(マネージャー)になったシーンにはisParentがチェックされている事が分かります(システム内で一つのシーンだけ)。
f:id:tomo_mana:20210420000540p:plain

(3) FirstSceneメッセージを受け取ることにしたシーンが最初にアクティブになります。(A)
いなければ、親が最初にアクティブになります(B)

f:id:tomo_mana:20210420001220p:plain
FirstScene指定 - message欄
f:id:tomo_mana:20210420001242p:plain
FirstScene指定あり(FieldScene)
f:id:tomo_mana:20210420001307p:plain
FirstScene指定なし

(4) フィールドからバトルに切り替わります。

次回

メッセージ送信処理はここまでにして、次回から戦闘画面の設計に入ります。