拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

C++ Qt框架跑酷小游戏课程设计:从工程结构到高分实现

C++ Qt框架跑酷小游戏课程设计:从工程结构到高分实现 简介一份基于C与Qt框架的跑酷小游戏源码属于高分课程设计项目适合计算机专业正在做期末大作业或课程设计的学生也适合想上手Qt开发的初学者。项目经导师指导并获评审99分代码完整可直接运行即使是新手也能按工程结构顺利跑通。压缩包共72个文件大小3.46MB包含53张PNG角色与场景素材、7个C源文件、6个头文件、2个UI界面文件以及pro工程配置和README说明目录按场景、角色、飞镖等模块划分方便阅读和二次开发。已有94人浏览学习。源码覆盖游戏主循环、角色控制、碰撞检测、攻击动画等关键逻辑素材目录里还有多组角色动作帧能清晰理解QPainter绘图、信号槽、定时器与事件处理等Qt核心机制。将这套源码作为模板既有助于完成高质量的课程作业也能在此基础上扩充新玩法、加深对客户端界面开发的理解。1. 一门“C基于Qt框架的跑酷小游戏”课程大作业到底能拿来讲什么每年到了课程设计季总有人抱着“C基于Qt框架的跑酷小游戏源码高分课程大作业”这个标题找参考。它听起来是个游戏但落到课程设计场景里它真正的价值不是“能跑”或者“画面上有人物在跳”而是把C的类封装、Qt的信号槽机制、事件循环、绘图刷新这几块硬骨头一次串起来。很多同学拿高分不是因为游戏多好玩而是因为工程结构干净、代码注释清楚、答辩时能讲明白每一个类为什么存在。这篇文章就是照着这个目标写的用一套最常见的Qt Widgets方案把跑酷游戏的骨架搭起来从工程目录到碰撞检测到存档给出能直接抄的代码和参数也把课程作业里最容易翻车的几个坑提前摆出来。适合C已经会写类、但没正经做过带界面项目的人目标是让你一周内交出一个讲得出设计思路、经得起追问的大作业。2. 技术选型与工程结构先决定用Widgets还是QML再动手写类2.1 为什么课程作业首选Qt Widgets而不是QML跑酷游戏用Qt做眼前有两条路QML配JavaScript或Widgets配C。不少教程推荐QML因为写界面动画快但我带过的课程设计里用QML翻车的比例反而高。原因很现实QML的动画系统虽然漂亮但课程答辩时老师会问“你JS里怎么管理游戏状态”“你的碰撞检测写在QML还是C里”这两问能答利索的学生不多。Widgets方案里游戏逻辑全部是C代码类关系图就是天然的答辩素材。另一个理由是调试习惯。大二大三的学生刚摆脱纯黑窗口对断点调试的依赖还很重。Widgets方案在Qt Creator里打断点、查变量、看调用栈和平时写控制台程序几乎没有差别。QML的调试器虽然也能用但动态语言那一套对没接触过前端的学生来说是额外一层心智负担。课程设计的时间本来就紧没必要把学习成本花在工具链上。还有评分角度。多数课程设计的评分标准里“代码量”和“注释量”是硬指标。Widgets方案下一个跑酷游戏拆出玩家类、障碍物类、游戏控制类再加上界面类代码量轻松上千行注释也自然分布在每个类里。QML方案界面部分代码量偏少老师一眼能看到的工作量就打折扣。所以只要不是老师明确要求QML我都建议用Widgets。2.2 工程目录把游戏逻辑和界面拆开后期改需求才不痛苦课程作业常见的错误是一个mainwindow.cpp里塞进所有东西绘图在MainWindow里碰撞检测在MainWindow里计时器也在MainWindow里。这样做短平快但一旦要加“按难度递增生成障碍物”或者“记录历史最高分”MainWindow就变成了一坨没人愿意动的代码。比较好的拆法是按职责分四个类加一个界面类。RunnerGame/ ├── RunnerGame.pro ├── main.cpp ├── game/ │ ├── player.h // 玩家坐标、速度、跳跃状态 │ ├── player.cpp │ ├── obstacle.h // 障碍物类型、坐标、移动速度 │ ├── obstacle.cpp │ └── gameengine.h // 游戏核心状态、难度、碰撞判定 │ └── gameengine.cpp └── ui/ ├── mainwindow.h // 界面绘图、按键事件、定时器 ├── mainwindow.cpp └── mainwindow.ui // 也可不用ui文件纯代码布局这个结构里最关键的一条规则gameengine不知道MainWindow的存在MainWindow知道gameengine。换句话说游戏逻辑层只负责“计算”界面层负责“显示”。比如跑酷里角色跳跃MainWindow调用gameengine-playerJump()gameengine算好新坐标后发出信号playerMoved(int x, int y)MainWindow收到信号才调用update()重绘。这样即使以后把界面从Widgets换成别的游戏逻辑一行不用改。main.cpp保持最简单只创建MainWindow对象并显示。有些同学喜欢在main.cpp里塞全局变量做游戏状态那是控制台编程的习惯在Qt工程里不推荐。全局变量一旦多起来信号槽跨类传参的优势就发挥不出来调试时也更难定位是哪个类改坏了状态。3. 用QTimer把游戏主循环跑起来定时器间隔和移动步长怎么配3.1 QTimer驱动下的“帧”概念16ms、30ms还是50ms跑酷游戏本质上是一个不断重复的循环移动角色、移动障碍物、检测碰撞、刷新画面。Qt没提供游戏引擎那种内置的game loop但QTimer完全可以胜任关键是把定时器间隔看成“帧率”来理解。跑酷游戏的逻辑不复杂帧率不需要做到60fps30fps约33ms一帧在课程作业里就已经很流畅实际我开始调参数时用的是50ms开发阶段更容易看出移动轨迹是否合理。// gameengine.cpp 中的初始化片段 const int kFrameIntervalMs 33; // 约30帧每秒跑酷游戏隔行扫描没问题 gameTimer new QTimer(this); connect(gameTimer, QTimer::timeout, this, GameEngine::onTimeout); gameTimer-start(kFrameIntervalMs);定时器间隔一旦定下来移动步长就要跟着帧率换算不要两套单位混用。比如障碍物横移速度设计成每秒钟300像素那么每帧移动量是300除以30帧等于10像素/帧。直接在onTimeout里写“障碍物x坐标减10”是当时的直观写法但如果后来把定时器从33ms改成16ms这10像素不变游戏速度直接变快一倍。所以更稳的写法是把“每帧移动量”做成一个变量定时器参数变化时只改一处。提示QTimer不是精确的实时定时器系统忙时触发间隔会偏长。课程作业场景下不用纠结这个但答辩时如果老师问“定时器准不准”能说出“它依赖事件循环不是硬实时”这句话就已经是加分项。3.2 信号槽在跑酷里的三种实际用法信号槽是Qt区别于其他C框架的标志跑酷游戏里至少有三种场景必须用它。第一种是按键到动作的传递MainWindow捕获QKeyEvent后发出信号gameengine的槽函数决定角色跳不跳第二种是碰撞事件gameengine检测到碰撞后发出gameOver信号MainWindow收到后暂停定时器并弹出结算框第三种是分数变化每越过一个障碍物gameengine发出scoreChanged(int)信号界面上的QLabel显示新分数。// MainWindow 构造函数里的连接片段 connect(m_gameEngine, GameEngine::scoreChanged, this, MainWindow::onScoreChanged); connect(m_gameEngine, GameEngine::gameOver, this, MainWindow::onGameOver);这里需要注意信号和槽的参数类型要严格匹配int就是int不要图方便传QString再转。曾经有人把scoreChanged(int)的槽写成onScoreChanged(QString)编译期不报错运行期connect失败的警告输出到控制台界面分数纹丝不动排查了半天才反应过来类型对不上。课程作业里如果出现“信号发了但槽没执行”的情况第一反应就是去应用输出面板看有没有“QObject::connect”相关的警告。第三种信号槽用法是状态同步。比如角色跳跃后离地高度变了gameengine里的坐标更新了但界面还不知道。这时候不要用全局变量去告知在跳跃函数末尾发一个playerMoved(int x, int y)信号MainWindow在对应槽函数里拿到坐标后调用update()。这样一来界面只在游戏状态真正变化时才重绘而不是靠定时器盲目刷。很多同学会在这三种场景之外过度使用信号槽连“玩家跳到最高点开始下落”这种纯内部状态也用信号通知这就没必要了信号槽是跨类的同一个类内部的逻辑尽量用普通函数调用。4. 障碍物生成、跳跃物理与碰撞检测玩法核心的三个实现细节4.1 障碍物生成随机延时 类型枚举别用固定数组硬编障碍物的生成策略直接影响游戏可玩性。最简单的是预置一条固定障碍序列比如第3秒出一个小障碍第5秒出一个高障碍循环播放。这种方式开发阶段好调试但玩两遍就腻了答辩时也讲不出什么设计点。常见做法是生成时用随机数决定障碍种类和出现的间隔同时对连续出现同一个障碍的情况做限制避免玩家碰到“一连三个高障碍”这种无解局面。// gameengine.cpp 中生成下一个障碍物的逻辑 void GameEngine::spawnObstacle() { if (m_sinceLastSpawn kMinSpawnIntervalMs) { return; } // 生成间隔留出跳跃落地的时间避免玩家无解 m_sinceLastSpawn 0; int randValue QRandomGenerator::global()-bounded(100); ObstacleType type; if (randValue 50) { type ObstacleType::LowBarrier; // 可跳过的矮障碍 } else if (randValue 85) { type ObstacleType::HighBarrier; // 必须蹲下的高障碍 } else { type ObstacleType::WideBarrier; // 占两个坐标格的宽障碍 } Obstacle obs(type, kSceneWidth); m_obstacles.append(obs); }生成时间间隔建议范围在800ms到1500ms之间具体数值要根据障碍物横向移动速度调整。原则是保证从障碍出现到它到达玩家位置的时间大于玩家完成一次跳跃所需的总时间。比如跳跃从起跳到落地大约需要600ms那最短生成间隔就设在700ms以上留100ms容错。这个参数在答辩时最好能主动说出来因为它证明你理解了游戏难度和可玩性之间的关系。4.2 C随机数在Qt里的正确打开方式从rand到QRandomGenerator热词里不少人还在搜“c随机数”这里必须强调一个课程作业里常见的坑直接用rand()配合time(NULL)做种子在Qt事件循环里不一定每次启动都不同。更麻烦的是rand()的随机质量一般生成出来的障碍序列容易出现“连着好几个同一类型”的情况。Qt从5.10开始推荐QRandomGenerator它自带全局实例不需要手动管理种子线程安全方面也处理好了。// 用 QRandomGenerator 取一个 0~99 的随机数 int randomValue QRandomGenerator::global()-bounded(100); // 取两个有符号整数之间的随机数 int offset QRandomGenerator::global()-bounded(-50, 51);使用bounded(int)时注意它返回0到传入值减1之间的整数比如bounded(100)返回0~99恰好可以用来做百分比判断。如果之前用了std::mt19937配合uniform_int_distribution也不是不行但要多写一个随机器对象和一个分布对象代码量就上去了。课程作业的评判标准里没有“必须用某个随机数算法”这一条用Qt自带的东西反而能强调你用了框架特性正好呼应标题里“基于Qt框架”这层意思。4.3 碰撞检测矩形相交是底线给玩家留一点容错跑酷游戏的碰撞检测最直接的做法是拿玩家矩形和障碍物矩形的QRect求intersects。这个方案能跑但实际玩起来手感偏“严苛”玩家刚起跳那几帧视觉上明明擦着障碍物顶部过去了判定却是撞上了。这是因为绘制出来的角色有圆角和阴影QRect只能表达一个规则矩形绘制效果和碰撞盒不一致是很正常的。常见做法是把碰撞矩形缩一圈像玩家原本是80×90的绘制区域碰撞盒只取中间60×70的部分// gameengine.cpp 中的碰撞检测 bool GameEngine::checkCollision(const Obstacle obs, const Player player) { // 故意留出 10 像素左右的视觉容错否则玩家会觉得“明明躲开了” QRect playerHitBox player.boundingRect().adjusted(5, 10, -5, -5); QRect obstacleHitBox obs.boundingRect().adjusted(5, 5, -5, 5); return playerHitBox.intersects(obstacleHitBox); }adjusted(5, 10, -5, -5)的含义是左边界向内收5上边界向下收10右边界向内收5下边界向上收5。这样碰撞盒比实际绘制区域小一圈游戏体验更宽容。数值别调太大否则会出现“人明明没碰到障碍物却死了”的反向翻车。这里有一个所有初做游戏的人都会踩的误区只看玩家矩形整体是否相交忘了区分“玩家是从左边撞上来的”还是“从上面落到障碍物上的”。如果是高空下落砸到障碍物顶部应该判为死亡但如果只是平行擦过碰撞盒缩小后就不该死。这个差异在答辩时可以用一两句话说清楚显得你考虑过“判定”和“手感”的关系。跳跃的物理参数同样需要调。常见做法是设定竖直初速度和一个向下重力加速度每帧更新速度再更新坐标。给一组能直接用的值初速度520像素/秒重力加速度1500像素/秒²在30fps下跳跃高度大约90像素滞空时间约700ms。这组参数配合900像素宽的跑道、300像素/秒的障碍物速度玩起来是“需要一点反应但不至于手忙脚乱”的难度。5. 课程设计最容易踩进去的六个坑乱码、计时器、内存、存档逐个排查5.1 中文乱码源码编码和MSVC编译器的永久恩怨现象界面按钮、QMessageBox里打好的中文编译能过运行显示一堆“锟斤拷”。原因分两类第一类是源码文件本身是UTF-8无BOM而Windows下MSVC编译器默认用本地代码页GBK去读第二类是Qt Creator的“文件编码”设置和构建套件里“始终使用默认编码”两个选项打架。解决在.pro文件里加一行强制指定UTF-8同时把Qt Creator的“Editor - 文件编码”改为UTF-8。具体写法如下# RunnerGame.pro 片段 QMAKE_CXXFLAGS /utf-8这是MSVC从2015 Update 2开始支持的编译选项加在.pro文件里一行搞定效果是让编译器明确“源代码就是UTF-8”。加完这行后重新qmake再构建中文显示就正常了。这条我在每届学生里都要提醒一次因为它编译期不报任何错只有运行期才暴露属于典型的“验收前夜爆发型”问题。5.2 定时器越跑越快或越跑越慢事件循环被阻塞现象游戏刚启动时节奏正常玩了十几秒后障碍物速度明显变快或者界面卡一下后整个游戏加速。原因onTimeout里做了耗时操作比如在游戏逻辑里直接写文件、加载图片把事件循环卡住。QTimer本身不会自动加速但如果你在timeout信号里新建了QTimer并start且没有正确管理父对象就会叠加出多个定时器同时触发。解决全局只保留一个gameTimer别在槽函数里再new定时器。如果你要临时暂停游戏调用gameTimer-stop()要恢复就start()。反复stop/start没问题但注意start()会重置计时起点不是“接着上次继续”。如果游戏里需要慢动作或者加速效果应调整障碍物移动步长而不是改定时器间隔改定时器间隔等于改了整个帧率容易出现手感突然变飘。5.3 游戏长时间运行CPU占用率居高不下重绘范围没控制现象任务管理器里CPU占用常驻30%以上风扇狂转。原因MainWindow里重写了paintEvent但每次update()都把整个窗口重新绘制了一遍。跑酷游戏的背景其实是不变的跑道和天空只有角色和障碍物在动全量重绘纯粹浪费。解决先看paintEvent里画了什么。如果背景是大面积渐变或图片可以单独把它画到一个QPixmap上每帧只pixmap绘制一次之后在paintEvent里用drawPixmap把背景整体贴上去再画动态物体。更简单的做法是设置setAutoFillBackground(false)去掉QWidget默认背景填充避免已经画了背景图片后又刷一层底色。课程作业阶段不追求极致优化但CPU占用这一点老师通常会关注主动在代码里留个背景缓冲的注释比答辩时被问得支支吾吾要好得多。5.4 存档路径写死导致找不到文件用QSettings和AppData目录现象历史最高分存不下来或者在家里运行正常拿到学校机房打开就报“无法保存”。原因把存档文件写成了相对路径“score.dat”。相对路径就取决于程序当前工作目录Qt Creator里运行时工作目录是构建目录双击运行exe时工作目录是exe所在目录两个环境对不上。解决用QSettings把数据写到系统标准配置目录代码简洁且跨平台不用改。// 保存最高分 QSettings settings(QSettings::IniFormat, QSettings::UserScope, RunnerGame, runner); settings.setValue(bestScore, m_bestScore); // 读取最高分 int bestScore settings.value(bestScore, 0).toInt();这段代码在Windows上会把ini文件写到AppData目录下Linux上写到家目录下的隐藏配置目录里老师拷走你的源码重新编译运行也不会出现找不到文件的问题。课程设计里保存存档用QSettings比手动写文本文件少很多边界问题比如编码问题、换行问题、文件不存在时的处理。5.5 角色跳跃动画生硬状态机没做好现象角色按跳跃键后直接瞬移到最高点再掉下来看起来像瞬移。原因跳跃逻辑没有维护“上升中/下落中/在地面”三种状态而是简单把y坐标直接改到一个固定高度。解决写一个状态枚举每帧根据状态更新y坐标和速度enum class JumpState { OnGround, Rising, Falling }; void GameEngine::updatePlayer() { if (m_player.jumpState JumpState::OnGround) { return; } if (m_player.jumpState JumpState::Rising) { m_player.vy - kGravity * kFrameIntervalSeconds; m_player.y - m_player.vy * kFrameIntervalSeconds; if (m_player.vy 0) { m_player.jumpState JumpState::Falling; } } else if (m_player.jumpState JumpState::Falling) { m_player.vy kGravity * kFrameIntervalSeconds; m_player.y m_player.vy * kFrameIntervalSeconds; if (m_player.y kGroundY) { m_player.y kGroundY; m_player.jumpState JumpState::OnGround; } } }这个写法的核心是速度变量vy每一帧都被重力修改位置是“基于上一帧速度的变化量”累加出来的不是瞬移。答辩时如果能画出这张状态转换图游戏玩法这一关基本就过了。5.6 内存泄漏障碍物不停new旧对象没人管现象跑久了内存占用缓步上涨游戏越来越卡。原因生成障碍物时用了new Obstacle然后放进列表但清除已移出屏幕的障碍物时只erase了列表元素没有delete。或者更隐蔽的直接用Obstacle *指针存进QListQList析构时不会自动帮你去delete指针。解决首选方案是存对象值而不是指针QList 当障碍物移出屏幕时直接removeAt对象自动析构不需要手动管理内存。只有当障碍物需要多态不同子类不同绘制方式时才用指针并且把指针交给Qt的对象树管理设置合适的parent。课程作业用值列表足够还少写一堆内存管理代码。6. 冲高分的一道甜点背景滚动、音效接入和你的专属存档设计前面把游戏本体做扎实了想再拿高一点的分就需要在“别人没做”的地方做文章。我推荐三个性价比高的方向。第一个是背景滚动把跑道上的参照物做成一个重复的QPixmap用滚动偏移量控制绘制位置每帧偏移量加上障碍物同样的速度画面一滚起来跑酷感立刻就有了。实现时注意偏移量对背景图宽度取模否则滚出屏幕后就露出空白底。第二个是音效。Qt的QSoundEffect类支持播放wav格式的音频文件跳跃、碰撞、结算各配一个短音效代码量不超过十行。关键点是音效文件要用qrc资源系统打包不要指望相对路径加载点击、跳跃这类动作音效的播放量很大QSoundEffect比QMediaPlayer轻量不少不会拖慢主循环。如果你手头没有现成音效素材课程作业场景下自己用Audacity生成几个短音效也很方便这比直接从网上下载来源不明的素材稳妥。第三个是存档设计再往前走一步。在最高分基础上增加一页“历史成绩记录”保存最近十局的分数和用时。这个小功能不复杂但答辩时能引出“为什么用QSettings不用文件”“记录多了怎么控制长度”这两个问题都能展示你考虑了数据持久化的边界条件void GameEngine::saveScoreToHistory(int score) { QSettings settings(QSettings::IniFormat, QSettings::UserScope, RunnerGame, runner); QStringList history settings.value(history).toStringList(); history.prepend(tr(%1|%2) .arg(QDateTime::currentDateTime() .toString(yyyy-MM-dd hh:mm:ss)) .arg(score)); // 只保留最近 10 条防止 ini 文件无限膨胀 while (history.size() 10) { history.removeLast(); } settings.setValue(history, history); }这是我做完这套跑酷项目的最深体会课程设计拿到高分的人通常不是游戏做得多华丽而是每一步都留了“能解释的为什么”。我当年交上去的版本在答辩时被问过“你为什么不把障碍物数量存文件里”一下子发现我只存了最高分没有考虑历史记录。当场补充设计思路后才圆回来从此落下了把存档字段想全的习惯。这个不大不小的教训现在分享给你希望帮你在验收前就把这类边界问题堵上省得被老师问住。本文还有配套的精品资源点击获取
返回列表