mcpField
E
ecs

ecs

github.com/drawcall-ai开发122 次安装
代码仓库

关于 ecs

ecs 是使用 EliCS Entity Component System 框架构建 TypeScript 游戏的技能。它介绍实体、组件和系统的组织方式,并涵盖生命周期、查询及状态到视图的同步。

主要功能

  1. 01创建 EliCS 世界与实体
  2. 02采用仅存储数据的组件
  3. 03初始化并更新系统
  4. 04按组件查询实体
  5. 05暂停和恢复系统
  6. 06输入、状态与视图分层

适用场景

  1. 01组织具有共同行为的游戏对象
  2. 02安排每帧游戏状态更新
  3. 03在菜单或对话期间暂停系统

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/drawcall-ai/skills --skill ecs

安装后在 Claude Code 中输入/ecs确认已加载。

详细介绍

实体组件系统

我们使用 EliCS,这是一个轻量、高性能的 TypeScript 实体组件系统框架。它采用面向数据的设计,优先使用组合而非继承,并通过高效的位掩码和类型化数组存储数据。

**核心概念:**实体(唯一对象)、组件(数据容器)、系统(逻辑处理器)、查询(实体选择器)和世界(管理所有内容的容器)。

**适用场景:**当游戏中有许多共享行为且相互交互的对象(敌人、投射物、拾取物、单位),并且采用状态驱动的更新循环时,ECS 会很有帮助——它将逻辑保留在系统中,将状态保留在组件中,避免代码陷入类的纠缠。对于只有少量定制对象的小型场景,ECS 则会带来额外开销;使用普通对象和渲染循环即可。以下章节介绍了足以使用 EliCS 进行开发的 API;如需查看准确的签名和完整类型定义,请查阅已安装的 elics 类型,而不要依赖本页,因为本页内容可能落后于软件包。

推荐架构

使用 3 个层:输入、状态和视图。

  • 将输入映射到状态(输入系统:接收用户输入,将其规范化并应用到状态。复杂输入可以拆分到多个系统中处理(例如,原始事件 → 规范化操作 → 状态更新)。每个输入系统都必须提供可命令式调用所有输入的方法,以便以编程方式模拟和测试用户输入。)
  • 每帧推进状态(状态系统:每帧推进游戏状态(例如,敌人追逐玩家)。)
  • 将状态同步到视图(视图系统:在视图中反映状态。必要时,在初始化期间订阅实体的创建和移除事件(例如,创建/销毁 3D 对象或 UI),并在每帧更新频繁变化的数据。)
推荐文件结构
  • src/components/*.ts:每个组件一个文件(例如,position.ts、velocity.ts)。组件只保存状态;行为应放在系统中。
  • src/systems/*.ts:每个系统一个文件(例如,input.ts、physics.ts)。每个系统都导出初始化函数和逐帧更新函数。
  • src/seed.ts:构建启动实体并为其添加组件,使游戏一开始就可以游玩。
  • src/main.ts:组合游戏。加载种子、注册系统、运行初始化,然后逐帧推进所有状态并同步到视图。

创建世界

“世界”是所有 ECS 元素共存的地方。

import { World } from 'elics'

const world = new World()

定义组件

组件是用于定义实体属性的数据容器。

import { createComponent, Types } from 'elics'
import { world } from './world.ts'

// Available Types:
// - Scalars: Types.Int8, Types.Int16, Types.Float32, Types.Float64, Types.Boolean, Types.String
// - Vectors: Types.Vec2, Types.Vec3, Types.Vec4, Types.Color (RGBA 0-1)
// - Entity reference: Types.Entity
// - Enum: Types.Enum
// - Types.Object holds any JS value (a Three.js mesh, an array) — not an ECS reference

// Define enums for type-safe state management using const assertions
export const UnitType = {
  Infantry: 'infantry',
  Cavalry: 'cavalry',
  Archer: 'archer',
} as const

export const CombatState = {
  Idle: 'idle',
  Moving: 'moving',
  Attacking: 'attacking',
  Defending: 'defending',
} as const

export const Position = createComponent(
  'Position',
  {
    value: { type: Types.Vec3, default: [0, 0, 0] },
  },
  '3D position coordinates',
)
//register components directly after creating them
world.registerComponent(Position)

export const Velocity = createComponent(
  'Velocity',
  {
    value: { type: Types.Vec3, default: [0, 0, 0] },
  },
  '3D velocity vector',
)
world.registerComponent(Velocity)

export const Health = createComponent(
  'Health',
  {
    value: { type: Types.Float32, default: 100 },
  },
  'Entity health points',
)
world.registerComponent(Health)

export const Unit = createComponent(
  'Unit',
  {
    type: { type: Types.Enum, enum: UnitType, default: UnitType.Infantry },
    state: { type: Types.Enum, enum: CombatState, default: CombatState.Idle },
    damage: { type: Types.Float32, default: 10, min: 1, max: 100 },
    armor: { type: Types.Int16, default: 0, min: 0, max: 50 },
    morale: { type: Types.Float32, default: 1.0, min: 0.0, max: 1.0 },
  },
  'Military unit with combat statistics',
)
world.registerComponent(Unit)

// Entity references
export const Target = createComponent('Target', {
  entity: { type: Types.Entity, default: null },
})
world.registerComponent(Target)

**重要:**向量类型(Vec2、Vec3、Vec4、Color)必须通过 getVectorView() 访问,而不能使用 getValue()/setValue()。如果使用了错误的访问器,EliCS 会在运行时抛出异常。

创建实体

实例化实体并为其添加组件:

const unit = world.createEntity()
unit.addComponent(Position, { value: [10, 20, 30] })
unit.addComponent(Velocity)
unit.addComponent(Health)
unit.addComponent(Unit, {
  type: UnitType.Infantry,
  state: CombatState.Moving,
  damage: 15,
  armor: 20,
  morale: 0.8,
})

// Reading and writing scalar values
const damage = unit.getValue(Unit, 'damage') // returns number
unit.setValue(Unit, 'state', CombatState.Attacking)

// Reading and writing vector values (returns TypedArray subarray)
const pos = unit.getVectorView(Position, 'value')
pos[0] = 100 // Modify X directly

// Check component existence
if (unit.hasComponent(Health)) {
  /* ... */
}

// Remove component or destroy entity
unit.removeComponent(Velocity)
unit.destroy() // entity.active becomes false

创建系统

系统包含核心逻辑。

import { createSystem, Entity } from 'elics'
import { Object3D } from 'three'
import { scene } from './scene.ts'
import { unitModel } from './unit-model.ts'

const movementQueryConfig = {
  movables: { required: [Position, Velocity] },
}

export class MovementSystem extends createSystem(movementQueryConfig) {
  update(delta: number, time: number) {
    this.queries.movables.entities.forEach((entity: Entity) => {
      const position = entity.getVectorView(Position, 'value')
      const velocity = entity.getVectorView(Velocity, 'value')
      position[0] += velocity[0] * delta
      position[1] += velocity[1] * delta
      position[2] += velocity[2] * delta
    })
  }
}

const unitViewQueryConfig = {
  units: { required: [Unit, Position] },
}

export class UnitViewSystem extends createSystem(unitViewQueryConfig) {
  public readonly models = new Map<Entity, Object3D>()

  init() {
    const create = (entity: Entity) => {
      const newModel = unitModel.clone()
      scene.add(newModel)
      this.models.set(entity, newModel)
    }
    const destroy = (entity: Entity) => {
      const oldModel = this.models.get(entity)!
      scene.remove(oldModel)
      this.models.delete(entity)
    }
    // Loop through existing and subscribe to new query entities
    this.queries.units.entities.forEach(create)
    this.queries.units.subscribe('qualify', create)
    this.queries.units.subscribe('disqualify', destroy)
  }

  update(delta: number, time: number) {
    this.queries.units.entities.forEach((entity) => {
      const model = this.models.get(entity)!
      const pos = entity.getVectorView(Position, 'value')
      model.position.set(pos[0], pos[1], pos[2])
    })
  }

  destroy() {
    // Cleanup when system is unregistered
  }
}

// Register with priority (lower runs first)
world.registerSystem(MovementSystem, { priority: 0 })
world.registerSystem(UnitViewSystem, { priority: 10 })

访问系统

使用 world.getSystem(...) 获取已注册的系统实例:

const enemySystem = world.getSystem(EnemySystem)!

// Access system properties and methods
enemySystem.spawnEnemy(position)

系统资源加载

框架会调用 init()(在 registerSystem 时)和 update()(每次调用 world.update 时)。它没有 load() 生命周期钩子——只有这两个钩子。如需异步加载资源,请为系统提供自己的 load() 方法,并在注册后、动画循环开始前自行调用它。这只是普通方法,不是框架钩子:除非你主动调用,否则不会有人调用它。

export class EnemySystem extends createSystem(queryConfig) {
  private model!: Group

  init() {
    /* called by registerSystem() — synchronous only */
  }

  async load() {
    // your own method — you must await it yourself (see below)
    this.model = (await new GLTFLoader().loadAsync('/models/enemy.glb')).scene
  }

  update(delta: number, time: number) {
    /* called every frame by world.update */
  }
}

在 main.ts 中,先注册系统(这会运行 init()),然后在循环开始前等待 load() 完成:

world.registerSystem(EnemySystem) // runs init()
await world.getSystem(EnemySystem)!.load()
renderer.setAnimationLoop(() => { ... })

系统生命周期控制

使用 stop() 和 play() 动态暂停和恢复系统:

const enemySystem = world.getSystem(EnemySystem)!

// Pause the system (e.g., during cutscenes or menus)
enemySystem.stop()

// Check if system is paused
if (enemySystem.isPaused) {
  console.log('Enemy system is paused')
}

// Resume the system
enemySystem.play()

**示例用例:**在对话期间暂停物理模拟,然后在玩家重新获得控制权时恢复。

查询谓词

使用 where 子句按组件值筛选实体:

const queryConfig = {
  lowHealth: {
    required: [Health, Unit],
    excluded: [Target], // Exclude entities with this component
    where: [{ component: Health, key: 'value', op: 'lt', value: 30 }],
  },
}

运算符:eq(等于)、ne(不等于)、lt(小于)、le(小于或等于)、gt(大于)、ge(大于或等于)、in(在数组中)、nin(不在数组中)。

更新世界

使用 delta 和 time 更新世界。

function render() {
  ...
  const delta = clock.getDelta()
  ...
  world.update(delta, clock.elapsedTime)
  ...
}

评论

支持 Markdown