Game architecture is hard. We think our first game, Lost Toys, turned out pretty great but, in the rush to ship, the code got a bit wiffy. That’s something that we want to avoid with our new game, so we’re spending a few weeks prototyping our architecture to address some of the pain-points in Lost Toys.
The Problem: Communication & State
Some of the areas we wanted to improve were how we managed state in the game (data) and how different components communicated with each other. In Lost Toys, state was owned by their respective components. Communication was handled by calling functions between components. Do you want character A to give something to character B?
// Note that characterA needs to know about characterB! this.give(characterB, something);
In fact, this is even the recommended Unity-way of communicating between MonoBehaviours.
In an ideal world and if you're really really careful about it, you can end up with this:
But more likely, you end up with this: (we did...)
There are a number of issues with this, but the big one is it’s very difficult to debug. More so when you move beyond just a few simple components.
What we want instead is something that would give us predictable communication between components and specialized locations for our game's state.
The Solution: Flux (but for games)
This is Flux. Its an web app architecture from Facebook that’s been buzzing lately in web communities. We’re currently build a web app using Flux and after a few thousand lines of code and multiple developers, its awesome.
Flux's main proposition is its unidirectional data flow. Using the diagram above lets go over the main pieces, starting with “Store”:
- Stores hold your games state. Anything that a component would need to know about outside of themselves. When the store’s state changes it notifies any listening components.
- Components (or “React Views” in the diagram above) are only responsible for their local behavior and creating actions. Components have two kinds of state: local state and global state. Global state always comes from a store and represents anything another component might need to know about. Local state is anything I never need to share outside of this component. When stores change, we throw away our old state and get our new state from the stores. If components need to change that state, they don't do it directly, instead, they create an action and send it on to the dispatcher.
- The dispatcher handles all actions and is a special kind of event emitter that tells all stores about an action. (It also has support for ordering which stores receive actions first but that’s for a different post.)
- Stores can then decide if they care about a particular action and update their state. When stores change their internal state they trigger an event for relevant components. What's nice about this is that an action can update state in multiple different stores.
- And we're back to the components.
Lets go through an example. Let's say you have a knight with 100 HP. He can attack or be attacked.
- We start with an enemy component. EnemyComponent creates an action called "HitKnight" with a value of 15 HP.
- The dispatcher sends the HitKnight action to all of our stores.
- The HealthStore holds our knight's current HP. When this store receives a HitKnight action it decrements its HP counter by the value specified on the action.
- The KnightComponent listening to the HealthStore updates its local state to the value contained in the HealthStore.
- Now, our knight component knows its HP.
It’s a contrived example but now all communication is unidirectional and state is grouped into stores, outside of our components. The huge huge benefit of this is that everything else that wants to know about the knight’s health can listen on the HealthStore as well. So the UI can change from green to yellow to red as the HealthStore decreases and EnemyComponent can go into “VictoryDance” when the HealthStore reaches zero. Step back and think about that for a while. It means that everything in your game will always have an accurate state. No more weird zombie bugs where the knight thinks it’s dead but the enemy doesn’t agree. It just works.
Even better, we can drop in a single line of logging into our dispatcher and get:
ACTION:HIT_KNIGHT:15 ... ... ...
We can also test components by mocking data into the stores. Furthermore we can serialize and deserialize snapshots of our stores and restore our game state back to another point in time.
That's Flux for games and so far gives us everything we wanted. Separating our game state from our components and more consistent communication. The unidirectional data flow also has also greatly simplified debugging. We'll be playing with this over the next couple of weeks and hope to have some code to share soon.