diff --git a/docs/guide/component.md b/docs/guide/component.md index f4d41641..dd42209b 100644 --- a/docs/guide/component.md +++ b/docs/guide/component.md @@ -55,25 +55,92 @@ class Health extends Component { } ``` -### 组件装饰器 +### @ECSComponent 装饰器 -**必须使用 `@ECSComponent` 装饰器**,这确保了: -- 组件在代码混淆后仍能正确识别 -- 提供稳定的类型名称用于序列化和调试 -- 框架能正确管理组件注册 +`@ECSComponent` 是组件类必须使用的装饰器,它为组件提供了类型标识和元数据管理。 + +#### 为什么必须使用 + +| 功能 | 说明 | +|------|------| +| **类型识别** | 提供稳定的类型名称,代码混淆后仍能正确识别 | +| **序列化支持** | 序列化/反序列化时使用该名称作为类型标识 | +| **组件注册** | 自动注册到 ComponentRegistry,分配唯一的位掩码 | +| **调试支持** | 在调试工具和日志中显示可读的组件名称 | + +#### 基本语法 ```typescript -// 正确的用法 +@ECSComponent(typeName: string) +``` + +- `typeName`: 组件的类型名称,建议使用与类名相同或相近的名称 + +#### 使用示例 + +```typescript +// ✅ 正确的用法 @ECSComponent('Velocity') class Velocity extends Component { dx: number = 0; dy: number = 0; } -// 错误的用法 - 没有装饰器 -class BadComponent extends Component { - // 这样定义的组件可能在生产环境出现问题 +// ✅ 推荐:类型名与类名保持一致 +@ECSComponent('PlayerController') +class PlayerController extends Component { + speed: number = 5; } + +// ❌ 错误的用法 - 没有装饰器 +class BadComponent extends Component { + // 这样定义的组件可能在生产环境出现问题: + // 1. 代码压缩后类名变化,无法正确序列化 + // 2. 组件未注册到框架,查询和匹配可能失效 +} +``` + +#### 与 @Serializable 配合使用 + +当组件需要支持序列化时,`@ECSComponent` 和 `@Serializable` 需要一起使用: + +```typescript +import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework'; + +@ECSComponent('Player') +@Serializable({ version: 1 }) +class PlayerComponent extends Component { + @Serialize() + name: string = ''; + + @Serialize() + level: number = 1; + + // 不使用 @Serialize() 的字段不会被序列化 + private _cachedData: any = null; +} +``` + +> **注意**:`@ECSComponent` 的 `typeName` 和 `@Serializable` 的 `typeId` 可以不同。如果 `@Serializable` 没有指定 `typeId`,则默认使用 `@ECSComponent` 的 `typeName`。 + +#### 组件类型名的唯一性 + +每个组件的类型名应该是唯一的: + +```typescript +// ❌ 错误:两个组件使用相同的类型名 +@ECSComponent('Health') +class HealthComponent extends Component { } + +@ECSComponent('Health') // 冲突! +class EnemyHealthComponent extends Component { } + +// ✅ 正确:使用不同的类型名 +@ECSComponent('PlayerHealth') +class PlayerHealthComponent extends Component { } + +@ECSComponent('EnemyHealth') +class EnemyHealthComponent extends Component { } ``` ## 组件生命周期 diff --git a/docs/guide/entity-query.md b/docs/guide/entity-query.md index 020f6907..11523b72 100644 --- a/docs/guide/entity-query.md +++ b/docs/guide/entity-query.md @@ -121,6 +121,65 @@ class CombatSystem extends EntitySystem { } ``` +#### nothing() - 不匹配任何实体 + +用于创建只需要生命周期方法(`onBegin`、`onEnd`)但不需要处理实体的系统。 + +```typescript +class FrameTimerSystem extends EntitySystem { + constructor() { + // 不匹配任何实体 + super(Matcher.nothing()); + } + + protected onBegin(): void { + // 每帧开始时执行 + Performance.markFrameStart(); + } + + protected process(entities: readonly Entity[]): void { + // 永远不会被调用,因为没有匹配的实体 + } + + protected onEnd(): void { + // 每帧结束时执行 + Performance.markFrameEnd(); + } +} +``` + +#### empty() vs nothing() 的区别 + +| 方法 | 行为 | 使用场景 | +|------|------|----------| +| `Matcher.empty()` | 匹配**所有**实体 | 需要处理场景中所有实体 | +| `Matcher.nothing()` | 不匹配**任何**实体 | 只需要生命周期回调,不处理实体 | + +```typescript +// empty() - 返回场景中的所有实体 +class AllEntitiesSystem extends EntitySystem { + constructor() { + super(Matcher.empty()); + } + + protected process(entities: readonly Entity[]): void { + // entities 包含场景中的所有实体 + console.log(`场景中共有 ${entities.length} 个实体`); + } +} + +// nothing() - 不返回任何实体 +class NoEntitiesSystem extends EntitySystem { + constructor() { + super(Matcher.nothing()); + } + + protected process(entities: readonly Entity[]): void { + // entities 永远是空数组,此方法不会被调用 + } +} +``` + ### 按标签查询 ```typescript @@ -493,6 +552,65 @@ const matcher2 = matcher.any(VelocityComponent); console.log(matcher === matcher2); // false ``` +## Matcher API 快速参考 + +### 静态创建方法 + +| 方法 | 说明 | 示例 | +|------|------|------| +| `Matcher.all(...types)` | 必须包含所有指定组件 | `Matcher.all(Position, Velocity)` | +| `Matcher.any(...types)` | 至少包含一个指定组件 | `Matcher.any(Health, Shield)` | +| `Matcher.none(...types)` | 不能包含任何指定组件 | `Matcher.none(Dead)` | +| `Matcher.byTag(tag)` | 按标签查询 | `Matcher.byTag(1)` | +| `Matcher.byName(name)` | 按名称查询 | `Matcher.byName("Player")` | +| `Matcher.byComponent(type)` | 按单个组件查询 | `Matcher.byComponent(Health)` | +| `Matcher.empty()` | 创建空匹配器(匹配所有实体) | `Matcher.empty()` | +| `Matcher.nothing()` | 不匹配任何实体 | `Matcher.nothing()` | +| `Matcher.complex()` | 创建复杂查询构建器 | `Matcher.complex()` | + +### 链式方法 + +| 方法 | 说明 | 示例 | +|------|------|------| +| `.all(...types)` | 添加必须包含的组件 | `.all(Position)` | +| `.any(...types)` | 添加可选组件(至少一个) | `.any(Weapon, Magic)` | +| `.none(...types)` | 添加排除的组件 | `.none(Dead)` | +| `.exclude(...types)` | `.none()` 的别名 | `.exclude(Disabled)` | +| `.one(...types)` | `.any()` 的别名 | `.one(Player, Enemy)` | +| `.withTag(tag)` | 添加标签条件 | `.withTag(1)` | +| `.withName(name)` | 添加名称条件 | `.withName("Boss")` | +| `.withComponent(type)` | 添加单组件条件 | `.withComponent(Health)` | + +### 实用方法 + +| 方法 | 说明 | +|------|------| +| `.getCondition()` | 获取查询条件(只读) | +| `.isEmpty()` | 检查是否为空条件 | +| `.isNothing()` | 检查是否为 nothing 匹配器 | +| `.clone()` | 克隆匹配器 | +| `.reset()` | 重置所有条件 | +| `.toString()` | 获取字符串表示 | + +### 常用组合示例 + +```typescript +// 基础移动系统 +Matcher.all(Position, Velocity) + +// 可攻击的活着的实体 +Matcher.all(Position, Health) + .any(Weapon, Magic) + .none(Dead, Disabled) + +// 所有带标签的敌人 +Matcher.byTag(Tags.ENEMY) + .all(AIComponent) + +// 只需要生命周期的系统 +Matcher.nothing() +``` + ## 相关 API - [Matcher](../api/classes/Matcher.md) - 查询条件描述符 API 参考 diff --git a/docs/guide/index.md b/docs/guide/index.md index 8009b745..acba3782 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -13,6 +13,9 @@ ### [系统架构 (System)](./system.md) 掌握系统的编写方法,实现游戏逻辑的处理。 +### [实体查询与 Matcher](./entity-query.md) +学习使用 Matcher 进行实体筛选和查询,掌握 `all`、`any`、`none`、`nothing` 等匹配条件。 + ### [场景管理 (Scene)](./scene.md) 了解场景的生命周期、系统管理和实体容器功能。 diff --git a/docs/guide/serialization.md b/docs/guide/serialization.md index 8be3066b..4c1f4aa9 100644 --- a/docs/guide/serialization.md +++ b/docs/guide/serialization.md @@ -190,6 +190,106 @@ class CollectionsComponent extends Component { } ``` +### 组件继承与序列化 + +框架完整支持组件类的继承,子类会自动继承父类的序列化字段,同时可以添加自己的字段。 + +#### 基础继承 + +```typescript +// 基类组件 +@ECSComponent('Collider2DBase') +@Serializable({ version: 1, typeId: 'Collider2DBase' }) +abstract class Collider2DBase extends Component { + @Serialize() + public friction: number = 0.5; + + @Serialize() + public restitution: number = 0.0; + + @Serialize() + public isTrigger: boolean = false; +} + +// 子类组件 - 自动继承父类的序列化字段 +@ECSComponent('BoxCollider2D') +@Serializable({ version: 1, typeId: 'BoxCollider2D' }) +class BoxCollider2DComponent extends Collider2DBase { + @Serialize() + public width: number = 1.0; + + @Serialize() + public height: number = 1.0; +} + +// 另一个子类组件 +@ECSComponent('CircleCollider2D') +@Serializable({ version: 1, typeId: 'CircleCollider2D' }) +class CircleCollider2DComponent extends Collider2DBase { + @Serialize() + public radius: number = 0.5; +} +``` + +#### 继承规则 + +1. **字段继承**:子类自动继承父类所有被 `@Serialize()` 标记的字段 +2. **独立元数据**:每个子类维护独立的序列化元数据,修改子类不会影响父类或其他子类 +3. **typeId 区分**:使用 `typeId` 选项为每个类指定唯一标识,确保反序列化时能正确识别组件类型 + +#### 使用 typeId 的重要性 + +当使用组件继承时,**强烈建议**为每个类设置唯一的 `typeId`: + +```typescript +// ✅ 推荐:明确指定 typeId +@Serializable({ version: 1, typeId: 'BoxCollider2D' }) +class BoxCollider2DComponent extends Collider2DBase { } + +@Serializable({ version: 1, typeId: 'CircleCollider2D' }) +class CircleCollider2DComponent extends Collider2DBase { } + +// ⚠️ 不推荐:依赖类名作为 typeId +// 在代码压缩后类名可能变化,导致反序列化失败 +@Serializable({ version: 1 }) +class BoxCollider2DComponent extends Collider2DBase { } +``` + +#### 子类覆盖父类字段 + +子类可以重新声明父类的字段以修改其序列化选项: + +```typescript +@ECSComponent('SpecialCollider') +@Serializable({ version: 1, typeId: 'SpecialCollider' }) +class SpecialColliderComponent extends Collider2DBase { + // 覆盖父类字段,使用不同的别名 + @Serialize({ alias: 'fric' }) + public override friction: number = 0.8; + + @Serialize() + public specialProperty: string = ''; +} +``` + +#### 忽略继承的字段 + +使用 `@IgnoreSerialization()` 可以在子类中忽略从父类继承的字段: + +```typescript +@ECSComponent('TriggerOnly') +@Serializable({ version: 1, typeId: 'TriggerOnly' }) +class TriggerOnlyCollider extends Collider2DBase { + // 忽略父类的 friction 和 restitution 字段 + // 因为 Trigger 不需要物理材质属性 + @IgnoreSerialization() + public override friction: number = 0; + + @IgnoreSerialization() + public override restitution: number = 0; +} +``` + ### 场景自定义数据 除了实体和组件,还可以序列化场景级别的配置数据: diff --git a/docs/guide/system.md b/docs/guide/system.md index 180f01ae..50eb1c61 100644 --- a/docs/guide/system.md +++ b/docs/guide/system.md @@ -157,8 +157,45 @@ const nameMatcher = Matcher.byName("Player"); // 匹配名称为 "Player" 的实 // 单组件匹配 const componentMatcher = Matcher.byComponent(Health); // 匹配拥有 Health 组件的实体 + +// 不匹配任何实体 +const nothingMatcher = Matcher.nothing(); // 用于只需要生命周期回调的系统 ``` +### 空匹配器 vs Nothing 匹配器 + +```typescript +// empty() - 空条件,匹配所有实体 +const emptyMatcher = Matcher.empty(); + +// nothing() - 不匹配任何实体,用于只需要生命周期方法的系统 +const nothingMatcher = Matcher.nothing(); + +// 使用场景:只需要 onBegin/onEnd 生命周期的系统 +@ECSSystem('FrameTimer') +class FrameTimerSystem extends EntitySystem { + constructor() { + super(Matcher.nothing()); // 不处理任何实体 + } + + protected onBegin(): void { + // 每帧开始时执行,例如:记录帧开始时间 + console.log('帧开始'); + } + + protected process(entities: readonly Entity[]): void { + // 永远不会被调用,因为没有匹配的实体 + } + + protected onEnd(): void { + // 每帧结束时执行 + console.log('帧结束'); + } +} +``` + +> 💡 **提示**:更多关于 Matcher 和实体查询的详细用法,请参考 [实体查询系统](/guide/entity-query) 文档。 + ## 系统生命周期 系统提供了完整的生命周期回调: @@ -563,9 +600,28 @@ class GameSystem extends EntitySystem { } ``` -### 2. 使用装饰器 +### 2. 使用 @ECSSystem 装饰器 -**必须使用 `@ECSSystem` 装饰器**: +`@ECSSystem` 是系统类必须使用的装饰器,它为系统提供类型标识和元数据管理。 + +#### 为什么必须使用 + +| 功能 | 说明 | +|------|------| +| **类型识别** | 提供稳定的系统名称,代码混淆后仍能正确识别 | +| **调试支持** | 在性能监控、日志和调试工具中显示可读的系统名称 | +| **系统管理** | 通过名称查找和管理系统 | +| **序列化支持** | 场景序列化时可以记录系统配置 | + +#### 基本语法 + +```typescript +@ECSSystem(systemName: string) +``` + +- `systemName`: 系统的名称,建议使用描述性的名称 + +#### 使用示例 ```typescript // ✅ 正确的用法 @@ -574,12 +630,41 @@ class PhysicsSystem extends EntitySystem { // 系统实现 } +// ✅ 推荐:使用描述性的名称 +@ECSSystem('PlayerMovement') +class PlayerMovementSystem extends EntitySystem { + constructor() { + super(Matcher.all(Player, Position, Velocity)); + } +} + // ❌ 错误的用法 - 没有装饰器 class BadSystem extends EntitySystem { - // 这样定义的系统可能在生产环境出现问题 + // 这样定义的系统可能在生产环境出现问题: + // 1. 代码压缩后类名变化,无法正确识别 + // 2. 性能监控和调试工具显示不正确的名称 } ``` +#### 系统名称的作用 + +```typescript +@ECSSystem('Combat') +class CombatSystem extends EntitySystem { + protected onInitialize(): void { + // 使用 systemName 属性访问系统名称 + console.log(`系统 ${this.systemName} 已初始化`); // 输出: 系统 Combat 已初始化 + } +} + +// 通过名称查找系统 +const combat = scene.getSystemByName('Combat'); + +// 性能监控中会显示系统名称 +const perfData = combatSystem.getPerformanceData(); +console.log(`${combatSystem.systemName} 执行时间: ${perfData?.executionTime}ms`); +``` + ### 3. 合理的更新顺序 ```typescript