SkillAgentSearch skills...

Reduxity

React-redux via UniRx + Redux.NET for Unity3D

Install / Use

/learn @austinmao/Reduxity
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

reduxity

React-redux via UniRx + Redux.NET for Unity3D += Zenject

Table of Contents

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:

  1. Observe input from any source
  2. Dispatch input to redux store
  3. Reducers convert input to Vector3 velocity
  4. State is updated with new velocity (!)
  5. Renderer observe state.velocity changes
  6. Renderer moves character

Now, the CharacterController only knows that state.velocity has changed and responds accordingly. This has a few benefits:

  1. You can unit test the observer (step 1), reducers (step 3), and renderers (step 5) separately. Hooray for decoupling!
  2. 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.
  3. 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.

  1. Create MonoInstaller in Project > Create > Zenject > Mono Installer. This is where you will bind classes to the Direct Injection container via Installers
  2. 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.
  3. Create Zenject SceneContext in Hierarchy > Create > Zenject > SceneContext. This is the entryway into your scene.
  4. In the SceneContext, click the + under Installers and ScriptableObjectInstallers
  5. Drag the MonoInstaller to SceneContext > Installers
  6. Drag the ScriptableObjectInstaller to SceneContext > ScriptableObjectInstallers

Next, let's set up the State.

  1. Create a State script. This will house your nested state object and initialize a default state.
  2. Create an IInitializable Zenject class like CharacterState : IInitializable. Specify state properties.
  3. In void Initialize(), set up the default state for this object.
  4. Repeat this for each node of the state tree. Finally, set up the State class 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 1 and then drag the GameObject into Element 0. Then, inject and use the bound instance without needing to do GetComponent or public GameObject.
  • Make sure you are dragging the actual GameObject you want a reference to. If it is the CharacterController, do not drag in the Transform.
  • 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 ID in the ZenjectBindingScript. 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.

  1. 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; }
    }
  1. 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
            }
        };
    }
}
  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
View on GitHub
GitHub Stars50
CategoryDevelopment
Updated1y ago
Forks4

Languages

C#

Security Score

80/100

Audited on Apr 1, 2024

No findings