本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:使用C++与QT5框架开发贪吃蛇小游戏是学习图形界面编程与基础游戏逻辑设计的经典案例。该项目基于QT强大的跨平台GUI开发能力,融合了面向对象编程、事件驱动机制、定时器控制和资源管理等核心技术。通过mainui.ui设计界面,C++代码实现蛇的移动、食物生成、碰撞检测等核心逻辑,并借助QRC资源文件管理和.pro项目配置完成编译构建。本实例涵盖从界面设计到游戏运行的完整流程,适合掌握QT5与C++在实际游戏开发中的综合应用。

Qt5与C++打造贪吃蛇:从零构建跨平台游戏的完整实践

你有没有试过在手机上玩一个简单的贪吃蛇,却突然被它的流畅动画和细腻交互惊艳到?别小看这“复古”小游戏——背后藏着现代GUI框架、事件驱动机制和状态管理的艺术。今天咱们就用 Qt5 + C++ ,亲手把这段经典搬进现实,不靠模板,不抄代码,一步步揭开它背后的工程逻辑。

准备好了吗?我们先来聊聊为什么选 Qt 这个“老将”。


说到跨平台 GUI 开发,很多人第一反应是 Electron 或者 Python 的 Tkinter。但如果你追求性能、原生体验和工业级稳定性,那 Qt 简直就是隐藏王者。尤其是在嵌入式设备、车载系统甚至医疗仪器中,Qt 几乎无处不在。而我们的目标——贪吃蛇,虽然简单,但它需要精确的定时控制、高效的图形渲染和灵敏的键盘响应,这些正是 Qt 最擅长的地方。

而且,Qt 不只是画界面那么简单。它有一套完整的 信号与槽机制 ,让你的对象之间通信变得像搭积木一样自然;还有强大的 元对象系统(Meta-Object System) ,让普通类也能动态绑定行为。这一切都通过 moc (元对象编译器)自动完成,开发者只需要写标准 C++ 代码,剩下的交给工具链。

举个例子:你想让点击按钮后蛇开始移动。传统做法可能是写一堆 if-else 判断消息类型,但在 Qt 里,一行 connect() 就搞定:

connect(startButton, &QPushButton::clicked, gameController, &GameController::startGame);

是不是清爽多了?这种解耦设计不仅让代码更易读,也为后期扩展留足空间——比如以后加个“网络对战”功能,根本不用动 UI 层。


那怎么搭建这个环境呢?推荐使用官方的 Qt Online Installer ,一路默认安装即可。重点是在组件选择页面勾上:
- Qt 5.15.2 MinGW 64-bit (适合大多数 Windows 用户)
- Qt Creator IDE
- Debugging Tools for Windows

装完打开 Qt Creator,进入 工具 → 选项 → 构建与运行 ,你会看到 Kits 配置面板。这里的关键是确认三件套匹配:
- 编译器:MinGW 9.0.0 64-bit
- 调试器:gdb(会自动识别)
- Qt 版本:指向你刚装的 Qt 5.15.2

✅ Kit 名建议命名为 Desktop Qt 5.15.2 MinGW 64-bit ,清晰明了。

一旦配置成功,新建项目时就能直接选用这个 Kit,再也不用手动调路径了 😎


接下来创建项目。名字就叫 gluttonous_snake 吧,记得路径不要有中文!选择 “Qt Widgets Application”,生成 .pro 文件后内容大概是这样:

QT += core gui widgets
TARGET = gluttonous_snake
TEMPLATE = app
SOURCES += main.cpp \
           mainwindow.cpp
HEADERS += mainwindow.h
FORMS += mainwindow.ui

别小看这几行,它们决定了整个项目的编译规则。比如 QT += widgets 表示启用 QWidget 模块,否则连窗口都打不开; FORMS += mainwindow.ui 则告诉 qmake 要调用 uic 工具把 UI 文件转成 C++ 代码。

说到这里,不得不提一句: .pro 文件其实是 qmake 的脚本语言,语法简洁但功能强大。如果你想开启 C++17 支持,只需加一行:

CONFIG += c++17

就这么简单。没有 XML 嵌套地狱,也没有 JSON 缩进烦恼,干净得像一杯白开水。


现在进入正题:如何用面向对象思想建模这个游戏?

很多初学者一上来就想画图、设颜色,结果逻辑乱成一团。其实应该反其道而行之——先定义清楚“有哪些东西”,再考虑“它们长什么样”。

在贪吃蛇的世界里,核心角色就三个:
1. 蛇(Snake)——会动,能长大
2. 食物(Food)——随机出现,被吃掉后重生
3. 游戏控制器(GameController)——统筹全局,判胜负

这三个完全可以抽象成独立的类,彼此职责分明。这才是真正的 OOP 思维:不是为了封装而封装,而是为了让复杂系统变得可维护。

🐍 蛇体怎么存?用 QList 最合适!

你可能会想:“蛇不就是一堆方块嘛,用数组就行。” 但问题是,蛇要前进——头往前伸一格,尾巴缩一格。如果用 std::vector ,每次删除首元素都要整体前移,效率 O(n),太慢!

QList<QPoint> 呢?尾插快,头删也快(Qt 内部做了优化),还自带索引访问。完美契合贪吃蛇的操作模式。

来看 Snake 类的设计:

class Snake {
public:
    enum Direction { Up, Down, Left, Right };

    Snake(int startX, int startY);
    void move();
    void grow();
    bool checkCollisionWithSelf() const;
    bool contains(const QPoint &pos) const;

    QList<QPoint> body() const { return m_body; }
    Direction direction() const { return m_direction; }
    void setDirection(Direction dir);

private:
    QList<QPoint> m_body;
    Direction m_direction;
};

注意到没? grow() 并不会立即增加身体长度,而是标记一下,等到下次 move() 时不删尾部就行了。这种“延迟增长”的设计非常巧妙,避免了在绘制时还要判断是否刚吃了食物。

至于方向控制,用了枚举而不是 int,杜绝了“传个 99 当方向”的低级错误。谁还没年轻时踩过这种坑呢😅

容器类型 插入效率 删除效率 查找效率 是否推荐
std::vector O(n) 头插慢 O(n) 头删慢 O(1)
std::deque O(1) 双端高效 O(1) 双端高效 O(1)
QList<QPoint> O(1) 尾插 O(1) 头删(实际为O(n),但Qt优化) O(1) ✅✅

最终我们选了 QList ,因为它和 Qt 其他组件配合得天衣无缝,调试起来也方便。

classDiagram
    class Snake {
        -QList~QPoint~ m_body
        -Direction m_direction
        +move()
        +grow()
        +checkCollisionWithSelf() bool
        +contains(QPoint) bool
        +setDirection(Direction)
    }

这张类图看着简单,实则体现了信息隐藏原则:外部只能通过接口操作蛇,看不到内部数据结构。万一哪天你要换成链表存储,只要接口不变,其他模块完全不受影响!


再说说食物(Food)。它看起来很简单,但也有讲究。

class Food {
public:
    Food(const QRect &boundary);
    void respawn();
    QPoint position() const { return m_pos; }

private:
    QPoint m_pos;
    QRect m_boundary;
};

关键在于 respawn() 函数。理想情况下,新食物不能生成在蛇身上。但如果让 Food 类自己去查 Snake 的身体位置,那就耦合了——Food 不该知道 Snake 的存在!

所以更好的做法是在 GameController 中统一协调:

void GameController::spawnFood() {
    do {
        m_food.respawn();
    } while (m_snake.contains(m_food.position()));
}

你看,这样 Food 依然保持独立,又能保证安全生成。这就是所谓的“高内聚、低耦合”。

flowchart TD
    A[开始生成食物] --> B{随机选取x,y}
    B --> C[检查是否在蛇体内]
    C -- 是 --> B
    C -- 否 --> D[设置新位置]
    D --> E[结束]

流程图画出来才发现,原来一个小功能背后也有这么多细节要考虑。


重头戏来了: GameController ——整个游戏的大脑🧠

它继承自 QObject ,这样才能使用信号与槽机制。结构如下:

class GameController : public QObject {
    Q_OBJECT

public:
    explicit GameController(QObject *parent = nullptr);
    void startGame();
    void pauseGame();
    void resetGame();

signals:
    void snakeMoved();
    void scoreChanged(int score);
    void gameOver();

private slots:
    void onTimeout();

private:
    Snake m_snake;
    Food m_food;
    QTimer *m_timer;
    int m_score;
    bool m_isRunning;

    void checkCollisions();
};

注意那个 Q_OBJECT 宏,它是魔法的起点。没有它,信号槽就不能工作。而 m_timer 是驱动游戏循环的心脏,通常设为每 150ms 触发一次 onTimeout()

碰撞检测是核心逻辑之一:

void GameController::checkCollisions() {
    const QPoint &head = m_snake.body().first();

    // 边界碰撞
    if (!m_gameArea.contains(head)) {
        emit gameOver();
        m_isRunning = false;
        return;
    }

    // 自咬
    if (m_snake.checkCollisionWithSelf()) {
        emit gameOver();
        m_isRunning = false;
        return;
    }

    // 吃食物
    if (head == m_food.position()) {
        m_snake.grow();
        m_food.respawn();
        m_score += 10;
        emit scoreChanged(m_score);
    }
}

所有判断都在这里集中处理,外面只管接收信号更新界面。这种分层架构,想想都觉得舒服~

classDiagram
    class GameController {
        -Snake m_snake
        -Food m_food
        -QTimer* m_timer
        -int m_score
        -bool m_isRunning
        +startGame()
        +pauseGame()
        +resetGame()
        +checkCollisions()
        -onTimeout()
    }

    GameController --> Snake : 使用
    GameController --> Food : 使用
    GameController ..> QTimer : 依赖

UI 设计部分,强烈推荐使用 Qt Designer ——所见即所得,拖拽控件就像搭乐高。

新建一个 mainui.ui 文件,往里面拖几个关键部件:
- QGraphicsView :主画布,用来画蛇和食物
- QPushButton ×3:开始、暂停、重启
- QLabel :显示分数和状态

命名规范很重要!建议统一格式:
- btnStart
- btnPause
- lblScore
- viewGame

这样你在代码里一眼就知道每个控件是干啥的,还能防止拼错名字导致空指针崩溃。

graph TD
    A[MainWindow] --> B[Central Widget]
    B --> C[Horizontal Splitter]
    C --> D[GraphicsView - 游戏画布]
    C --> E[Vertical Layout]
    E --> F[Start Button]
    E --> G[Pause Button]
    H[Restart Button]
    I[Status Label]

布局上采用左右分区:左边是动态游戏区,右边是操作按钮。中间用 QSplitter 分隔,用户可以自由调整比例,体验感拉满。


资源管理也不能马虎。图片、音效、字体这些静态文件,最好打包进可执行程序里,免得发布时漏拷文件。

Qt 提供了 .qrc 资源系统,超好用。新建 photo.qrc ,添加前缀 /images ,然后导入:
- snake_head.png
- food_apple.png
- background_tile.png

接着在 .pro 文件里声明:

RESOURCES += photo.qrc

编译时, rcc 工具会把这些资源编译成二进制数据,嵌入到 exe 中。加载时只需:

QPixmap pix(":/images/snake_head.png");

路径以 :/ 开头,表示来自资源系统。再也不用担心相对路径出错了!

样式美化也可以用 QSS(Qt Style Sheets),语法几乎和 CSS 一样:

QString styleSheet = R"(
    QMainWindow {
        background-color: #2b2b2b;
    }
    QPushButton {
        background-color: #4CAF50;
        color: white;
        border: none;
        padding: 10px;
        font-weight: bold;
        border-radius: 8px;
    }
    QPushButton:hover {
        background-color: #45a049;
    }
)";
this->setStyleSheet(styleSheet);

圆角按钮、悬停变色……现代感瞬间就有了✨

classDiagram
    class QPixmap {
        +load(QString path)
        +scaled(int w, int h)
        +isNull() bool
    }

    class QIcon {
        +QIcon(QString resourcePath)
    }

    class QGraphicsPixmapItem {
        +setPixmap(QPixmap)
        +setPos(qreal x, qreal y)
    }

    QPixmap --> QIcon : 构造输入
    QPixmap --> QGraphicsPixmapItem : 构造输入

到了联动环节,才是真正考验功力的时候。

我们需要把 UI 和逻辑串起来。主窗口类通常多重继承:

class MainWindow : public QMainWindow, private Ui::MainWindow {
    Q_OBJECT
};

构造函数里调 setupUi(this) 加载界面,然后连接信号槽:

connect(ui->btnStart, &QPushButton::clicked, this, &MainWindow::onGameStart);

或者更酷一点,用 Qt 的自动连接机制:只要槽函数名叫 on_btnStart_clicked() ,就会自动绑定,连 connect 都不用写!

键盘事件也要重写:

void MainWindow::keyPressEvent(QKeyEvent *event) {
    switch (event->key()) {
    case Qt::Key_Up:
    case Qt::Key_W:
        m_controller->setDirection(Snake::Up);
        break;
    // ...
    }
    event->accept();
}

但要注意防反向:不能让蛇立刻回头撞死自己。这个校验必须放在 GameController 里做,而不是 UI 层:

bool GameController::isValidDirectionChange(Snake::Direction newDir) {
    auto oldDir = m_snake.direction();
    return !((oldDir == Snake::Up && newDir == Snake::Down) ||
             (oldDir == Snake::Left && newDir == Snake::Right));
}

否则你就等于把业务逻辑泄露到了视图层,以后改规则还得改 UI,岂不是很麻烦?


游戏节奏由 QTimer 控制:

m_timer = new QTimer(this);
connect(m_timer, &QTimer::timeout, this, &MainWindow::updateScene);
m_timer->start(150); // 每150ms刷新一次

帧率不宜太快或太慢。150ms ≈ 6.67 FPS,刚好适合贪吃蛇的经典节奏。当然你可以随着得分提高逐渐加速,制造挑战感:

if (++m_foodEaten % 3 == 0 && m_interval > 50) {
    m_interval -= 10;
    m_timer->start(m_interval);
}

从 150ms 慢慢降到 50ms,速度翻三倍,刺激程度直线飙升!


最后是状态管理和发布部署。

我们可以用枚举定义四种状态:

enum class GameState {
    Initial, Running, Paused, GameOver
};

并在状态变化时发出信号:

emit gameStateChanged(GameState::GameOver);

UI 层监听并做出相应反应:
- 显示 Game Over 对话框
- 禁用按钮
- 停止接收键盘输入

分数系统也很重要。除了实时显示当前分,还可以用 QSettings 存储最高分:

QSettings settings("MyStudio", "GluttonousSnake");
settings.setValue("bestScore", m_score);

下次启动自动加载,成就感满满🏆

部署阶段,Windows 上运行:

windeployqt --dir ./dist --release GluttonousSnake.exe

它会自动复制所有依赖 DLL,包括 Qt5Core.dll , platforms/qwindows.dll 等。Linux 用 ldd 查依赖,macOS 用 macdeployqt 打包 .app

最终目录结构大概长这样:

dist/
├── GluttonousSnake.exe
├── platforms/
│   └── qwindows.dll
├── Qt5Core.dll
├── Qt5Gui.dll
└── photo.rcc

干净利落,拿出去就能玩!

graph TD
    A[启动程序] --> B[创建QApplication]
    B --> C[加载UI资源]
    C --> D[显示主窗口]
    D --> E{用户点击"开始"?}
    E -- 是 --> F[进入Running状态]
    F --> G[启动QTimer]
    G --> H[周期性更新蛇位置]
    H --> I{碰撞检测}
    I -- 成功吃到食物 --> J[加分并可能提速]
    I -- 触边或自咬 --> K[切换至GameOver状态]
    K --> L[弹出对话框]
    E -- 暂停 --> M[停止定时器]
    M --> N[冻结画面]
    N --> O{用户点击"继续"?}
    O -- 是 --> F

整个流程环环相扣,没有任何冗余步骤。


回过头看,这不仅仅是一个小游戏,更是一次完整的软件工程实践。我们用了:
- 面向对象设计拆解复杂问题
- MVC 思想分离 UI 与逻辑
- 信号槽实现松耦合通信
- 资源系统保障发布一致性
- 状态机确保行为可控

每一个决策都不是凭空而来,而是基于真实场景的权衡。

也许有一天,你会接到需求:“把这个游戏移植到车载屏幕上。” 到那时你会发现,当初那些看似“过度设计”的封装,反而成了快速适配的秘密武器。

毕竟, 好的代码不是写出来能跑就行,而是为未来而生 🚀

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:使用C++与QT5框架开发贪吃蛇小游戏是学习图形界面编程与基础游戏逻辑设计的经典案例。该项目基于QT强大的跨平台GUI开发能力,融合了面向对象编程、事件驱动机制、定时器控制和资源管理等核心技术。通过mainui.ui设计界面,C++代码实现蛇的移动、食物生成、碰撞检测等核心逻辑,并借助QRC资源文件管理和.pro项目配置完成编译构建。本实例涵盖从界面设计到游戏运行的完整流程,适合掌握QT5与C++在实际游戏开发中的综合应用。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐