【译】Doom3源码剖析(1/6)——引论

原文地址:doom3 source code review

转载请注明出处:【译】Doom3源码剖析(1/6)——引论



在2011年11月23号,id Software继续维持他们开放源码的作风,开放了他们先前游戏引擎的源代码。这次公布的源码是idTech4,这款游戏引擎曾用来制作猎魂,雷神之锤4,当然还有毁灭战士3。公布源代码之后数小时之内,Github上就已经fork了400多次。同时人们开始探索这款游戏的内部实现机制,并试着将该游戏转移到其他平台上。我(下文均指本文作者)也立即实现了Mac OS X Intel 版本,并得到了John Carmack的肯定

根据别人的评价,这次新发布的引擎是自从Doom iPhone发布后最好的代码了。我强烈推荐每个人来阅读这份代码,编译并运行它。

这份笔记记录了我对代码的理解,照例,我好好整理了一下这份笔记:我希望这可以帮助到一些人,我也希望这可以鼓励我们中间的一些人去读更多的代码,并成为更优秀的程序员。

第一部分:概述

第二部分:dmap工具

第三部分:渲染

第四部分:性能剖析

第五部分:脚本

第六部分:访谈(包括与John Carmack的对答)

从笔记到文章…



我意识到我将使用越来越多的图片以及越来越少的文字来解释代码。如今我已经使用gliffy(一种制作流程图的工具)去作图,但是这个工具有些限制(比如缺少alpha channel(此处不明,可能作者是要表达图形不能设置透明度吧))。我在想是否存在一个专门用来剖析3D引擎的工具,这个工具将使用SVG(可缩放矢量图形)和Javascript,不过我怀疑这样的工具是否存在?咳咳,扯远了,回到代码上…

背景



着手剖析这样一个史无前例的源代码是一件让人兴奋的事。2004年Doom3的发布,意味着它在视觉效果和音频效果上成为实时引擎上的一个新标准。最有名的就是游戏中使用的“Unified Lighting and Shadows”(使用了一个统一的光照阴影模型来实时渲染场景中的光照和阴影,这意味着光照是全局光照,而非局部光照,另外阴影可能使用了阴影体(锥)技术,上课时老师说Doom3确实使用了阴影体(锥)技术)。这种技术的出现使艺术家可以做出好莱坞一样的效果。即使8年后(这篇文章写于2012年6月8号),当你在游戏中的德尔塔实验室4区遇到HellKnight时,仍会被惊艳到。(原网址上图片显示不出来,在网上盗图一张)

初次接触



目前Doom3源码发布到Github上了,而不是之前的FTP服务器上,因为id Software公司的FTP服务器总是当机或过载。

最初的发布版本来自TTimo,可以使用VS2010专业版进行编译。不幸地是,VS2010 Express版本因为缺少MFC而不能使用。这确实令人失望,但是有些人已经移除了代码中对MFC的依赖,想要无MFC的代码请猛戳这里

获取Doom3的VS工程:

Window 7 :
==========
git clone https://github.com/TTimo/doom3.gpl.git

注:我(真的是我)使用的是git clone https://github.com/id-Software/DOOM-3.git。不过这份源码也是从TTimo fork过来的,主要因为我使用TTimo版本源码会报一个诡异的编译错误。

打开neo文件夹下的doom.sln。

注:具体编译运行过程,请参见这里,还有这里

对于阅读代码和剖析代码,我更喜欢使用Mac OS X上的XCode 4.0:因为它的搜索速度很快,支持语法高亮,并且可是使用Mac中的“Common键+鼠标左键”可以快速找到函数和变量的定义位置,这些功能给我一种超越VS的体验。XCode的工程有些小问题,但是经过几步简单修复即可。并且现在在Github上有一个“bad sector”的repository,这个repository上有一份源代码可以在Mac OS X Lion上很好运行。

获取Doom3的Xcode工程:

MacOS X :
=========
git clone https://github.com/badsector/Doom3-for-MacOSX-.git

打开工程。

注意:貌似VS2010在装了Visual Studio 2010 Productivity Power Tools后,可以使用语法高亮和“Control-Click”功能。我很难理解为什么不把这些平常的功能集成到VS2010中。

现在两份代码都已经准备就绪:只要轻轻按下一个键就可以生成可执行文件了!

  • 下载代码
  • 按下F8/Command-B
  • 运行!

Trivia:为了运行游戏,你必须把包含Doom3游戏资源的base文件夹覆盖到源代码的base文件夹里,因为源代码的base文件夹里面是空的,除了一个空的default.cfg。因为我不想浪费时间从Doom3的CD中解压资源文件,然后还要更新到1.3.1版本。我直接下载Steam版本。看上去id Software团队也做了同样的事,因为在VS工程的调试设置中,仍然存在“+set fs_basepath C:\Program Files (x86)\Steam\steamapps\common\doom 3”(你可以修改该路径以符合你的源代码位置)。

Trivia:游戏引擎的开发是用Visual Studio .NET(源码),但是代码里面却没有一行C#代码,并且为了正常编译,应使用VS2010专业版。

Trivia:Id Software开发团队看上去是黑客帝国的骨灰粉:Quake Ⅲ工作目录名称是“Trinity”(黑客帝国中的角色),而Doom3的源码都放在名叫“neo”(黑客帝国中的角色)的子目录下。

架构



整个工程被分为好几个解决方案,这些解决方案就反应了整个引擎的架构:

Projects Builds                 Observations
  Windows MacO SX  
Game gamex86.dll gamex86.so Doom3 gameplay(Doom3原版的游戏逻辑)
Game-d3xp gamex86.dll gamex86.so Doom3 eXPension (Ressurection) gameplay(Doom3邪恶复苏的游戏逻辑)
MayaImport MayaImport.dll - Part of the assets creation toolchain: Loaded at runtime in order to open Maya files and import monsters, camera path and maps.(运行时加载游戏资源文件,包括打开Maya模型文件,导入怪物数据,相机路径数据以及地图文件)
Doom3 Doom3.exe Doom3.app Doom 3 Engine(Doom3游戏引擎部分)
TypeInfo TypeInfo.exe - In-house RTTI helper: Generates GameTypeInfo.h: A map of all the Doom3 class types with each member size. This allow memory debugging via TypeInfo class.(这个不是很明白,好像是用TypeInfo类封装了所有的Doom3的类类型的成员大小,这样方便了开发中的内存调试,即内部支持RTTI机制(运行时类型信息))
CurlLib CurlLib.lib - HTTP client used to download files (Staticaly linked against gamex86.dll and doom3.exe)(在客户端使用HTTP协议下载文件).
idLib idLib.lib idLib.a id Software library. Includes parser,lexer,dictionary ... (Staticaly linked against gamex86.dll and doom3.exe).(id Software自己的代码库,包括解析器,词法分析器,字典数据结构...)

自从idTech2引擎后,每个引擎的里面都可以找到一个闭源的二进制代码(doom.exe)和一个开源的动态链接库(gamex86.dll)。:

自从2004年10月,大多数的源码可以通过Doom3 SDK获得:Doom3中仅仅缺少了Doom3的可执行代码,很多Modders 可以编译idlib.agamex86.dll,但是引擎核心仍然闭源。

Note:该游戏引擎并未使用C++标准库:所有的容器(map,linked list…)都重新实现了,而libc在代码中被大量使用。

Note:在游戏各个模块中,每个类都是对idClass类的扩展。这种方法允许引擎实现内部RTTI机制,并且通过类名实例化该类。

Trivia:从上图可以看到Doom3.exe工程的一些基本框架(比如Filesystem)。因为gamex86.dll也需要加载游戏资源(而Doom3.exe中已经有Filesystem),所以会存在一个重复利用的问题。从上图可以看到一些Doom3.exe中的子系统可以由gamex86.dll动态地加载(就是上图中箭头所表达的内容)。如果我们对DLL使用PE explorer进行分析,我们会发现gamex86.dll导出了一个方法:GetGameAPI(从名称可以看出,是用来获得Doom3.exe中的API,比如那些加载资源的子系统的API):

GetGameAPI的实现方式和这篇文章(Quake2如何加载render和game的dlls)(后面有时间会翻译下这篇文章,对于使用C来实现OO很有感触)一样:交换指针对象。

当Doom3.exe启动时:

  • 使用LoadLibrary来加载DLL到Doom3.exe的运行内存中。
  • 使用win32的GetProcAddress来获得dll中的GetGameAPI的地址。
  • 调用GetGameAPI
gameExport_t * GetGameAPI_t( gameImport_t *import );

最终Doom3.exe和Gamex86.dll进行交互时是通过下面所述的方式:Doom3.exe拥有一个idGame的指针,而Game.dll有一个gameImport_t的指针(gameImport_t中又包含了其他子系统,如idFileSystem)

以Doom3可执行对象的角度看Gamex86.DLL(翻译得不知所云,见谅!)(大概意思是Gamex86.DLL中封装的Doom3.exe的功能模块,这样Gamex86.ex就可以和Doom3.exe进行交互了,下面一段是同样的道理)

typedef struct {
    int                      version;               // API版本
    idSys *                   sys;                   // 可移植的系统服务
    idCommon *                common;                // common(不好翻译)
    idCmdSystem *            cmdSystem;             // 控制台命令系统
    idCVarSystem *            cvarSystem;            // 控制台变量系统
    idFileSystem *            fileSystem;            // 文件系统
    idNetworkSystem *         networkSystem;         // 网络系统
    idRenderSystem *          renderSystem;          // 渲染系统
    idSoundSystem *           soundSystem;           // 音频系统
    idRenderModelManager *    renderModelManager;    // render model manger(管理渲染的模型)
    idUserInterfaceManager * uiManager;             // UI接口的管理
    idDeclManager *           declManager;           // 声明的管理,感觉有C11中的decltype的类似功能
    idAASFileManager *        AASFileManager;        // AAS文件的管理
    idCollisionModelManager * collisionModelManager; // 碰撞模型的管理
} gameImport_t;

Doom3中封装的Game/Modd对象

typedef struct
{
    int        version;    // API版本
    idGame *        game;    // 运行游戏的接口
    idGameEdit *    gameEdit;    // 游戏编辑的接口,估计是给Modders用的吧
} gameExport_t;

Notes:有一个介绍这些子系统的好资源,Doom3 SDK 文档(后面有时间会翻译下这篇文章)。看上去这篇文章对2004年的代码理解很深(估计是开发组的成员写的)

代码



在深入钻研之前,我们先使用cloc(代码统计工具)分析下代码:

     ./cloc-1.56.pl neo

     2180 text files.
     2002 unique files.
     626 files ignored.

     http://cloc.sourceforge.net v 1.56  T=19.0 s (77.9 files/s, 47576.6 lines/s)

     -------------------------------------------------------------------------------
     Language                     files          blank        comment           code
     -------------------------------------------------------------------------------
     C++                            517          87078         113107         366433
     C/C++ Header                   617          29833          27176         111105
     C                              171          11408          15566          53540
     Bourne Shell                    29           5399           6516          39966
     make                            43           1196            874           9121
     m4                              10           1079            232           9025
     HTML                            55            391             76           4142
     Objective C++                    6            709            656           2606
     Perl                            10            523            411           2380
     yacc                             1             95             97            912
     Python                          10            108            182            895
     Objective C                      1            145             20            768
     DOS Batch                        5              0              0             61
     Teamcenter def                   4              3              0             51
     Lisp                             1              5             20             25
     awk                              1              2              1             17
     -------------------------------------------------------------------------------
     SUM:                          1481         137974         164934         601047
     -------------------------------------------------------------------------------
代码的行数并不能代表一切,但是这对理解这款引擎所花费的精力有一个很好的评估。601047行的代码量使理解难度看上去是Quake Ⅲ的两倍。下图从代码行数角度来看id Software引擎的历史。
#Lines of code Doom idTech1 idTech2 idTech3 idTech4
Engine 39079 143855 135788 239398 601032
Tools 341 11155 28140 128417 -
Total 39420 155010 163928 367815 601032

Note:在idTech3中的代码量的巨大提升主要是因为lcc代码(这是C编译器用来产生QVM字节码的,我估计是模仿Java的JVM的吧,呵呵,随便猜猜…)

Note:Doom3中没有Tool,因为Tool集成到游戏引擎中了。

从引擎的顶层设计来看,这里罗列了一些有趣的事实:

  • 这是id Software第一次在代码中使用C++而非C。John Carmack在我们的Q&A中对此进行了详细说明
  • 代码中使用了很多抽象和多态。但是使用了一个非常漂亮的技巧来避免某些对象在使用虚函数表时的性能损失。
  • 所有的游戏资源都以人类可读的文本形式进行存储,没有二进制形式。所以代码中广泛使用了语法分析器和解释器。John Carmack对此进行了详细说明
  • 模板只在引擎底层的实用(utility)类里使用(主要是在idLib使用模板),你绝不会在顶层设计看到模板。所以不会有看到Google的V8时那种刺瞎狗眼的感觉。
  • 这款代码的注释是id Software发布的代码中第二好的。唯一比它好的是Doom iphone,可能是因为它比Doom3发布晚吧。30%的注释可以称得上杰出,并且你很难在别的工程中发现这样好的注释了。在有些部分(比如dmp),注释确实比一般性的叙述更多。
  • OOP 的封装是代码看上去更干净,而且也易于阅读。
  • 底层汇编的优化时代已经过去了。一些技巧比如idMath::InvSqrt(神作),还有一些本地化的优化,但是大部分代码使用了我们可用的工具进行优化(比如GPU Shaders,OpenGL VBO,SIMD,Altivec(一个浮点和整型的单指令流多数据流(SIMD)指令集),SMP(对称多处理机),L2 Optimizations(每个模型处理过程中的R_AddModelSurfaces很类似L2))。

看下idTech4的代码规范(镜像pdf)也很有意思。这份代码规范是由John Carmack确立的(我非常欣赏关于const使用的介绍)。

展开主循环



下面是引擎中最重要的部分——主循环(暂时没有深入研究,大体上感觉使用了多线程,然后在一个while(1)的循环中进行游戏的更新,如游戏逻辑,渲染等等)。

idCommonLocal    commonLocal;           // OS Specialized object
idCommon *    common = &commonLocal;    // 使用了接口指针(因为Init是依赖OS的,它是一个抽象方法)
int WIINAPI 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_CreateConsole();

       // 因为引擎是多线程的,所以在此处进行初始化。每个代码临界区对应一个互斥量。
        for (int i = 0; i < MAX_CRITICAL_SECTIONS; i++ ) {
            InitializeCriticalSection( &win32.criticalSections[i] );
        }

        common->Init( 0, NULL, lpCmdLine );              // 获取到有多少显卡可用 (不是用OpenGL而是用系统调用来确定的l)

        Sys_StartAsyncThread(){                          // 下一帧由另一个线程运行
            while ( 1 ){
                usleep( 16666 );                         // 以60Hz速度运行
                common->Async();                           // Do the job
                Sys_TriggerEvent( TRIGGER_EVENT_ONE );   // 解锁其他线程
                pthread_testcancel();                      // 检测是否当前线程被主进程取消了
        }

        Sys_ShowConsole

        while( 1 ){
            Win_Frame();                                 // 显示/隐藏控制台
            common->Frame(){
                session->Frame()                         // 游戏逻辑部分
                {
                    for (int i = 0 ; i < gameTicsToRun ; i++ )
                        RunGameTic(){
                            game->RunFrame( &cmd );      // 从此处开始,代码跳到GameX86.dll的地址空间去执行.
                              for( ent = activeEntities.Next(); ent != NULL; ent = ent->activeNode.Next() )
                                ent->GetPhysics()->UpdateTime( time );  // ,主要是更新entities,比如进行一些物理碰撞什么的,不知道AI是不是也在这处理?
                        }
                }

                session->UpdateScreen( false ); // 更新屏幕画面
                {
                    renderSystem->BeginFrame
                        idGame::Draw            //前台渲染. 并不和GPU进行交互 !!
                    renderSystem->EndFrame
                        R_IssueRenderCommands   // 后台渲染. 发射GPU优化指令到GPU上.
                }
            }
        }
}

更多细节请参见完整的循环展开,当我阅读代码时,我会用这段循环来作为一条主线进行学习。

这是id Software引擎系列的标准主循环。除了Sys_StartAsyncThread的使用意味着Doom3使用了多线程。引入多线程的目的是为了除了对时间要求高(time-critical)的函数,这样就可以减少对帧率的限制。下面两个方面对时间要求就很高。

  • 混音
  • 用户输入

Trivia:idTech4的高层类对象都使用了基于虚函数的抽象类。这会带来性能损失,因为运行时查找虚函数表需要一些很多时间。但是这里使用了一个技巧,所有的对象都被静态的实例化(但是我看代码好像是定义成了全局对象,可能文中指的静态包括全局吧,毕竟这两者都是放在内存的数据段,不是很明白),注意下面idCommonLocalidCommon都是接口类:

idCommonLocal    commonLocal;                   // Implementation
idCommon *       common = &commonLocal;         // Pointer for gamex86.dll

因为一个对象被静态分配到内存的数据段后,比如commonLocal方法在调用时,编译器可以优化掉在虚函数查找虚函数表时的损失。这个接口指针在gamex86.dll和doom3.exe交互时使用,所以doom3.exe可以和gamex86.dll通过对象引用进行交换信息,但是在这个例子中不能优化掉虚函数表的性能损失(有点不明白,上面说可以,下面又说不可以?)。

Trivia:看了很多id Software的引擎后,我发现很多方法名从doom1以来就没有变过:比如鼠标和游戏手柄的输入一直用的就是IN_frame()

渲染器



两个重要的部分:

  • 因为Doom3使用了一个入口系统(portal system,我估计指的就是一个扩展系统,可以把别的工具或组件扩展到引擎中)。比如场景预处理工具dmap就完全从传统的bsp场景构建中分离。我在这里对其进行了更深入的讨论。下图显示了一个典型的bsp树的构建。

  • 运行时的渲染器有一个很有意思的架构,因为它把渲染分为了前台和后台:更多细节请戳这里

性能剖析



我是用了Xcode的Instrument工具来分析代码的运行耗时。分析结果请戳这里

脚本和虚拟机



每个版本的idTech都会更新它的虚拟机和脚本语言,这次也不例外。具体细节请戳这里

访谈



当阅读代码时,有些新奇的地方让我迷惑不解,因此我给John Carmack写信,他也非常友好地给我进行了详细而有深度的讲解,包括以下内容:

  • C++
  • 渲染器为啥要分成两个部分
  • 基于文本的游戏资源
  • 解释型的字节码

我把所有关于idTech4的视频和访谈报告整理并放在了这里

推荐读物



如果你很喜欢Doom的话,推荐下面两本书。

   

其他



夏天来啦,很久没关注了…

…毕竟读idTech4不是一件容易的事。而idTech5的源码不会很快发布(如果会的话),我将看看idTech3(Quake Ⅲ)的源码。如果很多人感兴趣的话,我会写点关于idTech3的东西。

时间: 2024-10-28 21:42:50

【译】Doom3源码剖析(1/6)——引论的相关文章

《Ruby源码剖析》现已上市

豆瓣页面已创建 天猫购买链接 当当预售地址 京东预售地址 来自原作者的祝贺: 我翻译的<Ruby under a Microscope>中文版<Ruby源码剖析>现在已经上市了. 这本书从2015年4月开始翻译,历经翻译--审核--修订--编辑一审--修订--编辑二审--修订--编辑三审--修订--终审--修订--印刷,现在终于上市了.在翻译期间有不少人追着我问这本书什么时候上市,那么现在大家终于可以入手了:天猫购买链接 京东或当当应该还没有铺开,马上就会有货了!另外电子书可能会在

Mongoose源码剖析:外篇之web服务器

转 http://www.cnblogs.com/skynet/archive/2010/07/24/1784110.html 这个博主里面很多关于mongoose的剖析 引言 在深入Mongoose源码剖析之前,我们应该清楚web服务器是什么?它提供什么服务?怎样提供服务?使用什么协议?客户端如何唯一标识web服务器的资源?下面我们抛开Mongoose,来介绍一个web服务的这些通性. web服务器:通常是指一个计算机程序(web服务器是什么?),在World Wide Web上提供诸如web

下载-深入浅出Netty源码剖析、Netty实战高性能分布式RPC、NIO+Netty5各种RPC架构实战演练三部曲视频教程

下载-深入浅出Netty源码剖析.Netty实战高性能分布式RPC.NIO+Netty5各种RPC架构实战演练三部曲视频教程 第一部分:入浅出Netty源码剖析 第二部分:Netty实战高性能分布式RPC 第三部分:NIO+Netty5各种RPC架构实战演练

Phaser实现源码剖析

在这里首先说明一下,由于Phaser在4.3代码里是存在,但并没有被开放出来供使用,但已经被本人大致研究了,因此也一并进行剖析. Phaser是一个可以重复利用的同步栅栏,功能上与CyclicBarrier和CountDownLatch相似,不过提供更加灵活的用法.也就是说,Phaser的同步模型与它们差不多.一般运用的场景是一组线程希望同时到达某个执行点后(先到达的会被阻塞),执行一个指定任务,然后这些线程才被唤醒继续执行其它任务. Phaser一般是定义一个parties数(parties一

【Java集合源码剖析】HashMap源码剖析

转载请注明出处:http://blog.csdn.net/ns_code/article/details/36034955 HashMap简介 HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长. HashMap是非线程安全的,只是用于单线程环境下,多线程环境下可以采用concurrent并发包下的concurrentHashMap. HashMap 实现了Serializable接口,因此它支持序列化,

转:【Java集合源码剖析】Vector源码剖析

转载请注明出处:http://blog.csdn.net/ns_code/article/details/35793865   Vector简介 Vector也是基于数组实现的,是一个动态数组,其容量能自动增长. Vector是JDK1.0引入了,它的很多实现方法都加入了同步语句,因此是线程安全的(其实也只是相对安全,有些时候还是要加入同步语句来保证线程的安全),可以用于多线程环境. Vector没有丝线Serializable接口,因此它不支持序列化,实现了Cloneable接口,能被克隆,实

下载BootStrap企业级应用培训课程(零基础、源码剖析,内部教材,项目实训)

全套500多课,附赠JS OOP编程,转一播放码.下载地址:http://pan.baidu.com/s/1kVLdZmf 第一季:基础篇,侧重于BootStrap 相关 API 详解.主要包含以下内容:Brackets前端开发工具详解.BootStrap框架三大核心-CSS.BootStrap框架三大核心-布局组件.BootStrap框架三大核心-JavaScript插件.附-BootStrap编码规范第二季:高级篇,侧重于BootStap源码解析与第三方扩展.主要包含以下内容:BootStr

菜鸟nginx源码剖析 框架篇(一) 从main函数看nginx启动流程(转)

俗话说的好,牵牛要牵牛鼻子 驾车顶牛,处理复杂的东西,只要抓住重点,才能理清脉络,不至于深陷其中,不能自拔.对复杂的nginx而言,main函数就是“牛之鼻”,只要能理清main函数,就一定能理解其中的奥秘,下面我们就一起来研究一下nginx的main函数. 1.nginx的main函数解读 nginx启动显然是由main函数驱动的,main函数在在core/nginx.c文件中,其源代码解析如下,涉及到的数据结构在本节仅指出其作用,将在第二节中详细解释. nginx main函数的流程图如下:

HashMap(2) 源码剖析(推荐)

今天看代码,想到去年发生的HashMap发生的CPU使用率100%的事件,转载下当时看的三个比较不错的博客(非常推荐) 参考:http://coolshell.cn/articles/9606.html   http://github.thinkingbar.com/hashmap-analysis/ http://developer.51cto.com/art/201102/246431.htm 在 Java 集合类中,使用最多的容器类恐怕就是 HashMap 和 ArrayList 了,所以