ゲーム化!tomo_manaのブログ

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

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

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

今回から、シーンによるプロジェクトの分割に挑戦します。

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


<目次>

前回までの流れ

マネージャーシーンの追加(#29)

第29回で、加算ロードを使った、マネージャーシーン(シーン同士のデータ受け渡し役)の追加に挑戦しました。
tomo-mana.hatenablog.com

テストモジュール、シーンの透過性アップ(#30)

第30回で、フィールドや戦闘シーンを、シーン単位でデバッグできるように、ドライバの役目を果たすモジュールを追加しました。これによってシーン間のインターフェースを明確にする必要が出て、透過性がアップします。
tomo-mana.hatenablog.com

シーン切り替えを仮想化(#31-1)

今回の記事に先立って、ドライバを意識しないでシーン切り替えができるように、仮想化の可能性について検討しました。また、仮想化がどれくらい複雑になりそうかを想定しました。
tomo-mana.hatenablog.com

今回やりたいこと

前回までの流れをまとめると、こんな感じです。

例えば、FIELD から BATTLE にシーンを切り替えるケースを考えます。
f:id:tomo_mana:20210412003834p:plain

通常は、SceneManager.LoadScene(BATTLE) のように指定し、FIELDシーン を破棄して BATTLEシーン に移ります(図の線)。もしくは、FIELD→MANAGER→BATTLEというように、すでに加算ロードされたシーン同士で、MANAGER を介してシーンを切り替えます(図の線)。

シーン切り替えの階層化

今回は、後者の加算ロードを使った切り替えを、もう少し階層的に組めるようにします。

以下の図で言うと、Map0BattleRegion0 に移るために、先ほどのSceneManager.LoadScene()のようにシーン切り替え(BattleRegion0) のような呼び方をすると(図の線)、実際は Map0→作業者A→FIELD→Manager→Battle→作業者B→BattleRegion0の順にアクティブなシーンを切り替えて(図の線)、最終的にMap0→BattleRegion0にシーンが切り替わるようにします。

f:id:tomo_mana:20210328014102p:plain
シーン切り替え(階層化)

このシーンの切り替えを仲介する機能を、サービス(SceneMessageService)と呼ぶことにします。


今回、目指すのはこれです。

f:id:tomo_mana:20210306001700p:plain
呼び出し階層

※図の矢印は、誰から誰をロードするかを示しています。ただし、マネージャーに繋がっているシーンは、マネージャーを呼び出すものとします。

階層の記述

先程のサービスには、誰を意識するか、誰を起動しなければならないか、を記述します。この時、意識するのは自分と直接繋がっている(階層になっている)サービスだけです。

先程の図では、
⚫︎マネージャーはフィールドとバトルドライバを意識します。また、マネージャーから起動した場合は全部のシーンを起動します。
⚫︎フィールドはマネージャーだけを意識します(他のシーンへの切り替えはマネージャーに任せる)。フィールドから起動した場合はマネージャーを起動します。
⚫︎バトルはバトルドライバだけを意識します(他のシーンへの切り替えはバトルドライバに任せる)。バトルから起動した場合はバトルドライバを起動します。
⚫︎バトルドライバは少し変則的です(ここが今回やりたかったところです)。バトルドライバは、マネージャーの代理なので、マネージャーがいるかどうかを意識しますが、マネージャーがいなかったら自分がマネージャーの代わりになります。バトルドライバから起動した場合はバトルを起動します。

上の例から、サービスは以下の2つを持てば良さそうです。
●親と子(階層構造)
●関係するシーンをロードするか(強制起動フラグ)

コード

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

// シーン登録
[System.Serializable]
public class SceneRegistration {
    // シーン名
    public string name;
    // ロードされていなければロードする(強制起動フラグ)
    public bool forceLoad;
}

public class SceneMessageService : MonoBehaviour
{
    [SerializeField]
    public SceneRegistration sceneParent;    // 親
    [SerializeField]
    public SceneRegistration[] sceneChildren;    // 子
    
    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 NeedToLoad(SceneRegistration r)
    {
        if( !string.IsNullOrEmpty( r.name ) ){
            if( !(r.name == SceneManager.GetSceneByName( r.name ).name) ){
                if( r.forceLoad ){
                    return true;
                }
            }
        }
        return false;
    }
}

コードの配置(Hierarchy、Inspector)

これを、各シーンに、以下のように配置します。
f:id:tomo_mana:20210412221124p:plain

f:id:tomo_mana:20210410231406p:plain
各シーンへの配置

各シーンでの設定を以下のようにします。

Field

f:id:tomo_mana:20210410230459p:plain

Manager

f:id:tomo_mana:20210410230535p:plain

Battle

f:id:tomo_mana:20210410230549p:plain

BattleDriver

f:id:tomo_mana:20210410230604p:plain

動作テスト

上記設定後、動作テストを行います。

フィールドからの起動は、マネージャー経由でバトルドライバ、バトルまで起動します。これは一般的な加算ロードでの起動方法と思います。
それに対して、バトルからの起動は、バトルドライバだけ起動します。これが今回のサービスでやりたかったことです。

フィールドからの起動
f:id:tomo_mana:20210410231013p:plain
マネージャーからの起動
f:id:tomo_mana:20210410231026p:plain
バトルからの起動
f:id:tomo_mana:20210410231039p:plain
バトルドライバからの起動
f:id:tomo_mana:20210410231051p:plain

つまづいた点

最終的に出来上がったコードはシンプルになりましたが、途中いろんな試行錯誤がありました。。(同じシーンが重複起動できてしまう、延々と起動してしまうなど)

今回のコードを作るために、加算ロードのいくつかの特徴について調べた内容は、以下にまとめました。
tomo-mana.hatenablog.com

次回

サービスへの参照を取得して、他のシーンをアクティブにできるようにします。