【ECS框架】专业级游戏架构 + 可视化调试插件,5年持续更新!附完整教程

这两天忘记更新了日志了

2025/7/29
ECS框架更新 v2.1.27

  • 修复QuerySystem失效问题
  • 移除Entity里多余的缓存机制
  • 内部优化实体ID生成机制(改用世代式ID池)
  • 修复EntitySystem设置更新时序时场景未初始化报错问题
  • 新增处理器异常错误捕捉机制

2025/8/6
ECS框架更新 v2.1.29版本

  1. 重大性能更新 matcher内部实现更替位运算更改为querysystem以优化性能(测试性能至少提升50倍)
  2. 新增soa存储架构用于优化大规模实体性能问题
    详情使用见文档: ecs-framework/docs/soa-storage-guide.md at master · esengine/ecs-framework
  3. BigIntFactory缓存优化
  4. 新增批量实体创建api并优化单实体创建性能
  5. 新增系统onInitialize生命周期,initialize更改为框架内部使用并修复可能多次初始化问题

下一步的计划要开始支持网络了,目前考虑采用混合架构 帧同步+状态同步,rpc通信,提供protobuf序列化、帧同步和快照功能,前后端同一套代码

1赞

赞赞赞 :+1:

这是我目前网络库组件构思的一种写法

目前考虑的是前后端同一套代码,我会提供一个编译插件再编译的时候剔除代码,客户端里不会包含__SERVER__的代码,后端使用nodejs也会编译后剔除__CLIENT__代码,并且为了方便protobuf的使用,采用装饰器简化了手写protobuf文件的过程。当然还有很多的装饰器这里没有列举,比如属性自动同步属性

 class PlayerComponent extends NetworkComponent {
     @SyncVar()
     public health: number = 100;
     
     @SyncVar({ hook: 'onNameChanged' })
     public playerName: string = 'Player';
     
     @SyncVar({ authorityOnly: true })
     public isReady: boolean = false;
     
     onNameChanged(oldName: string, newName: string) {
         const logger = createLogger('PlayerComponent');
         logger.info(`Name changed: ${oldName} -> ${newName}`);
     }
 }

不过这些都还没有完全实现完成,如果有兴趣尝鲜的小伙伴可以先看看开源库或者加入Q群可以一起交流

后端其实可以考虑之前社区里有个开源的TSRPC,可以在它基础上封装

1赞

感谢建议,我刚看了以下tsrpc框架确实很好,目前我的考虑是实现protobuf+ws主要是为了ecs实时状态同步,我看tsrpc更适合传统的业务api调用,两者感觉解决的是不同层面的问题。可以考虑以后提供tsrpc的集成方案,或者把rpc层做成插件化的可以用户自己选,我目前不能确定的是tsrpc替代我现在已经实现的syncvar系统是否会更好,可能要之后我要先试一下这个框架再评估以下再考虑是替换还是作为额外的rpc调用层,或者要具体试一下再什么具体场景下用tsrpc更合适一些

建议直接使用或者定制化tsrpc,会有1+1>2的效果. 虽然我看得出你完全有能力快速写一个更符合自定义需求的,但是有时候用户可能已经有了一些使用习惯.

1赞

有人已经用上了吗?谈谈使用感受不?我看评论区有写Xfoorge加这个,实际情况是怎么样的

哥们儿,你的文档是不是该更新了? 复制你文档代码到工程中,一片片的报错,不是变量名变了,就是接口已经没有了。

不好意思,最新的更新了,以github文档为准,有问题可以加群反馈哦,这里有时候看的不及时

战术mark

在JS上构建ECS,JS没有强内存管理,Cpu的Cache内存连续性带来的性能提升根本发挥不出来,不懂在JS上构建ECS的意义在哪里?

一个0.0000001秒的1000次调用,你优化10倍性能是毫无意义的。您这里列出的性能提升,如果有数据测试,欢迎举证。

如果只是为了简化开发,更应该考虑的是自己的OOP基础是不是扎实?而不是回退到面向函数编程。据我这么多年的观察,真的懂OOP的前端开发,不足5%

关于你说的这几个观点再实际游戏开发里,每帧只有16.67ms的预算下,哪怕再微小的优化累积效应也是显著的,而且目前再js中ecs的价值不在于性能,更在于可维护的架构。

还有soa和传统存储的对比测试我已经有写过单元test,同样的案例确实能带来一定的性能提升,具体测试代码都在源项目里。

关于你说的oop和ecs这是两个不同的设计理念,一个是数据驱动设计,将数据和行为分离,并且组合优于继承,能够避免深层继承树的复杂性,虽然没有内存控制,但是可以通过typearray等技术仍然可以获取可观的性能提升

很多人认为ecs主要的作用是提升性能
但其实性能真的没那么重要,最关键的是组合,这才是他最最重要的东西

1、每帧16.67ms那只是渲染帧的时间,逻辑帧一般采用20-30帧。所有它不是16.7是33.3或者50ms。

2、纯代码逻辑,我用OOP的情况下,同屏200个单位,有寻路,有碰撞检测,性能占比20%-30%。

纯CPU运算下,如果你存在CPU缓存的命中策略,性能是会有巨大提升,这也是ECS的唯一优势。但JS语言类似Java的GC机制,显然做不到这一点。纯逻辑代码,值得优化的点只有数据结构,其他都是没有任何性价比的事。
性能优化,应该基于当前性能瓶颈去做,而不是想当然的觉得少一次Update就叫优化。另外一个1秒的调用,优化到0.1秒是有意义的,一个0.00001毫秒的东西优化到0.000001毫秒是没有意义的。虽然这两样都叫优化了10倍。当然,从你一个程序员的角度这可以是你的KPI,但我从一个主程的角度,这是没有任何性价比的事。

3、面向数据编程,就是面向过程编程,这本就是一种设计模式的倒退。很多人觉得这种模式更好,那是因为压根就没掌握OOP。没有更高层的抽象和封装,开发效率比OOP要慢的多的多。
而OOP不等于放弃了组合,EntityComponent本来就是OOP里的概念。
至于什么时候用组合什么时候用继承,OOP的书第一章就写了,has a 和is a的区别。前者用组合,后者用继承。而继承本身是一种非常严格的定义,很多人用不好OOP,就是因为错把has a当继承来用。

你说得对,逻辑帧通常是 20-30fps。但这更说明了我的观点 - 在 33.3ms
的预算内,哪怕是几毫秒的优化在大规模场景下都是有意义的。我的测试显示
,20000 个实体的批量更新,SoA 能节省 32ms(46.90ms vs
14.63ms),这在逻辑帧预算中就很可观了

而且 我也同意应该基于实际瓶颈优化。但我再测试数据显示,在特定场景下(大规模批量
操作),ECS + SoA 确实能带来显著提升。你提到的"200个单位 20-30%
CPU",如果换成 20000 个单位做批量物理更新,差异就会很明显

除了性能,ECS 的核心价值确实在于架构。数据驱动设计让系统之间解耦,组
合优于继承避免了深层继承树。这不是 OOP的倒退,而是针对特定领域(游戏中大量同质实体)的设计模式选择,而且我的框架设计是混合式的 - 可以选择性启用SoA。小规模随机访问用传统存储,大规模批量操作用SoA。这样既避免了不必要的开销,又能在需要时获得性能提升。

ECS 在 JS 中的价值不是万能的性能银弹,而是在特定场景下的有效工具。我的测试数据也只是证明了在合适的场景下确实有显著收益

OOP vs ECS这个话题我们已经讨论够了,观点都表达清楚了。我不想再纠结这个问题,各自用各自认为合适的技术方案就行

2赞

没有连续内存的优势后,都不考虑内存连续布局,会发现oop与这个也不是硬冲突的,这个能帮助你更好的组合和调度,数据与逻辑分离的更彻底,

能不能用webassembly来弥补JavaScript的强内存管理上的短板? :thinking:

或者JavaScript不是有ArrayBuffer吗?它的内存不是连续的吗?用ArrayBuffer弥补JavaScript的强内存的短板?我想问 :thinking:

你对象是new出来的,分配的内存不受你的控制

@南城su 发现微信开发者工具中,entitySystem获取到的entities的类型为一个数组,只有一个元素,而且元素类型为Set
而浏览器中就是正常的,是一个entities数组。
怎么办?