2020 September 15th
I fell in love with game development when I first made some small flash games. Casual gaming boomed with flash in the mid-to-late 2000s and influenced the gaming industry profoundly. When my aspirations to create games were refueled in university I knew that I wanted to be able to publish my games to the browser. I wanted anyone to be able to play anywhere.
Flash inspired me to keep my approach scrappy and to make iteration very quick. I did a lot of heavy research at this point. I knew that architecting a framework or library to let me create games for the browser would be a rewarding but complex undertaking. Robert Nystrom's game programming patterns first introduced me to the Entity Component System architectural pattern. The flexibility was novel to me, having only really been exposed to the Gang of Four's Design Patterns. I set out to create an HTML5 game framework following this architecture in typescript - I named it Backpack.
In this blog, I want to detail, motivate, and (quickly) hack together some of the basic ideas of Backpack and the ECS architecture pattern.
Let's start our exploration with the Component part of Entity Component System. A component is simply raw data. It contains no logic. We can implement it as a simple structure. Backpack is written in Typescript and I chose to create an abstract class.
1abstract class Component {2// todo: implement concrete data fields3}4
Consider the two concrete components: Position
and Image
.
1class PositionComponent extends Component {2x: number = 0;3y: number = 0;4}56class ImageComponent extends Component {7source: string;8}9
We want to compose characters, objects, and scenes in our games by using these data-representations. If our Player is supposed to have a picture show on the screen, we compose them with an Image component. Having a component is synonymous with saying the Player
has a certain attribute.
This is a departure from traditional OOP, where one may create an Image
class that the Player
class inherits from. Now, how do we add additional behaviour without repeating ourselves? Is Player
able to inherit from multiple classes? This can lead to the Deadly Diamond of Death if those classes both implement a certain action. Although composition over inheritance is a tenet of OOP design, the paradigm does not do a good job of shepherding behaviour towards this.
An Entity is a collection of components. This is how we achieve our composition. Our Player
will be represented as an entity.
In Backpack, an entity is simply a unique ID. Instead, the magic happens within an EntityManager
. The EntityManager is responsible for holding all of the entities, registering new entities, and fetching entities. This is not dissimilar with record storage in databases.
1type Entity = Component[];23class EntityManager {4entities: Entity[];56public registerNewEntity(...components: Entity) {7entities.push([...components]);8// other operations (registering with a system)9}1011public getEntity(id: number) {12return entities[id];13}14}15
We can now create our Player
!
1EntityManager.registerNewEntity(2new Position(),3new Image('hero.png'),4)5
Our data is never explicitly shaped or restricted. Instead it exists in a grab-bag at run-time. We can add and remove attributes from our Player on the fly. This flexibility is one of the major strengths of an ECS architecture.
Our player kind of exists ephemerally now. We don't have a direct tie to where it has been created (and we don't need it necessarily). Instead we rely on the composition of our components. This is what allows such flexibility. For example, we can treat ImageComponents
all the same and not worry about the specificities of some "player image".
That being said, we haven't added any logic to our game yet. Systems are the final piece of that puzzle. Systems take entities with a certain set of components and perform some operation on them.
One additional responsibility of our EntityManager
will be to register Entities with our Systems based on each entity's components.
Given the registered entities, a system just needs to iterate over each one and perform some "update" logic.
1abstract class System {2public registeredEntityIDs: number[] = [];3public tick() {4for (entityID in this.registeredEntityIDs) {5this.update(entityID);6}7}89abstract update(entityID: number);10}1112
Our PositionSystem
will be in charge of updating the position data of our Player
when the keyboard keys are pressed.
1class PositionSystem {2update(entityID: number) {3const components = EntityManager.getEntity(entityID);4const positionComponent = components.find(Position);56if (Key.pressed(DOWN)) {7positionComponent.y -= 10;8}910if (Key.pressed(UP)) {11positionComponent.y += 10;12}13}14}15
Similarly, we would create an ImageSystem
that would look at the image source defined in the ImageComponent
and then draw it at the x, y coordinates described in the PositionComponent
.
The last piece is to run all of our systems during our game loop. For this, I created the main Backpack
engine class. At its simplest the engine would need to run every system's tick function.
1class Engine {2public systems: System[] = [];34init() {5// setup systems6window.requestAnimationFrame(this.tick);7}89tick() {10for (system in this.systems) {11system.tick()12}13window.requestAnimationFrame(this.tick)14}15}1617const Backpack = new Engine();18Backpack.init();19
The implementations above were the bases that I started with when building Backpack. I built a handful of common components that could be shared across games such as an AudioComponent
and TextComponent
. I also added animation utilities and easing functions. But it met my needs for scrappy and accessible.
In Search of Sand is one of my favourite Backpack games (arguably, it's more of a toy). It is a small simulation game on a board of tiles. Each tile can be one of a few materials. Each tile interacts with the tiles immediately neighbouring it based on the material types. Try placing different types of tiles and seeing the results of various combinations. Please give it a try below!
Thank you for reading. Thank you for exploring ideas with me.