Reduxity
React-redux via UniRx + Redux.NET for Unity3D
Install / Use
/learn @austinmao/ReduxityREADME
reduxity
React-redux via UniRx + Redux.NET for Unity3D += Zenject
Table of Contents
- Motivations
- Installation
- How to Use with Zenject
- How to Use without Zenject
- Examples
- Middleware
- Plans
- FAQ
- Resources
- Acknowledgements
Motivations
React-redux is a modern design pattern employed on websites and mobile apps, including Facebook.com. This project seeks to port its principles and practices to Unity3D. To understand how Reactive Programming and Redux can be incorporated into your project, follow these steps:
Why React
First, read this well written blog post on reactive programming in Unity3D using UniRx. This takes a classic character movement example and decouples reading keyboard/mouse inputs -> converting to velocity -> applying movement to a character.
Now, you should be convinced that reactive programming will lead to more efficient and maintanable code when compared with classical, imperative coding.
Why Redux
Note that there is still coupling between inputs and movement. What if you wanted to move the character regardless of input (e.g., on a timer or because of an event trigger)? You would have to create a reactive situation for both observing the event and handling the movement for each variation.
Enter Redux. Instead of components observing events and then rendering changes, they observe said events and then mutate a global state object (via actions and pure functions called "reducers"). Other components who's sole purpose is to render changes will then observe changes to the state and then render.
In the aforementioned example, CharacterController.Move would only care that a Vector3 state change occurreded using Redux. It would not care where that change came from.This can be done by:
- Observe input from any source
- Dispatch input to redux store
- Reducers convert input to Vector3 velocity
- State is updated with new velocity (!)
- Renderer observe state.velocity changes
- Renderer moves character
Now, the CharacterController only knows that state.velocity has changed and responds accordingly. This has a few benefits:
- You can unit test the observer (step 1), reducers (step 3), and renderers (step 5) separately. Hooray for decoupling!
- You can use any input to dispatch to the redux store. This includes third party assets that are not written reactively. All you need to do is dispatch to the store.
- You can debug how additions to the state affected your app one state change at a time.
For a standard CounterButton example, take a look at the Example below. Example _Scenes also includes the aforementioned CharacterMover and CounterButton examples.
Installation
Download or clone this repo. While Reduxity has dependencies on UniRx and redux.NET, these are included in this repo. You can also use Reduxity with Zenject for Direct Injection.
Using Zenject is highly recommended and encouraged. A full tutorial on how to use Zenject is forthcoming since it is a bit heady at first.
How to Use with Zenject
We recommend you use Reduxity with Zenject to encourage more modular, easier-to-test code. Once you get the hang of it, it will lead to less code that is easier to reason and less dependent on MonoBehaviours.
Example for this is in Reduxity.ZenjectPlayerMovementExamply.unity
First, let us set up Zenject. This is boilerplate for every Zenject project.
- Create MonoInstaller in Project > Create > Zenject > Mono Installer. This is where you will bind classes to the Direct Injection container via Installers
- Create ScriptableObjectInstaller in Project > Create > Zenject > Scriptable Object Installer. This is where you will have Settings that can be changed during runtime and persist between sessions.
- Create Zenject
SceneContextin Hierarchy > Create > Zenject > SceneContext. This is the entryway into your scene. - In the SceneContext, click the
+under Installers and ScriptableObjectInstallers - Drag the MonoInstaller to SceneContext > Installers
- Drag the ScriptableObjectInstaller to SceneContext > ScriptableObjectInstallers
Next, let's set up the State.
- Create a
Statescript. This will house your nested state object and initialize a default state. - Create an
IInitializableZenject class likeCharacterState : IInitializable. Specify state properties. - In
void Initialize(), set up the default state for this object. - Repeat this for each node of the state tree. Finally, set up the
Stateclass to include each of the nodes.
Next, set up Actions and Reducers. Actions take input parameters and are dispatched to the Redux store. They feed those parameters as a payload to reducers that will modify a new state object.
Next, set up the App, which will initialize the store with a default state and reducers. Here, you will inject each reducer and the state. Note that the App contains a public Store, which will be your method to dispatch Actions.
Finally, create Components that listen to changes in the State.
A Few Gotchas
Zenject can take some time to decipher. Here are a few gotchas:
- Need a reference to a GameObject? On the GameObject, go to Add Component > ZenjectBindingScript. Then, change the Component > Size to
1and then drag the GameObject intoElement 0. Then, inject and use the bound instance without needing to doGetComponentorpublic GameObject. - Make sure you are dragging the actual GameObject you want a reference to. If it is the
CharacterController, do not drag in theTransform. - You can also bind a GameObject directly to a script by dragging the script into the
Element 0. - If you have multiple injected GameObjects of the same type, you should give an
IDin theZenjectBindingScript. Then, you can reference this injected GameObject by ID via an Identifier.
How to Use without Zenject
Note: Direct injection via Zenject is the preferred method for using this library.
- Set up
State.cs. Don't forget you need to create a function to initialize state with default values.
public class State {
public CounterState Counter { get; set; }
/* default state at app start-up */
public State initialize() {
return new State {
Counter = new CounterState {
count = 0
}
};
}
}
public class CounterState {
public int count { get; set; }
}
- Set up your Actions and Reducers. Note that Reduxity follows the Ducks Module Proposal for bundling.
public class Action {
// actions must have a type and may include a payload. The payload will
// need to be specified in properties of the Action. See the CharacterMover
// module for an example of this.
public class Increment: IAction {}
public class Decrement: IAction {}
}
// reducers handle state changes by taking the previousState, applying an action,
// and then returning a new State without mutating the previousState. In theory,
// this can create a record of states that DevTools can reveal for easy debugging.
public static class Reducer {
public static State Reduce(State previousState, IAction action) {
// Debug.Log($"reducing with action: {action}");
if (action is Action.Increment) {
// always pass previousState and action cast as the action type
// this will lead to more easily replicable reducers
return Increment(previousState, (Action.Increment)action);
}
if (action is Action.Decrement) {
return Decrement(previousState, (Action.Decrement)action);
}
return previousState;
}
// include previousState and action in the constructor to make it faster and
// easier to replicate reducers later
public static State Increment(State previousState, Action.Increment action) {
// always return a new State in order to note mutate the previousState
return new State {
Counter = new CounterState {
count = previousState.Counter.count + 1
}
};
}
public static State Decrement(State previousState, Action.Decrement action) {
return new State {
Counter = new CounterState {
count = previousState.Counter.count - 1
}
};
}
}
- Set up the Store:
public class App : MonoBehaviour {
public static IStore<State> Store { get ; private set; }
private void Awake () {
// initialize store with default values
State initialState = new State {}.Initialize();
// genera
