I am trying to design an entity-component system in typescript. My goal is to create a variadic generic abstract System class that takes component classes as generic arguments.
The idea is that derived System classes can be declared with the components they use. They can then use this information (the component class objects) to pull the subset of entities they operate on from the list of all entities. This subset is the set of entities that have the required components.
For context, I have included the component and the entity code first:
interface IComponent {
owner: Entity | null;
}
type ComponentClass<C extends IComponent> = new (args: unknown[]) => C;
abstract class Entity {
public readonly name: string;
/** The pixijs scene node (pixijs display object). */
private _sceneNode: unknown;
protected _components: IComponent[] = [];
constructor(name: string) {
this.name = name;
}
public get components(): IComponent[] {
return this._components;
}
public addComponent(component: IComponent): void {
this._components.push(component);
component.owner = this;
}
public getComponent<C extends IComponent>(componentClass: ComponentClass<C>): C {
for (const component of this._components) {
if (component instanceof componentClass) {
return component as C;
}
}
throw new Error(`component '${componentClass.name}' missing from entity ${this.constructor.name}`);
}
public removeComponent<C extends IComponent>(componentClass: ComponentClass<C>): void {
const removeList: number[] = [];
this._components.forEach((component, index) => {
if (component instanceof componentClass) {
removeList.push(index);
}
});
removeList.forEach(index => {
this._components.splice(index, 1);
});
}
public hasComponent<C extends IComponent>(componentClass: ComponentClass<C>): boolean {
return this._components.some(component => {
return component instanceof componentClass;
});
}
}
interface ISystem {
onSpawnEntity(entity: Entity): void;
onDestroyEntity(entity: Entity): void;
pullEntities(entities: Entity[]): void;
update(dt_s: number) : void;
}
I am trying to make this a variadic generic that can take an arbitrary number of component
classes, each of which extends IComponent. Currently, it takes only 2, which demonstrates what I am trying to achieve:
abstract class System<C0 extends IComponent, C1 extends IComponent> implements ISystem {
protected _targets: [ComponentClass<C0>, ComponentClass<C1>];
protected _subjects: Entity[] = [];
constructor(targets: [ComponentClass<C0>, ComponentClass<C1>]) {
this._targets = targets;
}
public pullEntities(entities: Entity[]): void {
entities.forEach(entity => {
if(this.isEntitySubject(entity)) {
this._subjects.push(entity);
}
});
}
public onSpawnEntity(entity: Entity): void {
if(this.isEntitySubject(entity)) {
this._subjects.push(entity);
}
}
public onDestroyEntity(entity: Entity): void {
if(this.isEntitySubject(entity)) {
}
}
public update(dt_s: number) : void {
this._subjects.forEach(entity => {
const c0 = entity.getComponent(this._targets[0]);
const c1 = entity.getComponent(this._targets[1]);
this.updateEntity(c0, c1);
})
}
private isEntitySubject(entity: Entity): boolean {
return entity.hasComponent(this._targets[0]) &&
entity.hasComponent(this._targets[1]);
}
// the idea is that this is the only function systems will have to implement themselves,
// ideally want the args to be a variadic array of the component instances which the
// system uses.
protected updateEntity(c0: C0, c1: C1) {}
}
abstract class World
{
protected _systems: ISystem[] = [];
protected _entities: Entity[] = [];
public feedEntities(): void {
this._systems.forEach(system => {
system.pullEntities(this._entities);
});
}
public updateSystems(): void {
this._systems.forEach(system => {
system.update(20);
});
}
}
I have also included the example usage to provide more context:
////////////////////////////////////////////////////////////////////////////////
// EXAMPLE USAGE
////////////////////////////////////////////////////////////////////////////////
class PhysicsComponent implements IComponent {
public owner: Entity | null;
public x: number;
public y: number;
public mass: number;
}
class CollisionComponent implements IComponent {
public owner: Entity | null;
public bounds: {x: number, y: number, width: number, height: number};
}
// creating a new system, defining the component classes it takes.
class PhysicsSystem extends System<PhysicsComponent, CollisionComponent> {
protected updateEntity(physics: PhysicsComponent, collision: CollisionComponent) {
physics.x += 1;
physics.y += 1;
console.log(`entity: ${physics.owner.name} has position: {x: ${physics.x}, y: ${physics.y}}`);
}
}
class Person extends Entity {
constructor(name: string) {
super(name);
const physics = new PhysicsComponent();
physics.x = 20;
physics.y = 40;
this.addComponent(physics);
const collision = new CollisionComponent();
collision.bounds = {
x: 0, y: 0, width: 100, height: 100
};
this.addComponent(collision);
}
}
class GameWorld extends World {
constructor() {
super();
this._systems.push(new PhysicsSystem([PhysicsComponent, CollisionComponent]));
const e0 = new Person("jim");
const e1 = new Person("steve");
const e2 = new Person("sally");
this._entities.push(e0, e1, e2);
this.feedEntities();
}
}
const world = new GameWorld();
setInterval(() => {
world.updateSystems();
}, 300);
Is what I am trying to achieve even possible?