マルチシーンロードの活用法

はじめに

副団体長、コーディング課のkeyです!

今回はUnityでマルチシーンロードを使ってみようという会です。今まで単一のシーンでゲームを作ってきた方がほとんどだと思いますが、マルチシーンロードを身に着けるとUnityっぽさが消せるのと同時に出来ることの幅がかなり広がるのでぜひ最後まで読んでみてください!

対象者

  • Unityっぽさをなくしたい人
  • 複数シーンに同じものを配置する必要が出てきた人

マルチシーンロードとは?

 ほとんどの人がマルチシーンロードを使ったことがない、もしくは存在も知らないという人もいるかと思います。普段皆さんが何も意識せずに使っているものは単一シーンロードという手法です。
要は再生中に1つのシーンしか読み込まないということです。マルチシーンロードは再生中に2つ、3つと複数のシーンを同時に読み込むことを言います。

実際のUnityの画面を見るとこのような違いがあります。

単一シーンロードの画面
マルチシーンロードの画面

左側の単一シーンロードは皆さん見慣れていますよね。右側のマルチシーンロードの場合はこのようにゲーム再生中に複数シーンが同時に動いている状態を示します。

ではこれを使ってどういうことが出来るのかを見ていきましょう!

マルチシーンロードが出来ると何がうれしいの?

 前述の説明でマルチシーンロードがどういうものなのかのイメージはつかめたと思います。じゃあこれを使うことで何が出来るようになるの?っていう話ですよね。

結論を先に述べると全てのシーンで使うような汎用機能を別シーンに切り分けることが出来るチーム開発でコンフリクト(衝突)を発生させずに開発出来るというのが最大のメリットです。

 例を挙げましょう。どのシーンでも使う機能と言えばSE, BGM, セーブ機能, 画面遷移アニメーションなどですかね。
これらをシングルトンで実装したとしましょう。
この場合、どのシーンにも一度は配置する必要が出てきたり、もしくはそのシングルトンを自動生成するプログラムをアタッチする必要が出てきます。少ないシーン数であれば問題ありませんが、シーン数が増えるたびに再配置するのはちょっと面倒ですよね。

 こういう場合にマルチシーンロードが有用です。

どのシーンでも使用するスクリプトたちを1つのシーン(これ以降ManagerSceneと呼びます)にまとめて、どのシーンから開始したとしても必ずManagerSceneを読み込むようにします。そうすることで、全シーンに配置せずと同様の効果を得ることが出来ます。

 チーム開発でコンフリクトを発生させずに開発出来るというのはどういうことでしょうか?

これは複数人で同時に同じシーンを編集すると起こります。これが非常に厄介です。UnityのシーンファイルはYAML形式で管理されており、形式的ではあるものの、コンフリクト解決がそこそこ大変です。中身を少し見せるとこんな感じです。

--- !u!1 &330585543
GameObject:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  serializedVersion: 6
  m_Component:
  - component: {fileID: 330585546}
  - component: {fileID: 330585545}
  - component: {fileID: 330585544}
  - component: {fileID: 330585547}
  m_Layer: 0
  m_Name: Main Camera
  m_TagString: MainCamera
  m_Icon: {fileID: 0}
  m_NavMeshLayer: 0
  m_StaticEditorFlags: 0
  m_IsActive: 1

読めないことはないと思いますが、これのコンフリクト解決はやりたくないですね…
そこで担当者ごとにシーンを分割します。分け方は様々ですが、ここではUI, Sound, Playerなどとしておきます。UI開発者はUIシーンを、Sound開発者はSoundシーンを…などと分け、あとはメインとなるシーンからそれらを読み込むだけです。これで効率的に開発が出来ますね。

 どうですか? 便利そうだなぁって思ってもらえましたか?

ではいよいよ使い方に移りましょう!

基本的な使い方

 マルチシーンロードの実現方法だけの話だと単にいつも使っているシーンロード関数 SceneManager.LoadScene のAdditiveモードを使うだけです。

using UnityEngine;
using UnityEngine.SceneManagement;

public class AdditiveSceneLoadSample : MonoBehaviour
{
    void Start()
    {
        // "ManagerScene"というシーンを今現在開いているシーンに重ねて読み込む
        SceneManager.LoadSceneAsync("ManagerScene", LoadSceneMode.Additive);
    }
}

はい、以上。

…さすがに終わりません。
これだと結局また全シーンにこのプログラムをアタッチする必要があるので本末転倒です。また、面倒なことにUnityにはアクティブシーンという概念があります。アクティブシーンはゲームのメインシーンであり必ず必要です。Instantiateやnew GameObjectで生成されるオブジェクトが生成されるシーンでもあります。これはAdditiveシーンでないものが自動的に割り当てられます。つまりはゲーム再生時のシーンですね。
これが問題です。
もしここで別のシーンに切り替えるために今のアクティブシーンをアンロードしたらどうなるでしょうか?

アクティブシーンがなくなりますよね。なので、別のシーンをアクティブシーンにする必要があります。これは手動で行う必要があります

これじゃあやってられません。

より実用的な使い方

 ということでその問題を解決できるより実用的?な手法をお教えします。
前述の問題はアクティブシーンが一時的にでもなくなってしまうのが要因です。そこでManagerSceneを永続的にアクティブシーンにしようということをします。こうすることでこの問題は解決します。

やり方ですが、アクティブシーンに自動で設定するための一番簡単な手法は一番初めにManagerSceneを開くことです。でもそれだとゲーム開始時点で毎回ManagerSceneから再生する必要が出てきます。なので、これをどのシーンから始まっても一度そのシーンをアンロードし、ManagerSceneをアクティブシーンとして差し込むという力技で解決します。

using UnityEngine;
using UnityEngine.SceneManagement;

public class ManagerSceneLoader : MonoBehaviour
{
    private static string _mainSceneName = "";
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    static void Initialize()
    {
        _mainSceneName = "";
        _mainSceneName = SceneManager.GetActiveScene().name; // 現在のアクティブシーンのシーン名を取得
        SceneManager.LoadScene("ManagerScene"); // ManagerSceneを単一読み込み
        SceneManager.LoadScene(_mainSceneName, LoadSceneMode.Additive); // 初期で開いていたシーンをAdditiveモードで上から被せる
    }
}

これを書くだけで動きます。シーンに配置する必要はありません
RuntimeInitializeOnLoadMethod を使うことでシーンに配置することなく、ゲーム再生と同時に特定の関数を呼び出すことが出来ます。また、引数としてRuntimeInitializeLoadType.BeforeSceneLoad を使用することでAwake関数の実行よりも前にこの処理を行うことが出来ます。今回は誤作動を防ぐためにこの引数を使用しています。

実際にこれは制作中の人間カーリングのシーン遷移でも使っています。

シーン遷移の違和感が全くありませんよね。これはシーン遷移に関するオブジェクトをManangerSceneに配置することで実現されています。マルチシーンロードを使うとこんなことが出来るよーの一例だと思ってもらえれば大丈夫です。

デメリット(注意しないといけない点)

 マルチシーンロードで気を付けないといけないことが1つだけあります。それはInstantiateやnew GameObjectをしたときにオブジェクトが生成されるシーンがアクティブシーンであるということです。前述の運用方法の場合、アクティブシーンはManagerSceneになります。そのため、ManagerScene以外のシーンでオブジェクトを生成するプログラムを呼び出した場合、そのプログラムがあるシーンではなくManagerSceneに生成されます。これによって、シーンをアンロードしたときに生成したオブジェクトが残り続けるということが起こります。

例えばステージセレクトの画面でInstantiateを呼び出したとしましょう。
そこからステージセレクトシーンをアンロードして選択されたステージのシーンをロードします。
すると、ステージセレクトシーンで生成したオブジェクトがManagerSceneに生成されているため、ステージセレクトシーンを消したとしてもオブジェクトがそのシーンに紐づいていないため残り続けます。
これによって、ステージシーンの方で消せなかったオブジェクトが見えてしまいます。
 これの解決方法はInstantiateで生成するタイミングで引数として生成したいシーンに存在する親オブジェクトを指定することです。これによって、任意のシーンのもとにオブジェクトを生成することが出来ます。

using UnityEngine;

public class Sample : MonoBehaviour
{
    [SerializeField] private GameObject _prefab;
    [SerializeField] private GameObject _parent;
    
    void Start()
    {
        // _parentを親に設定して_prefabを生成する
        Instantiate(_prefab, _parent);
    }
}

マルチシーンロードで気を付けないといけないことはこれぐらいだと思います。

終わりに

今回はマルチシーンロードの使い方について紹介しました。マルチシーンロードを導入することでかなり高いレベルでの開発が出来ると思っています。その分、色々考えないといけないことが増えるのでチームレベルや個人の技量に応じて使うかどうかは決めましょう。今回紹介した使い方はあくまで一例であり、私が最近使い出した発展途上のものではあるので、色々なプロジェクトで使ってみて自分なりの正解を見つけてみてください! もしいい使い方があればぜひ教えてください!w

それではまた!

コメントを残す

CAPTCHA