Files
esengine/docs/guide/entity.md

427 lines
11 KiB
Markdown
Raw Normal View History

2025-09-28 12:26:51 +08:00
# 实体类
在 ECS 架构中实体Entity是游戏世界中的基本对象。实体本身不包含游戏逻辑或数据它只是一个容器用来组合不同的组件来实现各种功能。
## 基本概念
实体是一个轻量级的对象,主要用于:
- 作为组件的容器
- 提供唯一标识ID
- 管理组件的生命周期
::: tip 关于父子层级关系
实体间的父子层级关系通过 `HierarchyComponent``HierarchySystem` 管理,而非 Entity 内置属性。这种设计遵循 ECS 组合原则 —— 只有需要层级关系的实体才添加此组件。
详见 [层级系统](./hierarchy.md) 文档。
:::
2025-09-28 12:26:51 +08:00
## 创建实体
**重要提示:实体必须通过场景创建,不支持手动创建!**
实体必须通过场景的 `createEntity()` 方法来创建,这样才能确保:
- 实体被正确添加到场景的实体管理系统中
- 实体被添加到查询系统中,供系统使用
- 实体获得正确的场景引用
- 触发相关的生命周期事件
```typescript
// 正确的方式:通过场景创建实体
const player = scene.createEntity("Player");
// ❌ 错误的方式:手动创建实体
// const entity = new Entity("MyEntity", 1); // 这样创建的实体系统无法管理
```
## 添加组件
实体通过添加组件来获得功能:
```typescript
import { Component, ECSComponent } from '@esengine/ecs-framework';
2025-09-28 12:26:51 +08:00
// 定义位置组件
@ECSComponent('Position')
class Position extends Component {
x: number = 0;
y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
}
// 定义健康组件
@ECSComponent('Health')
class Health extends Component {
current: number = 100;
max: number = 100;
constructor(max: number = 100) {
super();
this.max = max;
this.current = max;
}
}
// 给实体添加组件
const player = scene.createEntity("Player");
player.addComponent(new Position(100, 200));
player.addComponent(new Health(150));
```
## 获取组件
```typescript
// 获取组件(传入组件类,不是实例)
const position = player.getComponent(Position); // 返回 Position | null
const health = player.getComponent(Health); // 返回 Health | null
// 检查组件是否存在
if (position) {
console.log(`玩家位置: x=${position.x}, y=${position.y}`);
}
// 检查是否有某个组件
if (player.hasComponent(Position)) {
console.log("玩家有位置组件");
}
// 获取所有组件实例(只读属性)
const allComponents = player.components; // readonly Component[]
2025-09-28 12:26:51 +08:00
// 获取指定类型的所有组件(支持同类型多组件)
const allHealthComponents = player.getComponents(Health); // Health[]
// 获取或创建组件(如果不存在则自动创建)
const position = player.getOrCreateComponent(Position, 0, 0); // 传入构造参数
const health = player.getOrCreateComponent(Health, 100); // 如果存在则返回现有的,不存在则创建新的
```
## 移除组件
```typescript
// 方式1通过组件类型移除
const removedHealth = player.removeComponentByType(Health);
if (removedHealth) {
console.log("健康组件已被移除");
}
// 方式2通过组件实例移除
const healthComponent = player.getComponent(Health);
if (healthComponent) {
player.removeComponent(healthComponent);
}
// 批量移除多种组件类型
const removedComponents = player.removeComponentsByTypes([Position, Health]);
// 检查组件是否被移除
if (!player.hasComponent(Health)) {
console.log("健康组件已被移除");
}
```
## 实体查找
场景提供了多种方式来查找实体:
### 通过名称查找
```typescript
// 查找单个实体
const player = scene.findEntity("Player");
// 或使用别名方法
const player2 = scene.getEntityByName("Player");
if (player) {
console.log("找到玩家实体");
}
```
### 通过 ID 查找
```typescript
// 通过实体 ID 查找
const entity = scene.findEntityById(123);
```
### 通过标签查找
实体支持标签系统,用于快速分类和查找:
```typescript
// 设置标签
player.tag = 1; // 玩家标签
enemy.tag = 2; // 敌人标签
// 通过标签查找所有相关实体
const players = scene.findEntitiesByTag(1);
const enemies = scene.findEntitiesByTag(2);
// 或使用别名方法
const allPlayers = scene.getEntitiesByTag(1);
```
## 实体生命周期
```typescript
// 销毁实体
player.destroy();
// 检查实体是否已销毁
if (player.isDestroyed) {
console.log("实体已被销毁");
}
```
## 实体事件
实体的组件变化会触发事件:
```typescript
// 监听组件添加事件
scene.eventSystem.on('component:added', (data) => {
console.log('组件已添加:', data);
});
// 监听实体创建事件
scene.eventSystem.on('entity:created', (data) => {
console.log('实体已创建:', data.entityName);
});
```
## 性能优化
### 批量创建实体
框架提供了高性能的批量创建方法:
```typescript
// 批量创建 100 个子弹实体(高性能版本)
const bullets = scene.createEntities(100, "Bullet");
// 为每个子弹添加组件
bullets.forEach((bullet, index) => {
bullet.addComponent(new Position(Math.random() * 800, Math.random() * 600));
bullet.addComponent(new Velocity(Math.random() * 100 - 50, Math.random() * 100 - 50));
});
```
`createEntities()` 方法会:
- 批量分配实体 ID
- 批量添加到实体列表
- 优化查询系统更新
- 减少系统缓存清理次数
## 最佳实践
### 1. 合理的组件粒度
```typescript
// 好的做法:功能单一的组件
@ECSComponent('Position')
class Position extends Component {
x: number = 0;
y: number = 0;
}
@ECSComponent('Velocity')
class Velocity extends Component {
dx: number = 0;
dy: number = 0;
}
// 避免:功能过于复杂的组件
@ECSComponent('Player')
class Player extends Component {
// 避免在一个组件中包含太多不相关的属性
x: number;
y: number;
health: number;
inventory: Item[];
skills: Skill[];
}
```
### 2. 使用装饰器
始终使用 `@ECSComponent` 装饰器:
```typescript
@ECSComponent('Transform')
class Transform extends Component {
// 组件实现
}
```
### 3. 合理命名
```typescript
// 清晰的实体命名
const mainCharacter = scene.createEntity("MainCharacter");
const enemy1 = scene.createEntity("Goblin_001");
const collectible = scene.createEntity("HealthPotion");
```
### 4. 及时清理
```typescript
// 不再需要的实体应该及时销毁
if (enemy.getComponent(Health).current <= 0) {
enemy.destroy();
}
```
## 调试实体
框架提供了调试功能来帮助开发:
```typescript
// 获取实体调试信息
const debugInfo = entity.getDebugInfo();
console.log('实体信息:', debugInfo);
// 列出实体的所有组件
entity.components.forEach(component => {
console.log('组件:', component.constructor.name);
});
```
实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。
## 实体句柄 (EntityHandle)
实体句柄是一种轻量级的实体引用方式,用于安全地存储和传递实体引用,同时能够检测悬空引用(引用已销毁的实体)。
### 为什么需要实体句柄?
直接存储 `Entity` 引用存在问题:
- 实体销毁后,引用变成悬空指针
- 无法检测实体是否已被销毁
- 实体索引被复用时,可能错误地引用新实体
实体句柄通过 **代数Generation** 机制解决这些问题:
```typescript
import {
EntityHandle,
makeHandle,
indexOf,
genOf,
isValidHandle,
NULL_HANDLE
} from '@esengine/ecs-framework';
// 句柄包含索引和代数两部分信息
// 28位索引 + 20位代数 = 48位在 JavaScript 安全整数范围内)
const handle = makeHandle(42, 1);
console.log(indexOf(handle)); // 42 - 实体索引
console.log(genOf(handle)); // 1 - 实体代数
console.log(isValidHandle(handle)); // true - 非空句柄
```
### 使用 EntityHandleManager
```typescript
import { EntityHandleManager, indexOf, genOf } from '@esengine/ecs-framework';
const manager = new EntityHandleManager();
// 创建实体句柄
const handle1 = manager.create();
console.log(manager.isAlive(handle1)); // true
// 销毁实体
manager.destroy(handle1);
console.log(manager.isAlive(handle1)); // false - 句柄已失效
// 创建新实体(索引会被复用,但代数会增加)
const handle2 = manager.create();
console.log(indexOf(handle1) === indexOf(handle2)); // true - 相同索引
console.log(genOf(handle2) > genOf(handle1)); // true - 代数增加
// 旧句柄无法匹配新实体
console.log(manager.isAlive(handle1)); // false - 代数不匹配
console.log(manager.isAlive(handle2)); // true - 代数匹配
```
### 检测悬空引用
代数机制可以检测 ABA 问题(索引被复用导致的错误引用):
```typescript
// 场景:存储敌人的句柄
let enemyHandle = manager.create();
// ... 游戏运行中,敌人被销毁
manager.destroy(enemyHandle);
// ... 新实体被创建,复用了相同索引
const newEntity = manager.create();
// 安全检测:旧句柄不再有效
if (!manager.isAlive(enemyHandle)) {
console.log('敌人已被销毁,句柄无效');
enemyHandle = NULL_HANDLE; // 清空引用
}
```
### 启用/禁用状态
句柄管理器还支持实体的启用/禁用状态:
```typescript
const handle = manager.create();
// 禁用实体(仍然存活,但不参与处理)
manager.setEnabled(handle, false);
console.log(manager.isEnabled(handle)); // false
console.log(manager.isAlive(handle)); // true - 仍然存活
// 重新启用
manager.setEnabled(handle, true);
```
### API 参考
#### 句柄函数
| 函数 | 说明 |
|-----|------|
| `makeHandle(index, generation)` | 创建句柄 |
| `indexOf(handle)` | 获取索引 |
| `genOf(handle)` | 获取代数 |
| `isValidHandle(handle)` | 检查是否非空 |
| `handleEquals(a, b)` | 比较两个句柄 |
| `handleToString(handle)` | 调试输出 |
#### EntityHandleManager 方法
| 方法 | 说明 |
|-----|------|
| `create()` | 创建新句柄 |
| `destroy(handle)` | 销毁句柄 |
| `isAlive(handle)` | 检查是否存活 |
| `isEnabled(handle)` | 检查是否启用 |
| `setEnabled(handle, enabled)` | 设置启用状态 |
| `aliveCount` | 存活实体数量 |
| `capacity` | 当前容量 |
### 常量
| 常量 | 值 | 说明 |
|-----|---|------|
| `NULL_HANDLE` | `0` | 空句柄 |
| `MAX_ENTITIES` | `268,435,456` | 最大实体数 |
| `MAX_GENERATION` | `1,048,576` | 最大代数值 |
## 下一步
- 了解 [层级系统](./hierarchy.md) 建立实体间的父子关系
- 了解 [组件系统](./component.md) 为实体添加功能
- 了解 [场景管理](./scene.md) 组织和管理实体