跳到主要内容基于 Qt 与 C++ 的经典游戏小球打砖块完整项目实战 | 极客日志C++大前端算法
基于 Qt 与 C++ 的经典游戏小球打砖块完整项目实战
本文详细介绍了如何使用 Qt 框架和 C++ 语言开发一款经典的小球打砖块游戏。文章涵盖了从架构设计(三层模式)、视觉系统(QGraphicsView 与 Scene-View-Item 模型)、游戏主循环驱动、图形元素封装(QGraphicsItem 继承)、图像资源加载与优化、动态场景更新技巧、键盘事件处理(防抖与状态管理)、小球运动与反射算法、碰撞检测实战以及游戏状态机设计等核心内容。通过完整的代码示例和流程图,帮助开发者掌握从界面绘制到逻辑封装的完整流程,构建高性能、可扩展的 2D 游戏引擎。
Qt 框架下小球打砖块游戏的设计与实现:从零构建一个高性能、可扩展的 2D 游戏引擎
'小球打砖块'是一款使用 C++ 语言和 Qt 框架开发的经典休闲游戏,支持跨平台运行,涵盖图形界面构建、事件处理、碰撞检测与游戏逻辑实现等核心技术。通过控制挡板反弹小球以消除砖块,玩家需完成关卡目标。本项目全面整合 GUI 设计、动画控制、状态管理与资源处理,适用于学习 Qt 应用开发与游戏编程基础。
架构先行:为什么你的游戏需要三层楼?
如果把所有逻辑都塞进一个类里,比如 MainWindow,那很快就会变成一团乱麻:绘图、移动、碰撞、音效全都混在一起。所以,我们采用经典的三层架构模式:
- 界面层(View):负责看,也就是画面展示;
- 逻辑层(Controller/Model):负责想,控制规则和状态流转;
- 资源管理层(Resource Manager):负责存,统一管理图片、声音等资产。
这三者之间通过信号槽机制解耦通信。Qt 的 QObject 派生类天然支持信号槽,这是实现松耦合的最佳武器。
视觉系统的基石:为何选择 QGraphicsView 而非 QWidget 直接绘图?
QGraphicsView 就像是为复杂交互式应用量身定做的舞台导演。它与 QWidget + QPainter 的对比如下:
| 特性 | QWidget + QPainter | QGraphicsView |
|---|
| 图元管理 | 手动维护列表 | 内置场景自动管理 |
| 局部刷新 | 全局重绘或手动裁剪 | 自动脏区域检测 |
| 动画支持 | 需配合 QTimer 自行实现 | 支持 QPropertyAnimation 等高级动画 |
| 坐标系统 | 设备坐标为主 | 场景 + 视图 + 项目三级坐标体系 |
| 事件分发 | 手动判断点击位置 | 自动映射并转发给对应 item |
Scene-View-Item 模型解析
- QGraphicsScene:后台大管家,管着所有的游戏角色(items),但它自己不上台;
- QGraphicsView:摄像机镜头,把你指定的场景拍出来显示在屏幕上;
- QGraphicsItem:演员本人,每个砖块、小球、挡板都是它的子类。
graph TD
A[QWidget 主窗口] --> B[QVBoxLayout 布局]
B --> C[QGraphicsView 视图]
C --> D[QGraphicsScene 场景]
D --> E[QGraphicsItem 砖块]
D --> F[QGraphicsItem 小球]
D --> G[QGraphicsItem 挡板]
从主窗口到最底层的图形项,层层嵌套,职责分明。这种结构不仅清晰,还特别适合后期扩展。
游戏的心跳:主循环是如何驱动一切的?
在 Qt 中,我们靠 QTimer 来模拟这颗心脏。设置一个固定时间步长(通常是 16ms,对应 60FPS),让它不断触发更新:
connect(m_timer, &QTimer::timeout, this, &GameScene::gameLoop);
m_timer->start(16);
每一帧典型的闭环流程如下:输入处理 → 小球位移计算 → 碰撞检测 → 状态更新 → 场景重绘。最关键的一点是逻辑更新与渲染分离。即使某帧卡了一下,也不影响物理逻辑的准确性。
让画面动起来:基于 QGraphicsItem 的可视化元素封装
现在进入实战环节。我们要把挡板、小球、砖块这些元素一个个做出来,并且要做得优雅、可复用。
继承 QGraphicsItem:定义可动的基本单元
所有图形元素都应继承自 QGraphicsItem。你需要重写三个核心函数:
class Ball : public QGraphicsItem {
public:
QRectF boundingRect() const override { return QRectF(-5, -5, 10, 10); }
void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override {
painter->setRenderHint(QPainter::Antialiasing);
painter->setBrush(Qt::yellow);
painter->setPen(Qt::darkGray);
painter->drawEllipse(-5, -5, 10, 10);
}
QPainterPath shape() const override {
QPainterPath path;
path.addEllipse(boundingRect());
return path;
}
};
为什么要返回 QPainterPath?因为默认的 collidesWithItem() 只用 boundingRect() 判断,那是矩形框。如果你的小球是圆的,就会出现明明没碰到却判定碰撞的情况。而一旦你重写了 shape(),Qt 会使用路径进行更精细的检测。
挡板、小球、砖块的具体实现对比
Paddle(挡板)
class Paddle : public QGraphicsItem {
QRectF boundingRect() const override {
return QRectF(-40, -8, 80, 16);
}
void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override {
painter->setRenderHint(QPainter::Antialiasing);
painter->setBrush(QColor(70, 130, 180));
painter->setPen(Qt::NoPen);
painter->drawRoundedRect(boundingRect(), 8, 8);
}
};
Brick(砖块)
class Brick : public QGraphicsItem {
Q_OBJECT
public:
Brick(qreal x, qreal y, const QColor &color) : m_color(color) {
setPos(x, y);
}
QRectF boundingRect() const override {
return QRectF(-35, -10, 70, 20);
}
void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override {
painter->setBrush(m_color);
painter->setPen(QPen(Qt::white, 1));
painter->drawRoundedRect(boundingRect(), 4, 4);
}
private:
QColor m_color;
};
| 属性 | Ball(小球) | Paddle(挡板) | Brick(砖块) |
|---|
| 形状 | 圆形 | 圆角矩形 | 圆角矩形 |
| 尺寸 | 直径 10px | 宽 80×高 16px | 宽 70×高 20px |
| 是否可动 | 是 | 是 | 否(销毁前固定) |
| 是否参与碰撞 | 是 | 是 | 是 |
| 是否可销毁 | 否 | 否 | 是 |
| Z 值(层级) | 中 | 高 | 低 |
Z 值决定了绘制顺序,数值越大越靠前。比如挡板一定要比砖块高,否则会被盖住。
抗锯齿与渐变填充
void Ball::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) {
painter->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
QRadialGradient gradient(0, 0, 5, -2, -2);
gradient.setColorAt(0, Qt::white);
gradient.setColorAt(1, Qt::yellow);
painter->setBrush(gradient);
painter->setPen(QPen(Qt::darkGray, 0.8));
painter->drawEllipse(boundingRect());
}
性能提醒:抗锯齿虽然好看,但耗性能。建议只在关键元素上启用,或者让用户自行开关。
砖块布局算法:整齐排列的艺术
砖块通常以矩阵形式出现在屏幕上方。为了居中对齐,我们需要动态计算起始 X 坐标:
const int cols = 10;
const int brickWidth = 70;
const int hSpacing = 10;
const int totalWidth = cols * brickWidth + (cols - 1) * hSpacing;
const int startX = (800 - totalWidth) / 2;
QColor colors[] = {Qt::red, Qt::magenta, Qt::blue, Qt::cyan, Qt::green};
for (int row = 0; row < 5; ++row) {
for (int col = 0; col < cols; ++col) {
qreal x = startX + col * (brickWidth + hSpacing);
qreal y = 50 + row * 30;
Brick *brick = new Brick(x, y, colors[row % 5]);
scene->addItem(brick);
m_bricks.append(brick);
}
}
图像资源加载与优化策略
静态绘图虽好,但真实游戏中常需加载背景图、精灵图等外部资源。
使用 QPixmap 加载并缓存图像
QPixmap background = QPixmap(":/images/bg_stars.png");
if (background.isNull()) {
qWarning() << "Failed to load background image!";
} else {
background = background.scaled(800, 600, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
scene->setBackgroundBrush(background);
}
- 资源路径使用
: 开头,表示从 Qt 资源系统(qrc)读取,打包后无需额外文件;
- scaled() 支持高质量插值算法;
- setBackgroundBrush() 会自动拉伸或平铺。
缓存复杂图形减少重复绘制
有些内容每次重绘代价很高,比如带阴影的文字提示。可以预先渲染成 QPixmap 缓存:
QPixmap cache(200, 50);
cache.fill(Qt::transparent);
QPainter p(&cache);
p.setRenderHint(QPainter::Antialiasing);
p.setFont(QFont("Arial", 16));
p.setPen(Qt::white);
p.drawText(cache.rect(), Qt::AlignCenter, "Press SPACE to Start");
p.end();
painter->drawPixmap(-100, -25, cache);
这样就把多次矢量操作变成了单次位图拷贝,效率翻倍。
高 DPI 适配不能少
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
pixmap.setDevicePixelRatio(devicePixelRatioF());
动态场景更新技巧
游戏运行时,场景时刻在变:小球飞、挡板滑、砖块炸。如何高效更新?
销毁砖块的标准姿势
void GameScene::destroyBrick(Brick *brick) {
removeItem(brick);
m_bricks.removeOne(brick);
delete brick;
emit brickDestroyed();
}
千万不要反过来!否则 delete 后再调 removeItem 会导致访问非法内存,程序崩溃分分钟的事。
paint() 函数里的性能雷区
避免在 paint() 中做字符串拼接、动态分配这类事:
void BadItem::paint(...) {
QString text = QString("Score: %1").arg(score);
painter->drawText(..., text);
}
void ScoreItem::updateScore(int newScore) {
if (newScore != m_score) {
m_score = newScore;
m_cachedText = QString("Score: %1").arg(m_score);
update();
}
}
视口刷新策略优化
view->setViewportUpdateMode(QGraphicsView::MinimalViewportUpdate);
这个模式只会重绘最小必要的区域,大幅降低 GPU 负载。结合 update() 的局部刷新能力,性能起飞。
键盘事件处理:让玩家的操作如丝般顺滑
再好的游戏逻辑,如果响应迟钝也白搭。我们来看看如何打造一套灵敏、稳定、防抖的输入系统。
输入状态管理:告别 auto-repeat 的坑
操作系统自带的键盘连打机制(auto-repeat)延迟不定,不同平台还不一样。所以我们不能依赖 keyPressEvent 的频率来控制移动速度,而应该把它当作按下/释放的状态标志。
class GameView : public QGraphicsView {
QSet<int> m_pressedKeys;
protected:
void keyPressEvent(QKeyEvent *event) override {
if (!event->isAutoRepeat()) {
m_pressedKeys.insert(event->key());
}
QGraphicsView::keyPressEvent(event);
}
void keyReleaseEvent(QKeyEvent *event) override {
if (!event->isAutoRepeat()) {
m_pressedKeys.remove(event->key());
}
QGraphicsView::keyReleaseEvent(event);
}
};
void GameScene::processInput() {
bool leftPressed = m_view->isKeyPressed(Qt::Key_Left);
bool rightPressed = m_view->isKeyPressed(Qt::Key_Right);
if (leftPressed && !rightPressed) {
m_paddle->moveLeft();
} else if (rightPressed && !leftPressed) {
m_paddle->moveRight();
}
}
信号槽解耦:把按左键变成向左移动
Qt 的信号槽机制简直是模块解耦神器。我们可以定义语义化的信号:
class GameControl : public QObject {
Q_OBJECT
signals:
void moveLeft();
void moveRight();
void pauseGame();
void startGame();
};
void MainWindow::keyPressEvent(QKeyEvent *event) {
switch (event->key()) {
case Qt::Key_Left:
emit m_control->moveLeft();
break;
case Qt::Key_Right:
emit m_control->moveRight();
break;
case Qt::Key_Space:
emit m_control->pauseGame();
break;
default:
QMainWindow::keyPressEvent(event);
}
}
connect(m_control, &GameControl::moveLeft, m_paddle, &Paddle::moveLeft);
connect(m_control, &GameControl::moveRight, m_paddle, &Paddle::moveRight);
connect(m_control, &GameControl::pauseGame, this, &GameScene::togglePause);
好处是什么?将来换成手柄、触控甚至语音控制,只要发射同样的信号就行,完全不用改其他代码。
classDiagram
class MainWindow {
+keyPressEvent()
-emit moveLeft()
-emit pauseGame()
}
class GameControl {
<<signal>>
+moveLeft()
+moveRight()
+pauseGame()
}
class GameScene {
+togglePause()
+startGame()
}
class Paddle {
+moveLeft()
+moveRight()
}
MainWindow --> GameControl : emits signals
GameControl --> GameScene : connected to
GameControl --> Paddle : connected to
实时反馈与异常处理:专业级输入控制系统
高手之间的差距往往体现在细节上。下面我们来加几个高级特性。
挡板加速曲线:从匀速到变速
class Paddle : public QGraphicsItem {
qreal m_velocity = 0;
qreal m_acceleration = 20;
qreal m_maxSpeed = 300;
private slots:
void updateMovement() {
if (m_targetDir != 0) {
m_velocity += m_targetDir * m_acceleration / 60;
m_velocity = qBound(-m_maxSpeed, m_velocity, m_maxSpeed);
} else {
m_velocity *= 0.85;
}
setX(x() + m_velocity / 60);
checkBounds();
}
};
配上一个 16ms 的定时器,就能做出非常真实的物理感。
去抖动:防止暂停键连点抽风
连续按 Space 很容易造成暂停→恢复→暂停的抖动。加个最小间隔即可:
void GameScene::togglePause() {
static QElapsedTimer lastToggle;
if (lastToggle.isValid() && lastToggle.elapsed() < 300) {
return;
}
lastToggle.restart();
m_paused = !m_paused;
if (m_paused) {
m_gameTimer->stop();
} else {
m_gameTimer->start();
}
}
输入屏蔽:暂停时不响应方向键
void GameScene::keyPressEvent(QKeyEvent *event) {
if (m_state == GameState::Paused || m_state == GameState::GameOver) {
if (event->key() == Qt::Key_Escape) {
emit exitRequested();
}
event->ignore();
return;
}
}
小球运动与反射算法:数学建模的艺术
终于到了最烧脑的部分——小球的运动轨迹和反弹逻辑。
位置、速度、加速度的 C++ 封装
class Ball : public QGraphicsItem {
QPointF m_position;
QPointF m_velocity;
QPointF m_acceleration;
public:
void advance(int phase) override {
if (!phase) return;
m_position += m_velocity;
update();
}
void setVelocity(qreal vx, qreal vy) {
m_velocity = QPointF(vx, vy);
}
};
每帧调用 advance() 推进状态,简洁明了。
边界反弹的矢量公式
传统做法是判断碰哪边墙就反哪个分量。但这无法应对斜面或动态角度表面。真正的解法是使用矢量反射公式:
$$ \vec{v}' = \vec{v} - 2(\vec{v} \cdot \hat{n})\hat{n} $$
QPointF reflectVector(const QPointF &v, const QPointF &n) {
qreal dot = v.x()*n.x() + v.y()*n.y();
return v - 2 * dot * n.normalized();
}
只要传入单位法线向量,就能算出正确的新速度。无论是上下左右墙,还是任意倾斜面,通吃。
增强玩法:挡板不同区域击球角度不同
经典设定来了:打中挡板中间,球垂直向上;靠近边缘,则反弹角度更斜。
QPointF calculatePaddleReflection(const Ball* ball, const Paddle* paddle) {
qreal relPos = (ball->pos().x() - paddle->centerX()) / (paddle->width() / 2);
QPointF fakeNormal(-relPos * 0.5, -1.0);
return reflectVector(ball->velocity(), fakeNormal.normalized());
}
relPos ∈ [-1, 1],越靠边,法线越倾斜,反射角也就越大。玩家瞬间就有我能控方向的错觉,游戏深度立马提升。
碰撞检测实战:圆形 vs 矩形
数学原理:找最近点距离
- 找到矩形上离圆心最近的那个点;
- 看这个点到圆心的距离是否小于半径。
bool checkBallBrickCollision(const QPointF& center, qreal radius, const QRectF& rect) {
qreal clampedX = qBound(rect.left(), center.x(), rect.right());
qreal clampedY = qBound(rect.top(), center.y(), rect.bottom());
qreal dx = center.x() - clampedX;
qreal dy = center.y() - clampedY;
return (dx*dx + dy*dy) < (radius*radius);
}
为了避免开方运算,我们比较的是距离平方,性能更高。
预检优化:先做 AABB 粗筛
QRectF ballRect = ball->sceneBoundingRect();
QRectF brickRect = brick->sceneBoundingRect();
if (!ballRect.intersects(brickRect)) continue;
这一招能过滤掉大部分无效检测,CPU 占用直降一半不止。
游戏状态机:掌控全局生命周期
enum GameState { Start, Playing, Paused, GameOver };
stateDiagram-v2
[*] --> Start
Start --> Playing: 用户点击开始
Playing --> Paused: 按下 P 键或菜单暂停
Paused --> Playing: 再次按下 P 键
Playing --> GameOver: 小球掉落底部三次
GameOver --> Start: 点击重新开始
state Playing {
[*] --> MovePaddle
[*] --> UpdateBall
[*] --> CheckCollisions
}
| 状态 | 定时器 | 输入响应 | UI 显示 |
|---|
| Start | 停 | 接受开始 | 显示 Press Start |
| Playing | 开 | 挡板控制 | 隐藏按钮,更新分数 |
| Paused | 停 | 仅恢复 | 显示 PAUSED 浮层 |
| GameOver | 停 | 重试 | 显示最终得分 |
void GameWidget::setState(GameState newState) {
currentState = newState;
switch (newState) {
case Start:
timer->stop();
startButton->show();
ball->resetPosition();
break;
case Playing:
timer->start(16);
startButton->hide();
break;
}
emit gameStateChanged(newState);
}
主循环集成:形成闭环控制系统
connect(timer, &QTimer::timeout, [&]() {
if (currentState != Playing) return;
ball->move();
checkWallCollisions();
checkPaddleCollision();
handleCollisions();
checkWinCondition();
checkLoseCondition();
});
connect(this, &GameWidget::gameStateChanged, [this](GameState s){ updateUiForState(s); });
整个系统形成了输入 → 状态 → 物理模拟 → 渲染 → 反馈的完美闭环。
结语:你已经拥有了构建任何 2D 游戏的基础能力
我们从架构设计讲到图形绘制,从键盘响应讲到物理模拟,再到状态管理,一步步搭建出了一个结构清晰、性能优良、易于扩展的 2D 游戏骨架。
这不仅仅是一个小球打砖块游戏,它是你通往更复杂项目的跳板。未来你可以轻松加入:
- 多关卡系统
- 道具系统(加速球、多球、火焰弹)
- 音效与背景音乐
- 存档与排行榜
- 甚至网络对战模式
记住一句话:优秀的设计让扩展变得简单,糟糕的设计让修改变得痛苦。
而现在,你手里握着的就是那份优秀的设计。继续加油,下一个爆款可能就是你做的。
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown 转 HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
- HTML 转 Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online