文章目录
一、项目要求
**【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();
}