My Windows Phone game Entangled was the first game I shipped that used an Entity/Component/System framework. I used a simplified version of the framework I developed for my much more ambitious still-in-progress game, and it worked fairly well (See previous posts on this topic here and here).
Now that I’m re-skinning and updating the game for a port to iOS (and perhaps Android), I’ve had some more experience with the Entity/Component/System framework. I’ll talk about that in this post, and give some examples of how I attempt to solve some game development problems with it (something that is often missing from other articles).
I’ll just summarize my framework here in a few paragraphs.
The game world consists of Entities. An entity is a class wrapper around some identifiers.
Associated with each entity (via the identifier) is a set of Components that define the entity. Some examples of Components are:
- Aspect: defines what the entity looks like (texture info, color, etc…)
- Placement: defines the entity’s location and orientation
- LaserInteractive: this is something specific to this particular game, and contains information about a core gameplay feature
- Input: defines whether or not this entity fires click/drag events when interacted with by the mouse or touch
- Entanglement: another game-specific component. It is used to link one entity’s position or orientation to another
- FrameAnimation: indicates that the entity should use spritesheet animation, and specifies things like framerate.
- SoundLoop: indicates a sound that is to be played (and volume, etc..).
- Scripts: this is a bit of a special one, and describes which custom scripts are associated with the entity.
Note that Components only define data, no logic. They essentially describe “intent”.
Next are the Systems. This is where the logic lies. For instance, the RenderingSystem loops through all entities that have Aspect and Placement Components and prepares the necessary stuff to draw them. It doesn’t need to do anything special to determine which entities these are. It just states declaratively that it’s interested in entities with those two Components, and those entities’ ids are automatically added to a list in the System.
Other examples of systems are as follows:
FrameAnimationSystem: It tracks entities with both Aspect and FrameAnimation components, and adjusts the Aspect Components with the correct location in the spritesheet, based off the current time and the framerate specified in FrameAnimation Component.
LaserSystem: This is the most complex system, and manages the main gameplay logic that involves entities with LaserInteractive Components.
EntanglementSystem: This is another game-specific system. This looks at entities with the Entanglement Component, and ensures their positions and/or orientations are kept in sync with the entity’s entangled pair. It also draws the entanglement visuals (it is not only the RenderingSystem that is responsible for drawing).
Winning System: This monitors certain entities’ state to see if the current level has been won.
So you can see that most systems deal with one particular component, but also require a set of other components in order to do their job.
Some systems operate over a similar set of entities (for instance, the LaserSystem and WinningSystem), but contain completely unrelated logic. So while they could be part of the same system, “one class, one purpose” dictates they should not be.
The good things
The benefits of Entity/Component/System frameworks are well-known, and I won’t go into detail about them here.
But I will say it’s been really good for quickly prototyping gameplay ideas for this game, since it’s easy to assemble new types of entities together from existing Components.
ECS also really encourages writing code exactly once. The temptation to copy-paste code really isn’t there, because it just isn’t necessary. And that reduces the chance of bugs.
It’s fair to look at examples where I’ve had to hack things into the framework and ask why.
There is the temptation to throw more data into existing Components. For instance, I eventually realized I needed to ensure that things were drawn in a certain z-order (I’m using alpha-blended sprites, so I’m not using a z-buffer). So I added a Layer property to the Aspect component. Is this the right place? Probably not, it doesn’t have anything to do with the Aspect. It may deserve a new Component altogether. But just for one additional property? (Actually, as I’m writing this, I realize that the Placement Component is probably the right place).
I also wanted to support dual textures and a custom rendering effect for some entities. I ended up hacking some stuff into the Aspect Component for this (if a certain bool is set, use the custom effect). I actually think this is sort of the right thing to do, but the part of the Aspect that defines the sprite needs to include this information. Currently it’s just a texture – I need another more complex abstraction there that the RenderingSystem understands.
Another example is that I needed some entities appear to vibrate back and forth, or slide in from behind other objects. However, I didn’t want to change the position of the entity from the point of view of most code (just from the point of view of the renderer). So I added an “AdditionalVisualOffset” and “AdditionVisualOrientation” to the Placement Component. Only the RenderingSystem looks at this.
That was definitely a shortcut/hack. A better solution would probably be to make the entity that has the visual Aspect be the child of the main entity. I support parent-child relationships between entities, so that one entity can be positioned relative to another. The child entity’s position could be adjusted to simulate vibration, without changing the position of its parent entity (which wouldn’t have Aspect). Unfortunately that would break an assumption I have that the main (parent) entity is the one that has the Aspect component. That assumption is needed for logic that sets the entity’s color (on Aspect) based on the type of laser light it emits (on LaserInteractive). I also have code that swaps the sprite used depending on information in LaserInteractive.
Unique logic (scripts)
The bulk of the game logic is contained in the Systems, but I often need more “one-off” logic. Some examples are:
- The code that scrolls new levels into view gradually, and “introduces” a level by highlighting certain objects, and sliding them into view.
- I have objects in the game that change the color of other objects that are placed inside them.
- I have plants that start to grow when you are in a winning state.
- The name of the level appears gradually, letter by letter, when the level is loaded.
- Some objects in the game world make a sound as you drag them… the fast you drag, the louder the sound.
- Animals pop up and offer you hints if you want.
These types of things are too “small and unique” to create new Systems for. So I have a concept of “Scripts” that can be attached to entities. I actually have a Scripts Component which is just a list of scripts that apply to that entity (scripts do get some special treatment though, so making them part of a Component may not be the best solution).
One special thing about scripts is that there is only one instance of a script, even if it is attached to many entities. One of the original motivations for this was to reduce the heap complexity for my objects (I’m using C#, and garbage collection time increases the more allocated objects you have).
As a result, all state for scripts is attached to a generic property bag that is part of the Scripts Component. Although this limits the kind of information scripts can store, it does have the following benefits:
- Since all scripts attached to one entity are updated together (in sequence), they are accessing the same region of memory in the property bag (better locality of reference).
- I don’t need any custom serialization code for a script (i.e. for save games) – all the script state is serialized in the property bag.
- It will allow me to easily use a separate runtime/interpreted scripting language for logic. I don’t do this for this game, but I do for the other larger game.
Getting/setting variables from a property bag makes the C# code in the scripts a little clunky, but this wouldn’t be an issue if I were using a separate scripting language as I mentioned in the last bullet (since I would then have syntactic sugar that makes interfacing with the underlying property bag look just like normal variables).
I should also mention that the scripts just assume the bare minimum about the entity to which they are attached; so even though I’m calling them “unique code”, they can be applied to any applicable entity. For instance, the “faster you drag an object the louder it gets” script just requires that the entity have Placement and SoundLoop.
One thing I still need to support is the ability to pass parameters to a script when attaching it to an entity.
Of course, sometimes you need to communicate between systems, or between scripts and systems. I use messages for that. You can send a message “somewhere”, or to a particular entity.
Systems handle messages (they declare which ones they can handle), and Scripts can also handle them if they are sent to a specific entity (in that case, all Scripts attached to an entity get a chance). This is the one case where the entity framework has special knowledge about the Scripts component.
I don’t have any fixed list of message types; I just add them as I need them. They are basically used to glue together bits of code where there is no obvious way to do it. Probably for several of these cases I could refactor things to avoid having to use messages.
Some examples of message use:
- PlaySound: A script or system can fire a message that says to play a sound. I could probably solve this in another more explicit way (e.g. having access to some audio manager or something), but currently from scripts there is no access to any particular game classes. So they just fire a message and hope someone handles it. In this case, the SoundSystem will handle it.
- ObtainWinningState: This gets information about if the player is winning the current level (the WinningSystem answers this). I have a script that makes some plants grow, and it uses this to know when and how fast to grow the plants.
- Click: This is sent by the InputSystem to the entity on which it detected a click. In order for anything interesting to happen, that entity will need to have a script attached to it that handles the click message.
- RevealTitle: Here’s one that I realized was totally flawed and bogus as I was writing this. When the level is loaded, there should be an entity that represents the title text of the level. It’s initially hidden. A level initialization script runs for a varying amount of time, and then fires this message. It’s picked up by the title text entity, which has a script that begins to reveal the letters one-by-one. The thing is, I don’t have a way to broadcast a message to all entities in the world. So the level initialization script actually needs to know which entity to send it to (it enumerates all entities and searches for those with specific Components that identify it as the title text entity). It follows then, that it could just attach the “reveal letters” script to that entity at that time. This is one example of an area that is not well thought out by me, and this is just some glue to hold it together.
- UnloadLevel: Another kind of useless one. The game keeps track of the entity that has the “initialize level” script attached to it. When its time to unload the level, it sends this message to that entity. That’s picked up by the “initialize level” script, which then frees all that level’s entities after a predetermined waiting period. I could just as easily have put the unload functionality in another script, and have the game just add that script to the entity. Instead, I used a message, because I needed to pass parameters (how many seconds to wait before unloading), and I have no way to pass parameters to a new script I’m attaching to an entity.
Other bits of functionality
I segregate groups of entities by their owner id (entities can be owned by other entities, or by a “top level group id”). Each level number has a unique “top level group id” associated with it, and all the entities for that level are created with that as their owner. This allows me to have multiple levels loaded at once. This lets me position levels at different spots in the world and pan between them (so two levels exist for a short period of time).
I have a way to enumerate all entities owned by a particular owner. So when it’s time for a loaded level to finally go away, I can say “enumerate all entities owned by 1234”, and then “free this list of entities”. Of course this involves scanning the entire entity list, and feels like a heavyweight operation.
Unfortunately a few of the systems care about this too. The WinningSystem is only interested in entities that are associated with the “current level”. And the LaserSystem needs to segregate entities by level so that they don’t interact with each other.
The LaserSystem solves this by the fact that I made a Constraint Component that defines an area that at entity operates within. A bit of a hack, but I suppose it’s reasonable.
The WinningSystem unavoidably (I think) needs to access some global state that indicates the current level entity owner.
One further thing I can think of. As you travel from the main menu to the levels, I wanted to fade the levels in from a distance. The only reasonable way to do this is to render them to a render target first, and then draw that with varying degrees of opacity.
Of course, I also wanted some of the menu UI to be a part of the ECS framework. It just makes things easier.
But how would the RenderingSystem know that these entities are to be rendered to a different buffer than these other ones? In fact, the RenderingSystem is only part of the entire rendering functionality, and doesn’t manage render targets and such. It just issues draw commands.
The best solution I could think of was to have multiple simultaneous but completely separate entity managers. The level entities operate on their own complete separate “space” than the UI/menu entities.
Unfortunately I had a bunch of global state (or rather singletons) that prevented this. There was a single EntityManager and SystemManager. Global stuff always comes back to bite you. Fortunately, removing global variables is usually fairly straightforward to do, if tedious. So I now have the ability to run two (or more) completely separate entity systems.
Brain dump over.