打字稿可变泛型类,接受扩展公共基础的可变数量的类对象?

Posted

技术标签:

【中文标题】打字稿可变泛型类,接受扩展公共基础的可变数量的类对象?【英文标题】:Typescript variadic generic class, accepting a variable number of class objects which extend a common base? 【发布时间】:2021-09-26 11:14:18 【问题描述】:

我正在尝试在 typescript 中设计一个实体组件系统。我的目标是创建一个可变参数泛型抽象 System 类,它将组件类作为泛型参数。

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

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

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;

我试图使它成为一个可变参数泛型,可以采用任意数量的组件 类,每个类都扩展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);

我正在努力实现的目标是否可能?

【问题讨论】:

【参考方案1】:

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

所以你的System 类可以是

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;

解释:

    TComponents extends IComponent[] 表示TComponent 必须是IComponent 的数组(或元组),例如PhysicsComponent[][PhysicsComponent, CollisionComponent](我没有测试,但我的代码的其他部分应该只适用于元组类型) 为了将TComponents转换为ComponentClasses,我使用了一个辅助类型ComponentClasses,它是一个Mapped type,特别是,映射一个元组类型只映射它的数字键,意味着ComponentClasses&lt;[PhysicsComponent, CollisionComponent]&gt;将返回@987654335 @ 要使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]));

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

【讨论】:

只想说声谢谢正是我需要的答案:)

以上是关于打字稿可变泛型类,接受扩展公共基础的可变数量的类对象?的主要内容,如果未能解决你的问题,请参考以下文章

如何在打字稿的泛型类中创建类型为“T”的新对象?

如何在 typescript 中为泛型类编写扩展作为 getter

Kotlin泛型总结 ★ ( 泛型类 | 泛型参数 | 泛型函数 | 多泛型参数 | 泛型类型约束 | 可变参数结合泛型 | out 协变 | in 逆变 | reified 检查泛型参数类型 )

打字稿:类型 ConstructorParameters 不接受泛型

带有约束的打字稿泛型不能分配给泛型接口

打字稿泛型-“扩展对象”毫无意义吗?最佳做法是啥?