ARTICLE DETAIL

资讯详情

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

C语言超级玛丽游戏源码解析:游戏循环、碰撞检测与编程实战

C语言超级玛丽游戏源码解析:游戏循环、碰撞检测与编程实战 简介这是一份基于C语言实现的经典超级玛丽游戏完整源码包适合对游戏开发感兴趣的C语言初学者和中级开发者用于学习早期游戏编程逻辑。源码覆盖游戏循环、角色控制、碰撞检测、图形渲染与音效播放等核心模块配有14个mp3音效、6个bmp图片素材以及工程解决方案、源代码和资源数据库等共33个文件整体压缩包约7.42MB结构清晰便于按模块研读。目前已有118人学习下载是分析经典游戏实现细节、提升程序结构设计能力的实用参考资料。通过阅读和修改源码能够直观理解游戏从想法到完整项目的落地流程从游戏循环到渲染细节追踪完整代码实现并在实际调试中获得宝贵的排错与扩展经验。1. C语言版的超级玛丽游戏源码先看清这份包里到底有什么C语言实现的超级玛丽游戏源码解压后你会看到.sln和.suo这样的Visual Studio工程文件说明这是一套能直接编译的Windows游戏工程不是零散的示例片段。这份资源的真正价值不在“能玩”而在它把游戏循环、角色控制、碰撞检测、图形绘制、音效播放这五件事用C语言完整串了一遍。适合刚学完指针、结构体、数组正想找一个能改着玩的C语言实战项目的学生也适合想快速回顾老式Windows编程思路的从业者。我按“先跑通、再拆逻辑、最后改造”的顺序把你会踩的坑一起说清楚。2. 拿到包先别急着运行工程结构与从sln构建的完整流程2.1 解压后的文件清单哪些是代码哪些是缓存这个压缩包的核心文件是SuperMushrooms.sln和SuperMushrooms.suo前者是Visual Studio的解决方案文件后者是用户选项文件。很多人第一次打开时会被suo迷惑以为它是工程的一部分其实suo记录的是你上次打开时的窗口状态、断点位置这类本地信息删掉也不影响编译。真正决定代码行为的是.c源文件和.h头文件游戏逻辑基本都集中在那几个C文件里有的版本甚至只有一个main.c。我拆这类老工程的习惯是先看sln里引用了哪些项目再看项目里有没有资源目录。超级玛丽需要图片和音效常见的目录约定是res、resource或者assets用来放.BMP位图和.WAV音频。拿到包后先把文件清单在脑子里过一遍能避免后面反复调整路径。文件作用是否需要提交.sln解决方案入口记录工程依赖需要.suo用户本地配置可自动生成不需要.c / .h游戏逻辑实现与声明需要.bmp / .wav图形与音效资源需要2.2 有IDE和没有IDE的两种启动路径如果你装了Visual Studio直接双击.sln打开把解决方案平台切到x86按CtrlF5编译运行即可。老工程第一次加载时VS可能提示“需要升级工程”一般选不升级也能正常打开升级反而可能引入新错误。如果你用的是VS2022这类新版本遇到“找不到Windows SDK”的提示去Visual Studio Installer里勾选“使用C的桌面开发”工作负载就能解决。没有IDE也不用卡在这MinGW-w64加上一条gcc命令就能编译。我按这版源码的思路给出一个通用构建命令gcc -o supermario main.c game.c graphics.c -lwinmm -lgdi32 -mwindows这条命令里main.c、game.c、graphics.c对应不同的功能模块如果你解压后只有一个main.c就把后面的源文件删掉只保留一个。-lwinmm是Windows多媒体库PlaySound音效函数依赖它-lgdi32是图形设备接口库BitBlt贴图依赖它-mwindows告诉链接器生成窗口程序而不是控制台程序不加这个参数运行时会多弹出一个黑框。2.3 第一件要确认的事工作目录和资源路径我在拆老代码时最常翻车的不是编译而是启动后图片音效全部加载失败。现象是程序能跑、窗口能弹但角色是白块、没有声音。根因往往只有一个当前工作目录不是exe所在目录。从Visual Studio里按F5启动时进程的工作目录默认是.vcxproj所在目录而双击exe启动时工作目录才是exe所在目录。如果代码里用相对路径res\mario.bmp两种启动方式的行为就不一致。我一般会先写一段取绝对路径的逻辑// 用GetModuleFileName获取exe所在目录再拼上资源路径 char path[MAX_PATH]; char fullPath[MAX_PATH]; GetModuleFileName(NULL, path, MAX_PATH); // 找到最后一个反斜杠把文件名截掉只保留目录部分 char* slash strrchr(path, \\); if (slash) *(slash 1) \0; // 拼上相对资源目录 sprintf(fullPath, %sres\\mario.bmp, path);这段代码的作用是无论从IDE启动还是双击exe启动程序都从exe自身的位置去找资源不再受工作目录影响。GetModuleFileName拿到的是当前进程的完整路径strrchr找到最后一个分隔符并截断最后用sprintf拼接资源路径。处理音频时同样操作只是把文件名后缀换成.wav。3. 核心逻辑拆解游戏循环、角色控制与碰撞检测的实现参数3.1 游戏循环的三段式结构任何动作游戏都离不开游戏循环超级玛丽也不例外。它的循环大体可以拆成“输入处理、状态更新、画面渲染”三件事。C语言写法通常很直白while (gameRunning) { // 1. 读取键盘状态转换为角色的移动意图 handleInput(); // 2. 更新角色位置、动画帧、碰撞结果 updateGame(16); // 3. 把最新画面绘制到窗口 renderGame(); // 4. 简单帧率控制让循环约每秒60次 Sleep(16); }这个循环的顺序是有讲究的先处理输入再更新逻辑最后绘制这样画出来的永远是“当前帧”的状态。如果把renderGame放到handleInput前面画面会滞后一整帧操作手感会明显发闷。Sleep(16)是早期工程里常用的帧率控制办法取“1000毫秒除以60帧约等于16毫秒”。更精确的做法是用timeGetTime计算两帧之间的真实时间差再做时间步长缩放但许多老工程为了简单直接写死固定步长。参数16的意思是一帧间隔约16ms如果你把Sleep改成0游戏会以几百帧的速度狂跑角色速度快到没法玩这就是为什么帧率控制是游戏循环里不能省的一步。3.2 角色控制按键映射与手感相关参数角色控制的核心是把键盘按键翻译成速度再把速度累加到坐标上。这个过程中有三组参数决定了手感重力加速度、跳跃初速度、水平移动速度。用C语言实现时通常是这样#define GRAVITY 0.5f #define JUMP_VEL -12.0f #define MOVE_SPEED 3.0f void updatePlayer(Player* p, bool left, bool right, bool jump) { // 先根据按键决定水平速度 p-vx 0; if (left) p-vx -MOVE_SPEED; if (right) p-vx MOVE_SPEED; // 重力每帧作用一次让角色加速下落 p-vy GRAVITY; // 只有站在地面时才能起跳防止空中连跳 if (jump p-onGround) { p-vy JUMP_VEL; p-onGround false; } // 最后用速度更新位置 p-x p-vx; p-y p-vy; }这段代码里有几个细节值得较真。坐标是float类型速度单位是“像素/帧”不是“像素/秒”。GRAVITY取值0.5看起来很小但因为每帧都会累加一秒60帧下来累计量很大角色下落会越来越快。JUMP_VEL取-12.0f表示向上跳的初速度负号是因为屏幕坐标系里y轴向下负值才是向上。版本里这组参数直接决定手感一次跳多高、下落多快全看这三个常量。调参时只改一处不够比如加大JUMP_VEL后跳得更高但滞空时间会更长这时候要同时调GRAVITY才能保持跳跃节奏。典型做法是JUMP_VEL的绝对值大约是GRAVITY的20到30倍起跳后的峰值高度才比较合理。3.3 碰撞检测AABB矩形相交与位置修正平台跳跃游戏的碰撞检测用不到复杂算法直观做法是判断两个矩形是否相交。每个角色、砖块、金币都可以抽象成一个矩形矩形相交即发生碰撞。C语言写这个判定函数很简洁bool checkCollision(Rect a, Rect b) { // 两个矩形相交的条件相互之间没有完全错开 return a.x b.x b.w a.x a.w b.x a.y b.y b.h a.y a.h b.y; }这个函数返回true表示两个矩形有重叠面积。它假设Rect结构体里包含x、y、w、h四个字段分别表示左上角坐标和宽高。为什么用矩形而不是逐像素检测因为平台跳跃游戏里碰撞对象大多是砖块、地面这类规则形状矩形检测速度快、逻辑直观对初学者来说更容易验证正确性。真正的坑在碰撞发生后的处理。拿到相交结果后不能直接把角色速度反向而是要把角色的坐标按最小穿透方向“推”出障碍物。比如角色从上方落到地面上检测到与地面碰撞后应该把角色的y坐标修正到地面的上边缘同时把vy归零、onGround置为true。只反弹不修正坐标角色会陷在砖块里反复抖动这是新手改造时最常出现的问题。3.4 用结构体数组保存关卡数据关卡地图本质是一张二维表。老派做法是把地图存成字符串数组每个字符代表一种物件#是砖块*是金币M是玛丽的出生点。C语言里常用结构体包装关卡信息typedef struct { int width; // 关卡横向格数 int height; // 关卡纵向行数 char map[20][40]; // 地图数据一个字符一个格子 } Level; // 两份关卡数据 Level levels[2] { { 20, 12, { ####################, #........*.........#, #......#####.......#, #.....*.....#....*.#, #....##....###.....#, #..................#, #........M.........#, #################### }}, // 第二关结构类似只列地图即可 };这样做的妙处在于游戏逻辑不关心具体关卡内容只遍历map数组里的每个字符按规则生成对应物件。#生成静态砖块*生成可收集的金币M在初始化时写入角色出生坐标。想加新关卡只需要在levels数组里追加一个结构体不用改任何逻辑代码。4. 图形与音效老式Windows程序怎么画出玛丽4.1 贴图位图加载与透明色处理这版源码运行在Windows图形环境下画图的核心API是BitBlt它的作用是把一块内存图像复制到窗口上。超级玛丽的角色、敌人、道具通常都打包在一张精灵图里通过指定源区域坐标来截取单帧。HDC hdc GetDC(hwnd); // 把精灵图第frameIndex帧复制到窗口的(x, y)位置 BitBlt(hdc, x, y, frameW, frameH, memDC, frameW * frameIndex, 0, SRCCOPY); ReleaseDC(hwnd, hdc);参数说明hdc是窗口的设备上下文可以理解为一个画布句柄(x, y)是角色在屏幕上绘制的左上角坐标frameW和frameH是单帧宽高frameW * frameIndex是精灵图中当前帧的横向偏移。整套逻辑的前提是精灵图里的每一帧宽度完全相同排成一行。透明色处理是另一个容易踩的点。精灵图大多用纯色背景比如洋红色RGB(255, 0, 255)。直接BitBlt会把背景色也画上去角色周围出现一块方方正正的色块。处理办法是换成TransparentBlt它支持指定颜色键遇到洋红色像素就不复制。代价是效率略低但一个角色几十个像素的复制量完全不是问题。4.2 音效播放PlaySound的同步陷阱音效是游戏沉浸感的重要来源。C语言在Windows下播放WAV最直接的手段是PlaySound用法很简单但有一个容易忽视的陷阱// 错误示范漏掉SND_ASYNC会让程序卡住直到音频播完 // PlaySound(res/jump.wav, NULL, SND_FILENAME); // 正确做法异步播放立即返回 PlaySound(res/jump.wav, NULL, SND_FILENAME | SND_ASYNC); // 背景音乐用循环播放 PlaySound(res/bgm.wav, NULL, SND_FILENAME | SND_ASYNC | SND_LOOP);SND_FILENAME表示第一个参数是文件名SND_ASYNC表示异步播放函数立即返回游戏循环继续跑SND_LOOP表示循环播放必须和SND_ASYNC一起用否则音乐会卡死主线程。掉进这坑的典型表现是跳跃后游戏卡顿0.5秒然后角色才弹起来。原因就是漏写了SND_ASYNCPlaySound在同步等待整个跳跃音效播完才返回游戏循环整个被阻塞。老式工程很少有混音概念一般只保证“跳一下响一声”如果同时想播背景音乐和跳跃音效需要考虑双PlaySound或引入DirectSound那不是这份源码的重点。4.3 动画帧切换与节奏控制玛丽的行走动画靠逐帧切换实现。切换节奏不能写到每次循环都换一帧那会闪成残影通常用“每N帧换一张图”的方式来限速static int tick 0; tick; // 每8帧切换一次角色帧0、1、2三帧循环 if (tick % 8 0) { frame (frame 1) % 3; } // 站立时固定第一帧不动不换图 if (!moving) { frame 0; tick 0; }这里的frame决定BitBlt复制精灵图的哪一帧。8帧循环意味着约0.13秒换一幅动作视觉上接近正常跑步节奏。如果你觉得动画太快把8改成12觉得太慢改成4。注意这个数字同时受帧率影响——Sleep(16)保证了帧率稳定所以“每8帧”才是一个可靠的时间度量。这套用计数器代替真实时间的做法在老游戏里非常常见优点是不用处理时间单位换算缺点是帧率不稳时动画速度也会漂移。5. 从编译到运行的真坑黑屏、无法解析符号与碰撞穿透排查5.1 现象一编译报“无法解析的外部符号”现象链接阶段报错提示类似unresolved external symbol PlaySoundA referenced in function Xxx。原因调用了PlaySound、BitBlt这类Windows API但忘了链接对应的库。PlaySound在winmm.lib里BitBlt在gdi32.lib里。IDE工程通常在项目设置里手动配置过命令行编译时没人帮你加于是链接器找不到函数实现。解决命令行编译时补上-lwinmm -lgdi32或在C代码顶部加pragma指令#pragma comment(lib, winmm.lib) #pragma comment(lib, gdi32.lib)这个坑的隐蔽之处在于编译阶段一切正常出错在链接阶段新手容易误认为是代码写错。先看错误信息里有没有unresolved external symbol有就是缺库跟逻辑无关。5.2 现象二窗口弹出但全黑画面不刷新现象程序启动后弹出一个窗口里面一片黑角色和地图都不显示。原因绘制函数没有被持续调用。Windows窗口程序有两种消息处理方式一种是等系统发WM_PAINT消息再画另一种是在主循环里主动画。如果代码里把绘制写在WM_PAINT分支里而主循环用的是GetMessage阻塞式取消息窗口就一直停在初始状态不刷新。解决主循环里改用PeekMessage配合主动绘制MSG msg; while (running) { // 非阻塞式取消息有消息才处理没有就继续渲染 while (PeekMessage(msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message WM_QUIT) { running false; } TranslateMessage(msg); DispatchMessage(msg); } // 消息处理完后立即更新和绘制 updateGame(16); renderGame(); Sleep(16); }GetMessage和PeekMessage的关键差别是GetMessage在无消息时挂起线程程序停在那一行PeekMessage无消息时立即返回主循环能继续执行绘制。游戏循环必须用PeekMessage这是很多C语言游戏源码跑起来黑屏的根因。5.3 现象三下落太快时玛丽直接穿过地面现象角色从高处落下时偶尔会穿到砖块下面或者直接掉出地图。原因碰撞检测用的是矩形相交判断但位置更新是一次性加上整帧位移。如果这一帧角色下落了20像素而砖块厚度只有16像素两个矩形在这一帧里没有重叠检测就漏掉了。这是老式固定步长更新的一个经典缺陷。解决限制最大下落速度把速度加到坐标前先做一次截断if (p-vy MAX_FALL_SPEED) { p-vy MAX_FALL_SPEED; }MAX_FALL_SPEED一般取砖块厚度的一半以下比如砖块高16像素速度上限就设8。这样每帧最大位移不超过砖块厚度矩形检测永远不会完全错过。另一个相对稳妥的办法是把一次位移拆成两步先移动y方向检测一次再移动x方向检测一次但代码量明显增加老工程通常直接限速。5.4 现象四图片音效加载不出来程序却正常运行现象编译运行都成功窗口正常但角色显示为空白框背景音乐消失。原因工作目录和资源目录对不上。从Visual Studio启动时当前工作目录通常是工程文件所在目录不是exe所在目录。代码里写res\mario.bmp但res目录在Debug输出文件夹里路径匹配失败。解决复制res目录到exe输出目录或用上一章写过的GetModuleFileName拼绝对路径。两种办法里我更推荐后者一劳永逸。IDE里的临时设置比如把工作目录改成$(SolutionDir)只对当前机器有效工程拷给别人或者换台电脑又得重新配。6. 把它变成自己的练习项目加分数、金币与换关卡6.1 给代码加一个简单的得分系统读懂源码后第一步改造建议从“得分系统”入手。它改动小、反馈直观能立刻看到效果。在角色与金币碰撞的位置加一段判断// 当前站立的位置是金币 if (tileType COIN) { score 100; // 每枚金币固定100分 tileType EMPTY; // 清掉金币防止二次得分 }这里有一个容易被忽略的细节金币消失之后地图数据也要同步更新。如果不把tileType改成EMPTY角色每次经过同一位置都会触发加分分数刷爆。所谓“得分系统”表面是加数字实质是把游戏状态持久化到关卡数据里。6.2 把关卡数据抽到文本文件里超级玛丽的关卡如果全部写在数组里加一行新关卡就要改代码重新编译。更合理的是把地图存成文本文件启动时用fgets逐行读入FILE* fp fopen(level1.txt, r); // 一行一行读入每行对应地图的一行 char line[64]; int row 0; while (fgets(line, sizeof(line), fp) row MAP_HEIGHT) { // 去掉末尾换行符 line[strcspn(line, \n)] \0; // 把这一行字符复制到地图数组 strcpy(level.map[row], line); row; } fclose(fp);这里用fgets而不是fscanf是因为fscanf读到空格或换行就会停无法处理“一行地图里有砖块有空地”的格式。fgets每次读取一整行正好映射成一整行地图数据。strcspn找出换行符位置并替换成字符串结束符这一步不做的话地图每行末尾会残留一个换行符显示时出现奇怪的错位。6.3 验证改造效果的一步加一条调试日志改完代码后不要急着看画面效果先确认逻辑没跑偏。我的习惯是把关键状态写到文本日志里比如角色坐标、当前帧号、分数值// 调试用把关键状态输出到日志文件 static FILE* logfp NULL; if (!logfp) logfp fopen(debug.log, w); fprintf(logfp, x%.1f y%.1f score%d frame%d\n, p-x, p-y, score, frame);用文本输出做验证的优势是画面显示可能因为渲染问题掩盖真实状态但坐标和分数不会说谎。比如角色卡墙画面上看起来是贴图重叠日志里却能清楚看到x坐标没有变化而y值在持续叠加。靠这份日志能快速区分“逻辑错了”和“渲染错了”省下大量反复运行调试的时间。从那以后我每次拿到老的C语言游戏工程都会先确认资源路径、链接库、工作目录这三件事再开始改逻辑。这套习惯让我少踩了很多次重复的坑希望帮到你。本文还有配套的精品资源点击获取
返回列表