前言
2011年11月doom3代码发布
2012年11月doom3 BFG代码发布
两个游戏内容虽然相差不多,但doom3 BFG版本在技术上有很大进化(多线程框架,SWF,立体渲染)。本文技术分析主要基于doom3BFG版本
idCommon是总控制类
绿色的是子系统,可以在各处调用。
Entity是基于继承的结构
整体分为两个线程,主线程(BackEnd)和Game线程(FrontEnd)。主线程负责获取输入,处理网络消息,调用渲染API,以及调度Game线程。Game线程主要运行游戏逻辑以及生成RenderCommandBuffer。据说这个架构在doom3的版本里就已经实现了,但当时两个线程是反过来的,主线程生成RenderCommandBuffer,子线程调用渲染API,(FrontEnd和BackEnd的名字可能就是从那时候来的),但实现后不知道为什么在某些机器上运行不稳定。因此doom3在在发布的最后时刻又改成了单线程实现。直到后来才发现OpenGL API非创建Device的线程上被调用时,在某些平台上会有问题,但所有文档上都没提过这事儿。(看来卡马克也是被各种坑啊)
主线程:
获取用户输入
接收和发送网络消息
交换RenderCommandBuffer
启动Game线程
渲染上一帧RenderCommandBuffer中的内容
Game线程:
运行一到多个逻辑帧
生成本帧的RenderCommandBuffer
说明:
1.为了保证游戏模拟结果与帧率无关(在多人网络环境中尤其要保证这一点),游戏逻辑是固定在每秒60帧的。并且时间是离散的,是毫秒的整数值,相同的输入总会得到相同的输出。(图中的RunFrame)
2.idFrameData是一个线程安全的DoubleBuffer,同一时刻一个buffer用于生成RenderCommand,另一个Buffer用于渲染。线程安全用原子操作实现。
3.整个架构只在线程对象的底层有锁的使用,上层实现中完全无锁。大量使用了只读对象和任务并行。
逻辑帧(RunFrame):
遍历entity更新
物理模拟
animation
更新RenderWorld中对应的实例
统一处理event
简单提一句event系统,event系统实现了调用的封装,这样就可以:
1. 对象间的操作可以通过网络传输,或者保存
2. 延迟调用,比如3秒后调用一下对象A的B方法
3. 脚本对象的绑定,所有的event方法都可以被脚本调用
生成RenderCommandBuffer(Draw)主要做两件事:基于BSP的精确Culling,将Culling的结果生成Light和DrawSurf_t的链表,用于渲染。Culling的过程比较复杂,下面展开讲一下。
如图
整个场景被划分成多个area,area之间由portal连接。area引用其内部的Entity和Light对象。Culling过程如下:
1.首先通过bsp树快速计算camera位于哪个area内
2.根据这个area和当前portal状态,计算出所有联通的area集合(有些portal是可以打开关闭的
3.由camera构造一个culling volume。用这个volume剔除当前area中的所有entity和light。
4.遍历area中的portal,用portal构造一个更小的culling volume(portal stack),如果volume为空,继续计算下一个portal。
5.如果不为空,用新的culling volume剔除portal相连area中的entity和light,将可见的加入可见列表。并递归第4步。
6.所有protal遍历结束后,函数返回。
Culling完成后得到所有可见的entity和light列表。随后要针对这些entity和light生成RenderCommandBuffer。分为两步,AddLight和AddModel:
AddLights
遍历链表中的Light
EvalueateRegister
复制渲染所需的数据
遍历light作用到的area和area内所有的entity
culling,确定一个light是否照到一个entity(只计算动态的entity,静态的是预计算好的,即interaction)
shadow相关
AddModel
遍历链表中的entity
遍历链表中的light,找到照到entity的所有Light
分配baseSurface,用于DepthPass
对于找到的light,依次分配surface
将DrawSurface链接到对应的light的链表中
需要指出的是,针对每一个Light的AddLight操作和每个Entity的AddModel操作都可以运行在单独的线程中。可以这样做是基于:
1. 线程安全的FrameDataBuffer,新的计算结果记录到分配出的对象中,并用链表链接到当前对象中
2. 引用到的其他对象都是只读的
大部分场景和光源都是静态的,如何加速静态场景的culling和渲染?
每个PortalArea本身也是一个Entity,并对应一个mesh
每个(Light,Entity)对生成一个对象,即Interaction
蒙皮模型和粒子不会计算interaction
场景加载后会预计算所有的静态的interaction,精确地计算被光源照到的表面的index
预计算的index会用于渲染(AddModel的过程中加入链表)
一个light移动后对应所有的interaction会被释放
橘黄色的都是本帧从FrameData中分配出的临时对象,这些对象保存在RenderCommandBuffer中,在下一个Frame中,主线程会使用这些数据进行渲染,渲染完后这些内存会被自动释放。蓝色的对象是从游戏逻辑模块中复制过来的数据。
多Pass,no lightmap
depth pass
每个light一个Pass
global stencil
no selfshadow
local stencil
has selfshadow
idTech系列引擎的材质系统是一个表达式求值系统。大部分的材质只是简单保存一些贴图,shader,颜色,渲染状态等。但另一些材质需要复杂一些的功能。想象一个不停地闪烁的灯,它的颜色实际上是时间t的函数。而材质系统正是提供了这样的功能。例如这样一个材质
models/weapons/soulcube/soulcube3fx { noSelfShadow translucent noShadows { if ( parm7 > 3 ) blend add map models/weapons/soulcube/soulcube3fx rgb scTable[ time * .5 ] } }
中括号内是这个材质的一个stage,parm7是使用这个材质的Entity的一个参数。这个材质的描述是当这个参数大于3时才执行这个stage,这个stage本身的操作是混合一张贴图和一个颜色,这个颜色是根据时间查表得来的。当一个Entity确定会被渲染后,在AddModel过程中会对其使用的材质做一次求值,if判断和查表操作都在这时计算。
//evaluate the reference shader to find our shader parms floatrefRegs[MAX_EXPRESSION_REGISTERS]; renderEntity->referenceShader->EvaluateRegisters(refRegs, renderEntity->shaderParms, tr.viewDef->renderView.shaderParms, tr.viewDef->renderView.time[renderEntity->timeGroup]* 0.001f, renderEntity->referenceSound );
虽然这些功能在现在的引擎中都可以在VertexShader和PixelShader中完成,但这些Entity粒度的计算确实没有必要在每个顶点,甚至每个pixel中都计算一次,更何况那还会shader组合爆炸的问题。
FPS是对实时性要求极高的游戏类型。特别是在多人网络游戏环境中,网络的带宽总是有限的,网络延迟也不可能无限小。那么如何实现一个一致的,高响应,低延迟的网络游戏架构就成为了一个极具挑战性的工作。
整理架构
各客户端每隔40毫秒将用户输入发送给服务器,服务器运行整个游戏逻辑,物理模拟等。然后每隔100毫秒左右将整个游戏的状态存成一个快照,将快照发送给个客户端,客户端接收到快照后,将自己的状态跟快照同步。
由于网络延迟的存在,客户端和服务器不可能完全同步。实际上,客户端总是领先于服务器,理想情况下,客户端的输入发送到服务器时,服务器的时间正好和客户端获取用户输入的时间相同。当然这只是理想情况,实际情况是客户端总是要领先的更多一些。由于客户端领先于精确的游戏世界(在服务器端),因此客户端需要根据已知的游戏世界的状态做一些预测,直到服务器发来下一个游戏世界的快照。然而新收到的快照的时间也早于客户端当前的游戏时间,客户端需要退回到快照中的游戏世界,然后重新预测直到当前的游戏时间。
快照中会有其他玩家最新的输入,这样有助于更准确的预测。由于用户输入的频率远小于网络同步的频率(10-20Hz),因此玩家几乎察觉不到自己身处的游戏世界是预测来的。
网络数据压缩
doom3使用了多种网络数据压缩方法,其中最有效的的是delta compression,因为游戏世界中的大部分对象的大部分属性是不变的,改变的只是小部分,因此跟上一个快照相比,我们只需要发送这个快照不同的部分。
参考
http://fabiensanglard.net/doom3_bfg/index.php
http://fabiensanglard.net/doom3_documentation/The-DOOM-III-Network-Architecture.pdf