You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ECS架构中如何实现仅靠泛型参数获取对应Component实例?

问题描述

我想要编写一个简单的Entity Component System(ECS),现有代码如下:

class Component {}
class Entity { readonly components: Array<Component> = [] }
class T extends Component {}
class F extends Component { get hello() { return 'world' } }
class World {
  readonly entities: Array<Entity> = []
  readonly components: Map<typeof Component, Array<Component>> = new Map()
  first_entity(T: typeof Entity) { return this.entities.find(_ => _ instanceof T) }
  all_entities(T: typeof Entity) { return this.entities.filter(_ => _ instanceof T) }
  first<T extends Component>(ctor: { new(...args: any[]): T }) { return this.components.get(ctor) }
  constructor() {
    this.components.set(T, [new T()])
    this.components.set(F, [new F(), new F()])
    console.log(this.first<F>(F))
  }
}
let world = new World()

我希望能这样调用:

world.first<F>() 
// 返回第一个F类型的Component实例

目前的实现需要同时传入泛型参数和构造函数(如world.first<F>(F)),但我希望仅通过泛型参数完成调用。此外,需要保证类型安全:world.first<F>()?.hello可以正确编译,而world.first<T>()?.hello无法编译。

解决方案

TypeScript泛型在运行时会被擦除,无法直接从泛型类型获取构造函数引用,我们可以通过维护类型名称与构造函数的映射来实现无参数调用,同时保证类型安全,以下是两种可行方案:

方案一:手动维护类型映射

适合组件类型固定的场景,直接在first方法中关联泛型类型与构造函数:

class Component {}
class Entity { readonly components: Array<Component> = [] }
class T extends Component {}
class F extends Component { get hello() { return 'world' } }

class World {
  readonly entities: Array<Entity> = []
  readonly components: Map<typeof Component, Array<Component>> = new Map()

  first_entity(T: typeof Entity) { return this.entities.find(_ => _ instanceof T) }
  all_entities(T: typeof Entity) { return this.entities.filter(_ => _ instanceof T) }

  first<C extends Component>(): C | undefined {
    // 手动关联组件类型名称与构造函数
    const ctorMap = {
      T: T,
      F: F
    } as const;
    
    const ctor = ctorMap[C.name as keyof typeof ctorMap];
    if (!ctor) return undefined;
    
    // 类型断言保证返回值类型正确
    const comps = this.components.get(ctor) as Array<C>;
    return comps?.[0];
  }

  constructor() {
    this.components.set(T, [new T()])
    this.components.set(F, [new F(), new F()])
    console.log(this.first<F>()) // 直接通过泛型调用
  }
}
let world = new World()

// 类型安全验证
world.first<F>()?.hello; // 编译通过
world.first<T>()?.hello; // 编译报错(T无hello属性),符合预期

方案二:自动注册维护映射

适合组件类型动态新增的场景,通过注册方法自动记录类型与构造函数的关联:

class Component {}
class Entity { readonly components: Array<Component> = [] }
class T extends Component {}
class F extends Component { get hello() { return 'world' } }

class World {
  readonly entities: Array<Entity> = []
  private readonly componentNameMap = new Map<string, typeof Component>()
  readonly components: Map<typeof Component, Array<Component>> = new Map()

  first_entity(T: typeof Entity) { return this.entities.find(_ => _ instanceof T) }
  all_entities(T: typeof Entity) { return this.entities.filter(_ => _ instanceof T) }

  // 注册组件时自动记录类型名称与构造函数的映射
  registerComponent<C extends Component>(ctor: { new(): C }, instances: Array<C>) {
    this.componentNameMap.set(ctor.name, ctor);
    this.components.set(ctor, instances);
  }

  first<C extends Component>(): C | undefined {
    const ctor = this.componentNameMap.get(C.name) as { new(): C };
    if (!ctor) return undefined;
    
    const comps = this.components.get(ctor) as Array<C>;
    return comps?.[0];
  }

  constructor() {
    this.registerComponent(T, [new T()]);
    this.registerComponent(F, [new F(), new F()]);
    console.log(this.first<F>());
  }
}
let world = new World()

// 类型安全验证
world.first<F>()?.hello; // 编译通过
world.first<T>()?.hello; // 编译报错,符合预期

核心说明

  • 利用类的name属性作为泛型类型与构造函数的关联键,绕开泛型擦除的限制。
  • 通过类型断言as Array<C>让TypeScript正确推导返回值类型,确保类型安全:存在对应属性的组件调用能通过编译,不存在的则报错。

内容的提问来源于stack exchange,提问作者eguneys

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 10:30:58