拆解《Windows游戏编程大师技巧》源码:从DirectDraw底层到现代引擎原理
简介《Windows游戏编程大师技巧第二版》随书光盘源码合集面向希望跟随经典教程系统学习Windows游戏开发、C与DirectX编程的读者尤其适合已购买纸质书但随书光盘遗失或损坏的开发者也适用于按章节实践源码的教学与自学场景。压缩包共有1035个文件大小约38.49MB包含159个cpp源码文件、140个exe可执行程序以及518个bmp位图、99个wav音效、27个pal调色板等配套素材可支撑各章节示例的编译运行与改造测试。目前已有492人学习使用是入门与进阶Windows游戏开发的实用资料。资源按照原书示例目录组织包含各章节工程与配套辅助文件读者可对照书中讲解直接构建工程运行可执行程序观察图形、音频与动画等关键效果也能修改源代码进行调试深入理解经典游戏编程技巧。压缩包内既有可直接运行的演示程序也有完整的源代码与素材便于动手修改、对比观察运行结果逐步掌握每个示例的实现流程。1. 这份《Windows游戏编程大师技巧(第二版) 源码.zip》值得拆开的不只是怀旧如果你的开发经历是从 Unity、Unreal 或者某个 Web 引擎开始的第一次打开《Windows游戏编程大师技巧(第二版) 源码.zip》大概率会懵里面没有场景文件、没有资源管线只有一堆 .cpp、.h 和 Visual C 老工程文件。但这恰恰是它最值钱的地方。它把一帧画面从“创建窗口 → 锁定后备表面 → 逐像素写入 → 翻转显示”的全过程摊在你面前每个像素都由代码亲手决定没有黑匣子。这份源码解决的是今天引擎封装之外的核心问题理解底层绘制、输入、音频和 3D 数学是如何被组织起来的。适合两类人一类是想把计算机图形学基础吃透的在校学生另一类是工作中总被“引擎为什么这么设计”追问的客户端工程师。它不是拿来复制粘贴的库是拆开看透的教材。2. 源码包里的资产分类先分清教程代码、引擎骨架与实验 Demo2.1 按目录、工程文件和源码体量识别三类内容拿到源码先别急着点开任意一个 .cpp第一步是资产盘点。解压后第一件事把目录树拉出来看结构。:: Windows 命令提示符下执行先切到源码根目录 cd /d D:\dev\gg :: 列出所有子目录看章节编号与命名规律 dir /b /ad :: 统计各类源文件规模判断哪些目录是大工程 dir /b /s *.cpp | find /c .cpp dir /b /s *.h | find /c .h这套源码大多按章分目录一章一个主题章内往往是“先一小段演示再一个综合 Demo”的递进结构。识别方法很直接目录名带数字前缀的是按章组织名字里带 demo、tutorial、engine 字样的是实验代码和引擎骨架根目录或 common 目录里的公共代码才是被所有示例共享的底层工具。老工程文件也有明显特征。看到大量 .dsw、.dsp 后缀说明这是 Visual C 6.0 时代的工程格式编译前需要转换。:: 列出全部旧版工程文件方便确认编译顺序 dir /b /s *.dsp *.dsw 2nul读源码前我习惯先看每个 .cpp 文件头部的注释块。老代码非常讲究文件开头的注释会写明“这个例程属于第几章、演示什么技术、依赖哪些公共库”。这个习惯在今天很多开源项目里已经看不到了但对快速定位目标代码帮助极大。2.2 六个技术主题从窗口消息到软渲染第二版源码看起来是一堆零散 Demo实际覆盖的是一套完整的游戏技术栈。我把它们归纳成六类方便按图索骥技术主题源码里常见的关键词今天的对应物窗口与消息循环WinMain、PeekMessage、TranslateMessage应用生命周期与主循环DirectDraw 2D 表面CreateSurface、Blt、BltFast、FlipGPU 纹理与提交精灵与透明SetColorKey、mask、blit图集、绘制排序、抠图调色板与位图LoadBitmap、palette、CLUT色彩管理、LUT输入与音频DirectInput、acquire、DirectSound输入系统、音频总线3D 与软渲染向量、矩阵、投影、Z-BufferCPU 光栅化、渲染管线这张表说明一件事这套源码不是单个功能的零散集合而是一套“游戏技术栈教学版本”。当年没有现代引擎窗口、绘制、输入、音频、3D 全部要亲手串起来今天引擎只不过把这一套封装成了调度器底层难点一个没少。知道某个主题在哪个目录调试和改造时能直接跳目标不用盲翻。2.3 值得反复读的三段核心代码与读法第一段是窗口与双缓冲循环。这是整份源码的地基值得读到能默写。核心结构是 PeekMessage 非阻塞消息循环加表面翻转。读的时候带着三个问题为什么用 PeekMessage 而不是 GetMessage为什么帧率控制用时间差而不是 Sleep为什么游戏绘制不进 WM_PAINT第二段是带颜色键的精灵绘制函数。源码里通常有两个版本一个走 BltFast 硬件加速一个逐像素拷贝。逐像素版本会直接体现“颜色键”的本质把某个颜色值当作透明。读法是先看它处理了几种位图格式有调色板、无调色板再对照硬件加速版本在行为上的差异。第三段是 3D 透视投影前的数学准备。别急着看渲染代码先看它怎么组织向量和矩阵结构体矩阵乘法是行主序还是列主序投影矩阵的 w 分量怎么用。现在写引擎的人依然要回答这些问题。:: 用 findstr 快速定位这三段代码 findstr /s /n /i PeekMessage *.cpp findstr /s /n /i SetColorKey *.cpp findstr /s /n /i Transform *.cpp定位之后按函数读不要按文件从头读到尾。老源码的函数普遍短小一个函数只干一件事读起来比现代引擎动辄几百行的函数轻松得多。3. 在老环境与现代环境之间选一个编译前先决定路线3.1 三条路线怎么选老 VC、新旧混合、纯命令行要不要还原当年的编译环境我的建议是不要。老版 Visual C 在今天的 Windows 上安装本身就是一场兼容性拉锯战装完还要面对调试器不支持当前系统的尴尬。更实际的是下面三条路线路线一现代 Visual Studio 加老 DirectX SDK 头文件。适合想最快看到画面的人。工程升级后在项目属性里处理字符集、库目录、警告级别三个点大部分示例就能跑起来。路线二纯命令行 cl 编译单个示例。适合只想验证某一章内容、不想建工程的人。打开 x86 本机工具命令提示符把 Include 和 Lib 指到老 DirectX SDK 目录一条 cl 命令编完。路线三完全跳过 DirectX用 GDI 或 SDL 重建依赖。适合想把源码里的算法搬到自己代码库的人具体的重建思路放在第 6 章展开。以我的习惯第一遍用路线一看到画面后立刻切路线二去读编译参数最后只保留真正想改造的三五个示例。别一口气编译全部工程——老源码里必然有一部分例程依赖特定硬件或驱动编不过不代表你的环境有错。3.2 解压、规整与工程升级的一串命令解压这步有个容易被忽略的坑路径过长。这本书的目录命名有章节编号和英文单词直接解压到桌面升级工具偶尔会抱怨路径超长。先定一个短根目录比如 D:\dev\gg。# 以短路径为目标目录解压 mkdir D:\dev\gg Expand-Archive -Path D:\download\Windows游戏编程大师技巧(第二版) 源码.zip -DestinationPath D:\dev\gg cd D:\dev\gg如果压缩包内层还套着一层同名目录需要手动上移一层避免出现“源码\源码\Chapter”这种冗余路径。这一步不处理后面找工程文件、写脚本都会多一层前缀纯属给自己添堵。接下来升级工程。老工程是 .dsw/.dsp 格式现代 Visual Studio 的升级向导会自动生成 .sln# 在 Visual Studio Developer Command Prompt 中执行一次只升一个工程 devenv D:\dev\gg\Chapter01\FirstWin.dsw /upgrade升级完成后目录里会多出同名 .sln 和临时文件。注意两点一次只升级一个工程别用通配符批量升级升级顺序先公共库再示例工程。公共库编译不过后面的示例全部白搭。3.3 第一次编译成功的验证目标与后续编译技巧不要以“所有示例都编译通过”为目标那只会让人在第 3 章就放弃。我给自己定的验证目标是三个先编译第 1 章的纯窗口示例证明消息循环与新工具链兼容。再编译一个 DirectDraw 精灵示例证明 DDraw 库、颜色键路径正常。最后编译一个完整游戏 Demo证明多文件工程没有库顺序问题。命令行编译单个示例可以精确控制依赖也更容易定位问题:: 打开 x86 Native Tools Command Prompt 后执行 cd /d D:\dev\gg\Chapter09\CrayonDemo cl /nologo /EHsc /MT /D WIN32_LEAN_AND_MEAN /I D:\dxsdk\Include main.cpp ^ /link /LIBPATH:D:\dxsdk\Lib /SUBSYSTEM:WINDOWS ^ user32.lib gdi32.lib winmm.lib dxguid.lib ddraw.lib这里每个参数我解释一下。/EHsc是 C 异常模型老代码基本不用异常但保留无害/MT静态链接运行时避免程序启动时缺 DLL/D WIN32_LEAN_AND_MEAN砍掉 windows.h 里游戏用不到的部分能减少宏冲突库的顺序上dxguid.lib必须出现在ddraw.lib之前因为 ddraw 里的接口标识符号由 dxguid 提供。提示升级工程后编译器如果提示找不到 ddraw.h通常不是 SDK 缺失而是 Include 目录没有把老 DirectX SDK 的路径排在最前面。编译通过后的验证也有讲究。老 DirectDraw 程序默认全屏独占在当前显示器上可能直接黑屏。验证目标应该是“窗口能创建、绘制循环在跑、按 Esc 能退出”而不是“画面要华丽”。先窗口模式再全屏先单缓冲再双缓冲。把层次拆开能定位一半以上的黑屏问题。4. 源码背后的三个必懂原理DirectDraw 表面、颜色键与调色板4.1 表面、Blt 与 Flip2D 时代的渲染循环今天讲 SwapChain2000 年前后讲的是主表面和后备表面本质是同一件事。DirectDraw 里主表面对应屏幕的显存后备表面在离屏内存中绘制一帧绘制完成后调用 Flip把主表面和后备表面交换。配合 PeekMessage 轮询消息老源码形成了一套固定节奏处理消息 → 清屏或复用背景 → 按顺序画精灵 → Flip。// 双缓冲循环的常见骨架简化示意 while (true) { // PeekMessage 保证消息多时不阻塞绘制 if (PeekMessage(msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message WM_QUIT) break; TranslateMessage(msg); DispatchMessage(msg); } // 绘制一帧先锁后备表面画背景和精灵再解锁 // ... 画背景、画精灵 ... // 翻转后备表面到主表面 lpddsprimary-Flip(NULL, DDFLIP_WAIT); }这段结构是全书出现频率最高的也解释了为什么老程序员强调“别在 WM_PAINT 里做游戏绘制”。WM_PAINT 由系统按需触发时机不可控PeekMessage 则是不停询问消息队列没有消息就把时间全给绘制。今天的游戏循环仍是这个模型只是把消息轮询换成了垂直同步和帧率限制。DDFLIP_WAIT表示等待翻转完成作用是防止画面撕裂。如果程序运行在窗口模式不能用 Flip得用 Blt 把后备表面拷贝到窗口客户区这一步是很多老示例移植到窗口模式时翻车的地方。4.2 颜色键透明从像素比较到硬件加速老源码里精灵透明有两种实现层次。第一层是掩码方式先把背景用 AND 操作“挖”出精灵轮廓再用 OR 把精灵像素填进去第二层是 DDraw 的 SetColorKey直接告诉硬件“看到这个颜色就跳过”。掩码方式现在还能在不少软件渲染器里看到原理非常朴素。// 逐像素颜色键绘制示意 for (int y 0; y spriteHeight; y) { for (int x 0; x spriteWidth; x) { unsigned char pix sprite[y * spriteWidth x]; if (pix ! transparentColor) { // 颜色键该颜色不写入 backBuffer[y * stride x] pix; } } }颜色键为什么通常选品红而不是黑色因为在老调色板环境里品红在游戏画面中出现频率最低误伤的像素最少。这个选色纪律到现在依然适用做图集、做擦除特效、做绿幕抠像都要挑画面里最稀有的颜色当透明键而不是“哪个颜色显眼用哪个”。4.3 调色板与 16 位色深老画面跑新显示器的第一道坎第二版源码大量示例已经切到 16 位高彩但调色板逻辑仍然保留。8 位调色板表面在现代显示器上最容易翻车系统会自动把调色板表面扩展成 RGB如果位图没有正确加载调色板画面就会整体偏色甚至红蓝互换。处理思路在源码时代和现在别无二致加载位图后先查颜色深度如果是调色板格式锁定表面后把像素按调色板查表扩展成 RGB再拷贝上屏。老代码里的 256 色调色板查找表在某些像素特效里比直接算 RGB 快得多这个思路后来演变成了 GPU 的调色板纹理。调色板还有个被遗忘的玄学技巧调色板动画。把背景的颜色映射表整体偏移实现流水、呼吸灯、旗帜飘动的效果整个过程不重画任何像素。今天的 LUT 动态调整就是它的直系后代。遇到老代码里跳动的背景先别怀疑显卡先看它是不是在做调色板偏移。5. 老代码编译避坑清单现象、原因、解决这一章是我重编这套源码时踩过且还在踩的五个问题每一条都按“现象 → 原因 → 解决”来写。翻车不可怕可怕的是不知道从哪开始查。5.1 字符集报错成片C2664 满天飞现象工程升级后凡是调用带 TCHAR 指针参数的 API 的地方都报 C2664错误信息显示“无法将 const char* 转换为 LPCWSTR”整个工程红成一片。 原因老源码写于 Unicode 默认之前字符串字面量全是窄字符。现代 Visual Studio 项目默认字符集是 UnicodeAPI 被宏映射成了宽字符版本窄字符串自然塞不进去。 解决项目属性 → 配置属性 → 高级 → 字符集改为“使用多字节字符集”。这是第一遍编译最省事的做法。如果想保留 Unicode就得把所有字符串字面量包进_T()宏改动面太大我建议第一遍先多字节改造阶段再逐步切 Unicode。5.2 DirectX SDK 头文件与 Windows SDK 互相覆盖现象预处理报“宏冲突”“重定义”尤其是 dinput.h、ddraw.h 里和 windows.h 共用的 BOOL、TRUE、FALSE 被重复定义链接阶段偶尔还会出现 duplicate symbol。 原因老 DirectX SDK 的部分类型定义与后来的 Windows SDK 重名两者头文件的包含顺序不同预处理结果也不同。这是新旧 SDK 混用的经典打架现场。 解决固定包含顺序先#include windows.h再包含 DirectX 头文件编译器里只把老 DirectX SDK 的 Include 目录放进去不要把老、新两套 SDK 目录同时塞进同一个 Include 路径。如果因为其他项目必须共存就给这套源码单独建一个属性表不要全局设置。5.3 Debug 版正常、Release 版花屏或闪退现象同一个示例Debug 版跑得挺好Release 版画面残缺、随机色块甚至启动就崩。 原因老代码大量变量没有显式初始化。Debug 构建会把内存清零掩盖了问题Release 构建栈上残留随机值直接参与像素计算。另外优化选项可能把未定义行为的读写顺序重排让问题更明显。 解决对关键结构体统一ZeroMemory或写构造函数每次锁表面后检查返回值失败就跳过绘制不要硬写像素Release 构建里先把“内联函数展开”关掉再试能进一步缩小问题范围。5.4 键盘鼠标失灵DirectInput 的 acquire 与焦点现象程序能画画面但按键没反应窗口失焦再切回来键盘彻底失效鼠标还可能飞出窗口。 原因DirectInput 设备在窗口失焦时会被系统自动释放源码里没有处理焦点恢复或者 acquire 失败后没有重试机制。 解决在 WM_ACTIVATE、WM_SETFOCUS/WM_KILLFOCUS 消息里调用设备的Unacquire()和Acquire()主循环里对每次读取设备返回的错误码做判断读取失败就跳过本帧输入不要直接拿旧数据继续算。这条坑现在依然存在于不少怀抱老代码的项目里。5.5 高 DPI 和显示器缩放把窗口搞糊现象4K 屏上运行窗口小得可怜Windows 缩放 150% 后窗口变糊、鼠标坐标对不准按钮。 原因老程序没有声明 DPI 感知系统把它当普通位图缩放自然模糊DirectInput 拿到的鼠标绝对坐标和窗口客户区比例不一致点击位置就会整体偏移。 解决启动时调用SetProcessDPIAware()或者在程序清单里声明 PerMonitorV1再对鼠标坐标做缩放换算。窗口模式下这个问题比全屏更常见因为全屏程序本来就独占分辨率绕开了缩放逻辑。6. 从源码到自己的引擎三条改造路线与一个对照验证方法6.1 路线一把 DirectDraw 调用收进一个渲染接口先定义最小渲染接口把表面创建、颜色键、翻转这几件核心操作收进去老例程只面向接口编程。这样随时可以把 DirectDraw 实现换成 GDI 实现。class RenderDevice { public: virtual bool CreateSurface(int w, int h) 0; virtual void SetColorKey(unsigned char r, unsigned char g, unsigned char b) 0; virtual void Blit(int x, int y, void* pixels) 0; virtual void Flip() 0; };接口设计参照第 4 章讲的三个原理命名不要照搬老代码而是按现代习惯重新定义。这个改造投入最小适合拿来做网格、地图、调色板这类逻辑验证。6.2 路线二用 SDL2 或 OpenGL 重写渲染后端把后备表面当纹理处理CPU 侧逐像素写好后整体上传到 GPU。SDL2 的 Texture 加 UpdateRect几乎一一对应老代码里的 BltFast切换成本最低。OpenGL 路线则可以用 GL_PIXEL_UNPACK_BUFFER 映射像素适合想借机接触现代渲染方式的人。这条路线保留了源码的全部算法逻辑只换掉最底层绘制调用。6.3 验证方法三组像素对照改造完成后怎么证明结果仍然正确人眼说“差不多”不算数。跑老程序截一张图再跑新程序截一张对比三个位置精灵边缘的透明衔接处、背景渐变中间值、碰撞检测边界。RGB 差值在 ±2 以内算通过超出就要回头查颜色键或像素格式转换。这个对照法比肉眼对比快得多也更容易说服别人。6.4 四个能带走的工程习惯第一变量先初始化别指望编译器帮你清零。第二表面和设备的每次操作都检查返回值失败就停止不硬画。第三多字节转 Unicode 要一次转完别在工程里混用两套字符串。第四程序能跑起来之后补一条命令行编译脚本别只依赖 IDE。这套源码最大的价值不是某个精灵算法而是让你养成“每一步都知道正在发生什么”的习惯。我后来再看现代引擎的渲染循环总能从这套老源码里找到对应物。当年觉得是绕远路的底层细节现在全成了理解引擎的捷径。如果你真想补上引擎黑匣子外的那层认知这份源码值得花一个周末一帧一帧亲手画出来。希望帮到你。本文还有配套的精品资源点击获取