发动机叫:StarEngine
Github:https://github.com/star-e/starengine/
此次的修正,主要就是勒代洛热了“面向全国统计数据”和“触发器构架”三个优点,和引起的一连串解构。
这首诗如是说了发动机的总体构架,会较为长,有错字请嘿嘿。
面向全国统计数据(Data-Oriented)
StarEngine并没借助业内盛行的ECS(Entity-Component-System)构架,而要合作开发了崭新的如前所述Graph的面向全国统计数据构架。
ECS存有下列许多难题:
虚拟、模块间的可视化没标准化的规范化,怎样表述USB是个难题。
ECS的同时实现形式各种各样,极难达成一致一致意见,不易于推展、课堂教学。
ECS构架能十分繁杂(比如说Unity的DOTS)。ECS构架通常借助了倚赖转化成,认知构架这类须要牺牲十分不懈努力。
相对而言,Graph与ECS有许多关联性,经营理念是相连的。
Graph与ECS的差别是他们透过C#同时实现F83E43Se,而并非倚赖转化成。
他们的同时实现有下列的特征:
如前所述Adjacency List的Graph同时实现,能透过OutEdge List同时实现索引关系,能很好的表示Entity与Entity间的亲密关系。对于图状的业务逻辑,有著天生的支持,比如说SceneGraph,RenderGraph等。
如前所述boost.graph的C#同时实现,有著业内精心打磨过的USB设计,适应性广,易于课堂教学。
Graph有著明确的业务范围,只维护核心的图状统计数据结构。构架感较为弱,统计数据感更强。
前人对Graph的研究非常透彻,归纳出了下列许多基本Concepts:
他们能选择合适的统计数据结构来同时实现相应的Concept,以达到最优的性能。通常的ECS只有一种同时实现,不具备如此的灵活性。同时由于boost的C#设计,具有标准化的USB。这样彻底地分离了USB与同时实现,使得切换同时实现的成本非常低。boost有著大量的图论算法比如说DFS、BFS,省下的合作开发量非常可观。
在此基础之上,StarEngine引入了更多的Concept,来解决发动机合作开发中的常见难题。
Named Graph:Vertex有名字的图。
Ownership Graph:拥有额外父子亲密关系树的图。
Addressable Graph:能用Path索引Vertex的图。比如说 /Assets/Model/Sponza.fbx
UUID Graph:能用UUID索引Vertex的图。
Component Graph:Vertex Property以Struct of Array存储的图。
Polymorphic Graph:Vertex支持运行时多态的图。
透过以上6种Concept的自由组合,他们能同时实现发动机中的绝大部分图状功能。
触发器构架
上面他们已经用Graph替代了ECS,还有多线程的难题。
Unity的DOTS透过Jobs系统同时实现了这点,他们也须要两个多线程/触发器构架。
这里他们使用的是C++23 Executors,具体同时实现是libunifex。
C++23 Executors
C++23的Executors包罗万象,极难概括Executors究竟干了些什么,这里尝试着罗列一下
抽象出了执行机构Executor(类似于线程池)和调度机构Scheduler,为触发器/异构编程打好了基础。
抽象出了Sender/Receiver概念,他们是对“触发器操作”的抽象、封装。
透过Sender/Receiver,对触发器操作的cancel、exception等副作用进行了标准化的管理。这里C++借助了函数式编程技术Monad。
提供了库函数,方便触发器流程的构筑。比如说sequence、when_all、repeat_n等。
透过上述库函数,使得结构化并发(structured concurrency)成为可能。由此带来的好处能类比结构化编程,大幅降低了编程繁杂度。这是编程范式上的改变。
透过结构化并发,能将RAII拓广到触发器领域,不再须要shared_ptr/weak_ptr管理触发器对象生命周期。
整合了C++20的coroutine,透过co_await进一步简化结构化并发的编写。C++从此有了真正的Async/Await。更美好的是,coroutine能视作Sender,获得对cancel/exception更好的处理能力。
总之,透过Executors,他们能大幅简化触发器、多线程的编写。许多写法在以前是不可想象的。
Graph+Executors
透过Graph+Executors,他们在功能和性能上是能和DOTS+Jobs抗衡的,毕竟他们是原生C++配合C#的零开销抽象,打不过Jobs+Burst Compiler是有点说不过去的。
合作开发难易度见仁见智,C++语言更难,但心智模型简单。C#语言简单,但构架更繁杂。
Executors写起来差不多是下面这种感觉。
好吧,我承认C++确实有点难懂!我第一次看executor的代码也宛如天书,但认知了之后还是较为简单的,比任何C++触发器构架都要简单。
executors的表达力非常之强,整个app的构架逻辑寥寥数行就能顺利完成。像这样的逻辑,让我用asio写很可能是写不对的。
图形发动机新优点
在有了Graph构架支持后,能做的功能就许多了,首先是图形发动机部分。
他们的图形发动机由两张Graph组成,分别是执行图(Executor Graph)与内容图(Content Graph)。
执行图 Executor Graph
执行图是对硬件平台的抽象。
现在的格斗游戏对图形发动机非常苛刻,有下列许多需求必须满足。
跨各个硬件平台。移动平台须要低功耗、PC、主机平台须要挖性能。
对低、中、高配有足够的伸缩性,良好的硬件兼容性。
对不同的格斗游戏类型,图形算法管线各不相同,须要差异化。
DCC要求快速迭代,须要实时预览+快速烘培。
这些需求是互相矛盾的,一种硬件构架不能满足全部需求。发动机须要对硬件平台进行适当抽象、使得其可更改、可配置。
首先Executor Graph满足Ownership Graph概念,这是对硬件父子亲密关系的抽象。
其次Executor Graph是两个网络,不同节点(Vertex)间能传输统计数据(带宽不同)、能进行Gpu/Cpu同步。
有了Executor Graph,他们能根据不同平台、不同配置、不同算法、不同用途,自表述合适的硬件构架。比如说:
对于双显卡的笔记本电脑,他们能在集成显卡上算动画,然后在独立显卡上渲染。
对于美术用的工作站,他们能在一块显卡上预览,在另一块显卡上实时烘培。
对于烘培用的工作站,他们能完全使用4路Gpu加速烘培,减少迭代时间。
对于移动平台,他们能用最简单的构架,获得最好的兼容性和低功耗,也能对主流机型进行特别适配。
对于主机平台,他们能选择两个最优化的构架,挖掘平台的极限性能。
在合理的抽象上,Executor Graph隐藏掉同时实现细节,用户能透过声明式的编程加以控制,自动化、简化合作开发。
内容图 Content Graph
构成图形发动机的另一张图是内容图(Content Graph)。
Content Graph是对各类内容(Content)的抽象,其建立在Executor Graph之上,相关性很高,因为资源都是存放在硬件上的。
每个Content都有各自的居住证,在Executor Graph的不同节点上创建。Content能透过UUID索引,所以是个UUID Graph。
Content互相间有引用亲密关系,总体构成两个DAG(有向无圈图),包含下列这些内容:
这里解释下几个特别的Content:
Pipeline (PPL):类似Unity的Shader,决定了物体在哪些Render Queue用哪个Shader Program渲染。包含了相应的PSO管线状态。
Root Signature Graph (RSG):包含所有Shader Program
Resource Graph (RESG):跟踪了所有可读写资源的状态,根据不同的硬件Tier、用途、驻留,选择合适的分配策略。
Value Graph (VG):保存了GPU须要用到的统计数据,结构类似JSON,是个Addressable Graph。用途类似Unity的Shader.SetGlobal。
Render Dependency Graph (RDG):描述了渲染管线的Pass/Subpass流程、资源的状态转换、在哪些Executor Graph节点运行、同步等信息。
Render Queue Graph (RQG):描述了整个场景的RenderQueue排序,是个树状结构。每个节点会绑定两个Root Signature Graph节点,是多对一的亲密关系。
Render Graph (RG):最终的渲染任务,是Render Dependency Graph的实例。决定哪些RenderQueue在哪些RenderPass渲染。绑定所有用到的资源、场景、统计数据。
与Frame Graph的差别
他们的Render Graph是离线制作的,这点与Frame Graph动态计算不同。Render Graph通常只是Frame Graph的子图,并非一帧用到的所有Render Pass都拿来一起优化。这有下列许多原因:
运行时不能保证Frame Graph构建正确,还是存有编译错误、非最优化的可能。把编译放在格斗游戏运行时,是有风险的。
每帧都编译,有固定的开销,能做成离线总是更好的。
独立子图更容易定位错误,更容易单元测试、性能测试。
更好的组合性,更好F83E43Se性。Render Graph能拿来做别的事情,比如说触发器计算任务、贴图生成等。
Render Graph与Render Graph间的统计数据交换、状态跟踪,由Resource Graph负责。由于并非全局优化,性能可能达不到最优,但这个取舍我觉得能接受。
他们透过Task Graph
组织须要用到Render Graph,比如说ShadowMap计算、场景渲染、Post-Process渲染等。Task Graph须要每帧动态构建,但粒度较为粗,能包含CPU任务,十分于Unity的SRP。
设计上,他们希望透过Task Graph + Render Graph同时实现所有渲染算法。其中还有许多功能须要Scene Graph提供,比如说物件剔除、地形管理、LOD管理等。这个随着版本的升级,会慢慢加入。
资产系统(Asset)
他们的资产管理系统无耻抄袭Unity,就不赘述了。
选择Unity的原因主要就是更偏好UUID而并非Path。就像身份证和户口,身份证是唯一的,但户籍地址是会变更的。用UUID的话,资产移动会容易许多。
他们用UUID来实现硬引用(Hard link),用Path来同时实现软引用(Soft link)。
资产图(Asset Graph)
Asset Graph用于管理他们所有的资产,它拥有文件系统的树状结构。同时也是个DAG,用于表示索引亲密关系。最后是个UUID Graph,能用UUID索引。
Asset Graph大致有如下许多资产类型。
相比之前的Content Graph,大部分是相同的。
但是注意,这里的Asset和上文提到的Content是不一样的。Content是(Cook)处理后的产物,而Asset是原始的资产文件,比如说图片、fbx等。有时它们结构上的差别会很大,比如说Cook过程会把Prefab扁平化,或者把Mesh打碎成Cluster。
和Content Graph相比,这里少了Render Graph等运行时用到的内容,多了Shader Graph和Shader Module三个Shader相关的资产。
这里解释下Shader Graph和Root Signature Graph的具体用法。
Shader Graph/Shader Modules
他们的Shader Graph之前如是说过,当时没可视化看起来不直观,现在能显示啦!
他们的优点是能根据命名自动连线,编辑时大致排个序就行。
比如说上图中,节点自上而下两个个拼接,运行时自下而上逐个运行。编译成功就是能用的Shader Graph。
Shader Graph里的节点他们称为Shader Module,存放在Shader Modules里标准化管理。
为了生成最后的Shader Program,光有单个Shader是不够的。现代图形API有很强的全局性,须要通盘考虑所有资源借助与状态变换。Shader也是如此,须要总体布局Descriptor,提高命令提交性能。
我们透过Root Signature Graph来同时实现这一目的。
Root Signature Graph
Root Signature Graph (RSG)根据Descriptor的更新频率、Shader使用的集中度,构成两个树状结构。根节点的Descriptor更新频率最低(比如说Per-Frame)、叶节点的更新频率最高(比如说Per-Instance)。
在叶节点下,挂载各个Shader Program。
RSG从叶节点的Shader Program中收集所有用到的Attribute(Buffer、Texture、Constant等),生成Constant Buffer、Descriptor布局,然后分配寄存器(register),最后构建Root Signature。
在构建完Root Signature之后,就能生成真正的Shader Program了。
他们的渲染管线,是自下而上逐步构建的,从最小单元的Shader Module,组合成Shader Graph,再由Root Signature Graph统筹管理,最后交给Render Graph绘制。
这样做的好处是,每个部分是解耦的。
Shader Graph专心于图形效果的同时实现,能大幅修正效果,不用担心上游统计数据
Root Signature Graph接管了Constant Buffer布局、Descriptor布局,(大幅)降低了用户的心智负担。
Render Graph修正渲染管线的成本很低,甚至允许多套PBR、NPR管线同时存有、组合使用。
传统发动机往往要同时兼顾所有方面,又没自动化工具,很容易出现错字bug。
渲染部分差不多讲完了,他们来如是说下场景组织!
Prefab (Recursive)
他们的Prefab虽然叫Prefab,但没抄Unity。
他们抄的是Pixar的USD(Universal Scene Description)。
USD是两个用于场景表达的库,主要就用于影视动画制作,Unity和UE都(部分)支持。他们的同时实现也仅支持USD的一小部分。
USD主要就由Layer构成,有点像Unity的Prefab。主要就是为了解决下列许多难题:
能用小场景一点点组合成大场景。
能程序编辑,也能美术编辑。
文件尽量只读,透过层叠的形式,在上层改写。
能分布式编辑,各自修改,最后组合。减少多人协作冲突。
压缩统计数据量,减少冗余度。
易于统计数据交换。
举个例子,下图中shot_Anim动画层引用了Buzz.usd与Woody.usd,构成了动画场景。随后shot_FX.usd特效层透过sublayer的形式组合了shot_Anim.usd,并改写了许多动画。最后shot_Lighting.usd也透过sublayer组合了shot_FX.usd,进行最后的光照合成。
这样做的好处是,Lighting、FX、Anim人员能编辑自己的usd文件,互不干扰。减少了冲突,整个DCC流程会十分畅通。
格斗游戏场景的编辑,其实也须要这样组织,合作开发中有时出现场景变动导致过场动画须要重做,这其实是能避免的。
他们的Prefab对应USD的Layer,会尽量保持兼容性,目前只同时实现了Sublayer与Reference。希望以后能直接导入.usd文件。
USD概念繁多,之前提到的Addressable、Path等概念都源自USD,非常值得学习。
脚本系统(实验中)
到现在为止,他们都是在用C++合作开发,但C++较为难写,他们发动机的写法又相对非主流(非倚赖转化成,不反转控制),不适合通常格斗游戏逻辑的编写。再加上有热更新需求,脚本系统是必须要有的。
他们希望最终用户使用脚本编写格斗游戏。
格斗游戏的脚本语言选择其实不多,Lua、JavaScript(TypeScript)、C#是较为主流的脚本语言。
他们选择的是JavaScript,原因有下列几点:
JavaScript性能足够好,部分平台能开启JIT。
格斗游戏业内已有实际应用,比如说Cocos,安全可靠。
厂商支持好,虚拟机有Google的V8、Apple的JavaScriptCore、Facebook的Hermes、Mozilla的Spider Monkey。选择许多。
生态良好,格斗游戏、Web、App相关合作开发人员许多,大家较为熟悉JS。
具体的落地方案,他们选择的是集成React-Native。
React与React-Native
React与React-Native都是facebook的开源库。
React是JavaScript的UI库,主要就用于Web前端。React-Native(RN)则是如前所述React的跨平台App构架。
为什么选择集成这么个看起来和格斗游戏开发没什么亲密关系的库?有几点原因:
React是现成的UI库,一定程度上他们不须要同时实现格斗游戏UI构架了。
React-Native的JS部分包含许多功能,比如说LogBox、Timer等,不须要再合作开发。
React-Native的C++后端能自己同时实现。比如说Office就同时实现了Windows版本,知乎轮子哥vczh(亲切)还写了RN到C++的binding库。Office都在用RN,总体还是较为靠谱的。
React-Native借助Yoga支持CSS的flex排版,Unity的UIElement也用了Yoga,算是标准同时实现了。
React-Native抽象了JS的虚拟机USB(JSI),方便了C++与JS可视化。
可以调试、热加载,利于合作开发。
ReactNative有几个线程值得注意
JavaScript线程,这个是最关键的线程,他们的脚本都在上面跑。这个线程上也能跑C++代码,通过TurboModule可视化。
Native线程,在上面跑Native Module,能和发动机直接可视化。与上面的JS线程发消息通信。
UI线程,负责排版。如果UI和发动机是独立的话,也能单独处理UI逻辑。
他们已经同时实现了ReactNative的后端,能跑格斗游戏脚本。但UI部分暂时还未全部支持。
编辑器
编辑器界面用的Dear ImGui,虽然很想用他们的React-Native来同时实现,但Dear ImGui实在太香了。预计短期内不会替换。
由于他们的构架较为面向全国统计数据,在没反射、对象标注的情况下,也能同时实现Inspector。
他们透过Executors同时实现了Active Object设计模式,管理统计数据的读写冲突。
多亏了Executors,写编辑器还是较为容易的,总体是多线程触发器的,用起来没什么卡顿。遇到有些繁杂操作极难原子化的时候,他们会弹出进度条阻止用户进一步操作,比如说打开场景。
展望
至此他们顺利完成了一个最低限度的格斗游戏发动机,有渲染、有脚本、有资产导入。
未来还有更多的功能能同时实现,目前的规划是:
更多的图形功能,进一步验证Render Graph,提高镜头表现。同时增强Scene Graph功能。
可视化脚本系统 (Blueprint),他们称为Trigger Graph。希望能导出成C++与JS,同时满足快速迭代、性能、热更新。
结语
真心感谢看完的朋友,希望这首诗能激发大家更多的灵感。
有许多细节没具体展开,以后再做详细如是说吧。