更新v2.2.0文档
This commit is contained in:
@@ -63,14 +63,14 @@ class Health extends Component {
|
||||
- 框架能正确管理组件注册
|
||||
|
||||
```typescript
|
||||
// ✅ 正确的用法
|
||||
// 正确的用法
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// ❌ 错误的用法 - 没有装饰器
|
||||
// 错误的用法 - 没有装饰器
|
||||
class BadComponent extends Component {
|
||||
// 这样定义的组件可能在生产环境出现问题
|
||||
}
|
||||
@@ -90,7 +90,7 @@ class ExampleComponent extends Component {
|
||||
* 用于初始化资源、建立引用等
|
||||
*/
|
||||
onAddedToEntity(): void {
|
||||
console.log(`组件 ${this.constructor.name} 被添加到实体 ${this.entity.name}`);
|
||||
console.log(`组件 ${this.constructor.name} 已添加,实体ID: ${this.entityId}`);
|
||||
this.resource = new SomeResource();
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ class ExampleComponent extends Component {
|
||||
* 用于清理资源、断开引用等
|
||||
*/
|
||||
onRemovedFromEntity(): void {
|
||||
console.log(`组件 ${this.constructor.name} 从实体 ${this.entity.name} 移除`);
|
||||
console.log(`组件 ${this.constructor.name} 已移除`);
|
||||
if (this.resource) {
|
||||
this.resource.cleanup();
|
||||
this.resource = null;
|
||||
@@ -108,30 +108,58 @@ class ExampleComponent extends Component {
|
||||
}
|
||||
```
|
||||
|
||||
## 访问实体
|
||||
## 组件与实体的关系
|
||||
|
||||
组件可以通过 `this.entity` 访问其所属的实体:
|
||||
组件存储了所属实体的ID (`entityId`),而不是直接引用实体对象。这是ECS数据导向设计的体现,避免了循环引用。
|
||||
|
||||
在实际使用中,**应该在 System 中处理实体和组件的交互**,而不是在组件内部:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Damage')
|
||||
class Damage extends Component {
|
||||
damage: number;
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number;
|
||||
max: number;
|
||||
|
||||
constructor(damage: number) {
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.damage = damage;
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
|
||||
// 在组件方法中访问实体和其他组件
|
||||
applyDamage(): void {
|
||||
const health = this.entity.getComponent(Health);
|
||||
if (health) {
|
||||
health.takeDamage(this.damage);
|
||||
isDead(): boolean {
|
||||
return this.current <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Damage')
|
||||
class Damage extends Component {
|
||||
value: number;
|
||||
|
||||
constructor(value: number) {
|
||||
super();
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 推荐:在 System 中处理逻辑
|
||||
class DamageSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Health, Damage));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health)!;
|
||||
const damage = entity.getComponent(Damage)!;
|
||||
|
||||
health.current -= damage.value;
|
||||
|
||||
// 如果生命值为0,销毁实体
|
||||
if (health.isDead()) {
|
||||
this.entity.destroy();
|
||||
entity.destroy();
|
||||
}
|
||||
|
||||
// 应用伤害后移除 Damage 组件
|
||||
entity.removeComponent(damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,9 +174,27 @@ class Damage extends Component {
|
||||
class ExampleComponent extends Component {
|
||||
someData: string = "example";
|
||||
|
||||
showComponentInfo(): void {
|
||||
console.log(`组件ID: ${this.id}`); // 唯一的组件ID
|
||||
console.log(`所属实体: ${this.entity.name}`); // 所属实体引用
|
||||
onAddedToEntity(): void {
|
||||
console.log(`组件ID: ${this.id}`); // 唯一的组件ID
|
||||
console.log(`所属实体ID: ${this.entityId}`); // 所属实体的ID
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果需要访问实体对象,应该在 System 中进行:
|
||||
|
||||
```typescript
|
||||
class ExampleSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(ExampleComponent));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const comp = entity.getComponent(ExampleComponent)!;
|
||||
console.log(`实体名称: ${entity.name}`);
|
||||
console.log(`组件数据: ${comp.someData}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -245,7 +291,7 @@ class WeaponConfig extends Component {
|
||||
### 1. 保持组件简单
|
||||
|
||||
```typescript
|
||||
// ✅ 好的组件设计 - 单一职责
|
||||
// 好的组件设计 - 单一职责
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
@@ -258,7 +304,7 @@ class Velocity extends Component {
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// ❌ 避免的组件设计 - 职责过多
|
||||
// 避免的组件设计 - 职责过多
|
||||
@ECSComponent('GameObject')
|
||||
class GameObject extends Component {
|
||||
x: number;
|
||||
@@ -330,16 +376,11 @@ class Inventory extends Component {
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 避免在组件中存储实体引用
|
||||
### 4. 引用其他实体
|
||||
|
||||
当组件需要关联其他实体时(如父子关系、跟随目标等),**推荐方式是存储实体ID**,然后在 System 中查找:
|
||||
|
||||
```typescript
|
||||
// ❌ 避免:在组件中存储其他实体的引用
|
||||
@ECSComponent('BadFollower')
|
||||
class BadFollower extends Component {
|
||||
target: Entity; // 直接引用可能导致内存泄漏
|
||||
}
|
||||
|
||||
// ✅ 推荐:存储实体ID,通过场景查找
|
||||
@ECSComponent('Follower')
|
||||
class Follower extends Component {
|
||||
targetId: number;
|
||||
@@ -349,11 +390,269 @@ class Follower extends Component {
|
||||
super();
|
||||
this.targetId = targetId;
|
||||
}
|
||||
}
|
||||
|
||||
getTarget(): Entity | null {
|
||||
return this.entity.scene?.findEntityById(this.targetId) || null;
|
||||
// 在 System 中查找目标实体并处理逻辑
|
||||
class FollowerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Follower, Position));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const follower = entity.getComponent(Follower)!;
|
||||
const position = entity.getComponent(Position)!;
|
||||
|
||||
// 通过场景查找目标实体
|
||||
const target = entity.scene?.findEntityById(follower.targetId);
|
||||
if (target) {
|
||||
const targetPos = target.getComponent(Position);
|
||||
if (targetPos) {
|
||||
// 跟随逻辑
|
||||
const dx = targetPos.x - position.x;
|
||||
const dy = targetPos.y - position.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance > follower.followDistance) {
|
||||
// 移动靠近目标
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这种方式的优势:
|
||||
- 组件保持简单,只存储基本数据类型
|
||||
- 符合数据导向设计
|
||||
- 在 System 中统一处理查找和逻辑
|
||||
- 易于理解和维护
|
||||
|
||||
**避免在组件中直接存储实体引用**:
|
||||
|
||||
```typescript
|
||||
// 错误示范:直接存储实体引用
|
||||
@ECSComponent('BadFollower')
|
||||
class BadFollower extends Component {
|
||||
target: Entity; // 实体销毁后仍持有引用,可能导致内存泄漏
|
||||
}
|
||||
```
|
||||
|
||||
## 高级特性
|
||||
|
||||
### EntityRef 装饰器 - 自动引用追踪
|
||||
|
||||
框架提供了 `@EntityRef` 装饰器用于**特殊场景**下安全地存储实体引用。这是一个高级特性,一般情况下推荐使用存储ID的方式。
|
||||
|
||||
#### 什么时候需要 EntityRef?
|
||||
|
||||
在以下场景中,`@EntityRef` 可以简化代码:
|
||||
|
||||
1. **父子关系**: 需要在组件中直接访问父实体或子实体
|
||||
2. **复杂关联**: 实体之间有多个引用关系
|
||||
3. **频繁访问**: 需要在多处访问引用的实体,使用ID查找会有性能开销
|
||||
|
||||
#### 核心特性
|
||||
|
||||
`@EntityRef` 装饰器通过 **ReferenceTracker** 自动追踪引用关系:
|
||||
|
||||
- 当被引用的实体销毁时,所有指向它的 `@EntityRef` 属性自动设为 `null`
|
||||
- 防止跨场景引用(会输出警告并拒绝设置)
|
||||
- 防止引用已销毁的实体(会输出警告并设为 `null`)
|
||||
- 使用 WeakRef 避免内存泄漏(自动GC支持)
|
||||
- 组件移除时自动清理引用注册
|
||||
|
||||
#### 基本用法
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, EntityRef, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Parent')
|
||||
class ParentComponent extends Component {
|
||||
@EntityRef()
|
||||
parent: Entity | null = null;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const scene = new Scene();
|
||||
const parent = scene.createEntity('Parent');
|
||||
const child = scene.createEntity('Child');
|
||||
|
||||
const comp = child.addComponent(new ParentComponent());
|
||||
comp.parent = parent;
|
||||
|
||||
console.log(comp.parent); // Entity { name: 'Parent' }
|
||||
|
||||
// 当 parent 被销毁时,comp.parent 自动变为 null
|
||||
parent.destroy();
|
||||
console.log(comp.parent); // null
|
||||
```
|
||||
|
||||
#### 多个引用属性
|
||||
|
||||
一个组件可以有多个 `@EntityRef` 属性:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Combat')
|
||||
class CombatComponent extends Component {
|
||||
@EntityRef()
|
||||
target: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
ally: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
lastAttacker: Entity | null = null;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const player = scene.createEntity('Player');
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
const npc = scene.createEntity('NPC');
|
||||
|
||||
const combat = player.addComponent(new CombatComponent());
|
||||
combat.target = enemy;
|
||||
combat.ally = npc;
|
||||
|
||||
// enemy 销毁后,只有 target 变为 null,ally 仍然有效
|
||||
enemy.destroy();
|
||||
console.log(combat.target); // null
|
||||
console.log(combat.ally); // Entity { name: 'NPC' }
|
||||
```
|
||||
|
||||
#### 安全检查
|
||||
|
||||
`@EntityRef` 提供了多重安全检查:
|
||||
|
||||
```typescript
|
||||
const scene1 = new Scene();
|
||||
const scene2 = new Scene();
|
||||
|
||||
const entity1 = scene1.createEntity('Entity1');
|
||||
const entity2 = scene2.createEntity('Entity2');
|
||||
|
||||
const comp = entity1.addComponent(new ParentComponent());
|
||||
|
||||
// 跨场景引用会失败
|
||||
comp.parent = entity2; // 输出错误日志,comp.parent 为 null
|
||||
console.log(comp.parent); // null
|
||||
|
||||
// 引用已销毁的实体会失败
|
||||
const entity3 = scene1.createEntity('Entity3');
|
||||
entity3.destroy();
|
||||
comp.parent = entity3; // 输出警告日志,comp.parent 为 null
|
||||
console.log(comp.parent); // null
|
||||
```
|
||||
|
||||
#### 实现原理
|
||||
|
||||
`@EntityRef` 使用以下机制实现自动引用追踪:
|
||||
|
||||
1. **ReferenceTracker**: Scene 持有一个引用追踪器,记录所有实体引用关系
|
||||
2. **WeakRef**: 使用弱引用存储组件,避免循环引用导致内存泄漏
|
||||
3. **属性拦截**: 通过 `Object.defineProperty` 拦截 getter/setter
|
||||
4. **自动清理**: 实体销毁时,ReferenceTracker 遍历所有引用并设为 null
|
||||
|
||||
```typescript
|
||||
// 简化的实现原理
|
||||
class ReferenceTracker {
|
||||
// entityId -> 引用该实体的所有组件记录
|
||||
private _references: Map<number, Set<{ component: WeakRef<Component>, propertyKey: string }>>;
|
||||
|
||||
// 实体销毁时调用
|
||||
clearReferencesTo(entityId: number): void {
|
||||
const records = this._references.get(entityId);
|
||||
if (records) {
|
||||
for (const record of records) {
|
||||
const component = record.component.deref();
|
||||
if (component) {
|
||||
// 将组件的引用属性设为 null
|
||||
(component as any)[record.propertyKey] = null;
|
||||
}
|
||||
}
|
||||
this._references.delete(entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 性能考虑
|
||||
|
||||
`@EntityRef` 会带来一些性能开销:
|
||||
|
||||
- **写入开销**: 每次设置引用时需要更新 ReferenceTracker
|
||||
- **内存开销**: ReferenceTracker 需要维护引用映射表
|
||||
- **销毁开销**: 实体销毁时需要遍历所有引用并清理
|
||||
|
||||
对于大多数场景,这些开销是可以接受的。但如果有**大量实体和频繁的引用变更**,存储ID可能更高效。
|
||||
|
||||
#### 最佳实践
|
||||
|
||||
```typescript
|
||||
// 推荐:适合使用 @EntityRef 的场景 - 父子关系
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
@EntityRef()
|
||||
parent: Entity | null = null;
|
||||
|
||||
position: { x: number, y: number } = { x: 0, y: 0 };
|
||||
|
||||
// 可以直接访问父实体的组件
|
||||
getWorldPosition(): { x: number, y: number } {
|
||||
if (!this.parent) {
|
||||
return { ...this.position };
|
||||
}
|
||||
|
||||
const parentTransform = this.parent.getComponent(Transform);
|
||||
if (parentTransform) {
|
||||
const parentPos = parentTransform.getWorldPosition();
|
||||
return {
|
||||
x: parentPos.x + this.position.x,
|
||||
y: parentPos.y + this.position.y
|
||||
};
|
||||
}
|
||||
|
||||
return { ...this.position };
|
||||
}
|
||||
}
|
||||
|
||||
// 不推荐:不适合使用 @EntityRef 的场景 - 大量动态目标
|
||||
@ECSComponent('AITarget')
|
||||
class AITarget extends Component {
|
||||
@EntityRef()
|
||||
target: Entity | null = null; // 如果目标频繁变化,用ID更好
|
||||
|
||||
updateCooldown: number = 0;
|
||||
}
|
||||
|
||||
// 推荐:这种场景用ID更好
|
||||
@ECSComponent('AITarget')
|
||||
class AITargetBetter extends Component {
|
||||
targetId: number | null = null; // 存储ID
|
||||
updateCooldown: number = 0;
|
||||
}
|
||||
```
|
||||
|
||||
#### 调试支持
|
||||
|
||||
ReferenceTracker 提供了调试接口:
|
||||
|
||||
```typescript
|
||||
// 查看某个实体被哪些组件引用
|
||||
const references = scene.referenceTracker.getReferencesTo(entity.id);
|
||||
console.log(`实体 ${entity.name} 被 ${references.length} 个组件引用`);
|
||||
|
||||
// 获取完整的调试信息
|
||||
const debugInfo = scene.referenceTracker.getDebugInfo();
|
||||
console.log(debugInfo);
|
||||
```
|
||||
|
||||
#### 总结
|
||||
|
||||
- **推荐做法**: 大部分情况使用存储ID + System查找的方式
|
||||
- **EntityRef 适用场景**: 父子关系、复杂关联、组件内需要直接访问引用实体的场景
|
||||
- **核心优势**: 自动清理、防止悬空引用、代码更简洁
|
||||
- **注意事项**: 有性能开销,不适合大量动态引用的场景
|
||||
|
||||
组件是 ECS 架构的数据载体,正确设计组件能让你的游戏代码更模块化、可维护和高性能。
|
||||
Reference in New Issue
Block a user