15
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Unity #3Advent Calendar 2020

Day 17

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

Last updated at Posted at 2020-12-17

はじめに

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

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

Unityでの状態管理を良い感じにしてくれる君

調べていたら、下記の2つのライブラリを発見したので、それぞれ解説してみたいと思います。

ImtStateMachine

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

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

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

Unidux

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

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

Unidux では、PageScene という概念でそれぞれの状態(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 に記述します。

PageName.cs

public enum PageName
{
    PAGE_TITLE,
    PAGE_CONTENTS,
    PAGE_RESULT,
}

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

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 クラスに記述します。

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] をつけています。

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

Dispatcher

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

Reducer

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

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)します。

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

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

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);
    }
}
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> でシングルトン内のパラメータにアクセスすることができます。

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 がいちばん手前になるように、 CanvasSort Order1 に変更します。

png

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

これで完成です。

gif

おわりに

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

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

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

参考リンクまとめ

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

15
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?