bit-ec:不想上重型 ECS?试试这个 Cocos 专用轻量 EC

它解决什么问题

bit-ecs 那种稀疏集合加密集数组的重型 ECS,是为了应付大批量实体高频增删设计的,但不是每个项目都有这个量级的需求。中小型项目里更常见的诉求其实是”数据和逻辑分开写,组件之间的更新顺序能精确控制,最好还能配个可视化编辑器直接拖属性”,上一套完整 ECS 有点杀鸡用牛刀。

bit-ec 就是为这类场景做的:一个专为 Cocos Creator 优化的轻量实体组件框架,组件区分”数据组件”和”逻辑组件”,更新顺序你说了算,还支持多个世界(比如同时存在的多个战斗场景)互不干扰。

安装

bit-event 是 peer 依赖:

npm install @gongxh/bit-ec @gongxh/bit-event

核心用法

定义组件

组件继承 Component,用装饰器注册,属性装饰器支持的类型比较全,包括 Cocos 常用类型(spriteframeprefabvec3color 等),方便配套编辑器直接可视化配置:

import { Component, _ecdecorator } from '@gongxh/bit-ec';
const { ecclass, ecprop } = _ecdecorator;

enum ComponentType {
    Health,
    Movable,
}

@ecdecorator.ecclass('Health', ComponentType.Health)
class HealthComponent extends Component {
    @ecprop({ type: 'int' }) maxHp: number = 100;
    @ecprop({ type: 'int' }) curHp: number = 100;
}

组件生命周期

class MoveComponent extends Component {
    onAdd(): void {
        // 组件被添加到实体的那一刻
    }

    onEnter(): void {
        // 实体正式进入场景,这时候可以安全地拿同实体的其他组件
        const health = this.getComponent<HealthComponent>(ComponentType.Health);
    }

    update(dt: number): void {
        // 需要先把 needUpdate 设为 true 才会被调用
    }

    onRemove(): void {
        // 组件从实体移除
    }
}

onAddonEnter 分开,是因为组件刚被添加时,同实体上的其他组件可能还没就位,这时候去 getComponent 拿别的组件容易拿到 undefined。等到 onEnter 触发,实体上该有的组件都已经齐了,这时候拿组件互相引用才靠得住。

创建世界和实体

import { ECManager } from '@gongxh/bit-ec';

ECManager.init(); // 全局只调用一次

// componentUpdateOrderList 决定了各组件类型 update 的执行先后顺序
const world = ECManager.createECWorld('battle-1', battleNode, [
    ComponentType.Movable,
    ComponentType.Health,
], 300, 10);

const entity = ECManager.createEntity('battle-1', 'Monster_01');

// 每帧
world.update(dt);

多个 createECWorld 可以同时存在,比如 PVP 场景里两个互相独立的小战场,各自的实体、组件互不干扰,一个世界 update 出问题不会影响另一个。

组件更新顺序为什么要精确控制

游戏逻辑里经常有依赖顺序的更新:移动组件先算完新位置,碰撞组件再基于新位置判断是否命中,血量组件最后根据命中结果扣血。如果更新顺序是随意的(比如按组件添加顺序或者哈希顺序),同样的代码在不同时候跑出来的行为可能不一致,调试起来很痛苦。

createECWorldcomponentUpdateOrderList 参数就是让你显式声明这个顺序,框架严格按这个顺序跑每种组件类型的 update,行为可预测。

避坑提醒

  • 别在 onAdd() 里调用 getComponent() 去拿同实体的其他组件,大概率拿不到,要拿组件互相引用的逻辑放到 onEnter() 里。
  • update(dt) 默认不会被调用,组件需要显式把 needUpdate 设为 true,纯数据组件不需要每帧更新的话别设,省一点遍历开销。
  • 多世界场景下,createEntity 第一个参数是世界名,同一个实体配置在不同世界里创建出来的是完全独立的两份实例,互相不共享状态。

项目信息

组件之间跨系统通信、或者要通知 UI 层刷新,配合 bit-event 一起用最顺手,这也是它唯一的 peer 依赖。