DEV Community

Cover image for Building a tiny type-safe typescript ECS (Entity-component-system)
Trym Nilsen
Trym Nilsen

Posted on

Building a tiny type-safe typescript ECS (Entity-component-system)

I have been down a "rewatching old game developer conference videos" sized rabbit hole lately and recently I Nerd sniped myself thinking

Could I make a small ECS in typescript to play around with?.

In my spare time I work on a medieval city builder named Kingdom Architect and I have chosen to go for a Gameobjects-Component like architecture. This has worked fine, but I have also found it challenging to handle interactions between components and system level behaviour. Is it wise to completely change the architecture if I want to ever ship my project, probably not... Is it fun, heck yes!

Inspired by Maxwell Forbes blog post I set out on my adventures. Some of the things I wanted to improve and have in my own little playground was a strict query and access functionality. You only got what you asked for, but what you asked for would be there fully typed. I also wanted to avoid defining the set of components for a system multiple times or in multiple places.

What I ended up with was an API that looked like this

class CounterComponent extends EcsComponent {
    currentValue: number = 0;
}

const counterSystem = new EcsSystem({
    counter: CounterComponent,
});

counterSystem.withUpdate(({counter}) => {
    counter[0].currentValue += 42;
    console.log("Amount during update", counter[0].currentValue);
});
Enter fullscreen mode Exit fullscreen mode

Based on the object provided in the constructor of a new system, I both know which components my system is interested in and I have a data structure and typing info for when I provide these components in the update loop.

Turns out the magic sauce was in the InstanceType utility type. With this I could take my "query object" with class prototypes and convert it to a an object that I could use as the argument for the update function.

export type QueryData<T extends QueryObject = QueryObject> = {
    [P in keyof T]: InstanceType<T[P]>[];
};
Enter fullscreen mode Exit fullscreen mode

In case you are wondering what the QueryObject is, it looks like this and is used in the System class.

export type ComponentFn<T extends EcsComponent = EcsComponent> = new (
    ...args: any[]
) => T;

export interface QueryObject<T extends ComponentFn = ComponentFn> {
    [componentName: string]: T;
}

export class EcsSystem<T extends QueryObject = QueryObject> {
    private onUpdate: UpdateFunction<T> | null = null;

    constructor(public query: Readonly<T>) {}

    runUpdate(components: QueryData<T>, gameTime: number) {
        if (this.onUpdate) {
            this.onUpdate(components, gameTime);
        }
    }

    withUpdate(updateFunction: UpdateFunction<T>): void {
        this.onUpdate = updateFunction;
    }
}
Enter fullscreen mode Exit fullscreen mode

The next step in making my ECS playground is hooking up my system with a mapping between entities (that I plan on letting be a number, like in Maxwell's system) and running the update loop/querying for components.

If you want the full source, all 47 lines of it, I have made a gist Here

Top comments (0)