DEV Community

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

Posted on

2 1 1

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

Image of Stellar post

How a Hackathon Win Led to My Startup Getting Funded

In this episode, you'll see:

  • The hackathon wins that sparked the journey.
  • The moment José and Joseph decided to go all-in.
  • Building a working prototype on Stellar.
  • Using the PassKeys feature of Soroban.
  • Getting funded via the Stellar Community Fund.

Watch the video 🎥

Top comments (0)

Postmark Image

"Please fix this..."

Focus on creating stellar experiences without email headaches. Postmark's reliable API and detailed analytics make your transactional emails as polished as your product.

Start free

👋 Kindness is contagious

DEV is better (more customized, reading settings like dark mode etc) when you're signed in!

Okay