はじめに
Unityを使ってインタラクティブなインスタレーションをよく作っていますが、ゲーム等と同様に「タイトル画面」→「体験画面」→「結果画面」→「タイトル画面」...のような画面遷移・状態制御をしなければならないことが多くあります。
毎回スクラッチで作っていくのもしんどくなってきたので、良い感じにやる方法を調べて、テンプレートにしていきたく、備忘録的にまとめました。
Unityでの状態管理を良い感じにしてくれる君
調べていたら、下記の2つのライブラリを発見したので、それぞれ解説してみたいと思います。
ImtStateMachine
ImtStateMachine
は、Sinoa氏が開発している IceMilkTea
という、Unityゲームエンジン向け開発サポートライブラリの一部です。
ソースが1ファイルしかないので、導入が簡単でパフォーマンスも高く、ライセンスがゆるい点などが魅力的です。
このライブラリは、1つのシーンにおいて各オブジェクトの状態制御が簡単に行えるので、 1シーンで完結する場合の状態制御をしたいときに有効 だと思います。
下記の記事が詳しく解説してくれているので、非常にわかりやすいです。
Unidux
こちらは Redux
という、状態を管理するフレームワークをUnityで扱うためのプラグインです。
もともとReduxは、ReactJSが扱うUIのstate(状態)を管理をするためのフレームワークとしてつくられたため、Webアプリを作る人には馴染みがあるかもしれません。
Reduxの説明に関しては、下記の記事が参考になりました。
Unidux では、Page
と Scene
という概念でそれぞれの状態(State)を管理しています。
いわゆる1画面が1Pageに相当し、1つのPageのなかに1~n[個]のSceneがあるようなイメージです。
Uniduxではシーン遷移機能を兼ね備えており、シーン遷移時(Page遷移時)にパラメータを渡すこともできるので、マルチシーンを行き来する場合や、1画面に複数シーンを配置したいときなどの状態管理に対して有効 であると思いました。
Uniduxを用いたマルチシーンでの状態管理のサンプル
今回はUniduxを用いた複数シーンの状態管理を行うサンプルを作ってみました。
本家のSceneTrasitionのサンプルや、下記の記事を大いに参考にして作成しました。
最終サンプル
こちらにアップしました。
開発環境
- Unity 2019.4.14f1
- Unidux v0.3.4
- UniRx v7.1.0
Page, Scene構成
今回は以下のようなPage, Scene構成にしてみたいと思います。
どのPageにも共通して存在する Base
というSceneの上で、それぞれのPageに対応しているSceneが切り替わるイメージです。
状態(State)に応じてPageが切り替わり、それに応じて対応しているSceneが切り替わります。
Contents
というSceneでは、異なる2つの状態(Type)を持つようにし、自由に行き来できるようにします。
Scene | Page | Type |
---|---|---|
Base | (Permanent) | - |
Title | Title | - |
Contents | Contents | Type1 |
Contents | Contents | Type2 |
Result | Result | - |
State定義
まず、管理したい状態(State)の定義を行います。
1: それぞれのPage, Sceneの状態を PageName.cs
, SceneName.cs
に記述します。
public enum PageName
{
PAGE_TITLE,
PAGE_CONTENTS,
PAGE_RESULT,
}
2: 状態管理したいオブジェクトを State
クラスに記述します。
[Serializable]
public class State : StateBase
{
public SceneState<SceneName> Scene = new SceneState<SceneName>();
public PageState<PageName> Page = new PageState<PageName>();
}
3: 各Pageと、そこに追加したいSceneの対応を SceneConfig
クラスに記述します。
public class SceneConfig : ISceneConfig<SceneName, PageName>
{
private IDictionary<SceneName, int> _categoryMap;
private IDictionary<PageName, SceneName[]> _pageMap;
public IDictionary<SceneName, int> CategoryMap
{
get
{
return this._categoryMap = this._categoryMap ?? new Dictionary<SceneName, int>()
{
{SceneName.Base, SceneCategory.Permanent},
{SceneName.Title, SceneCategory.Page},
{SceneName.Contents, SceneCategory.Page},
{SceneName.Result, SceneCategory.Page},
};
}
}
public IDictionary<PageName, SceneName[]> PageMap
{
get
{
return this._pageMap = this._pageMap ?? new Dictionary<PageName, SceneName[]>()
{
{PageName.PAGE_TITLE, new[] {SceneName.Title}},
{PageName.PAGE_CONTENTS, new[] {SceneName.Contents}},
{PageName.PAGE_RESULT, new[] {SceneName.Result}},
};
}
}
}
Uniduxの設定
次に、Uniduxを使うための設定をいくつか行います。
PageData
各Pageにて、保持しておきたいデータを定義します。
その際に IPageData
というinterfaceを継承してclassを作成しておくことで、Uniduxのシングルトンからパラメータがアクセスできるようにしておきます。
また、今回はInspectorから ContentsType
を指定しておきたかったので、 [Serializable]
をつけています。
[Serializable]
public class ContentsPageData : IPageData
{
public ContentsType ContentsType;
public int MouseClickCount
{
get => this.mouseClickCount;
set => this.mouseClickCount = value;
}
private int mouseClickCount;
public ContentsPageData()
{
this.mouseClickCount = 0;
}
public ContentsPageData(ContentsType ContentsType, int mouseClickCount)
{
this.ContentsType = ContentsType;
if(mouseClickCount > 0)
{
this.mouseClickCount = mouseClickCount;
}
else
{
this.mouseClickCount = 0;
}
}
}
Unidux(Singleton)
どのページにも共通で存在する Base
シーンのHierarchyに、Uniduxのシングルトンを作成します。
Dispatcher
Dispatcherは、状態(State)を変更するActionを、後述するReducerに伝える役割を持っています。
UniduxではUniRxが使えるので、購読しているイベント(Obeservable)発生時に応じてReducerにActionを渡しています。
Reducer
Reducerは、管理している状態(State)を変更できる存在です。
Dispatcherから送られてきたActionを実行し、状態を変更します。
今回は、この部分はサンプルからほとんど変えていません。
public class PageReducer : PageDuck<PageName, SceneName>.Reducer
{
public PageReducer() : base(new SceneConfig())
{
}
public override object ReduceAny(object state, object action)
{
// It's required for detecting page scene state location
var _state = (State) state;
var _action = (PageDuck<PageName, SceneName>.IPageAction) action;
_state.Page = base.Reduce(_state.Page, _state.Scene, _action);
return state;
}
}
Watcher
管理している状態に変更があった際に、それをUnity側で監視するためのWatcherを、共通の Base
シーンに追加します。
ここで実際のScene変更 SceneManager.LoadSceneAsync()
等を行っています。
シーン遷移
今回はシーン遷移するためのActionとしてButtonを用います。
Dispatcherに、そのイベントが発生したら(今回はボタンを押したら)PageやSceneが切り替わるように設定をしていきます。
Subscribe()
のなかで Unidux.Dispatch(action)
することで、Reducerにページ情報 PageDuck<PageName, SceneName>
を送信(Push)します。
this.GetComponent<Button>()
.OnClickAsObservable()
.Select(_ => PageDuck<PageName, SceneName>.ActionCreator.Push(PageName.PAGE_TITLE))
.Subscribe(action => Unidux.Dispatch(action))
.AddTo(this);
また、 Contents
シーンでは画面をクリックした回数をカウントして保持しておき、 Result
シーンに遷移するときにその数値を Score
として渡すようにしてみます。
public class CountMouseClickDispatcher : MonoBehaviour
{
private ContentsPageData contentsPageData;
// Start is called before the first frame update
void Start()
{
this.UpdateAsObservable()
.Where(_ => Input.GetMouseButtonDown(0))
.Where(_ => !EventSystem.current.IsPointerOverGameObject())
.Subscribe(_ =>
{
var type = Unidux.State.Page.GetData<ContentsPageData>().ContentsType;
var count = Unidux.State.Page.GetData<ContentsPageData>().MouseClickCount;
count += 1;
this.contentsPageData = new ContentsPageData(type, count);
this.ChangeSceneData();
})
.AddTo(this);
}
private void ChangeSceneData()
{
var action = PageDuck<PageName, SceneName>.ActionCreator.SetData(this.contentsPageData);
Unidux.Dispatch(action);
}
}
public class GoToResultButtonDispatcher : MonoBehaviour
{
private int score;
void Start()
{
this.GetComponent<Button>().OnClickAsObservable()
.Select(_ => this.score = Unidux.State.Page.GetData<ContentsPageData>().MouseClickCount)
.Subscribe(_ => this.DispatchPageData())
.AddTo(this);
}
private void DispatchPageData()
{
var action = PageDuck<PageName, SceneName>.ActionCreator.Push(PageName.PAGE_RESULT, new ResultPageData(this.score));
Unidux.Dispatch(action);
}
}
シーン描画
先述しましたが、管理しているStateの情報を取得することができるので、それをRendererで描画してみたいと思います。
Renderer
Unidux.State.Page.GetData<T>
でシングルトン内のパラメータにアクセスすることができます。
private void Render(State state)
{
ContentsPageData pageData = state.Page.GetData<ContentsPageData>();
this.ContentsTypeText.text = pageData.ContentsType.ToString();
}
+α: ページ遷移時にフェードイン・アウトを足す
ページを遷移するときにフェード等のトランジションを使うこともよくあるので、
プラスアルファとして今回はシンプルな黒のフェードイン・アウトを追加してみようと思います。
条件として、フェードイン・アウトはPage間の遷移時にのみ実行され、同一Pageでパラメータが変更された場合には実行されないようにします。
(つまり、 Contents
シーンでTypeが変更されても、フェードが発動しないようにします)
1: まず、共通で使用する Base
シーンにフェード用の Image
を追加します。
各Sceneが Base
シーンの上に描画されてもフェード用の Image
がいちばん手前になるように、 Canvas
の Sort Order
を 1
に変更します。
2: フェードアニメーションを行う SceneTransitionFaderRenderer
クラスを作成します。
フェード自体は Image
の透明度を制御するだけなので、[0,1]でいいかんじにアニメーションするやつが書ければOKです。
3: Page全体を監視している PageWatcher
にて、状態変更の命令があったら最初にフェードインが走るように設定をします。
4: フェードインが完了したら、SceneTransitionFaderRenderer
内でフラグ CanSceneTransition
を立てるようにします。
5: 先ほどのフラグを PageWatcher
で監視しておくことで、フェードインが完了した後に、Page, Sceneの変更処理およびフェードアウトが走るようにします。
Unidux.Subject
.Where(state => state.Page.IsStateChanged && this.currentPageName != state.Page.Name)
.Subscribe(_ => this.faderRenderer.FadeIn())
.AddTo(this);
this.UpdateAsObservable()
.Where(_ => this.faderRenderer.CanSceneTransition)
.Subscribe(_ =>
{
this.UpdatePage(Unidux.State);
this.sceneWatcher.UpdateScenes(Unidux.State.Scene);
this.currentPageName = Unidux.State.Page.Name;
})
.AddTo(this);
これで完成です。
おわりに
Unityで良い感じに状態管理をしたく、 ImtStateMachine
と Unidux
について調べました。
ImtStateMachine
はひとつのシーン内での状態管理に便利で、 Unidux
は複数シーンを行き来するときの状態管理に便利であることがわかりました。
Uniduxに関しては、実際にサンプルを作ってみました。
まだまだ改良の余地はかなりあると思いますが、マルチシーンにて状態管理するサンプルが増えた程度に思っていただければ幸いです。
参考リンクまとめ
各所に記載していた参考記事や、その他参考にした記事をまとめています。