docs: 完善装饰器和 Matcher API 文档
This commit is contained in:
@@ -55,25 +55,92 @@ class Health extends Component {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 组件装饰器
|
### @ECSComponent 装饰器
|
||||||
|
|
||||||
**必须使用 `@ECSComponent` 装饰器**,这确保了:
|
`@ECSComponent` 是组件类必须使用的装饰器,它为组件提供了类型标识和元数据管理。
|
||||||
- 组件在代码混淆后仍能正确识别
|
|
||||||
- 提供稳定的类型名称用于序列化和调试
|
#### 为什么必须使用
|
||||||
- 框架能正确管理组件注册
|
|
||||||
|
| 功能 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **类型识别** | 提供稳定的类型名称,代码混淆后仍能正确识别 |
|
||||||
|
| **序列化支持** | 序列化/反序列化时使用该名称作为类型标识 |
|
||||||
|
| **组件注册** | 自动注册到 ComponentRegistry,分配唯一的位掩码 |
|
||||||
|
| **调试支持** | 在调试工具和日志中显示可读的组件名称 |
|
||||||
|
|
||||||
|
#### 基本语法
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 正确的用法
|
@ECSComponent(typeName: string)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `typeName`: 组件的类型名称,建议使用与类名相同或相近的名称
|
||||||
|
|
||||||
|
#### 使用示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 正确的用法
|
||||||
@ECSComponent('Velocity')
|
@ECSComponent('Velocity')
|
||||||
class Velocity extends Component {
|
class Velocity extends Component {
|
||||||
dx: number = 0;
|
dx: number = 0;
|
||||||
dy: 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 { }
|
||||||
```
|
```
|
||||||
|
|
||||||
## 组件生命周期
|
## 组件生命周期
|
||||||
|
|||||||
@@ -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
|
```typescript
|
||||||
@@ -493,6 +552,65 @@ const matcher2 = matcher.any(VelocityComponent);
|
|||||||
console.log(matcher === matcher2); // false
|
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
|
## 相关 API
|
||||||
|
|
||||||
- [Matcher](../api/classes/Matcher.md) - 查询条件描述符 API 参考
|
- [Matcher](../api/classes/Matcher.md) - 查询条件描述符 API 参考
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
### [系统架构 (System)](./system.md)
|
### [系统架构 (System)](./system.md)
|
||||||
掌握系统的编写方法,实现游戏逻辑的处理。
|
掌握系统的编写方法,实现游戏逻辑的处理。
|
||||||
|
|
||||||
|
### [实体查询与 Matcher](./entity-query.md)
|
||||||
|
学习使用 Matcher 进行实体筛选和查询,掌握 `all`、`any`、`none`、`nothing` 等匹配条件。
|
||||||
|
|
||||||
### [场景管理 (Scene)](./scene.md)
|
### [场景管理 (Scene)](./scene.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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### 场景自定义数据
|
### 场景自定义数据
|
||||||
|
|
||||||
除了实体和组件,还可以序列化场景级别的配置数据:
|
除了实体和组件,还可以序列化场景级别的配置数据:
|
||||||
|
|||||||
@@ -157,8 +157,45 @@ const nameMatcher = Matcher.byName("Player"); // 匹配名称为 "Player" 的实
|
|||||||
|
|
||||||
// 单组件匹配
|
// 单组件匹配
|
||||||
const componentMatcher = Matcher.byComponent(Health); // 匹配拥有 Health 组件的实体
|
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
|
```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 {
|
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. 合理的更新顺序
|
### 3. 合理的更新顺序
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
|||||||
Reference in New Issue
Block a user