目录
贪吃蛇小游戏
一.项目介绍及演示
1. 项目介绍
贪吃蛇游戏规则:简单来说,贪吃蛇游戏规则是玩家通过四个方向键来控制蛇的移动控制其在地图上吃豆子。蛇运动过程中撞到蛇身,则立即结束游戏
2. 项目演示
3. 窗口介绍
3.1 游戏大厅窗口
3.2 游戏关卡选择窗口
3.3 游戏房间窗口
二、创建项目及资源配置
1. 创建项目(QWidget)
在桌面上创建一个名为Snake的文件夹,用于存放项目代码
新建一个项目
2. 资源配置(图片声音素材)
在上述创建完成后,新建一个qrc文件,步骤如下:
此时资源文件,如图示:
三、项目实现
1. 游戏大厅
1.1 重写绘图事件
在gamehall.h文件中,进行重写绘图事件函数的声明
鼠标当到painEvent函数上点击Alt+Enter(回车),为painEvent函数添加定义
在上述过程后,开始编辑painEvent函数。
第一步:先实例化画家,使用QPainter painter(this)实例化一个画家,this是让画家在当前窗口(gamehall)进行绘制。注意在使用QPainter需要包含头文件#include<QPainter>
第二步:再实例化绘图设备,将图片加载到绘图设备中。注意在使用QPixmap需要包含头文件#include<QPixmap>
第三步:再进行绘画,使用drawPixmap()方法,参数1和参数2,表示绘制开始的坐标;参数3,表示绘制的宽度;参数4,表示绘制的高度;参数5,表示绘制的图片。
void GameHall::paintEvent(QPaintEvent *event)
{
//实例化画家,this:让画家在当前窗口进行绘制
QPainter painter(this);
//实例化绘图设备
QPixmap pix(":/res/game_hall.png");
//绘画 0,0从界面左上角开始绘画根据窗口的宽和高进行绘制
painter.drawPixmap(0,0,this->width(),this->height(),pix);
}
1.2 设置窗口大小、图标、标题
第一步,先修改窗口的大小通过setFixedSize()方法将窗口的宽度设置为1000,高度设置为800。
第二步,设置窗口的图标先通过QIcon()将图片加载到项目中,再通过setWindowIcon()方法设置到窗口中。
第三步,设置窗口的标题通过setWindowTitle()方法将文字设置到窗口标题中。
GameHall::GameHall(QWidget *parent)
: QWidget(parent)
, ui(new Ui::GameHall)
{
ui->setupUi(this);
//设置窗口大小
this->setFixedSize(1000,800);
//设置窗口的图标
this->setWindowIcon(QIcon(":/res/ico.png"));
//设置窗口标题
this->setWindowTitle("游戏大厅");
}
1.3 设置开始按钮样式
第一步,通过QPushButton创建一个游戏开始按钮,按钮文本设置为"开始游戏",this在父窗口中显示。
第二步,通过move移动按钮再窗口中的位置,消除按钮的边框。
第三步,修改按钮中的文本样式,需要先创建一个字体样式(字体为黑体,大小为20像素),最后通过setFont方法将字体样式加载到按钮中
GameHall::GameHall(QWidget *parent)
: QWidget(parent)
, ui(new Ui::GameHall)
{
ui->setupUi(this);
//设置窗口大小
this->setFixedSize(1000,800);
//设置窗口的图标
this->setWindowIcon(QIcon(":/res/ico.png"));
//设置窗口标题
this->setWindowTitle("游戏大厅");
//添加开始游戏按钮
QPushButton * startButton = new QPushButton("开始游戏",this);
//设置按钮的位置
startButton->move(450,600);
//消除按钮边框
startButton->setStyleSheet("QPushButton{border:0px}");
//创建字体样式
QFont font("黑体", 20);
//将字体样式加入到按钮文本中
startButton->setFont(font);
}
点击运行,此时游戏大厅绘制成功
2. 游戏关卡选择
2.1 创建GameSelect类
先创建一个新的类,名为为GameSelect,基类选择QWidget
2.2 连接按钮和窗口跳转
在gamehall.cpp中进行编辑按钮的信号和槽,实现点击按钮跳转至关卡选择窗口
第一步,先创建出一个GameSelect窗口,在创建时要包含头文件#include "gameselect.h"
第二步,进行连接使用connnect()方法进行信号和槽函数之间的连接,使用Lambda表达式进行槽函数的编写。
第三步,编辑Lambda表达式,使用close()方法关闭当前窗口。
第四步,将关卡选择窗口设置为同游戏大厅窗口大小相同。
第五步,打开游戏关卡选择窗口。
第六步,给按钮添加点击音效,首先在.pro文件中添加multimedia,然后使用play()进行添加音效。
#include "gamehall.h"
#include "ui_gamehall.h"
#include "gameselect.h"
#include <QPainter>
#include <QPixmap>
#include <QIcon>
#include <QPushButton>
#include <QFont>
#include <QSound>
GameHall::GameHall(QWidget *parent)
: QWidget(parent)
, ui(new Ui::GameHall)
{
ui->setupUi(this);
//设置窗口大小
this->setFixedSize(1000,800);
//设置窗口的图标
this->setWindowIcon(QIcon(":/res/ico.png"));
//设置窗口标题
this->setWindowTitle("游戏大厅");
//添加开始游戏按钮
QPushButton * startButton = new QPushButton("开始游戏",this);
//设置按钮的位置
startButton->move(450,600);
//消除按钮边框
startButton->setStyleSheet("QPushButton{border:0px}");
//创建字体样式
QFont font("黑体", 20);
//将字体样式加入到按钮文本中
startButton->setFont(font);
//新建GameSelect窗口
GameSelect * gameSelect = new GameSelect;
//Lambda表达式
connect(startButton,&QPushButton::clicked,[=](){
//先关闭当前窗口
this->close();
//设置gameSelect窗口大小跟gamehall窗口大小一样
gameSelect->setGeometry(this->geometry());
//打开gameselect窗口
gameSelect->show();
//添加按钮点击的音效
QSound::play(":/res/clicked.wav");
});
}
编写gameselect.h文件,同游戏大厅步骤,这里不重复进行介绍,,详细看下下方代码注释。
#include "gameselect.h"
#include "gamehall.h"
#include <QPainter>
#include <QPixmap>
#include <QPushButton>
#include <QIcon>
#include <QSound>
GameSelect::GameSelect(QWidget *parent) : QWidget(parent)
{
//设置窗口大小
this->setFixedSize(1000,800);
//设置窗口的图标
this->setWindowIcon(QIcon(":/res/ico.png"));
//设置窗口标题
this->setWindowTitle("关卡选择");
//创建返回按钮
QPushButton * returnbutton = new QPushButton(this);
returnbutton->resize(50,50);
returnbutton->setIcon(QIcon(":/res/back.png"));
returnbutton->setIconSize(returnbutton->size());
returnbutton->move(900,700);
//建立信号和槽函数的联系
connect(returnbutton,&QPushButton::clicked,[=](){
//创建游戏大厅窗口
GameHall * gamehall = new GameHall;
//关闭当前窗口
this->close();
//展开游戏大厅窗口
gamehall->show();
//添加音效
QSound::play(":/res/clicked.wav");
});
//创建文本样式
QFont font("黑体",30);
//创建简单难度按钮
QPushButton * simplebutton = new QPushButton("简单模式",this);
//将样式设置到按钮中
simplebutton->setFont(font);
//设置按钮在窗口中的位置
simplebutton->move(400,170);
//进行信号与槽的连接
//创建普通难度按钮
QPushButton * commonbutton = new QPushButton("普通模式",this);
//将样式设置到按钮中
commonbutton->setFont(font);
//设置按钮在窗口中的位置
commonbutton->move(400,270);
//创建困难难度按钮
QPushButton * difficultybutton = new QPushButton("困难模式",this);
//将样式设置到按钮中
difficultybutton->setFont(font);
//设置按钮在窗口中的位置
difficultybutton->move(400,370);
//创建历史记录按钮
QPushButton * hisbutton = new QPushButton("历史记录",this);
//将样式设置到按钮中
hisbutton->setFont(font);
//设置按钮在窗口中的位置
hisbutton->move(400,470);
}
void GameSelect::paintEvent(QPaintEvent *event)
{
//实例化画家,this:让画家在当前窗口进行绘制
QPainter painter(this);
//实例化绘图设备
QPixmap pix(":/res/game_select.png");
//绘画
painter.drawPixmap(0,0,this->width(),this->height(),pix);
}
3. 游戏房间
3.1 创建GameRoom类
根据上面步骤后,新建GameRoom类
3.2 对游戏房间数据结构进行封装
#ifndef GAMEROOM_H
#define GAMEROOM_H
#include <QWidget>
#include<QSound>
enum class SnakeDirect{
UP=0,
DOWN,
LEFT,
RIGHT
};
class GameRoom : public QWidget
{
Q_OBJECT
public:
explicit GameRoom(QWidget *parent = nullptr);
//重写绘图事件
void paintEvent(QPaintEvent *event);
void moveUp(); //向上移动
void moveDown();//向下移动
void moveLeft();//向左移动
void moveRight();//向右移动
bool checkFail();//判断游戏结束
void createNewFood();//创建食物
void setTimeout(int timeout){moveTimeout =timeout;}
private:
const int KSnakeNodeWidth = 20; //蛇节点的宽度
const int KSnakeNodeHeight = 20; //蛇节点的高度
const int KDefaultTimeout =200; //蛇的默认移动速度
QList<QRectF> snakeList; //表示贪吃蛇链表
QRectF foodRect; //表示食物节点
SnakeDirect moveDirect = SnakeDirect::UP; //设置蛇移动方向默认向上
QTimer *timer; //定时器
bool isGameStart =false; //表示是否开始游戏
QSound *sound;
int moveTimeout = KDefaultTimeout;
};
#endif // GAMEROOM_H
3.3 初始化游戏房间界面
//设置窗口大小
this->setFixedSize(1000,800);
//设置窗口的图标
this->setWindowIcon(QIcon(":/res/ico.png"));
//设置窗口标题
this->setWindowTitle("游戏房间");
3.4 绘制界面窗口
void GameRoom::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
QPixmap pix;
pix.load(":/res/game_room.png");
painter.drawPixmap(0,0,800,800,pix);
pix.load(":/res/bg1.png");
painter.drawPixmap(800,0,200,850,pix);
}
3.5 蛇的移动
贪吃蛇的持续动态移动,本质上是定时器不断发出信号,调用moveup等移动函数,更新链表结构,最后重新绘图呈现给我们。
//创建上下左右按钮
QPushButton * up = new QPushButton("↑",this);
QPushButton * down = new QPushButton("↓",this);
QPushButton * left = new QPushButton("←",this);
QPushButton * right = new QPushButton("→",this);
up->move(880,400);
down->move(880,480);
left->move(840,440);
right->move(920,440);
up->setFont(font);
down->setFont(font);
left->setFont(font);
right->setFont(font);
up->setStyleSheet("QPushButton{border:0px}");
down->setStyleSheet("QPushButton{border:0px}");
left->setStyleSheet("QPushButton{border:0px}");
right->setStyleSheet("QPushButton{border:0px}");
//按钮控制移动方向,不能直接掉头
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;
});
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));
}
void GameRoom::moveDown()
{
QPointF leftTop; //左上角坐标
QPointF rightBottom; //右下角坐标
auto snakeNode = snakeList.front();//头
int headX = snakeNode.x();
int headY = snakeNode.y();
if(headY>this->height())
{
leftTop = QPointF(headX,0);
}else {
leftTop=snakeNode.bottomLeft();
}
rightBottom = leftTop + QPointF(KSnakeNodeWidth,KSnakeNodeHeight);
snakeList.push_front(QRectF(leftTop,rightBottom));
}
void GameRoom::moveLeft()
{
QPointF leftTop; //左上角坐标
QPointF rightBottom; //右下角坐标
auto snakeNode = snakeList.front();//头
int headX = snakeNode.x();
int headY = snakeNode.y();
if(headX<0)
{
leftTop=QPointF(800-KSnakeNodeWidth,headY);
}else{
leftTop=QPointF(headX-KSnakeNodeWidth,headY);
}
rightBottom = leftTop + QPointF(KSnakeNodeWidth,KSnakeNodeHeight);
snakeList.push_front(QRectF(leftTop,rightBottom));
}
void GameRoom::moveRight()
{
QPointF leftTop; //左上角坐标
QPointF rightBottom; //右下角坐标
auto snakeNode = snakeList.front();//头
int headX = snakeNode.x();
int headY = snakeNode.y();
if(headX>800-2*KSnakeNodeWidth)
{
leftTop=QPointF(0,headY);
}else{
leftTop=snakeNode.topRight();
}
rightBottom = leftTop + QPointF(KSnakeNodeWidth,KSnakeNodeHeight);
snakeList.push_front(QRectF(leftTop,rightBottom));
}
3.6 绘制蛇头身体尾巴
//绘制蛇
//蛇头
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{
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);
3.6 绘制食物与分数
//绘制食物节点
//先进行加载食物
pix.load(":/res/food.bmp");
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.03,100,50,pix);
QPen pen;
pen.setColor(Qt::black);
painter.setPen(pen);
QFont font("黑体",22);
painter.setFont(font);
painter.drawText(this->width()*0.9,this->height()*0.08,QString("%1").arg(snakeList.size()));
//往文件中写分数
int c = snakeList.size();
QFile file("C:/Users/26256/Desktop/his.txt");
if(file.open(QIODevice::WriteOnly | QIODevice::Text))
{
QTextStream out(&file);
int num = c;
out << num;
file.close();
}
3.7 判定游戏结束
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;
}
3.8 随机生成食物节点
void GameRoom::createNewFood()
{
foodRect=QRectF(qrand() %(800/KSnakeNodeWidth)*KSnakeNodeWidth,
qrand() %(this->height()/KSnakeNodeHeight)*KSnakeNodeHeight,
KSnakeNodeWidth,
KSnakeNodeHeight
);
}
3.9 创建游戏开始和暂停按钮
//开始游戏 结束游戏
QPushButton * startButton = new QPushButton("开始",this);
QPushButton *failButton = new QPushButton("暂停",this);
startButton->move(860,100);
failButton->move(860,150);
startButton->setFont(font2);
failButton->setFont(font2);
connect(startButton,&QPushButton::clicked,[=](){
isGameStart=true;
timer->start(moveTimeout);
sound = new QSound(":res/Trepak.wav");
sound->play();
sound->setLoops(-1);
});
connect(failButton,&QPushButton::clicked,[=](){
isGameStart=false;
timer->stop();
sound->stop();
3.10 实现定时器的超时槽函数
timer = new QTimer(this);
connect(timer,&QTimer::timeout,[=]()
{
int cont =1;
if(snakeList.front().intersects(foodRect))
{
createNewFood();
cont++;
QSound::play(":/res/eatfood.wav");
}
while(cont--)
{
switch (moveDirect) {
case SnakeDirect::UP :
moveUp();
break;
case SnakeDirect::DOWN:
moveDown();
break;
case SnakeDirect::LEFT:
moveLeft();
break;
case SnakeDirect::RIGHT:
moveRight();
break;
}
}
snakeList.pop_back();
update();
});
3.11 设置游戏退出按钮
//退出游戏按钮
QPushButton *backButton = new QPushButton("退出",this);
backButton->move(860,700);
backButton->setFont(font2);
QMessageBox * messbox = new QMessageBox(this);
QPushButton * ok = new QPushButton("ok");
QPushButton * no = new QPushButton("no");
messbox->addButton(ok,QMessageBox::AcceptRole);
messbox->addButton(no,QMessageBox::ResetRole);
messbox->setText("确定要退出游戏吗?");
connect(backButton,&QPushButton::clicked,[=](){
messbox->show();
messbox->exec();
QSound::play("clicked.wav");
GameSelect* select = new GameSelect;
if(messbox->clickedButton()==ok)
{
this->close();
select->show();
}else{
messbox->close();
}
});
3.12 获取历史战绩
//创建历史记录按钮
QPushButton * hisbutton = new QPushButton("历史记录",this);
//将样式设置到按钮中
hisbutton->setFont(font);
//设置按钮在窗口中的位置
hisbutton->move(400,470);
connect(hisbutton,&QPushButton::clicked,[=](){
QWidget * widget = new QWidget();
widget->setFixedSize(200,80);
widget->setWindowTitle("历史战绩");
QTextEdit * edit = new QTextEdit(widget);
edit->setFixedSize(200,80);
edit->setFont(font);
QFile file("C:/Users/26256/Desktop/his.txt");
file.open(QIODevice::ReadOnly);
QTextStream in(&file);
int data = in.readLine().toInt();
edit->append("得分为:");
edit->append(QString::number(data));
widget->show();
});
四.贪吃蛇移动的逻辑讲解
1.贪吃蛇的一次普通移动:
调用moveup等移动函数,创建一个新的头结点,头插到蛇的链表结构中,此时链表多一个节点。
调用链表的pop_back()函数尾删,保持链表整体长度与原来一致。
2.贪吃蛇吃到食物的一次移动:
记录吃到的食物数量cont,调用cont+1次移动函数,使链表头部多cont+1个节点。最后尾删一次,使链表长度比原来长cont。
3.贪吃蛇的持续移动:
创建定时器,将信号与相应逻辑的槽函数关联。定时发送信号,执行槽函数,实现逻辑。
原文链接: