关于 koota
Koota 是一种面向 TypeScript 开发的 ECS 状态管理工具,以实体和可组合特征组织数据,并通过查询和系统更新状态。它强调逻辑与视图分离,适合构建可组合的运行时逻辑。
主要功能
- 01实体与特征组合
- 02关系图建模
- 03按原型查询实体
- 04动作执行状态变更
- 05系统响应状态变化
- 06React 集成
适用场景
- 01组织游戏或应用的实体状态
- 02批量查询并更新实体数据
- 03将核心逻辑与视图层分离
- 04在 React 中订阅状态变化
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ pmndrs/ koota --skill koota安装后在 Claude Code 中输入/koota确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/architecture.mdqueries.mdreact-hooks.mdreact-patterns.mdrelations.mdruntime.md详细介绍
Koota ECS
Koota 使用具有可组合特征的实体来管理状态。
术语表
- Entity - 指向由特征定义的数据的唯一标识符。从世界中生成。
- Trait - 可复用的数据定义。可以基于架构(SoA)、基于回调(AoS)或作为标签。
- Relation - 实体之间的有向连接,用于构建图。
- World - 所有实体及其数据(特征)的上下文。
- Archetype - 实体共享的特征唯一组合。
- Query - 获取符合某个原型的实体。批量更新状态的主要方式。
- Action - 离散、同步的数据变更(创建、更新、销毁)。可在任何调用位置复用。
- System - 响应式协调器,用于观察状态变化并协调工作,包括异步工作流。在帧循环或事件回调中运行。
设计原则
面向数据
行为与数据分离。数据定义为特征,实体由特征组合而成,系统通过查询来变更特征上的数据。完整示例请参阅 基本用法。
可组合的系统
将系统设计为小型、单一用途的单元,而不是按顺序完成所有工作的单体函数。每个系统都应处理一个方面,以便能够独立启用或停用行为。
// Good: Composable systems - each can be enabled/disabled independently
function applyVelocity(world: World) {}
function applyGravity(world: World) {}
function applyFriction(world: World) {}
function syncToDOM(world: World) {}
// Bad: Monolithic system - can't disable gravity without disabling everything
function updatePhysicsAndRender(world: World) {
// velocity, gravity, friction, DOM sync all in one function
}
这样可以支持功能标志、调试(停用某个系统以隔离问题)以及灵活的运行时配置。
将视图与逻辑解耦
将核心状态和逻辑(“core”)与视图(“app”)分离:
- 独立于渲染运行逻辑
- 在保留状态的同时切换视图(2D ↔ 3D)
- 在工作线程或服务端运行逻辑
优先使用特征和动作,而非类
尽量不要使用类来封装数据和行为。使用特征表示数据,使用动作表示行为。只有在外部库要求时(例如 THREE.js 对象),或用户更偏好使用类时,才使用类。
目录结构
如果用户有偏好的结构,请遵循该结构。否则,请参考以下建议:目录结构应与应用的数据模型组织方式相对应。将核心状态/逻辑与视图层分开:
- 核心 - 纯 TypeScript。包括特征、系统、动作和世界。不导入视图。
- 视图 - 从世界读取数据,通过动作进行变更。按领域/功能组织。
src/
├── core/ # Pure TypeScript, no view imports
│ ├── traits/
│ ├── systems/
│ ├── actions/
│ └── world.ts
└── features/ # View layer, organized by domain
文件按角色而非功能切片组织。特征和系统具有可组合性,无法清晰地映射到各个功能。
有关详细模式和 monorepo 结构,请参阅 references/architecture.md。
特征类型
| 类型 | 语法 | 适用场景 | 示例 |
|---|---|---|---|
| SoA(架构) | trait({ x: 0 }) | 简单的基本数据 | Position、Velocity、Health |
| AoS(回调) | trait(() => new Thing()) | 复杂对象/实例 | Ref(DOM)、Keyboard(Set) |
| 标签 | trait() | 无数据,仅用作标记 | IsPlayer、IsEnemy、IsDead |
特征命名约定
| 类型 | 模式 | 示例 |
|---|---|---|
| 标签 | 以 Is 开头 | IsPlayer、IsEnemy、IsDead |
| 关系 | 介词形式 | ChildOf、HeldBy、Contains |
| 特征 | 名词 | Position、Velocity、Health |
关系
关系用于在实体之间构建图,例如层级结构、库存和目标关系。
import { relation, trait } from 'koota'
const ChildOf = relation({ autoDestroy: 'orphan' }) // Hierarchy
const Contains = relation({ store: { amount: 0 } }) // With data
const Targeting = relation({ exclusive: true }) // One target only
// Build graph
const parent = world.spawn()
const child = world.spawn(ChildOf(parent))
const gold = world.spawn()
const silver = world.spawn()
const inventory = world.spawn(Contains(gold), Contains(silver))
// Query children of parent
const children = world.query(ChildOf(parent))
// Query all entities with any ChildOf relation
const allChildren = world.query(ChildOf('*'))
// Query relation targets
const targets = inventory.targetsFor(Contains)
// Filter by traits on the target entity
const IsRare = trait()
silver.add(IsRare)
// Target filters can use any legal query, not just a single trait
const rareInventories = world.query(Contains(IsRare))
// Get targets from entity
const items = inventory.targetsFor(Contains) // Entity[]
const target = child.targetFor(ChildOf) // Entity | undefined
有关详细模式、遍历、有序关系和反模式,请参阅 references/relations.md。
基本用法
import { trait, createWorld } from 'koota'
// 1. Define traits
const Position = trait({ x: 0, y: 0 })
const Velocity = trait({ x: 0, y: 0 })
const IsPlayer = trait()
// 2. Create world and spawn entities
const world = createWorld()
const player = world.spawn(Position({ x: 100, y: 50 }), Velocity, IsPlayer)
// 3. Query and update
world.query(Position, Velocity).updateEach(([pos, vel]) => {
pos.x += vel.x
pos.y += vel.y
})
实体
实体是由特征组成的唯一标识符。它们从世界中生成。
// Spawn
const entity = world.spawn(Position, Velocity)
// Read/write traits
entity.get(Position) // Read trait data
entity.set(Position, { x: 10 }) // Write (triggers change events)
entity.add(IsPlayer) // Add trait
entity.remove(Velocity) // Remove trait
entity.has(Position) // Check if has trait
// Destroy
entity.destroy()
实体 ID
实体在内部是一个数字,其中打包了实体 ID、代际 ID(用于回收)和世界 ID。可以安全地直接存储,以用于持久化或网络传输。
entity.id() // Just the entity ID (reused after destroy)
entity // Full packed number (unique forever)
类型标注
使用 TraitRecord 获取 entity.get() 返回值的类型
type PositionRecord = TraitRecord<typeof Position>
查询
查询会获取符合某种原型的实体,是批量更新状态的主要方式。
// Query and update
world.query(Position, Velocity).updateEach(([pos, vel]) => {
pos.x += vel.x
pos.y += vel.y
})
// Read-only iteration (no write-back)
const data: Array<{ x: number; y: number }> = []
world.query(Position, Velocity).readEach(([pos, vel]) => {
data.push({ x: pos.x, y: pos.y })
})
// Get first match
const player = world.queryFirst(IsPlayer, Position)
// Filter with modifiers
world.query(Position, Not(Velocity)) // Has Position but not Velocity
world.query(Or(IsPlayer, IsEnemy)) // Has either trait
对于包含数据的查询,优先使用 updateEach/readEach,而不是 for...of + entity.get()。readEach 仍会将实体作为第二个参数提供。
注意: updateEach/readEach 仅返回包含数据的特征(SoA/AoS)。标签、Not() 和关系筛选条件不包含在内:
world.query(IsPlayer, Position, Velocity).updateEach(([pos, vel]) => {
// Array has 2 elements - IsPlayer (tag) excluded
})
有关变更跟踪、查询缓存和高级模式,请参阅 references/queries.md。
React 集成
导入: 从 'koota' 导入核心类型(World、Entity)。从 'koota/react' 导入 React 钩子。
变更检测: entity.set() 和 world.set() 会触发变更事件,使 useTrait 等钩子重新渲染。对于直接修改对象的 AoS 特征,请手动调用 entity.changed(Trait) 发出变更信号。
有关 React 钩子和操作,请参阅 references/react-hooks.md。
有关组件模式(App、Startup、Renderer、视图同步、输入),请参阅 references/react-patterns.md。
运行时
系统会查询世界并更新实体。通过帧循环(持续运行)或事件处理程序(离散运行)来运行系统。
有关系统、帧循环、事件驱动模式和时间管理,请参阅 references/runtime.md。