本次做的是QT项目:贪吃蛇
效果图:
我们先来看一下效果图:
游戏大厅的界面
首先是我们的第一个界面,游戏大厅的界面:
关卡选择 和 历史战绩 界面
接下来是类似于菜单页面的 关卡选择 和 历史战绩 界面:
吃一个食物是10分:
游戏房间界面
接下来就是我们的游戏房间界面:
分为三种模式:
简单模式
正常模式
复杂模式
准备材料:
- 背景图片
- 点击音效
- 部分图片作为点击时的图标
- 蛇的组成图片、食物图片、积分栏
创建项目(基本操作):
成功创建出一个项目。
前面没有截图的部分步骤按照默认的来即可。
根据我们分为三个大类的内容创建完整文件和导入需要的资源文件。
我们需要创建的 贪吃蛇项目 可以分为:游戏大厅、游戏关卡选择、游戏房间开始游戏。
需要导入的资源文件就是 游戏所需的背景图,音效等资源文件。
创建新的类
导入资源文件
创建成功后的界面
创建后的界面就是这样,我们再点击运行,查看程序是否能够正常运行,以免后续代码写好,最开始却出现了错误。
一、游戏大厅
我们的游戏大厅非常简单。
需要的就是一张背景图片、一个“开始游戏”按钮、点击按钮后的音效。
接下来,我们开始操作,第一部分的 游戏大厅 与 第二部分 模式选择(关卡难度)的操作室类似的,看懂了第一部分操作的话,就尝试一下第二部分的实现吧。
设计 gamehall.h 文件
首先,我们要 重写绘画对象 。使用 paintEvent 函数。
设计 gamehall.cpp 文件
其次,我们要在 gamehall.cpp 文件中定义 paintEvent函数 和 设计游戏界面属性和其他内容。
// 这里是 gamehall.cpp 文件
// 这里是 gamehall.cpp 文件
// 这里是 gamehall.cpp 文件
#include "gamehall.h"
#include "gameselect.h"
#include <QPainter>
#include <QPixmap>
#include <QIcon>
#include <QPushButton>
#include <QFont>
#include <QSound>
GameHall::GameHall(QWidget *parent) : QWidget(parent)
{
/* 设置界面属性 */
/* ------------------------------------------------------------------------------------------------------------ */
this->setWindowTitle("贪吃蛇大作战"); // 设计游戏窗口文字
this->setWindowIcon(QIcon(":res/ico.png")); // 设计游戏窗口图标
this->setFixedSize(1200,900); // 设计游戏界面大小
/* 创建 开始游戏 按钮,点击跳转至下一界面:关卡选择界面 */
/* 按钮设计 */
/* 注意:内容,文本格式,在窗口中的位置,边框问题(无边框),信号槽,点击需有音效 */
/* ------------------------------------------------------------------------------------------------------------ */
QPushButton *strBtn = new QPushButton(this);
strBtn->setText("开始游戏");
QFont font("方正舒体",40);
strBtn->setFont(font);
strBtn->move(this->width() * 0.5 - strBtn->width(),this->height() * 0.8);
strBtn->setStyleSheet("QPushButton{border:0px;}");
connect(strBtn,&QPushButton::clicked,[=](){
this->close();
GameSelect *gameSelect = new GameSelect;
gameSelect->setGeometry(this->geometry()); // gameselext窗口大小设计为gamehall窗口的大小
gameSelect->show(); // 显示出来,相当于跳转界面
QSound::play(":res/clicked.wav");// 点击音效
//我们可以发现 加上了 QSound的头文件,还是出现了红色下划线,显示出错,接下来去查看 QT助手 了解一下问题所在
});
}
void GameHall::paintEvent(QPaintEvent *event)
{
/* 绘制绘画对象 */
/* ------------------------------------------------------------------------------------------------------------ */
QPainter painter(this);
QPixmap pix(":res/game_hall.jpg");
painter.drawPixmap(0,0,this->width(),this->height(),pix);
}
现在我们的 gamehall.cpp 的代码已经写完了,但是发现 QSound 出现问题,无法运行,怎么解决这个问题呢?
查看QT助手,搜索 QSound 是如何使用的。
如何查看 QT助手?
找到你的安装路径下的 bin 文件中的 assistant.exe 程序。
我们可以看到它的使用条件。
后续的很多知识点都需要QT助手中查询了解他们的参数或者使用条件。
现在在 .pro 文件中添加上条件
接下来,我们可以对现在的代码进行测试了,出现游戏大厅的画面,并且点击按钮可以跳转下一界面,这部分代码就成功了。
关于头文件放置位置,可以放在 .h 文件中,方便后续其他文件直接引用.h文件,不用再多次敲写进文件中。可以写在 .cpp 文件中,三部分各自写在自己的 .cpp 文件中,基本互相不影响。
按照自己喜好 和 代码可读性 放置在文件中。
二、模式选择(关卡选择)
第二部分的处理 与 第一部分类似。
先是对该界面的 绘图对象进行重写,像在画布上布置内容一样,先选画纸(属性),再对界面中不能直接绘制的对象进行设计代码。
第二界面(关卡选择模式设计)
//这里是 gameselect.cpp 文件
//这里是 gameselect.cpp 文件
//这里是 gameselect.cpp 文件
#include "gameselect.h"
#include "gamehall.h" // 跳转返回到上一界面
#include "gameroom.h" // 跳转进入下一界面
GameSelect::GameSelect(QWidget *parent) : QWidget(parent)
{
/* 设置 该界面的属性 */
/* ------------------------------------------------------------------------------------------------------------ */
this->setWindowTitle("关卡选择"); // 设计游戏窗口文字
this->setWindowIcon(QIcon(":res/ico.png")); // 设计游戏窗口图标
this->setFixedSize(1200,900); // 设计游戏界面大小
/* 该界面的内容有 1.三种模式选择 2.一个返回上一界面的按钮 3.一个查看历史战绩的按钮 */
/* ------------------------------------------------------------------------------------------------------------ */
//一个返回上一界面的按钮
QPushButton *backBtn = new QPushButton(this);
backBtn->setIcon(QIcon(":res/back.png"));
backBtn->move(this->width() * 0.05,this->height() * 0.05);
backBtn->setFixedSize(50,50);
// 三种模式选择
QPushButton *simpleBtn = new QPushButton(this);
QPushButton *normalBtn = new QPushButton(this);
QPushButton *complexBtn = new QPushButton(this);
/* 设计文字按钮时,我们要考虑他们的 1.文本内容 2.文本格式(字体,大小等) 3.按钮位置 4.边界(边框问题等) */
/* ------------------------------------------------------------------------------------------------------------ */
QFont font("华文行楷",30);
simpleBtn->setText("简单模式");
normalBtn->setText("正常模式");
complexBtn->setText("困难模式");
simpleBtn->setFont(font);
normalBtn->setFont(font);
complexBtn->setFont(font);
simpleBtn->move(this->width() * 0.1,this->height() * 0.8);
normalBtn->move(this->width() * 0.3,this->height() * 0.8);
complexBtn->move(this->width() * 0.5,this->height() * 0.8);
simpleBtn->setStyleSheet("QPushButton{border:0px;}");
normalBtn->setStyleSheet("QPushButton{border:0px;}");
complexBtn->setStyleSheet("QPushButton{border:0px;}");
// 一个查看历史战绩的按钮
QPushButton *hisBtn = new QPushButton(this);
hisBtn->setText("历史战绩");
hisBtn->setFont(font);
hisBtn->move(this->width() * 0.7,this->height() * 0.8);
hisBtn->setStyleSheet("QPushButton{border:0px;}");
}
void GameSelect::paintEvent(QPaintEvent *event)
{
/* 重写绘画对象 */
QPainter painter(this);
QPixmap pix(":res/game_select.png");
painter.drawPixmap(0,0,this->width(),this->height(),pix);
}
我们在暂时不对按钮进行信号槽操作,而先查看界面,我们查看按钮的位置是否合适,字体大小等情况。
现在我们要对第二部分中的五个按钮进行 ** connect **操作 了。
依然是理解清楚每一个按钮关联的部分,存在现下无法完善的情况,但是在后续的代码续写中,可以逐渐完善内容。
左上的返回按钮是 跳转返回到第一界面,可以直接完成;
下方的三个关卡选择按钮则要联系到 第三部分 的 gameroom 界面,可以进行跳转,但要注意 区分难度模式 的变量在后续可能需要再补充进来;
历史战绩按钮的思考方向是,第三部分 游戏时得到的积分传递过来,所以也是需要联系到后续的某个变量。怎么 查看 历史战绩 ? 将一个文本编辑器放置在新的widget中,点击按钮后,弹出新的widget界面,方便查看。
backBtn 的 connect 操作
/* backBtn 的 connect 操作 */
connect(backBtn,&QPushButton::clicked,[=](){
this->close(); // 第二界面关闭或者隐藏(hide())
GameHall *gameHall = new GameHall; // 创建新的第一界面
gameHall->show(); // 显示第一界面
QSound::play(":res/clicked.wav"); // 记得每一次点击都要有点击音效
});
三种难度模式 的 connect 操作
/* 三种难度模式 的 connect 操作 */
GameRoom *gameRoom = new GameRoom; // 每一次跳转第三界面都需要在对应的connect中创建一次,直接在connect外创建
connect(simpleBtn,&QPushButton::clicked,[=](){
this->close();
//GameRoom *gameRoom = new GameRoom;
gameRoom->setGeometry(this->geometry()); // 确保跳转后的大小是一致的
gameRoom->show();
// 显示简单模式界面(如果设计了 三种模式不同的话,后续需要在这里添加变量,用来区分 不同难度)
QSound::play(":res/clicked.wav"); // 记得每一次点击都要有点击音效
});
connect(normalBtn,&QPushButton::clicked,[=](){
this->close();
//GameRoom *gameRoom = new GameRoom;
gameRoom->setGeometry(this->geometry()); // 确保跳转后的大小是一致的
gameRoom->show();
// 显示简单模式界面(如果设计了 三种模式不同的话,后续需要在这里添加变量,用来区分 不同难度)
QSound::play(":res/clicked.wav"); // 记得每一次点击都要有点击音效
});
connect(complexBtn,&QPushButton::clicked,[=](){
this->close();
//GameRoom *gameRoom = new GameRoom;
gameRoom->setGeometry(this->geometry()); // 确保跳转后的大小是一致的
gameRoom->show();
// 显示简单模式界面(如果设计了 三种模式不同的话,后续需要在这里添加变量,用来区分 不同难度)
QSound::play(":res/clicked.wav"); // 记得每一次点击都要有点击音效
});
history 的 connect 操作
/* history 的 connect 操作 */
/* history 的 connect 操作 */
/* history 的 connect 操作 */
connect(hisBtn,&QPushButton::clicked,[=](){
QSound::play(":res/clicked.wav"); // 记得每一次点击都要有点击音效
/* 弹出一个新的widget窗口,在这个窗口中进行操作 */
QWidget *newWidget = new QWidget;
newWidget->setWindowTitle("历史战绩");
newWidget->setWindowIcon(QIcon(":res/ico.png"));
newWidget->setFixedSize(this->width() * 0.5,this->height() * 0.5);
/* 在新窗口 widget 中创建 文本编辑器,进行对历史数据的查看 */
QTextEdit *edit = new QTextEdit(newWidget); // 注意引用头文件
edit->setFixedSize(this->width() * 0.5,this->height() * 0.5);
edit->setFont(font); // 这里的font是之前统一定义的,在文字按钮设计字体处定义的,不要迷糊
// 接下来就是在 edit 中输出 历史战绩,怎么进行?
// 我们首先要想办法得到这个 历史战绩。怎么得到?
// 在游戏结束时我们可以得到的 积分 就是我们的历史成绩,我们可以将 积分 存在txt文本中,查看历史战绩时,就读取文本就好
/* 从文本中读取的操作是怎样的呢? */
QFile file("D:/SHIMIR/Documents/Project/history.txt"); // 记得使用 / 作为文件分隔符 和 记得头文件使用
file.open(QIODevice::ReadOnly);
// open 的打开方式 QIODevice::ReadOnly / QIODevice::WriteOnly / QIODevice::ReadWrite
// 我们这里只需要读,不需要对其修改或写入
// 文件打开后,我们要拿到里面的内容
QTextStream in(&file);
int data = in.readLine().toInt(); // 只读取一行
// 拿到的内容,我们要放置在 edit 上显示出来
edit->append("历史得分为: ");
edit->append(QString::number(data));
// 最后 我们要让新的窗口在 this 上显示出来
newWidget->show();
});
效果查看
以下是第二界面创建成功的演示(为了演示连贯性,对第三界面做了返回键设计,后续会讲):
补充:如何使用QFile来打开文件并进行读取:创建一个file对象
创建了一个QFile对象,并尝试打开指定路径的文件。
包含头文件,首先需要包含QFile
的头文件:
#include <QFile>
创建QFile
对象:使用构造函数创建一个QFile
对象,并传入文件的路径:
QFile file("路径");
其中,“路径”是你要打开的文件的完整路径,例如"C:/Users/username/Documents/myfile.txt"
。
打开文件:使用open()
方法来打开文件。
if (!file.open(QIODevice::ReadOnly)) {
// 处理无法打开文件的情况
return;
}
通常,你需要指定打开模式,如只读、写入或追加模式。例如,以只读模式打开文件。
读取文件内容
(1)打开文件后,你可以使用readAll()
、read()
或其他相关方法来读取文件内容。例如,读取整个文件内容到一个字符串中:
QByteArray data = file.readAll();
QString contents(data);
(2)逐字节读取:
while (!file.atEnd()) {
char buffer[1024];
qint64 bytesRead = file.read(buffer, sizeof(buffer)); // 以字节为单位读取文件内容
if (bytesRead > 0) {
// 处理读取到的数据
QString readData(buffer, bytesRead);
// ...
}
}
file.read(buffer, sizeof(buffer)); 这一行是以字节为单位读取文件内容的。
file.read()函数 会从文件中读取指定数量的字节,并将其存储在缓冲区 buffer 中。
sizeof(buffer) 指定了缓冲区的大小,即每次读取的最大字节数。
qint64 bytesRead = file.read(buffer, sizeof(buffer)); 这行代码会返回实际读取到的字节数。如果 bytesRead > 0,则表示成功读取了一些数据,可以通过 QString readData(buffer, bytesRead); 将这些数据转换为 QString 进行进一步处理。
while 循环会逐块(每块最多1024字节)读取文件内容,直到文件末尾。因此,这段代码实现了逐字节读取文件的功能。
(3)以文本形式处理文件,并且希望每次读取一行,可以使用**QTextStream**
类 读取文件内容:
#include <QFile>
#include <QTextStream>
#include <QDebug>
int main() {
QFile file("路径");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qWarning() << "无法打开文件";
return 1;
}
QTextStream in(&file);
// 读取第一行
QString line = in.readLine();
// 将读取的字符串转换为整数
int data = line.toInt();
qDebug() << "读取的数据:" << data;
// 如果你想继续读取更多的行,可以使用循环
while (!in.atEnd()) {
line = in.readLine();
// 处理每行的内容
// ...
}
file.close();
return 0;
}
在这个示例中,我们使用了QIODevice::Text
标志来指示我们打算以文本模式读取文件。然后,通过QTextStream
对象in
读取文件内容,readLine()
方法用于读取一行文本。toInt()
方法将读取的字符串转换为整数。
请注意,如果文件中的内容不是有效的整数,toInt()
可能会失败。你可能需要检查转换是否成功,并适当处理错误情况。
关闭文件:完成读取操作后,记得关闭文件:
file.close();
完整的示例代码如下:
#include <QFile>
#include <QDebug>
int main() {
QFile file("路径");
if (!file.open(QIODevice::ReadOnly)) {
qWarning() << "无法打开文件";
return 1;
}
QByteArray data = file.readAll();
QString contents(data);
qDebug() << "文件内容:";
qDebug() << contents;
file.close();
return 0;
}
请确保替换“路径”为实际的文件路径,并根据需要处理文件读取过程中可能出现的错误。
三、游戏房间
现在是本次项目的重点部分 游戏 的设计部分了。
我们先观察一下游戏房间的布局
我们观察发现
- 需要 两种画布,也就是作为 左侧草地(游戏场地) 和 右侧控制台区域;
- 需要 绘制蛇的部分 蛇头(四种方向) 和 蛇身(蛇身蛇尾一样图形);
- 需要 积分栏 及 积分栏中直接绘制的数字;
- 需要 开始游戏控制开关按钮;
- 需要 方向控制按钮;
- 需要 退出游戏按钮(返回上一界面)。
(一)重写绘图对象
我们首先还是先 重写绘图对象,设计好第三界面 上需要出现的内容,然后在一一实现他们的功能。
/* 重写绘图对象 */
void GameRoom::paintEvent(QPaintEvent *event)
{
// 绘图对象
QPainter painter(this);
QPixmap pix;
/* 我们要考虑一下界面中会出现的物品 比如 左侧游戏草地, 右侧操作台按钮 蛇的出现 食物的出现 积分栏的出现 */
/* ------------------------------------------------------------------------------------------------------------ */
// 左侧草地
pix.load(":res/game_room.png");
painter.drawPixmap(0,0,1000,900,pix);
// 右侧控制台
pix.load(":res/bg1.png");
painter.drawPixmap(1000,0,200,1000,pix);
// 绘制蛇
/* 我们在绘制蛇的时候,要了解蛇的组成 蛇头 蛇身 蛇尾 */
/* ------------------------------------------------------------------------------------------------------------ */
/* 蛇头 的绘制 ,我们要考虑蛇的行动方向,选择蛇头图片 这是就需要一些变量来描写蛇的属性 */
/* 现在我们转到 .h 文件中 定义成员变量 */
// 绘制食物
// 绘制积分栏
}
绘制蛇
我们在绘制蛇的时候,要了解蛇的组成 蛇头 蛇身 蛇尾
蛇头 的绘制 ,我们要考虑蛇的行动方向,选择蛇头图片 这是就需要一些变量来描写蛇的属性
现在我们转到 .h 文件中 定义成员变量
/* 这里是 gameroom.h文件 */
/* 这里是 gameroom.h文件 */
/* 这里是 gameroom.h文件 */
// 蛇的移动方向
enum class SnakeDirect{
UP = 0,
DOWN,
LEFT,
RIGHT
};
private:
/* 蛇的属性 */
/* ------------------------------------------------------------------------------------------------------------ */
// 首先是蛇的链表,链表是由一个个矩形组成的,我们要规定一下蛇结点的长度
const int ksnakeNodeWidth = 20;
const int ksnakeNodeHeight = 20;
QList<QRectF> snakeList;
// 用蛇的移动速度来设计不同难度的关卡
const int kDefaultTimeout = 300;// 固定的移动速度
int moveTimeout = kDefaultTimeout;// 需要设计一个函数,让moveTimeout的值可以与关卡难度值匹配
// 蛇的移动方向 使用枚举 将蛇的移动方向列举出来, 默认蛇的移动方向为向上移动
SnakeDirect moveDirect = SnakeDirect::UP;
我们根据定义好的移动方向变量转向 .cpp 文件中,接着绘制蛇头。
/* 这里是 gameroom.cpp文件 */
/* 这里是 gameroom.cpp文件 */
/* 这里是 gameroom.cpp文件 */
// 绘制蛇
/* 我们在绘制蛇的时候,要了解蛇的组成 蛇头 蛇身 蛇尾 */
/* ------------------------------------------------------------------------------------------------------------ */
/* 蛇头 的绘制 ,我们要考虑蛇的行动方向,选择蛇头图片 这是就需要一些变量来描写蛇的属性 */
/* 现在我们转到 .h 文件中 定义成员变量 */
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);
绘制食物和房间
从1的中的移动方向我们可以同样的操作对不同模式的关卡房间进行不同的绘制。
模式的选择,关联到第二界面的模式选择。
跳转不同模式的关卡房间代码如下:
/* 这里是gameselect.cpp文件 */
/* 跳转不同模式的关卡房间 */
GameRoom *gameRoom = new GameRoom; // 每一次跳转第三界面都需要在对应的connect中创建一次,直接在connect外创建
connect(simpleBtn,&QPushButton::clicked,[=](){
this->close();
//GameRoom *gameRoom = new GameRoom;
gameRoom->setGeometry(this->geometry()); // 确保跳转后的大小是一致的
gameRoom->styleGameRoom(GameStyle::SIMPLE);// 控制游戏房间难度
gameRoom->show();
// 显示简单模式界面(如果设计了 三种模式不同的话,后续需要在这里添加变量,用来区分 不同难度)
QSound::play(":res/clicked.wav"); // 记得每一次点击都要有点击音效
});
connect(normalBtn,&QPushButton::clicked,[=](){
this->close();
//GameRoom *gameRoom = new GameRoom;
gameRoom->setGeometry(this->geometry()); // 确保跳转后的大小是一致的
gameRoom->styleGameRoom(GameStyle::NORMAL); // 控制游戏房间难度
// 显示简单模式界面(如果设计了 三种模式不同的话,后续需要在这里添加变量,用来区分 不同难度)
QSound::play(":res/clicked.wav"); // 记得每一次点击都要有点击音效
});
connect(complexBtn,&QPushButton::clicked,[=](){
this->close();
//GameRoom *gameRoom = new GameRoom;
gameRoom->setGeometry(this->geometry()); // 确保跳转后的大小是一致的
gameRoom->styleGameRoom(GameStyle::COMPLEX);
gameRoom->show();
// 显示简单模式界面(如果设计了 三种模式不同的话,后续需要在这里添加变量,用来区分 不同难度)
QSound::play(":res/clicked.wav"); // 记得每一次点击都要有点击音效
});
/* 这里是 gameroom.h 文件 */
enum class GameStyle{
SIMPLE = 0,
NORMAL,
COMPLEX
};
public:
// 游戏难度
void styleGameRoom(GameStyle style)
{
styleRoom = style;
}
private:
// 游戏难度
GameStyle styleRoom = GameStyle::NORMAL;
/* 这里是 gameroom.cpp 文件 */
// 不同模式下的房间
/*pix.load(":res/game_room.png");*/
if(styleRoom == GameStyle::SIMPLE)
{
pix.load(":res/simple_room.png");
}
else if(styleRoom == GameStyle::NORMAL)
{
pix.load(":res/normal_room.png");
}
else if(styleRoom == GameStyle::COMPLEX)
{
pix.load(":res/hard_room.png");
}
painter.drawPixmap(0,0,1000,900,pix);
绘制不同模式下的食物
/* 这里是 gameroom.h 文件 */
private:
// 食物
QRectF foodNode;
/* 这里是 gameroom.cpp 文件 */
//绘制食物
if(styleRoom == GameStyle::SIMPLE)
{
pix.load(":res/simple_food.png");
}
else if(styleRoom == GameStyle::NORMAL)
{
pix.load(":res/normal_food.bmp");
}
else if(styleRoom == GameStyle::COMPLEX)
{
pix.load(":res/hard_food.png");
}
painter.drawPixmap(foodNode.x(),foodNode.y(),foodNode.width(),foodNode.height(),pix);
绘制积分栏
/* 这里是 gameroom.cpp 文件 */
/* 这里是 gameroom.cpp 文件 */
/* 这里是 gameroom.cpp 文件 */
// 绘制积分栏
// 绘制分数
pix.load(":res/sorce_bg.png");
painter.drawPixmap(1050,100,100,50,pix);
/* QPen直接绘制积分,它用于定义绘图的笔刷样式,
* 包括线条的宽度、颜色、样式(如实线、虚线、点线等)以及端点和连接点的样式。*/
QPen pen;
pen.setColor(Qt::black);
painter.setPen(pen);
QFont qfont("楷体",30);
painter.setFont(qfont);
painter.drawText(this->width() * 0.92, this->height() * 0.16,
QString("%1").arg(snakeList.size()));
/* QString("%1").arg(snakeList.size())是用来格式化字符串的。
* 这里将snakeList.size()的值转换成字符串,并插入到"%1"这个占位符中。
* 这样做可以确保字符串的格式正确,特别是当需要插入变量时。 */
// 积分写入历史战绩中
QFile file("D:/SHIMIR/Documents/SnakesGame/history.txt"); // 将积分放入进 history.txt 文件中,第二界面的历史战绩读取文件
int c = snakeList.size(); // 得到蛇链表的长度
if(file.open(QIODevice::WriteOnly | QIODevice::ReadWrite))
{
QTextStream out(&file);
int num = c - 3; // 得到的积分为蛇链长度减去起始蛇的长度
out << num;
qDebug() << num;
file.close(); // 记得关闭文件
}
绘制游戏失败画面
/* 这里是 gameroom.cpp 文件 */
/* 这里是 gameroom.cpp 文件 */
/* 这里是 gameroom.cpp 文件 */
// 绘制游戏失败效果
// 这串代码放置在 重写绘图对象 中
if(checkFail())
{
pen.setColor(Qt::red);
painter.setFont(qfont);
painter.setPen(pen);
painter.drawText(this->width() * 0.4,this->height() * 0.5,QString("GAME OVER!"));
/* 游戏失败是我们在游戏进行时出现的问题,游戏进行时,我们需要定时器来执行蛇的移动 */
/* 这里咱不用理解,只是提前写在了这个位置 */
/* 同理sound的写法也是,是为了停止游戏进行时出现的音乐 */
timer->stop();
QSound::play(":res/gameover.wav");
qDebug() << sound; // 调试使用
qDebug() << " gameover!!!1 ";
sound->stop();
}
// Gameroom成员函数 定义
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))
{
qDebug() << "游戏检测失败";
return true;
}
}
}
return false;
}
// 成员函数的声明
// 游戏是否失败
bool checkFail();
(二)设计按钮
现在我们开始对 游戏房间 的整体进行设计
GameRoom 属性设计
/* gameroom.cpp文件 的构造函数中 */
GameRoom::GameRoom(QWidget *parent) : QWidget(parent)
{
this->setFixedSize(1200,900);
this->setWindowIcon(QIcon(":res/ico.png"));
this->setWindowTitle("贪吃蛇大作战");
}
初始化贪吃蛇
/* gameroom.h文件 */
//创建食物
void createNewFood();
/* 定义开始游戏时 的变量
* 比如我们我们需要使用定时器去执行 蛇的移动速度 */
// 其他变量
// 音乐
QSound *sound;
// 定时器
QTimer *timer;
// 游戏是否开始
bool isGameStart = false;
/* gameroom.cpp文件 的构造函数中 */
/* 在按钮设计前,我们首先要初始化贪吃蛇和食物,
* 让它结合绘制paintEvent中的代码出现在界面上 */
/* 初始化贪吃蛇 */
snakeList.push_back(QRectF(this->width() * 0.5,this->height() * 0.5,ksnakeNodeWidth,ksnakeNodeHeight));
// 创建食物
createNewFood();
// 定时器
timer = new QTimer(this);
connect(timer,&QTimer::timeout,[=](){
int cnt = 1; // 定义一个计数器变量cnt并初始化为1。这个计数器用于控制蛇移动的次数
/* 检查蛇头(snakeList.front())是否与食物节点(foodNode)相交。如果相交,则表示蛇吃到了食物 */
if(snakeList.front().intersects(foodNode))
{
createNewFood(); //如果吃到食物,则生成新的食物节点
++cnt; // 吃到食物后,增加计数器cnt的值,以便让蛇多移动一次
QSound::play(":res/eatfood.wav"); // 播放吃食物的音效
}
while(cnt--) // 移动蛇 就是 移动每个节点的位置 重新渲染一遍,让它看上去像是移动了的
{
/* 根据moveDirect变量的值,调用相应的移动函数(moveUp、moveDown、moveLeft或moveRight)来更新蛇的位置 */
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();: 在每次移动之后,移除蛇的最后一个节点(尾部),
* 这样可以保持蛇的长度不变(除非吃到食物)。
* 如果吃到食物,由于cnt增加了,会在下一次循环中多移动一次,从而增加蛇的长度。
* 此处是否还在疑惑为什么要删除尾结点?
* 这就与我们其他的函数相关了
* 往上看,我们可以看到在了解到蛇的移动方向时,我们的移动函数都会执行
* 是否我们删除尾结点与这个移动函数相关呢?*/
snakeList.pop_back();
update();
});
/* createNewFood函数 */
void GameRoom::createNewFood()
{
foodNode = QRectF(qrand() % (1000/ksnakeNodeWidth) * ksnakeNodeWidth,
qrand() % (this->height()/ksnakeNodeHeight) * ksnakeNodeHeight,
ksnakeNodeWidth,
ksnakeNodeHeight);
}
现在我们看一下效果图,把除了按钮外的布局都完成了。
我们可以看到蛇的组成只有一个头,而没有身子。
怎么解决这个问题?
还是不要着急,学习了我们的移动函数部分,结合 为什么删除尾结点 思考 你怎么解决这个只有蛇头的问题。
方向移动
我们要考虑 四种方向 的各自情况,比如 到达边界 的情况 和 未接触边界 的情况
向上移动
我们可以从图中看到蛇的两种向上移动的情况
我们要找到 蛇的头结点 ,根据每一次移动的头结点位置 通过链表移动它的蛇身。
每一次移动得到的 新的头结点 要插入进链表之中。这就意味着每一次的向上移动,我们都会插入一个新的结点,整个链表的大小就会加一。
所以初始化贪吃蛇的最后我们要删除最后一个结点,结点内部并不存有数据什么,我们只是用结点在画面中的位置显示出来一条蛇。
/* 向上移动代码 */
/* 向上移动代码 */
/* 向上移动代码 */
void GameRoom::moveUp()
{
QPointF leftTop;
QPointF rightBottom;
auto snakeHead = snakeList.front(); // 原始头结点
int headX = snakeHead.x(); // 头结点的坐标
int headY = snakeHead.y();
if(headY < ksnakeNodeHeight) // 向上移动 到达上边界 越过边界的部分要从下边界回来
{
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 snakeHead = snakeList.front();
int headX = snakeHead.x();
int headY = snakeHead.y();
if(headY + ksnakeNodeHeight * 2 > this->height())
{
leftTop = QPointF(headX,0);
}
else
{
//leftTop = QPointF(headX,headY + ksnakeNodeHeight);
leftTop = snakeHead.bottomLeft();
}
rightBottom = leftTop + QPointF(ksnakeNodeWidth,ksnakeNodeHeight);
snakeList.push_front(QRectF(leftTop,rightBottom));
}
/* 向左移动代码 */
void GameRoom::moveLeft()
{
QPointF leftTop;
QPointF rightBottom;
auto snakeHead = snakeList.front();
int headX = snakeHead.x();
int headY = snakeHead.y();
if(headX - ksnakeNodeWidth < 0)
{
leftTop = QPointF(1000 - 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 snakeHead = snakeList.front();
int headX = snakeHead.x();
int headY = snakeHead.y();
if(headX + ksnakeNodeWidth > 980)
{
leftTop = QPointF(0,headY);
}
else
{
//leftTop = QPointF(headX,headY + ksnakeNodeHeight);
leftTop = snakeHead.topRight();
}
rightBottom = leftTop + QPointF(ksnakeNodeWidth,ksnakeNodeHeight);
snakeList.push_front(QRectF(leftTop,rightBottom));
}
如何让初始化贪吃蛇更美观?
所以我们怎么让 初始的贪吃蛇 显示好看一下?
只要将初始化贪吃蛇的代码修改一下即可。
//初始化贪吃蛇
snakeList.push_back(QRectF(this->width() * 0.5,this->height() * 0.5,ksnakeNodeWidth,ksnakeNodeHeight));
moveUp();
moveUp();
开始游戏/暂停游戏
接下来就是QPushButtonde 通常操作
/* gameroom.h文件 */
// 蛇的移动速度
void snakeMoveTimeout(int timeout){
moveTimeout = timeout;
}
/* gameroom.cpp文件 的构造函数中 */
// 开始游戏 暂停游戏
QFont font("方正舒体",20);
QPushButton *strBtn = new QPushButton(this);
QPushButton *stopBtn = new QPushButton(this);
strBtn->move(1030,280);
stopBtn->move(1030,340);
strBtn->setFont(font);
stopBtn->setFont(font);
strBtn->setText("开始游戏");
stopBtn->setText("暂停游戏");
/* 因为后续游戏失败和游戏暂停是都会触发音乐停止,所以在外部设计 */
sound = new QSound(":res/Trepak.wav");
// 信号槽
connect(strBtn,&QPushButton::clicked,[=](){
//sound = new QSound(":res/Trepak.wav");
sound->play();
sound->setLoops(-1); 为了让那个音乐循环播放
isGameStart = true;
timer->start(moveTimeout); // 就是控制蛇的移动速度的
qDebug() << " 游戏正常运行 ";
});
connect(stopBtn,&QPushButton::clicked,[=](){
QSound::play(":res/clicked.wav");
isGameStart = false;
timer->stop();
sound->stop();
qDebug() << " 游戏暂停 ";
});
开始与暂停按钮,注意 timer->start(moveTimeout); 就是控制蛇的移动速度的
/* 这里是gameselect.cpp文件 */
影响不同关卡模式 的原因 要在模式跳转按钮处 设置好
/* 这里是gameselect.cpp文件 */
/* 这里是gameselect.cpp文件 */
/* 这里是gameselect.cpp文件 */
GameRoom *gameRoom = new GameRoom; // 每一次跳转第三界面都需要在对应的connect中创建一次,直接在connect外创建
connect(simpleBtn,&QPushButton::clicked,[=](){
this->close();
//GameRoom *gameRoom = new GameRoom;
gameRoom->setGeometry(this->geometry()); // 确保跳转后的大小是一致的
//这里就是 影响不同关卡模式 的原因
gameRoom->styleGameRoom(GameStyle::SIMPLE);// 控制游戏房间难度
gameRoom->snakeMoveTimeout(300);
gameRoom->show();
// 显示简单模式界面(如果设计了 三种模式不同的话,后续需要在这里添加变量,用来区分 不同难度)
QSound::play(":res/clicked.wav"); // 记得每一次点击都要有点击音效
});
connect(normalBtn,&QPushButton::clicked,[=](){
this->close();
//GameRoom *gameRoom = new GameRoom;
gameRoom->setGeometry(this->geometry()); // 确保跳转后的大小是一致的
//这里就是 影响不同关卡模式 的原因
gameRoom->styleGameRoom(GameStyle::NORMAL); // 控制游戏房间难度
gameRoom->snakeMoveTimeout(200);
// 显示简单模式界面(如果设计了 三种模式不同的话,后续需要在这里添加变量,用来区分 不同难度)
QSound::play(":res/clicked.wav"); // 记得每一次点击都要有点击音效
});
connect(complexBtn,&QPushButton::clicked,[=](){
this->close();
//GameRoom *gameRoom = new GameRoom;
gameRoom->setGeometry(this->geometry()); // 确保跳转后的大小是一致的
//这里就是 影响不同关卡模式 的原因
gameRoom->styleGameRoom(GameStyle::COMPLEX);
gameRoom->snakeMoveTimeout(100);
gameRoom->show();
// 显示简单模式界面(如果设计了 三种模式不同的话,后续需要在这里添加变量,用来区分 不同难度)
QSound::play(":res/clicked.wav"); // 记得每一次点击都要有点击音效
});
方向控制
// 方向控制键
QFont dfont("方正舒体",40);
QPushButton *up = new QPushButton(this);
QPushButton *down = new QPushButton(this);
QPushButton *left = new QPushButton(this);
QPushButton *right = new QPushButton(this);
up->move(1070,470);
down->move(1070,570);
left->move(1030,520);
right->move(1110,520);
up->setFont(dfont);
down->setFont(dfont);
left->setFont(dfont);
right->setFont(dfont);
up->setText("↑");
down->setText("↓");
left->setText("←");
right->setText("→");
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;
});
安装上方向控制键,你可以使用 文字按钮(setText)或者 使用 图标按钮(setIcon)找寻合适的图片 设计这个按键。
将 按钮 和 对应的移动方向匹配,需要注意不能与前进方向相反运动,否则自己碰到自己就失败了。如:
退出游戏按钮
/* gameroom.h文件 */
// 音乐
QSound *sound; // 由于该sound 可作为 全局变量在游戏开始时响起
/* gameroom.cpp文件 的构造函数中 */
// 字体在上面的开始暂停按钮处设置过了,我们这里直接使用即可
// 退出游戏键
QPushButton *exitBtn = new QPushButton(this);
exitBtn->move(1030,800);
exitBtn->setFont(font);
exitBtn->setText("退出游戏");
QMessageBox *msg = new QMessageBox(this);
//msg->setWindowIcon(QIcon(":res/ico.png"));
msg->setIcon(QMessageBox::Question);
msg->setWindowTitle("退出游戏");
msg->setText("是否退出游戏,返回上一界面?");
QPushButton *ok = new QPushButton("ok");
QPushButton *cancel = new QPushButton("cancel");
msg->addButton(ok,QMessageBox::AcceptRole);
msg->addButton(cancel,QMessageBox::RejectRole);
connect(exitBtn,&QPushButton::clicked,[=](){
QSound::play(":res/clicked.wav");
// 弹出一个对话框
msg->show();
msg->exec();
if(msg->clickedButton() == ok)
{
this->close();
GameSelect *select = new GameSelect;
select->show();
select->setGeometry(this->geometry());
QSound::play(":res/clicked.wav");
qDebug() << " 退出游戏,返回上一界面 ";
sound->stop(); // 停止的是 游戏开始时候一直响着的游戏
qDebug() << " 停止背景音乐 ";
}
else
{
QSound::play(":res/clicked.wav");
qDebug() << " 返回 ";
msg->close();
}
});