こちらはUnityゆるふわサマーアドベントカレンダー 2019 5日目の記事となります。
Unidux(redux)でUnityのお手軽状態管理
状態を管理するフレームワークであるReduxをUnityで扱うためのプラグインのUniduxを使ってみたい思います。
サンプル
最終サンプル
SDこはくちゃんズの誰かを状態監視しながら走らせます。
SDこはくちゃんズの要素として、人物{こはく, みさき, ゆうこ}、服装{夏服, 冬服}があり、彼女らを各方向に走らせたいので位置{東, 西, 南, 北}を持つとします。
なので、Stateの要素、StateElementとして{person, cloth, position}を考え、各々のActionを作っていきます。
筆者の知識レベル
筆者の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にする
こんな感じになると思います。
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フィールドを追加します。
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を確認することができます。
////
[Serializable]
public partial class State : StateBase
{
public PersonState personState = new PersonState(){person = PersonState.Person.Kohaku};
}
////
////
private static IReducer[] Reducers
{
get {
// Assign your reducers
return new IReducer[]
{
new PersonAction.Reducer(),
};
}
}
////
こんな感じで確認できます。
PersonStateが変わった時の処理の作成
では、PersonStateが変わった時に人が変わるような処理をMonoBehaviourで書きます。
StateはUnidux.Stateから読み込むことが出来ます。
また、UniduxはSubjectが使えるのでそのままUniRxのように書くことが出来ます。
以下のように書いて、Playerにアタッチして、各フィールドにゲームオブジェクト(kohaku, misaki, yuko)をアタッチします。
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を変えると人が変わるようになったと思います。
残りのClothStateとPositionStateも同じように作っていけばいけますので省略していきます。
ClothState
ClothState作成
using System;
using Unidux;
namespace App
{
[Serializable]
public class ClothState : StateElement
{
public Cloth cloth;
public enum Cloth
{
Summer,
Winter,
}
}
}
ClothAction作成
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にアタッチして各々の夏服冬服をセット。
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);
}
}
こんな感じで服装を変更出来るようになります。
PositonState
PositionState作成
using System;
using Unidux;
namespace App
{
[Serializable]
public class PositionState : StateElement
{
public Position position;
public enum Position
{
East,
West,
South,
North,
}
}
}
PositionAction作成
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フィールドを追加後、遷移の矢印のパラメータを適切な形に設定してください。
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]から動きを制御できると思います。
最終的なState.csとUnidux.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};
}
}
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追加
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;
}
}
}
}
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;
}
}
}
}
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作成
今回はひとところにまとめて書きました。
適当なオブジェクト(カメラ)にアタッチしてください
ビルドして動かせます。
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());
});
}
}