Designing a Protocol-Oriented, Event-Driven Entity-Component System*
I have decided to pick up DASHockey again. When I last touched it (almost two years ago), the codebase was complete garbage. I tackled the game as a foray into developing for the then-new AppleTV and to be able to present it at the Green Mountains Game Festival. As such, getting a complete game with AI out in a week was a pretty large task. Since I’m tackling it anew, I figured I should be a good programmer and rewrite the game from the ground up. I eventually want DASHockey to have iPhone-connected remotes for true party game multiplayer, but for now let’s focus on the base architecture.
This post assumes the reader has a decent understanding of Swift. Thankfully, it’s a very verbose language, so it shouldn’t be too hard to get the high-level architecture from it. See Apple’s Swift handbook for a great resource on Swift.
The Entity
In this protocol-oriented, event-driven example, the base entity isn’t too different from the classic pattern.:
Here, we have a simple array of Component
objects (which we’ll get to), a name and tag to reference this entity by, and methods for getting/setting these values. But what about finding components in our bucket?
Here, we use generics to be able to return any type of component (This is very similar to how Unity handles its ECS). In the singular search, we use the power of optionals to elegantly return nil if there’s no such component attached to this entity. For the multi-search, we return an empty array. In the sample use, notice that we use ComponentType.self
. The .self
is necessary to reference the class type, and not just the member type. It’s a small syntactical hiccup, but I think it is worth the power and flexibility it provides. If you’re savvy, you’ll notice this is in an extension of Entity
. I did this simply for organization purposes: we don’t declare any new member variables, so we can place these function in an extension. I name my extensions in comments for easy finding; this one is the ‘search extension’.
For adding and updating components, we use a similar extension:
Adding is simple enough, but what exactly is happening with update
? Swift 2 added the where
clause – basically a filter on an array. Thus, in update
, we are iterating over all of our components, and if the current one is an Updateable
component, we call its update
method. It’ll make more sense in a little bit.
That, at its core, is the entity in this system. It’s not very complicated, but can be easily expanded, which is always a great goal to have.
The Component
At its root, the Component
class has similar initializers, name and tag functions as the Entity
class. The interesting functionality comes from the use of protocols:
This is designating that any component that conforms to the Updateable
component will have to contain a definition for the function update
that takes a double as a parameter (and similarly for the Renderable
protocol). What this does, at least in my setup, is that it takes items like the players, puck, and nets, and turns them from entities that would have to be handled by the scene to components that are held by a single entity. Basically, it’ just a reduction of one more layer, and it can be argued that it’s just renaming the architecture, shifting it up a level, but more importantly, it allows the puck object to contain all of its own logic, broken up into extensions and protocol implementations. Yes, in games with hundreds of thousands of objects this would not be ideal, but in DASHockey where there are never more than ten objects in a scene, it works very well. For instance:
Here’s the puck’s implementation of the Renderable
protocol. Granted, it is extremely simple, but it allows for complete customization on a puck-level basis. In a sense, its similar to prefabs in Unity.
Another high-level advantage of this system is that it allows for event-like components, like a GoalScored
component:
This, in turn, allows the code called for when a goal is scored be this:
This code is incredibly concise, verbose, and clean. It pushes all of the logic of the objects into the objects themselves. It’s not without its flaws: it won’t work well for larger systems, it is hard to customize objects on a per-instance basis, and objects are authoritative over their own implementation. For DASHockey though, this is a great system that allows the game to run in a concise matter.