Unity UIでページやダイアログごとにシーンを分離する実装パターン

こちらは[Unity Advent Calendar](http://qiita.com/advent-calendar/2015/unity)11日目の記事となります。
前日は[toru_inoue](http://qiita.com/toru_inoue)さんによる[タイムラインGUI TimeFlowShiki をオープンソースにしてみた](http://sassembla.github.io/Public/2015:12:10%2000-00-00/2015:12:10%2000-00-00.html)でした。


# 今回のお題
本来はSOOMLA LevelUpのAssetに関する記事を書こうと思っていたのですが、火曜日にAsset Storeを見たらSOOMLA LevelUpが消えているという衝撃的な事実が発覚したため、急遽内容を変更させていただきましたorz

最近Unity UIでメニュー画面を作る際、ページやダイアログなど、要素ごとにシーンを分離してシームレスにロードするパターンがお気に入りです。
Unity5.3から追加されたマルチシーンエディット機能の恩恵も受けられますので、その点でもオススメ出来るかもしれません。

# メリット
各シーンにページを1つだけ配置するので、遷移が多くなりがちなメニュー画面も細分化して管理しやすくなります。
また、分業しやすくなりますので、複数人数での開発も向いてるかもです。

# デメリット
シーンを読み込むため、PrefabをInstantiateするのに比べるとオーバーヘッドが多くなります。
ただ、古めのAndroid端末でもほとんど違和感は感じませんので、大量にコールしない限りは問題ありません。

あとはBuild Settingで大量のシーンを登録する必要が出てきますが、これはご愛嬌ということで。

# 実装してみよう
では、実装してみましょう。

## iTweenをインポート
ページ遷移のアニメーションに使いますので、Asset StoreからiTweenをインポートします。
iTween大好き。イェー。

## スクリプトを準備
下記3つのスクリプトを準備します。

### ページ管理スクリプト
ページを管理するスクリプトで、メニューの基幹シーンに配置します。
とりあえずデモということで、ボタン制御処理も含めています。

```csharp:Pager.cs
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

public class Pager : MonoBehaviour
{
    // インスペクターでGoToHogePageButtonを紐付ける
    public Button goToHogePageButton;

    // インスペクターでCloseButtonを紐付ける
    public Button closeButton;

    // ページのスタック
    private List<PageAbstract> pages = new List<PageAbstract>();

    private void Start()
    {
        // 各ボタンにリスナーを追加
        goToHogePageButton.onClick.AddListener(OnGoToHogePageButtonClick);
        closeButton.onClick.AddListener(OnCloseButtonClick);

        // Closeボタンは隠しておく
        closeButton.gameObject.SetActive(false);
    }

    // ページをスタックします
    public void AddSubMenu(PageAbstract page)
    {
        if (pages.Count == 0)
        {
            // スタックが0→1になる時にcloseボタン表示
            closeButton.gameObject.SetActive(true);
        }
        pages.Add(page);
    }

    // ページを閉じます
    public void RemoveSubMenu(PageAbstract page)
    {
        if (pages.Contains(page))
        {
            pages.Remove(page);
        }
        Destroy(page.gameObject);
        if (pages.Count == 0)
        {
            // スタックが0になる時にcloseボタン隠す
            closeButton.gameObject.SetActive(false);
        }
    }

    // GoToHogePageボタンを押したときの処理です
    private void OnGoToHogePageButtonClick()
    {
        // HogePageを開く
        Application.LoadLevelAdditiveAsync("HogePage");
    }

    // Closeボタンを押したときの処理です
    private void OnCloseButtonClick()
    {
        // 一番上に積まれているページを閉じる
        var surfaceSubMenu = pages.LastOrDefault();
        if (null != surfaceSubMenu)
        {
            surfaceSubMenu.Hide();
        }
    }
}
```

### ページ基幹クラス
ページの量産を楽にするため、ページ基幹クラスを準備します。
ページ遷移アニメーションなどの共通処理はここに書いておくイメージです。

```csharp:PageAbstract.cs
using UnityEngine;
using UnityEngine.UI;
using System.Collections;

abstract public class PageAbstract : MonoBehaviour
{
    // フェードイン/アウトアニメーションの秒数
    private const float FADE_TIME = 0.3f;

    // アニメーション表示させたいコンテンツ。Canvas直下のPanelでOK
    public GameObject contents;
    // ページのマネージャー
    protected Pager pager;
    // フェードアニメーション時のAlpha値調整のために、CanvasにCanvasGroupコンポーネントをアタッチしてここにセットしておく
    protected CanvasGroup canvasGroup;

    protected virtual void Start()
    {
        canvasGroup = gameObject.AddComponent<CanvasGroup>();

        // Pagerを探す
        var pagerGameObject = GameObject.Find("Pager");
        if (null != pagerGameObject)
        {
            pager = pagerGameObject.GetComponent<Pager>();
            pager.AddSubMenu(this);
        }
        else
        {
            Debug.Log("Pager not found.");
        }
        Show();
    }

    // ページの表示アニメーション
    protected virtual void Show()
    {
        iTween.ValueTo(gameObject, iTween.Hash(
                "from", 0f,
                "to", 1f,
                "time", FADE_TIME,
                "onupdate", "OnValueUpdate"
            ));

        iTween.MoveFrom(contents, iTween.Hash(
                "y", -Screen.height, 
                "time", FADE_TIME, 
                "easetype", iTween.EaseType.linear));
    }

    // ページの非表示アニメーション
    public virtual void Hide()
    {
        iTween.ValueTo(gameObject, iTween.Hash(
                "from", 1f,
                "to", 0f,
                "time", FADE_TIME,
                "onupdate", "OnValueUpdate"
            ));

        iTween.MoveTo(contents, iTween.Hash(
                "y", -Screen.height, 
                "time", FADE_TIME, 
                "easetype", iTween.EaseType.linear,
                "oncomplete", "OnHideComplete",
                "oncompletetarget", gameObject));
    }

    private void OnValueUpdate(float value)
    {
        canvasGroup.alpha = value;
    }

    private void OnHideComplete()
    {
        pager.RemoveSubMenu(this);
    }
}
```

### ページのスクリプト
最後に、ページのスクリプトです。
先ほど作った基幹クラスを継承させます。
中身は空っぽでOKです。

```csharp:HogePage.cs
using UnityEngine;
using System.Collections;

public class HogePage : PageAbstract
{
}
```


## メニューのシーンを準備
続いて、メニューのシーンを準備します。
Canvasを2つ作り、ひとつは「MainCanvas」、もうひとつは「SurfaceCanvas」と名前を付けます。
SurfaceCanvasは常に前面に表示したいので、CanvasコンポーネントのSort Orderを1にします。
![スクリーンショット 2015-12-10 22.43.25.png](https://qiita-image-store.s3.amazonaws.com/0/74903/ea7d6392-6585-1f18-9274-768b374f2c42.png)



MainCanvasに、ページ呼び出し（ページ遷移）用の「GoToHogePageButton」を、
SurfaceCanvasに、ページを閉じるための「CloseButton」を配置します。
![スクリーンショット 2015-12-10 22.45.59.png](https://qiita-image-store.s3.amazonaws.com/0/74903/cd24edda-2915-fe4a-0af5-a0943c363b72.png)


### ページ管理オブジェクトを配置
空のゲームオブジェクトを作り、名前を「Pager」にします。
Pager.csスクリプトをアタッチし、インスペクターでGoToHogePageButtonとCloseButtonをひも付けしておきます。
![スクリーンショット 2015-12-10 22.46.31.png](https://qiita-image-store.s3.amazonaws.com/0/74903/a6edd56c-e8e7-fa8a-a368-2dc5154c29c4.png)


ここまでの準備が出来たら、シーンを保存しましょう。
シーンの名前は「Menu」にしておきます。

## ページのシーンを準備
ページ用に新しいシーンを作成します。
画面遷移したことがわかりやすいように、Panelを配置して真っ赤にでもしておきましょうか。

次に、CanvasにHogePageスクリプトをアタッチします。
HogePageスクリプトのContentsにはPanelをひも付けます。
![スクリーンショット 2015-12-10 22.51.42.png](https://qiita-image-store.s3.amazonaws.com/0/74903/9eb490e2-39f7-d8cc-b121-25232efc9265.png)


シーンをロードしたときに邪魔になりますので、Main Camera、Directional Light、EventSystemをシーンから削除します。
![スクリーンショット 2015-12-10 22.52.40.png](https://qiita-image-store.s3.amazonaws.com/0/74903/0cf5f3e2-2750-8808-e371-0654b8515251.png)


最後に、シーンを保存しましょう。
名前は「HogePage」にしておきます。


## Build Settingsにシーンを登録する
先ほど作ったMenuとHogePageシーンを登録しておきます。
![スクリーンショット 2015-12-10 22.53.15.png](https://qiita-image-store.s3.amazonaws.com/0/74903/9d7c52f8-8aad-7a19-7af2-386192d4cfd6.png)


# 実行してみよう
Menuシーンを開き、Editorで実行します。
上手くページ遷移できましたか？
ページをたくさん作って連続でページ遷移させると、ちゃんとスタックしていきます。


# マルチシーンエディット機能の恩恵
Unity5.3をお使いであれば、MenuシーンとHogePageシーンを両方開いた状態で実行してみてください。
すると、HogePageがちゃんとページスタックに積まれた状態で開きます。
これってちょっとステキなことですよね。


# まとめ
管理が楽でページをガンガン増やせるので、結構重宝してます。
バリバリな方々はもっと素晴らしいパターンを使われているかと思いますので、ぜひ教えてくださいましm(_ _)m