开源项目
bit-ecs:稀疏集合 + 密集数组撑起来的高性能 ECS
高性能 ECS 框架,稀疏集合加密集数组实现零 GC 查询迭代,适合大量实体频繁增删的场景,配套可视化编辑器可视化配置组件。
bit-ecs:稀疏集合 + 密集数组撑起来的高性能 ECS
它解决什么问题
弹幕游戏、大型 RTS、需要同屏处理成百上千个单位的游戏,最容易被面向对象那一套写法拖垃:继承层级一深,一个”会飞的会打的会加血的怪”到底该继承哪个基类就能吵半天,而且对象越多,遍历判断类型的开销越大,GC 也跟着遭罪。
ECS(实体-组件-系统)架构把”是什么”(组件,纯数据)和”做什么”(系统,纯逻辑)拆开,实体只是个数字 ID,靠组件的组合决定它的行为。bit-ecs 在这个基础上,用稀疏集合加密集数组的数据布局,让频繁增删实体和组件这件事本身也做到高性能。
安装
npm install @gongxh/bit-ecs
零依赖,独立使用。
核心概念
- 实体(Entity)——就是个数字 ID,本身不携带任何数据
- 组件(Component)——纯数据结构,比如位置、血量、速度
- 系统(System)——逻辑代码,处理”同时拥有某几种组件”的所有实体
- 世界(World)——管理实体、组件、系统的容器
核心用法
定义组件
组件必须实现 reset(),因为组件是从对象池里回收复用的,不重置会带着上一个实体的脏数据:
import { Component, _ecsdecorator } from '@gongxh/bit-ecs';
const { ecsclass, ecsprop } = _ecsdecorator;
@ecsclass('Position')
export class PositionComponent extends Component {
@ecsprop({ type: 'float' }) x: number = 0;
@ecsprop({ type: 'float' }) y: number = 0;
reset(): void {
this.x = 0;
this.y = 0;
}
}
定义系统
系统在 onInit() 里配置要查询哪些实体,update(dt) 里写每帧逻辑:
import { System, _ecsdecorator } from '@gongxh/bit-ecs';
const { ecsystem } = _ecsdecorator;
@ecsystem('Move')
export class MoveSystem extends System {
protected onInit(): void {
this.matcher.allOf(PositionComponent, VelocityComponent);
}
update(dt: number): void {
this.query.iterate2(PositionComponent, VelocityComponent, (entity, pos, vel) => {
pos.x += vel.x * dt;
pos.y += vel.y * dt;
});
}
}
allOf 表示实体必须同时有这两个组件才会被这个系统处理,还有 anyOf(任一)、excludeOf(必须不含)、optionalOf(可选,查询时一并带出但不作为过滤条件)可以组合。
iterate2 这类按数量区分的迭代器(1~4 个组件)是零 GC 的,比通用的 iterate(...types)(支持最多 8 个组件,几乎无 GC)性能更好,热路径(每帧都跑的系统)优先用固定数量版本。
创建世界,跑起来
import { World } from '@gongxh/bit-ecs';
const world = new World('battle', 1024); // 最大实体数建议 2 的指数
world.addSystem(new MoveSystem());
world.initialize(); // 必须调用一次
const entity = world.createEmptyEntity();
world.addComponent(entity, PositionComponent);
world.addComponent(entity, VelocityComponent);
// 每帧调用
world.update(dt);
也可以用 SystemGroup 把系统分组,配置 frameInterval 让某些不需要每帧都跑的系统降频,比如 AI 决策系统每 3 帧跑一次:
const aiGroup = world.SystemGroup('AI', 3);
命令缓冲:为什么删除组件不是立刻生效
removeEntity、addComponent、removeComponent 都不是调用后立刻改变数据结构,而是延迟到下一帧 update() 前统一处理。这是命令缓冲模式,为的是避免系统正在遍历实体的过程中,数据结构被中途修改导致遍历错乱或者漏处理。
代价是:你在同一帧内调用 removeComponent 之后,马上再 getComponent 去读,读到的还是删除前的数据——它还没真正被移除。这个延迟生效的特性容易让人以为是 bug,实际是设计使然。
性能特点
稀疏集合加密集数组这套布局,对”频繁加实体、删实体、加组件、删组件”这类场景做了偏向优化,系统遍历性能也不错,代价是内存占用会比纯稠密数组方案高一些——用空间换的是增删的时间。查询器本身会做条件复用,同样的查询条件多次调用不会重复计算,用了掩码做匹配,实体一多的时候也不会线性扫描全部组件类型。
避坑提醒
- 组件的
reset()一定要把所有字段都清干净,漏一个字段在对象池复用时就是隐藏的脏数据 bug,而且这种 bug 通常要跑很多局才会暴露,很难复现。 - 增删操作是延迟生效的,同一帧内”删除再查询”拿到的是旧数据,不要依赖同帧内立即生效的假设写逻辑。
- 世界的
maxEntityCount建议设成 2 的指数(比如 512、1024),跟内部数据结构的扩容策略更契合。
项目信息
- GitHub: https://github.com/gongxh0901/bit-framework/tree/main/bit-ecs
- npm: @gongxh/bit-ecs
- 可视化编辑器: Cocos Store - kunpoec(付费,基于 Creator 3.8.6 开发)
- 许可证: MIT License
如果你的项目更偏向”节点少、逻辑相对简单”的中小型游戏,不需要 ECS 这么重的架构,可以看看更轻量的 bit-ec。