こちらはUnity Advent Calendar11日目の記事となります。
前日はtoru_inoueさんによるタイムラインGUI TimeFlowShiki をオープンソースにしてみたでした。
今回のお題
本来はSOOMLA LevelUpのAssetに関する記事を書こうと思っていたのですが、火曜日にAsset Storeを見たらSOOMLA LevelUpが消えているという衝撃的な事実が発覚したため、急遽内容を変更させていただきましたorz
最近Unity UIでメニュー画面を作る際、ページやダイアログなど、要素ごとにシーンを分離してシームレスにロードするパターンがお気に入りです。
Unity5.3から追加されたマルチシーンエディット機能の恩恵も受けられますので、その点でもオススメ出来るかもしれません。
メリット
各シーンにページを1つだけ配置するので、遷移が多くなりがちなメニュー画面も細分化して管理しやすくなります。
また、分業しやすくなりますので、複数人数での開発も向いてるかもです。
デメリット
シーンを読み込むため、PrefabをInstantiateするのに比べるとオーバーヘッドが多くなります。
ただ、古めのAndroid端末でもほとんど違和感は感じませんので、大量にコールしない限りは問題ありません。
あとはBuild Settingで大量のシーンを登録する必要が出てきますが、これはご愛嬌ということで。
実装してみよう
では、実装してみましょう。
iTweenをインポート
ページ遷移のアニメーションに使いますので、Asset StoreからiTweenをインポートします。
iTween大好き。イェー。
スクリプトを準備
下記3つのスクリプトを準備します。
ページ管理スクリプト
ページを管理するスクリプトで、メニューの基幹シーンに配置します。
とりあえずデモということで、ボタン制御処理も含めています。
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();
        }
    }
}
ページ基幹クラス
ページの量産を楽にするため、ページ基幹クラスを準備します。
ページ遷移アニメーションなどの共通処理はここに書いておくイメージです。
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です。
using UnityEngine;
using System.Collections;
public class HogePage : PageAbstract
{
}
メニューのシーンを準備
続いて、メニューのシーンを準備します。
Canvasを2つ作り、ひとつは「MainCanvas」、もうひとつは「SurfaceCanvas」と名前を付けます。
SurfaceCanvasは常に前面に表示したいので、CanvasコンポーネントのSort Orderを1にします。

MainCanvasに、ページ呼び出し(ページ遷移)用の「GoToHogePageButton」を、
SurfaceCanvasに、ページを閉じるための「CloseButton」を配置します。

ページ管理オブジェクトを配置
空のゲームオブジェクトを作り、名前を「Pager」にします。
Pager.csスクリプトをアタッチし、インスペクターでGoToHogePageButtonとCloseButtonをひも付けしておきます。

ここまでの準備が出来たら、シーンを保存しましょう。
シーンの名前は「Menu」にしておきます。
ページのシーンを準備
ページ用に新しいシーンを作成します。
画面遷移したことがわかりやすいように、Panelを配置して真っ赤にでもしておきましょうか。
次に、CanvasにHogePageスクリプトをアタッチします。
HogePageスクリプトのContentsにはPanelをひも付けます。

シーンをロードしたときに邪魔になりますので、Main Camera、Directional Light、EventSystemをシーンから削除します。

最後に、シーンを保存しましょう。
名前は「HogePage」にしておきます。
Build Settingsにシーンを登録する
先ほど作ったMenuとHogePageシーンを登録しておきます。

実行してみよう
Menuシーンを開き、Editorで実行します。
上手くページ遷移できましたか?
ページをたくさん作って連続でページ遷移させると、ちゃんとスタックしていきます。
マルチシーンエディット機能の恩恵
Unity5.3をお使いであれば、MenuシーンとHogePageシーンを両方開いた状態で実行してみてください。
すると、HogePageがちゃんとページスタックに積まれた状態で開きます。
これってちょっとステキなことですよね。
まとめ
管理が楽でページをガンガン増やせるので、結構重宝してます。
バリバリな方々はもっと素晴らしいパターンを使われているかと思いますので、ぜひ教えてくださいましm(_ _)m
