从0开始手敲游戏引擎

现在的游戏引擎越来越成熟,业务框架也愈发完善,客户端程序员往往只需要做简单的逻辑工作就足以支持策划的需求,或者只制作结点与工具提供给策划配置即可,非常便利
但是,这也会使客户端程序过多停留于上层业务,而疏远底层技术的实现,导致对于进阶的需求或底层bug容易束手无策,只能求救引擎程序员。而引擎程序员显然是没有那么多energy熟悉业务需求的,最好还是客户端程序自己能够解决。那么,如何保持客户端程序不因业务逻辑而迷失,保持自己的技术竞争力呢?当然是熟悉游戏引擎底层架构了!

我理解的客户端程序
  1. gameplay技术扎实,能轻易处理各种业务工作,与策划无缝协作
  2. 能从更加宏观的角度解决问题,如对流程、机制、框架的优化
  3. 有技术追求,熟悉相关引擎模块实现,能进行引擎改进、性能优化等,熟络核心技术

那么,就从0开始开发runtime引擎,看看游戏引擎背后的本质吧

环境搭建
git,Cmake,BuildType
用_ASSERT宏,看清Assert失败的具体文件和行号
BuildType为Debug下,_ASSERT生效:
Release下,_ASSERT这个宏被跳过,不会被编译,所以不会触发相关逻辑

OS-API SDKs-Platform Layer(平台独立层,抹除平台差异)-Core System-Resource-Low Level Renderer-Systems

数学运算库
数学运算在游戏引擎中随处可见。渲染流程中需要计算各种空间变换和各种裁剪优化算法,物理模拟中的碰撞检测算法,动画模块中骨骼矩阵的计算,都离不开数学运算库。数学运算的实现直接影响到所有其他模块的运算效率。在一般商用引擎中,这个部分都会进行深度优化,以达到极致的性能表现,避免成为引擎的性能瓶颈
有成熟的数学运算库,如DirectXMath 或 跨平台能力比较强的Eigen
点与向量、矩阵、变换、四元数、

内存管理
操作系统本身管理着非常多复杂的数据结构,例如进程、线程、文件结构、网络流等等。这些结构本身需要占用系统内存,而且也会在运行过程中不停地动态分配和释放。
一方面,随机地分配和释放内存,会在连续内存中形成内存碎片,影响内存整体的可用性。另一方面,操作系统作为底层管理,希望尽量留出更多的内存供上层应用使用。因此,操作系统需要一种高效的内存管理算法,能克服内存碎片的同时,尽量保持高效。

目前大部分操作系统的内存管理算法都采用伙伴算法,或者伙伴算法的一些变种,来管理操作系统内核本身的内存。伙伴算法的核心思想其实也很简单,总结为以下几点:
  1. 内存块按照2的倍数进行划分。
  2. 空闲内存块以链表形式组织。
  3. 伙伴位图来记录伙伴内存块的空闲情况。
  4. 分配内存时,如果小内存块没有了,就把大内存块分裂成2个小内存块。
  5. 当互为伙伴关系的小内存块都空闲时,合并为大内存块。

当然在实际应用中会有非常多的数据结构来维持算法运作,这里就不详细展开了。不过,伙伴算法是内存管理算法中必须理解的思想,非常值得学习

每一个进程都有可能在任意时刻向操作系统申请任意大小的连续内存,并且希望操作系统尽快提供。
  • 但是我们知道,内存分配是一个系统调用,而系统调用的执行会发生内核态和用户态切换,是一个非常耗时的过程。我们并不希望这样的过程频繁发生。
  • 实际上,一个应用进程想要运行在目标操作系统上,必须要有运行库[3]支撑。运行库一方面对不同的操作系统之间的差异进行了高级抽象,使得一些具有平台差异的底层功能可以直接在不同操作系统之间移植。另一方面也为程序运行提供大量基本功能,使得开发者不需要每次重复编写这些基础功能。
  • 在游戏领域,不管是服务端还是客户端,都存在大量动态分配内存的情况。使用tcmalloc或者jemallo来加强内存分配的效率是一个不错的选择

像游戏这样的应用进程,对实时性要求极高,因此我们想尽量避免在运行时向频繁地向操作系统申请内存一般做法是一次性向操作系统申请大量内存,然后由进程自身管理这些内存。这样内存的申请与释放只会发生再进程内部,而不会影响到操作系统。但是这也意味着进程本身需要针对自身的特点,实现一个稳定、高效的内存管理模块,这并不是一件简单的事情。

另外,对于有经验的游戏程序员来说,除了游戏引擎底层的内存管理外,还会在上层业务系统进行内存管理。这种情况一般发生在系统中出现大量可复用对象时,例如UI控件、特效对象等等。由业务系统自己管理这些可复用对象,动态地分配和回收,可以节省对象在引擎层面的构造时间,进一步提高系统效率,是游戏开发中非常常见的优化手段之一。

基于Free List的内存分配模块
  • MemoryManager:提供Allocate和Free接口给其他模块调用。
  • Allocator:实际处理内存分配的小单元。一个MemoryManager下管理多个Allocator。每一个Allocator负责特定大小的内存块分配。内存块大小以2的幂倍增。
  • Freelist:每个Allocator上都有一个Freelist来维护当前大小还有多少可用内存块。
  • Block:特定大小的内存块。例如4、8、16、32等。当申请大小为7的内存块时,会得到大小为8的内存块。当申请大小为14的内存块时,会得到大小为16的内存块。
  • Page:每次向操作系统申请内存的单位。根据对应的Allocator把Page划分成若干个小块后挂接到Freelist上。


基础渲染
光线投射、入射,主要用于3D建模
光线追踪、折射、反射、二次射线,需要处理噪点
光栅化、三角形、图元、插值
图元光栅化示意图
DX渲染管线

接入API
Blender光追 效果很好
OpenGL 状态机
由于和DX在概念上是相似的,可以封装相同的部分,对上层来说结构都一样,做到跨平台抽象

天空盒和光照模型
Blinn-Phone这种是经验模型了,调效果只能依据经验,现在早已变成PBR(能量守恒)、BRDF


PBR

蒙皮、动画、UI

物理引擎






comments powered by Disqus