资讯动态

C/C++推箱子小游戏源码解析:从地图设计到游戏逻辑

发布时间:2026/9/28 8:03:40 来源:尧图企业网站定制
简介面向C语言/C初学者的推箱子小游戏完整项目资源以经典益智小游戏为载体帮助学习者掌握数组、循环、条件判断、碰撞检测等核心编程概念适合课程设计、自学实践或作为程序设计课教学案例。压缩包内共11个文件包含可执行exe程序、cpp主源文件、rar源码工程、jpg图片素材、wav背景音乐以及wmv讲解视频等整体约725.44MB既可直接运行体验游戏也能对照源码逐行理解地图存储、人物移动、箱子推送到胜负判定的完整实现。已有475人浏览/学习属于热度较高的练手项目。讲解视频会逐步演示从环境搭建、核心算法到排错思路配合本地调试能更快掌握常见写法和程序结构地图采用二维数组建模人物移动与箱子推挤均围绕键盘控制展开可在理解之后自行设计更多关卡。图片素材与背景音乐素材一应俱全方便二次开发或打包成完整课程项目。1. 推箱子小游戏C/C 课程设计里最值得拆的一份源码C 语言学到数组和循环之后大部分人都会进入一个尴尬期练习题刷了不少但离“写一个能跑起来的东西”还差一口气。推箱子是 C/C 小游戏里最适合拿来破局的题材玩法听着简单但地图存储、角色移动、碰撞判定、胜利检测这些游戏基本功全都能在几百行代码里讲清楚。这份资源给得很实在一个双击就能开的 exe、一份把核心逻辑都放在 main.cpp 里的完整工程、一个讲解视频外加全套图片和音乐素材。适合要做课程设计答辩的人也适合想从“刷题模式”切换到“做项目模式”的 C 语言初学者。我建议别急着编译先把 exe 玩通再顺着源码走一遍逻辑。2. 解压之后先别急着编译项目里每个文件是干什么的2.1 压缩包里的文件清单exe、贴图、音乐、视频和源码的分工下载下来是个 zip解压后东西不少初看容易懵。其实文件分成了四类各干各的活。第一类是编译好的成品boxman.exe这个是直接拿来玩的。第二类是游戏素材blank.jpg是空白地板wall.jpg是墙box.jpg是普通箱子redbox.jpg是推到目的地上的红色箱子people.jpg是玩家end.jpg是目的地music.wav是背景音乐。第三类是讲解视频Video_2018-05-24_234516.wmvwmv 格式Windows 自带的播放器就能打开视频里基本是把上面这些文件怎么配合、代码怎么编译讲一遍。第四类是源码boxman项目工程.rar真正的工程文件在里面外面单独放的main.cpp是为了让你不找源码也能直接看到代码。这里有个经典误区很多人解压后只看得到 exe 和一堆图片就开始在论坛里问“源码在哪”。源码在 rar 里rar 解开之后是一个完整的 Visual Studio 工程核心代码就一个main.cpp。我拿到压缩包的第一件事就是把 rar 解出来确认工程结构和图片素材是不是同级的。2.2 main.cpp 的代码骨架初始化、游戏循环、收尾三件事推箱子这种小游戏逻辑全在一个main.cpp里反而是优势。对于一个还没读过“百行以上工程”的人来说一个文件比十个文件友好得多。整个程序跑起来就干三件事初始化资源、进入游戏循环、退出时清理。#include graphics.h // EasyX 图形库负责窗口、贴图、按键 #include conio.h // _getch() 读取键盘输入 #include mmsystem.h // 播放 wav 音乐 #pragma comment(lib, winmm.lib) // 链接多媒体库mciSendString 靠它 int main() { // 1. 初始化创建窗口、加载贴图、播放音乐 initgraph(640, 512); // 窗口宽 640高 512 loadimage(imgWall, wall.jpg); mciSendString(open music.wav alias bgm, NULL, 0, NULL); // 2. 游戏循环读按键 - 更新地图 - 重绘画面 while (running) { int dir getDirectionKey(); movePlayer(dir); drawMap(); } // 3. 收尾关闭音乐、关闭窗口 mciSendString(close bgm, NULL, 0, NULL); closegraph(); return 0; }代码逻辑说明初始化阶段只做“创建窗口、加载素材、播放音乐”三件事游戏循环里反复执行“读输入、改数据、画画面”。这个套路几乎所有图形小游戏通用你以后写贪吃蛇、俄罗斯方块骨架也是长这样。参数说明initgraph(640, 512)里的两个数字是窗口宽和高单位是像素。为什么是 512因为地图是 8 行乘 8 列每格 64 像素8 × 64 512刚好正方形。留出来的 128 像素宽度后面可以显示步数、状态文字。mciSendString里的alias bgm是给音乐起的别名后面play、close都靠这个名字找到它。2.3 用 Visual Studio 打开老工程升级提示、工作目录、Debug 和 Release这份工程的年代明显偏早用新版 Visual Studio 打开时大概率会弹一个“需要升级”的提示。直接点确定就行升级完成后工程照样能编译。这里我多说一句别拿 VSCode 去折腾这个项目。VSCode 配置 C/C 环境本身不复杂但你要手动把 EasyX 的库目录、头文件目录、链接器参数全部配一遍对新手来说全是坑。Visual Studio 双击.sln就能开省下的是真实时间。工程打开后有一个概念必须先搞清楚工作目录。程序运行时loadimage(wall.jpg)是相对路径它去“当前工作目录”找文件。用 VS 调试运行时工作目录默认是工程文件所在目录所以素材和.vcxproj放一起就能正常显示。但如果你直接把 exe 拷走exe 所在的目录才是工作目录图片和音乐没跟着走就会闪退或黑屏。这个问题后面避坑章会详细展开。编译配置也建议看一眼Debug 和 Release 两个配置Debug 带着调试信息、体量大Release 是最终交付版。课程设计答辩时交 Release 更稳体积小而且不依赖调试运行库。我一般会在拿到工程后先切到 Release重新生成一次确保从零编译也能过而不是靠 VS 里的残留缓存跑通。3. 推箱子的核心逻辑一张地图就是一组数字3.1 地图编码表二维数组的每一格是什么推箱子最巧妙的地方是把一张地图变成了一串数字。C 语言数组天然适合表达这种网格结构map[y][x]一行代码就能定位到任意一格比用指针链表去组织地图直观得多。这份工程里地图的每个取值都对应一张贴图数字含义对应素材0空地blank.jpg1墙wall.jpg2目的地end.jpg3普通箱子box.jpg4玩家people.jpg5箱子已在目的地redbox.jpg6玩家站在目的地end.jpg 上叠加 people.jpg5和6这两个状态是整套编码的精髓。箱子推到目的地后不能只画一个箱子那样你不知道它“算不算到位”所以用 5 表示“红箱”玩家站到目的地后也不能把目的地抹掉否则箱子再推回来就没法识别了所以用 6 表示“人站在终点上”。我在很多课程设计里看到有人用额外一个boxOnTarget布尔数组去记这件事思路对但代码会变绕。直接用编码状态一个数组全搞定。const int ROWS 8, COLS 8; int map[ROWS][COLS] { {1,1,1,1,1,1,1,1}, {1,0,0,2,0,0,0,1}, {1,0,0,0,3,0,0,1}, {1,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,1}, {1,0,0,0,4,0,0,1}, {1,1,1,1,1,1,1,1} };这是一张能通关的 demo 关卡玩家在下方箱子在第二行靠右目的地在第一行中间。外圈全是墙这样移动时天然不会越界。这个细节很重要——我见过有人把地图边框做成 0结果玩家一路走出地图程序直接访问越界数组弹窗崩溃。单关卡写死数组没问题但你要是自己设计地图最外层务必保留一整圈墙。3.2 玩家移动判定先看空地再看能不能推箱子顺序不能反移动是整个游戏最复杂的部分。玩家按方向键后面前可能是空地、墙、箱子或者目的地。处理顺序有讲究先判断要不要动再判断动了之后原来那格恢复成什么。void movePlayer(int dx, int dy) { int px, py; findPlayer(px, py); // 遍历地图找到数字 4 或 6 所在的位置 int nx px dx, ny py dy; // 玩家要去的位置 int bx nx dx, by ny dy; // 箱子要被推到的位置 // 情况一前方是空地或目的地玩家直接走 if (map[ny][nx] 0 || map[ny][nx] 2) { if (map[py][px] 6) map[py][px] 2; // 原来站在目的地恢复成目的地 else map[py][px] 0; // 原来是空地恢复成空地 map[ny][nx] (map[ny][nx] 2) ? 6 : 4; } // 情况二前方是箱子检查箱子前方是否还能推 else if (map[ny][nx] 3 || map[ny][nx] 5) { if (map[by][bx] 0 || map[by][bx] 2) { map[by][bx] (map[by][bx] 2) ? 5 : 3; // 箱子被推走或推到目的地 map[ny][nx] (map[ny][nx] 5) ? 6 : 4; // 玩家站到箱子原来位置 if (map[py][px] 6) map[py][px] 2; else map[py][px] 0; } } // 情况三前方是墙或箱子推不动什么都不做 }代码逻辑说明三段分支对应三种情况——空地直接走箱子能推就连人带箱一起动墙或者箱子顶死就原地不动。第二种分支里必须“先动箱子再动玩家”因为玩家原来站的位置需要用到map[py][px]判断是不是 6站在目的地上的玩家。如果先把玩家位置清成 0这个信息就丢了箱子推到目的地后地板编码会错乱。参数说明dx、dy是方向增量上方向是(0, -1)右方向是(1, 0)下方向是(0, 1)左方向是(-1, 0)。调用时只需要传这四个组合之一。这一段代码是所有逻辑里最容易写错的地方我当时照着书敲的时候就在这翻过车少判断一个map[ny][nx] 5结果箱子在目的地时玩家推不动游戏直接卡死。3.3 胜利判定地图里还有没有“光杆箱子”什么时候算赢所有箱子都到了目的地。翻译成数组逻辑就是全图找不到数字 3。因为箱子一旦到位就会变成 5而不是继续保留 3。bool checkWin() { for (int i 0; i ROWS; i) { for (int j 0; j COLS; j) { if (map[i][j] 3) return false; } } return true; // 没有普通箱子了说明全部到位 }这段代码只有 10 行但它是整个游戏的“结束信号”。checkWin()放在每次movePlayer之后调用返回true就弹出胜利提示然后退出循环。判断条件的妙处在于不必关心箱子在哪个位置也不必记录什么“箱子总数”地图状态本身就包含了答案。这也是二维数组地图方案相对其他做法的优势逻辑和显示用的同一份数据不会出现“地图上明明有红箱判定却说没赢”的别扭情况。4. EasyX 图形与音效素材加载和渲染的细节4.1 loadimage 加载 jpg格式、路径、缩放三个隐藏前提这份工程用 EasyX 图形库。EasyX 本身不是 VS 自带的需要单独装这个放到避坑章讲。加载图片的代码长这样IMAGE imgWall, imgBox, imgEnd, imgBlank, imgPeople, imgRedBox; void loadResources() { loadimage(imgWall, wall.jpg, SIZE, SIZE, true); loadimage(imgBox, box.jpg, SIZE, SIZE, true); loadimage(imgEnd, end.jpg, SIZE, SIZE, true); loadimage(imgBlank, blank.jpg, SIZE, SIZE, true); loadimage(imgPeople, people.jpg, SIZE, SIZE, true); loadimage(imgRedBox, redbox.jpg, SIZE, SIZE, true); }参数说明loadimage第一个参数是图片对象地址第二个是文件名第三第四个参数是目标宽高第五个参数true表示平滑缩放。这里的SIZE和地图格子大小是同一个值。为什么要在加载时指定缩放因为素材原图尺寸很可能不是 64 × 64直接贴会出现格子间错位、露边。在加载阶段统一尺寸比在绘制阶段每个putimage都传一次缩放参数干净得多。路径问题这里提前提醒一句wall.jpg是相对路径程序的工作目录里没有这张图加载就会失败。用 VS 调试时工作目录是工程目录所以素材放在工程目录下就行直接双击 exe 时素材必须和 exe 在同一层目录。这就是后面要讲的“调试正常、双击闪退”经典翻车的根源。4.2 渲染循环先画地板和墙再画角色顺序反了会出现叠层穿帮绘制函数按地图数组把每格贴图画到窗口上。逻辑不复杂但图层顺序有讲究地基性质的元素先画动态角色后画。void drawMap() { for (int i 0; i ROWS; i) { for (int j 0; j COLS; j) { int x j * SIZE, y i * SIZE; switch (map[i][j]) { case 0: putimage(x, y, imgBlank); break; case 1: putimage(x, y, imgWall); break; case 2: putimage(x, y, imgEnd); break; case 3: putimage(x, y, imgBox); break; case 4: putimage(x, y, imgPeople); break; case 5: putimage(x, y, imgRedBox); break; case 6: putimage(x, y, imgEnd); // 先画目的地 putimage(x, y, imgPeople); // 再画站在上面的人 break; } } } }代码逻辑说明case 6要画两张图先画end.jpg再叠people.jpg这样能看到“人站在终点上”的效果。顺序反过来的话目的地被盖住视觉上就是个普通玩家站在空地上玩家一旦走开终点突然出现很出戏。case 5只画红箱一张就行因为它已经包含目的地信息了。坐标计算这里有个常量统一性问题。代码里所有x j * SIZE、y i * SIZE说明SIZE、ROWS、COLS、窗口大小四者是联动的。我习惯在工程开头就把它们写在一起const int SIZE 64; // 每格边长 64 像素 const int ROWS 8, COLS 8; // 地图 8 行 8 列如果地图尺寸改了只动这三个常量就行。把数字散落在代码各处改起来就是一场灾难。4.3 _getch 读方向键为什么方向键按了没反应控制台程序读键盘用_getch()但方向键在 Windows 下返回的不是一个字节而是两个字节第一个是前缀0xE0第二个才是真正的扫描码。很多照着教程手敲代码的人只读一次返回值直接把0xE0拿去和 72、80 比较结果永远匹配不上。int getDirectionKey() { int ch _getch(); if (ch 0xE0 || ch 0) { // 方向键或功能键的前缀 ch _getch(); switch (ch) { case 72: return 0; // 上 case 80: return 1; // 下 case 75: return 2; // 左 case 77: return 3; // 右 default: return -1; // 其他功能键忽略 } } if (ch w || ch W) return 0; if (ch s || ch S) return 1; if (ch a || ch A) return 2; if (ch d || ch D) return 3; return -1; }代码逻辑说明先读第一个字节如果是0xE0或者0说明后面还有一个扫描码再_getch()一次。72/80/75/77分别是上下左右的扫描码。这个写法同时支持方向键和 WASD方便不习惯方向键的人操作。0xE0和0两种前缀都要处理因为主键盘方向键返回0xE0小键盘方向键有时返回0。只判断一种某些键盘上就会失灵。参数说明这段代码里的数字是扫描码不是 ASCII 码。W的 ASCII 码是 87ascll 里H是 72但方向键“上”的扫描码也是 72两者含义不同别混在一起。这也是为什么有些人按W有效、按方向键没反应的玄学原因——只处理了 ASCII 分支没处理扩展键分支。5. 常见问题与避坑指南从编译到运行的一条龙排错5.1 编译报错无法打开 easyx.h 或 graphics.h现象工程一编译就报fatal error C1083: 无法打开包括文件: easyx.h或者graphics.h同样打不开。原因EasyX 不是 Visual Studio 自带的库必须单独下载安装。老教程默认你已经装好但新手几乎都会卡在这一步。这个和源码本身没关系纯粹是环境缺件。解决去 EasyX 官网下载安装包安装时选择你正在用的 VS 版本装完重启 VS 重新生成。另外如果双击 exe 提示缺少VCRUNTIME140.dll之类那是缺Microsoft Visual C Redistributable运行库去微软官网装最新版 x64 运行库即可跟源码无关属于运行环境问题。这里我再强调一次整个项目直接在 VS 里跑别去 VSCode 里配 C/C 环境VSCode 配 EasyX 要手动设置头文件路径、库路径、链接参数对新手来说一步错步步错。5.2 双击 exe 闪退或界面出来了但全是黑块白块现象用 VS 调试运行一切正常双击 exe 要么一闪就退要么贴图全是色块。原因闪退多半是图片加载失败程序里没有对loadimage返回值做检查素材缺失时直接崩溃。黑块是因为贴图资源没找到EasyX 绘制时输出的是默认底色。根本原因是工作目录变了调试运行时 VS 会把工作目录设成工程目录双击 exe 时工作目录是 exe 所在目录素材不跟着走就找不到。解决把整个资源包原样解压boxman.exe、所有 jpg、music.wav保持在同一层目录里。不要单独把 exe 拷出去也不要从几个不同压缩包凑素材。血泪经验我当年为了“干净”只拷 exe 出去结果所有关卡贴图全黑查了半天才发现是路径问题。5.3 方向键按了没反应但 WASD 正常现象游戏能跑按字母键能走按方向键完全没反应。原因_getch()读方向键返回两个字节代码只读了一次把前缀0xE0当成有效方向值和扫描码比对全部失败。这类代码在旧教程里出现频率很高很多是从 Turbo C 时代继承下来的写法对不上 Windows 的键盘输入模型。解决按第四章 4.3 节的双字节读取方案改。判断0xE0和0两种前缀再读第二字节匹配 72、80、75、77。改完之后方向键和 WASD 都能用兼容性最好。5.4 fopen、scanf 在 VS2019 及以后版本报 C4996现象编译时出现error C4996: fopen: This function or variable may be unsafe或者scanf同样报错。原因新版 Visual Studio 默认开启 SDL 安全检查把老的 C 运行时函数标记为不安全提示你用fopen_s、scanf_s替换。很多网上流传的老代码都是用fopen、scanf写的放到新编译器下就会触发。解决两条路。一是改代码把fopen改成fopen_sscanf改成scanf_s注意这两个函数参数个数不一样改写时容易顺手改错。二是不想动代码就在项目属性 → C/C → 预处理器 → 预处理器定义里加上_CRT_SECURE_NO_WARNINGS编译告警直接消失。课程设计我建议选第一条答辩时老师问起来你至少能说出“这是新版编译器更安全的检查机制”这句话。5.5 游戏能跑但背景音乐没声音现象画面、操作都正常就是从头到尾没有音乐。原因mciSendString打开音频文件失败。常见原因有四类music.wav不在工作目录、wav 文件不是标准 PCM 编码、winmm.lib没链接、别名bgm被占用或之前重复 open 了。压缩包里的 wav 一般没问题问题更多出在你自己替换音乐之后。解决先确认 wav 和 exe 同目录再确认文件是标准 PCM 格式。很多从网页下载的 wav 其实是压缩编码mciSendString播放不了用格式转换工具转成 PCM 即可。链接问题在代码开头加#pragma comment(lib, winmm.lib)这份工程本身已经加了如果你是从零新建工程抄代码别漏掉这一行。重复 open 的问题可以在 open 前先close bgm忽略错误继续执行。6. 进阶玩法加步数统计、多关卡和一步撤回跑通这份工程之后可以往三个方向做小改造。第一个是步数统计声明一个int steps 0在movePlayer成功移动玩家之后steps再在窗口右侧用outtextxy绘制出来。地图是 8 × 8 格每格 64 像素占 512 像素宽窗口开 640 宽的话右边正好留了 128 像素放文字这个布局在 initgraph 那里就预留好了。第二个是加多关卡。把二维数组改成三维map[MAX_LEVEL][ROWS][COLS]再定义int level 0。切换关卡时把map[level]拷贝到当前工作数组或者直接让所有逻辑函数都能接收关卡索引。改动量不大但效果很唬人答辩时从“一关”变“十关”观感完全不同。第三个是撤回一步。这个最实用也最考验数据结构。我常用做法是维护一个地图历史栈数组history[100][ROWS][COLS]每次移动前memcpy(history[stepCount], map, sizeof(map))把当前状态压栈。撤回时memcpy(map, history[stepCount - 1], sizeof(map))恢复上一帧。// 移动前保存现场 if (stepCount 100) { memcpy(history[stepCount], map, sizeof(map)); stepCount; } // 按 R 键撤回一步 if (key r stepCount 0) { stepCount--; memcpy(map, history[stepCount], sizeof(map)); }这段代码的逻辑是历史记录只允许回退 100 步超过就忽略新记录撤回只在stepCount 0时生效。memcpy整块拷贝地图比逐格循环赋值快得多也省得写两层 for。注意撤回之后步数也要跟着减不然统计对不上。我拿到这个资源时第一件事不是读代码而是先双击 exe 玩了两把把“推箱子在目的地变红”这个手感记住再回头看main.cpp。后来给学弟推荐项目我都要求他们按同样的顺序走一遍先跑通、再拆代码、最后改一个自己设计的关卡。从那以后每次拿到陌生工程我都强制先跑 exe 再看源码这个习惯帮我省掉的排查时间比任何技巧都值钱。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑