« Tag set 的数据结构优化 | 返回首页 | 内存对齐问题和编译器优化 »

预制件和对象集的管理

最近在用自研引擎开发项目时,发现了一些问题。在解决问题的同时,也逐步对之前的设计做了一些调整。一开始只是一些小修复,慢慢的发展成了大规模的代码重构。

最开始源于我重新设计了 ECS 框架。在新设计下,可以用 C/Lua 混合组织数据。为未来优化热点做好准备。我们借此机会重新思考了 ECS 框架下应该如何组织代码的问题。发现一个关键点就是,要尽量去掉系统中对象之间的引用关系。每类对象最好是成组分批的处理业务,每个模块都只做最简单的事情。但同一件事情尽量处理更多的数据、对象。

比如对象的构建和销毁,通常会随着对象构成的复杂度上升而演变为一件越来越复杂的事务。我们之间在设计 ECS 框架时,就设计了大量的机制来正确执行 Entity 的构建流程。一个 Entity 可以由若干 Component 类型动态组合,并非每个 Component 都能独立初始化。有时它们是相关联的。例如 A B C 组合成一个 Entity ,初始化 A 和 B 后,才能根据 A B 的结果初始化 C 。

加入预制件的概念后,这个过程更为复杂。预制件其实是一个包含很多对象的组。从预制件还原设计好的对象,不光是要逐个初始化对象,还需要重建设计时构建的这些对象之间的关联。例如,整个游戏场景就可以是单个预制件。加载场景就是把预设的构成整个场景的所有对象在运行时重建的过程。

在传统的 OOP 框架中,我们往往顺次构造每个对象,然后再尝试还原它们之间的关系(比如场景树中的层次父子关系);但在 ECS 框架中,我们管理的其实是若干 Components 的集合。按 Component 分类初始化可能更为简单。关联本身很可能局限于同类 Component 之间(例如场景节点),而不涉及 Entity 。

我们在重构之后,取消了之前的那套基于 Entity 为单位的初始化(以及销毁)机制。改为模块自己把构造流程的 System 添加到合适的 Pipeline 中。所以,在新机制下,Entity 看起来是批量同时构建出来的。也就是说,如果一个 Entity 的构造如果分 A B C 三个步骤的话,其实框架是在步骤 A 处理了所有的 Entity 后,再批量做步骤 B 以及步骤 C 。

这样做的代价是,Entity 的构建全部变成了异步过程。我们不再可以直接用代码做这样的操作:

  1. E = CreateEntity()
  2. S = Getstate(E)
  3. CreateEntity(S)

先创建一个 Entity E ,然后取得 E 的某些状态 S ,再用 S 继续创建下一个 Entity 。

无法同步创建一个 Entity 到底会给引擎使用者带来多少麻烦?这个问题我们认真讨论过好几次。首先,在有了预制件后,其实我们很少需要直接从空创建一个 Entity ,而是用 prefab:instance() 从预制件数据中创建出来。这个问题就转换为,预制件的 instance (实例化)方法是否应该是一个异步过程。

目前,我们的答案是,实例化过程应该是异步的。但可以提供有限的同步能力。

对于游戏引擎来说。当我们想把一组预先组织好的数据在运行时重新组织起来。这个过程完全同步完成其实本身就不现实。因为它涉及了大量的资源 IO 和解析工作。如果想等所有配套资源都就绪再返回,让后面的代码可以立即 100% 读写这组对象,那么必然会在单个调用中造成长时间的等待。

可即使在加载场景阶段,我们依然希望这个过程可以分部完成,同时期有机会去画个进度条或是做些更复杂的事情。如果是同步接口,恐怕就必须依赖多线程这样更复杂容易出错的机制了。

当然,我们也可以把资源加载剥离出来,提供消息机制,通知调用者何时真的完成。而资源之外的部分则同步创建。但其实我们很难界定什么属于资源数据。如果纹理模型这些算资源数据,那预制件本身算不算?

我们现在的做法是,预制件的实例化同步返回的只有一个场景上的虚拟空节点。这个空节点的结构简单、是完全确定的。它可以直接在 World 中同步创建出来,只有 SRT 等这些简单的属性。而预制件中需要重建的场景树全部都是异步构建的。它们的构建过程分布在框架的 pipeline 中,最快也要等待下一个渲染帧才能真正构建好。

对外的接口是以回调函数的形式控制从预制件中创建出来的对象集的。也就是在创建对象的调用时,就应该传入 ready (创建完毕)update (每帧更新)message(响应消息)这些函数。

从外面看,这组对象是隐藏在根上的虚拟节点之后的。我们没有提供 API 绕过根节点枚举出内部的具体对象。对对象的操作都必须在回调函数中完成。所以从外部访问具体对象都必须通过“消息”来完成,否则就只能控制根节点。而根节点,也可以在创建完毕后同步进行控制,不必等实际对象组都准备好)。

Comments

回调问题不算问题,可以在业务层想办法处理。比如很多语言都有await这种语法级支持,再不济,自己也可以封装一个事务队列模块来处理一些串行,并行行为。

面对云风大哥,右手就是一个赞

感觉跟ue的架构思想差不多啊

期待基于ECS制作的游戏早日上市

风哥啥时候开源

会有回调地狱的问题吗?

Post a comment

非这个主题相关的留言请到:留言本