简介:一款面向计算机相关专业学生与Qt初学者的C++课程设计资源,基于Qt框架实现跑酷小游戏,源码完整、可编译运行,适合正在准备期末大作业或需要项目实战练习的开发者。包内共72个文件,涵盖7个cpp源码、6个h头文件、2个ui界面文件,以及pro项目管理、qrc资源索引和53张png图片素材,压缩包约3.46MB;代码按界面、场景、角色等模块划分,结构清晰,便于理解游戏循环、碰撞检测与Qt界面设计。该项目为大二高分大作业,评审得分99分,功能完整,涉及角色控制、障碍生成、飞镖攻击、计分逻辑等多个模块,并附有README说明,能帮助读者快速掌握游戏框架与二次开发思路。同时资源附带清晰的目录结构说明,方便定位关键代码;游戏素材齐全,可直接替换图片定制主题。目前已有94人学习下载,作为课程设计参考或Qt游戏开发入门范例均有较高价值。
1. 把“高分课程大作业”当成一个能跑的产品来拆:这套 Qt 跑酷源码到底在给你什么
距课程设计提交只剩一周,你打开 Qt Creator,新建了一个 QWidget 工程,然后对着空窗口发呆——这是很多人在做 C++ 课程大作业时的真实状态。C++ 基于 Qt 框架的跑酷小游戏源码,乍看是个“小游戏”,实际是一份标准的 Qt Widgets 项目骨架:游戏循环用 QTimer 驱动、画面用 QPainter 绘制、交互靠键盘事件接管,再加上重力模拟、碰撞检测和积分系统,正好覆盖了 Qt 课程里最常考的几个知识点,代码量也足够写进“项目概况”那一页 PPT。它适合两类人:一是要以“能跑、能演示、能答辩”为底线的课程设计选手,二是刚学完 C++ 想找个图形界面项目练手的 Qt 新手。这篇笔记按我的落地习惯,把这套源码拆成框架、玩法、高分结构和编译调试点四层,照着复现完,你手里就是一份能理直气壮交上去的作业。
2. 用 QTimer 和 QPainter 搭出跑酷最小循环:三个类撑起一个能交差的 Demo
2.1 为什么选 QWidget + QPainter 而不是 QML
先回答一个常见疑问:Qt 做游戏不是有 QML 吗?QML 做动画、做粒子效果确实比 Widgets 省事很多,但课程作业这条路线有坑。QML 虽然写界面快,大量逻辑却落在 JavaScript 这一层,答辩时老师问“你的类封装在哪、继承关系是什么”,你很难讲清楚。Widgets 这边,界面是一个 QWidget 子类,绘制、事件、状态全部收在 C++ 类里,代码量也实打实摆在那里,目录结构一眼就是“正经项目”。如果你只在 Qt Creator 里见过 QML 模板,这次建议直接选 Widgets Application,省去不必要的学习成本。
另一个理由是编译链路简单。QML 工程在 Qt 5.15 上往往要带 qml 目录、qrc 资源、还有一堆 import,换到室友的机器上部署时少一个文件就白屏。Widgets 程序编译出来就是一个 exe,配合 windeployqt 打包,拷贝整个文件夹就能跑。对课程提交来说,可迁移性就是安全感。
2.2 游戏主循环:用 QTimer 驱动 onTick,而不是 while(1)
跑酷游戏本质是一个不断刷新的状态机,每个刷新周期做三件事:处理输入、更新实体位置、重绘画面。Qt 里最稳的做法是用 QTimer 定时触发一个槽函数,而不是自己在子线程里跑 while(1)。原因很简单:QPainter 绘制必须发生在 GUI 线程的事件循环里,你在子线程里画界面要么崩溃、要么黑屏,而 QTimer 的 timeout 信号天然就在主线程。
下面是最小工程的类声明,三个类各管一摊:GameWidget 管窗口和调度,Player 管角色状态,Obstacle 管障碍物实体。
// gamewidget.h — 游戏窗口骨架 class GameWidget : public QWidget { Q_OBJECT public: explicit GameWidget(QWidget *parent = nullptr); protected: void paintEvent(QPaintEvent *event) override; void keyPressEvent(QKeyEvent *event) override; private slots: void onTick(); // 每个定时器周期调用一次 private: QTimer m_timer; // 游戏主循环驱动 Player m_player; // 玩家实体 QVector<Obstacle> m_obstacles; // 障碍物列表 int m_score = 0; // 当前分数 bool m_running = false; // 游戏是否进行中 int m_elapsedFrames = 0; // 累计帧数 };构造函数里要做的不是启动一堆逻辑,而是把定时器参数定好,把键盘事件接住。
// gamewidget.cpp — 构造函数 GameWidget::GameWidget(QWidget *parent) : QWidget(parent) { setFocusPolicy(Qt::StrongFocus); // 必须设置,否则收不到键盘事件 setFixedSize(800, 400); // 锁死窗口,坐标逻辑不用考虑缩放 connect(&m_timer, &QTimer::timeout, this, &GameWidget::onTick); m_timer.setInterval(16); // 约 60 FPS 刷新 }这里有两个参数值得记住。setInterval(16)是 60 FPS 的标准帧间隔,显示器的垂直同步节奏也是 60Hz,这样角色移动看起来不跳帧。setFixedSize(800, 400)锁死窗口后,paintEvent 里的所有坐标都可以按绝对值写死,不用处理窗口拉伸带来的换算,这个决定能在后面帮你省掉一大块布局调试时间。如果你想把游戏窗口嵌进更大的页面,可以把固定尺寸改成 resize,但障碍物生成坐标和地面高度都要跟着改,属于吃力不讨好,课程设计不推荐。
2.3 onTick 里更新逻辑,paintEvent 里只画状态
游戏循环的正确姿势是:把状态更新集中在一个地方,把绘制集中在另一个地方,两者通过成员变量通信。很多新手在 paintEvent 里写角色移动,一刷新就 new 一堆临时对象,结果整个程序卡得像幻灯片。这里给出 onTick 的完整实现:
void GameWidget::onTick() { if (!m_running) { return; // 游戏结束或未开始时不更新逻辑 } m_elapsedFrames++; // 帧计数前进一格 m_player.update(); // 先更新角色物理:重力、跳跃、落地 // 障碍物生成:每 90 帧生成一个,速度随分数提升 int spawnInterval = qMax(30, 90 - m_score / 200); if (m_elapsedFrames % spawnInterval == 0) { m_obstacles.append(Obstacle(width())); } // 障碍物移动与碰撞检测 for (int i = m_obstacles.size() - 1; i >= 0; --i) { m_obstacles[i].moveLeft(2 + m_score / 500); if (m_player.collidesWith(m_obstacles[i])) { m_running = false; // 只改状态,结算交给独立函数 } if (m_obstacles[i].x() + m_obstacles[i].width() < 0) { m_obstacles.removeAt(i); // 出屏就删,防止容器膨胀 } } update(); // 请求 Qt 重绘当前窗口 }这段代码里有两个细节必须说透。第一,for 循环从size() - 1往前遍历再removeAt(i),是因为删除元素会让后面的索引前移,正序遍历会跳过紧挨着被删元素的下一个;从后往前删永远不会漏。第二,update()只是一个请求标记,Qt 会在事件循环空闲时调用 paintEvent,你不在 onTick 里直接绘制,就不会出现画面撕裂和重入问题。
之后 paintEvent 只需要读取成员变量画出来。地面的绘制、玩家的矩形、障碍物的矩形,全部用 QPainter 的基本图元完成,20 行以内就能写完。这个“更新与绘制分离”的结构,也是后两章一切高分支撑的基础。
3. 跳跃、障碍与碰撞检测:把“跑酷感”落到算法而不是手感调参
3.1 重力与跳跃:一帧一积分,让角色有“物理感”
跑酷游戏的“跑酷感”不是靠随机抖动调出来的,而是靠一组稳定的物理参数。常见做法是把角色的 Y 坐标改为由速度积分得到:每帧给垂直速度叠加一个固定的重力加速度,然后按速度移动位置。伪物理模型看起来简单,但参数配不好就出现两种情况:重力太小角色飘在天上,重力太大落地像钢板砸下来。
下面是我常用的参数组合,按 60 FPS 帧率设计。
void Player::update() { // 跳跃:只在“未起跳”时消费按键请求 if (m_jumpRequested && !m_isJumping) { m_vy = -8.0f; // 初速度,负方向为向上 m_isJumping = true; m_jumpRequested = false; // 请求消费掉,避免连跳 } if (m_isJumping) { m_vy += 0.4f; // 重力加速度 m_y += m_vy; // 按速度积分位置 if (m_y >= m_groundY) { m_y = m_groundY; // 落地钳制,不穿地面 m_isJumping = false; m_vy = 0.0f; } } }参数说明:初速度 -8.0、重力 0.4、地面高度按窗口 400 像素下取 300。这个组合的跳跃最高点在起跳后大约 20 帧出现,峰值高度约 80 像素,刚好能越过一个 40 像素高的障碍物,又不至于一蹦就撞到窗口顶部。如果你想让手感更轻快,把初速度改成 -9.0、重力改成 0.35,跳跃弧线会更高更飘;想走沉重路线就反过来。唯一的原则是:调整后必须把“能否跳过最大障碍物”重新验证一遍,这是最常见的玄学调参翻车点。
跳跃按键不能直接在 keyPressEvent 里改坐标,而是只记录请求。
void GameWidget::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Space) { if (!m_running) { startGame(); // 死亡后按空格重开 } else { m_player.requestJump(); // 写请求,不写坐标 } } QWidget::keyPressEvent(event); }原因很简单:keyPressEvent 是事件驱动,可能一秒钟触发好多次,而 update 是固定频率。如果你在按键事件里直接改 m_y,角色就会把多次按键的位移叠加在同一帧里,直接飞出屏幕。把请求和消费分开,是 Qt 游戏循环里最值得养成的一个习惯。
3.2 障碍物生成:让出屏节奏可用公式表达
障碍物生成不能完全随机,否则会出现连续三根柱子堵死路线的“死局”。常见做法是用帧计数取模来定间隔,再在间隔内加少量随机偏移。下面这个 Obstacle 构造函数在窗口右缘生成实体:
Obstacle::Obstacle(int windowWidth) { m_x = windowWidth; // 从右缘进入视野 m_w = 40 + qrand() % 30; // 宽度 40~69 像素 m_h = 40 + qrand() % 40; // 高度 40~79 像素 m_y = kGroundY - m_h; // 底部贴地,高度从地面向上生长 }需要说明的是qrand()在 Qt 5 里仍然可用,但你必须先qsrand(QTime::currentTime().msec())做种子初始化,否则每次运行生成的障碍物序列完全一样,答辩演示时会被老师一眼看穿。更稳妥的做法是用 C++11 的std::mt19937配合随机设备,代码只多两行,但“这里用了现代 C++ 随机数”可以成为答辩加分句。障碍物不能只随机宽度和高度,位置节奏也要留出反应时间:当障碍物在 800 像素宽的窗口里以每帧 2 像素的速度左移时,从出现到到达角色大约需要 200 帧,约 3.3 秒,这个窗口期足以让玩家做出一次完整判断。
3.3 AABB 碰撞检测:矩形相交判断的四个边界
跑酷游戏的碰撞检测不需要像素级精确,AABB(轴对齐包围盒)就够了。所谓 AABB,就是把角色和障碍物都看成矩形,然后判断两个矩形是否相交。相交的数学条件是:两个矩形在 X 轴投影重叠,同时在 Y 轴投影也重叠。
bool Player::collidesWith(const Obstacle &ob) const { int px1 = m_x, py1 = m_y; int px2 = m_x + m_w, py2 = m_y + m_h; int ox1 = ob.x(), oy1 = ob.y(); int ox2 = ob.x() + ob.width(), oy2 = ob.y() + ob.height(); return px1 < ox2 && px2 > ox1 && py1 < oy2 && py2 > oy1; }这里必须用严格小于和严格大于,不能用<=和>=。原因很微妙:两个矩形刚好贴边时,视觉上还没有重叠,但用小于等于就会判定为碰撞,角色会“隔空而死”。另一个坑是碰撞检测的时机,如果障碍物每帧移动 2 像素,而角色每帧最多移动 1 像素,那么连续两帧之间不会跳过重叠区域,不需要做更细的插值检测;但如果你调了速度参数让单帧移动超过角色宽度的一半,就会出现“穿模过墙”,这时才需要把检测拆成两段。对课程作业的速度量级,这个 AABB 足够。
分数结算也放在帧计数轴上,每帧加一个固定分,同时把高分写入成员变量。注意不要每帧都调用update()去刷新 QLabel 上的分数文字,那样会拖慢主循环;分数文字可以用独立 QTimer 每 500 毫秒刷新一次,或者直接在 paintEvent 里用drawText画在画布上。课程设计建议用后者,少一个控件就少一组信号槽要管理。
4. 课程大作业的高分源码结构:让答辩老师一眼看到你的封装能力
4.1 把绘制和逻辑分开:paintEvent 里不许出现业务代码
我见过不少拿低分的 Qt 大作业,问题不在功能不全,而在代码全堆在 paintEvent 和 keyPressEvent 里。老师打开源码,看到 300 行的 paintEvent,第一印象就是“结构不清晰”,后面功能做得再好也难翻身。高分的源码结构,核心就一句话:paintEvent 里只读取成员变量并绘制,不做任何逻辑判断、不 new 对象、不调用业务函数。
我一般会在绘制代码里遵循三条纪律。第一,地面、背景、分数这些静态元素放在一个drawBackground()私有函数里;角色和障碍物分别进drawPlayer()和drawObstacles()。这样新增一个绘制元素就是新增一个函数,而不是往 paintEvent 里再塞 20 行。第二,所有坐标计算提前在 onTick 里完成,paintEvent 只做 QPainter 的搬运工。第三,涉及图片资源时,在构造函数里把 QPixmap 一次性加载并缓存为成员变量,绝不放在 paintEvent 里加载——文件 I/O 阻塞会让画面间歇卡顿,这是“程序一跑就掉帧”的常见病因。
一个可参考的绘制分离结构:
void GameWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); drawBackground(painter); // 地面、天空渐变 drawScore(painter); // 分数文字 drawPlayer(painter); // 玩家矩形或精灵图 drawObstacles(painter); // 障碍物列表 drawStateText(painter); // “按空格开始”等提示 QWidget::paintEvent(event); }这段代码的价值不是省几行,而是让读代码的人一眼看出画面由哪几部分组成。答辩时你甚至可以对着这个函数讲:“整个游戏的画面分五个图层绘制,背景在最底,状态文字在最上。” 这句话本身就在回应评分表里“程序结构清晰”那一栏。
4.2 四状态机:用 enum 管住开始、运行、暂停、死亡
游戏流程只用 bool 变量管理是新手最容易翻车的地方。m_running 一个 bool 只能表达“开”和“关”,但跑酷游戏实际有四个状态:待开始、运行中、暂停、死亡。用 bool 组合去区分这四种状态,代码会变成一堆if (m_running && !m_paused),改一个条件就牵动全局。我习惯的做法是引入枚举状态机:
enum class GameState { Ready, // 待开始,显示标题和操作提示 Running, // 运行中,正常刷新逻辑 Paused, // 暂停,QTimer 继续走但逻辑不更新 Dead // 死亡,显示得分和重开提示 };配合状态机,onTick 里的分支变得非常清晰:
void GameWidget::onTick() { switch (m_state) { case GameState::Running: m_elapsedFrames++; m_player.update(); // ...障碍物生成、移动、碰撞检测 update(); break; case GameState::Paused: // 不更新游戏逻辑,但可以在这里做菜单动画 break; default: break; // Ready 和 Dead 都不需要逻辑更新 } }状态机的优越性在键盘事件里体现得更明显。按键变成事件时先查状态:Running 时按空格是跳跃,Dead 时按空格是重开,Ready 时按空格是开始。这三个动作如果要靠 if 拼 bool 变量,写着写着就会混。状态机是课程设计里性价比最高的“结构优化”,代码量只增加 10 行,但答辩时你能说出“我用了状态机管理游戏流程”,直接命中评分点。
4.3 参数配置集中化:把魔数清零,让调参只改一处
一份拿高分的源码,不应该出现遍地都是的 8.0f、0.4f、90 帧这些魔数。把这些参数集中放是一个很显功力的动作。常见做法是在项目里加一个Config命名空间:
namespace Config { constexpr int kGameWidth = 800; constexpr int kGameHeight = 400; constexpr int kGroundY = 300; constexpr float kGravity = 0.4f; constexpr float kJumpVelocity = -8.0f; constexpr int kBaseSpawnInterval = 90; constexpr int kBaseSpeed = 2; }所有类引用配置时都写Config::kGravity,而不是 0.4f。这带来一个实际好处:你调游戏手感时只需要开这一个文件,改完重新编译就能看效果,不用在十个文件里 Ctrl+F 找魔法数字。更进一步,把玩家体积、障碍物尺寸范围也放进 Config,复现这套源码的人改几行就能变成不同难度版本,这也是“可扩展性”的直接体现。
资源文件也要用 Qt 资源系统管理,而不是依赖本地图片路径。新建一个 res.qrc,把图标、背景图、音效文件全部加进去,代码里用":/images/player.png"这种前缀路径访问。用 qrc 的好处是发布时只有一个 exe 加一个可选的 dll 目录,不会出现图片找不到的黑屏事故。课程设计提交时,老师解压你的压缩包找到 exe 就能跑,比“需要把图片放在指定目录”的版本可靠得多。
4.4 类与文件划分:让目录结构自己会说话
高分源码的最后一个特征是文件划分合理。常见的划分方式是五个文件:main.cpp 只管创建 QApplication 和主窗口;gamewidget.cpp/h 是游戏窗口,负责调度;player.cpp/h 是角色逻辑;obstacle.cpp/h 是障碍物实体;config.h 是参数集中地。如果障碍物有多种类型,再拆出 obstacle 基类和两个子类。
文件拆分的原则是“换一个功能只动一个文件”。要改跳跃力度,只改 player.cpp;要改障碍物生成节奏,只改 gamewidget.cpp 里的生成逻辑;要加新障碍物,继承 Obstacle 后改生成分支。这种内聚度在答辩时很容易被注意到,老师翻一下文件列表就能看出你对“高内聚低耦合”的理解不是背概念,而是在代码里做出来了。顺便说一句,类和文件名一定要和内容对得上,见过太多 student.cpp 里装着一坨游戏逻辑的作业,那种代码不管功能多全,在老师那里第一印象已经降价了。
5. 编译与运行避坑:Qt 5.15 的 dependent 报错、定时器崩溃和中文乱码
5.1 编译期报错:error: dependent '..\qt\5.15.2\msvc2019_64\include\qtwidgets'打不开
这是 Qt 5 课程作业里最经典的开局翻车。现象是编译报错,错误信息长这样:
:-1: error: dependent '..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\qtwidgets' does not exist后面还可能跟着一堆QWidget: No such file or directory。看到这么长的相对路径,第一反应别去检查 Qt 安装目录,问题基本出在 .pro 工程文件上。原因:Qt 5 分成 core、gui、widgets 等多个模块,只有 core 和 gui 是默认链接的。你在代码里#include <QWidget>用的是 qtwidgets 模块的头文件,但 .pro 里没声明要链接这个模块,qmake 就不会把头文件搜索路径加进来,于是编译器顺着一个不存在的相对路径去找头文件,报出这串吓人的英文。
解决方法是打开 .pro 文件,确认里面至少有这行:
QT += core gui widgets greaterThan(QT_MAJOR_VERSION, 4): QT += widgets第一行是显式声明要 Qt Widgets 模块;第二行是兼容 Qt 4 的老写法,很多模板会带上。新建工程如果选的是 “Qt Widgets Application” 模板,这行会自动写进去;如果是手敲的 .pro 或者从旧工程改的,十有八九就漏在这里。改完 .pro 后重新 qmake,再编译,这个报错就会消失。这条的经验是:Qt 的报错信息里那个一长串..\开头的路径,不是在告诉你文件在哪里,而是在告诉你有某个模块没有启用。
5.2 运行时崩溃:关掉窗口程序就崩,定时器还在后台跳
现象:游戏运行正常,但点窗口右上角的关闭按钮时,程序偶尔闪退;或者退出后任务管理器里还留着进程。更隐蔽的变体是:关闭游戏窗口后,另一个界面还在不断刷新,日志持续输出。
原因:QTimer 的 timeout 信号连接到了某个槽函数,而槽函数所属的对象在窗口销毁时已经被释放,但定时器没有停止。Qt 的信号槽机制在连接存在且发送者仍存活时,会尝试调用接收者的槽。如果接收者已经 delete,对应的就是“访问已释放内存”,轻则静默崩溃,重则随机 SEGFAULT。另一个常见病因是在构造函数里用 lambda 捕获了 this,而这个 lambda 对象生命周期长于窗口。
解决方法是养成两个习惯。第一,在 GameWidget 的析构函数里显式m_timer.stop(),这是常规解法,任何时候都不会错。第二,所有 connect 都指定接收者上下文为 this:
connect(&m_timer, &QTimer::timeout, this, &GameWidget::onTick); // 接收者 this,安全如果你用了 lambda 连接,也把 this 作为第三个参数传进去,让 Qt 在 this 销毁时自动断开连接。这个习惯在 Qt 5 里能避开九成由生命周期引起的崩溃。另外补充一条排查手段:在 Qt Creator 的“应用程序输出”面板里看到The program has unexpectedly finished时,用调试模式运行,崩溃时调用栈会停在出问题的代码行,比瞎改参数有效得多。血泪经验是:关闭窗口崩溃这类问题,十次里有八次是定时器或 lambda 生命周期,不要先去怀疑你的碰撞检测写错了。
5.3 中文乱码:按钮上的“开始游戏”变成了一串符号
现象:在 Qt Creator 源码里写的中文字符串,编译运行后显示成乱码,但英文完全正常。有的人换一台电脑又好了,显得很“玄学”。
原因:这是编码链不一致的问题。源码文件本身是 UTF-8 编码(Qt Creator 默认),但 MSVC 编译器在没有显式指示时,默认按本地代码页读源文件,中文 Windows 下是 GBK。于是编译器把 UTF-8 的字节流当成 GBK 去解析,字符串字面量里的中文字节就被翻译成了错误的字符序列,运行时就显示乱码。Qt 5 的QString存储的是 Unicode,但源代码这一步已经歪了,后面怎么转都补不回来。
解决方法是让编译器强制按 UTF-8 读源码,在 .pro 里加一行:
msvc: QMAKE_CXXFLAGS += /utf-8这个参数让 MSVC 把源文件和目标文件都按 UTF-8 处理,中文字符串字面量表现和 GCC 一致。如果你用的是 MinGW 编的 Qt,默认就是 UTF-8,一般不会遇到这个问题。还有一个偏方是把源码文件转成 UTF-8 with BOM,MSVC 看到 BOM 也会切换到 UTF-8 解析,但每加一个文件都要手动转,非常烦。统一用/utf-8编译选项是最省心的。另外,不要在源码里写QString("中文")这种老写法,改用QStringLiteral("中文")或QString::fromUtf8("中文"),性能更好,语义也明确。如果你做完这步还乱码,检查一下是不是用了tr()包中文字符串,Qt 5 的翻译机制在国内课程作业场景下反而容易引入二次编码问题,直接去掉最干净。
5.4 高分屏下窗口模糊、点击偏移:高 DPI 缩放两个坑
现象:在 2K 或 4K 显示器上,编译出来的程序窗口很小、文字模糊,鼠标点击的位置和界面元素对不上。这通常出现在 Windows 高分屏上,DPI 缩放一介入,Qt 默认策略就出问题。
原因:Windows 会把高分辨率显示器的缩放比例设为 125% 或 150%,而 Qt 5.15 默认不感知缩放,界面按逻辑像素绘制,再被系统拉伸,自然模糊。点击偏移则是因为鼠标事件坐标和绘制坐标用了不同的缩放基准。
解决是在 main.cpp 里,QApplication 构造之前设置高 DPI 属性:
int main(int argc, char *argv[]) { #if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); #endif QApplication::setHighDpiScaleFactorRoundingPolicy( Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); QApplication app(argc, argv); GameWidget w; w.show(); return app.exec(); }这几行的要点是:setAttribute必须发生在 QApplication 对象创建之前,写成全局 static 变量声明处也可以,就是不能放在app构造之后,否则设置无效。PassThrough策略让 Qt 使用非整数的实际缩放比例,而不是四舍五入到 100% 或 200%,能减少文字边缘发虚的现象。做完之后窗口初始尺寸会随缩放变大,你setFixedSize(800, 400)设的是逻辑像素,实际物理像素会按比例放大,这是预期行为,不用改。注意这行代码不同 Qt 版本行为有细微差别,但 5.6 到 5.15 都兼容,可以直接抄。
5.5 换台电脑就闪退:缺 Qt dll 和 VC 运行库
现象:在你自己机器上跑得好好的 exe,拷到室友电脑上双击,要么弹窗提示找不到 Qt5Widgets.dll,要么直接报错 0xc000007b,要么提示缺少 VCRUNTIME140.dll。
原因:Qt 默认是动态链接运行库,你的 exe 在运行时需要找 Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll 这些动态库。这些库在你机器上有是因为 Qt 安装目录在 PATH 里;室友机器没装 Qt,自然找不到。0xc000007b 则多半是架构不匹配——你编的是 64 位,拷到 32 位系统上运行,位数对不上。
解决方法是发布时带上 Qt 官方提供的部署工具。用 Qt 自带的命令行工具,进入你编译输出目录,执行:
windeployqt 你的游戏名.exe这个工具会把 exe 依赖的所有 Qt dll、插件目录、以及必要的运行库一并复制到当前目录,然后把整个文件夹拷给室友即可。要注意:windeployqt 也有 32 位和 64 位之分,Qt Creator 里你配的构建套件是 MSVC2019 64-bit,命令行里也要打开对应的 “Qt 5.15.2 (MSVC 2019 64-bit)” 快捷方式,不能拿 MinGW 的部署工具去补 MSVC 编的 exe。另外,MSVC 构建的程序默认还需要 VC 运行库,如果你的目标机器是较干净的 Windows,把对应版本的 vcredist 安装包一起放进去,或者提前让室友装上。更省事的一条路是在 .pro 里加QMAKE_LFLAGS += /STATIC开启静态链接,把 Qt 核心库编进 exe,但 Qt 5.15 的静态构建包需要自己去 Qt 官网下载,课程作业通常不值得折腾,windeployqt 已经够用。
6. 最后一步提价:QSS 美化、状态机补齐与 30 分钟验证
6.1 用 QSS 把“学生界面”变成“完工界面”
课程设计的界面往往是扣分重灾区,因为默认 QWidget 长什么样,大家的作业就长什么样。QSS 是这里最划算的投入:在项目里加一个 qss 文件,或者在 main.cpp 里用app.setStyleSheet(...)设置全局样式,给背景、分数标签、按钮加上圆角和渐变,视觉感受会提高一个档次。比如给分数区加一个透明黑底白字的效果,只需要十几行:
QLabel#scoreLabel { color: #ffffff; background-color: rgba(0, 0, 0, 150); border-radius: 8px; padding: 4px 12px; font: bold 16px "Microsoft YaHei"; }注意 QLabel 要设置 objectName 为 scoreLabel,QSS 才匹配得上。这个细节如果写进答辩介绍,“我用 QSS 实现了界面美化”会比你口头说“界面挺好看的”有说服力得多。
6.2 答辩前最值得做的三个验证
功能写完到提交之间,留出半天做验证,按这个清单过一遍最省心。第一,连续运行 30 分钟,打开任务管理器观察内存占用曲线,如果持续上涨,说明有对象在循环里不断 new 而没有 delete,回头查 onTick 里的临时对象和障碍物容器。第二,快速连按空格、按住空格不松手,看角色会不会出现“跳跃消失”或者卡进地面;正常表现是每帧请求被正确消费,角色只跳一次固定高度。第三,把窗口从默认尺寸拖大再拖小,如果界面元素没有错位,说明你的布局是稳定的。这个验证如果放到演示前一天做,可能一夜都在改代码。
我做 Qt 课程设计那会儿的习惯是:功能全跑通以后,再花一天时间慢下来读自己写的代码,把每个数字为什么是这个值都写进注释,把状态机的分支画在纸上对一遍。答辩时老师盯着我看,问“跳跃请求为什么不在 keyPressEvent 里处理而是放在 update 里”,当时我能把原因讲清楚,那种准备充分的从容,比多加一个特效有用得多。希望帮到你。
本文还有配套的精品资源,点击获取