Sep 7, 2021

Building an ECS in TypeScript

Deeper Dive: Entities

Our Entity implementation is about as simple as can be.

/**
* An entity is just an ID. This is used to look up its associated
* Components.
*/

type Entity = number

The complete Entity implementation. Seriously.

But let me use this as an excuse to tell a little story. The story is about the most beautiful refactor of my life.

My First Entity Implementation

When I was programming an ECS for the first time, I did what any good programmer would, and invented a pointless abstraction in fear of what the future might bring.

class Entity {
private repr: string

constructor(private id: number) {
this.repr = 'Entity(' + id + ')';
}

public toString(): string {
return this.repr;
}
}

My original Entity implementation was a class, but it didn’t do anything, and I never actually called toString() on it.

I am being a bit harsh on myself here. After all, I had no idea what kinds of things programming a game would demand. Perhaps I’d find a dire need to squirrel away information inside the Entity?

The Most Beautiful Refactor of My Life

I wrote that first implementation on October 30, 2016.

On July 13, 2018, nearly two years of programming later, I had built a pretty hefty codebase with this ECS engine as the backbone. It managed tens of thousands of Entities across a variety of levels. The Entities ranged from the player, to enemies, to items, to environment objects, to even pieces of the GUI. The Entity class was referenced across several dozen files.

But all this time, I’d never put anything else inside the Entity class. So I had a simple thought: could we just change it to a number? Would this possibly work?

There was no reason to except elegance. Wanting to change this was pure greed—but the greed where you want a program to be simpler just because it’s more beautiful that way.

Worth a try. I deleted the whole class, replacing it with the simple type alias above.

-    class Entity {
- private repr: string
-
- constructor(private id: number) {
- this.repr = 'Entity(' + id + ')';
- }
-
- public toString(): string {
- return this.repr;
- }
- }
+ type Entity = number

Replacing the Entity implementation entirely.

Then, I changed the only place in the entire codebase an Entity was created, a single line in the ECS’s addEntity() function:

-    let entity = new Entity(this.nextEntityID);
+ let entity: Entity = this.nextEntityID;

Changing the single line of code that used it.

… and that was it.

I pressed “compile.” The whole game compiled. No errors.

I ran the game. No problems.

I could not believe it.