目录
[1. 创建项目](#1. 创建项目)
[2. 添加项目资源文件](#2. 添加项目资源文件)
[1. 游戏大厅界面](#1. 游戏大厅界面)
[2. 关卡选择界面](#2. 关卡选择界面)
[3. 游戏房间界面](#3. 游戏房间界面)
[3.1 封装贪吃蛇数据结构](#3.1 封装贪吃蛇数据结构)
[3.2 初始化游戏房间界面](#3.2 初始化游戏房间界面)
[3.3 蛇的移动](#3.3 蛇的移动)
[3.4 初始化贪吃蛇本体和食物节点](#3.4 初始化贪吃蛇本体和食物节点)
[3.5 实现定时器的超时槽函数](#3.5 实现定时器的超时槽函数)
[3.6 实现各个方向的移动](#3.6 实现各个方向的移动)
[3.7 重写绘图事件函数进行渲染](#3.7 重写绘图事件函数进行渲染)
[3.8 检查是否自己会撞到自己](#3.8 检查是否自己会撞到自己)
[3.9 设置游戏开始和游戏暂停按钮](#3.9 设置游戏开始和游戏暂停按钮)
[3.10 设置退出游戏按钮](#3.10 设置退出游戏按钮)
[3.11 获取历史战绩](#3.11 获取历史战绩)
一、项目介绍
贪吃蛇游戏是一款休闲益智类游戏。它通过控制蛇头方向吃食物,从而使得蛇变得越来越长。
在本游戏中设置了上下左右四个方向键来控制蛇的移动方向。食物的产生是随机生成的,当蛇每吃一次食物就会增加一节身体,同时游戏积分也会相应的加一。
在本游戏的设计中,蛇的身体会越吃越长,身体越长对应的难度就越大,因为一旦蛇头和身体相交游戏就会结束。
本项目使用 Qt 实现一款简单的贪吃蛇游戏。
主要界面如下:
界面一:游戏大厅界面
界面二:关卡选择界面
界面三:游戏界面
最终游戏效果:
二、项目创建与资源配置
1. 创建项目
🌴****游戏大厅界面:
- 打开Qt-Creator 创建项目。 注意项目名中不能包含空格、回车、中文等特殊字符
- 选择基类为QWidget, 类名为GameHall代表游戏大厅界面
- 选择MinGW 64-bit编译套件
🌴关卡选择界面:
- 添加新文件
- 继承自QWidget, 类名为GameSelect代表游戏关卡界面
- 点击完成
🌴游戏房间界面:
- 添加新文件
- 继承自QWidget, 类名为GameRoom代表游戏房间界面
- 点击完成
2. 添加项目资源文件
- 拷贝资源文件到项目文件中
- 在项目中添加资源文件
- 给资源文件起名为:res, 点击下一步
- 点击完成
- 添加前缀
- 添加资源文件
三、项目实现
1. 游戏大厅界面
游戏大厅界面比较简单, 只有一个背景图和一个按钮。
- 背景图的渲染,我们通过QT的绘图事件完成。
cpp
#include <QPainter>
void GameHall::paintEvent(QPaintEvent *event)
{
(void)event;// 去掉警告小技巧
QPainter painter(this);// 实例化画家对象
QPixmap pix(":res/game_hall.png");// 实例化绘画设备
painter.drawPixmap(0, 0, this->width(), this->height(), pix);// 绘画
}
- 按钮的响应我们通过QT的信号和槽机制完成。
cpp
#include "gameselect.h"
#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->setWindowTitle("贪吃蛇游戏");
this->setWindowIcon(QIcon(":res/ico.png"));
// 设置开始游戏按钮
QPushButton* startBtn = new QPushButton(this);
startBtn->setText("开始游戏");
startBtn->move(this->width() * 0.4, this->height() * 0.7);
startBtn->setStyleSheet("QPushButton { border : 0px; }");
QFont font("华文行楷", 23, QFont::ExtraLight, false);
startBtn->setFont(font);
// 点击按钮就会进入游戏关卡界面
GameSelect* select = new GameSelect;
connect(startBtn, &QPushButton::clicked, [=](){
select->setGeometry(this->geometry());// 设置窗口固定
this->close();// 关闭游戏大厅界面
select->show();// 显示游戏关卡界面
// 注意:使用 QSound 类时, 需要添加模块:multimedia
QSound::play(":res/clicked.wav");// 添加按钮点击音效
});
}
效果示例:
2. 关卡选择界面
关卡选择界面包含一个背景图和五个按钮,背景图的绘制和游戏大厅背景图绘制⼀样,同样使用的是Qt中的绘图事件。
- 背景图的渲染,我们通过QT的绘图事件完成。
cpp
#include <QPainter>
void GameSelect::paintEvent(QPaintEvent *event)
{
(void)event;
QPainter painter(this);// 实例化画家对象
QPixmap pix(":res/game_select.png");// 实例化绘画设备
painter.drawPixmap(0, 0, this->width(), this->height(), pix);// 绘画
}
- 按钮的响应我们通过QT的信号和槽机制完成。
cpp
#include "gameselect.h"
#include "gamehall.h"
#include "gameroom.h"
#include <QIcon>
#include <QFont>
#include <QPushButton>
#include <QSound>
#include <QFile>
#include <QTextStream>
#include <QTextEdit>
#include <QDateTime>
GameSelect::GameSelect(QWidget *parent) : QWidget(parent)
{
// 设置游戏关卡界面
this->setFixedSize(1000, 800);
this->setWindowTitle("关卡选择");
this->setWindowIcon(QIcon(":res/ico.png"));
// 设置字体格式
QFont font("华文行楷", 20, QFont::ExtraLight, false);
// 创建返回按钮
QPushButton* backBtn = new QPushButton(this);
QPixmap pix(":res/back.png");
backBtn->setIcon(pix);
backBtn->move(this->width() * 0.9, this->height() * 0.9);
connect(backBtn, &QPushButton::clicked, [=](){
emit this->backGameHall();// emit 关键字触发信号
QSound::play(":res/clicked.wav");
});
// 返回游戏大厅界面
connect(this, &GameSelect::backGameHall, [=](){
this->hide();
GameHall* gameHall = new GameHall;
gameHall->show();
});
GameRoom* gameRoom = new GameRoom();
// 设置简单模式按钮
QPushButton* simpleBtn = new QPushButton(this);
simpleBtn->setStyleSheet("QPushButton { background-color:#0072C6; color:white; "
"border:2px groove gray; border-radius:10px; padding:6px; }");
simpleBtn->setFont(font);
simpleBtn->setText("简单模式");
simpleBtn->move(395, 140);
connect(simpleBtn, &QPushButton::clicked, [=](){
gameRoom->setTimeout(300);
gameRoom->setGeometry(this->geometry());// 设置窗口固定
this->hide();
gameRoom->show();
QSound::play(":res/clicked.wav");
});
// 设置正常按钮模式
QPushButton* normalBtn = new QPushButton(this);
normalBtn->setStyleSheet("QPushButton { background-color:#0072C6; color:white; "
"border:2px groove gray; border-radius:10px; padding:6px; }");
normalBtn->setFont(font);
normalBtn->setText("正常模式");
normalBtn->move(395, 260);
connect(normalBtn, &QPushButton::clicked, [=](){
gameRoom->setTimeout(200);
gameRoom->setGeometry(this->geometry());// 设置窗口固定
this->hide();
gameRoom->show();
QSound::play(":res/clicked.wav");
});
// 设置困难模式按钮
QPushButton* hardBtn = new QPushButton(this);
hardBtn->setStyleSheet("QPushButton { background-color:#0072C6; color:white; "
"border:2px groove gray; border-radius:10px; padding:6px; }");
hardBtn->setFont(font);
hardBtn->setText("困难模式");
hardBtn->move(395, 380);
connect(hardBtn, &QPushButton::clicked, [=](){
gameRoom->setTimeout(100);
gameRoom->setGeometry(this->geometry());// 设置窗口固定
this->hide();
gameRoom->show();
QSound::play(":res/clicked.wav");
});
// 获取历史战绩
QPushButton* hisBtn = new QPushButton(this);
hisBtn->setStyleSheet("QPushButton { background-color:#0072C6; color:white; "
"border:2px groove gray; border-radius:10px; padding:6px; }");
hisBtn->setFont(font);
hisBtn->setText("历史战绩");
hisBtn->move(395, 500);
connect(hisBtn, &QPushButton::clicked, [=](){
QSound::play(":res/clicked.wav");
QFile file("D:/code/qt/Snake/1.txt");
file.open(QIODevice::ReadOnly);
QTextStream in(&file);// 创建文本流对象
int data = in.readLine().toInt();// 读取第一行作为整形数据
QWidget* newWidget = new QWidget;// 一定不能添加到对象书上
newWidget->setWindowTitle("历史战绩");
newWidget->setFixedSize(500, 300);
QTextEdit* edit = new QTextEdit(newWidget);// 将编辑框置于新窗口上
edit->setFont(font);
edit->setFixedSize(500, 300);
// 获取系统时间
QDateTime currentTime = QDateTime::currentDateTime();
QString time = currentTime.toString("yyyy-MM-dd hh:mm:ss");
edit->setText("时间:");
edit->append(time);
edit->setText("得分为:");
edit->append(QString::number(data));
newWidget->show();
});
}
效果示例:
3. 游戏房间界面
游戏房间界面包含下面几个部分:
- 背景图的绘制。
- 蛇的绘制、蛇的移动、判断蛇是否会撞到自己。
- 积分的累加和绘制。
在这里我们要考虑几个比较核心的问题:
1. 怎么让蛇动起来?
- 我们可以用一个链表表示贪吃蛇,一个小方块表示蛇的一个节点, 我们设置蛇的默认长度为3;
- 向上移动的逻辑就是在蛇的上方加入一个小方块, 然后把最后一个小方块删除即可;
- 需要用到定时器 Qtimer 每100 - 200ms 重新渲染。
2. 怎么判断蛇有没有吃到食物?
- 判断蛇头和食物的坐标是否相交,Qt 有相关的接口调用。
3. 怎么控制蛇的移动?
- 借助QT的实践机制实现, 重写keyPressEvent即可, 在函数中监控想要的键盘事件即可。
- 我们通过绘制四个按钮,使用信号和槽的机制控制蛇的上、下、左、右移动方向。
3.1 封装贪吃蛇数据结构
cpp
#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 startGame();
void setTimeout(int count) {moveTimeout = count;}
void createNewFood();// 生成食物
void moveUp();// 蛇向上移动
void moveDown();// 蛇向下移动
void moveLeft();// 蛇向左移动
void moveRight();// 蛇向右移动
bool checkFail();// 判断游戏是否结束
private:
snakeDirect moveDirect = snakeDirect::UP;// 定义蛇的移动方向,默认朝上
bool isGameStart = false;// 表示是否开始游戏
QTimer* timer;// 定时器
QList<QRectF> snakeList;// 表示贪吃蛇链表
QRectF foodRect;// 表示食物节点
const int kDefaultTimeout = 200;// 表示贪吃蛇默认移动时间
const int kSnakeNodeWidth = 20;// 表示蛇身体节点的宽度
const int kSnakeNodeHeight = 20;// 表示蛇身体节点的高度
int moveTimeout = kDefaultTimeout;
QSound* sound;// 音频
};
3.2 初始化游戏房间界面
- 设置窗口大小、标题、图标等
cpp
#include <QIcon>
GameRoom::GameRoom(QWidget *parent) : QWidget(parent)
{
// 初始化窗口设置
this->setFixedSize(1000, 800);
this->setWindowTitle("贪吃蛇游戏");
this->setWindowIcon(QIcon(":res/ico.png"));
}
3.3 蛇的移动
- 蛇的移动方向为:上、下、左、右。通过在游戏房间中布局四个按钮来控制蛇的移动方向。
- 注意: 这里贪吃蛇不允许直接掉头, 比如当前是向上的, 不能直接修改为向下。
cpp
// 方向控制
QPushButton* Up = new QPushButton(this);
QPushButton* Down = new QPushButton(this);
QPushButton* Left = new QPushButton(this);
QPushButton* Right = new QPushButton(this);
QString buttonStyle =
"QPushButton {"
" border: 2px solid #8f8f8f;"
" border-radius: 10px;"
" background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #f0f0f0, stop: 1 #bbbbbb);"
" color: #000000;"
"}"
"QPushButton:hover {"
" background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #ffffff, stop: 1 #8f8f8f);"
"}"
"QPushButton:pressed {"
" background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #dadada, stop: 1 #949494);"
"}";
Up->move(880, 400);
Down->move(880, 480);
Left->move(840, 440);
Right->move(920, 440);
QFont font("楷体", 24, QFont::ExtraLight, false);
Up->setStyleSheet(buttonStyle);
Up->setFont(font);
Up->setText("↑");
Down->setStyleSheet(buttonStyle);
Down->setFont(font);
Down->setText("↓");
Left->setStyleSheet(buttonStyle);
Left->setFont(font);
Left->setText("←");
Right->setStyleSheet(buttonStyle);
Right->setFont(font);
Right->setText("→");
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;
}
});
3.4 初始化贪吃蛇本体和食物节点
cpp
GameRoom::GameRoom(QWidget *parent) : QWidget(parent)
{
// 初始化贪吃蛇
snakeList.push_back(QRectF(this->width() * 0.5, this->height() * 0.5, kSnakeNodeWidth, kSnakeNodeHeight));
moveUp();
moveUp();
createNewFood();// 初始化食物
}
- moveUp() 的功能是将蛇向上移动一次, 即在上方新增一个节点, 但不删除尾部节点。
- createNewFood() 方法的功能是随机创建一个食物节点:
cpp
void GameRoom::createNewFood()
{
foodRect = QRectF(
qrand() % (this->width() / kSnakeNodeWidth) * kSnakeNodeWidth,
qrand() % (this->height() / kSnakeNodeHeight) *kSnakeNodeHeight,
kSnakeNodeWidth,
kSnakeNodeHeight);
}
3.5 实现定时器的超时槽函数
定时器是为了实现每隔一段时间能处理移动的逻辑并且更新绘图事件。
1. 首先, 需要判断蛇头和食物节点坐标是否相交
- 如果相交, 需要创建新的食物节点, 并且需要更新蛇的长度, 所以 count 需要 +1 ;
- 如果不相交, 那么直接处理蛇的移动即可。
2. 根据蛇移动方向 moveDirect 来处理蛇的移动, 处理方法是在前方加一个, 并且删除后方节点;
3. 重新触发绘图事件, 更新渲染。
cpp
GameRoom::GameRoom(QWidget *parent) : QWidget(parent)
{
timer = new QTimer(this);
connect(timer, &QTimer::timeout, [=](){
int count = 1;
if (snakeList.front().intersects(foodRect)){
createNewFood();
++count;
QSound::play(":res/eatfood.wav");
}
while (count--) {
// 处理蛇的移动
switch (moveDirect) {
case snakeDirect::UP:
moveUp();
break;
case snakeDirect::DOWN:
moveDown();
break;
case snakeDirect::LEFT:
moveLeft();
break;
case snakeDirect::RIGHT:
moveRight();
break;
default:
qDebug() << "非法移动方向";
break;
}
}
// 删除最后一个节点
snakeList.pop_back();
update();
});
}
3.6 实现各个方向的移动
各个方向的移动主要在于更新矩形节点的坐标, 要注意的是一定要处理边界的情况, 当边界不够存储一个新的节点时, 我们需要处理穿墙逻辑。
- 实现向上移动
cpp
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));
}
- 实现向下移动
cpp
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));
}
- 实现向左移动
cpp
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));
}
- 实现向右移动
cpp
void GameRoom::moveRight()
{
QPointF leftTop;// 左上角坐标
QPointF rightBottom;// 右下角坐标
auto snakeNode = snakeList.front();
int headX = snakeNode.x();
int headY = snakeNode.y();
// 如果右面剩余的空间不够放入⼀个新的节点, 相当于到墙边了, 要处理穿墙逻辑
if (headX > 760){
leftTop = QPointF(0, headY);
}else {
leftTop = snakeNode.topRight();// 返回矩形右上角的坐标
}
rightBottom = leftTop + QPointF(kSnakeNodeWidth, kSnakeNodeHeight);
snakeList.push_front(QRectF(leftTop, rightBottom));
}
3.7 重写绘图事件函数进行渲染
重写基类的 paintEvent() 方法进行渲染:
- 渲染背景图
- 渲染蛇头
- 渲染蛇身体
- 渲染蛇尾巴
- 渲染右边游戏控制区域
- 渲染食物节点
- 渲染当前分数
- 游戏结束渲染 game over!
cpp
void GameRoom::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
QPixmap pix;
pix.load(":res/game_room.png");
painter.drawPixmap(0, 0, 800, 800, pix);
painter.setRenderHint(QPainter::Antialiasing);// 设置抗锯齿
// 渲染蛇头
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);
// 渲染食物
pix.load(":res/food.bmp");
painter.drawPixmap(foodRect.x(), foodRect.y(), foodRect.width(), foodRect.height(), pix);
// 渲染右边区域
pix.load(":res/bg1.png");
painter.drawPixmap(800, 0, 200, 1000, pix);
// 渲染分数
pix.load(":res/sorce_bg.png");
painter.drawPixmap(this->width() * 0.85, this->height() * 0.02, 90, 40, pix);
QPen pen;
pen.setColor(Qt::black);
painter.setPen(pen);
QFont font("楷体", 22, QFont::ExtraLight, false);
painter.setFont(font);
painter.drawText(this->width() * 0.9, this->height() * 0.06, QString("%1").arg(snakeList.size()));
// 如果失败,渲染 GAME OVER
if (checkFail()){
pen.setColor(Qt::red);
painter.setPen(pen);
QFont font("楷体", 50, QFont::ExtraLight, false);
painter.setFont(font);
painter.drawText(this->width() * 0.5 - 250, this->height() * 0.5, QString("GAME OVER"));
timer->stop();
QSound::play(":res/gameover.wav");
sound->stop();
}
}
3.8 检查是否自己会撞到自己
cpp
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.9 设置游戏开始和游戏暂停按钮
cpp
// 开始游戏 & 暂停游戏
QPushButton* startBtn = new QPushButton(this);
QPushButton* stopBtn = new QPushButton(this);
QFont ft("楷体", 20, QFont::ExtraLight, false);
// 设置按钮的位置
startBtn->move(860, 150);
stopBtn->move(860, 200);
// 设置按钮文本
startBtn->setText("开始");
stopBtn->setText("暂停");
// 设置按钮样式
startBtn->setStyleSheet("QPushButton {"
"background-color: white;"// 按钮背景颜色
"color: #4CAF50;" // 文字颜色
"border: 2px solid #4CAF50;" // 边框
"border-radius: 8px;" // 边框圆角
"padding: 10px 20px;" // 内边距
"}");
stopBtn->setStyleSheet("QPushButton {"
"background-color: white;"// 按钮背景颜色
"color: #4CAF50;" // 文字颜色
"border: 2px solid #4CAF50;" // 边框
"border-radius: 8px;" // 边框圆角
"padding: 10px 20px;" // 内边距
"}");
// 设置按钮字体格式
startBtn->setFont(ft);
stopBtn->setFont(ft);
connect(startBtn, &QPushButton::clicked, [=](){
sound = new QSound(":res/Trepak.wav");
sound->play();
sound->setLoops(-1);// 循环播放
isGameStart = true;
timer->start(moveTimeout);
});
connect(stopBtn, &QPushButton::clicked, [=](){
sound->stop();
isGameStart = false;
timer->stop();
});
3.10 设置退出游戏按钮
当我们点击退出游戏按钮时,当前游戏房间窗口不会立即退出,而是会弹窗提示,提示我们是否要退出游戏,效果如下图示:
这个弹窗提示我们是通过 Qt 中的消息盒子来实现的,具体实现过程如下:
cpp
// 退出游戏按钮
QPushButton* exitGame = new QPushButton(this);
exitGame->setStyleSheet("QPushButton {"
"background-color: white;" // 按钮背景颜色
"color: #4CAF50;" // 文字颜色
"border: 2px solid #4CAF50;" // 边框
"border-radius: 8px;" // 边框圆角
"padding: 10px 20px;" // 内边距
"}");
exitGame->move(825, 730);
exitGame->setText("退出");
exitGame->setFont(font);
// 消息提示
QMessageBox* messageBox = new QMessageBox(this);
QPushButton* okbtn = new QPushButton("ok");
QPushButton* cancelbtn = new QPushButton("cancel");
messageBox->setWindowTitle("退出游戏");// 设置消息对话框的标题
messageBox->setText("确认退出游戏吗?");// 设置消息对话框内容
messageBox->setIcon(QMessageBox::Question); //设置消息对话框类型
messageBox->addButton(okbtn, QMessageBox::AcceptRole);//Accept Role:接受的角色
messageBox->addButton(cancelbtn, QMessageBox::RejectRole);//Reject Role:排斥作用
connect(exitGame, &QPushButton::clicked, [=](){
messageBox->show();// 消息提示
QSound::play(":res/clicked.wav");
messageBox->exec();// 阻塞等待用户输入
GameSelect* s = new GameSelect;
if (messageBox->clickedButton() == okbtn){
this->close();
//如果点击了 Ok 按钮,那么就会跳转到游戏关卡界⾯
s->show();
s->setGeometry(this->geometry());
QSound::play(":res/clicked.wav");
}else{
messageBox->close();
QSound::play(":res/clicked.wav");
}
});
3.11 获取历史战绩
对于历史战绩的获取我们是通过 Qt 中的读写文件操作来实现的。具体实现过程如下:
- 写文件:往文件中写入蛇的长度
cpp
int c = snakeList.size();
QFile file("D:/code/qt/Snake/1.txt");
if (file.open(QIODevice::WriteOnly | QIODevice::Text)){
QTextStream out(&file);
int num = c;
out << num;// 将int类型数据写入文件
file.close();
}
- 读文件:读取写入文件中蛇的长度
cpp
QFile file("D:/code/qt/Snake/1.txt");
file.open(QIODevice::ReadOnly);
QTextStream in(&file);// 创建文本流对象
int data = in.readLine().toInt();// 读取第一行作为整形数据
效果示例:
四、项目源码
🌵gamehall.h
cpp
#ifndef GAMEHALL_H
#define GAMEHALL_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class GameHall; }
QT_END_NAMESPACE
class GameHall : public QWidget
{
Q_OBJECT
public:
GameHall(QWidget *parent = nullptr);
~GameHall();
//重写绘图事件,绘制游戏大厅界面
void paintEvent(QPaintEvent *event);
private:
Ui::GameHall *ui;
};
#endif // GAMEHALL_H
🌵gamehall.cpp
cpp
#include "gamehall.h"
#include "ui_gamehall.h"
#include "gameselect.h"
#include <QPainter>
#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->setWindowTitle("贪吃蛇游戏");
this->setWindowIcon(QIcon(":res/ico.png"));
// 设置开始游戏按钮
QPushButton* startBtn = new QPushButton(this);
startBtn->setText("开始游戏");
startBtn->move(this->width() * 0.4, this->height() * 0.7);
startBtn->setStyleSheet("QPushButton { border : 0px; }");
QFont font("华文行楷", 23, QFont::ExtraLight, false);
startBtn->setFont(font);
// 点击按钮就会进入游戏关卡界面
GameSelect* select = new GameSelect;
connect(startBtn, &QPushButton::clicked, [=](){
select->setGeometry(this->geometry());// 设置窗口固定
this->close();// 关闭游戏大厅界面
select->show();// 显示游戏关卡界面
// 注意:使用 QSound 类时, 需要添加模块:multimedia
QSound::play(":res/clicked.wav");// 添加按钮点击音效
});
}
GameHall::~GameHall()
{
delete ui;
}
void GameHall::paintEvent(QPaintEvent *event)
{
(void)event;// 去掉警告小技巧
QPainter painter(this);// 实例化画家对象
QPixmap pix(":res/game_hall.png");// 实例化绘画设备
painter.drawPixmap(0, 0, this->width(), this->height(), pix);// 绘画
}
🍇gameselect.h
cpp
#ifndef GAMESELECT_H
#define GAMESELECT_H
#include <QWidget>
class GameSelect : public QWidget
{
Q_OBJECT
public:
explicit GameSelect(QWidget *parent = nullptr);
//重写绘图事件,绘制游戏关卡选择界面
void paintEvent(QPaintEvent *event);
signals:
void backGameHall();// 信号声明
};
#endif // GAMESELECT_H
🍇gameselect.cpp
cpp
#include "gameselect.h"
#include "gamehall.h"
#include "gameroom.h"
#include <QPainter>
#include <QIcon>
#include <QFont>
#include <QPushButton>
#include <QSound>
#include <QFile>
#include <QTextStream>
#include <QTextEdit>
#include <QDateTime>
GameSelect::GameSelect(QWidget *parent) : QWidget(parent)
{
// 设置游戏关卡界面
this->setFixedSize(1000, 800);
this->setWindowTitle("关卡选择");
this->setWindowIcon(QIcon(":res/ico.png"));
// 设置字体格式
QFont font("华文行楷", 20, QFont::ExtraLight, false);
// 创建返回按钮
QPushButton* backBtn = new QPushButton(this);
QPixmap pix(":res/back.png");
backBtn->setIcon(pix);
backBtn->move(this->width() * 0.9, this->height() * 0.9);
connect(backBtn, &QPushButton::clicked, [=](){
emit this->backGameHall();// emit 关键字触发信号
QSound::play(":res/clicked.wav");
});
// 返回游戏大厅界面
connect(this, &GameSelect::backGameHall, [=](){
this->hide();
GameHall* gameHall = new GameHall;
gameHall->show();
});
GameRoom* gameRoom = new GameRoom();
// 设置简单模式按钮
QPushButton* simpleBtn = new QPushButton(this);
simpleBtn->setStyleSheet("QPushButton { background-color:#0072C6; color:white; "
"border:2px groove gray; border-radius:10px; padding:6px; }");
simpleBtn->setFont(font);
simpleBtn->setText("简单模式");
simpleBtn->move(395, 140);
connect(simpleBtn, &QPushButton::clicked, [=](){
gameRoom->setTimeout(300);
gameRoom->setGeometry(this->geometry());// 设置窗口固定
this->hide();
gameRoom->show();
QSound::play(":res/clicked.wav");
});
// 设置正常按钮模式
QPushButton* normalBtn = new QPushButton(this);
normalBtn->setStyleSheet("QPushButton { background-color:#0072C6; color:white; "
"border:2px groove gray; border-radius:10px; padding:6px; }");
normalBtn->setFont(font);
normalBtn->setText("正常模式");
normalBtn->move(395, 260);
connect(normalBtn, &QPushButton::clicked, [=](){
gameRoom->setTimeout(200);
gameRoom->setGeometry(this->geometry());// 设置窗口固定
this->hide();
gameRoom->show();
QSound::play(":res/clicked.wav");
});
// 设置困难模式
QPushButton* hardBtn = new QPushButton(this);
hardBtn->setStyleSheet("QPushButton { background-color:#0072C6; color:white; "
"border:2px groove gray; border-radius:10px; padding:6px; }");
hardBtn->setFont(font);
hardBtn->setText("困难模式");
hardBtn->move(395, 380);
connect(hardBtn, &QPushButton::clicked, [=](){
gameRoom->setTimeout(100);
gameRoom->setGeometry(this->geometry());// 设置窗口固定
this->hide();
gameRoom->show();
QSound::play(":res/clicked.wav");
});
// 获取历史战绩
QPushButton* hisBtn = new QPushButton(this);
hisBtn->setStyleSheet("QPushButton { background-color:#0072C6; color:white; "
"border:2px groove gray; border-radius:10px; padding:6px; }");
hisBtn->setFont(font);
hisBtn->setWindowIcon(QIcon(":res/ico.png"));
hisBtn->setText("历史战绩");
hisBtn->move(395, 500);
connect(hisBtn, &QPushButton::clicked, [=](){
QSound::play(":res/clicked.wav");
QFile file("D:/code/qt/Snake/1.txt");
file.open(QIODevice::ReadOnly);
QTextStream in(&file);// 创建文本流对象
int data = in.readLine().toInt();// 读取第一行作为整形数据
QWidget* newWidget = new QWidget;// 一定不能添加到对象书上
newWidget->setWindowTitle("历史战绩");
newWidget->setWindowIcon(QIcon(":res/ico.png"));
newWidget->setFixedSize(500, 300);
QTextEdit* edit = new QTextEdit(newWidget);// 将编辑框置于新窗口上
edit->setFont(font);
edit->setFixedSize(500, 300);
// 获取系统时间
QDateTime currentTime = QDateTime::currentDateTime();
QString time = currentTime.toString("yyyy-MM-dd hh:mm:ss");
edit->setText("时间:");
edit->append(time);
edit->setText("得分为:");
edit->append(QString::number(data));
newWidget->show();
});
}
//重写绘图事件,绘制游戏关卡选择界面
void GameSelect::paintEvent(QPaintEvent *event)
{
(void)event;
QPainter painter(this);// 实例化画家对象
QPixmap pix(":res/game_select.png");// 实例化绘画设备
painter.drawPixmap(0, 0, this->width(), this->height(), pix);// 绘画
}
🌴gameroom.h
cpp
#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 startGame();
void setTimeout(int count) {moveTimeout = count;}
void createNewFood();// 生成食物
void moveUp();// 蛇向上移动
void moveDown();// 蛇向下移动
void moveLeft();// 蛇向左移动
void moveRight();// 蛇向右移动
bool checkFail();// 判断游戏是否结束
private:
snakeDirect moveDirect = snakeDirect::UP;// 定义蛇的移动方向,默认朝上
bool isGameStart = false;// 表示是否开始游戏
QTimer* timer;// 定时器
QList<QRectF> snakeList;// 表示贪吃蛇链表
QRectF foodRect;// 表示食物节点
const int kDefaultTimeout = 200;// 表示贪吃蛇默认移动时间
const int kSnakeNodeWidth = 20;// 表示蛇身体节点的宽度
const int kSnakeNodeHeight = 20;// 表示蛇身体节点的高度
int moveTimeout = kDefaultTimeout;
QSound* sound;// 音频
};
#endif // GAMEROOM_H
🌴gameroom.cpp
cpp
#include "gameroom.h"
#include "gameselect.h"
#include <QIcon>
#include <QPixmap>
#include <QPainter>
#include <QTimer>
#include <QDebug>
#include <QFile>
#include <QPushButton>
#include <QMessageBox>
#include <QTextEdit>
#include <QSound>
GameRoom::GameRoom(QWidget *parent) : QWidget(parent)
{
// 初始化窗口设置
this->setFixedSize(1000, 800);
this->setWindowTitle("贪吃蛇游戏");
this->setWindowIcon(QIcon(":res/ico.png"));
// 初始化贪吃蛇
snakeList.push_back(QRectF(this->width() * 0.5, this->height() * 0.5, kSnakeNodeWidth, kSnakeNodeHeight));
moveUp();
moveUp();
createNewFood();// 初始化食物
timer = new QTimer(this);
connect(timer, &QTimer::timeout, [=](){
int count = 1;
if (snakeList.front().intersects(foodRect)){
createNewFood();
++count;
QSound::play(":res/eatfood.wav");
}
while (count--) {
// 处理蛇的移动
switch (moveDirect) {
case snakeDirect::UP:
moveUp();
break;
case snakeDirect::DOWN:
moveDown();
break;
case snakeDirect::LEFT:
moveLeft();
break;
case snakeDirect::RIGHT:
moveRight();
break;
default:
qDebug() << "非法移动方向";
break;
}
}
// 删除最后一个节点
snakeList.pop_back();
update();
});
// 开始游戏 & 暂停游戏
QPushButton* startBtn = new QPushButton(this);
QPushButton* stopBtn = new QPushButton(this);
QFont ft("楷体", 15, QFont::ExtraLight, false);
// 设置按钮的位置
startBtn->move(840, 150);
stopBtn->move(840, 220);
// 设置按钮文本
startBtn->setText("开始");
stopBtn->setText("暂停");
// 设置按钮样式
startBtn->setStyleSheet("QPushButton {"
"background-color: white;"// 按钮背景颜色
"color: #4CAF50;" // 文字颜色
"border: 2px solid #4CAF50;" // 边框
"border-radius: 8px;" // 边框圆角
"padding: 10px 20px;" // 内边距
"}");
stopBtn->setStyleSheet("QPushButton {"
"background-color: white;"// 按钮背景颜色
"color: #4CAF50;" // 文字颜色
"border: 2px solid #4CAF50;" // 边框
"border-radius: 8px;" // 边框圆角
"padding: 10px 20px;" // 内边距
"}");
// 设置按钮字体格式
startBtn->setFont(ft);
stopBtn->setFont(ft);
connect(startBtn, &QPushButton::clicked, [=](){
sound = new QSound(":res/Trepak.wav");
sound->play();
sound->setLoops(-1);// 循环播放
isGameStart = true;
timer->start(moveTimeout);
});
connect(stopBtn, &QPushButton::clicked, [=](){
sound->stop();
isGameStart = false;
timer->stop();
});
// 方向控制
QPushButton* Up = new QPushButton(this);
QPushButton* Down = new QPushButton(this);
QPushButton* Left = new QPushButton(this);
QPushButton* Right = new QPushButton(this);
QString buttonStyle =
"QPushButton {"
" border: 2px solid #8f8f8f;"
" border-radius: 10px;"
" background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #f0f0f0, stop: 1 #bbbbbb);"
" color: #000000;"
"}"
"QPushButton:hover {"
" background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #ffffff, stop: 1 #8f8f8f);"
"}"
"QPushButton:pressed {"
" background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #dadada, stop: 1 #949494);"
"}";
Up->move(880, 400);
Down->move(880, 480);
Left->move(840, 440);
Right->move(920, 440);
QFont font("楷体", 15, QFont::ExtraLight, false);
Up->setStyleSheet(buttonStyle);
Up->setFont(font);
Up->setText("↑");
Down->setStyleSheet(buttonStyle);
Down->setFont(font);
Down->setText("↓");
Left->setStyleSheet(buttonStyle);
Left->setFont(font);
Left->setText("←");
Right->setStyleSheet(buttonStyle);
Right->setFont(font);
Right->setText("→");
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* exitGame = new QPushButton(this);
exitGame->setStyleSheet("QPushButton {"
"background-color: white;" // 按钮背景颜色
"color: #4CAF50;" // 文字颜色
"border: 2px solid #4CAF50;" // 边框
"border-radius: 8px;" // 边框圆角
"padding: 10px 20px;" // 内边距
"}");
exitGame->move(840, 725);
exitGame->setText("退出");
exitGame->setFont(font);
// 消息提示
QMessageBox* messageBox = new QMessageBox(this);
QPushButton* okbtn = new QPushButton("ok");
QPushButton* cancelbtn = new QPushButton("cancel");
messageBox->setWindowIcon(QIcon(":res/ico.png"));// 设置消息对话框的图标
messageBox->setWindowTitle("退出游戏");// 设置消息对话框的标题
messageBox->setText("确认退出游戏吗?");// 设置消息对话框内容
messageBox->setIcon(QMessageBox::Question); //设置消息对话框类型
messageBox->addButton(okbtn, QMessageBox::AcceptRole);//Accept Role:接受的角色
messageBox->addButton(cancelbtn, QMessageBox::RejectRole);//Reject Role:排斥作用
connect(exitGame, &QPushButton::clicked, [=](){
messageBox->show();// 消息提示
QSound::play(":res/clicked.wav");
messageBox->exec();// 阻塞等待用户输入
GameSelect* s = new GameSelect;
if (messageBox->clickedButton() == okbtn){
this->close();
//如果点击了 Ok 按钮,那么就会跳转到游戏关卡界⾯
s->show();
s->setGeometry(this->geometry());
QSound::play(":res/clicked.wav");
}else{
messageBox->close();
QSound::play(":res/clicked.wav");
}
});
}
// 重写绘图事件,实现贪吃蛇游戏游戏背景效果
void GameRoom::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
QPixmap pix;
pix.load(":res/game_room.png");
painter.drawPixmap(0, 0, 800, 800, pix);
painter.setRenderHint(QPainter::Antialiasing);// 设置抗锯齿
// 蛇 = 蛇头 + 蛇身体 + 蛇尾巴
// 渲染蛇头
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);
// 渲染食物
pix.load(":res/food.bmp");
painter.drawPixmap(foodRect.x(), foodRect.y(), foodRect.width(), foodRect.height(), pix);
// 渲染右边区域
pix.load(":res/bg1.png");
painter.drawPixmap(800, 0, 200, 1000, pix);
// 渲染分数
pix.load(":res/sorce_bg.png");
painter.drawPixmap(this->width() * 0.85, this->height() * 0.02, 100, 50, pix);
QPen pen;
pen.setColor(Qt::black);
painter.setPen(pen);
QFont font("楷体", 15, QFont::ExtraLight, false);
painter.setFont(font);
painter.drawText(this->width() * 0.9, this->height() * 0.075, QString("%1").arg(snakeList.size()));
// 往文件中写入得分
int c = snakeList.size();
QFile file("D:/code/qt/Snake/1.txt");
if (file.open(QIODevice::WriteOnly | QIODevice::Text)){
QTextStream out(&file);
int num = c;
out << num;// 将int类型数据写入文件
file.close();
}
// 如果失败,渲染 GAME OVER
if (checkFail()){
pen.setColor(Qt::red);
painter.setPen(pen);
QFont font("微软雅黑", 40, QFont::ExtraLight, false);
painter.setFont(font);
painter.drawText(this->width() * 0.1, this->height() * 0.5, QString("GAME OVER"));
timer->stop();
QSound::play(":res/gameover.wav");
sound->stop();
}
}
// 创建食物
void GameRoom::createNewFood()
{
foodRect = QRectF(
qrand() % (this->width() / kSnakeNodeWidth) * kSnakeNodeWidth,
qrand() % (this->height() / kSnakeNodeHeight) *kSnakeNodeHeight,
kSnakeNodeWidth,
kSnakeNodeHeight);
}
// 实现向上移动
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 > 760){
leftTop = QPointF(0, headY);
}else {
leftTop = snakeNode.topRight();// 返回矩形右上角的坐标
}
rightBottom = leftTop + QPointF(kSnakeNodeWidth, kSnakeNodeHeight);
snakeList.push_front(QRectF(leftTop, rightBottom));
}
// 判断游戏是否结束
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;
}