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);
});
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]>[];
};
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;
}
}
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)