194 lines
5.6 KiB
Markdown
194 lines
5.6 KiB
Markdown
# OpenSpec Change: enterprise-gaming-ecs
|
|
|
|
- **Change ID**: enterprise-gaming-ecs
|
|
- **Package**: `@suaveplan/ecs`
|
|
- **Category**: gaming/engine
|
|
- **Target path**: `gaming/engine/ecs`
|
|
- **Status**: PROPOSED
|
|
- **Date**: 2026-04-24
|
|
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/ecs.md`
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
Deliver `@suaveplan/ecs` on the genesis branch from scratch at enterprise quality. The package provides an archetype-based Entity Component System with bitset queries — the foundational runtime for all gameplay systems. Entities are integer IDs; components are plain typed structs stored in contiguous typed arrays per archetype; queries filter archetypes by bitset AND/NOT masks for O(1) iteration. Genesis adds structured errors via `@suaveplan/error`, Zod schemas for all component descriptors, OTel span instrumentation on world tick and query execution, injectable clock and RNG, `AbortSignal` for async tick cancellation, and property-based tests with fast-check.
|
|
|
|
---
|
|
|
|
## Current-state audit (genesis)
|
|
|
|
Package does not exist on genesis yet — fresh build. No prior source, no test results, no coverage data.
|
|
|
|
---
|
|
|
|
## Available workspace deps on genesis
|
|
|
|
| Package | Role |
|
|
|---|---|
|
|
| `@suaveplan/error` (`workspace:^`) | Base `EcsError`; stable `ECS.*` error codes |
|
|
| `@suaveplan/types` (`workspace:^`) | `Clock`, `IdGenerator`, `LoggerLike` |
|
|
| `@suaveplan/events` (`workspace:^`) | Entity lifecycle events (spawn, despawn, component added/removed) |
|
|
| `zod` (`catalog:forms`) | Component descriptor schemas, query option schemas |
|
|
| `@opentelemetry/api` (`catalog:observability`) | Spans on `world.tick`, `query.run`, `world.spawn` |
|
|
|
|
---
|
|
|
|
## Public API surface
|
|
|
|
### Core
|
|
|
|
```ts
|
|
// World factory
|
|
function createWorld(options?: WorldOptions): World
|
|
|
|
interface WorldOptions {
|
|
maxEntities?: number // default 65536
|
|
clock?: Clock
|
|
logger?: LoggerLike
|
|
}
|
|
|
|
interface World {
|
|
spawn(components?: ComponentInit[]): Entity
|
|
despawn(entity: Entity): void
|
|
addComponent<T>(entity: Entity, descriptor: ComponentDescriptor<T>, value: T): void
|
|
removeComponent(entity: Entity, type: ComponentType): void
|
|
hasComponent(entity: Entity, type: ComponentType): boolean
|
|
getComponent<T>(entity: Entity, descriptor: ComponentDescriptor<T>): T | undefined
|
|
query(mask: QueryMask): QueryResult
|
|
tick(dt: number, signal?: AbortSignal): void
|
|
registerSystem(system: System): void
|
|
destroy(): void
|
|
}
|
|
|
|
type Entity = number & { readonly __brand: 'Entity' }
|
|
|
|
// Component descriptor registry
|
|
function defineComponent<T>(name: string, schema: ZodType<T>): ComponentDescriptor<T>
|
|
|
|
interface ComponentDescriptor<T> {
|
|
readonly type: ComponentType
|
|
readonly name: string
|
|
readonly schema: ZodType<T>
|
|
}
|
|
|
|
type ComponentType = number & { readonly __brand: 'ComponentType' }
|
|
|
|
// Query API
|
|
interface QueryMask {
|
|
all?: ComponentType[]
|
|
none?: ComponentType[]
|
|
any?: ComponentType[]
|
|
}
|
|
|
|
interface QueryResult {
|
|
entities: readonly Entity[]
|
|
size: number
|
|
forEach(cb: (entity: Entity) => void): void
|
|
}
|
|
|
|
// System API
|
|
interface System {
|
|
readonly name: string
|
|
update(world: World, dt: number): void
|
|
}
|
|
|
|
// Archetype internals (exposed for advanced consumers)
|
|
interface Archetype {
|
|
readonly mask: bigint
|
|
readonly entities: readonly Entity[]
|
|
readonly size: number
|
|
}
|
|
```
|
|
|
|
### Error codes
|
|
|
|
```ts
|
|
type EcsErrorCode =
|
|
| 'ECS_ENTITY_NOT_FOUND'
|
|
| 'ECS_COMPONENT_NOT_REGISTERED'
|
|
| 'ECS_MAX_ENTITIES_EXCEEDED'
|
|
| 'ECS_INVALID_QUERY'
|
|
| 'ECS_WORLD_DESTROYED'
|
|
```
|
|
|
|
---
|
|
|
|
## New additions for genesis
|
|
|
|
1. **Bitset queries via BigInt masks** — archetype mask stored as `bigint`; AND/NOT/ANY evaluated with bitwise ops for O(1) archetype filtering.
|
|
2. **Typed array component storage** — Float32Array/Int32Array backing per numeric component type; fallback to generic Map for complex structs.
|
|
3. **OTel instrumentation** — `ecs.tick` span with attribute `ecs.system_count`; `ecs.query` histogram for query execution time; `ecs.entities_alive` gauge.
|
|
4. **Reactive entity events** — `@suaveplan/events` bus fires `ecs:spawn`, `ecs:despawn`, `ecs:component:add`, `ecs:component:remove`.
|
|
5. **Property-based tests** — fast-check: entity count monotonicity, query result consistency with exhaustive scan, archetype mask algebra.
|
|
6. **Benchmark suite** — `__benchmarks__/ecs.bench.ts`: 10k entity spawn, 100k query iteration, 60fps tick with 10 systems.
|
|
|
|
---
|
|
|
|
## File layout
|
|
|
|
```text
|
|
gaming/engine/ecs/
|
|
package.json
|
|
tsconfig.json
|
|
vitest.config.ts
|
|
src/
|
|
index.ts
|
|
error-codes.ts
|
|
constants/
|
|
index.ts
|
|
types/
|
|
common.ts
|
|
common.md
|
|
core/
|
|
world/
|
|
world.ts
|
|
world.md
|
|
world.types.ts
|
|
world.test.ts
|
|
index.ts
|
|
archetype/
|
|
archetype.ts
|
|
archetype.md
|
|
archetype.types.ts
|
|
archetype.test.ts
|
|
index.ts
|
|
component/
|
|
component.ts
|
|
component.md
|
|
component.types.ts
|
|
component.test.ts
|
|
index.ts
|
|
query/
|
|
query.ts
|
|
query.md
|
|
query.types.ts
|
|
query.test.ts
|
|
index.ts
|
|
system/
|
|
system.ts
|
|
system.md
|
|
system.types.ts
|
|
system.test.ts
|
|
index.ts
|
|
utils/
|
|
bitset.ts
|
|
bitset.md
|
|
bitset.test.ts
|
|
__benchmarks__/
|
|
ecs.bench.ts
|
|
tests/
|
|
integration.test.ts
|
|
property.test.ts
|
|
```
|
|
|
|
---
|
|
|
|
## Impact
|
|
|
|
**Downstream**: `@suaveplan/game-loop` orchestrates world ticks; `@suaveplan/r3f-*` packages read ECS component state for rendering. No existing genesis package currently depends on it.
|
|
|
|
**Breaking changes from main**: none — fresh build.
|
|
|
|
**Prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events` published to Verdaccio.
|