新闻详情

新闻详情

首页 / 资讯中心 / 详情

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

发布时间:2026/10/1 20:18:50来源:尧图网络
C++与DirectX 11吃豆人源码解析:从渲染管线到幽灵AI状态机
简介这是一份面向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 的像素读成格子类型导出成代码里的数组就行。这一步做完你就能自己设计地图而不是只能玩作者给的那一张。验证改动是否正确的办法很朴素改完先跑一遍完整游戏看吃豆人能不能正常吃豆、幽灵会不会卡死、能量剂状态切换是否干净。我踩过的坑是只测了正常路径没测「吃到能量剂瞬间被幽灵碰到」这种边界结果状态机直接乱掉。从那以后我每次改状态相关逻辑都强制走一遍「吃能量剂 → 追幽灵 → 计时结束 → 恢复正常」的完整循环再补一个「能量剂生效时被吃」的反例。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Codex插件精选:10个装完没卸过的高效工具与配置指南 2026/10/1 21:06:12

Codex插件精选:10个装完没卸过的高效工具与配置指南

1. 为什么我最终只留下了这 10 个 Codex 插件刚上手 Codex 那阵子,我跟很多人一样,看到插件市场里琳琅满目的条目就手痒,恨不得把首页推荐的全都点一遍安装。结果呢?CLI 启动越来越慢,/responses端点时不时报错&#x…

阅读更多 →
MATLAB结构体(struct)实战:从创建到字段操作与数据管理 2026/10/1 21:06:12

MATLAB结构体(struct)实战:从创建到字段操作与数据管理

1. 为什么要用struct做结构化数据?——先搞懂它在解决什么问题很多初学MATLAB的朋友都会有这样一个阶段:脚本里写满了name MATLAB、version 2024a、path C:/xxx这种散装变量。单个变量用起来挺直观,但数据一多就乱套了。比如你要管理多个测…

阅读更多 →
从路由表到回程路由:华为静态路由配置与排错全解析 2026/10/1 21:06:12

从路由表到回程路由:华为静态路由配置与排错全解析

提到静态路由配置,很多刚接触网络的朋友第一反应是“它不过就是敲几条命令的事”。实际上,我在企业里做网络日常维护这几年,见过的最多的排障场景恰恰就发生在静态路由这条“最简单的配置”上——有的是下一跳地址写错,有的是忘配…

阅读更多 →
Maven 3.8+报错Blocked mirror:根因分析与三种解决方案 2026/10/1 21:06:12

Maven 3.8+报错Blocked mirror:根因分析与三种解决方案

1. 现场还原:先看懂 “Blocked mirror” 到底拦住了什么1.1 一条完整的报错长什么样先说一下我当时的情况。项目一直用 Maven 3.6.3 跑得好好的,某天为了统一构建环境把本机 Maven 升到了 3.8.8,结果执行mvn clean package -DskipTests的时候…

阅读更多 →
Aider-TUI上手:终端里的AI结对编程与Git自动提交实战 2026/10/1 21:06:11

Aider-TUI上手:终端里的AI结对编程与Git自动提交实战

1. 为什么是"Aider"——从AI结对编程到终端里的AI同事1.1 先搞清楚Aider-TUI到底做了什么我一直觉得,AI编程工具和"结对编程"之间的关系,很多人是误会的。Copilot是"补全",Cursor是"对话框里的编辑器&quo…

阅读更多 →
想做小程序不知从哪开始?做小程序的步骤保姆级拆解一文讲透 2026/10/1 21:06:05

想做小程序不知从哪开始?做小程序的步骤保姆级拆解一文讲透

想做小程序却卡在第一步?新手的迷茫很正常 几乎每个新手都经历过这个阶段:搜了半天信息越看越乱,有人说要注册公众号,有人说要买服务器,越看越觉得复杂。 做小程序的步骤到底是什么?第一步该干嘛&#xff1…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉