【Qt】探索Qt框架:开发经典贪吃蛇游戏的全过程与实践

引言

贪吃蛇游戏是一款历史悠久且广受欢迎的经典电子游戏,最早可以追溯到1976年的"Snake Game"。它在1997年作为诺基亚手机的内置游戏而变得家喻户晓,其简单的操作和上瘾的游戏性迅速赢得了全球玩家的喜爱。贪吃蛇的流行度不仅体现在其在手机平台上的普及,还扩展到了个人电脑、游戏机等其他平台,成为跨时代的电子游戏代表作。

使用Qt框架开发贪吃蛇游戏具有多方面的优势。Qt是一个功能强大且跨平台的应用程序开发框架,它提供了丰富的GUI控件和工具,使得用户界面的设计变得直观和高效。Qt的事件驱动架构和网络功能为实现响应式控制和多人在线游戏提供了强大支持。此外,Qt的开源特性和活跃的社区支持,为开发者提供了大量的资源和文档,降低了开发难度和成本。Qt的性能优化和集成开发环境也极大提升了开发效率,使得使用Qt开发贪吃蛇游戏成为一个理想的选择。

项目链接:

源代码:gitee:https://gitee.com/q-haodong/test_-qt/tree/master/Qt/greedy_snake

效果演示视频https://live.csdn.net/v/409583?spm=1001.2014.3001.5501

Qt贪吃蛇游戏

1. Qt框架的使用简介

在Qt贪吃蛇代码中,Qt的基本组件和模块主要包括:

  • QWidget: 作为大多数Qt控件的基类,QWidget代表了一个窗口或窗口内部的一个区域,可以包含其他控件和进行绘图。
  • QMainWindow: 表示主窗口,是大多数应用程序的顶层窗口。
  • QDialog: 用于创建对话框,用于用户交互。
  • QPushButton: 用于创建按钮控件,用户可以点击触发事件。在代码中,按钮用于开始游戏、选择游戏难度、返回主界面等操作。
  • QPainter: 用于绘制图形界面的2D图形类。在paintEvent函数中使用,用于绘制游戏界面、蛇的身体、食物等。
  • QIcon: 用于创建和处理图标,设置应用程序或控件的图标。
  • QTimer: 用于创建定时器,可以周期性地触发事件,如游戏中蛇的移动。
  • QSound: 用于播放声音效果,增强用户体验。
  • QFont: 用于定义字体的外观,如字体大小、风格等。
  • QMessageBox: 用于显示警告框或其他消息,与用户进行交互。
  • QFileQTextStream: 用于文件操作,如读取或写入数据。在游戏代码中,用于记录游戏分数。
  • QShortcut: 允许定义快捷键,使用户可以使用键盘快捷方式执行操作,如使用箭头键控制蛇的方向。
  • QPainterQPixmap: 一起使用来绘制图像。QPixmap是一个图像对象,QPainter用于绘制这个图像。
  • QListQRectF: QList是一个容器,用于存储列表数据;QRectF代表一个矩形区域,用于定义蛇的身体节点。
  • QApplication: 每个Qt应用程序的主对象,负责处理应用程序的控制流程。
  • 信号和槽机制: Qt的信号和槽是其核心特性之一,用于对象间的通信。例如,按钮的clicked信号可以连接到自定义槽函数,当按钮被点击时执行特定的操作。

2. 贪吃蛇游戏设计

2.1 游戏规则和玩法介绍

贪吃蛇是一款经典的电子游戏,其基本规则和玩法如下:

  • 目标:玩家控制一条蛇在游戏区域内移动,目标是吃掉随机出现的食物。
  • 控制:通常使用键盘的箭头键来控制蛇头的移动方向,上、下、左、右。
  • 成长:每吃掉一个食物,蛇的长度就会增加一节。
  • 障碍:蛇不能撞到自己的身体,否则游戏结束。
  • 得分:玩家通过吃掉食物获得分数,通常是根据食物的数量来累计得分。

2.2 游戏界面设计概述

贪吃蛇游戏的界面设计通常包括以下几个部分:

  1. 游戏区域:这是显示蛇和食物的主要区域,通常是一个矩形的网格。
  2. 开始/暂停按钮:允许玩家开始游戏或在游戏进行中暂停。
  3. 退出按钮:玩家可以通过点击退出按钮来结束当前游戏。
  4. 分数显示:显示玩家当前的得分,通常位于游戏区域的上方或角落。
  5. 游戏难度选择:可能包括不同难度级别的选择,影响食物的出现频率或蛇的移动速度。
  6. 历史战绩:显示玩家过去游戏的最高分或其他历史记录。
  7. 声音效果:游戏中可能会包含点击按钮的声音反馈和游戏结束时的特殊音效。

这些界面元素通过Qt的各种组件实现,例如使用QPushButton来创建按钮,使用QPainterQPixmap来绘制游戏区域的图形,以及使用QTimer来控制游戏的刷新率和蛇的移动速度。此外,界面的布局和样式通过Qt的布局管理器和样式表来设计,确保界面的美观和用户友好性。

3. 核心代码解析

3.1 主界面(GameHall)

3.1.1 布局和功能介绍

GameHall 类代表贪吃蛇游戏的主界面,它为玩家提供了进入游戏的起点。以下是主界面的主要布局和功能:

  • 窗口尺寸:主界面设置为固定大小,例如1000x800像素,提供一个足够大的显示区域。
  • 窗口图标:设置了一个窗口图标,增强了应用程序的专业外观。
  • 窗口标题:显示窗口的标题,例如“贪吃蛇游戏”,告知用户应用程序的名称。
  • 开始游戏按钮:界面上有一个明显的“开始游戏”按钮,这是玩家进入游戏的主要入口。按钮具有工具提示,说明其功能和快捷操作方式。
  • 绘图区域:通过覆盖paintEvent函数,主界面可以包含自定义的绘图逻辑,用于显示背景图像或游戏厅的静态画面。
    在这里插入图片描述

3.1.2 代码实现分析

在代码层面,GameHall 类的实现涉及以下几个关键部分:

  1. 构造函数 (GameHall::GameHall(QWidget *parent)): 初始化主界面,设置窗口大小、图标和标题。使用Ui::GameHall命名空间中的类来加载和设置用户界面布局。
GameHall::GameHall(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::GameHall)
{
    ui->setupUi(this);

    this->setFixedSize(1000, 800); //设置窗口大小
    this->setWindowIcon(QIcon(":res/ico.jpg")); // 设置窗口图标
    this->setWindowTitle("贪吃蛇游戏");

    QPushButton* strBtn = new QPushButton(this);
    strBtn->move(410, 500);
    strBtn->setText("开始游戏");
    strBtn->setToolTip("这是游戏开始按钮,鼠标点击或者回车键进入!");
    strBtn->setToolTipDuration(3000);
    ButtonStyle::setCommonStyle_green(strBtn, 30);

    // 为按钮设置回车快捷键
    strBtn->setShortcut(Qt::Key_Return);

    GameSelect* gameSelect = new GameSelect;
    connect(strBtn, &QPushButton::clicked, [=](){
        QSound::play(":res/clicked.wav");
        this->close();
        gameSelect->setGeometry(this->geometry()); // 第二个窗口和第一个窗口一样大
        gameSelect->show();
    });

}

  1. 按钮创建与布局:创建一个QPushButton实例,设置其位置、文本、工具提示和样式。按钮的位置使用move方法进行定位,文本设置为“开始游戏”,工具提示提供了额外的使用说明。

  2. 按钮样式设置:使用ButtonStyle::setCommonStyle_green函数来设置按钮的样式,例如背景颜色、边框、字体大小等。这为按钮提供了统一的外观和感觉。

  3. 信号连接:通过Qt的信号和槽机制,将“开始游戏”按钮的clicked信号连接到一个槽函数,当按钮被点击时执行。在这个槽函数中,播放点击声音,关闭当前的主界面,并展示GameSelect窗口,这是选择游戏模式的界面。

  4. 绘图事件处理 (GameHall::paintEvent(QPaintEvent *event)): 重写paintEvent以自定义绘图逻辑。使用QPainterQPixmap在窗口上绘制背景图像,创建更加吸引人的视觉效果。

void GameHall::paintEvent(QPaintEvent *event)
{
     Q_UNUSED(event); // 告诉编译器event参数是未使用的
    // 实例化画家
    QPainter painter(this);

    // 实例化绘图设备
    QPixmap pix(":res/game_hall.jpg");

    // 绘画
    painter.drawPixmap(0,0, this->width(), this->height(), pix);
}
  1. 析构函数 (GameHall::~GameHall()): 清理并删除由GameHall类分配的资源,特别是ui成员,这是用户界面的布局对象。
GameHall::~GameHall()
{
    delete ui;
}

3.2 游戏选择界面(GameSelect)

3.2.1 功能介绍

GameSelect 类构成了贪吃蛇游戏的选择界面,允许玩家根据自己的喜好选择不同的游戏难度。这个界面通常包含以下几个关键功能:

  • 窗口尺寸与图标:设置窗口的大小和图标,与主界面类似,提供了一致的用户体验。
  • 游戏模式按钮:提供了不同难度级别的按钮,如“简单模式”、“常规模式”和“困难模式”,每个按钮都对应一个特定的蛇移动速度。
  • 历史战绩按钮:允许玩家查看他们之前的游戏得分记录。
  • 返回按钮:一个返回到主界面的按钮,允许玩家在任何时候返回到游戏的起点。
    在这里插入图片描述

3.2.2 代码实现分析

在代码层面,GameSelect 类的实现涉及以下关键部分:

  1. 构造函数 (GameSelect::GameSelect(QWidget *parent)): 初始化游戏选择界面,设置窗口的尺寸、图标和标题。
GameSelect::GameSelect(QWidget *parent) : QWidget(parent)
{
    this->setFixedSize(1000, 800); // 设置窗口大小
    this->setWindowIcon(QIcon(":res/ico.jpg"));
    this->setWindowTitle("游戏选择界面");


    // 创建并设置返回键的位置
    QPushButton* backBtn = new QPushButton(this);
    backBtn->move(80, 50);
    backBtn->setIcon(QIcon(":res/back.png"));
    backBtn->setToolTip("点击此按钮或按键盘Esc键返回游戏大厅界面");
    backBtn->setShortcut(Qt::Key_Escape);
    backBtn->setIconSize(QSize(50, 50)); // 设置图标大小

    // 设置样式表
    ButtonStyle::setCommonStyle_write(backBtn, 26);

    connect(backBtn, &QPushButton::clicked, [=](){
        this->close();
        QSound::play(":res/clicked.wav");
        GameHall* gameHall = new GameHall;
        gameHall->show();
    });


    GameRoom* gameRoom = new GameRoom;

    QPushButton* simpleBtn = new QPushButton(this);
    simpleBtn->move(420, 300);
    simpleBtn->setText("简单模式");
    simpleBtn->setToolTip("点击此按钮或按数字键1进入简单模式");
    simpleBtn->setShortcut(Qt::Key_1);
    ButtonStyle::setCommonStyle_green(simpleBtn, 26);
    connect(simpleBtn, &QPushButton::clicked, [=](){
        QSound::play(":res/clicked.wav");
        this->close();
        gameRoom->setGeometry(this->geometry());
        gameRoom->show();

        gameRoom->setTimeout(300);
    });


    QPushButton* normalBtn = new QPushButton(this);
    normalBtn->move(420, 380);
    normalBtn->setText("常规模式");
    normalBtn->setToolTip("点击此按钮或按数字键2进入常规模式");
    normalBtn->setShortcut(Qt::Key_2);
    ButtonStyle::setCommonStyle_green(normalBtn, 26);
    connect(normalBtn, &QPushButton::clicked, [=](){
        QSound::play(":res/clicked.wav");
        this->close();
        gameRoom->setGeometry(this->geometry());
        gameRoom->show();

        gameRoom->setTimeout(200);
    });


    QPushButton* hardBtn = new QPushButton(this);
    hardBtn->move(420, 460);
    hardBtn->setText("困难模式");
    hardBtn->setToolTip("点击此按钮或按数字键3进入困难模式");
    hardBtn->setShortcut(Qt::Key_3);
    ButtonStyle::setCommonStyle_green(hardBtn, 26);
    connect(hardBtn, &QPushButton::clicked, [=](){
        QSound::play(":res/clicked.wav");
        this->close();
        gameRoom->setGeometry(this->geometry());
        gameRoom->show();

        gameRoom->setTimeout(100);
    });

    QPushButton* hisBtn = new QPushButton(this);
    hisBtn->move(420, 560);
    hisBtn->setText("历史战绩");
    hisBtn->setToolTip("点击此按钮或按键盘H查看历史记录");
    hisBtn->setShortcut(Qt::Key_H);
    ButtonStyle::setCommonStyle_write(hisBtn, 26);

    connect(hisBtn, &QPushButton::clicked, [=](){
        QWidget* widget = new QWidget;
        widget->setWindowTitle("历史战绩");
        widget->setFixedSize(500, 300);

        QTextEdit* edit = new QTextEdit(widget);
        QFont font("微软雅黑", 24);
        edit->setFont(font);
        edit->setFixedSize(500, 300);

        QFile file("D:/Acode/class111/test_-qt/Qt/greedy_snake/snake/history.txt");
        file.open(QIODevice::ReadOnly);

        QTextStream in(&file);
        int data = in.readLine().toInt();

        edit->append("得分为: ");
        edit->append(QString::number(data));
        widget->show();
    });

}
  1. 按钮创建:为每种游戏难度创建QPushButton实例,并设置它们的位置、文本、工具提示和快捷键。这些按钮允许玩家通过点击或按下相应的数字键来选择游戏模式。

  2. 按钮样式设置:使用ButtonStyle::setCommonStyle_green函数为游戏模式按钮设置样式,使用不同的颜色和字体大小来区分按钮。

  3. 信号连接:将每个游戏模式按钮的clicked信号连接到槽函数。在这些槽函数中,播放点击声音,关闭当前的选择界面,并根据所选难度设置蛇的移动速度,然后启动游戏房间界面。

  4. 历史战绩功能:为历史战绩按钮设置信号连接,当点击时,读取存储在文件中的最高分,并在一个新窗口中显示。

  5. 绘图事件处理 (GameSelect::paintEvent(QPaintEvent *event)): 重写paintEvent以在界面上绘制背景图像,增强视觉效果。

void GameSelect::paintEvent(QPaintEvent *event)
{
    Q_UNUSED(event);

    QPainter painter(this); // 实例化画图对象

    QPixmap pix(":res/game_select.jpg");
    painter.drawPixmap(0, 0, this->width(), this->height(), pix);
}
  1. 快捷键设置:为返回按钮和每个游戏模式按钮设置了快捷键,提供了快捷的操作方式。

  2. 声音效果:在界面转换和按钮点击时播放声音效果,增强了交互体验。

3.3 游戏房间(GameRoom)

3.3.1 游戏逻辑和核心算法解析

GameRoom 类是实现贪吃蛇游戏逻辑的核心,包括蛇的移动、食物生成和游戏失败条件的检测。

  1. 初始化蛇的状态:在构造函数中,初始化蛇的位置和状态,蛇的身体由一个QList<QRectF>来表示,其中QRectF是定义蛇身体每个节点的矩形区域。

  2. 食物生成:通过createNewFood()函数在游戏区域内随机生成食物,食物也是一个QRectF对象。

  3. 蛇的移动:蛇的移动通过moveUp()moveDown()moveLeft()moveRight()函数实现,这些函数根据蛇的当前移动方向来更新蛇身体的位置。

  4. 游戏循环:使用QTimer来控制游戏循环和蛇的移动频率。timertimeout信号触发时,蛇会根据当前方向移动,并且检查是否吃掉食物或撞到自己或墙壁。

  5. 游戏失败条件:通过checkFail()函数检查蛇是否撞到自己或墙壁,如果是,则游戏结束。

  6. 绘制游戏元素:在paintEvent()中使用QPainterQPixmap绘制蛇的身体、食物和游戏背景。
    在这里插入图片描述

3.3.2 代码实现细节

  1. 构造函数 (GameRoom::GameRoom(QWidget *parent)): 初始化游戏房间,设置窗口大小、图标和标题,初始化蛇的位置,创建食物和定时器。
GameRoom::GameRoom(QWidget *parent) : QWidget(parent)
{
    this->setFixedSize(1000, 800); //设置窗口大小
    this->setWindowIcon(QIcon(":res/ico.jpg")); // 设置窗口图标
    this->setWindowTitle("贪吃蛇游戏");

    // 初始化贪吃蛇
    snakeList.push_back(QRectF(this->width() / 2, this->height() / 2, kSnakeNodeWidth, kSnakeNodeHeight));

    moveUp();
    moveUp();

    // 创建食物
    createNewFood();

    timer = new QTimer(this);

    connect(timer, &QTimer::timeout, [=](){
        int cnt = 1; // 默认移动一个
        if (snakeList.front().intersects(foodRect)) // 判断蛇头是否与蛇相交
        {
            createNewFood();
            cnt++; // 有食物相交的话移动2个
        }

        while(cnt--)
        {
            switch (moveDirect) {
            case SnakeDirect::UP:
                moveUp();
                break;
            case SnakeDirect::DOWN:
                moveDown();
                break;
            case SnakeDirect::LEFT:
                moveLeft();
                break;
            case SnakeDirect::RIGHT:
                moveRight();
                break;
            default:
                break;
            }
        }

        snakeList.pop_back(); // 删除最后一个节点
        update();
    });

    // 开始游戏 暂停游戏
    QPushButton* strBtn = new QPushButton(this);
    strBtn->move(840, 120);
    strBtn->setText("开始/暂停");
    strBtn->setToolTip("点击此按钮或按键盘P键,切换游戏开始和暂停");
    strBtn->setShortcut(Qt::Key_P);
    ButtonStyle::setCommonStyle_green(strBtn, 20);

    connect(strBtn, &QPushButton::clicked, [=](){
        if (isGameStart == false)
        {
            isGameStart = true;
            timer->start(moveTimeout);
            sound = new QSound(":res/Trepak.wav");
            sound->play();
            sound->setLoops(-1);
        }
        else
        {
            isGameStart = false;
            timer->stop();
            sound->stop();
        }
    });

    QPushButton* backBtn = new QPushButton(this);
    backBtn->move(855, 200);
    backBtn->setIcon(QIcon(":res/back.png"));
    backBtn->setToolTip("点击此按钮或按键盘Esc键返回游戏大厅界面");
    backBtn->setShortcut(Qt::Key_Escape);
    backBtn->setIconSize(QSize(50, 50)); // 设置图标大小
    // 设置样式表
    ButtonStyle::setCommonStyle_write(backBtn, 26);
    connect(backBtn, &QPushButton::clicked, [=](){
        this->close();
        QSound::play(":res/clicked.wav");
        GameSelect* gameselect = new GameSelect;
        gameselect->show();

        if (isGameStart == true) {
            isGameStart = false;
            timer->stop();
            sound->stop();
        }
    });


    // 方向控制
    QPushButton* up = new QPushButton(this);
    QPushButton* down = new QPushButton(this);
    QPushButton* left = new QPushButton(this);
    QPushButton* right = new QPushButton(this);

    // 为每个按钮创建快捷键
    QShortcut* upShortcut = new QShortcut(QKeySequence(Qt::Key_Up), this);
    QShortcut* downShortcut = new QShortcut(QKeySequence(Qt::Key_Down), this);
    QShortcut* leftShortcut = new QShortcut(QKeySequence(Qt::Key_Left), this);
    QShortcut* rightShortcut = new QShortcut(QKeySequence(Qt::Key_Right), this);

    // 连接快捷键的activated信号到按钮的clicked信号
    connect(upShortcut, SIGNAL(activated()), up, SLOT(animateClick()));
    connect(downShortcut, SIGNAL(activated()), down, SLOT(animateClick()));
    connect(leftShortcut, SIGNAL(activated()), left, SLOT(animateClick()));
    connect(rightShortcut, SIGNAL(activated()), right, SLOT(animateClick()));


    up->move(875, 405);
    down->move(875,455);
    left->move(810,455);
    right->move(930,455);

    up->setText("↑");
    ButtonStyle::setCommonStyle_green(up,20);

    down->setText("↓");
    ButtonStyle::setCommonStyle_green(down,20);

    left->setText("←");
    ButtonStyle::setCommonStyle_green(left,20);

    right->setText("→");
    ButtonStyle::setCommonStyle_green(right,20);

    connect(up, &QPushButton::clicked, [=](){
        if (moveDirect != SnakeDirect::DOWN)
        {
            moveDirect = SnakeDirect::UP;
        }
    });

    connect(down, &QPushButton::clicked,[=](){
        if (moveDirect != SnakeDirect::UP)
        {
            moveDirect = SnakeDirect::DOWN;
        }
    });

    connect(left, &QPushButton::clicked, [=](){
        if (moveDirect != SnakeDirect::RIGHT)
        {
            moveDirect = SnakeDirect::LEFT;
        }
    });

    connect(right, &QPushButton::clicked, [=](){
        if (moveDirect != SnakeDirect::LEFT)
        {
            moveDirect = SnakeDirect::RIGHT;
        }
    });


    // 退出游戏
    QPushButton* ExitBtn = new QPushButton(this);
    ExitBtn->move(860, 700);
    ExitBtn->setText("退出");
    ButtonStyle::setCommonStyle_green(ExitBtn, 20);

    QMessageBox* messageBox = new QMessageBox(this);
    QPushButton* ok = new QPushButton("ok");
    QPushButton* cancel = new QPushButton("cancel");

    messageBox->addButton(ok, QMessageBox::AcceptRole);
    messageBox->addButton(cancel, QMessageBox::RejectRole);

    messageBox->setWindowTitle("警告");
    messageBox->setText("是否确认退出!");
    messageBox->setIcon(QMessageBox::Warning);



    connect(ExitBtn, &QPushButton::clicked, [=](){
        messageBox->show();
        messageBox->exec(); // 事件轮询
        QSound::play(":res/clicked.wav");

        if(messageBox->clickedButton() == ok)
        {
            this->close();
        }
        else
        {
            messageBox->close();
        }
    });
}
  1. 蛇的移动
    移动函数通过计算蛇头的新位置并更新snakeList来实现蛇的移动逻辑。
    移动后,蛇的尾部节点被移除,模拟蛇的移动效果。
void GameRoom::moveUp()
{
    QPointF leftTop; // 左上角坐标
    QPointF rightBottom; // 右下角坐标

    auto snakeNode = snakeList.front(); //头
    int headX = snakeNode.x();
    int headY = snakeNode.y();

    if (headY < 0) // 穿墙了
    {
        leftTop = QPointF(headX, this->height() - kSnakeNodeHeight);
    }
    else
    {
        leftTop = QPointF(headX, headY - kSnakeNodeHeight);
    }

    rightBottom = leftTop + QPointF(kSnakeNodeWidth, kSnakeNodeHeight);

    snakeList.push_front(QRectF(leftTop, rightBottom));
}
  1. 食物生成
    createNewFood()函数在随机位置生成食物,确保食物不会与蛇的身体重叠。
void GameRoom::createNewFood()
{
    foodRect = QRectF(qrand() % (800/ kSnakeNodeWidth) * kSnakeNodeWidth,
                      qrand() % (this->height() / kSnakeNodeHeight) * kSnakeNodeHeight,
                      kSnakeNodeWidth,
                      kSnakeNodeHeight);
}
  1. 定时器和游戏循环
    通过QTimertimeout信号,周期性地触发蛇的移动。
    如果蛇吃掉食物,增加蛇的长度并生成新的食物。
// 创建食物
createNewFood();

timer = new QTimer(this);

connect(timer, &QTimer::timeout, [=](){
    int cnt = 1; // 默认移动一个
    if (snakeList.front().intersects(foodRect)) // 判断蛇头是否与蛇相交
    {
        createNewFood();
        cnt++; // 有食物相交的话移动2个
    }

    while(cnt--)
    {
        switch (moveDirect) {
        case SnakeDirect::UP:
            moveUp();
            break;
        case SnakeDirect::DOWN:
            moveDown();
            break;
        case SnakeDirect::LEFT:
            moveLeft();
            break;
        case SnakeDirect::RIGHT:
            moveRight();
            break;
        default:
            break;
        }
    }

    snakeList.pop_back(); // 删除最后一个节点
    update();
});
  1. 游戏失败条件
    checkFail()函数通过比较蛇的每个节点来检测是否有自我碰撞或撞墙的情况。
bool GameRoom::checkFail()
{
    // 遍历蛇的身体节点,检查是否有重叠
    for (int i = 0; i < snakeList.size(); ++i)
    {
        for (int j = i + 1; j < snakeList.size(); ++j)
        {
            if (snakeList.at(i) == snakeList.at(j))
            {
                // 发现重叠,游戏失败
                return true;
            }
        }
    }
    // 没有发现重叠,游戏继续
    return false;
}
  1. 绘制逻辑 (GameRoom::paintEvent(QPaintEvent *event)):
    使用QPainter绘制游戏背景、蛇的身体、食物和分数。
    根据蛇的当前移动方向选择相应的蛇头图像。
void GameRoom::paintEvent(QPaintEvent *event)
{
    Q_UNUSED(event);
    QPainter painter(this); // 实例化画家对象
    QPixmap pix;

    pix.load(":res/game_room.jpg");

    painter.drawPixmap(0, 0, 800, 800, pix);

    pix.load(":res/bg1.jpeg");
    painter.drawPixmap(800, 0, 200, 800, pix);

    // 绘制蛇:蛇头 + 蛇身体 + 蛇尾巴
    // 绘制蛇头:上 下 左 右
    if(moveDirect == SnakeDirect::UP)
    {
        pix.load(":res/up.png");
    }
    else if (moveDirect == SnakeDirect::DOWN)
    {
        pix.load(":res/down.png");
    }
    else if (moveDirect == SnakeDirect::LEFT)
    {
        pix.load(":res/left.png");
    }
    else if (moveDirect == SnakeDirect::RIGHT)
    {
        pix.load(":res/right.png");
    }

    auto snakeHead = snakeList.front();
    painter.drawPixmap(snakeHead.x(), snakeHead.y(), snakeHead.width(), snakeHead.height(), pix);

    // 绘制蛇身
    pix.load(":res/Bd.png");
    for (int i = 1; i < snakeList.size()-1; ++i)
    {
        auto node = snakeList.at(i);
        painter.drawPixmap(node.x(), node.y(), node.width(), node.height(), pix);
    }

    // 绘制蛇尾巴
    auto snakeTail = snakeList.back();
    painter.drawPixmap(snakeTail.x(), snakeTail.y(), snakeTail.width(), snakeTail.height(), pix);

    // 绘制食物
    pix.load(":res/food.png");
    painter.drawPixmap(foodRect.x(), foodRect.y(), foodRect.width(), foodRect.height(), pix);

    // 绘制分数
    pix.load(":res/sorce_bg.png");
    painter.drawPixmap(this->width()*0.85, this->height()*0.06, 90, 40, pix);

    QPen pen;
    pen.setColor(Qt::black);
    painter.setPen(pen);
    QFont font("微软雅黑");
    font.setPixelSize(24); // 设置字体大小为20像素
    painter.setFont(font);
    painter.drawText(this->width()*0.9, this->height()*0.1, QString("%1").arg(snakeList.size()));


    // 往文件中写分数
    int c = snakeList.size();
    QFile file("D:/Acode/class111/test_-qt/Qt/greedy_snake/snake/history.txt");
    if (file.open(QIODevice::WriteOnly | QIODevice::Text))
    {
        QTextStream out(&file);
        out << c;
        qDebug() << c;
        file.close();
    } else {
         qDebug() << "文件打开失败";
    }

    // 绘制游戏失败效果
    if (checkFail())
    {
        pen.setColor(Qt::red);
        painter.setPen(pen);
        QFont font("微软雅黑", 50);
        painter.drawText(this->width()*0.45, this->height()*0.5, QString("GAME OVER!"));
        timer->stop();
        QSound::play(":res/gameover.wav");
    }
}
  1. 分数和游戏状态显示
    在游戏界面上绘制当前分数,通常基于蛇的长度。
    如果游戏失败,显示“GAME OVER!”并停止游戏循环。
    // 往文件中写分数
    int c = snakeList.size();
    QFile file("D:/Acode/class111/test_-qt/Qt/greedy_snake/snake/history.txt");
    if (file.open(QIODevice::WriteOnly | QIODevice::Text))
    {
        QTextStream out(&file);
        out << c;
        qDebug() << c;
        file.close();
    } else {
         qDebug() << "文件打开失败";
    }

    // 绘制游戏失败效果
    if (checkFail())
    {
        pen.setColor(Qt::red);
        painter.setPen(pen);
        QFont font("微软雅黑", 50);
        painter.drawText(this->width()*0.45, this->height()*0.5, QString("GAME OVER!"));
        timer->stop();
        QSound::play(":res/gameover.wav");
    }
}

3.4 按钮样式(ButtonStyle)

3.4.1 代码实现

  1. 类定义 (buttonstyle.h): 声明了两个静态方法,用于设置不同风格的按钮样式。
class ButtonStyle {
public:
    static void setCommonStyle_green(QPushButton *button, size_t font_size);
    static void setCommonStyle_write(QPushButton *button, size_t font_size);
};
  1. 样式设置 (buttonstyle.cpp): 实现了上述声明的方法,使用 Qt 的样式表(styleSheet)来定义按钮的视觉样式。
void ButtonStyle::setCommonStyle_green(QPushButton *button, size_t font_size) {
    QString styleSheet = R"(
        QPushButton {
            background-color: #4CAF50;
            border: 2px solid #4CAF50;
            border-radius: 10px;
            font: bold %1px '微软雅黑';
            color: white;
            padding: 8px;
            padding-left: 20px;
            padding-right: 20px;
        }
        QPushButton:hover {
            background-color: #45a049;
        }
        QPushButton:pressed {
            background-color: #3e8e41;
        }
    )";
    button->setStyleSheet(styleSheet.arg(font_size));
}

void ButtonStyle::setCommonStyle_write(QPushButton *button, size_t font_size) {
    QString styleSheet = R"(
        QPushButton {
            font: bold %1px '微软雅黑';
            border: 2px solid #a3a3a3;
            border-radius: 16px;
            background-color: rgba(255, 255, 255, 0);
            padding: 8px;
            padding-left: 20px;
            padding-right: 20px;
        }
        QPushButton:hover {
            background-color: rgba(180, 180, 180, 0.3);
        }
        QPushButton:pressed {
            background-color: rgba(180, 180, 180, 0.4);
        }
    )";
    button->setStyleSheet(styleSheet.arg(font_size));
}

3.4.2 实现效果

  1. 绿色按钮样式 (setCommonStyle_green): 这种方法设置了一个绿色主题的按钮样式,具有以下特点:
    背景颜色为绿色(#4CAF50)。
    边框为绿色,宽度为2px。
    字体为粗体,使用微软雅黑字体,字体大小由传入的 font_size 参数决定。
    按钮文本颜色为白色。
    按钮具有圆角和内边距。
    当鼠标悬停在按钮上时,背景颜色会变暗(#45a049),按下时颜色会更暗(#3e8e41)。
    在这里插入图片描述
  2. 白色按钮样式 (setCommonStyle_write): 这种方法设置了一个白色半透明主题的按钮样式,特点包括:
    字体和大小设置与绿色按钮相同。
    边框为灰色,宽度为2px。
    背景颜色为白色,且具有透明度(rgba(255, 255, 255, 0))。
    按钮具有更大的圆角和内边距。
    鼠标悬停时,背景颜色会变为半透明的浅灰色(rgba(180, 180, 180, 0.3)),按下时透明度更高(rgba(180, 180, 180, 0.4))。
    在这里插入图片描述
    通过使用 ButtonStyle 类,贪吃蛇游戏中的所有按钮都可以轻松地应用统一和专业的外观,增强了用户界面的一致性和美观性。

4. 图形和音效

4.1 图形资源

在贪吃蛇游戏中,图形资源主要包括:

  1. 游戏背景:这是游戏的主要背景图像,为玩家提供视觉上的上下文环境。
  2. 蛇的身体:蛇的身体通常由一系列矩形或圆形的图形组成,这些图形在游戏区域内根据蛇的移动而更新位置。
  3. 食物:食物的图形通常是一个吸引玩家注意力的小图标,用来表示蛇需要收集的目标。
  4. 按钮和图标:游戏界面中的按钮,如“开始游戏”、“暂停游戏”、“退出游戏”等,以及它们的图标,都需要图形资源。

4.2 音效资源

音效资源增强了游戏的互动性和沉浸感,包括:

  1. 点击声:当玩家点击按钮或进行选择时播放的声音效果。
  2. 游戏音乐:背景音乐,为游戏提供连续的音频体验。
  3. 游戏结束:当玩家的游戏失败时播放的特殊音效

4.3 在Qt中加载和使用资源

在Qt中,图形和音效资源可以通过以下方式加载和使用:

  1. 资源文件(.qrc):Qt支持使用资源文件来管理图像、音乐等资源。资源文件是一个XML文件,其中列出了所有资源的路径和访问方式。资源文件可以被编译到应用程序中,使得资源可以与应用程序一起打包和部署。
    在Qt Creator中创建资源文件,并使用<file>标签指定资源的路径。
    使用:/res/image.png这样的语法来访问资源文件中的图像。

  2. QPixmap:用于加载和显示图像资源。例如,使用QPixmap(":res/game_background.jpg")来加载游戏背景图像。

  3. QPainter和QWidget:使用QPainter对象在QWidget或其子类上绘制图像。在paintEvent函数中,可以调用drawPixmap方法来绘制图像资源。

  4. QSound:用于播放音效资源。例如,使用QSound(":res/click.wav")来加载点击声音效果,并调用play方法播放。

  5. QMediaPlayer(可选):如果需要更高级的音频控制,可以使用QMediaPlayer类来加载和播放背景音乐。

  6. 信号和槽:将图形和音效资源的加载与游戏逻辑相结合,例如,在按钮的clicked信号连接到播放音效的槽函数上。

  7. 性能考虑:为了提高性能,图像和音效资源应该预先加载并缓存,避免在游戏运行时频繁加载资源。

示例代码:

// 加载和使用图像资源
QPixmap backgroundPixmap(":res/game_background.jpg");
QPainter painter(this);
painter.drawPixmap(0, 0, width(), height(), backgroundPixmap);

// 加载和播放音效资源
QSound clickSound(":res/click.wav");
clickSound.play();

// 使用QMediaPlayer播放背景音乐
QMediaPlayer *musicPlayer = new QMediaPlayer;
musicPlayer->setMedia(QUrl("qrc:/res/game_music.mp3"));
musicPlayer->play();

5. 调试和优化

5.1 调试技巧

  1. 使用Qt Creator的Debugger: 利用Qt Creator内置的调试器进行断点设置、单步执行、监视变量和查看调用栈。
  2. 日志输出: 在代码中添加日志输出语句,用于记录程序执行流程和变量状态,可以使用qDebug()函数。
  3. 单元测试: 编写单元测试来验证各个函数和模块的正确性,确保代码更改不会引入新的错误。
  4. 图形化调试: 对于图形和界面相关的问题,使用Qt Creator的图形化调试工具,如像素完美模式和DOM Inspector。
  5. 性能分析: 使用性能分析工具,如Valgrind或Qt Creator的性能分析器,来检测内存泄漏和性能瓶颈。
  6. 简化复杂函数: 将复杂的函数分解为更小的、更易于管理的函数,有助于定位问题。

5.2 性能优化建议

  1. 减少重绘区域: 仅在必要时调用update()repaint(),并且尽量缩小更新的范围。
  2. 优化绘制逻辑: 在paintEvent中减少不必要的计算和绘制操作,例如,通过缓存绘制结果来避免重复绘制。
  3. 使用更高效的数据结构: 根据需要选择合适的数据结构,以减少查找、插入和删除操作的时间复杂度。
  4. 减少对象创建: 避免在频繁调用的函数中创建和销毁对象,因为对象的创建和销毁成本较高。
  5. 合理使用定时器: 确保定时器的间隔设置合理,避免过快触发而导致的CPU占用过高。
  6. 资源加载和缓存: 预先加载游戏中使用的所有资源,并在内存中缓存,避免在游戏运行时频繁读取磁盘。

5.3 代码重构和改进方法

  1. 代码审查: 定期进行代码审查,以发现潜在的设计问题和改进代码质量。
  2. 提取重复代码: 将重复的代码块重构为独立的函数或类,以减少代码冗余。
  3. 使用设计模式: 根据需要应用设计模式,如观察者模式、单例模式或工厂模式,来提高代码的可维护性和扩展性。
  4. 接口和抽象类: 定义清晰的接口和抽象类,以促进代码的低耦合和高内聚。
  5. 依赖注入: 使用依赖注入来减少类之间的依赖关系,提高代码的灵活性和可测试性。
  6. 重构工具: 利用Qt Creator或IDE的重构工具来安全地进行重命名、移动和更改代码结构。
  7. 代码格式化: 保持代码风格的一致性,使用Qt Creator的代码格式化功能或.editorconfig文件来自动格式化代码。
  8. 文档和注释: 确保代码有充分的文档和注释,这有助于他人理解和维护代码

6. 扩展功能和未来方向

6.1 扩展功能

  1. 高分榜:
    集成一个高分榜功能,记录玩家的得分,并允许玩家在游戏结束后上传自己的得分。
    提供本地和在线的高分榜,显示最高分和历史最佳成绩。
  2. 不同主题:
    允许玩家选择不同的游戏主题,如宇宙、森林、海洋等,每个主题都有独特的图形和音效。
    通过主题包扩展游戏,用户可以下载和安装新的主题包。
  3. 自定义皮肤:
    允许玩家为蛇选择不同的皮肤或颜色,提供个性化的游戏体验。
  4. 增强的音效和音乐:
    提供更丰富的音效和背景音乐选项,玩家可以根据喜好选择不同的音乐风格。
  5. 成就系统:
    实现一个成就系统,为完成特定任务或达到某些里程碑的玩家提供奖励。
  6. 教程和帮助:
    提供游戏教程和帮助系统,帮助新玩家快速上手。

6.2 未来方向

  1. 网络对战:
    开发网络对战功能,允许玩家在线竞争或合作。
    实现匹配系统,根据玩家的得分或技能水平进行匹配。
  2. 社交功能:
    集成社交媒体分享功能,允许玩家分享自己的得分和成就。
  3. 跨平台游戏:
    确保游戏可以在不同的设备和平台上运行,如PC、移动设备和游戏机。
  4. 云存储:
    使用云服务存储玩家的得分和进度,使玩家可以在不同设备上继续游戏。
  5. 人工智能对手:
    开发具有人工智能的对手,为玩家提供更具挑战性的游戏体验。
  6. 游戏内购买:
    提供游戏内购买选项,如特殊皮肤、主题包或其他虚拟商品。
  7. 多语言支持:
    增加对多种语言的支持,扩大游戏的受众范围。
  8. 增强现实(AR)和虚拟现实(VR):
    探索将游戏扩展到AR或VR平台的可能性,提供沉浸式游戏体验。

6.3 技术实现

  • 高分榜: 使用数据库或后端服务存储得分,实现得分的上传和检索。
  • 网络对战: 使用网络编程技术实现客户端之间的通信,可能需要一个中央服务器来协调游戏会话。
  • 云存储: 集成云服务API,如华为云、阿里云或腾讯云,实现数据的同步和存储。
  • 人工智能: 应用机器学习算法,创建具有不同策略的AI对手。

7. 总结

使用Qt开发贪吃蛇游戏是一个实践和展示Qt强大功能的过程,同时也是深入了解C++在现代应用开发中应用价值的机会。以下是通过本项目获得的一些关键经验和认识:

  • Qt的跨平台能力:Qt作为一个跨平台的框架,允许开发者编写一次代码,然后在Windows、macOS、Linux、iOS和Android等多个平台上运行。这极大地提升了开发效率和软件的可移植性。
  • 丰富的GUI组件:Qt提供了一套丰富的GUI组件,包括按钮、标签、文本框等,这些组件在构建用户界面时非常有用。它们不仅外观美观,而且具有很好的用户体验。
  • 事件驱动的编程模型:Qt的事件处理机制简化了用户交互的处理。通过信号和槽机制,Qt应用程序能够响应各种用户操作和系统事件,这在开发贪吃蛇游戏中尤为关键。
  • 绘图和动画支持:Qt的绘图系统为游戏提供了绘制图形和动画的能力。使用QPainter和QPixmap,可以轻松实现游戏的视觉表现。
  • 性能优化:Qt应用程序的性能可以通过各种方式进行优化,包括减少不必要的重绘、使用更高效的数据结构和算法,以及利用Qt的资源系统预加载和缓存资源。
  • 音频和多媒体集成:Qt的QSound和QMediaPlayer类简化了游戏中音频效果和背景音乐的集成。
  • 代码的可维护性和扩展性:使用Qt和C++编写的代码结构清晰,易于维护和扩展。面向对象的编程范式使得代码更加模块化。
  • 学习曲线:虽然Qt和C++具有强大的功能,但它们也有一个学习曲线。通过实际项目,如贪吃蛇游戏的开发,可以加深对Qt框架和C++语言特性的理解。
  • 社区和文档支持:Qt拥有一个活跃的社区和详尽的文档,为开发者提供了丰富的学习资源和问题解答。
  • 游戏开发的深度和广度:通过开发贪吃蛇游戏,我们可以看到Qt和C++在游戏开发中的应用潜力,从简单的2D游戏到复杂的3D图形和网络对战游戏。

总之,使用Qt开发贪吃蛇游戏不仅是一种技术上的实践,也是对Qt和C++在游戏开发中应用潜力的探索。通过这个项目,开发者可以学习到宝贵的技能,并为未来的软件开发工作打下坚实的基础。

结尾

随着本文的结束,我希望读者不仅对使用Qt开发贪吃蛇游戏有了深入的了解,而且也被激励去尝试自己的项目。开发应用程序,尤其是游戏,是一段充满挑战和学习机会的旅程。鼓励动手实践,探索Qt文档,参与社区,开发个性化项目,并分享您的经验。同时,也要持续学习,接受反馈,并迭代改进。软件开发是一个不断进步的过程,享受编程和创造的过程,享受您在开发中取得的每一个进步和成功。记住,每一个伟大的开发者都是从编写第一个“Hello World”程序开始的。所以,不要害怕犯错,不要害怕从头开始。每个项目都是成长和学习的机会。现在,开始您的Qt开发之旅,创造令人兴奋的新作品,并与世界分享您的创造力吧!

相关推荐

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-16 02:50:03       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-16 02:50:03       72 阅读
  3. 在Django里面运行非项目文件

    2024-07-16 02:50:03       58 阅读
  4. Python语言-面向对象

    2024-07-16 02:50:03       69 阅读

热门阅读

  1. ARIMA模型(AutoRegressive Integrated Moving Average Model)

    2024-07-16 02:50:03       19 阅读
  2. linux高级编程(sqlite数据库调用)

    2024-07-16 02:50:03       22 阅读
  3. 欠拟合与过拟合

    2024-07-16 02:50:03       21 阅读
  4. [C/C++入门][输入输出]2、字符三角形

    2024-07-16 02:50:03       23 阅读
  5. Unsloth 微调 Llama 3

    2024-07-16 02:50:03       19 阅读
  6. Pyinstaller打包后__file__定位当前绝对路径错误

    2024-07-16 02:50:03       21 阅读
  7. 单一职责原则

    2024-07-16 02:50:03       20 阅读
  8. 知识图谱和向量库

    2024-07-16 02:50:03       23 阅读