ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Windows游戏编程大师源码新编:DirectX老工程Win11移植指南

Windows游戏编程大师源码新编:DirectX老工程Win11移植指南 简介《Windows游戏编程大师技巧第二版》随书光盘中的源码包面向想真正动手实践经典游戏开发技术的读者尤其适合刚学完Windows编程基础、希望以完整例子快速理解游戏循环、位图动画、输入响应等核心环节的初学者。压缩包内共收录1035个文件整体大小约38.49MB素材与代码种类齐全159个cpp源文件构成了各示例的主体518个bmp位图提供图形素材99个wav音效配合声音播放演示140个exe程序方便直接查看运行效果另有pal调色板、h头文件、rc资源脚本等补全工程细节。目前已有492人学习下载口碑实用常被作为自学配套资料。源码按书籍章节划分成不同演示模块从基础的窗口创建、位图贴取到动画绘制、音效处理均有覆盖学习者既可以打开工程逐步跟踪调试也能直接运行exe对照书中的讲解验证结果极大缩短了重新组织工程与寻找素材的时间从而更高效地吸收Windows游戏编程中的经典技法。1. 拿到《Windows游戏编程大师技巧(第二版)》的源码包先别急着双击解压《Windows游戏编程大师技巧(第二版) 源码.zip》是那本经典红皮书配套光盘的完整抓取里面是2002年前后DirectX 8.x时代的老工程。我的建议很直接把这包东西当成“源码考古现场”而不是一个能双击就编译的现代项目。直接解压后对着.sln一顿操作其实根本没有.sln第一轮基本都会翻车我见过太多人在字符集和链接库上卡一下午。这篇笔记按“拆包结构→环境搭建→核心代码走读→踩坑记录→验证技巧”的顺序推进目标是让你在两小时内把第4章的2D游戏循环跑起来并知道后面3D章节复用代码时该动哪些地方。适合三类人做Windows游戏课程设计的在校生、想把老引擎改成自己毕业设计的同学、以及单纯想重温经典游戏框架的老程序员。2. 拆包看结构光盘镜像的目录布局决定你后面的运气2.1 解压后的第一眼Chapters、Tools 与 DirectX SDK先把zip落到磁盘上路径要选对。我一般用7-Zip命令行来做这件事因为能一次把解压结果和目录结构都看全# Windows CMD / PowerShell 均可建议先用短路径建目录 mkdir D:\GPG # 解压整个光盘抓取内容到 D:\GPG 7z x D:\Downloads\Windows游戏编程大师技巧(第二版) 源码.zip -oD:\GPG # 查看顶层目录结构 dir /s /b D:\GPG | more这段命令的要点在输出目录D:\GPG上。老代码里大量使用相对路径来引用纹理、音效等素材比如“..\..\textures\floor.bmp”如果解压路径带中文或层级过深部分例子的素材路径会先断掉。现代编译器对长路径处理能力强了很多但既然目标是让二十年前的工程少报警告直接用全英文短路径是最省事的选择。解压完成后顶层通常会看到三个标志性目录Chapters、Tools 和 DirectX SDK。Chapters 是按章节编号排的例子程序每个子目录里有 .cpp 和 .h 源码部分章节还带 bitmap、wav 等素材Tools 是原书光盘附带的第三方工具集绘图和音频处理工具居多DirectX SDK 是当时随书附带的 DirectX 8.x 开发包。不过这套老 SDK 我不建议安装后面第 3 章会讲为什么直接用现代 Windows SDK 反而更干净。注意 Chapters 里的章节和书本章节一一对应但前几章GDI 画线、消息循环和后几章的 3D 例子之间存在明显的代码复用关系后面的 3D 工程会直接引用前面封装好的部分接口所以不要指望单拉一个 3D 章节出来就能独立编译。这里有一个细节值得提醒网上流传的这份 zip偶尔拿到手是“伪加密”状态——解压时报要密码但压缩包本身根本没有加密数据。遇到这种情况不用到处找密码用 7-Zip 直接按回车跳过密码即可数据能正常解开如果 7-Zip 也拒绝再用工具修复一次压缩包头的加密标志位。这个现象在流传很久的 zip 里非常常见先确定是伪加密还是真加密省得瞎折腾一天。2.2 工程文件格式.dsw 和 .dsp 留下的坑看 Chapters 下某个示例的目录你会看到 .cpp、.h、.dsp、.dsw 这些文件混在一起。.dsp 和 .dsw 是 Visual C 6.0 时代的工程文件格式VS2019 还能打开 .dsw 做一次性升级VS2022 基本已经放弃这条兼容路径。所以摆在面前只有两条路要么用 VS2019 去打开旧工程赌运气要么在 VS2022 上手搓一个新工程。我一般走第二条路原因是 .dsw 升级过程会带进来一批 VC6 时代的默认配置比如没有关闭的优化、不兼容的预编译头设置与其给它收拾烂摊子不如把纯净的 .cpp 拷贝出来自己建工程三分钟能解决的问题绝不花十分钟去跟升级向导搏斗。做法很粗暴# 把第4章的源码拷到临时工作目录准备手动建工程 mkdir D:\work\demo copy D:\GPG\Chapters\ch04\*.cpp D:\work\demo\ copy D:\GPG\Chapters\ch04\*.h D:\work\demo\ dir D:\work\demo拷贝时记住一件事一个章节目录下的 demo主文件通常是带章节目录名的那个 cpp比如 ch04_ddraw.cpp其余 cpp 是它依赖的辅助模块。如果直接全部拷贝进工程可能出现两个文件都定义了 main 或 WinMain导致链接阶段的多重定义报错——这是常见的低水平翻车。稳妥做法是先看每个 cpp 开头有没有 main/WinMain有入口函数的保留一份其余按工程需要挑。# 快速找出哪些源文件定义了入口函数 findstr /s /n /i WinMain main( D:\work\demo\*.cppfindstr 返回结果里带“WinMain”或“main(”的文件就是入口所在其余文件按功能决定是否加入工程。这样筛完再建项目能把多重定义报错从根源上避免掉。提示所有操作都围绕一份拷贝出来的副本进行原始 zip 保持不解压第二次。后面调乱文件编码或删错素材时后悔药就在原包里。3. 搭建编译环境让 DirectX 8 时代的代码在 Windows 10/11 上重新站起来3.1 装对 Visual Studio 负载再谈 DirectX 头文件在动手配置工程之前要确认 Visual Studio 装的是“使用 C 的桌面开发”工作负载而不是 UWP 那种精简版。判断方法很简单新建项目模板里能看到“Windows 桌面应用程序向导”说明桌面负载在如果只有“空白应用通用 Windows”说明没装全去 Visual Studio Installer 补。这一步不过后面所有关于 windows.h 的头文件都会变成“无法打开包括文件”的编译错误而且这种错误很容易被误判成 DirectX SDK 的问题实际上跟 DirectX 一点关系都没有。接下来是 DirectX 头文件问题。书里代码大量包含 ddraw.h、dinput.h、dsound.h、d3d8.h。这些头文件里基础的在较新 Windows SDK 中还有保留比如 ddraw.h、dinput.h、dsound.h 都还在但 d3dx8.h、d3dx9.h 这类 DX 辅助库的头文件并不在 Windows SDK 里需要额外处理。我的做法是不安装光盘里那份老 DirectX SDK而是下载微软最后独立分发的 DirectX SDK June 2010解压到一个固定路径只需要用到里面的 Include 和 Lib 两份内容。装完 SDK 之后把它的 Include 和 Lib 目录接进工程顺序有讲究DirectX 的 Include 目录必须放在 Windows SDK 之前否则老代码同时依赖两者时会出现头文件版本错乱。这个顺序问题在 3D 章节尤其明显2D 章节多半还碰不到。在 VS 项目的“VC 目录”里配置 Include 目录为“老 DXSDK\Include”加上“$(WindowsSDK_IncludePath)”Lib 目录同理。如果你暂时不想装独立 SDK只跑前四章的 2D 例子Windows SDK 自带的 ddraw.h 和 dinput.h 已经够用等真正碰到 d3dx8.h 再补也不迟。3.2 工程属性表五项配置逐行核对建好空工程、把源码加进去之后第一件事不是编译而是打开项目属性把下面这几项审一遍配置项新工程默认值推荐设置理由字符集使用 Unicode 字符集使用多字节字符集老代码全是 char、char[]Unicode 下一半 API 调用类型不匹配Windows SDK 版本10.0已安装的最新版保持 10.0不需要切旧版最新版同样兼容老 APIC 语言标准C17 或最新使用默认C14老代码里某些写法在 C17 下会触发警告或错误预处理定义WIN32;_DEBUG;_CONSOLE追加_WIN32_WINNT0x0601声明目标系统 Windows 7 以上避免老 API 被条件编译屏蔽链接器-输入空追加 dxguid.lib解决 DirectDrawCreateEx 等函数外部符号找不到的问题这里面最典型的是字符集。老源码几乎都用 char 定义字符串而且 API 调用也按 ANSI 版写VS 新建工程默认 Unicode 字符集导致 CreateWindow、SetWindowText 这类宏会展开成 W 版本把 char* 塞进去就是一片“无法从 char* 转换为 LPWSTR”的类型错误。把它改成多字节字符集等于回到老代码的默认状态至少三分之一的编译报错会瞬间消失。_WIN32_WINNT 这个宏很多人会忽略。老代码写的时候系统目标默认是 Windows 2000/XP而新 SDK 的头文件里部分 API 和结构体成员会按 _WIN32_WINNT 的值决定要不要声明。不定义它时编译器按默认较低版本来某些新一点的结构体成员拿不到报一个“找不到成员”的错误让人摸不着头脑加上 0x0601 之后大部分条件编译的路径都走到了兼容面最大。链接器输入那栏也常出事。DirectDrawCreateEx 函数本身在 ddraw.lib 里但它用到的 IID_IDirectDraw7 等 GUID 常量定义在 dxguid.lib 里。老工程在 VC6 里默认链接了 dxguid.lib新工程没有于是出现“LNK2019无法解析的外部符号 IID_IDirectDraw7”——加一个 dxguid.lib 就安静了。这属于那种一次踩过就再也不忘的坑。3.3 两条可抄的编译路径IDE 与命令行配置做完编译方式看个人习惯。用 VS 的 MSBuild 命令行也能编适合想脚本化的场景# 在“开发者 PowerShell”里编译第4章工程已手动升级为 .vcxproj 后 msbuild D:\work\demo\demo.vcxproj /p:ConfigurationDebug /p:Platformx86 /mPlatform 必须写明 x86。理由很简单这份资源里所有代码都是 32 位假设指针直接存进 DWORD 的地方、坐标用 long 的地方、以及若干靠内存布局做快速位图操作的写法在 64 位下会出各种匪夷所思的错误。Debug 配置优先于 Release初期跑通逻辑再谈优化。如果用命令行直编一个不依赖工程文件的最小示例也能验证环境是否通cl /nologo /EHsc /D_WIN32_WINNT0x0601 /ID:\DXSDK\Include ^ D:\work\demo\ch04_ddraw.cpp ^ ddraw.lib dxguid.lib dinput8.lib dsound.lib winmm.lib ^ /link /SUBSYSTEM:WINDOWS/nologo 去掉编译器的版本横幅/EHsc 开启 C 异常处理老代码大多是 C 风格但用 C 编译器编译这参数能避免一些异常相关的链接坑。/link /SUBSYSTEM:WINDOWS 告诉链接器这是窗口程序而不是控制台程序这样双击运行不会每次弹出黑色命令行窗口。如果代码里用的是 main 而不是 WinMain还得追加 /ENTRY:mainCRTStartup否则链接器会按 WinMain 去找入口点报一个 LNK2001 未解决的外部符号 WinMain。这条也是高频翻车点先确认你那个 cpp 的入口函数叫啥再决定参数。4. 核心代码走读消息泵、DirectDraw 封装与输入回路4.1 游戏循环为什么用 PeekMessage 而不是 GetMessage这份资源的前几章里几乎所有 2D 例子的主循环都是同一个模子用 PeekMessage 非阻塞地取消息没有消息就执行一次游戏逻辑。许多新手会疑惑为什么不让消息循环老老实实阻塞等待——毕竟教科书上写的是 GetMessage。区别在于帧率的控制权。GetMessage 在没有消息时会让出线程程序挂起等系统叫醒游戏循环的节奏完全被消息驱动帧率忽高忽低。PeekMessage 则是不管有没有消息都立即返回当消息队列为空时落到 else 分支里执行 Game_Main()这样一帧游戏逻辑的步进就由循环自身控制消息只是间隙里被顺手处理掉。书里示例大概长这样// 游戏主循环PeekMessage 非阻塞取消息空队列时跑游戏逻辑 while (TRUE) { if (PeekMessage(msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message WM_QUIT) { break; // 收到退出消息跳出循环 } TranslateMessage(msg); // 键盘消息转义处理字符消息 DispatchMessage(msg); // 交还窗口过程处理 } else { Game_Main(); // 用户游戏主函数读输入、更新、渲染 Sleep(1); // 让出 1ms避免独占 CPU } }PM_REMOVE 参数表示消息从队列里取出后删除如果换成 PM_NOREMOVE 则只查看不取走容易让同一条消息被反复处理新手调代码时注意区分。Sleep(1) 这行在当年有争议不少人觉得游戏循环里不该主动睡但事实是没有它这个循环在空转时会占满一个 CPU 核心而多睡 1ms 对绝大多数 60FPS 以内的游戏毫无影响CPU 占用率却能从 100% 降到个位数。这个习惯我到现在写代码还保留着。Game_Main 是书里约定的游戏逻辑函数你可以按自己的项目改名。它在每帧里承担三件事读输入状态、更新游戏对象、把画面画到后备表面。多数例子里这三个步骤被拆成独立子函数方便后续章节替换渲染部分而不动循环骨架。4.2 DirectDraw 初始化SetCooperativeLevel 与 SetDisplayMode 的组合逻辑DirectDraw 是 DirectX 8 时代做 2D 的主力书里第 4 章开始就用它代替 GDI 画图。初始化序列几乎恒定创建 Draw 对象、设置协作级别、设定显示模式、创建主表面和后备表面。下面这段是原书示例里最常见的封装我把关键调用加了注释#include ddraw.h #pragma comment(lib, ddraw.lib) LPDIRECTDRAW7 lpDD NULL; LPDIRECTDRAW7 CreateDDraw(HWND hwnd, int width, int height, int bpp) { // 创建 DirectDraw 对象IID_IDirectDraw7 表示用 7.0 接口 HRESULT hr DirectDrawCreateEx(NULL, (void**)lpDD, IID_IDirectDraw7, NULL); if (FAILED(hr)) return NULL; // 全屏独占模式必须传顶层窗口句柄 hr lpDD-SetCooperativeLevel(hwnd, DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN); // 指定分辨率与色深bpp 常见 16 或 32老代码多用 16 hr lpDD-SetDisplayMode(width, height, bpp, 0, 0); return lpDD; }DirectDrawCreateEx 的第一个参数传 NULL 表示使用主显示器第二个参数被迫用了 void**这是老 COM 接口的惯例写法。SetCooperativeLevel 是全屏模式成败的关键DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN 组合起来才允许你修改显示模式少了任何一个SetDisplayMode 都会返回错误。DDSCL_EXCLUSIVE 要求句柄必须是顶层窗口如果你把句柄传成控件或子窗口同样会失败。全屏在 Windows 10/11 上翻车率不低所以更稳的是直接跳过 SetDisplayMode 跑窗口模式。做法是把协作级别改成 DDSCL_NORMAL然后通过 CreateSurface 创建一个带 DDSCAPS_PRIMARYSURFACE 标志的主表面再用 Blt 把后备表面内容往上贴。窗口模式下不存在翻转Flip动作老代码里凡是调用 Flip 函数的地方都要替换成 BltFast。这个改动量不大收益很高分辨率切换失败和全屏独占导致的调试困境都能绕开。4.3 输入回路DirectInput8 的轮询风格为何更适合游戏输入部分书里给了两套方案一套是 Win32 消息驱动的键盘处理WM_KEYDOWN另一套是 DirectInput8 的设备轮询。后者才是游戏真正用的原因在于消息驱动键盘在快速连按时会丢按键——WM_KEYDOWN 这条消息在系统队列里是有缓冲上限的同时按三个键以上更是不一定发得全这对需要组合键的格斗操作是致命伤。DirectInput8 的思路是完全绕过消息系统直接读键盘设备状态。流程是DirectInput8Create 拿到工厂接口CreateDevice 创建键盘设备SetDataFormat 把数据格式定成标准键盘布局c_dfDIKeyboardAcquire 独占设备然后每帧 GetDeviceState 一次取走 256 字节的按键状态。每个字节对应一个键非零表示按下代码写出来大致是这样#include dinput.h #pragma comment(lib, dinput8.lib) #pragma comment(lib, dxguid.lib) LPDIRECTINPUT8 g_pDI NULL; LPDIRECTINPUTDEVICE8 g_pKeyboard NULL; char key[256]; // 每帧的键盘状态快照 void PollKeyboard(void) { HRESULT hr g_pKeyboard-GetDeviceState(sizeof(key), key); if (FAILED(hr)) { // 设备被抢占比如用户按了 CtrlAltDel重新获取 g_pKeyboard-Acquire(); } }key 数组的下标对应 DIK_* 常量比如 DIK_LEFT 就是左方向键判断按键时写 if (key[DIK_LEFT] 0x80) 就能知道左键是否按住0x80 是因为键状态高位为 1 表示按下。轮询帧率跟游戏循环同步不存在消息队列堵住导致输入延迟的问题这正是游戏不用消息键盘的根本原因。Acquire 在切换窗口或系统弹安全对话框后会失败所以 GetDeviceState 返回错误时重新 Acquire 一次是标准恢复动作。音频部分的 DirectSound8 与键盘逻辑同构创建对象、设置协作级别、创建主缓冲和次级缓冲、往缓冲里写波形数据并 Play。书里配套的 wav 加载函数直接读 RIFF 文件头代码不长但建议调试时先确认素材是 16 位 PCM 编码的 wav有些网上找的 WAV 是 MP3 编码伪装成 wav 扩展名加载会直接失败。注意DirectDraw 表面锁定时返回的 pitch 和你想象的每行字节数经常不一致老代码用 lPitch 而不是 width*bpp/8 去跳行的写法一定要保留否则画面会出现斜切或错位。5. 避坑与排查二十年前代码移植到今天的五个翻车现场5.1 现象C1083“无法打开包括文件 windows.h”原因Visual Studio 只装了 UWP 负载或根本没装“使用 C 的桌面开发”桌面 API 的头文件和库路径不存在。这个最容易跟 DirectX 混淆不要一看到头文件错误就去装老 SDK。解决进 Visual Studio Installer勾选“使用 C 的桌面开发”确认右侧组件列表里的“Windows 10 SDK”处于选中状态装完重启 VS 再编译。验证方式新建一个空 C 控制台程序写一行 #include windows.h 能编译通过说明基础环境已具备。像这种几百个源文件同时报同一个头文件缺失的场景先把工作负载装对再谈别的别在项目属性里瞎翻。5.2 现象LNK2019 无法解析的外部符号 IID_IDirectDraw7原因DirectDrawCreateEx 的实现在 ddraw.lib 里但接口的 GUID 常量定义在 dxguid.libVC6 老工程默认链了后者新工程没链。解决项目属性-链接器-输入-附加依赖项里追加 dxguid.lib同时确认列表里已有 ddraw.lib、dinput8.lib、dsound.lib、winmm.lib。这个坑在 DirectInput 和 DirectSound 部分同样会发作一次把五个库全加上可以少走弯路。如果某个 GUID 符号换了库名还在报错用 dumpbin /symbols 查对应 .lib 里是否真有这个符号能快速定位是不是手滑拿到 64 位版本的库。5.3 现象程序一进全屏就黑屏或崩溃返回错误码 DDERR_EXCLUSIVEMODE_ALREADYSET原因老式全屏独占模式在现代 Windows 显示驱动模型下的兼容性很差系统已经接管了显示模式切换不支持应用随意 SetDisplayMode更常见的是窗口句柄或协作级别没配对导致第二次调用时直接报“独占模式已被设置”。解决放弃全屏按 4.2 讲的方式改窗口模式加 BltFast若一定要全屏用 DXWnd 这类社区包装器把 DirectDraw 调用转接到现代图形接口反而比在源码里硬调驱动稳定。别指望靠修改分辨率参数比如把 640x480 改成 1920x1080解决问题问题不在分辨率而在模式本身。遇到这个报错时先检查 SetCooperativeLevel 返回的是不是 DD_OK这一步失败后面全是无效状态。5.4 现象源码注释全是乱码修改文件后再编译报语法错误原因2002 年的源码文件是 GBK/GB2312 编码而现代 Visual Studio 默认按 UTF-8 解码中文字符被读成乱码一旦你在这个乱码视图里保存过文件连代码里的字符串常量都可能被写坏。解决VS 里用“文件→打开→带编码打开”编码选“简体中文(GB2312)”文件会正确显示再次保存时建议另存为 UTF-8 with BOM既保留中文又避免后续工具链误读。如果文件已经被二次保存污染从原始 zip 里重新解压那份文件再来一次——所以一开始就别在原目录里改拷贝出来再动手。中文乱码不影响功能但影响阅读尤其是书里不少注释里藏着设计思路读不懂注释后面排错会慢很多。5.5 现象窗口模式跑起来画面模糊、鼠标坐标整体偏移原因Windows 10/11 默认对高 DPI 屏幕做缩放老程序被当作低分辨率程序拉伸显示GDI 和 DirectDraw 的坐标也跟着错位。解决在生成的 exe 上右键-属性-兼容性-更改高 DPI 设置勾选“替代高 DPI 缩放行为”缩放执行选择“应用程序”想从代码层面一劳永逸就在 WinMain 开头调用 SetProcessDPIAware()告诉系统这程序自己处理 DPI。前者适合快速验证后者适合做完后再整体收进代码。调试时还有一个连带现象窗口标题栏正常但客户区只有左上角一块在刷新多半也是 DPI 缩放导致的无效区域计算错误优先把缩放行为改掉再看。6. 验证游戏循环还活着给老代码加一个 FPS 调试覆盖层前面几章把工程跑通之后你会发现一个尴尬的现实老代码没有方便的调试输出全屏下连 printf 都看不见。我的习惯是给每个移植后的工程先加一个 FPS 统计函数——把帧率写到窗口标题栏几秒钟就能确认游戏循环的节奏是否正常。实现思路在游戏主循环的 Game_Main() 里每帧调用一次统计函数每过一秒算一次帧数然后把结果连同原标题一起 SetWindowText 到窗口标题栏。代码如下// 放在全局区 #include mmsystem.h #pragma comment(lib, winmm.lib) static DWORD s_lastTick timeGetTime(); static int s_frameCnt 0; // 放在 Game_Main() 末尾 void CheckFPS(HWND hwnd, const char* title) { s_frameCnt; DWORD now timeGetTime(); if (now - s_lastTick 1000) { float fps s_frameCnt * 1000.0f / (now - s_lastTick); char buffer[128]; wsprintf(buffer, %s - %.1f FPS, title, fps); SetWindowText(hwnd, buffer); s_lastTick now; s_frameCnt 0; } }timeGetTime 精度是毫秒级凑合够用想更准可以换 QueryPerformanceCounter但对验证循环来说 timeGetTime 的误差不影响判断。注意这里故意用 char 和 wsprintf是因为前面设置了多字节字符集——如果你把字符集改回 UnicodeSetWindowText 和 wsprintf 都会被宏展开成宽字符版本这段代码的参数类型就会集体报错。这也是为什么我在 3.2 里反复强调字符集要先定好的原因。放置位置也有讲究。窗口消息处理函数里别放那里由系统事件驱动频率不稳一定要放在和 PeekMessage 同一节奏的 Game_Main 里测出来的才是真实游戏帧率。如果发现标题栏数字长期是个位数先别怀疑代码逻辑——回第 5 章看全屏与 DPI 那两条多半是窗口模式或缩放惹的祸。这个覆盖层还有一个额外用途判断消息泵是否卡死。某次我把一个老工程的窗口初始化改成全屏、失败后没有任何回退窗口根本起不来加了 FPS 覆盖层后发现标题栏完全不刷新才定位到是 SetDisplayMode 失败后没检查返回值后续全在无效状态下执行。从那以后我每次移植老工程第一步永远是加这个标题栏 FPS 层第二步才去动渲染逻辑省掉的调试时间早就超过了写这几十行代码的成本。希望帮到你。本文还有配套的精品资源点击获取
返回列表