Typescript 可变参数泛型 class,接受可变数量的 class 扩展公共基础的对象?

Typescript variadic generic class, accepting a variable number of class objects which extend a common base?

我正在尝试用打字稿设计一个实体组件系统。我的目标是创建一个可变参数通用抽象 System class,它将组件 classes 作为通用参数。

想法是派生的 System classes 可以用它们使用的组件声明。然后他们可以使用此信息(组件 class 对象)从所有实体列表中提取他们操作的实体子集。此子集是具有所需组件的实体集。

对于上下文,我先包含了组件和实体代码:

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;
}

我正在尝试使它成为一个可以采用任意数量组件的可变泛型 classes,每个扩展 IComponent。目前,它只需要 2 个,这说明了我正在努力实现的目标:

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

我还包含了示例用法以提供更多上下文:

////////////////////////////////////////////////////////////////////////////////
// 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);

我想要实现的目标是否可行?

TypeScript 没有“可变泛型”,但它有 Tuple types

所以你的Systemclass可以

type ComponentClasses<T extends IComponent[]> = { [K in keyof T]: T[K] extends IComponent ? ComponentClass<T[K]> : never };

abstract class System<TComponents extends IComponent[]> implements ISystem {
    protected _targets: ComponentClasses<TComponents>;
    protected _subjects: Entity[] = [];

    constructor(targets: ComponentClasses<TComponents>) {
        this._targets = targets;
    }

    protected abstract updateEntity(...components: TComponents): void;
}

解释:

  1. TComponents extends IComponent[]表示TComponent必须是IComponent的数组(或元组),例如PhysicsComponent[][PhysicsComponent, CollisionComponent](我没有经过测试,但我的代码的其他部分应该只适用于元组类型)
  2. 为了将 TComponents 转换为它们的 ComponentClasses,我使用了辅助类型 ComponentClasses,它是一个 Mapped type,特别是,映射元组类型仅映射其编号键,意味着 ComponentClasses<[PhysicsComponent, CollisionComponent]> 将 return [ComponentClass<PhysicsComponent>, ComponentClass<CollisionComponent>]
  3. 为了使 updateEntity 方法接受可变数量的参数,使用了 Rest Parameters 语法。在 TypeScript 中,它允许使用元组类型标记多个参数。

PhysicsSystem的例子:

class PhysicsSystem extends System<[PhysicsComponent, CollisionComponent]> {
    protected override 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}}`);
    }
}

如果您更改 physicscollision 参数的类型,它将无法编译。

GameWorld中:

this._systems.push(new PhysicsSystem([PhysicsComponent, CollisionComponent]));

如果更改参数数组,它也不会编译。