在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就好了。抱着这样的想法,我最终寻找到了 gfxwgpu-rs 这两个库。

由于之前个人知识所限,很长一段时间mu引擎采用了glium 作为图形的实现。但这个库存在几个很严重的问题:

  • 只使用 OpenGL 作为 backend。这意味着跨平台的范围将非常受限;
  • 很多结构没法 Send / Sync(比如ShaderProgram),因此没法方便的做很多多线程相关的操作;
  • 作者本人已经放弃了项目开发,由社区维护。这意味着这个库的未来将是不确定的。

最后,寻找一个更通用的替代品成为必然。gfx是一个vulkan-like的跨平台方案,但显得过于底层;基于其构建的 wgpu-rs 库看起来就非常符合这个引擎的需求了。

在将已经实现的渲染代码替换为wgpu的过程中,经历了一段曲折的学习过程。总体来说,wgpu这样的贴近最新图形API的库,会把很多东西变的explicit,同时也避免了很多全局状态的mutation。尽管看起来会更复杂,但确实是更不容易出bug的。其架构上的变化也让多线程渲染成为可能。(这个后面需要探索)

Asset

将文件加载为资源听起来是一件非常trivial的事情,但开发了游戏引擎之后,才发现并非“加载一个文件到某个runtime format”那么简单。例如:

  1. 加载asset的来源是?(对于开发期,可能是目录;对于build后,可能是打包好的二进制文件)
  2. 当加载的asset需要在多处共享(e.g. Sprite)时,应该如何处理?
  3. 如何做资源加载的cache,避免同一个文件的反复读取和重复创建?
  4. ……

在各种考虑下,最后将相关功能分成了 assetresource 两个模块:

  • asset 负责将一个路径的文件读取为某个runtime format的流程。(目前只支持从文件,未来考虑做pack流程)
  • resource 负责处理资源的共享和加载cache问题。它提供了如下结构:
    • ResourceRef<T> 是一个可 clone、可跨线程 的资源指针
    • ResManager 是一个全局的资源管理器,可以从作为specs的resource获取。它的作用主要是管理 ResourcePool 并提供一些快速存取的函数
    • ResourcePool<T> 是管理一个类型资源的资源池
      • 可以通过 add 添加一个资源,获取一个 ResourceRef<T>
      • 可以通过 ResourceRef<T> 获得资源的引用
      • 可选的缓存机制:通过 add_with_keyget_by_key,根据路径(或者其他)key,添加和获取资源。
      • 引用计数机制:当指向一资源的 ResourceRef 数量归零时,在帧末删除资源。(潜在的性能影响,可能有更合适的时机?)

可以看到,目前的设计把资源管理的问题完全让渡给了 resource 模块,并和资源加载解耦。


总而言之,目前Mu引擎还处在一个急速发展的阶段。之后我也会持续把开发过程中的各种设计思路和想法分享出来。