【QT5】<应用> 小游戏:贪吃蛇

文章目录

一、项目要求

二、需求分析

三、实现效果

四、代码


一、项目要求

**【1】主要实现:**游戏界面存在一条蛇🐍,使用键盘wsad或者↑↓←→键盘可以控制蛇的行走方向。同时界面中会随机出现食物,蛇可以吃食物,然后蛇的身体就会变长。吃完一个食物之后才会刷新另外一个食物。

**【2】游戏结束:**蛇碰壁。

【3】玩家分数: 由蛇的存活时间以及长度综合计算。time为蛇存活的秒数,lent-len0表示蛇增加的节点数量。同时,玩家分数需要实时显示

**【4】重新开始:**点击相应的按钮。

**【5】加速模式:**双击键盘可以加速蛇的移动。例如按两下w键,蛇的速度变为原来的2倍。

**【6】不同食物:**食物有多种类别,不同的食物会导致蛇变长的长度不同。


二、需求分析

【1】针对主要实现:

  • 创建蛇(Snake)食物(Food) 和**主要窗口(Widget)**类。
  • Snake类: 创建方法来实现蛇的移动 (头插一个,尾删一个)、蛇的方向检测 (根据键盘值)、蛇是否死亡 (是否碰壁)以及在窗口中绘制蛇(通过传入painter)。
  • Food类: 创建方法来实现食物的坐标生成和类型生成 (利用随机数)、在窗口中绘制食物 。用type整型变量来标志食物类型,当type=1时蛇吃掉后加1节,当type=2时蛇吃掉后加2节,以此类推。
  • Widget类: 创建方法来实现整体的按键检测、绘制图像、计算玩家分数。为了让蛇不断移动,使用定时器(QTimer)来周期性触发槽函数,在槽函数中执行蛇吃食物、食物更新、画面更新等操作。
  • **按键控制:**①↑↓←→键盘控制蛇的移动;②空格键控制游戏的开始/暂停(控制定时器开始/停止)。
  • **综合不同类的绘图:**将主要逻辑的Widget类中实例化的painter对象作为形参传入Snake和Food类中,在它们各自的方法中进行各自的绘图操作。

【2】针对重新开始:

  • 在ui设计器中添加QPushButton,并添加相应的槽函数。
  • 槽函数中将分数、暂停标志、时间计数器等属性恢复初始值。
  • 槽函数中将已有的蛇、食物对象delete,再创建新的蛇、食物对象。
  • 槽函数中需要重新聚焦于gameWidget,否则点击按钮后需要再点击gameWidget才能检测键盘的输入。重新聚焦调用"ui->gameWidget->setFocus();"

【3】针对不同速度:

  • 在Snake类中创建属性speed,当speed为1时表示每次触发定时器槽函数执行蛇前进1格,当speed为2时表示每次触发定时器槽函数执行蛇前进2格。
  • 初始化speed为1,当用户手动按下两下↑后会加速,直到用户改变蛇的方向

【4】针对不同食物:

  • 在Food类中创建属性type,当type为1时表示食物类型为1(蛇吃后增加1节),当type为2时表示食物类型为2(蛇吃后增加2节),当type为3时表示食物类型为3(蛇吃后增加3节)。
  • 类型1 设定为绿色的圆形
  • 类型2 设定为深蓝色的椭圆
  • 类型3 设定为浅蓝色的方形

三、实现效果


四、代码

【1】food.h:

cpp 复制代码
#ifndef FOOD_H
#define FOOD_H

#include <QWidget>
#include <QPainter>
#include <QRandomGenerator>


/***********************************************************
  * @类名:	Food
  * @摘要:	食物类
  * @作者:	柯同学
  * @注意:	三种食物类型:type值为几就加几节蛇身
  *********************************************************/
class Food : public QWidget
{
    Q_OBJECT
public:
    explicit Food(QWidget *parent = nullptr);
    int getX();         //返回食物横坐标
    int getY();         //返回食物纵坐标
    int getRadius();    //返回食物半径
    int getType();      //返回食物类型
    void generateFood();//生成食物的坐标和类型
    void paintFood(QPainter &painter);//绘制食物

private:
    int foodX;  //食物横坐标
    int foodY;  //食物纵坐标
    int radius; //食物半径
    int type;   //食物类型:1类型为增加1节,2类型增加2节,3类型增加3节
};

#endif // FOOD_H

【2】food.cpp:

cpp 复制代码
#include "food.h"

/***********************************************************
  * @函数名:Food
  * @功  能:食物类的构造函数
  * @参  数:parent父对象
  * @返回值:无
  *********************************************************/
Food::Food(QWidget *parent)
    : QWidget(parent)
    , radius(15)
{
    //初始的食物也随机
    generateFood();
}


/***********************************************************
  * @函数名:getX
  * @功  能:返回食物的横坐标
  * @参  数:无
  * @返回值:食物的横坐标
  *********************************************************/
int Food::getX()
{
    return foodX;
}


/***********************************************************
  * @函数名:getY
  * @功  能:返回食物的纵坐标
  * @参  数:无
  * @返回值:食物的纵坐标
  *********************************************************/
int Food::getY()
{
    return foodY;
}


/***********************************************************
  * @函数名:getRadius
  * @功  能:返回食物的半径
  * @参  数:无
  * @返回值:食物的半径
  *********************************************************/
int Food::getRadius()
{
    return radius;
}


/***********************************************************
  * @函数名:getType
  * @功  能:返回食物的类型
  * @参  数:无
  * @返回值:食物的类型
  *********************************************************/
int Food::getType()
{
    return type;
}


/***********************************************************
  * @函数名:generateFood
  * @功  能:随机生成食物的坐标、类型
  * @参  数:无
  * @返回值:无
  *********************************************************/
void Food::generateFood()
{
    //随机生成坐标,距边界radius降低反应难度
    foodX = QRandomGenerator::global()->bounded(10 + radius, 580 - radius * 2);
    foodY = QRandomGenerator::global()->bounded(20 + radius, 570 - radius * 2);
    //随机生成类型[1,4)
    type = QRandomGenerator::global()->bounded(1, 4);
}


/***********************************************************
  * @函数名:paintFood
  * @功  能:根据食物类型的不同,绘制不同的食物类型。
  * @参  数:painter---画家对象
  * @返回值:无
  * @说  明:type=1---类型1:绿色的圆形
  *         type=2---类型2:深蓝色的椭圆形
  *         type=3---类型3:淡蓝色的矩形
  *********************************************************/
void Food::paintFood(QPainter &painter)
{
    QPen pen;
    QBrush brush;
    //根据type不同,绘制不同的食物
    switch (this->type){
    case 1:
        pen.setColor(QColor(43, 220, 112));
        brush.setColor(QColor(43, 220, 112));
        brush.setStyle(Qt::SolidPattern);
        painter.setPen(pen);
        painter.setBrush(brush);
        painter.drawEllipse(QRect(foodX, foodY, radius, radius));
        break;
    case 2:
        pen.setColor(QColor(0, 62, 146));
        brush.setColor(QColor(0, 62, 146));
        brush.setStyle(Qt::SolidPattern);
        painter.setPen(pen);
        painter.setBrush(brush);
        painter.drawEllipse(QRect(foodX, foodY, radius*2, radius));
        break;
    case 3:
        pen.setColor(QColor(29, 130, 154));
        brush.setColor(QColor(29, 130, 154));
        brush.setStyle(Qt::SolidPattern);
        painter.setPen(pen);
        painter.setBrush(brush);
        painter.drawRect(QRect(foodX, foodY, radius, radius));
        break;
    }
}

【3】snake.h:

cpp 复制代码
#ifndef SNAKE_H
#define SNAKE_H

#include <QWidget>
#include <QEvent>
#include <QPaintEvent>
#include <QPainter>

//枚举蛇前进的方向
enum Direct{
    MOVE_UP,
    MOVE_DOWN,
    MOVE_LEFT,
    MOVE_RIGHT
};

/***********************************************************
  * @类名:	Snake
  * @摘要:	蛇类
  * @作者:	柯同学
  * @注意:	有两种速度:speed为1就每次timeout前进1格,以此类推。
  *         在原有方向的基础上,再次按原方向键能切换到二倍速模式,
  *         比如蛇在往上,再次按up键就两倍速,直到切换方向。
  *********************************************************/
class Snake : public QWidget
{
    Q_OBJECT
public:
    explicit Snake(QWidget *parent = nullptr);
    void paintSnake(QPainter &painter); //绘制蛇身
    void keyPress(QKeyEvent *event);    //键盘控制移动方向
    void autoMove();        //沿着方向自动移动
    void frontAddBody();     //根据方向增加首节点
    QRect getSnakeHead();   //获取蛇的首节点信息
    int getLength();        //返回蛇身节点个数
    bool isOver();          //返回蛇是否死亡,死亡为true

private:
    QList<QRect> body;  //蛇身
    int nodeWidth;      //节点宽度
    int nodeHeight;     //节点高度
    int moveDirect;     //移动方向
    int speed;          //移动速度:1表示默认速度,2表示两倍速度
};

#endif // SNAKE_H

【4】snake.cpp:

cpp 复制代码
#include "snake.h"


/***********************************************************
  * @函数名:Snake
  * @功  能:蛇的构造函数
  * @参  数:parent---父对象
  * @返回值:无
  * @说  明:初始化三节蛇身
  *********************************************************/
Snake::Snake(QWidget *parent)
    : QWidget(parent)
    , nodeWidth(20)
    , nodeHeight(20)
    , moveDirect(MOVE_LEFT)
    , speed(1)
{
    //初始给定三节蛇身
    body.append(QRect(400, 300, nodeWidth, nodeHeight));
    body.append(QRect(400 + nodeWidth, 300, nodeWidth, nodeHeight));
    body.append(QRect(400 + nodeWidth * 2, 300, nodeWidth, nodeHeight));
}


/***********************************************************
  * @函数名:getLength
  * @功  能:获取蛇身的长度,即有几个节点
  * @参  数:无
  * @返回值:蛇身的长度
  *********************************************************/
int Snake::getLength()
{
    return body.length();
}


/***********************************************************
  * @函数名:isOver
  * @功  能:判定蛇是否碰墙死亡
  * @参  数:无
  * @返回值:true---蛇死亡,false---蛇没有死亡
  *********************************************************/
bool Snake::isOver()
{
    if (body[0].y() <= 20 || body[0].y() + nodeHeight >= 570 ||
        body[0].x() <= 10 || body[0].x() + nodeWidth >= 580) {
        return true;
    }
    return false;
}


/***********************************************************
  * @函数名:keyPress
  * @功  能:按键检测,控制蛇的前进方向
  * @参  数:event---按键的事件
  * @返回值:无
  * @说  明:在原有方向的基础上,再次按原方向键能切换到二倍速模式
  *********************************************************/
void Snake::keyPress(QKeyEvent *event)
{
    switch(event->key()) {
    case Qt::Key_Up://蛇往上
        if (MOVE_UP == moveDirect){
            speed = 2;
        }
        else if (moveDirect != MOVE_DOWN){
            moveDirect = MOVE_UP;
            speed = 1;
        }
        break;
    case Qt::Key_Down://蛇往下
        if (MOVE_DOWN == moveDirect){
            speed = 2;
        }
        else if (moveDirect != MOVE_UP){
            moveDirect = MOVE_DOWN;
            speed = 1;
        }
        break;
    case Qt::Key_Left://蛇往左
        if (MOVE_LEFT == moveDirect){
            speed = 2;
        }
        else if (moveDirect != MOVE_RIGHT){
            moveDirect = MOVE_LEFT;
            speed = 1;
        }
        break;
    case Qt::Key_Right://蛇往右
        if (MOVE_RIGHT == moveDirect){
            speed = 2;
        }
        else if (moveDirect != MOVE_LEFT){
            moveDirect = MOVE_RIGHT;
            speed = 1;
        }
        break;
    default:
        break;
    }
}


/***********************************************************
  * @函数名:autoMove
  * @功  能:根据speed属性,让蛇前进1格或2格
  * @参  数:无
  * @返回值:无
  * @说  明:蛇的前进做法:每次头插一个节点,删除最后的一个节点
  *********************************************************/
void Snake::autoMove()
{
    if (1 == speed){
        frontAddBody();
        body.removeLast();
    }
    else if (2 == speed){
        frontAddBody();
        frontAddBody();
        body.removeLast();
        body.removeLast();
    }
}


/***********************************************************
  * @函数名:frontAddBody
  * @功  能:根据前进方向,头插不同位置的节点
  * @参  数:无
  * @返回值:无
  * @说  明:由于碰壁就死亡了,无需越界判定
  *********************************************************/
void Snake::frontAddBody()
{
    switch (moveDirect){
    case MOVE_UP:
            body.insert(0, QRect(body[0].x(), body[0].y() - nodeHeight, nodeWidth, nodeHeight));
        break;
    case MOVE_DOWN:
            body.insert(0, QRect(body[0].x(), body[0].y() + nodeHeight, nodeWidth, nodeHeight));
        break;
    case MOVE_LEFT:
            body.insert(0, QRect(body[0].x() - nodeWidth, body[0].y(), nodeWidth, nodeHeight));
        break;
    case MOVE_RIGHT:
            body.insert(0, QRect(body[0].x() + nodeWidth, body[0].y(), nodeWidth, nodeHeight));
        break;
    }
}


/***********************************************************
  * @函数名:getSnakeHead
  * @功  能:返回首节点的拷贝,以防外界修改。
  * @参  数:无
  * @返回值:无
  *********************************************************/
QRect Snake::getSnakeHead()
{
    return body[0];
}


/***********************************************************
  * @函数名:paintSnake
  * @功  能:绘制蛇身,遍历QList
  * @参  数:painter---画家对象
  * @返回值:无
  *********************************************************/
void Snake::paintSnake(QPainter &painter)
{
    //设置painter
    QPen pen(QColor(64, 65, 66));
    QBrush brush(QColor(183, 1, 1));
    painter.setPen(pen);
    painter.setBrush(brush);
    //绘制蛇身
    for (int i = 0; i < body.length(); ++i) {
        painter.drawRect(body[i]);
    }
}

【5】widget.h:

cpp 复制代码
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include "snake.h"
#include "food.h"
#include <QPainter>
#include <QTimer>

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE


/***********************************************************
  * @类名:	Widget
  * @摘要:	主要界面类
  * @作者:	柯同学
  * @注意:	在该类中进行蛇吃食物、游戏结束等逻辑的实现
  *********************************************************/
class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();
    void paintEvent(QPaintEvent *event) override;   //绘图
    void keyPressEvent(QKeyEvent *event) override;  //按键检测
    void snakeEatFood();    //蛇吃食物,更新食物
    void computeScore();    //计算玩家分数

private slots:
    void timeOut();         //定时器触发的槽函数
    void on_pushButton_clicked();//"重新开始"按钮的槽函数

private:
    Ui::Widget *ui;
    Snake *snake;   //蛇
    Food *food;     //食物
    QTimer *timer;  //定时器,固定100ms触发
    bool isPause;   //开始或暂停的标志,true为暂停
    int timerCount; //定时器触发次数
    double score;   //玩家分数
};
#endif // WIDGET_H

【6】widget.cpp:

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>


/***********************************************************
  * @函数名:Widget
  * @功  能:主要窗口的构造函数
  * @参  数:parent---父对象
  * @返回值:无
  *********************************************************/
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
    , isPause(true)
    , timerCount(0)
    , score(0)
{
    ui->setupUi(this);
    //实例化蛇
    snake = new Snake(ui->gameWidget);
    //实例化食物
    food = new Food(ui->gameWidget);
    //设置定时器
    timer = new QTimer(this);
    connect(timer, SIGNAL(timeout()), this, SLOT(timeOut()));
    //设置聚焦对象
    ui->gameWidget->setFocus();
}


/***********************************************************
  * @函数名:~Widget
  * @功  能:主要窗口的析构函数
  * @参  数:无
  * @返回值:无
  * @说  明:由于QT的对象树机制,绑定好父对象后无需手动delete其他空间
  *********************************************************/
Widget::~Widget()
{
    delete ui;
}


/***********************************************************
  * @函数名:~Widget
  * @功  能:主要窗口的析构函数
  * @参  数:无
  * @返回值:无
  *********************************************************/
void Widget::paintEvent(QPaintEvent *event)
{
    Q_UNUSED(event)
    QPainter painter(this);
    //判定游戏是否结束,结束则显示提示
    if (snake->isOver()) {//蛇死亡,游戏结束
        timer->stop();
        isPause = true;
        QFont font("宋体", 30, QFont::ExtraBold,false);
        painter.setFont(font);
        QRect showArea(200, 250, 200, 100);
        painter.drawText(showArea, Qt::AlignHCenter | Qt::AlignVCenter, "游戏结束!");
    }
    //绘制蛇和食物
    snake->paintSnake(painter);
    food->paintFood(painter);
    //实时显示玩家分数
    ui->scoreLabel->setText("分数: " + QString::number(score));
}


/***********************************************************
  * @函数名:keyPressEvent
  * @功  能:重写的按键检测事件函数
  * @参  数:event---按键事件
  * @返回值:无
  * @说  明:以防游戏结束后还能控制,首先需要判定游戏是否结束
  *********************************************************/
void Widget::keyPressEvent(QKeyEvent *event)
{
    //游戏未结束才进行按键控制
    if (!(snake->isOver())) {
        //按键控制蛇移动
        snake->keyPress(event);
        //按键控制暂停
        if (event->key() == Qt::Key_Space) {
            if (isPause){
                isPause = false;
                timer->start(100);
            }
            else {
                isPause = true;
                timer->stop();
            }
        }
    }
}


/***********************************************************
  * @函数名:snakeEatFood
  * @功  能:蛇与食物接触,则给蛇加节点,给食物重新生成坐标和类型
  * @参  数:无
  * @返回值:无
  * @说  明:生成食物必须放在蛇加节点之后
  *********************************************************/
void Widget::snakeEatFood()
{
    //蛇与食物接触
    if (abs(snake->getSnakeHead().x() - food->getX()) <= food->getRadius() &&
        abs(snake->getSnakeHead().y() - food->getY()) <= food->getRadius())
    {
        //蛇吃不同类型食物加不同节
        int count = food->getType();
        while (count--){
            snake->frontAddBody();
        }
        //再次生成食物
        food->generateFood();
    }
}


/***********************************************************
  * @函数名:computeScore
  * @功  能:计算玩家分数
  * @参  数:无
  * @返回值:无
  * @说  明:分数由两部分构成:时间分数、长度分数。
  *         每坚持1秒,分数增加1分。
  *         每多一节身体,分数增加10分。
  *********************************************************/
void Widget::computeScore()
{
    double timeScore = (timerCount / 10.0) * 1;     //时间的分数,每秒1分
    int lenScore = (snake->getLength() - 3) * 10;   //长度的分数,增一节10分
    if (lenScore >= 0)
        this->score = timeScore + lenScore;
}


/***********************************************************
  * @函数名:timeOut
  * @功  能:定时器的槽函数
  * @参  数:无
  * @返回值:无
  *********************************************************/
void Widget::timeOut()
{
    //蛇自动移动,吃食物
    snake->autoMove();
    snakeEatFood();
    //计算玩家分数
    timerCount++;
    computeScore();
    //更新界面
    this->update();
}


/*****************************************************************
  * @函数名:on_pushButton_clicked
  * @功  能:"重新开始"按钮的槽函数
  * @参  数:无
  * @返回值:无
  * @说  明:需要重新聚焦于gameWidget,否则点击按钮后需要再点击gameWidget
  ***************************************************************/
void Widget::on_pushButton_clicked()
{
    //清除相关属性
    timerCount = 0; //定时器计数清零
    score = 0;      //分数清零
    isPause = true; //恢复暂停
    //删除已有成员对象
    if (snake) {
        delete snake;
    }
    if (food) {
        delete food;
    }
    //创建新对象
    snake = new Snake(ui->gameWidget);
    food = new Food(ui->gameWidget);
    //更新界面
    this->update();
    ui->gameWidget->setFocus();//重新聚焦于游戏窗口
}

【7】main.cpp: 系统自动生成的,没有改动。

cpp 复制代码
#include "widget.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget w;
    w.show();
    return a.exec();
}
相关推荐
ZSYP-S6 分钟前
Day 15:Spring 框架基础
java·开发语言·数据结构·后端·spring
yuanbenshidiaos9 分钟前
c++------------------函数
开发语言·c++
程序员_三木21 分钟前
Three.js入门-Raycaster鼠标拾取详解与应用
开发语言·javascript·计算机外设·webgl·three.js
是小崔啊31 分钟前
开源轮子 - EasyExcel01(核心api)
java·开发语言·开源·excel·阿里巴巴
tianmu_sama37 分钟前
[Effective C++]条款38-39 复合和private继承
开发语言·c++
黄公子学安全40 分钟前
Java的基础概念(一)
java·开发语言·python
liwulin050641 分钟前
【JAVA】Tesseract-OCR截图屏幕指定区域识别0.4.2
java·开发语言·ocr
jackiendsc1 小时前
Java的垃圾回收机制介绍、工作原理、算法及分析调优
java·开发语言·算法
Oneforlove_twoforjob1 小时前
【Java基础面试题027】Java的StringBuilder是怎么实现的?
java·开发语言
羚羊角uou1 小时前
【C++】优先级队列以及仿函数
开发语言·c++