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

こちらは[Unityゆるふわサマーアドベントカレンダー 2019](https://qiita.com/nkjzm/items/7c933bb5c772a0b75512) 5日目の記事となります。

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

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

## サンプル
[最終サンプル](https://github.com/kyusque/UniduxSample)
SDこはくちゃんズの誰かを状態監視しながら走らせます。

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

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

<img width="1174" alt="スクリーンショット 2019-08-15 10.23.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158368/81621fb6-6004-f54b-e753-04334b990529.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
以下をインポート

- [SDこはくちゃんズ](http://unity-chan.com/download/releaseNote.php?id=SDKohakuchanz)
- [Unidux](https://github.com/mattak/Unidux/releases/download/0.3.4/Unidux-v0.3.4.unitypackage)

[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にする

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

<img width="1440" alt="スクリーンショット 2019-08-15 8.36.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158368/9009ac3b-100c-837f-65e0-4fbb21bad532.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フィールドを追加します。

```c#: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の更新処理を入れます。

```c#:
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を確認することができます。

```c#:State.cs
////
[Serializable]
public partial class State : StateBase
{
    public PersonState personState = new PersonState(){person = PersonState.Person.Kohaku};
}
////
```

```c#:Unidux.cs
////
private static IReducer[] Reducers
{
    get {
        // Assign your reducers
        return new IReducer[]
        {
            new PersonAction.Reducer(), 
        };
    }
}
////
```

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

<img width="404" alt="スクリーンショット 2019-08-15 8.51.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158368/a7711e84-8a21-e573-5f24-91d79baa3364.png">


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

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

```c#: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を変えると人が変わるようになったと思います。

<img width="1178" alt="スクリーンショット 2019-08-15 9.13.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158368/4634a2be-5c29-d82e-4b9c-543dabdde900.png">

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

### ClothState

#### ClothState作成
```c#:ClothState.cs
using System;
using Unidux;

namespace App
{
    [Serializable]
    public class ClothState : StateElement
    {
        public Cloth cloth;
        
        public enum Cloth
        {
            Summer,
            Winter,
        }
    }
}
```
#### ClothAction作成

```c#: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にアタッチして各々の夏服冬服をセット。

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

```

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

<img width="1180" alt="スクリーンショット 2019-08-15 9.48.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158368/92cb6062-1198-f714-1d02-4082cc828489.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作成
```c#: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フィールドを追加後、遷移の矢印のパラメータを適切な形に設定してください。

<img width="1165" alt="スクリーンショット 2019-08-15 10.02.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158368/ed6c43e8-7c36-8273-99a3-d727298ced4f.png">


```c#: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]から動きを制御できると思います。

<img width="1289" alt="スクリーンショット 2019-08-15 10.23.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158368/915eff42-bcfb-238a-b4d1-0b2d4516e576.png">



### 最終的なState.csとUnidux.cs

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

```

```c#: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追加
```c#: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;
            }
        }
    }
}

```

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

```

```c#: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作成

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

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


            });
    }
}
```

<img width="1378" alt="スクリーンショット 2019-08-15 23.55.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158368/2dd5efc7-d3d7-eb82-c69c-e96a24436e6e.png">