Posted at
UnityDay 11

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

More than 3 years have 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