在Gameplay Programmer的工作之余,我还能做些什么,来探索一些更有意义的东西呢?Mu引擎的开始,是我给这个问题的一个答案。
工作后的业余时间里,尽管我一直在做着一些小的项目方面的探索,但终究碍于没有大把的时间能够投入,并没有一些很好的产出。去年曾经尝试移植了AcademyCraft到MC 1.12.2,并有着去做一些新内容的愿望,但在移植完成以后出于精力产出比的考量,还是放弃了。
果然还是想做一些更有自己的表达、更有个人标签的作品在里面。一些曾经做过的东西已经发展到了顶点,而另一些东西需要被创造和孕育——我内心的声音,坚定的如此说道。
去做一些小而有趣的demo,或者去做一个长期且有深度的实验 —— 不管怎样,都要为未来的独立游戏开发打好基础。而Mu引擎,这个基于Rust语言、ECS架构的实验性引擎,属于后一种尝试。
缘起
游戏引擎,是个游戏程序员光听名字就心生敬畏的存在。毕竟它意味着百万甚至千万行级别的代码库、错综复杂的依赖关系,以及数不清的相互关联的模块。在如Unity和Unreal这样的泛用型引擎越来越完善的时代,自己重新再造一个游戏引擎听起来似乎只是“reinvent the wheel”,所以为何要庸人自扰呢?
首先,大的codebase正意味着臃肿和难以改进。摸清unity和unreal的脾性后,自然都会理解他们各自存在着各种各样的毛病。就比如说unity跨平台的各种bug,library build问题,以及加载scene必然卡住主线程等……很多问题不去修改引擎源码,根本难以得到解决,而普通的小团队绝对没有财力去负担unity源码的授权费用,也没有力气在浩如烟海的codebase里深挖。(btw,unity引擎的无数API设计/文档真的是垃圾……)
其次,rust语言比起传统的游戏开发语言有着很多诱人的特性。C++作为时代遗留的产物,尽管万能,却庞大而臃肿;C#这样的GC型语言,所造成的的GC Stall是想要保证一流的体验的游戏要去尽量避免的;lua等脚本语言就更不用说了。而rust作为一个精心考虑过的、内存安全的、支持函数式风格编程的非GC型语言,看起来天生适合游戏编程。
新事物要战胜旧事物,有一个迂回曲折的过程。C++、C#是目前游戏工业的流行选择,可能在一二十年以后它们仍然会有广泛重要的用途。但一个更好用、更安全的游戏编程语言,一定会在未来抢占新的阵地。这个探索,不如从现在就开始做。
创建一个简单但完整的rust游戏引擎,创造一个适合自己(以及大部分游戏开发者)的开发工作流,然后用这个引擎最终去ship属于自己的游戏——这是我对Mu引擎的愿景。
基础思路
1. 整合轮子,而非重新发明
Mu必然成为一个众多rust library的”缝合怪”。因为个人的力量毕竟有限,不可能从WinAPI开始写起。这个引擎的基础任务就是对各方面的资源进行整合,提供给用户(游戏开发者)一个干净、方便的体验。
2. 开发体验优先
把游戏开发的工作流放在第一位,尽可能去提供最好的编辑器体验和编码体验。
3. 跨平台
未来的游戏一定是不局限于一个平台的。pc,手机,主机……就算一开始不会全部进行支持,但也不要给跨平台支持创造障碍。
目前踩过的坑
目前Mu处于产出第一版MVP的过程中。对这个MVP的期望是:有基本的图形、资源加载、编辑器支持。目前Graphics、Asset部分的基础架子都搭的差不多了,而编辑器相关的内容还在艰难踩坑的过程中。因此先记录一下这两个模块的相关经验吧:
Graphics
关于图形渲染,一个最基本的问题是:我要用什么图形API?在十年前,OpenGL可能是低代价跨平台的最佳选择,但现在的情况正在产生变化。Vulkan、DX12、Metal这一代图形API的横空出世,代表着OpenGL这样stateful、接管大量底层实现的API退出历史舞台,而让位于更加精细、explicit的图形编程方式。
尽管如此,对于一个人来说,去花大量精力钻研这种复杂的底层图形API也是不可承受的代价。要是能有一个更加好用,能够一份调用跨多个平台的图形API就好了。抱着这样的想法,我最终寻找到了 gfx 和 wgpu-rs 这两个库。
由于之前个人知识所限,很长一段时间mu引擎采用了glium 作为图形的实现。但这个库存在几个很严重的问题:
- 只使用 OpenGL 作为 backend。这意味着跨平台的范围将非常受限;
- 很多结构没法
Send
/Sync
(比如ShaderProgram),因此没法方便的做很多多线程相关的操作; - 作者本人已经放弃了项目开发,由社区维护。这意味着这个库的未来将是不确定的。
最后,寻找一个更通用的替代品成为必然。gfx是一个vulkan-like的跨平台方案,但显得过于底层;基于其构建的 wgpu-rs 库看起来就非常符合这个引擎的需求了。
在将已经实现的渲染代码替换为wgpu的过程中,经历了一段曲折的学习过程。总体来说,wgpu这样的贴近最新图形API的库,会把很多东西变的explicit,同时也避免了很多全局状态的mutation。尽管看起来会更复杂,但确实是更不容易出bug的。其架构上的变化也让多线程渲染成为可能。(这个后面需要探索)
Asset
将文件加载为资源听起来是一件非常trivial的事情,但开发了游戏引擎之后,才发现并非“加载一个文件到某个runtime format”那么简单。例如:
- 加载asset的来源是?(对于开发期,可能是目录;对于build后,可能是打包好的二进制文件)
- 当加载的asset需要在多处共享(e.g. Sprite)时,应该如何处理?
- 如何做资源加载的cache,避免同一个文件的反复读取和重复创建?
- ……
在各种考虑下,最后将相关功能分成了 asset
和 resource
两个模块:
asset
负责将一个路径的文件读取为某个runtime format的流程。(目前只支持从文件,未来考虑做pack流程)resource
负责处理资源的共享和加载cache问题。它提供了如下结构:ResourceRef<T>
是一个可 clone、可跨线程 的资源指针ResManager
是一个全局的资源管理器,可以从作为specs的resource获取。它的作用主要是管理ResourcePool
并提供一些快速存取的函数ResourcePool<T>
是管理一个类型资源的资源池- 可以通过
add
添加一个资源,获取一个ResourceRef<T>
- 可以通过
ResourceRef<T>
获得资源的引用 - 可选的缓存机制:通过
add_with_key
;get_by_key
,根据路径(或者其他)key,添加和获取资源。 - 引用计数机制:当指向一资源的
ResourceRef
数量归零时,在帧末删除资源。(潜在的性能影响,可能有更合适的时机?)
- 可以通过
可以看到,目前的设计把资源管理的问题完全让渡给了 resource
模块,并和资源加载解耦。
总而言之,目前Mu引擎还处在一个急速发展的阶段。之后我也会持续把开发过程中的各种设计思路和想法分享出来。