Unity3D
Unity
uGUI
UnityDay 11

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

More than 1 year has passed since last update.

こちらは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つのスクリプトを準備します。

ページ管理スクリプト

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

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();
        }
    }
}

ページ基幹クラス

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

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です。

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

MainCanvasに、ページ呼び出し(ページ遷移)用の「GoToHogePageButton」を、
SurfaceCanvasに、ページを閉じるための「CloseButton」を配置します。
スクリーンショット 2015-12-10 22.45.59.png

ページ管理オブジェクトを配置

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

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

ページのシーンを準備

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

次に、CanvasにHogePageスクリプトをアタッチします。
HogePageスクリプトのContentsにはPanelをひも付けます。
スクリーンショット 2015-12-10 22.51.42.png

シーンをロードしたときに邪魔になりますので、Main Camera、Directional Light、EventSystemをシーンから削除します。
スクリーンショット 2015-12-10 22.52.40.png

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

Build Settingsにシーンを登録する

先ほど作ったMenuとHogePageシーンを登録しておきます。
スクリーンショット 2015-12-10 22.53.15.png

実行してみよう

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

マルチシーンエディット機能の恩恵

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

まとめ

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