Doom3源码剖析(2)--架构简介(译)
前段时间看到国外一博客上的几篇Doom3代码分析的文章,感觉分析得不错,就自己翻译了一下,以下是译文,有不太理解地方就直接贴了原文。大部分是按自己的理解翻译的,可能有些地方译的并不精确。有喜欢的朋友可以在这里看原文:http://fabiensanglard.net/doom3/,这里欢迎各种讨论。
简介
2011年11月23日,id Software依照惯例发布了前一代引擎的源码,这次发布的是Doom3引擎,id Tech 4。
Within hours the GitHub repository was forked more than 400 times,大家在不同的平台上编译并且查看游戏的内部实现机制。我也马上开始了Mac版本代码的学习。
这份代码在清晰度和注释方面,是继id Software发布Doom iPhone代码后,最好的一份代码。我建议每一个爱好者都去读一读,并研习这份代码的实现。
这里是我的笔记,里面记录了我理解到的一些内容。像往常一样,我将其整理地清晰了一些,我希望它能帮助一些朋友节省一些时间,并且我也希望它能促使我们阅读更多的代码,并成为更优秀的程序员。
背景
拿到这份具有开创性的引擎代码是一件很令人兴奋的事情。早在2004年Doom3发行时,它确立了实时3D图形和音频的新标准。它首次使技术上允许美术创作人员像好莱坞模式那样去表达自己的想法。尽管8年过去了,首次在Delta Labs 4遇见HellKnight的情景仍然很震撼:
视频在这里
由卡马克创造的“统一的照明和阴影”概念在这个视频里表现得很有活力,我将在后面的渲染部分再介绍这部分内容。
初次的接触
Doom3源代码发布现在开始用github,而不再是以前的ftp server。
从TTimo发布的原始代码很容易在Visual Studio 2010上编译。不幸的是,不能用Visual Studio 2010 Express版编译,因为Doom3代码中使用了MFC,而Express版没有。
windows代码在这里:git clone https://github.com/TTimo/doom3.gpl.git
我喜欢在Mac系统下用XCode浏览代码。相比于Visual Studio,它在搜索速度和变量的高亮方面都给我很享受的体验。我想用XCode来编译程序,但是项目设置有一些问题。不过很容易通过几个简单的步骤修复好。这里有一个github已包含了所有的修改。
git clone https://github.com/badsector/Doom3-for-MacOSX-
这两份代码都很容易编译,可以一键完成。
杂记:为了运行游戏,base文件夹下需要包含Doom3 的资源。由于我的机器上没有cd驱动器,所以我下载了Steam版本。似乎id Software也做了同样的事,在Visual Studio的项目设置里仍然包含了这样的字符"+set fs_basepath C:\Program Files (x86)\Steam\steamapps\common\doom 3" !
架构
解决方案(solution)中项目的划分反映了整个引擎的架构
下面以更可视化的方式来概括这个架构
Doom3的大部分代码在2004年10月的时候可以通过发布的Doom3 SDK中看到,但是SDK里没有Doom3执行文件部分代码。在SDK中可以构建idLib部分和gamex86部分,核心引擎部分那时仍然是闭源的。
注意:在游戏模块(gamex86),每一个类都是从idClass继承的,这样引擎可以执行内部RTTI,并且也可以通过类名字来实例化类。
杂记:你仔细观察上面的架构图,可能会发现有一些基本框架(例如FileSystem)只在Doom3.exe中。这里就会有一个问题,就是当gamex86也要加载资源的时候。
问:那么gamex86是如何处理的呢?
答:这些子系统是从Doom3.exe中动态加载到gamex86.dll中的,这也是指示箭头出现的原因。
如果我们使用PE explorer等工具查看gamex86.dll,我们会看到gamex86.dll导出了一个函数:GetGameAPI:
这是同Quake2加载renderer和game 动态库方式一模一样:当Doom3启动gamex86.dll的时候,会交换对象的指针
- 通过LoadLibrary将dll加载到进程的内存空间中
- 通过GetProcAddress获取GetGameAPI在gamex86.dll中的地址
- 调用GetGameAPI
gameExport_t * GetGameAPI_t( gameImport_t *import );
在最后完成“握手”时,Doom3.exe有一个指针指向idGame对象,gamex86.dll有一个指针指向gameImport_t对象,这个对象包含了gamex86.dll中没有的子系统,比如idFileSystem。
typedef struct {
int version; // API version
idSys * sys; // non-portable system services
idCommon * common; // common
idCmdSystem * cmdSystem; // console command system
idCVarSystem * cvarSystem; // console variable system
idFileSystem * fileSystem; // file system
idNetworkSystem * networkSystem; // network system
idRenderSystem * renderSystem; // render system
idSoundSystem * soundSystem; // sound system
idRenderModelManager * renderModelManager; // render model manager
idUserInterfaceManager * uiManager; // user interface manager
idDeclManager * declManager; // declaration manager
idAASFileManager * AASFileManager; // AAS file manager
idCollisionModelManager * collisionModelManager; // collision model manager
} gameImport_t;
typedef struct {
int version; // API version
idGame * game; // interface to run the game
idGameEdit * gameEdit; // interface for in-game editing
} gameExport_t;
注意:一个好消息是,id tech4的开发者在Doom3 SDK 文档页很好的描述了各个子系统。
C… ++ ?!
- 在id Software历史上这是第一次用C++代码代替了C。关于此事我问了卡马克:
"
Hello John,
I wonder what motivated to move the team to C++ for idtech4.
Fab
"
"
There was a sense of inevitability to it at that point, but only about half the programmers really had C++ background in the beginning. I had C and Objective-C background, and I sort of "slid into C++" by just looking at the code that the C++ guys were writing. In hindsight, I wish I had budgeted the time to thoroughly research and explore the language before just starting to use it.
You may still be able to tell that the renderer code was largely developed in C, then sort of skinned into C++.
Today, I do firmly believe that C++ is the right language for large, multi-developer projects with critical performance requirements, and Tech 5 is a lot better off for the Doom 3 experience.
John Carmack
"
他也在2004年的时候有评论:
"
Is C++ a better language for developing games than C? Today, most games are developed in C++, and I know that Doom 3 was developed in C++. Having worked with both C and C++ for quite a while, do you think C++ is really a better language for developing games?"
"
Yes, it is. I'm not a fan of the more complex features of C++, but grouping things into classes is a Good Thing.
John Carmack
"
- 代码中使用了大量抽象和多态。但是一个漂亮的技巧可以避免某些对象因虚函数表而带来的性能问题。
- 所有的资源都使用可读性很好的文本方式存储。没有太多的二进制内容。代码中广泛地使用了Lexer/Parser的组合。
- 模板被用在低层实用类中(idLib),但是从不会在高层代码中出现,不会像Google的V8代码那样刺瞎你的眼。
- 在代码注释方面,它是id Software的代码中第二好的,最好的是Doom iPhone代码,可能因为它比Doom3是更近的一段时间编写的。
- 看一下编码约定也是很有趣的事情,在这里。
主循环概览
这里是主循环展开后的代码:(更详细的在这里)
idCommonLocal commonLocal; // OS Specialized object
idCommon * common = &commonLocal; // Interface pointer (since Init is OS dependent it is an abstract method
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow )
{
Sys_SetPhysicalWorkMemory( 192 << 20, 1024 << 20 ); //Min = 201,326,592 Max = 1,073,741,824
Sys_GetCurrentMemoryStatus( exeLaunchMemoryStats );
Sys_CreateConsole();
SetErrorMode( SEM_FAILCRITICALERRORS );
for ( int i = 0; i < MAX_CRITICAL_SECTIONS; i++ ) {
InitializeCriticalSection( &win32.criticalSections[i] );
}
Sys_Milliseconds();
common->Init( 0, NULL, lpCmdLine ); // Assess how much VRAM is available (not done via OpenGL but OS call)
Sys_StartAsyncThread() // 开启异步线程
{ // 异步线程展开(译注:异步线程中并没有找到类似的代码)
while ( 1 )
{
usleep( 16666 ); // Run at 60Hz
common->Async(); // Do the job
Sys_TriggerEvent( TRIGGER_EVENT_ONE ); // Unlock other thread waiting for inputs
pthread_testcancel(); // Check if we have been cancelled by the main thread (on shutdown).
}
}
Sys_ShowConsole
while ( 1 )
{
Win_Frame();
common->Frame();
}
}
这是id Software引擎的标准主循环,除了Sys_StartAsyncThread函数表明Doom3是多线程的,这个线程的目的是为了处理时间敏感的功能,引擎不希望这部分被帧率限制住。
- 声音处理
- 用户输入处理
杂记:id Tech4的高层对象都是抽象类,这将会使程序在运行时从虚函数表中查找虚函数地址而导致性能问题。但是这里有一个“技巧”来避免这个问题。所有的对象被以静态的方式实例化,如下:
idCommonLocal commonLocal; // Implementation
idCommon * common = &commonLocal; // Interface manipulated in the code
这样编译器在编译时就可以决定被调用的函数,从而不用去查找虚函数表。这实际上是一个双赢,since the pointer does not have to be dereferenced at runtime either since its address is known at compile time.
杂记:已经读过6、7款id Software引擎的我发现,有一些函数名从doom1开始就一直没有改变过:负责处理鼠标和游戏杆输入的函数仍然叫IN_frame()
渲染器
Doom3中最令人兴奋的当然是渲染器部分。这里有太多的内容,所以我将这部分的笔记分到另一篇文章里了。在这里
如果你对(场景)预处理和入口系统(portal system)很感兴趣,可以看这里。
新的标准
新的标准可以概括为一个词:Unification
前一代引擎在渲染所有元素的时候,使用的是不同的方式,经常要花费很长时间的预处理。Quake2中的基于光能传递的lightmap,在设计者能看到最终的效果前需要等很长时间。
物理引擎
(译注:原文这里没有内容,大概还没写好)
脚本和虚拟机
(译注:原文这里没有内容,大概还没写好)
地图加载
我记忆中,加载所消耗的时间很长,我一直想知道在加载场景的时候,程序背后都做了些什么。
结语
It was not always easy to focus...
But overall it was very educationnal to read most of it.