359 lines
7.2 KiB
Markdown
359 lines
7.2 KiB
Markdown
|
|
# 组件系统
|
|||
|
|
|
|||
|
|
在 ECS 架构中,组件(Component)是数据和行为的载体。组件定义了实体具有的属性和功能,是 ECS 架构的核心构建块。
|
|||
|
|
|
|||
|
|
## 基本概念
|
|||
|
|
|
|||
|
|
组件是继承自 `Component` 抽象基类的具体类,用于:
|
|||
|
|
- 存储实体的数据(如位置、速度、健康值等)
|
|||
|
|
- 定义与数据相关的行为方法
|
|||
|
|
- 提供生命周期回调钩子
|
|||
|
|
- 支持序列化和调试
|
|||
|
|
|
|||
|
|
## 创建组件
|
|||
|
|
|
|||
|
|
### 基础组件定义
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
|||
|
|
|
|||
|
|
@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;
|
|||
|
|
max: number;
|
|||
|
|
|
|||
|
|
constructor(max: number = 100) {
|
|||
|
|
super();
|
|||
|
|
this.max = max;
|
|||
|
|
this.current = max;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 组件可以包含行为方法
|
|||
|
|
takeDamage(damage: number): void {
|
|||
|
|
this.current = Math.max(0, this.current - damage);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
heal(amount: number): void {
|
|||
|
|
this.current = Math.min(this.max, this.current + amount);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isDead(): boolean {
|
|||
|
|
return this.current <= 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 组件装饰器
|
|||
|
|
|
|||
|
|
**必须使用 `@ECSComponent` 装饰器**,这确保了:
|
|||
|
|
- 组件在代码混淆后仍能正确识别
|
|||
|
|
- 提供稳定的类型名称用于序列化和调试
|
|||
|
|
- 框架能正确管理组件注册
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// ✅ 正确的用法
|
|||
|
|
@ECSComponent('Velocity')
|
|||
|
|
class Velocity extends Component {
|
|||
|
|
dx: number = 0;
|
|||
|
|
dy: number = 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ❌ 错误的用法 - 没有装饰器
|
|||
|
|
class BadComponent extends Component {
|
|||
|
|
// 这样定义的组件可能在生产环境出现问题
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 组件生命周期
|
|||
|
|
|
|||
|
|
组件提供了生命周期钩子,可以重写来执行特定的逻辑:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
@ECSComponent('ExampleComponent')
|
|||
|
|
class ExampleComponent extends Component {
|
|||
|
|
private resource: SomeResource | null = null;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 组件被添加到实体时调用
|
|||
|
|
* 用于初始化资源、建立引用等
|
|||
|
|
*/
|
|||
|
|
onAddedToEntity(): void {
|
|||
|
|
console.log(`组件 ${this.constructor.name} 被添加到实体 ${this.entity.name}`);
|
|||
|
|
this.resource = new SomeResource();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 组件从实体移除时调用
|
|||
|
|
* 用于清理资源、断开引用等
|
|||
|
|
*/
|
|||
|
|
onRemovedFromEntity(): void {
|
|||
|
|
console.log(`组件 ${this.constructor.name} 从实体 ${this.entity.name} 移除`);
|
|||
|
|
if (this.resource) {
|
|||
|
|
this.resource.cleanup();
|
|||
|
|
this.resource = null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 访问实体
|
|||
|
|
|
|||
|
|
组件可以通过 `this.entity` 访问其所属的实体:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
@ECSComponent('Damage')
|
|||
|
|
class Damage extends Component {
|
|||
|
|
damage: number;
|
|||
|
|
|
|||
|
|
constructor(damage: number) {
|
|||
|
|
super();
|
|||
|
|
this.damage = damage;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 在组件方法中访问实体和其他组件
|
|||
|
|
applyDamage(): void {
|
|||
|
|
const health = this.entity.getComponent(Health);
|
|||
|
|
if (health) {
|
|||
|
|
health.takeDamage(this.damage);
|
|||
|
|
|
|||
|
|
// 如果生命值为0,销毁实体
|
|||
|
|
if (health.isDead()) {
|
|||
|
|
this.entity.destroy();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 组件属性
|
|||
|
|
|
|||
|
|
每个组件都有一些内置属性:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
@ECSComponent('ExampleComponent')
|
|||
|
|
class ExampleComponent extends Component {
|
|||
|
|
someData: string = "example";
|
|||
|
|
|
|||
|
|
showComponentInfo(): void {
|
|||
|
|
console.log(`组件ID: ${this.id}`); // 唯一的组件ID
|
|||
|
|
console.log(`所属实体: ${this.entity.name}`); // 所属实体引用
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
|
|||
|
|
## 复杂组件示例
|
|||
|
|
|
|||
|
|
### 状态机组件
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
enum EntityState {
|
|||
|
|
Idle,
|
|||
|
|
Moving,
|
|||
|
|
Attacking,
|
|||
|
|
Dead
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@ECSComponent('StateMachine')
|
|||
|
|
class StateMachine extends Component {
|
|||
|
|
private _currentState: EntityState = EntityState.Idle;
|
|||
|
|
private _previousState: EntityState = EntityState.Idle;
|
|||
|
|
private _stateTimer: number = 0;
|
|||
|
|
|
|||
|
|
get currentState(): EntityState {
|
|||
|
|
return this._currentState;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
get previousState(): EntityState {
|
|||
|
|
return this._previousState;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
get stateTimer(): number {
|
|||
|
|
return this._stateTimer;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
changeState(newState: EntityState): void {
|
|||
|
|
if (this._currentState !== newState) {
|
|||
|
|
this._previousState = this._currentState;
|
|||
|
|
this._currentState = newState;
|
|||
|
|
this._stateTimer = 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateTimer(deltaTime: number): void {
|
|||
|
|
this._stateTimer += deltaTime;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isInState(state: EntityState): boolean {
|
|||
|
|
return this._currentState === state;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 配置数据组件
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
interface WeaponData {
|
|||
|
|
damage: number;
|
|||
|
|
range: number;
|
|||
|
|
fireRate: number;
|
|||
|
|
ammo: number;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@ECSComponent('WeaponConfig')
|
|||
|
|
class WeaponConfig extends Component {
|
|||
|
|
data: WeaponData;
|
|||
|
|
|
|||
|
|
constructor(weaponData: WeaponData) {
|
|||
|
|
super();
|
|||
|
|
this.data = { ...weaponData }; // 深拷贝避免共享引用
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 提供便捷的访问方法
|
|||
|
|
getDamage(): number {
|
|||
|
|
return this.data.damage;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
canFire(): boolean {
|
|||
|
|
return this.data.ammo > 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
consumeAmmo(): boolean {
|
|||
|
|
if (this.data.ammo > 0) {
|
|||
|
|
this.data.ammo--;
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 最佳实践
|
|||
|
|
|
|||
|
|
### 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('GameObject')
|
|||
|
|
class GameObject extends Component {
|
|||
|
|
x: number;
|
|||
|
|
y: number;
|
|||
|
|
dx: number;
|
|||
|
|
dy: number;
|
|||
|
|
health: number;
|
|||
|
|
damage: number;
|
|||
|
|
sprite: string;
|
|||
|
|
// 太多不相关的属性
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. 使用构造函数初始化
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
@ECSComponent('Transform')
|
|||
|
|
class Transform extends Component {
|
|||
|
|
x: number;
|
|||
|
|
y: number;
|
|||
|
|
rotation: number;
|
|||
|
|
scale: number;
|
|||
|
|
|
|||
|
|
constructor(x = 0, y = 0, rotation = 0, scale = 1) {
|
|||
|
|
super();
|
|||
|
|
this.x = x;
|
|||
|
|
this.y = y;
|
|||
|
|
this.rotation = rotation;
|
|||
|
|
this.scale = scale;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. 明确的类型定义
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
interface InventoryItem {
|
|||
|
|
id: string;
|
|||
|
|
name: string;
|
|||
|
|
quantity: number;
|
|||
|
|
type: 'weapon' | 'consumable' | 'misc';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@ECSComponent('Inventory')
|
|||
|
|
class Inventory extends Component {
|
|||
|
|
items: InventoryItem[] = [];
|
|||
|
|
maxSlots: number;
|
|||
|
|
|
|||
|
|
constructor(maxSlots: number = 20) {
|
|||
|
|
super();
|
|||
|
|
this.maxSlots = maxSlots;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
addItem(item: InventoryItem): boolean {
|
|||
|
|
if (this.items.length < this.maxSlots) {
|
|||
|
|
this.items.push(item);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
removeItem(itemId: string): InventoryItem | null {
|
|||
|
|
const index = this.items.findIndex(item => item.id === itemId);
|
|||
|
|
if (index !== -1) {
|
|||
|
|
return this.items.splice(index, 1)[0];
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4. 避免在组件中存储实体引用
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// ❌ 避免:在组件中存储其他实体的引用
|
|||
|
|
@ECSComponent('BadFollower')
|
|||
|
|
class BadFollower extends Component {
|
|||
|
|
target: Entity; // 直接引用可能导致内存泄漏
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ✅ 推荐:存储实体ID,通过场景查找
|
|||
|
|
@ECSComponent('Follower')
|
|||
|
|
class Follower extends Component {
|
|||
|
|
targetId: number;
|
|||
|
|
followDistance: number = 50;
|
|||
|
|
|
|||
|
|
constructor(targetId: number) {
|
|||
|
|
super();
|
|||
|
|
this.targetId = targetId;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getTarget(): Entity | null {
|
|||
|
|
return this.entity.scene?.findEntityById(this.targetId) || null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
组件是 ECS 架构的数据载体,正确设计组件能让你的游戏代码更模块化、可维护和高性能。
|