一个国人做的游戏引擎开发完成!

2023-05-30 0 1,086

发动机叫: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有著相异亲密关系

Graph与ECS的差别是他们透过C#同时实现F83E43Se,而并非倚赖转化成。

他们的同时实现有下列的特征:

如前所述Adjacency List的Graph同时实现,能透过OutEdge List同时实现索引关系,能很好的表示Entity与Entity间的亲密关系。对于图状的业务逻辑,有著天生的支持,比如说SceneGraph,RenderGraph等。

如前所述boost.graph的C#同时实现,有著业内精心打磨过的USB设计,适应性广,易于课堂教学。

Graph有著明确的业务范围,只维护核心的图状统计数据结构。构架感较为弱,统计数据感更强。

前人对Graph的研究非常透彻,归纳出了下列许多基本Concepts:

一个国人做的游戏引擎开发完成!

Graph基本分类

他们能选择合适的统计数据结构来同时实现相应的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写起来差不多是下面这种感觉。

一个国人做的游戏引擎开发完成!借助libunifex同时实现的app流程

好吧,我承认C++确实有点难懂!我第一次看executor的代码也宛如天书,但认知了之后还是较为简单的,比任何C++触发器构架都要简单。

executors的表达力非常之强,整个app的构架逻辑寥寥数行就能顺利完成。像这样的逻辑,让我用asio写很可能是写不对的。

图形发动机新优点

在有了Graph构架支持后,能做的功能就许多了,首先是图形发动机部分。

他们的图形发动机由两张Graph组成,分别是执行图(Executor Graph)与内容图(Content Graph)。

执行图 Executor Graph

执行图是对硬件平台的抽象。

现在的格斗游戏对图形发动机非常苛刻,有下列许多需求必须满足。

跨各个硬件平台。移动平台须要低功耗、PC、主机平台须要挖性能。

对低、中、高配有足够的伸缩性,良好的硬件兼容性。

对不同的格斗游戏类型,图形算法管线各不相同,须要差异化。

DCC要求快速迭代,须要实时预览+快速烘培。

这些需求是互相矛盾的,一种硬件构架不能满足全部需求。发动机须要对硬件平台进行适当抽象、使得其可更改、可配置。

一个国人做的游戏引擎开发完成!Executor Graph示意图

首先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 Graph。

Shader Graph里的节点他们称为Shader Module,存放在Shader Modules里标准化管理。

一个国人做的游戏引擎开发完成!

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。

一个国人做的游戏引擎开发完成!Root Signature Graph例子。三个RSG节点分别为Depth和Scene

RSG从叶节点的Shader Program中收集所有用到的Attribute(Buffer、Texture、Constant等),生成Constant Buffer、Descriptor布局,然后分配寄存器(register),最后构建Root Signature。

一个国人做的游戏引擎开发完成!

在构建完Root Signature之后,就能生成真正的Shader Program了。

一个国人做的游戏引擎开发完成!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,进行最后的光照合成。

一个国人做的游戏引擎开发完成!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与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可视化。

可以调试、热加载,利于合作开发。

一个国人做的游戏引擎开发完成!React Native现在的构架

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,同时满足快速迭代、性能、热更新。

结语

真心感谢看完的朋友,希望这首诗能激发大家更多的灵感。

有许多细节没具体展开,以后再做详细如是说吧。

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务