In Jan of 2022 we spent some time looking at the Flux pattern, specifically in Blazor at the time. We got quickly shifted to another project and back in Unity land. We didn’t have an exact match for the flux pattern in Unity and thought it was worth continuing to explore its application, so we started Fluxity. I’m not going to go into detail on the pattern itself, there’s much better more detailed articles elsewhere for that. In less web-centric terms, the flux pattern is about managing read access to shared state (The Store, filled with Features), a single point of ingress for requests to change shared state (The Dispatcher with Actions), and as a matter of course, getting observability on state changes and requests (Effects). For a more thorough explanation check out these:
Flux is not commonly thought of as a pattern used in games. It comes from the UI world. So our efforts are in some sense experimental. We aren’t just using it as an elaborate way to push data into UGui elements. It’s been about a year and a half, we’ve used Fluxity in a half a dozen internal and client projects. So it’s time for some reflection and analysis. To prevent this post from being unbearably long, it’ll be multi-part. To ease in, we’re going to talk about how we’ve used Fluxity.
One of the first things we naturally slid into was actually not about state management at all, but using the dispatcher to send messages between disconnected parts of the game, akin to the Mediator pattern. Where previously we might have had specific interfaces injected, so other systems could be informed or instructed to take actions. This shifts the responsibility out to a third party. In the case that there was already an orchestration object dealing with it, that moves into the more general form of Effects in the Fluxity set up. In analogy terms, this is shifting away from direct messaging, a la a text message, or an event, a subscription to my newsletter, to the megaphone.
Player:TakeDamage(int amt)
{
health -= amt;
if (health <= 0)
{
Die();
}
}
//The very direct messaging mechanism
// How did I get all of these, DI? ctor, why do I need to know they exist?
Player:Die()
{
_spawnSystem.RespawnCloseTo(position);
_feedbackSystem.PlayDeathFeedback(transform);
_analytics.PlayerDied(Scene.name, position);
}
//The event mechanism, somehow those systems are bound to this object event directly.
// If this object actually goes away, we then have to rebind to all new instances.
Player:Die()
{
//under the hood we still have pointers to all those systems
// and now they need to either capture us in a closure
OnPlayerDied?.Invoke(this);
//or, they all have to match the same signature, or interface
}
//The dispatcher mechanism, this is a bit more indirect, it needs to know about 1 thing
// and to know what context it should bundle.
Player:Die()
{
//we only have reference to dispatcher nothing else
_dispatcher.Dispatch(new PlayerDied(transform));
}
Runtime Collection
I’m stealing the name from Unity ScriptableObject Architecture e.g. https://github.com/unity-atoms/unity-atoms . Creating a state that holds a list of all of a type of thing, like every collectable in the level, or every enemy. Anything that wants to know about changes kinda can, but primarily this ends up being a convenient way to split complex logic over multiple systems. The core state is now centralised and isolated, so the system that wants to check for conditions for creation, e.g. if there aren’t any enemies in a given quadrant for a given time, spawn some. It can iterate over the collection and dispatch a request to spawn or add more when it wants. It does not own the collection. At the same time, the lava system can tick over all of the enemies and any that are below the lava, can dispatch requests to destroy them. It’s a nice clear distinction, no one system owns it, and the collection always exists.
Constants, Settings, Database
By storing gameplay data or constants in stores, any part of the game that wants them can easily observe the feature from code. Having them all end up in the same place, makes it straightforward to pull data from various sources, SOs, json, local dbs and getting that into the ready for use form. This also allows us to easily mix in any need for some data to change based on user settings. If the player changes the difficulty mid game, we dispatch and can update the central source of truth for all enemy damage output for example.
Actual Flux stuff
The pattern is intended to manage coherent shared state across many readers, so UI can be updated. So we certainly do put things like the player lives in a Feature and when it changes, update the UI text field. Where this gets interesting is when you expand what a view actually is. It’s just as straightforward to update a text field, as it is to change a material or spawn a particle effect. Similarly, we certainly do dispatch requests to get data from the web, and via an Effect dispatch the result back, but we also use effects to bridge between elements that operate within Fluxity to systems and objects that are entirely unaware that Fluxity exists. By having an effect pass the message along to a system or object we get to merge the two worlds fairly easily.
Usage
A common occurrence is for an object or system to own its own data, not interacting with Fluxity at all necessarily. If we encounter a need to respond to changes or query the values in that other object’s data from the outside world, we pull that data out into a Feature. The original owner now dispatches to modify it. This is a little cumbersome at first, but is a relatively small impact on the original class, and now that other concern has multiple ways of observing, or polling, or running side effects based on the data or the dispatches concerning it.
Upcoming
In future parts we’ll get into: