I started an experiment to try and create the silver bullet of design patterns when writing game code. I’ve done roughly four iterations of it already (mainly using it for academic assignments) but I wouldn’t call it a silver bullet just yet. Maybe just a bullet for now. I am, however, sharing this to try to create some traction for people that share my motivations.
I also want to try to create something more strictly established than ECS. To achieve that, I’ve aimed to tackle three problems that I’ve struggled with when dealing with gamedev code in general: avoiding singleton/reference hell, focusing on easy/fast engine adoption that supports designers, and improving efficiency by allowing teams to reuse previously developed systems.
I want to start off by defining the five rules required for this experiment to succeed:
- Components are just data.They are also the only things scripts or editors have to access and alter gameplay at runtime.
- Systems process data. Systems are only concerned with component data and other connected systems.
- No crashes occur during runtime if a component/system is added/removed/updated.
- Systems can function independently and within a sandboxed/multithreaded environment.
- Game objects (or entities) only serve to group components together.
Game Event Buffer
The first aspect is the simplest and is essentially the easiest part to understand: events. Everything that can happen in our game will be defined as an event and will be considered the moving parts that make a game simulate.
Defining these events are also a powerful first step when you start planning your game. You could even go as far as defining every single one of them in your GDD. How to define these events are up to you but you’ll probably only get the hang of it after understanding systems and components.
Apart from processing component data, systems are also where we will define our game events. Let’s create a system and call it CharacterSystem. To enforce rule #3, we have to make sure that if we rip out this system, changing the state of the character in our game should not be possible anymore. That is why we also define our character related events within systems. We should, however, be careful. Doing something like removing a system mid game loop when there could be dependencies will most likely crash the game, violating rule #3. That is why events are essentially just integers whose values only exist inside of buffers. Which brings us to components.
You are probably used to components from patterns like ECS or just from game engines. However, these components are a bit different. They only do up to two things:
- Expose properties to designers or scripts.
- Store buffers of events coming from or used by systems.
When you reach the example, point #2 above is actually done for you by the EZS plugin, but it is important to understand why these buffers exist.
It is also important to remember rule #1. These components cannot contain any logic. They are meant to be stupid and that stupidity helps enforcement of rules #2, #3, and #4.
To conclude your understanding of the experiment, we first need to paint a picture. By picture, I mean let's use the things we know so far and create a little scenario. Then we will visualize how an event traverses through our buffers within a single game loop.
The life of a game event
For our systems we’ll create two:
And each system will have its component:
Of course our event should also exist somewhere. I’m going to keep it simple and define just one event and call it ToggleWeapon. We add it in the CharacterSystem because it relates to the character’s weapon and the event shouldn’t exist anymore if that system is remove/disabled:
Remember our components all have what we call a buffer of events. They get used by systems (if they are registered) to react on what the event is meant to perform. In our case, the KeyboardComponent will also be storing bindings triggered by KeyboardSystem which will add an event to its buffer. When the key is pressed we still only have the ToggleWeapon event sitting in the KeyboardComponent event buffer:
We need to get the event into the CharacterComponent, so the CharacterSystem that owns the code to toggle the weapon can process its buffer. To achieve this we create another system, an intermediate between the two, which is the final aspect to the experiment:
If either of the systems are removed/disabled, the same has to happen to the intermediate system. There is potential for good Venn diagrams in our GDD here but more on that later.
Now we can invoke an event from the KeyboardSystem that will eventually end up in the CharacterSystem and do exactly what we need it to do. I’ve added numbered arrows to show the flow of the ToggleWeapon event’s life, which ends once it is processed by the CharacterSystem:
I’ve put the scenario above into a .unitypackage that you can try for yourself. It comes with a library I developed called EZS and it takes care of all of the low level stuff. It also (hopefully) allows for easy adoption so designers can focus on just creating events, systems, and components. I haven’t gone through the effort of documenting it either, so some of the stuff might seem a bit foreign. I am, however, hoping that the simplicity of the API will help it teach itself to you by example and based on the content of this article.
Download link for the latest build and examples: https://github.com/fanus/ezs-unity/releases
I’ve found that encapsulation is pretty liberating when it comes to designing code, but I believe it has even greater potential when developing games. Systems often need to be reworked due some obscure bug or a mechanic that profoundly supports gameplay. Planning can also be hard to solidify upfront because you can’t always predict what will happen until something is actually played. By keeping our systems and naively engineered mechanics in a state of flux, we should be able to conveniently iterate over systems and eventually develop more player centric games.
I will be developing a mini game using this approach. The goal is to continuously improve, solve issues, and consider feedback as I go along. So expect more examples and diagrams.