LoginSignup
8

More than 3 years have passed since last update.

posted at

updated at

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

こちらは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

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
What you can do with signing up
8