简介:这是一套面向C/C++学习者和游戏开发入门者的泡泡堂(炸弹人)2D小游戏完整源码,适合用作课程设计、编程实训或自学练手项目。压缩包共346个文件,约75.03MB,其中1个核心cpp源码与cbp工程文件构成主体,另有235张png、50张jpg图片及13个wav音频,配套psd分层素材、txt说明和可执行程序,便于直接运行、替换美术资源与对照调试。已有3608人学习,说明其在同类练手项目中具有一定参考热度。源码围绕游戏循环、画面渲染、键盘输入处理、角色移动与炸弹爆炸碰撞检测展开,并演示EGE图形库的窗口绘制、图片加载和音乐播放整合方式;读者可据此理解类与对象如何组织角色、地图和道具,掌握资源路径配置与编译链接要点,也能在现有结构上扩展关卡、道具和玩法,积累完整的2D游戏开发经验。
1. 先别急着编译:这套泡泡堂源码到底能跑出什么
如果你搜到这套泡泡堂(炸弹人)小游戏 C/C++ 完整源码,多半不是想再玩一遍,而是想拆它的游戏循环、格子碰撞和资源加载。它把角色移动、放炸弹、爆炸波及、可破坏墙体、背景音乐和图片素材揉在一个可直接编译的 C++ 工程里,适合学 C++ 游戏编程的练手场景,也适合拿来做课程设计或二次改玩法。它不是商业级引擎,渲染和音频大概率是教学级封装,别指望开箱即用。
我建议先别急着点运行。先确认图形库和音频库,再把资源路径从作者机器上的绝对路径改成相对路径,最后才看核心逻辑。源码的价值不在“能跑”,而在你能把炸弹倒计时、爆炸链式反应和精灵绘制拆成自己的模块。下面按资源落地、核心循环、渲染音频、避坑和调参五步走。
2. 拆资源包:从图片音乐到可编译工程的落地顺序
拿到一个 C/C++ 小游戏源码包,最容易被忽略的不是语法,而是“它默认在什么目录下运行”。很多泡泡堂源码在作者机器上能跑,换一台机器就黑屏、无声、甚至编译报错,问题往往出在资源路径、图形库和音频接口。先把工程当黑匣子拆开,确认哪些文件是源码、哪些是资源、哪些是工程配置,后面改逻辑才不会被环境问题拖着走。
2.1 先看目录:源码、图片、音频和配置文件的边界
我一般先不打开 IDE,而是在源码根目录列两级目录,再用 grep 把资源扩展名和加载函数扫一遍。这样能快速判断这套代码是“资源散落在工程根目录”,还是“有独立的 res 目录”,也能看出它用的是哪套图形/音频接口。常见目录不会完全一样,但职责大致如下:
| 类型 | 常见扩展名/文件名 | 源码里对应的加载方式 | 检查点 |
|---|---|---|---|
| C++ 源码 | .cpp、.h、.c | 主循环、玩家、炸弹、地图 | 是否有 main.cpp 和游戏循环 |
| 图片资源 | .bmp、.png、.jpg | loadimage、SDL_LoadBMP、贴图函数 | 文件名大小写、透明色 |
| 音频资源 | .wav、.mp3、.ogg | PlaySound、mciSendString、Mix_OpenAudio | 是循环 BGM 还是短音效 |
| 地图/配置 | .txt、.dat、.ini | fopen、ifstream、解析函数 | 行列数是否和数组一致 |
| 工程文件 | .sln、.vcxproj、CMakeLists.txt | IDE 或 CMake | 平台位数、链接库 |
在源码根目录执行下面这段扫描,先把所有资源引用和库调用列出来。命令里的-R是递归,-I忽略二进制文件,-n带行号,-E用扩展正则;如果你在 Windows 上,可以用常见的 shell 环境执行,不要直接塞进 cmd。
# 列出源码里出现的图片/音频资源名,先建立资源清单 grep -RInE '\.(bmp|png|jpg|jpeg|wav|mp3|ogg)' . --include='*.cpp' --include='*.h' --include='*.c' # 再找加载函数,判断图形库和音频库 grep -RInE 'loadimage|putimage|SDL_LoadBMP|PlaySound|mciSendString|Mix_OpenAudio' .逻辑说明:第一条命令帮你找出所有硬编码在源码里的资源文件名,后面改路径时按这份清单逐项核对,避免漏掉某张精灵图。参数说明:--include限制只扫 C/C++ 文件,防止把二进制资源当文本读;如果源码用了.cc或.hpp,把扩展名补进去。第二条命令是选型判断,看到loadimage/putimage基本可以确定是 EasyX 风格接口,看到SDL_前缀就是 SDL2 系,看到PlaySound说明至少依赖 Windows 多媒体接口。
这一步不要急着改代码。先把扫描结果复制到一个临时文本里,按“图片、音频、地图、字体”分类。后面编译报错时,你能快速判断是缺文件、路径错,还是库没链接。很多所谓“源码跑不起来”的问题,最后都落在资源清单没对齐上。
2.2 依赖确认:图形库和音频库不是标准 C++ 的一部分
C++ 标准库只提供容器、字符串、文件流这些基础设施,不提供窗口、贴图和声音播放。这套泡泡堂源码能显示画面,必然依赖某个图形层;能放背景音乐,必然依赖某个音频接口。常见组合是 EasyX + Windows 多媒体接口、SDL2 + SDL_mixer,或者 Win32 GDI + PlaySound。选型理由不复杂:教学源码偏爱 EasyX,因为接口短、贴图快;跨平台项目偏爱 SDL2,因为窗口和音频抽象更完整;老式 Win32 代码则直接调 GDI。
判断依赖后,先检查工程配置里的“附加包含目录”和“附加库目录”,再看链接器输入。如果源码包自带工程文件,优先用原工程打开,不要一上来就新建工程,否则很容易漏掉预处理器宏和链接库。下面是一个最小 CMake 骨架,只用来把源文件和 Windows 音频库串起来;实际源文件名要按你的目录替换,不要照抄文件名。
cmake_minimum_required(VERSION 3.16) project(bomber LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 源文件列表按实际源码替换,不要假设文件名 add_executable(bomber src/main.cpp src/game.cpp src/resource.cpp ) # Windows 下 PlaySound/mciSendString 常见依赖 target_link_libraries(bomber PRIVATE winmm) # 如果源码是 SDL2 系,取消下面两行并按本机 SDL2 安装路径配置 # find_package(SDL2 REQUIRED) # target_link_libraries(bomber PRIVATE SDL2::SDL2 SDL2::SDL2main)逻辑说明:add_executable把源码编成一个可执行文件,target_link_libraries把音频接口需要的winmm链进来。参数说明:CMAKE_CXX_STANDARD 17是保守选择,老代码如果用了 C++11 也能降;SDL2::SDL2main在 Windows 上负责入口替换,缺它会出现main重定义或入口找不到。注意库的位数必须和工程一致,32 位工程配 32 位库,64 位配 64 位,混用会报一堆看不懂的链接错误。
提示:先备份原工程文件,再改包含目录和链接库。原工程能跑的话,优先修路径,不要重写构建系统。
2.3 第一次编译:把工作目录和资源路径钉死
编译通过但运行黑屏,是这类源码最常见的第一次翻车。原因通常不是图片没打包,而是相对路径依赖“当前工作目录”。在 IDE 里点调试,工作目录可能是工程目录;双击 exe,工作目录可能变成 exe 所在目录,两边不一致,res/xxx.bmp就找不到了。我的做法是先写一个资源路径函数,统一从 exe 所在目录出发,再把所有加载调用改成走这个函数。
#include <windows.h> #include <string> #include <filesystem> std::string exeDir() { char buf[MAX_PATH] = {0}; GetModuleFileNameA(nullptr, buf, MAX_PATH); // 取 exe 全路径的父目录 return std::filesystem::path(buf).parent_path().string(); } std::string resPath(const std::string& rel) { // 统一从 exe 目录拼 res,避免双击运行时工作目录变化 return (std::filesystem::path(exeDir()) / "res" / rel).string(); }逻辑说明:GetModuleFileNameA拿到当前进程的 exe 全路径,parent_path()取所在目录,resPath再把资源相对路径拼上去。参数说明:MAX_PATH是传统路径长度限制,普通教学项目够用;如果资源目录很深,改用宽字符接口或动态长度。调用时把loadimage(&img, "res/player.bmp")改成loadimage(&img, resPath("player.bmp").c_str()),这样无论从哪里启动,路径都稳定。
如果资源不在 exe 目录下,而是在工程目录,有两个选择:一是把res整个复制到输出目录,二是在 IDE 调试设置里把工作目录固定到工程目录。前者更适合交付,后者更适合开发。无论选哪种,都在启动时打印一次资源绝对路径,黑屏时先看日志,不要凭感觉改代码。
3. 炸弹人核心循环:地图网格、移动与碰撞的 C++ 实现
泡泡堂这类炸弹人玩法,底层不是复杂的物理引擎,而是“格子状态 + 定时器 + 范围扩散”。地图是二维数组,玩家和炸弹都落在格子上,移动是像素插值,碰撞只查目标格。把这套模型写顺,C++ 游戏编程里最常练的循环、输入、碰撞、计时就都串起来了。下面用一套可替换的骨架代码讲清楚,实际源码里的类名和图形接口可以不同,逻辑可以照着搬。
3.1 用二维数组描述可破坏地图
先把地图元素枚举出来:空地、硬墙、软墙、炸弹、火焰、道具。硬墙不可破坏,软墙可以被炸掉,炸弹占格但不应永久写进地图层,否则玩家会被自己的炸弹卡住。教学源码常见做法是地图层和实体层分开,地图层只存静态地形,炸弹和火焰放独立容器或覆盖层。下面是为了讲清楚而合并在一起的简化版,实际落地时建议拆开。
constexpr int ROW = 13; constexpr int COL = 15; constexpr int TILE = 48; // 像素格子大小,必须和精灵帧一致 enum TileType { EMPTY = 0, HARD = 1, SOFT = 2, BOMB = 3, FLAME = 4, ITEM = 5 }; int grid[ROW][COL] = { {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}, {1,0,0,2,0,0,2,0,0,2,0,0,0,0,1}, // 中间行按地图文件或编辑器填充,这里省略 {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} }; bool inBounds(int r, int c) { return r >= 0 && r < ROW && c >= 0 && c < COL; } bool blocked(int r, int c) { // 越界、硬墙、软墙都算阻挡;炸弹是否阻挡要看你的玩法 return !inBounds(r, c) || grid[r][c] == HARD || grid[r][c] == SOFT; }逻辑说明:ROW/COL决定地图大小,TILE决定一个格子多少像素,grid存静态地形。blocked只做地形碰撞,炸弹和玩家实体另外查,这样不会出现“炸弹写进地图后忘记清除”的连锁问题。参数说明:TILE必须和图片帧宽高一致,否则贴图会错位;ROW/COL要和资源里的地图文件行列数一致,不一致时先改地图文件,不要硬改数组。
为什么不用像素级碰撞?因为炸弹人玩法天然是格子对齐的,用格坐标判断可以避免斜穿墙角、半格卡墙这些玄学问题。新手常见误用是把玩家坐标直接除以格子大小后取整,却不处理负数和边界,结果走到地图左边就数组越界。inBounds先判边界,再查地形,顺序不要反。
3.2 玩家移动与格子对齐
玩家移动我一般用“目标格检测 + 单轴移动 + 松键吸附”。先根据输入算下一帧的像素位置,再换算成目标格,目标格可走才更新坐标。斜向移动要拆成两个轴分别判断,否则角色会从两个软墙的夹角穿过去。松键后把坐标吸附到格中心,能显著减少贴墙抖动。
struct Player { float x = TILE * 1.5f; float y = TILE * 1.5f; float speed = 4.0f * TILE; // 每秒 4 格 }; bool canStand(float nx, float ny) { int c = int(nx / TILE); int r = int(ny / TILE); return !blocked(r, c); } void updatePlayer(Player& p, float dt, int dx, int dy) { if (dx == 0 && dy == 0) { // 松键吸附到格中心,减少像素级卡墙 p.x = (int(p.x / TILE) + 0.5f) * TILE; p.y = (int(p.y / TILE) + 0.5f) * TILE; return; } float nx = p.x + dx * p.speed * dt; float ny = p.y + dy * p.speed * dt; // 单轴分别判断,避免斜穿墙角 if (dx != 0 && canStand(nx, p.y)) p.x = nx; if (dy != 0 && canStand(p.x, ny)) p.y = ny; }逻辑说明:dt是上一帧到当前帧的秒数,speed是像素每秒,乘出来才是本帧位移。canStand把像素坐标换算成格坐标,再查blocked。参数说明:speed = 4.0f * TILE表示每秒走四格,想调快就改系数;dt要做上限保护,窗口拖动后不要让它变成几秒,否则角色会瞬移穿墙。
这里有一条血泪经验:像素坐标和格子坐标混用时,所有“取整”都要明确是向下取整还是四舍五入。int(nx / TILE)对正数没问题,如果地图有负坐标或摄像机偏移,先加偏移再取整。另外,玩家贴图锚点如果是左上角,碰撞盒要单独设,不要直接拿整张图当碰撞盒,否则角色会比视觉上更宽。
3.3 炸弹放置、爆炸波及与链式反应
炸弹的核心参数只有三个:倒计时、威力半径、火焰持续时间。倒计时用dt扣减,不要用Sleep卡住主循环;爆炸时向四个方向逐格扩散,遇到硬墙停止,遇到软墙破坏并停止,遇到另一颗炸弹就触发链式反应。链式反应用队列做广度优先,比递归安全,炸弹多了也不会爆栈。
struct Bomb { int r, c; int power = 2; float fuse = 2.5f; float flameTime = 0.5f; bool alive = true; }; void explode(std::vector<Bomb>& bombs, int r, int c, int power) { static const int dr[4] = {1, -1, 0, 0}; static const int dc[4] = {0, 0, 1, -1}; for (int d = 0; d < 4; ++d) { for (int step = 1; step <= power; ++step) { int nr = r + dr[d] * step; int nc = c + dc[d] * step; if (!inBounds(nr, nc) || grid[nr][nc] == HARD) break; if (grid[nr][nc] == SOFT) { grid[nr][nc] = EMPTY; // 软墙被炸掉 break; } if (grid[nr][nc] == BOMB) { // 实战中在这里找到对应炸弹,把 fuse 设为 0,下一帧触发链式 } // 火焰覆盖建议写入独立 flame 层,不要直接写进地形层 } } }逻辑说明:外层四方向,内层按power逐格推进;越界或硬墙立即停止,软墙破坏后停止,炸弹触发链式。参数说明:fuse = 2.5f是炸弹从放到炸的秒数,power = 2是左右上下各扩两格,flameTime控制火焰停留时间,实际渲染时用它做消失动画。链式反应不要在主循环里递归调用explode,用队列记录待爆炸的炸弹,统一在下一帧处理。
注意:火焰层和地形层要分开。把火焰写进
grid后,玩家查询碰撞会把火焰当墙,表现就是“炸完走不过去”。
4. 资源加载与渲染:图片、精灵动画和背景音乐怎么接
核心逻辑跑通后,画面和声音才是让这套源码“像个游戏”的部分。图片资源通常有两种组织方式:一张精灵表按帧裁剪,或者每个动作一组独立图片。音频也分两类:背景音乐要循环、不能每次重新打开文件;炸弹和道具音效要短、要能异步触发。渲染层则要处理双缓冲和帧率,否则画面闪烁、炸弹计时漂移。
4.1 图片资源加载与透明色/精灵表
如果源码里的角色是一张横向排列的精灵表,加载后不要整张画到屏幕上,要按帧宽高裁剪。BMP 通常没有 alpha 通道,教学源码会用洋红色RGB(255,0,255)做透明色;PNG 则直接带 alpha,但要看图形库是否支持。下面以 EasyX 风格接口示意,实际源码如果用的是 SDL2,把loadimage/putimage换成SDL_LoadBMP/SDL_BlitSurface即可。
struct SpriteSheet { IMAGE img; int frameW = 48; int frameH = 48; int frames = 4; COLORREF transparent = RGB(255, 0, 255); }; bool loadSheet(SpriteSheet& s, const std::string& path) { loadimage(&s.img, path.c_str()); // 如果图片是洋红底,需要设置透明色或使用 PNG 带 alpha 的版本 return true; } void drawFrame(const SpriteSheet& s, int frame, int px, int py) { int sx = frame * s.frameW; // 从精灵表裁剪一帧贴到屏幕坐标 putimage(px, py, s.frameW, s.frameH, &s.img, sx, 0); }逻辑说明:frameW/frameH是一帧的像素尺寸,frames是动作总帧数,sx是裁剪起点。参数说明:frameW/frameH必须和TILE对齐,否则角色会跨格;transparent只在 BMP 透明色方案里用,PNG 方案可以删掉。如果源码是每个动作一组文件,把SpriteSheet换成std::vector<IMAGE>,按frame下标取图即可。
图片路径最容易出大小写问题。Windows 文件系统不敏感,但打包到某些环境或压缩包里会敏感,Player.bmp和player.bmp可能只认一个。加载失败时先打印路径和文件是否存在,不要直接怀疑图形库。
4.2 背景音乐与音效的触发时机
背景音乐我一般用mciSendString打开一次、循环播放,炸弹音效用PlaySound异步触发。PlaySound简单,但同一时间只能播一个声音,适合短音效;mciSendString支持 wav/mp3,但命令字符串要处理好引号和路径。不要在每次放炸弹时重新open音频文件,否则会卡顿,还会留下未关闭的 alias。
#include <windows.h> #include <mmsystem.h> void playBgm(const std::string& path) { static bool opened = false; if (!opened) { std::string cmd = "open \"" + path + "\" alias bgm"; mciSendStringA(cmd.c_str(), nullptr, 0, nullptr); opened = true; } mciSendStringA("play bgm repeat", nullptr, 0, nullptr); } void playSfx(const std::string& path) { // 异步播放,避免阻塞游戏循环 PlaySoundA(path.c_str(), nullptr, SND_ASYNC | SND_FILENAME); }逻辑说明:playBgm第一次调用时open并起别名bgm,之后只发play bgm repeat循环;playSfx用SND_ASYNC立即返回。参数说明:SND_FILENAME表示按文件名播放,SND_ASYNC表示异步;如果路径带空格,mci命令里的引号不能省。音效多路并发要求高时,PlaySound不够用,需要换支持多通道的音频库,这是选型边界。
提示:背景音乐文件如果很大,不要在游戏循环里反复读盘。启动时打开一次,切场景时停止或换曲,退出时记得
close bgm。
4.3 双缓冲与帧率控制
直接往窗口 DC 上画,角色一多就会闪。双缓冲的思路是先把这一帧画到内存位图,再一次性刷到屏幕。EasyX 有BeginBatchDraw/FlushBatchDraw,SDL2 有SDL_RenderPresent,Win32 GDI 则要自己创建兼容 DC。帧率控制不要只写Sleep(16),因为Sleep不精确,逻辑更新和渲染也会绑死。我一般用dt驱动逻辑,再限制单帧最大步长。
void gameLoop() { const float dtCap = 0.05f; // 单帧最大 50ms,防止窗口拖动后逻辑跳跃 ULONGLONG last = GetTickCount64(); while (running) { ULONGLONG now = GetTickCount64(); float dt = (now - last) / 1000.0f; last = now; if (dt > dtCap) dt = dtCap; update(dt); // 逻辑更新:移动、炸弹倒计时、碰撞 BeginBatchDraw(); // 开始双缓冲 render(); // 画地图、角色、炸弹、UI FlushBatchDraw(); // 一次性刷到屏幕 } }逻辑说明:GetTickCount64取毫秒时间,除以 1000 得秒;dtCap防止调试断点或拖动窗口后dt过大。参数说明:dtCap = 0.05f对应 20 FPS 的最大步长,正常 60 FPS 时dt约 0.016;如果游戏逻辑要求更稳,可以改成固定时间步累加。BeginBatchDraw/FlushBatchDraw是 EasyX 的双缓冲接口,换成 SDL2 时删掉这两行,用SDL_RenderClear和SDL_RenderPresent替代。
渲染和逻辑要分离。不要在render里扣炸弹倒计时,也不要在update里贴图。这样你调帧率时不会改变玩法,录屏逐帧对逻辑也方便。
5. 避坑排查:编译、资源和运行时的 5 个翻车点
这套源码的坑大多不在算法,而在环境和资源。下面五条是我拆类似 C/C++ 小游戏时最常遇到的翻车点,每条按“现象、原因、解决”写,照着排查能省不少时间。
5.1 字符集不匹配导致中文路径读不到
现象:资源加载函数返回失败,日志里的路径是乱码,图片和音频都读不到。原因:工程字符集设置和源码字符串编码不一致,fopen、PlaySoundA这类窄字符接口对中文路径支持不稳定,源码文件本身也可能是 GBK 或 UTF-8 混用。解决:先把资源目录改成全英文路径,源码文件统一保存为 UTF-8;如果必须用中文路径,改用宽字符接口CreateFileW、PlaySoundW,并在工程里把字符集设成 Unicode。不要一边用std::string拼中文路径,一边调窄字符 API,这种组合最容易出玄学问题。
5.2 链接器报 unresolved external symbol
现象:编译通过,链接阶段报LNK2019,符号名看起来像PlaySoundA、mciSendStringA或某个图形库函数。原因:头文件找到了,但对应的.lib没加进链接器输入,或者工程是 x64 却链了 x86 的库。解决:先看报错符号属于哪个库,音频接口补winmm.lib,SDL2 补SDL2main.lib和SDL2.lib;再检查工程平台和库目录位数是否一致。不要用“关闭链接器警告”糊过去,缺库的符号运行时还会出问题。
5.3 资源路径依赖工作目录,双击 exe 就黑屏
现象:IDE 里调试正常,双击输出目录的 exe 就黑屏、无声,或者只显示 UI 不显示地图。原因:源码里写的是相对路径,IDE 调试时工作目录是工程目录,双击时工作目录变成 exe 目录,两边资源根不一致。解决:用GetModuleFileName拼 exe 目录,所有资源走统一resPath;或者在构建后把res复制到输出目录。启动时打印一次资源绝对路径,黑屏先看这个日志,比盲目改绘制代码有效。
5.4 音频格式不支持或播放线程卡死
现象:背景音乐不响,放炸弹时画面卡一下,或者切场景后声音重叠。原因:PlaySound默认同步播放会阻塞主循环,且同一时间只支持一个声音;mciSendString每次重新open会累积 alias,mp3 文件在部分环境还需要额外解码支持。解决:短音效用SND_ASYNC,背景音乐只open一次并play repeat;退出时closealias;优先用 wav 做音效,mp3 只做背景音乐。如果多路音效同时触发,换支持混音的音频库,不要硬扛。
5.5 炸弹爆炸越界与链式递归爆栈
现象:炸弹放在地图边缘时程序崩溃,或者一堆炸弹连锁时卡死、栈溢出。原因:爆炸扩散没有先判inBounds,直接查grid[nr][nc];链式反应用递归且没有访问标记,炸弹互相触发形成环。解决:扩散循环第一步判边界,硬墙立即break;链式反应用队列做广度优先,记录已触发炸弹,火焰写入独立层。下面这段是边界判断的最小写法,先保证不越界,再谈表现。
for (int d = 0; d < 4; ++d) { for (int step = 1; step <= power; ++step) { int nr = r + dr[d] * step; int nc = c + dc[d] * step; if (!inBounds(nr, nc)) break; // 先判边界 if (grid[nr][nc] == HARD) break; // 硬墙停止 if (grid[nr][nc] == SOFT) { // 软墙破坏后停止 grid[nr][nc] = EMPTY; break; } // 火焰覆盖写入独立层,链式炸弹入队而不是递归 } }逻辑说明:inBounds必须在查grid之前调用,短路求值保证不会先越界。参数说明:power是爆炸半径,dr/dc是四方向偏移;链式炸弹不要在这里直接递归explode,把坐标推进队列,下一帧统一处理。
6. 进阶玩法:把单机泡泡堂改成可调参的对战 Demo
单机版跑通后,最值得做的不是加更多贴图,而是把玩法参数抽出来,让炸弹人变成可调参的对战 Demo。我一般先做一张参数表,把炸弹威力、冷却、移速、敌人数量、道具掉率固定成配置,再用状态机管玩家和敌人。这样改玩法不用翻遍源码,录屏验证也有明确对照。
| 参数名 | 默认值 | 调大后的效果 | 风险 |
|---|---|---|---|
| bombPower | 2 | 爆炸范围更远 | 容易炸到自己,地图软墙消耗过快 |
| bombCooldown | 0.3s | 放弹间隔更短 | 输入连发会导致炸弹堆叠 |
| moveSpeed | 4 格/秒 | 走位更灵活 | 碰撞检测跟不上会穿墙 |
| enemyCount | 3 | 压力更大 | 寻路简单时敌人会挤在角落 |
| itemDropRate | 0.25 | 道具更多 | 随机数没播种时每局一样 |
状态机不用写复杂,玩家和敌人各一个枚举就够:IDLE、MOVE、PLACE_BOMB、HURT、DEAD。每帧先根据输入或 AI 决策切状态,再在update里按状态扣时间和改坐标。敌人 AI 可以先做“随机走 + 避开炸弹”的保守版,别一上来写 A*,地图小的时候随机走反而更容易调。
enum class ActorState { IDLE, MOVE, PLACE_BOMB, HURT, DEAD }; struct Actor { ActorState state = ActorState::IDLE; float stateTime = 0.0f; int hp = 1; }; void updateActor(Actor& a, float dt) { a.stateTime += dt; switch (a.state) { case ActorState::IDLE: // 有输入时切 MOVE,有放弹键时切 PLACE_BOMB break; case ActorState::PLACE_BOMB: if (a.stateTime > 0.15f) { // 放弹前摇,防止一帧连放 a.state = ActorState::IDLE; a.stateTime = 0.0f; } break; case ActorState::HURT: if (a.stateTime > 0.5f) a.state = ActorState::IDLE; break; default: break; } }逻辑说明:stateTime记录当前状态持续时间,PLACE_BOMB加前摇可以避免按键重复触发。参数说明:0.15f是放弹前摇,0.5f是受伤硬直,都可以放进配置表。验证时把dt固定成1/60,录屏逐帧看炸弹倒计时和爆炸帧是否对齐;如果只有实际运行时漂移,优先查dtCap和音频阻塞。
从那以后我每次改炸弹逻辑,都先把dt固定成1/60再录屏逐帧对一遍,确认倒计时、火焰持续和链式触发在同一帧序列里,再回到实时循环。希望帮到你。
本文还有配套的精品资源,点击获取