Files
esengine/docs/guide/entity.md

11 KiB
Raw Blame History

实体类

在 ECS 架构中实体Entity是游戏世界中的基本对象。实体本身不包含游戏逻辑或数据它只是一个容器用来组合不同的组件来实现各种功能。

基本概念

实体是一个轻量级的对象,主要用于:

  • 作为组件的容器
  • 提供唯一标识ID
  • 管理组件的生命周期

::: tip 关于父子层级关系 实体间的父子层级关系通过 HierarchyComponentHierarchySystem 管理,而非 Entity 内置属性。这种设计遵循 ECS 组合原则 —— 只有需要层级关系的实体才添加此组件。

详见 层级系统 文档。 :::

创建实体

重要提示:实体必须通过场景创建,不支持手动创建!

实体必须通过场景的 createEntity() 方法来创建,这样才能确保:

  • 实体被正确添加到场景的实体管理系统中
  • 实体被添加到查询系统中,供系统使用
  • 实体获得正确的场景引用
  • 触发相关的生命周期事件
// 正确的方式:通过场景创建实体
const player = scene.createEntity("Player");

// ❌ 错误的方式:手动创建实体
// const entity = new Entity("MyEntity", 1); // 这样创建的实体系统无法管理

添加组件

实体通过添加组件来获得功能:

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 = 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));

获取组件

// 获取组件(传入组件类,不是实例)
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[]

// 获取指定类型的所有组件(支持同类型多组件)
const allHealthComponents = player.getComponents(Health);  // Health[]

// 获取或创建组件(如果不存在则自动创建)
const position = player.getOrCreateComponent(Position, 0, 0);  // 传入构造参数
const health = player.getOrCreateComponent(Health, 100);       // 如果存在则返回现有的,不存在则创建新的

移除组件

// 方式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("健康组件已被移除");
}

实体查找

场景提供了多种方式来查找实体:

通过名称查找

// 查找单个实体
const player = scene.findEntity("Player");
// 或使用别名方法
const player2 = scene.getEntityByName("Player");

if (player) {
  console.log("找到玩家实体");
}

通过 ID 查找

// 通过实体 ID 查找
const entity = scene.findEntityById(123);

通过标签查找

实体支持标签系统,用于快速分类和查找:

// 设置标签
player.tag = 1; // 玩家标签
enemy.tag = 2;  // 敌人标签

// 通过标签查找所有相关实体
const players = scene.findEntitiesByTag(1);
const enemies = scene.findEntitiesByTag(2);
// 或使用别名方法
const allPlayers = scene.getEntitiesByTag(1);

实体生命周期

// 销毁实体
player.destroy();

// 检查实体是否已销毁
if (player.isDestroyed) {
  console.log("实体已被销毁");
}

实体事件

实体的组件变化会触发事件:

// 监听组件添加事件
scene.eventSystem.on('component:added', (data) => {
  console.log('组件已添加:', data);
});

// 监听实体创建事件
scene.eventSystem.on('entity:created', (data) => {
  console.log('实体已创建:', data.entityName);
});

性能优化

批量创建实体

框架提供了高性能的批量创建方法:

// 批量创建 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. 合理的组件粒度

// 好的做法:功能单一的组件
@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 装饰器:

@ECSComponent('Transform')
class Transform extends Component {
  // 组件实现
}

3. 合理命名

// 清晰的实体命名
const mainCharacter = scene.createEntity("MainCharacter");
const enemy1 = scene.createEntity("Goblin_001");
const collectible = scene.createEntity("HealthPotion");

4. 及时清理

// 不再需要的实体应该及时销毁
if (enemy.getComponent(Health).current <= 0) {
  enemy.destroy();
}

调试实体

框架提供了调试功能来帮助开发:

// 获取实体调试信息
const debugInfo = entity.getDebugInfo();
console.log('实体信息:', debugInfo);

// 列出实体的所有组件
entity.components.forEach(component => {
  console.log('组件:', component.constructor.name);
});

实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。

实体句柄 (EntityHandle)

实体句柄是一种安全的实体引用方式,用于解决"引用已销毁实体"的问题。

问题场景

假设你的 AI 系统需要追踪一个目标敌人:

// 错误做法:直接存储实体引用
class AISystem extends EntitySystem {
    private targetEnemy: Entity | null = null;

    setTarget(enemy: Entity) {
        this.targetEnemy = enemy;
    }

    process() {
        if (this.targetEnemy) {
            // 危险!敌人可能已被销毁,但引用还在
            // 更糟糕:这个内存位置可能被新实体复用了
            const health = this.targetEnemy.getComponent(Health);
            // 可能操作了错误的实体!
        }
    }
}

使用句柄的正确做法

每个实体创建时会自动分配一个句柄,通过 entity.handle 获取:

import { EntityHandle, NULL_HANDLE, isValidHandle } from '@esengine/ecs-framework';

class AISystem extends EntitySystem {
    // 存储句柄而非实体引用
    private targetHandle: EntityHandle = NULL_HANDLE;

    setTarget(enemy: Entity) {
        // 保存敌人的句柄
        this.targetHandle = enemy.handle;
    }

    process() {
        if (!isValidHandle(this.targetHandle)) {
            return; // 没有目标
        }

        // 通过句柄获取实体(自动检测是否有效)
        const enemy = this.scene.findEntityByHandle(this.targetHandle);

        if (!enemy) {
            // 敌人已被销毁,清空引用
            this.targetHandle = NULL_HANDLE;
            return;
        }

        // 安全操作
        const health = enemy.getComponent(Health);
    }
}

完整示例:技能目标锁定

import {
    EntitySystem, Entity, EntityHandle, NULL_HANDLE, isValidHandle
} from '@esengine/ecs-framework';

@ECSSystem('SkillTargeting')
class SkillTargetingSystem extends EntitySystem {
    // 存储多个目标的句柄
    private lockedTargets: Map<Entity, EntityHandle> = new Map();

    // 锁定目标
    lockTarget(caster: Entity, target: Entity) {
        this.lockedTargets.set(caster, target.handle);
    }

    // 获取锁定的目标
    getLockedTarget(caster: Entity): Entity | null {
        const handle = this.lockedTargets.get(caster);

        if (!handle || !isValidHandle(handle)) {
            return null;
        }

        // findEntityByHandle 会检查句柄是否有效
        const target = this.scene.findEntityByHandle(handle);

        if (!target) {
            // 目标已死亡,清除锁定
            this.lockedTargets.delete(caster);
        }

        return target;
    }

    // 释放技能
    castSkill(caster: Entity) {
        const target = this.getLockedTarget(caster);

        if (!target) {
            console.log('目标丢失,技能取消');
            return;
        }

        // 安全地对目标造成伤害
        const health = target.getComponent(Health);
        if (health) {
            health.current -= 10;
        }
    }
}

句柄 vs 实体引用

场景 推荐方式
同一帧内临时使用 直接用 Entity 引用
跨帧存储(如 AI 目标、技能目标) 使用 EntityHandle
需要序列化保存 使用 EntityHandle(数字类型)
网络同步 使用 EntityHandle(可直接传输)

API 速查

// 获取实体的句柄
const handle = entity.handle;

// 检查句柄是否非空
if (isValidHandle(handle)) { ... }

// 通过句柄获取实体(自动检测有效性)
const entity = scene.findEntityByHandle(handle);

// 检查句柄对应的实体是否存活
const alive = scene.handleManager.isAlive(handle);

// 空句柄常量
const emptyHandle = NULL_HANDLE;

下一步