Unidux is practical application architecture for Unity3D UI.
It's inspired by Redux.
Install
Import unitypackage from latest releases.
Usage
- Create your Unidux singleton and place it to unity scene.
using UniRx;
using Unidux;
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 State InitialState
{
get
{
return Instance.InitialStateJson != null
? JsonUtility.FromJson<State>(Instance.InitialStateJson.text)
: new State();
}
}
public static Store<State> Store
{
get { return Instance._store = Instance._store ?? new Store<State>(InitialState, new Count.Reducer()); }
}
public static object Dispatch<TAction>(TAction action)
{
return Store.Dispatch(action);
}
void Update()
{
Store.Update();
}
}
Note: ReplaySubject
is a ReactiveX concept
provided by UniRx in this example.
- Create state class to store application state.
using System;
[Serializable]
public class State : StateBase
{
public int Count = 0;
}
- Define action to change state. Define Reducer to move state.
public static class Count
{
// specify the possible types of actions
public enum ActionType
{
Increment,
Decrement
}
// actions must have a type and may include a payload
public class Action
{
public ActionType ActionType;
}
// ActionCreators creates actions and deliver payloads
// in redux, you do not dispatch from the ActionCreator to allow for easy testability
public static class ActionCreator
{
public static Action Create(ActionType type)
{
return new Action() {ActionType = type};
}
public static Action Increment()
{
return new Action() {ActionType = ActionType.Increment};
}
public static Action Decrement()
{
return new Action() {ActionType = ActionType.Decrement};
}
}
// reducers handle state changes
public class Reducer : ReducerBase<State, Action>
{
public override State Reduce(State state, Action action)
{
switch (action.ActionType)
{
case ActionType.Increment:
state.Count++;
break;
case ActionType.Decrement:
state.Count--;
break;
}
return state;
}
}
}
- Create Renderer to display state and attach it to Text GameObject.
[RequireComponent(typeof(Text))]
public class CountRenderer : MonoBehaviour
{
void OnEnable()
{
var text = this.GetComponent<Text>();
Unidux.Subject
.TakeUntilDisable(this)
.StartWith(Unidux.State)
.Subscribe(state => text.text = state.Count.ToString())
.AddTo(this)
;
}
}
- Create dispatcher to update count and attach it to GameObject.
[RequireComponent(typeof(Button))]
public class CountDispatcher : MonoBehaviour
{
public Count.ActionType ActionType = Count.ActionType.Increment;
void Start()
{
this.GetComponent<Button>()
.OnClickAsObservable()
.Subscribe(state => Unidux.Store.Dispatch(ActionType))
.AddTo(this)
;
}
}
That's it!
Example
Dependencies
API
StateBase
public class State : StateBase
{
public int Count;
}
<StateBase>.Clone()
State _state = new State();
State _clonedState = _state.Clone();
Create a deep clone of the current state. Useful for Immutability.
Store
IReducer[] reducers = new IReducer[]{};
Store _store = new Store<State>(State, reducers);
// State must extend StateBase
<Store>.State
Get the state as passed to the constructor.
<Store>.Dispatch(object)
Dispatch an event of TAction
object,
which will trigger a Reducer<TAction>
.
<Store>.ApplyMiddlewares(params Middleware[] middlewares)
Apply middlewares to Store object which implement delegate function of Middleware.
<Store>.Update()
When at least one reducer has been executed, trigger all the renderers with a copy of the current state.
<Store>.ForceUpdate()
Trigger all registered renderers with a copy of the current state regardless of any reducers having been executed.
SingletonMonoBehaviour
public class Foo : SingletonMonoBehaviour<Foo> {}
A singleton base class to extend.
Extends MonoBehaviour
.
<SingletonMonoBehaviour>.Instance
public class Foo : SingletonMonoBehaviour<Foo> {}
Foo.Instance
The instance of the base class.
Performance
Clone()
Default implemention of StateBase.Clone()
is not fast, because it uses BinaryFormatter
& MemoryStream
.
And Unidux creates new State on every State chaning (it affects a few milliseconds).
So in case of requiring performance, override clone method with your own logic.
e.g.
[Serializable]
class State : StateBase
{
public override object Clone()
{
// implement your custom deep clone code
}
}
Equals()
Default implemention of StateBase.Equals()
and StateElement.Equals()
is not fast, because it uses fields and properties reflection.
In case of edit state on UniduxPanel's StateEditor, it calls Equals()
in order to set IsStateChanged
flags automatically.
So in case of requiring performance, override Equals()
method with your own logic.
e.g.
[Serializable]
class State : StateBase
{
public override bool Equals(object obj)
{
// implement your custom equality check code
}
}
Thanks
- @austinmao for suggestion of Ducks and UniRx.
- @pine for description improvement.
- @jesstelford for fix document.