はじめに

Unity 5.3 から追加されたマルチシーン編集をつかうことで、ゲームオブジェクトをシーン単位で分割・管理することができます。
分割できるようになったことで、”膨大な数のゲームオブジェクトが配置された広大なステージを分割して必要なシーンだけロードする”ことが容易になりました。
そこで、今回は実際にゲームを作成しながらマルチシーン編集を使った実装方法をまとめてみました。

作成したゲームの内容

ゲームの概要

ステージ全体の形状は以下の写真の通りです。
「RunStageA」の青丸のスタートから「RunStageE」の青枠のゴールまで続く一本道を障害物を避けながら進むゲームです。
海に落ちるとゲームオーバーになります。

AllStage

作成したゲームを WebGL に書き出しました


百聞は一見。まずは作ったゲームをプレイしてみましょう。
操作は矢印キーで前後左右の動き、スペースキーでジャンプです。
一番注目して欲しいところは、画面の右上に現在のステージ全体の状態を表示している部分です。
自分がいるステージ( = 子シーン)が、RunStageA から順に変わって行くときの瞬間に注目してください。
また、今回のゲームでは先日の Unite 2016 で紹介されていた Cinematic Image Effect を組み込み、スタート画面で選べるようにしました。
もしよければ、こちらに関しても是非試してみてください。
では、どうぞ!
ゲームの WebGL へ

実装

マルチシーン編集に関連して実際にやったことは、以下の通りです。

※便宜上、Hierarchy , Sceneウィンドウ上に展開されている、複数のシーンたちを「子シーン」と命名します。ちなみに、「親シーン」に相当するものはありません。これについては後述

  • 1つのステージを複数の子シーンに分割
  • ステージ全体で使われるオブジェクト(プレイヤーやゲームのシステム部分)を1つの子シーンにまとめて、常に生きている状態にする
  • 自分(キャラクタ)のいるステージの判定して、必要な子シーンだけをロードする
  • ゲームオーバーもしくは ゴール時に、子シーン群を初期状態に戻す
それぞれ、詳しく説明したいと思います。

1つのステージを複数の子シーンに分割

今回一つのステージは、Unity Editor 上でのスケールが x:20, z:20 のかなり広大な面積です。
ゆえに、RunStage A ~ E の 5 つの子シーンに分割しました。

※ちなみに下のスクリーンショットのように子シーンを Hierarchy 上に配置した編集状態を当初は保存できるものだとおもっていましたが、どうやら出来ないようです。(いわゆる、親シーンの保存のようなことが出来ない)

同じことを考えている方がいるようで、こういった便利スクリプトを作っている方がいました。
[[Unity][マルチシーン]シーン構成保存・復元エディタスクリプト]

MSEStages

ゲームのシステム部分やプレイヤーなどは、一つのシーンにまとめる

RunStageA にキャラクタなど他のシーンでも使うゲームオブジェクトを置いてしまうと、 RunStageA 以外の子シーンをアンロードした際にどうなるのでしょうか?
当然ですが、RunStageA 配下なのでキャラクタなどは破棄されてしまいます。
そこで、子シーンの生き死にに影響を受けたくないオブジェクトは、1 つの子シーンにまとめてしまいます。
今回は、Character シーンにまとめました。
この Character シーンを常に生きた状態にすることでステージ全体で使われるオブジェクトを守ることができます。

自分(キャラクタ)のいる子シーンの判定して、必要な子シーンだけをロードする

今回、RunStageA~E は表示される順番がきまっているため、子シーンの名前を配列としてもたせています。
自分がどの子シーンにいるのかの状態は、プレイヤーが接地している地面とのあたり判定の引数から子シーンを特定し、シーン名の配列の index をグローバル変数に入れて保存しています。
また、前にいた子シーンの index もグローバル変数としてもたせています。
今いる子シーンと前にいた子シーンを比較することで、プレイヤーがゴールに進んでいるのか、スタートの方に戻っているのかを判定して、前に行っても後ろに行っても常に自分のいる子シーンとその前後だけを表示させるようにしています。
ゲームオブジェクトがどの子シーンに含まれているのかを知ることは簡単で、ゲームオブジェクトの scene プロパティーで取得できます。
以下は、ステージの状態を管理するためのコードになります。
cs:StageManager.cs

public class StageManager : MonoBehavior{

    string[] SceneNames = new string[] {
        "RunStageA",
        "RunStageB",
        "RunStageC",
        "RunStageD",
        "RunStageE"
    };

    int NowSceneIndex = 0;
    int LastSceneIndex = 0;

    bool IsChanged = false;

    void Update() {
        if(IsChanged){
            ControllStageEnvironment ();
             IsChanged = false;
        }
    }

   void OnCollisionEnter(Collision other) {
        // 今いるシーンの名前から配列内の index を取得する
        int SceneIndex = Array.IndexOf(SceneNames, other.gameObject.scene.name);

        // 今いるシーンを常にアクティブ状態にする
        SceneManager.SetActiveScene (other.gameObject.scene);

        // Scene が切り替わった
        if (NowSceneIndex != SceneIndex) {
            LastSceneIndex = NowSceneIndex;
            NowSceneIndex = SceneIndex;

            IsChanged = true;
        }
    }

   private void ControllStageEnvironment(){
        if (NowSceneIndex > LastSceneIndex) {
            //  今いるインデックスの方が大きければゴールに向かっている
            if (NowSceneIndex > 1) {
                SceneManager.UnloadScene (SceneNames[LastSceneIndex - 1]);
            }

            if (SceneNames.Length > NowSceneIndex + 1) {
                SceneManager.LoadSceneAsync (SceneNames [NowSceneIndex + 1], LoadSceneMode.Additive);
            }
        } else {
            //  今いるインデックスの方が小さければスタートに戻っている
            if (SceneNames.Length > LastSceneIndex + 1) {
                SceneManager.UnloadScene (SceneNames[LastSceneIndex + 1]);
            }

            if (NowSceneIndex > 0) {
                SceneManager.LoadSceneAsync (SceneNames [NowSceneIndex - 1], LoadSceneMode.Additive);
            }
        }

    }

}
※ SceneManager.UnloadScene()を OnTriggerEnter() や、OnCollisionEnter()で呼ぶと Unity Editor がフリーズします。
SceneManager.UnloadScene() を今回は Update()で呼んで応急処置しています。 2016/4/18 現在

ゲームオーバーもしくはゴール時に、子シーン群を初期状態に戻す

繰り返しプレイするために、ゲームを初期状態に戻す必要があります。
今ロードされている子シーンの破棄とスタート地点付近の RunStageA と RunStageB の読み込みです。
また、Unity 5.4 から追加される SceneManager.sceneLoaded イベントで子シーンの初期化が出来てから、ゲームをスタートさせています。
以下は、ゲームが初期状態になったことを判定するコードです。
cs  
    void OnStart() {
        SceneManager.sceneLoaded += OnSceneLoaded;
    }

    void OnSceneLoaded(Scene scene, LoadSceneMode loadSceneMode) {
        if(NowState == State.NOT_START | NowState == State.END){
            if(!IsStageALoaded){
                // RunStageAが読み込まれているか
                IsStageALoaded = (scene.name == "RunStageA");
            }
            if(!IsStageBLoaded){
                // RunStageBが読み込まれているか
                IsStageBLoaded = (scene.name == "RunStageB");
            }
            if(IsStageALoaded && IsStageBLoaded){

                // ゲームスタート
                ChangeState (State.MENU);
                // キャラクタの初期化
                this.transform.rotation = Quaternion.Euler((new Vector3(0, 0, 0)));
                this.transform.position = new Vector3(0, 1.018f, 0);
                deadCollision.transform.localPosition = new Vector3(0, -0.8f, 0);
            }
        }
    }

シーンの設定

子シーンを加算的に読み込んだ場合 Lighting などのシーン毎の設定は、アクティブ状態にあるシーンの設定が適用されます。
この設定は読み込んでいる全ての子シーンに対して、適用されます。
どの子シーンをアクティブにするかの切り替えは、SceneManager.SetActiveScene(Scene scene) を使います。

最後に

今回は、1つのシーンをマルチシーン編集を使って分割して、キャラクタの周りの子シーンだけをロードするようにしました。
これを応用すれば、広大なステージを持つゲームを比較的容易に開発することも可能でしょう。
是非皆さんも、チャレンジしてみてください!