Unity Game Development

Tool Spotlight - Fluxity Part 2

by Steve Halliwell · June 13, 2024

In the Part 1, we looked at how we’re using Flux in Unity for games. In part 2 we’re going to talk over some of the pros and cons. We’re going to continue to talk in generalities, as some of the projects we’ve used Fluxity on are still NDA’ed or at the very least, not widely public knowledge. Let’s kick off on a positive note.

Pro - State immutability

State immutability is kinda the point of using the pattern, so it may seem odd to declare it a pro. But you commonly have data that is shared, or in high contention, or owned by a system but manipulated by others. Moving that into a Feature by pattern, simplifies or removes questions around caching, stale data, off by 1 frame type issues. It also removes the category of issues that arise from “something changed this data in a way I did not expect”. The data cannot be changed without request, and if you need to know about that change you have mechanisms to be informed of the change. Similarly, uncertainties like “is it safe for me to reference this data” are removed, the answer is yes.

On a similar note, all states always exist, the way we set up and bind elements of Fluxity together means Features have their starting values before, services are injected, and before Unity Start is going to run. You can obviously have that initial state be empty or some flag stating not yet loaded, but it cannot be null (as it’s a value type). This has the delightful knock on that we can make presenters execute their callback when they subscribe to present the starting state, and they don’t need any special handling internally for the first presentation.

Pro - Single location of change

This is very interlinked with the previous Pro, and again is a core part of the pattern. But there is only 1 thing that can change the values in a feature, its reducers. This has a nice DRY outcome too. Outside of Flux, as objects grow, what might have been a simple field mutation, gets entangled with other parts of the object or system, it’s why we tend towards properties / mutators. This ends up with objects being defensive, their internal logic is structured to ensure that external usages of it, do not break it or put it into unexpected states. Another way to see this defensiveness is as a leaky abstraction or an additional responsibility, the object is now aware of the ways it is being misused and accounts for them. With Feature and its Reducers, we’ve found it more natural that those concerns do not want to bleed through. Since it’s simply not possible to micro-orchestrate a sequence of interrelated changes to the Feature, you make a new Action to dispatch, that handles them all at once. The other aspect of this is enforcing distance between the cause of change and the resulting effect. It’s easy to imagine things like the player accidentally growing to include the score object and enemy spawner calling or notifying them directly upon player death. Even if that’s a blind call, Action or void func(void), that still includes a file or adds pointers/references to each other. Not the end of the world but probably undesirable. Any kind of messaging system would aid in this problem, the interesting bit in Flux is that the resulting Effects are very built in to the pattern, they are very composable, and immediately isolated/orthogonal to each other. The other nice bit of implementation detail is that the way we tend to implement Effects, the class/file the Effect is defined in determines what causes it to be notified.

//the effect binding, in the FluxityInitializer elsewhere.
.Effect(new PlayerStatisticsEffects(), PlayerStatisticsEffects.RegisterAll)

//the effect class itself
public class PlayerStatisticsEffects
{
    private readonly FeatureObserver<PlayerStatisticsState> _playerStats = new();

    internal static void RegisterAll(FluxityRegisterEffectContext<PlayerStatisticsEffects> context)
    {
        context
            .Method<InitialiseGameCommand>(x => x.OnInitialiseGame)
            .Method<ExitGameCommand>(x => x.OnExitGameSavePlayerStats)
            ;
    }

    private void OnInitialiseGame(InitialiseGameCommand _, IDispatcher dispatcher)
    {
        var loader = new LocalDBPlayerStatisticsStoreSerialiser();
        var data = loader.Deserialise();
        dispatcher.Dispatch(new UpdatePlayerStatisticsCommand(data));
    }

    private void OnExitGameSavePlayerStats(ExitGameCommand _1, IDispatcher _2)
    {
        var saver = new LocalDBPlayerStatisticsStoreSerialiser();
        saver.Serialise(_playerStats.State.PlayerStatistics);
    }
}

Pro - Observable

Our current implementation could be more robust on this front but making each Feature be an observable is very convenient. If you only need to respond when something changes you don’t need to reach for a poll that checks a delta, it’s already observable. We found that the mechanism for using it, a Presenter, changed a lot over time. Originally it was a specific derived monobehaviour descendant, with a bunch of code reuse jammed in a middle layer, easy but not simple. We ran into multiple use cases where we didn’t want or need the presenter to be a monobehaviour, it wanted to be a simple c# object, or we wanted to compose the presenting functionality into an existing class. This structure also meant that Fluxity was forcing one specific way of using Unity on you for displaying data change. This is now reworked into a FeatureObserver and Fluxity itself takes no stance on how you use Unity to display that.

Pros - Tooling/Editor Windows

Very quickly we added editor tooling to support the use, inspection, and debug-ability of Fluxity, you can see an overview of them on the github page. Being able to see the current values in all Features is just the absolute minimum of usability here, being in Unity you’d expect to be able to see the current value of the objects in the editor. The Dispatch History window is also extremely useful in tracking down why a bug might be happening, giving you the chronology and payload of everything that has gone through the dispatcher in the current session. This coupled with the runtime bindings window has also been valuable in gaining understanding of a set up that you didn’t write yourself.

Pros - Gradual adoption

Other than requiring our existing DI tool Flume, Fluxity happily sits alongside whatever the structure of your project is. You set up its bindings and then it kinda just plays by Unity’s rules as you would expect. Because Fluxity is a tool not a framework, it has allowed us to put it in a project and gradually; using it on for some parts of the system, without having to throw existing structures like manager singletons, services, or interconnected monobehaviour and Unity messaging logic. Want to get information into the Fluxity structure, make a DispatcherHandle and dispatch something, want to get information out of it, make a FeatureObserver and ask for state or be notified. Similarly bridging from Fluxity dispatch to other systems is very aligned with Effects. We’ve also seen this be useful in freeform exploration around “what and how do I want this part of the game or system to work”, getting the behaviour or structure sound first and then worrying about if state needs to be shared or other parts of the system by pulling parts of that up into Fluxity structures.

Pros - Simple

A lot of the actual bits of Fluxity that you write are very simple code. We’ve touched on this previously but for completeness:

States are just a handful of fields with a static Create

public struct AudioSettingsState
{
    public float masterVolume;
    public float musicVolume;
    public float sfxVolume;

    public static AudioSettingsState Create(float master, float music, float sfx)
    {
        return new AudioSettingsState
        {
            masterVolume = master,
            musicVolume = music,
            sfxVolume = sfx,
        };
    }
}

Reducers are often a handful of lines, most commonly just assignments and a return.

private static CountDownState OnCancelCountDown(
    CountDownState state, 
    CancelCountDownCommand command)
{
    state.GameTimer = Maybe.None<IGameTimer>();
    return state;
}

private static CountDownState OnSetCountDown(
    CountDownState state, 
    SetCountDownCommand command)
{
    state.GameTimer = new Maybe<IGameTimer>(command.GameTimer);
    return state;
}

Effects are often some simple guard logic and then calls to a service or a dispatch.

public void OnPrepareMiniGame(PrepareMiniGameCommand command, IDispatcher _2)
{
    _sfxMusicSystem.TransitionToMusicTrack(_miniGameState.State.Details.MusicTrack);
}

public void OnBeginSession(BeginSessionCommand _1, IDispatcher _2)
{
    _sfxMusicSystem.TransitionToMusicTrack(MusicTrack.Main);
}

Bindings are one line and functional in style.

public override void RegisterFluxity(FluxityRegisterContext context)
{
    var machineSpecificSettings = MachineSpecificSettingsPersist.Load();
    var videoSettings = VideoSettingsState.Create(machineSpecificSettings.videoResolutionScale);
    //...

    context
        .Feature(AimSettingsState.Create(), AimSettingsStateReducers.RegisterAll)
        .Feature(CountDownState.Create(), CountDownStateReducers.RegisterAll)
        .Feature(LeaderboardState.Create(), LeaderboardStateReducers.RegisterAll)
        .Feature(videoSettings, VideoSettingsStateReducers.RegisterAll)
        //...

        .Effect(new GunFeedbackEffects(), GunFeedbackEffects.RegisterAll)
        .Effect(new ImpactUserHitResultEffect(), ImpactUserHitResultEffect.RegisterAll)
        //...
    ;
}

Why is that good? Personal preference aside (we’ll get to it shortly), the lines that make up this logic very rarely need documentation or more than a glance over to understand. In our experience they very rarely have bugs as they don’t tend to be in the kind of shape or logic where bugs tend to creep in. Moreover, Kernighan’s law, this lines up nicely with the idea that debugging is harder than writing it in the first place. So these bits being very simple, coupled with our tooling makes it relatively quick to rule these in or out as the cause of the bug and frees up your debugging smarts to focus elsewhere.

Pros - Loose Coupling

Again mostly by pattern here, but if two objects or systems don’t want to know about each other, they really don’t. Multiple objects or systems can interact without any explicit knowledge of the other, as they only use (include) the commands they will to dispatch, and the struct(s) for the states they wish to reference/observe. This might otherwise be hidden via interfaces, meaning you don’t know the exact type (in theory), but in the Flux pattern you don’t even know it exists. The shared language between objects can become these simple Actions (Commands) and Features (structs), rather than referencing each other directly, but in an abstracted way.

Pros - Different bugs

Relating to our previous pro, and certainly a strange statement. We see an entirely different set of bugs we get when using Fluxity. It would be an overreach to state that total number of bugs is down. But because of the change in structure, and simple nature of parts of Fluxity usage, bugs are less often in the guts of an interaction and more about the order of dispatches, or forgetting to wire something up, or having state drift apart. In some rare cases these solutions become very straightforward to implement, a reducer or effect needing to happen after another can be resolved by effect dispatch re-ordering, or changing the specific Action, that the reducer or effect is triggered by.

Pros - Personal preference

This section is very much, “a pro in my opinion and to my taste”. For some, these may well be cons for the exact same reasons.

Smaller Pieces

Fluxity having lots of little simple bits encourages the programmer to split up their logic along lines of requests, commands, results, knock-ons, effects, and beyond, splitting into presenters, views, and game systems. This means there are lots of files but they are often rather short, once you find the file or files that deal with the part of the game or logic that you are working with, they are quite short.

Split Data and Logic

Flux wants to be a more functional programming style, so the data is defined, held, and accessed entirely separately from the mechanism(s) that mutate it and it doesn’t know what it is. More than that, data is very far removed from the reason why you might want it to change. There are not often objects that have intricate internal state and methods at the heart of the Fluxity application. At a simpler level, our data only cares about what the state or feature it represents needs, not what might make it convenient for methods to also have at hand.

Cons - What did the dispatch do?

Dispatched Actions are blind, you don’t know what they did, you don’t even know if they happened immediately as we added a queuing mechanism to allow chained dispatches to resolve rather than be an error. Flux pattern would argue that you shouldn’t need to know what happens, dispatching should be the end of your responsibility. In practice, especially when using gradual adoption or when first trying out Fluxity, you want to know what just changed in the middle of some non-trivial method you’re working with. This is somewhat frustrating as it’s a shift in responsibility forced on you by the pattern. One that becomes less obvious or handwaved when the matching Reducers and Effects might have conditional logic in them.

Cons - Actions as Commands

Flux pattern calls the things you dispatch Actions, we weren’t doing that. Action is already a base type in C#, so we settled on calling them Commands, which is only marginally better and perhaps misleading. You are not commanding anything, and it’s not following the Command pattern. Action gives a better indication that something happened, and the system needs to churn through it, Command, implies you already know what must happen and are directing that upon the system. This nudges the programmer towards thinking, “OK, I need to know what I want to occur at the site where I dispatch, and ensure I’m bundling the correct information with the command”. That can certainly be the case, but it doesn’t nudge you towards, “an agent would has just requested this thing, good luck. throws a package over the fence.”, which should be a valid line of thinking.

Cons - Terms

There’s a lot of proper nouns at play, Store, Feature, State, Action, Dispatcher, Effect, Reducer, Initialiser, Binding, Observer, Presenter, and View just to talk about what you did or are trying to do with Flux(ity). Getting up to speed on all this terminology is not an overnight task and as soon as you’re in the middle of it they fly fast and often. “This is kinda unavoidable for any communication pattern, but this does feel like it’s more new words to take on than throwing MVVM and a singleton, or pub/sub broker at someone. Add on top of that this is (kinda) a functional programming pattern, so add in Pure, Closure, Value, Immutable. We’re not really Monadic so we dodged that bullet but just barely. Don’t worry though we muddied that up too by using a Maybe type in a number of our projects.

Cons - How do I…

You know what you want to do, and if it was just a big blob of Unity monobehaviours or a GameManager, you know how you’d implement it. But there’s this Fluxity thing in the project and it seems like this should map to it, but how? What do I have to dispatch, and where? Should this be a new State or is it just more fields in an existing one? Do I need a reducer… Oh I don’t have that information available. How do I get it? Is this an Effect or an Observer? This problem is intertwined with the terms issue, but the bigger con here is you can force it to work in a lot of different ways. Some of those feel more ill fitting than others, but without the gut feel or vibe check it’s pretty easy to get it all working in an ill fitting way and at the very end have another team member say, they would have put that in a State and Presented it instead of passing the information out via an Effect.

Cons - Boilerplate, glue, and so many files

There’s a lot of little bits to write to add a new thing from scratch. A new Feature, with new Actions and Reducers, and the binding that into your FluxityInitialiser. We’ve seen snippets of these earlier. None of that is overly complicated but it is a bunch of lines of code to create that can feel like a big hurdle to get started. I’ve not found that too onerous or tedious yet but we can definitely make this nicer with either code snippets in the IDE or code gen to auto create some of this binding. Right now you at least need to change binding and a new file with the State, the Actions, the Reducers in it. If you need Effects, that’s another file, probably per responsibility or domain. Then you need to dispatch from somewhere, that’s in some other file. To see all the parts that cause change or take part in a dispatch flow, you might want to see 5 files at once. That probably doesn’t fit on a monitor and if you aren’t used to working across a lot of different files, just plain disorienting. Due to so many small bits and files, we also end up with lots of small functions and classes, and end up approaching Java levels of long and specific class names. Some favourite examples include, UserAttemptTriggerPullOutOfAmmoCommand, DecreaseControllerRumbleIntensityCommand, and HitReceiverReloadSceneDispatcher.

Cons - Unity, Functional, IL2CPP

Not a Fluxity specific issue but, due to when this project started and our need to support IL2CPP, there’s some games we have to play and some structure we don’t get to use. The first being the need for an ICommand interface, and our Commands (Flux Actions) to be classes not structs. So that we can pipe them all safely through a Dispatch<T> where T : ICommand, new(). In regular C#, you can have T in your generics be a struct, the JIT will just deal with it and each T you use ends up being an overload. IL2Cpp does not do that, it needs a base type object so it can void pointer in cpp. We don’t want these to be classes, we’d like them to be value types, but we have to make them reference type objects so we can build for Android. The second being the with keyword. Most reducers would like to be

private static AudioSettingsState ChangeMasterVolume(
    AudioSettingsState state, 
    ChangeMasterVolumeCommand command)
    => state with { masterVolume = command.volume};

But cannot/couldn’t be in the c# version in Unity at the time of starting the project. We then also run into requirements around using record instead of struct for our State type in the Feature, which (at least for a time) meant they would end up internally as init only classes. We didn’t want to have to think about an allocation cost per reducer run. Not being able to reach for an init only record struct, also means our State types are structs with a static Create method by convention rather than enforced by the language.

Cons - It’s not a UI Framework

A trivial presenter, that is one that only cares about one Feature, is delightfully straightforward in most cases. However things that might be called Controllers in MVC, that orchestrate their content based on multiple Features are only slightly more straightforward than they used to be. Our current solution for them is to use a FeatureObserverAggregate, to update a Display method when any of them change. But Fluxity has no opinion on it and isn’t really here to help with that problem. Along the same lines we end up with uncertainty on where the dispatching should occur, should the presenter know what needs to be dispatched and buttons be bound to one of its methods, or should the view know about dispatching. The core of this con is sort of wanting to have our cake and eat it too. We’d like Fluxity to not demand we structure our project in a particular way, but we would like it to be very clear how we should use it in the structure we do have. We can generously call this evolving best practice, and say we’ll talk about the conventions we’ve (currently) settled on for dealing with this in another part.

Upcoming

There’s still more to discuss, Part 1 will continue to be updated with links to all parts.