Tizeng's blog Ordinary Gamer

《游戏引擎架构》读书笔记

2025-11-27
Tizeng

本来没打算做笔记,感觉书上的东西都比较好懂,不想再搬运一次,但发现时间久了之前看的东西确实会没什么印象,还是做一下记录,尤其是对那些“哦原来是这样”的内容。

(2025.1.15晚)终于告一段落,书中的很多内容还是比较范的,要是早几年读会帮助更大,如果毕业前读了找工作将无敌。

第5章-游戏支持系统

这一章讲了一些基本的子系统如何管理和初始化的问题。

内存对齐

有一节说到如果内存要按4字节对齐,则内存地址的最低有效位只能是0x0、0x4、0x8和0xC,写成二进制分别是0b0000、0b0100、0b1000、0b1100,即最后两位必须位0。 原因是只有这样这个数才是4的倍数,想一下确实是这样没错,因为要是4的倍数,则必须增加整数倍的0b0100,那么最后两位永远会是0。 那么为什么只需要4就行了呢?我们要对齐的是4个字节32位,4只有两位而已。答案是地址的单位是字节,而不是位,也就是说每个地址对应的门牌号下的数据就有1个字节8位,因此只要这个地址被4整除,则必定是按4字节对齐的。 还有一个隐含信息,若一个操作系统的指针大小是4字节,则这个系统可以分配的内存就是2的32次方字节,即4GB。 有所得,AI的确增加了不少学习效率。

要将地址对齐至所需的大小(必须是2的幂),先取出最低有效位LSB(least significant byte),方法是用一个mask掩码做一次与运算,这个掩码用需要对齐的字节数大小减去1即可得到。 然后就可以用需要对齐的字节数减去取出的LSB,就是地址需要补上的字节数。

内存池

在使用内存池时,会提前分配一些内存,使用一个freelist去保存池子里的空闲元素(之前看虚幻中一些容器的实现和lua源码时都有遇到),而我们需要指针指向这些元素来进行索引,但实际上,由于这些元素是空闲的,我们可以直接利用它们本身,储存下一个元素的索引来形成链表,只要这个链表的长度不超过每个元素大小所能表示的极限(如16位可以支持65536个元素)。 很聪明的做法。

内存重定位

当内存经过多次分配和释放之后,可能会存在很多碎片,导致缓存命中率下降,为了清理这些碎片,我们需要进行内存重定位,即合并那些散落的内存片段。 这个操作的一个副作用是会改变内存的地址,毕竟它被移动到了别处,会导致外部之前缓存的地址失效,要解决这个问题可以使用句柄(handle),我们每次执行分配后返回的是一个索引,实际的指针或地址储存在一张表中,我们通过这个索引去查找真正的地址。 这种情况下执行内存重定位,只需保证前后内存中的内容不变,而不必担心地址发生变化,因为外部并不知道具体的地址。

第7章-游戏循环

介绍了游戏的基本循环逻辑,也就是常说的tick。 其中提到时钟变量的概念,计算机通过启动后cpu进行的周期数目,来计算过了多长的时间,cpu的运行速度越快,频率越高,一秒钟所经过的cpu周期数目就越多,我们可以用一个64位整数,来记录经过了多少周期,已知cpu的频率,就可以计算出时间。

浮点数只适合存储相对较短的持续时间,计算帧间隔时,是先用前后经过的64位时钟周期相减,再将结果转化为float。要知道精确的周期信息,应该使用int64。

多处理器下处理循环时,不同硬件的方式不一样,有的是多个大核(xbox360),有的则是一个大核+多个小核(ps3)。

多线程有两种形式:

  • 任务模型:每个主要子系统运行于独立线程,动画、物理、渲染等
  • 作业模型:将工作分割为多个细小、独立的作业(job),加入队列,交给闲置的处理器去处理,这样做的优点是可以减少处理器的空闲,但会增加信息通信成本

第10章-渲染

计算阴影时,有一个阴影贴图的技术,是事先在光源位置和方向渲染一张贴图,计算从光源出发,第一个被遮挡的距离,然后将这个距离储存在贴图坐标中,在最终计算像素颜色的时候,根据片段对应的世界坐标,比较该位置距光源的距离和对应贴图中储存深度的大小,如果比它大,则表示被遮挡。 由于只需要储存深度数据,通常使用一个24位或32位浮点数,达到比较精确的效果,而非传统的8位灰度。 很有意思的技术。

第11章-动画

引擎并不在意骨骼,只在意关节。

局部关节是相对直属父关节指定的,其变换结果是将以该关节为坐标系表示的点变换到父关节空间表示的点或矢量。

第12章-碰撞和刚体

形状是否为凸是个很重要的性质,能让很多问题变得容易解决,而凹形状可以分解为多个凸形状的组合,因此往往只要解决了凸,同样的问题也可以在凹上解决。 如何判断多边形是否为凸?有下面几个常用的方法:

  • 所有点的连线都在多边形内部,即不会穿过形状表面两次或以上
  • 对凸体积来说,可以检测所有三角形的法线,其方向不存在其他三角形

要判断两个形状是否重合,可以使用分离轴定理,即是否可以找到一条直线(或平面),使两个形状在其上的投影不相交,如果可以则说明这两个凸形状也不相交。 由于AABB(axis-aligned bounding box)是轴对齐的,若存在分离轴则必定是坐标轴,因此检测两个AABB是否相交只需要检查每个轴上的最小最大坐标是否重叠。

GJK算法是一个非常高效的检测凸形状是否重合的算法,具体流程还没时间细看,主要思路是利用闵可夫斯基差,计算两个形状内所有点的差,如果这两个形状相交则一定存在至少一个相同的点,它们的差是0,即原点。 那么我们就可以利用某种方法在尽量少计算点的情况下,朝原点去逼近,如果能在闵可夫斯基差中找到一个三角形(或四面体)让其包含原点,由于是凸多边形它们必然被总集合包裹,就可以证明这两个形状是有相交的,否则就没有。

刚体动力学

在游戏世界中,我们可以做一些假设来简化物理的模拟,如物体都遵循牛顿运动定律、物体都是完美的刚体(不发生形变)。

将物体的运动分解为线性动力学和旋转动力学,前者可以将物体看作一个理想化的质点,让我们只关心旋转以外的运动。

把力看作是时间、位置、速度和加速度变化的函数,它们又都和t有关且是导数关系,那么它实际上可以看作是一个常微分方程,如果能对它求解,我们就可以得到一个随时间变化的函数,得到物体随时间变化的位置和速度信息。妙呀,从没这样考虑过。

在游戏和物理模拟中,常用韦尔莱积分来得到常微分方程的近似解。 它利用的是对位置函数r(t1+dt)和r(t1-dt)分别进行泰勒展开然后求和,这样会约掉位置函数的一阶导数(即速度),留下二阶导数(加速度),三阶导数和其他奇数阶也被约去,由于dt非常小,我们可以再忽略其他项,然后用受力和质量来表示加速度,这样就可以用上一个位置r(t1-dt)和当前位置r(t1),得到r(t1+dt)即下一个时刻的位置。 速度可以直接使用求得的下一时刻的位置,与当前位置的插值以及dt得到。具体公式就不列了,主要是思路。 泰勒级数还是牛逼。

游戏驱动刚体

12.5.1.2节提到,在物理世界中移动游戏驱动刚体,不能简单的每帧设置其位置和方向,会产生不连贯性,通常会使用冲量来移动,以时间向前积分得到刚体在时步末时的位置。 这个不是很理解,根据之前描述的,游戏驱动的刚体直接被当作无限大质量不就行了吗

弹道

在射击游戏中,玩家可能从某个缝隙看到目标,但是其碰撞是实心的,要解决这个情况可以放弃碰撞查询而使用渲染查询,在某个渲染阶段中生成一张纹理,其每个像素对应一个游戏对象的唯一标识符,然后判断准星下是否为敌人角色即可。 从没想过还能这样。

第14章-运行时游戏性系统

除了常见的面向对象,以对象为中心的架构,还有以属性为中心的架构,即所有游戏对象都是属性的集合,属性按类型被集中管理,可以存在数据库或表中,以游戏对象的唯一标识符作为索引。由此衍生出两种内存布局

  • SoA(struct of array):属性为中心的布局,只有很少数的struct,但其中包含了所有属性类型的数组,这样可以最大程度的增加内存的缓存命中率,因为相同类型的数据此时在内存中是连续存储的
  • AoS(array of struct):传统的布局,所有不同类型的属性都放在同一个struct中,由这个struct去构成数组

上一篇 ALS学习

Comments

Content