LoginSignup
15
8

More than 3 years have passed since last update.

Unidux(redux)でUnityのお手軽状態管理

Last updated at Posted at 2019-08-15

こちらはUnityゆるふわサマーアドベントカレンダー 2019 5日目の記事となります。

Unidux(redux)でUnityのお手軽状態管理

状態を管理するフレームワークであるReduxをUnityで扱うためのプラグインのUniduxを使ってみたい思います。

サンプル

最終サンプル
SDこはくちゃんズの誰かを状態監視しながら走らせます。

SDこはくちゃんズの要素として、人物{こはく, みさき, ゆうこ}、服装{夏服, 冬服}があり、彼女らを各方向に走らせたいので位置{東, 西, 南, 北}を持つとします。

なので、Stateの要素、StateElementとして{person, cloth, position}を考え、各々のActionを作っていきます。

スクリーンショット 2019-08-15 10.23.41.png

筆者の知識レベル

筆者のReduxに対する認識は「状態をログとして保存したり再現するのに便利な設計」らしいよ程度です。

筆者の各用語の認識

  • State
    • アプリケーションの状態を再現するのに必要な全情報を持つ。シリアル化できる。
  • StateElement
    • State中の各要素
  • Action(大)
    • StateElement毎にStateを変更する為の操作を定義するもの。
    • ActionType、Action(小)、Reducerからなる。
      • ActionTypeはStateの変更の仕方の種類
      • Action(小)はActionTypeをもつ値オブジェクト
      • Reducerは渡されたStateとAction(小)に応じてStateを変更する関数
    • 直接アプリ内で触ることはない(Reducerが勝手にやってくれる)
    • (後述のDicpatcherの為、ActionCreaterも作っておく)
  • Dispatcher
    • アプリのUIの操作やイベントなどに応じたActionをお願いするもの。
    • Unityの場合はMonoBehaviourになると思われる。
    • やって欲しいActionをここでDispathするとReducerが勝手に(略)
    • ActionはActionCreaterから生成する。
    • ActionCreaterを作った人が完璧なメソッドを作ってくれてるはずなので、Actionの具体的な中身を考える必要はない(はず)
    • Stateを変える働きのみ。Stateの変更を検知してそれに応じた処理はUniRxなどで別に作る。
    • Uniduxの場合、UniduxPanelでUnityエディタ上のインスペクタからStateをいじれるので最初は書く必要が無いかも

準備

Unity Editor Version: 2018.4.2f1
以下をインポート

[GameObject]>[CreateEmpty]で空オブジェクト(Player)を作成
Playerの下で更に[GameObject]>[CreateEmpty]を三回繰り返してkohaku,misaki,yukoを作成
ohaku,misaki,yukoにAssets/UnityChan/SD_Kohaku_chanz/Modelsのモデルを各々が夏服と冬服を持つように入れる
[GameObject]>[3D Object]>[Plane]で床を作成して、インスペクターでstaticにチェック
[Window]>[AI]>[NavMesh]>[Bake]>[Bake]でPlaneをNavMeshにする

こんな感じになると思います。

スクリーンショット 2019-08-15 8.36.55.png

Unidux

Unidux準備

基本的にはproject windowで[Create]>[Unidux]から順番に作っていけば大丈夫です。

[Create]>[Unidux]>[State]でState.cs(デフォルト)を作成
[Create]>[Unidux]>[Unidux]でUnidux.cs(デフォルト)を作成し、カメラにアタッチ

これで準備が整いました。
この後、StateElementとActionを作る毎に、Stateには作ったStateElementのフィールドを、Unidux.csにはReducersにActionのReducerを追加していく形になります。

StateElement作成

では、先ほどのState{person, cloth, position}の各々のStateElementとActionを作っていきます。
順にやっていきます。

PersonState

PersonStateの作成

[Create]>[Unidux]>[State Element]でPersonStateを作成

enumでPersonを作成し、personフィールドを追加します。

PersonState.cs
using System;
using Unidux;

namespace App
{
    [Serializable]
    public class PersonState : StateElement
    {
        public Person person;
        public enum Person
        {
            Kohaku,
            Misaki,
            Yuko
        }
    }
}

PersonActionの作成

PersonStateに対してActionを作っていきます。
ActionですがDuckで作ると楽なので、[Create]>[Unidux]>[Duck]でPersionActionを作成します。
enumでActionTypeを作成し、ActionにActionType型のtypeフィールドを追加します。
その後、ReducerにActionTypeごとにStateを更新する処理を加えてください。
switchにActionTypeを入れるとIDEの機能で全場合分けが出てくるので、case毎にStateの更新処理を入れます。

using System;
using Unidux;

namespace App
{
    public static class PersonAction
    {
        public enum ActionType
        {
            ChangeToKohaku,
            ChangeToMisaki,
            ChangeToYuko,
        }

        public class Action
        {
            public ActionType type;
        }

        public static class ActionCreator
        {
        }

        public class Reducer : ReducerBase<State, Action>
        {
            public override State Reduce(State state, Action action)
            {
                switch (action.type)
                {
                    case ActionType.ChangeToKohaku:
                        state.personState.person = PersonState.Person.Kohaku;
                        break;
                    case ActionType.ChangeToMisaki:
                        state.personState.person = PersonState.Person.Misaki;
                        break;
                    case ActionType.ChangeToYuko:
                        state.personState.person = PersonState.Person.Yuko;
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }

                return state;
            }
        }
    }
}


取り合えずこのタイミングでState.csとUnidux.csに以下のように追記を行って、[Window]>[UniduxPanel]で確認します。
[UniduxPanel]のIStoreAccessorにUniduxをアタッチされたオブジェクト(カメラ)を追加すると、現在のStateを確認することができます。

State.cs
////
[Serializable]
public partial class State : StateBase
{
    public PersonState personState = new PersonState(){person = PersonState.Person.Kohaku};
}
////
Unidux.cs
////
private static IReducer[] Reducers
{
    get {
        // Assign your reducers
        return new IReducer[]
        {
            new PersonAction.Reducer(), 
        };
    }
}
////

こんな感じで確認できます。

スクリーンショット 2019-08-15 8.51.39.png

PersonStateが変わった時の処理の作成

では、PersonStateが変わった時に人が変わるような処理をMonoBehaviourで書きます。
StateはUnidux.Stateから読み込むことが出来ます。
また、UniduxはSubjectが使えるのでそのままUniRxのように書くことが出来ます。
以下のように書いて、Playerにアタッチして、各フィールドにゲームオブジェクト(kohaku, misaki, yuko)をアタッチします。

ChangePerson
using System;
using System.Collections;
using System.Collections.Generic;
using App;
using UniRx;
using UnityEngine;

public class PersonChange : MonoBehaviour
{
    public GameObject kohaku;
    public GameObject misaki;
    public GameObject yuko;

    private void OnEnable()
    {
        App.Unidux
            .Subject
            .TakeUntilDisable(this)
            .StartWith(App.Unidux.State)
            .Subscribe(state =>
            {
                switch (state.personState.person)
                {
                    case PersonState.Person.Kohaku:
                        kohaku.SetActive(true);
                        misaki.SetActive(false);
                        yuko.SetActive(false);
                        break;
                    case PersonState.Person.Misaki:
                        kohaku.SetActive(false);
                        misaki.SetActive(true);
                        yuko.SetActive(false);
                        break;
                    case PersonState.Person.Yuko:
                        kohaku.SetActive(false);
                        misaki.SetActive(false);
                        yuko.SetActive(true);
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }).AddTo(this);
    }
}

UniduxPanelでStateを変えると人が変わるようになったと思います。

スクリーンショット 2019-08-15 9.13.59.png

残りのClothStateとPositionStateも同じように作っていけばいけますので省略していきます。

ClothState

ClothState作成

ClothState.cs
using System;
using Unidux;

namespace App
{
    [Serializable]
    public class ClothState : StateElement
    {
        public Cloth cloth;

        public enum Cloth
        {
            Summer,
            Winter,
        }
    }
}

ClothAction作成

ClothAction.cs
using System;
using Unidux;

namespace App
{
    public static class ClothAction
    {
        public enum ActionType
        {
            ToSummer,
            ToWinter,
        }

        public class Action
        {
            public ActionType type;
        }

        public static class ActionCreator
        {
        }

        public class Reducer : ReducerBase<State, Action>
        {
            public override State Reduce(State state, Action action)
            {
                switch (action.type)
                {
                    case ActionType.ToSummer:
                        state.clothState.cloth = ClothState.Cloth.Summer;
                        break;
                    case ActionType.ToWinter:
                        state.clothState.cloth = ClothState.Cloth.Winter;
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
                return state;
            }
        }
    }
}

State.csとRedux.csへのClothStateの追記を忘れずに。

ClothState更新時の処理

kohaku, misaki, yukoにアタッチして各々の夏服冬服をセット。

ChangeCloth.cs
using System;
using System.Collections;
using System.Collections.Generic;
using App;
using UniRx;
using UnityEngine;

public class ChangeCloth : MonoBehaviour
{
    public GameObject summer;
    public GameObject winter;

    private void Start()
    {
        App.Unidux.Subject
            .TakeUntilDisable(this)
            .StartWith(App.Unidux.State)
            .Subscribe(state =>
            {
                if (!gameObject.activeSelf) return;

                switch (state.clothState.cloth)
                {
                    case ClothState.Cloth.Summer:
                        summer.SetActive(true);
                        winter.SetActive(false);
                        break;
                    case ClothState.Cloth.Winter:
                        summer.SetActive(false);
                        summer.SetActive(true);
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }).AddTo(this);
    }
}

こんな感じで服装を変更出来るようになります。

スクリーンショット 2019-08-15 9.48.55.png

PositonState

PositionState作成

c#.PositionState.cs
using System;
using Unidux;

namespace App
{
    [Serializable]
    public class PositionState : StateElement
    {
        public Position position;

        public enum Position
        {
            East,
            West,
            South,
            North,
        }
    }
}

PositionAction作成

PositionAction.cs
using System;
using Unidux;

namespace App
{
    public static class PositionAction
    {
        public enum ActionType
        {
            ToEast,
            ToWest,
            ToSouth,
            ToNorth,
        }
        public class Action
        {
            public ActionType ActionType;
        }

        public static class ActionCreator
        {
        }

        public class Reducer : ReducerBase<State, Action>
        {
            public override State Reduce(State state, Action action)
            {
                switch (action.ActionType)
                {
                    case ActionType.ToEast:
                        state.positionState.position = PositionState.Position.East;
                        break;
                    case ActionType.ToWest:
                        state.positionState.position = PositionState.Position.West;
                        break;
                    case ActionType.ToSouth:
                        state.positionState.position = PositionState.Position.South;
                        break;
                    case ActionType.ToNorth:
                        state.positionState.position = PositionState.Position.North;
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
                return state;
            }
        }
    }
}

State.csとRedux.csへのClothStateの追記を忘れずに。

PositionState更新時の処理

AnimationControllerと連動させるので少しややこしいです。

以下のスクリプトを各モデルにアタッチ(計六体)して、AnimatorにAnimationControllerをセットしてください。
その後、AnimationControllerの画面のparameterでbool型のmoveフィールドを追加後、遷移の矢印のパラメータを適切な形に設定してください。

スクリーンショット 2019-08-15 10.02.32.png

PositionChange.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UniRx;
using UniRx.Triggers;
using UnityEngine;
using UnityEngine.AI;

namespace App
{
    [RequireComponent (typeof (NavMeshAgent))]
    [RequireComponent (typeof (Animator))]
    public class PositionChange : MonoBehaviour
    {
        Animator anim;
        NavMeshAgent agent;
        Vector2 smoothDeltaPosition = Vector2.zero;
        Vector2 velocity = Vector2.zero;

        private void OnEnable()
        {
            anim = GetComponent<Animator> ();
            agent = GetComponent<NavMeshAgent> ();
            // 位置を自動的に更新しません
            agent.updatePosition = false;

            App.Unidux
                .Subject
                .TakeUntilDisable(this)
                .StartWith(App.Unidux.State)
                .Subscribe(state =>
                {
                    switch (state.positionState.position)
                    {
                        case PositionState.Position.East:
                            agent.destination = new Vector3(-5, 0, 0);
                            break;
                        case PositionState.Position.West:
                            agent.destination = new Vector3(5, 0, 0);
                            break;
                        case PositionState.Position.South:
                            agent.destination = new Vector3(0, 0, 5);
                            break;
                        case PositionState.Position.North:
                            agent.destination = new Vector3(0, 0, -5);
                            break;
                        default:
                            throw new ArgumentOutOfRangeException();
                    }
                })
                .AddTo(this);

            this.UpdateAsObservable()
                .TakeUntilDisable(this)
                .Subscribe(_ =>
                {
                    Vector3 worldDeltaPosition = agent.nextPosition - transform.position;

                    // worldDeltaPosition をローカル空間にマップします
                    float dx = Vector3.Dot(transform.right, worldDeltaPosition);
                    float dy = Vector3.Dot(transform.forward, worldDeltaPosition);
                    Vector2 deltaPosition = new Vector2(dx, dy);

                    // deltaMove にローパスフィルターを適用します
                    float smooth = Mathf.Min(1.0f, Time.deltaTime / 0.15f);
                    smoothDeltaPosition = Vector2.Lerp(smoothDeltaPosition, deltaPosition, smooth);

                    // 時間が進んだら、velocity (速度) を更新します
                    if (Time.deltaTime > 1e-5f)
                        velocity = smoothDeltaPosition / Time.deltaTime;

                    bool shouldMove = velocity.magnitude > 0.5f && agent.remainingDistance > agent.radius;

                    // アニメーションのパラメーターを更新します
                    anim.SetBool("move", shouldMove);
                    //anim.SetFloat("velx", velocity.x);
                    //anim.SetFloat("vely", velocity.y);

                }).AddTo(this);

        }

        void OnAnimatorMove ()
        {
            transform.position = agent.nextPosition;
        }
    }
}

[Window]>[UniduxPanel]>[State]から動きを制御できると思います。

スクリーンショット 2019-08-15 10.23.08.png

最終的なState.csとUnidux.cs

State.cs
using System;
using Unidux;

namespace App
{
    [Serializable]
    public partial class State : StateBase
    {
        public PersonState personState = new PersonState(){person = PersonState.Person.Kohaku};
        public ClothState clothState = new ClothState(){cloth = ClothState.Cloth.Summer};
        public PositionState positionState = new PositionState(){position = PositionState.Position.East};
    }
}

Unidux.cs
using Unidux;
using UniRx;
using UnityEngine;

namespace App
{
    public sealed class Unidux : SingletonMonoBehaviour<Unidux>, IStoreAccessor
    {
        public TextAsset InitialStateJson;

        private Store<State> _store;

        public IStoreObject StoreObject
        {
            get { return Store; }
        }

        public static State State
        {
            get { return Store.State; }
        }

        public static Subject<State> Subject
        {
            get { return Store.Subject; }
        }

        private static IReducer[] Reducers
        {
            get {
                // Assign your reducers
                return new IReducer[]
                {
                    new PersonAction.Reducer(), 
                    new ClothAction.Reducer(), 
                    new PositionAction.Reducer(),
                };
            }
        }

        private static State InitialState
        {
            get
            {
                return Instance.InitialStateJson != null
                    ? UniduxSetting.Serializer.Deserialize(
                        System.Text.Encoding.UTF8.GetBytes(Instance.InitialStateJson.text),
                        typeof(State)
                    ) as State
                    : new State();
            }
        }

        public static Store<State> Store
        {
            get { return Instance._store = Instance._store ?? new Store<State>(InitialState, Reducers); }
        }

        public static object Dispatch<TAction>(TAction action)
        {
            return Store.Dispatch(action);
        }

        void Start()
        {
            // Assign your middlewares
            // Store.ApplyMiddlewares(SampleMiddleware.Process);
        }

        void Update()
        {
            Store.Update();
        }
    }
}

(おまけ)Dispatcher

せっかくなので、キーボード入力でActionを生成してDispatchしStateを変更出来るようにします。
各ActionにDispatcherで使うActionCreaterを定義します。
人物はspaceキー、服装はreturnキー、位置は十字キーに割り当てます。
前二つはStateを読み込んで次の要素に変更する形にします。

各ActionのActionCreater追加

PersonAction.cs
using System;
using Unidux;

namespace App
{
    public static class PersonAction
    {
        public enum ActionType
        {
            ChangeToKohaku,
            ChangeToMisaki,
            ChangeToYuko,
        }

        public class Action
        {
            public ActionType type;
        }

        public static class ActionCreator
        {
            public static Action ChangeToNextPerson(State state)
            {
                switch (state.personState.person)
                {
                    case PersonState.Person.Kohaku:
                        return new Action(){type = ActionType.ChangeToMisaki};
                    case PersonState.Person.Misaki:
                        return new Action(){type = ActionType.ChangeToYuko};
                    case PersonState.Person.Yuko:
                        return new Action(){type = ActionType.ChangeToKohaku};
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }
        }

        public class Reducer : ReducerBase<State, Action>
        {
            public override State Reduce(State state, Action action)
            {
                switch (action.type)
                {
                    case ActionType.ChangeToKohaku:
                        state.personState.person = PersonState.Person.Kohaku;
                        break;
                    case ActionType.ChangeToMisaki:
                        state.personState.person = PersonState.Person.Misaki;
                        break;
                    case ActionType.ChangeToYuko:
                        state.personState.person = PersonState.Person.Yuko;
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }

                return state;
            }
        }
    }
}

ClothAction.cs
using System;
using Unidux;

namespace App
{
    public static class ClothAction
    {
        public enum ActionType
        {
            ToSummer,
            ToWinter,
        }

        public class Action
        {
            public ActionType type;
        }

        public static class ActionCreator
        {
            public static Action ChangeCloth(State state)
            {
                switch (state.clothState.cloth)
                {
                    case ClothState.Cloth.Summer:
                        return new Action(){type = ActionType.ToWinter};
                    case ClothState.Cloth.Winter:
                        return new Action(){type = ActionType.ToSummer};
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }
        }

        public class Reducer : ReducerBase<State, Action>
        {
            public override State Reduce(State state, Action action)
            {
                switch (action.type)
                {
                    case ActionType.ToSummer:
                        state.clothState.cloth = ClothState.Cloth.Summer;
                        break;
                    case ActionType.ToWinter:
                        state.clothState.cloth = ClothState.Cloth.Winter;
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
                return state;
            }
        }
    }
}

PositionAction.cs
using System;
using Unidux;

namespace App
{
    public static class PositionAction
    {
        public enum ActionType
        {
            ToEast,
            ToWest,
            ToSouth,
            ToNorth,
        }
        public class Action
        {
            public ActionType type;
        }

        public static class ActionCreator
        {
            public static Action ToEast() => new Action(){type = ActionType.ToEast};
            public static Action ToWest() => new Action(){type = ActionType.ToWest};
            public static Action ToSouth() => new Action(){type = ActionType.ToSouth};
            public static Action ToNorth() => new Action(){type = ActionType.ToNorth};
        }

        public class Reducer : ReducerBase<State, Action>
        {
            public override State Reduce(State state, Action action)
            {
                switch (action.type)
                {
                    case ActionType.ToEast:
                        state.positionState.position = PositionState.Position.East;
                        break;
                    case ActionType.ToWest:
                        state.positionState.position = PositionState.Position.West;
                        break;
                    case ActionType.ToSouth:
                        state.positionState.position = PositionState.Position.South;
                        break;
                    case ActionType.ToNorth:
                        state.positionState.position = PositionState.Position.North;
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
                return state;
            }
        }
    }
}

Dispatcher作成

今回はひとところにまとめて書きました。
適当なオブジェクト(カメラ)にアタッチしてください
ビルドして動かせます。

Dispatcher.cs
using System;
using System.Collections;
using System.Collections.Generic;
using App;
using UniRx;
using UniRx.Triggers;
using UnityEngine;

public class Dispatcher : MonoBehaviour
{
    private void Start()
    {
        this.UpdateAsObservable()
            .TakeUntilDisable(this)
            .Where(_ => Input.anyKeyDown)
            .Subscribe(_ =>
            {
                if (Input.GetKeyDown(KeyCode.Space))
                {
                    App.Unidux.Dispatch(PersonAction.ActionCreator.ChangeToNextPerson(App.Unidux.State));
                }

                if (Input.GetKeyDown(KeyCode.Return))
                {
                    App.Unidux.Dispatch(ClothAction.ActionCreator.ChangeCloth(App.Unidux.State));
                }

                if(Input.GetKeyDown(KeyCode.RightArrow)) App.Unidux.Dispatch(PositionAction.ActionCreator.ToEast());
                if (Input.GetKeyDown(KeyCode.LeftArrow)) App.Unidux.Dispatch(PositionAction.ActionCreator.ToWest());
                if (Input.GetKeyDown(KeyCode.DownArrow)) App.Unidux.Dispatch(PositionAction.ActionCreator.ToSouth());
                if (Input.GetKeyDown(KeyCode.UpArrow)) App.Unidux.Dispatch(PositionAction.ActionCreator.ToNorth());


            });
    }
}

スクリーンショット 2019-08-15 23.55.10.png

15
8
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
8