尧图精选

C++与DirectX 11吃豆人源码解析:从渲染管线到幽灵AI状态机

🕒 发布时间:2026/10/1 20:18:52 📁 来源:尧图网络
简介这是一份面向C与DirectX 11图形编程学习者的吃豆人游戏完整源码工程以1980年街机原版为灵感适合想通过实战项目理解2D渲染、游戏循环与AI状态机的开发者练手。压缩包共61个文件约6MB包含18个h头文件与15个cpp源文件承载游戏逻辑8个hlsl着色器负责渲染管线另有png、gif等贴图与动画素材以及sln、vcxproj等Visual Studio 2017工程配置可直接编译运行。项目实现了方向键移动、能量剂追逐、多阶段幽灵行为等玩法每个幽灵拥有基于原版的独立AI并记录了Pinky与Inky行为差异的已知问题。世界由分离立方体构成的2D地图生成并对相邻立方体做合并优化以减少三角形数量、消除z-fighting。目前已有109人学习读者可从中获得DirectXTK集成方式、精灵表渲染、角色与幽灵状态切换等可复用的实现思路。1. 从一份 C 与 DirectX 11 的吃豆人源码包说起很多人第一次想用 C 写游戏卡住的地方不是语法而是「窗口怎么开、精灵怎么画、循环怎么转」。这份《使用 C 和 DirectX 11 开发的吃豆人游戏》源码包正好把这条链路完整走了一遍它用 C 配合 DirectX 11 复刻了 1980 年那版吃豆人的核心玩法地图、幽灵 AI、能量剂、多阶段追逐与逃跑状态都在里面。它依赖 DirectXTK通过 NuGet 包引入解决方案面向 Visual Studio 2017。适合两类人一类是学过 C 基础、想找一个能编译能跑的小游戏项目练手的人另一类是想研究经典街机 AI 状态机怎么落地的人。下面我按「能编译、能看懂、能改」的顺序拆一遍。2. 工程结构与 DirectX 11 渲染管线先搞清楚每个文件在干什么2.1 目录清单与模块职责拿到压缩包先别急着双击 .sln先看目录。这个项目的文件组织是典型的「按职责分文件」而不是「按类分文件」理解这一点后面读代码会顺很多。文件/目录职责Main.cpp程序入口创建窗口与主循环Game.cpp / Game.h游戏主逻辑状态更新与渲染调度World.cpp / World.h地图生成、立方体合并优化Character.cpp / Character.h角色基类位置与移动Pacman 相关逻辑吃豆人移动、吃豆、被吃判定Ghost.cpp / Ghost.h幽灵基类与各自 AIDots.cpp / Dots.h豆子与能量剂管理Camera.cpp / Camera.h相机含底部视角ShaderManager着色器加载与管理StepTimer.h固定步长计时Caption.cpp / Caption.h文字/标题渲染Resources精灵表、图标、manifest、rcPacman.sln / .vcxprojVS2017 解决方案与工程文件packages.configNuGet 依赖声明DirectXTK这里有个容易被忽略的点World 是把 2D 地图用分离立方体渲染的然后对相邻立方体做了合并优化目的是减少三角形数量、消除 z-fighting。你在底部视角能直接看到这个优化结果。如果你后面想改成纯 2D 精灵渲染World 这一层是首先要动的地方。2.2 渲染循环与 StepTimer 的配合DirectX 11 的典型结构是「初始化设备 → 每帧清屏 → 更新 → 绘制 → Present」。这个项目用 StepTimer.h 做固定步长更新把逻辑更新和渲染解耦。常见做法是逻辑固定 60Hz渲染尽量快这样幽灵移动速度不会因为帧率波动而忽快忽慢。// Main.cpp 主循环的典型骨架按项目结构还原 while (msg.message ! WM_QUIT) { if (PeekMessage(msg, nullptr, 0, 0, PM_REMOVE)) { TranslateMessage(msg); DispatchMessage(msg); } else { m_timer.Tick([]() { // 固定步长内更新游戏逻辑 m_game-Update(m_timer); }); m_game-Render(); // 渲染与逻辑分离 } }逻辑说明PeekMessage 非阻塞取消息没有消息时才走更新和渲染避免窗口拖动时卡死。Tick 内部按固定步长回调保证幽灵和吃豆人速度一致。参数说明StepTimer 的步长一般设 1/60 秒如果你觉得整体偏快改的是游戏内速度系数而不是步长否则物理表现会变。2.3 地图生成与立方体合并World 用立方体拼 2D 地图再合并相邻立方体。合并的判断通常是「同类型且相邻」就合成一个更大的矩形。这一步不做三角形数量会成倍上涨低端显卡上直接掉帧。// World.cpp 合并相邻立方体的思路伪代码还原 for (int y 0; y height; y) { for (int x 0; x width; x) { if (map[y][x] WALL !merged[y][x]) { int w 1; // 向右扩展同类型格子 while (x w width map[y][x w] WALL) w; // 生成一个宽 w 的立方体标记已合并 AddCube(x, y, w); for (int i 0; i w; i) merged[y][x i] true; } } }逻辑说明按行扫描遇到未合并的墙就向右扩展生成一个宽矩形。参数说明合并只做水平方向是最简版本垂直方向合并能进一步减面但要注意别把通道口堵住。坑在于合并后碰撞检测要按矩形算不能还按单格算否则吃豆人会卡在墙里。3. 幽灵 AI 与状态机1980 版行为怎么在代码里落地3.1 四个幽灵的差异化 AI项目说明里写得很清楚每个幽灵都有基于 1980 版原版的独特 AI唯一区别是 Pinky 和 Inky 的行为存在已知且被记录的错误作者没有在自己的版本里实现这个 bug。这句话信息量很大——它意味着你拿到的是一个「修正版」AI而不是像素级复刻。常见做法是Blinky 直接追吃豆人当前位置Pinky 追吃豆人前方若干格Inky 用吃豆人和 Blinky 的位置做向量运算取目标点Clyde 距离远时追、距离近时回角落。这些目标点再配合「幽灵不能反向」的移动规则就形成了原版那种似追非追的手感。// Ghost.cpp 目标点计算示意 Vector2 Ghost::ComputeTarget(const Vector2 pacmanPos, const Vector2 pacmanDir) { switch (m_type) { case GhostType::Blinky: return pacmanPos; // 直接追 case GhostType::Pinky: return pacmanPos pacmanDir * 4; // 前方 4 格 case GhostType::Inky: return pacmanPos (pacmanPos - m_blinkyPos) * 2; // 向量翻倍 case GhostType::Clyde: return (Distance(m_pos, pacmanPos) 8) ? pacmanPos : m_scatterCorner; } }逻辑说明每个幽灵返回一个目标格寻路时选「离目标最近且不反向」的方向。参数说明Pinky 的前方格数、Inky 的翻倍系数、Clyde 的 8 格阈值都是可调手感参数改大改小直接影响难度。注意这些数值不是原版精确值是作者调过的手感值。3.2 追逐与逃跑的状态切换游戏有不同阶段幽灵可以追吃豆人也可以逃跑。这个切换由「能量剂是否生效」驱动。吃到能量剂后所有幽灵进入 frightened 状态方向反转并远离吃豆人计时结束或吃到幽灵后恢复。// Game.cpp 状态切换 void Game::UpdateGhostStates(float dt) { if (m_frightenedTimer 0.0f) { m_frightenedTimer - dt; for (auto g : m_ghosts) g-SetState(GhostState::Frightened); } else { for (auto g : m_ghosts) g-SetState(GhostState::Chase); // 或 Scatter按阶段表 } }逻辑说明用一个全局计时器控制所有幽灵的恐惧状态简单直接。参数说明frightenedTimer 的初始值决定能量剂持续时间原版随关卡递减这里可以做成按关卡查表。坑在于状态切换那一帧如果没做方向反转幽灵会原地抖动。3.3 移动与网格对齐吃豆人和幽灵都在网格上移动但视觉上是平滑的。常见做法是逻辑位置按格算渲染位置做插值。如果直接按像素移动转弯时会对不齐格子出现「卡角」。// Character.cpp 网格对齐移动 void Character::Move(float dt, const Vector2 dir) { m_pos dir * m_speed * dt; // 接近格子中心时吸附保证能转弯 Vector2 center RoundToGrid(m_pos); if (Distance(m_pos, center) m_snapThreshold) m_pos center; }逻辑说明移动后判断是否接近格心接近就吸附避免累积误差。参数说明snapThreshold 太小会卡角太大会瞬移一般取速度乘以步长的量级。这个阈值是手感玄学调的时候要边跑边看。4. 编译与运行VS2017 DirectXTK 的完整落地步骤4.1 环境准备与 NuGet 还原项目面向 Visual Studio 2017依赖 DirectXTK。packages.config 里声明了 NuGet 包所以第一步是让 VS 还原包。# 在解决方案目录下用 nuget 还原或直接在 VS 里右键解决方案还原 nuget restore Pacman.sln逻辑说明packages.config 是老式 NuGet 管理方式VS2017 打开解决方案时会提示还原。参数说明如果还原失败检查 NuGet 源是否可用或手动在 VS 的「管理 NuGet 程序包」里搜 DirectXTK 安装。注意 DirectXTK 版本要和工程里的引用一致版本错位会报链接错误。4.2 编译配置与常见报错打开 Pacman.sln确认平台是 x86 还是 x64Debug 还是 Release。DirectX 项目常见报错集中在三处找不到 d3d11.lib、找不到 DirectXTK 头文件、manifest 冲突。!-- Pacman.vcxproj 里需要确认的链接库 -- AdditionalDependencies d3d11.lib;dxgi.lib;d3dcompiler.lib;%(AdditionalDependencies) /AdditionalDependencies逻辑说明DirectX 11 的核心库是 d3d11、dxgi、d3dcompiler缺一个都会链接失败。参数说明如果用了 DirectXTK 的 SpriteBatch还要确保 DirectXTK 的 lib 路径在附加库目录里。settings.manifest 负责 DPI 感知如果报 manifest 重复检查 resource.rc 里是否重复引用了 manifest。4.3 运行与操作编译通过后直接 F5 运行。控制是方向键Left/Right/Up/Down 移动。手感比 1980 版稍快这是作者有意调的。窗口标题和图标由 directx.ico 和 caption.png 提供。提示第一次运行如果黑屏先确认显卡驱动支持 DirectX 11再检查是否在虚拟机里跑——部分虚拟机的 D3D 支持不完整会直接创建设备失败。5. 避坑与排查这份源码最容易翻车的五个地方5.1 编译报「无法打开 DirectXTK 头文件」现象一编译就报 CommonStates.h 或 SpriteBatch.h 找不到。原因NuGet 包没还原或者还原到了错误的 packages 目录。解决右键解决方案选「还原 NuGet 程序包」确认 packages 目录生成再检查工程属性里的附加包含目录是否指向 packages\directxtk.版本\include。5.2 运行时报「D3D11CreateDevice 失败」现象程序启动即崩调试输出显示设备创建失败。原因显卡不支持 DirectX 11或远程桌面/虚拟机环境 D3D 受限。解决换本机物理显卡环境运行如果必须在受限环境改用 WARP 软件渲染设备调试但性能会明显下降。5.3 幽灵卡在墙角或原地抖动现象幽灵追到拐角处不动或来回抖。原因网格吸附阈值和转向判定冲突或者状态切换那一帧没做方向反转。解决调大 snapThreshold 让角色更早对齐格心在 Frightened 切换时强制反转方向并重置目标格。5.4 地图出现闪烁或 z-fighting现象墙面接缝处有闪烁条纹。原因相邻立方体共面且深度值相同。解决确认 World 的合并逻辑生效合并后的大立方体不会和相邻小立方体共面必要时给墙面加微小深度偏移。5.5 吃豆人穿过豆子却没吃到现象视觉上碰到了豆子但分数没涨。原因碰撞检测用的是渲染插值后的位置而豆子判定用的是逻辑格位置两者不同步。解决统一用逻辑格位置做碰撞判定渲染位置只负责画。6. 进阶玩法把这份源码改成你自己的实验场跑通之后这份源码真正的价值在于它是一个可改的实验场。我给你三个我实际会做的改法按难度递增。第一个是调幽灵 AI 参数做难度曲线。把 ComputeTarget 里的前方格数、翻倍系数、Clyde 阈值抽成配置文件按关卡读表。这样你不用重编译就能试不同难度比硬编码强很多。我一般会做一个 CSV每关一行改完直接跑。第二个是把 World 的立方体渲染换成纯 2D 精灵。Resources 里已经有 pacman.png、ghosts.png、dot.png、map.png精灵表是现成的。你要做的是保留 World 的碰撞数据把渲染层换成 SpriteBatch 画 2D 图。这样三角形数量骤降低端设备也能跑满帧。// 用 DirectXTK 的 SpriteBatch 画 2D 精灵替换立方体渲染 m_spriteBatch-Begin(); m_spriteBatch-Draw(m_pacmanTex.Get(), m_pacmanScreenPos, nullptr, Colors::White, 0.0f, XMFLOAT2(0, 0), 1.0f); m_spriteBatch-End();逻辑说明Begin/End 之间批量提交精灵比逐个画效率高。参数说明第三个参数是源矩形精灵表要传对应帧的区域旋转和缩放按需给。注意 SpriteBatch 的坐标系和 3D 相机不同换渲染层时相机那套要一起改。第三个是加一个简单的关卡编辑器。地图本质是二维数组你写个工具把 map.png 的像素读成格子类型导出成代码里的数组就行。这一步做完你就能自己设计地图而不是只能玩作者给的那一张。验证改动是否正确的办法很朴素改完先跑一遍完整游戏看吃豆人能不能正常吃豆、幽灵会不会卡死、能量剂状态切换是否干净。我踩过的坑是只测了正常路径没测「吃到能量剂瞬间被幽灵碰到」这种边界结果状态机直接乱掉。从那以后我每次改状态相关逻辑都强制走一遍「吃能量剂 → 追幽灵 → 计时结束 → 恢复正常」的完整循环再补一个「能量剂生效时被吃」的反例。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →