【Unity】単一シーン/マルチシーンで良い感じに状態管理がしたい

## はじめに

Unityを使ってインタラクティブなインスタレーションをよく作っていますが、ゲーム等と同様に「タイトル画面」→「体験画面」→「結果画面」→「タイトル画面」...のような画面遷移・状態制御をしなければならないことが多くあります。

毎回スクラッチで作っていくのもしんどくなってきたので、良い感じにやる方法を調べて、テンプレートにしていきたく、備忘録的にまとめました。

## Unityでの状態管理を良い感じにしてくれる君
調べていたら、下記の2つのライブラリを発見したので、それぞれ解説してみたいと思います。

- [ImtStateMachine](https://github.com/Sinoa/IceMilkTea/blob/develop/Packages/IceMilkTea/Runtime/Core/UnitCode/PureCsharp/StateMachine.cs)
- [Unidux](https://github.com/mattak/Unidux)

### ImtStateMachine

`ImtStateMachine` は、Sinoa氏が開発している `IceMilkTea` という、Unityゲームエンジン向け開発サポートライブラリの一部です。
ソースが1ファイルしかないので、導入が簡単でパフォーマンスも高く、ライセンスがゆるい点などが魅力的です。

このライブラリは、1つのシーンにおいて各オブジェクトの状態制御が簡単に行えるので、 **1シーンで完結する場合の状態制御をしたいときに有効** だと思います。

下記の記事が詳しく解説してくれているので、非常にわかりやすいです。

- [ステートマシン実装の決定版ImtStateMachineについて語り尽くす](https://qiita.com/BelColo/items/a94c9ccc2d5174dc29a3)

### Unidux

こちらは `Redux` という、状態を管理するフレームワークをUnityで扱うためのプラグインです。
もともとReduxは、ReactJSが扱うUIのstate(状態)を管理をするためのフレームワークとしてつくられたため、Webアプリを作る人には馴染みがあるかもしれません。

Reduxの説明に関しては、下記の記事が参考になりました。

- [Redux入門【ダイジェスト版】10分で理解するReduxの基礎](https://qiita.com/kitagawamac/items/49a1f03445b19cf407b7)
- [Reduxを分かりやすく解説してみた](https://future-architect.github.io/articles/20200429/)
- [Redux 入門 〜Reduxの基礎を理解する〜](https://qiita.com/soarflat/items/bd319695d156654bbe86)

Unidux では、`Page` と `Scene` という概念でそれぞれの状態(State)を管理しています。
いわゆる1画面が1Pageに相当し、1つのPageのなかに1~n[個]のSceneがあるようなイメージです。

Uniduxではシーン遷移機能を兼ね備えており、シーン遷移時(Page遷移時)にパラメータを渡すこともできるので、**マルチシーンを行き来する場合や、1画面に複数シーンを配置したいときなどの状態管理に対して有効** であると思いました。

## Uniduxを用いたマルチシーンでの状態管理のサンプル

今回はUniduxを用いた複数シーンの状態管理を行うサンプルを作ってみました。
本家のSceneTrasitionのサンプルや、下記の記事を大いに参考にして作成しました。

- [Unidux/Examples/SceneTransition](https://github.com/mattak/Unidux/tree/master/Assets/Plugins/Unidux/Examples/SceneTransition)
- [Uniduxでゲームサイクルを実装する](https://tech.mobilefactory.jp/entry/2018/12/10/163322)

### 最終サンプル
こちらにアップしました。

- https://github.com/genki-ist/Unidux-SceneTransitionSample

### 開発環境
- 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` に記述します。

```c#:PageName.cs

public enum PageName
{
    PAGE_TITLE,
    PAGE_CONTENTS,
    PAGE_RESULT,
}
```

2: 状態管理したいオブジェクトを `State` クラスに記述します。

```c#:State.cs
[Serializable]
public class State : StateBase
{
    public SceneState<SceneName> Scene = new SceneState<SceneName>();
    public PageState<PageName> Page = new PageState<PageName>();
}
```
3: 各Pageと、そこに追加したいSceneの対応を `SceneConfig` クラスに記述します。

```c#:SceneConfig.cs
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]` をつけています。

```c#:ContentsPageData.cs
[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のシングルトンを作成します。

![png](https://raw.githubusercontent.com/genki-ist/Unidux-SceneTransitionSample/main/Assets/Docs/screenshot1.png)

#### Dispatcher
Dispatcherは、状態(State)を変更するActionを、後述するReducerに伝える役割を持っています。
UniduxではUniRxが使えるので、購読しているイベント(Obeservable)発生時に応じてReducerにActionを渡しています。

#### Reducer
Reducerは、管理している状態(State)を変更できる存在です。
Dispatcherから送られてきたActionを実行し、状態を変更します。
今回は、この部分はサンプルからほとんど変えていません。

```c#:PageReducer.cs
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)します。

```c#
this.GetComponent<Button>()
    .OnClickAsObservable()
    .Select(_ => PageDuck<PageName, SceneName>.ActionCreator.Push(PageName.PAGE_TITLE))
    .Subscribe(action => Unidux.Dispatch(action))
    .AddTo(this);
```

また、 `Contents` シーンでは画面をクリックした回数をカウントして保持しておき、 `Result` シーンに遷移するときにその数値を `Score` として渡すようにしてみます。

```c#:CountMouseClickDispatcher.cs
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);
    }
}
```

```c#:GoToResultButtonDispatcher.cs
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>` でシングルトン内のパラメータにアクセスすることができます。

```c#
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` に変更します。

![png](https://raw.githubusercontent.com/genki-ist/Unidux-SceneTransitionSample/main/Assets/Docs/screenshot2.png)

2: フェードアニメーションを行う `SceneTransitionFaderRenderer` クラスを作成します。
フェード自体は `Image` の透明度を制御するだけなので、[0,1]でいいかんじにアニメーションするやつが書ければOKです。

3: Page全体を監視している `PageWatcher` にて、状態変更の命令があったら最初にフェードインが走るように設定をします。

4: フェードインが完了したら、`SceneTransitionFaderRenderer` 内でフラグ `CanSceneTransition` を立てるようにします。

5: 先ほどのフラグを `PageWatcher` で監視しておくことで、フェードインが完了した後に、Page, Sceneの変更処理およびフェードアウトが走るようにします。

```c#
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);
```

これで完成です。

![gif](https://raw.githubusercontent.com/genki-ist/Unidux-SceneTransitionSample/main/Assets/Docs/unidux_scene_transition_sample.gif)

## おわりに
Unityで良い感じに状態管理をしたく、 `ImtStateMachine` と `Unidux` について調べました。
`ImtStateMachine` はひとつのシーン内での状態管理に便利で、 `Unidux` は複数シーンを行き来するときの状態管理に便利であることがわかりました。

Uniduxに関しては、実際にサンプルを作ってみました。

まだまだ改良の余地はかなりあると思いますが、マルチシーンにて状態管理するサンプルが増えた程度に思っていただければ幸いです。

## 参考リンクまとめ
各所に記載していた参考記事や、その他参考にした記事をまとめています。

- [ステートマシン実装の決定版ImtStateMachineについて語り尽くす](https://qiita.com/BelColo/items/a94c9ccc2d5174dc29a3)
- [Redux入門【ダイジェスト版】10分で理解するReduxの基礎](https://qiita.com/kitagawamac/items/49a1f03445b19cf407b7)
- [Reduxを分かりやすく解説してみた](https://future-architect.github.io/articles/20200429/)
- [Redux 入門 〜Reduxの基礎を理解する〜](https://qiita.com/soarflat/items/bd319695d156654bbe86)
- https://github.com/mattak/Unidux/tree/master/Assets/Plugins/Unidux/Examples/SceneTransition
- [Uniduxでゲームサイクルを実装する](https://tech.mobilefactory.jp/entry/2018/12/10/163322)
- [Unidux(redux)でUnityのお手軽状態管理](https://qiita.com/kyusque/items/518948002a97a375cb4f)
- [【Unity】ボタンを押したときに画面クリックは無視する](https://nn-hokuson.hatenablog.com/entry/2017/07/12/220302)