Composing Backpack

2020 September 15th


Composing Backpack

game development
architecture
software
typescript

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.

Components = The Data

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.

1
abstract class Component {
2
// todo: implement concrete data fields
3
}
4

Consider the two concrete components: Position and Image.

1
class PositionComponent extends Component {
2
x: number = 0;
3
y: number = 0;
4
}
5
6
class ImageComponent extends Component {
7
source: 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.

Matrix Transpose
Deadly Diamond of Death image by RokerHRO, released into the public domain.

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.

Entities = Mixing Data

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.

1
type Entity = Component[];
2
3
class EntityManager {
4
entities: Entity[];
5
6
public registerNewEntity(...components: Entity) {
7
entities.push([...components]);
8
// other operations (registering with a system)
9
}
10
11
public getEntity(id: number) {
12
return entities[id];
13
}
14
}
15

We can now create our Player!

1
EntityManager.registerNewEntity(
2
new Position(),
3
new 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".

Systems = Logic

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.

1
abstract class System {
2
public registeredEntityIDs: number[] = [];
3
public tick() {
4
for (entityID in this.registeredEntityIDs) {
5
this.update(entityID);
6
}
7
}
8
9
abstract update(entityID: number);
10
}
11
12

Our PositionSystem will be in charge of updating the position data of our Player when the keyboard keys are pressed.

1
class PositionSystem {
2
update(entityID: number) {
3
const components = EntityManager.getEntity(entityID);
4
const positionComponent = components.find(Position);
5
6
if (Key.pressed(DOWN)) {
7
positionComponent.y -= 10;
8
}
9
10
if (Key.pressed(UP)) {
11
positionComponent.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.

Putting it together

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.

1
class Engine {
2
public systems: System[] = [];
3
4
init() {
5
// setup systems
6
window.requestAnimationFrame(this.tick);
7
}
8
9
tick() {
10
for (system in this.systems) {
11
system.tick()
12
}
13
window.requestAnimationFrame(this.tick)
14
}
15
}
16
17
const Backpack = new Engine();
18
Backpack.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!

Play

Thank you for reading. Thank you for exploring ideas with me.


Conversation

Design, Images, and Website © Justin Mills 2019 - 2024
Subscribe with RSS | Made with love in Ottawa 🍁