目录
先看效果
项目介绍
界面一:游戏大厅界面
界面二:关卡选择界面编辑
界面三:游戏界面
游戏大厅页面
游戏关卡选择页面
游戏房间页面
封装贪吃蛇数据结构
初始化游戏房间界面
设置窗口大小、标题、图标等
蛇的移动
初始化贪吃蛇本体和食物节点
实现各个方向的移动
实现向上移动
实现向下移动
实现向左移动
实现向右移动
检查是否自己会不会撞自己
设置游戏开始和游戏暂停按钮
设置退出游戏按钮
获取历史战绩
先看效果
项目介绍
贪吃蛇游戏是⼀款休闲益智类游戏。它通过控制蛇头⽅向吃⻝物,从⽽使得蛇变得越来越⻓。在本游戏中设置了上下左右四个⽅向键来控制蛇的移动⽅向。⻝物的产⽣是随机⽣成的,当蛇每吃⼀次⻝物就会增加⼀节⾝体,同时游戏积分也会相应的加⼀。在本游戏的设计中,蛇的⾝体会越吃越⻓,⾝体越⻓对应的难度就越⼤,因为⼀旦蛇头和⾝体相交游戏就会结束。
界面一:游戏大厅界面
当用户点击 “开始游戏” 按钮之后,就会进⼊到关卡选择界面。
界面二:关卡选择界面
在关卡选择界⾯上设置了三个游戏模式按钮,分别是:简单模式、正常模式、困难模式;⼀个 “历史战绩” 按钮;⼀个返回游戏⼤厅界⾯的按钮。当我们点击三个游戏按钮中的任意⼀个时,就会进⼊游戏房间界⾯,游戏房间界⾯如下:
界面三:游戏界面
在刚进⼊游戏房间界⾯时,⼀定不能点击 “退出” 按钮,如果点击 “退出” 按钮,那么程序就会异常退出。点击的顺序⼀定是:先点击 “开始” 按钮,最后才能点击 “退出” 。
游戏大厅页面
主要内容重写了绘图事件
//重写绘图事件
void paintEvent(QPaintEvent *event);
void GameHall::paintEvent(QPaintEvent *event)
{
(void) event;
//实例化画家对象
QPainter painter(this);
//Qpixmap
QPixmap pix(":/res/game_hall.png");
//画家绘画
painter.drawPixmap(0,0,this->width(),this->height(),pix);
}
固定窗口的大小,同时将其与的窗口也设置为(1000,800),设置窗口的图标以及窗口的标题名,使用信号和槽函数点击开始按钮跳转的游戏选择窗口。
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("贪吃蛇游戏");
QFont font("华为行楷",24);
QPushButton* starBtn = new QPushButton(this);
starBtn->move(430,530);
starBtn->setFont(font);
starBtn->setText("开始游戏");
starBtn->setStyleSheet("QPushButton{border:0px;}"); //去掉边框
//跳转到第二个窗口
GameSelect * gameSelect = new GameSelect;
connect(starBtn,&QPushButton::clicked,[=](){
this->close();
//设置第二个窗口和第一个窗口一样大
gameSelect->setGeometry(this->geometry());
gameSelect->show();
QSound::play(":/res/clicked.wav");
});
}
使用的知识点总结:
通过使用setFixedSize设置窗口的大小 使用setWindowIcon设置左上角的图标 使用setWindowTitle设置文本标题 使用lambda表达式 使用信号和槽函数跳转到第二个页面
游戏关卡选择页面
实现代码如下:
//绘图事件
void paintEvent(QPaintEvent *event);
GameSelect::GameSelect(QWidget *parent) : QWidget(parent)
{
this->setFixedSize(1000,800); //设置窗口大小(其实可以设置也可以不设置)
this->setWindowIcon(QIcon(":/res/ico.png"));
this->setWindowTitle("游戏关卡选择");
QPushButton* backBtn = new QPushButton(this);
backBtn->move(900,730);
backBtn->setIcon(QIcon(":/res/back.png"));
//信号和槽 跳转到第一个页面
connect(backBtn,&QPushButton::clicked,[=](){
this->close();
GameHall* gameHall = new GameHall;
gameHall->show();
//加上音效
QSound::play(":/res/clicked.wav");
});
QFont font("华文行楷",24);
GameRoom* gameRoom = new GameRoom;
QPushButton * simpleBtn = new QPushButton(this);
simpleBtn->move(420,160);
simpleBtn->setText("简单模式");
simpleBtn->setFont(QFont(font));
connect(simpleBtn,&QPushButton::clicked,[=](){
this->close();
gameRoom->show();
QSound::play(":/res/clicked.wav");
gameRoom->setTimerout(400);
});
QPushButton * normalBtn = new QPushButton(this);
normalBtn->move(420,260);
normalBtn->setText("正常模式");
normalBtn->setFont(QFont(font));
connect(normalBtn,&QPushButton::clicked,[=](){
this->close();
gameRoom->setGeometry(this->geometry());
gameRoom->show();
QSound::play(":/res/clicked.wav");
gameRoom->setTimerout(200);
});
QPushButton * difficultBtn = new QPushButton(this);
difficultBtn->move(420,360);
difficultBtn->setText("困难模式");
difficultBtn->setFont(QFont(font));
connect(difficultBtn,&QPushButton::clicked,[=](){
this->close();
gameRoom->setGeometry(this->geometry());
gameRoom->show();
QSound::play(":/res/clicked.wav");
gameRoom->setTimerout(50);
});
QPushButton * history = new QPushButton(this);
history->move(420,460);
history->setText("历史战绩");
history->setFont(QFont(font));
QSound::play(":/res/clicked.wav");
connect(history,&QPushButton::clicked,[=](){
QWidget * widget = new QWidget;
widget->setWindowTitle("历史战绩");
widget->setFixedSize(500,300);
QTextEdit* edit = new QTextEdit(widget);
edit->setFont(font);
edit->setFixedSize(500,300);
//获取历史战绩 == 获取蛇的长度 -3 就表示吃的食物
QFile file("C:/Users/19846/Desktop/greedy_snack/1.txt");
file.open(QIODevice::ReadOnly);
QTextStream in(&file);
int data = in.readLine().toInt();
edit->append("得分为:");
edit->append(QString::number(data));
widget->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);
}
游戏房间页面
• 蛇的绘制、蛇的移动、判断蛇是否会撞到自己
• 积分的累加和绘制
在这⾥我们要考虑几个比较核心的问题:
1. 怎么让蛇动起来?
• 我们可以用⼀个链表表示贪吃蛇,⼀个⼩方块表示蛇的⼀个节点, 我们设置蛇的默认长度为3;
• 向上移动的逻辑就是在蛇的上方加入⼀个小方块, 然后把最后⼀个小方块删除即可;
• 需要用到定时器Qtimer 每100 - 200ms 重新渲染。
2. 怎么判断蛇有没有吃到⻝物?
• 判断蛇头和⻝物的坐标是否相交,Qt 有相关的接口调用。
3. 怎么控制蛇的移动?
• 借助QT的实践机制实现, 重写keyPressEvent即可, 在函数中监控想要的键盘事件即可
• 我们通过绘制四个按钮,使⽤信号和槽的机制控制蛇的上、下、左、右移动⽅向
封装贪吃蛇数据结构
//枚举蛇的移动方向
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 crateNewFood(); //创建食物
void setTimerout(int timeout)
{
moveTimeout = timeout;
}
signals:
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;
};
初始化游戏房间界面
设置窗口大小、标题、图标等
GameRoom::GameRoom(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
// 初始化窗⼝设置
this->setFixedSize(1000, 800);
this->setWindowTitle("贪吃蛇游戏");
this->setWindowIcon(QIcon(":res/ico.png"));
}
蛇的移动
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->setStyleSheet("QPushButton{border:0px;}");
Up->setFont(font);
Up->setText("↑");
Down->setStyleSheet("QPushButton{border:0px;}");
Down->setFont(font);
Down->setText("↓");
Left->setStyleSheet("QPushButton{border:0px;}");
Left->setFont(font);
Left->setText("←");
Right->setStyleSheet("QPushButton{border:0px;}");
Right->setFont(font);
Right->setText("→");
connect(Up,&QPushButton::clicked,this,[=](){
if(moveDirect != SnakeDirect::DOWN)
moveDirect = SnakeDirect::UP;
});
connect(Down,&QPushButton::clicked,this,[=](){
if(moveDirect != SnakeDirect::UP)
moveDirect = SnakeDirect::DOWN;
});
connect(Left,&QPushButton::clicked,this,[=](){
if(moveDirect != SnakeDirect::RIGHT)
moveDirect = SnakeDirect::LEFT;
});
connect(Right,&QPushButton::clicked,this,[=](){
if(moveDirect != SnakeDirect::LEFT)
moveDirect = SnakeDirect::RIGHT;
})
初始化贪吃蛇本体和食物节点
GameRoom::GameRoom(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
// 初始化贪吃蛇
snakeList.push_back(QRectF(this->width() * 0.5, this->height() * 0.5,
kSnakeNodeWidth, kSnakeNodeHeight));
moveUp();
moveUp();
// 初始化⻝物
createNewFood();
}
moveUp() 的功能是将蛇向上移动⼀次, 即在上方新增⼀个节点, 但不删除尾部节点。 createNewFood() 方法的功能是随机创建⼀个食物节点:
void GameRoom::createNewFood()
{
foodRect = QRectF(
qrand() % (this->width() / kSnakeNodeWidth) * kSnakeNodeWidth ,
qrand() % (this->height() / kSnakeNodeWidth) * kSnakeNodeWidth,
kSnakeNodeWidth,
kSnakeNodeHeight);
}
实现定时器的超时槽函数 定时器的是为了实现每隔⼀段时间能处理移动的逻辑并且更新绘图事件。
• ⾸先, 需要判断蛇头和食物节点坐标是否相交
◦ 如果相交, 需要创建新的食物节点, 并且需要更新蛇的长度, 所以 cnt 需要 +1 ;
◦ 如果不相交, 那么直接处理蛇的移动即可。
• 根据蛇移动方向 moveDirect 来处理蛇的移动, 处理方法是在前方加⼀个, 并且删除后方节点; • 重新触绘图事件, 更新渲染。
GameRoom::GameRoom(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, [=](){
int cnt = 1;
// 判断是否贪吃蛇和⻝物是否相交
if (snakeList.front().intersects(foodRect)) {
createNewFood();
++cnt;
}
while(cnt--) {
// 处理蛇的移动
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();
});
}
实现各个方向的移动
实现向上移动
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(); //蛇头x
int headY = snakeNode.y(); //蛇头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 rightButtom;
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);
}
rightButtom = leftTop +QPointF(kSnakeNodeWidth,kSnakeNodeHeight);
snakeList.push_front(QRectF(leftTop,rightButtom));
}
实现向右移动
void GameRoom::moveRight()
{
QPointF leftTop;
QPointF rightButtom;
auto snakeNode = snakeList.front();
int headX = snakeNode.x();
int headY = snakeNode.y();
if(headX + kSnakeNodeWidth > 780)
{
leftTop = QPointF(0,headY);
}
else
{
leftTop = snakeNode.topRight();
}
rightButtom = leftTop + QPointF(kSnakeNodeWidth,kSnakeNodeHeight);
snakeList.push_front(QRectF(leftTop,rightButtom));
}
重写绘图事件函数进行渲染 重写基类的 paintEvent() 方法进行渲染:
- 渲染背景图
- 渲染蛇头
- 渲染蛇⾝体
- 渲染蛇尾巴
- 渲染右边游戏控制区域
- 渲染食物节点
- 渲染当前分数
- 游戏结束渲染 game over!
void gameroom::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
QPixmap pix;
pix.load(":res/simple_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/body.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.heig
ht(),pix);
//渲染⻝物
pix.load(":res/normal_food.bmp");
painter.drawPixmap(foodRect.x(), foodRect.y(), foodRect.width(),
foodRect.height(), pix);
//渲染右边区域
pix.load(":res/right_area.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.90, 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();
}
}
检查是否自己会不会撞自己
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;
}
设置游戏开始和游戏暂停按钮
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{border:0px;}");
stopbtn->setStyleSheet("QPushButton{border:0px;}");
//设置按钮字体格式
startbtn->setFont(ft);
stopbtn->setFont(ft);
connect(startbtn,&QPushButton::clicked,this,[=](){
sound = new QSound(":res/Trepak.wav"); //声⾳路径
sound->play(); //播放
sound->setLoops(-1); //循环播放
isGameStart = true;
timer->start(moveTimeout);
});
connect(stopbtn,&QPushButton::clicked,this,[=](){
sound->stop();
isGameStart = false;
timer->stop();
});
设置退出游戏按钮
//退出游戏按钮
QPushButton *ExitGame = new QPushButton(this);
ExitGame->setStyleSheet("QPushButton{border:0px;}");
ExitGame->move(860,750);
ExitGame->setFont(ft);
ExitGame->setText("退出");
//消息提⽰
QPushButton *okbtn = new QPushButton("Ok");
QPushButton *cancelbtn = new QPushButton("Cancel");
QMessageBox *msg = new QMessageBox(this);
msg->setWindowTitle("退出游戏"); //设置消息对话框的标题
msg->setText("确认退出游戏吗?"); //设置消息对话框内容
msg->setIcon(QMessageBox::Question); //设置消息对话框类型
msg->addButton(okbtn,QMessageBox::AcceptRole); //Accept Role:接受的⻆⾊
msg->addButton(cancelbtn,QMessageBox::RejectRole); //Reject Role:排斥
作⽤
connect(ExitGame,&QPushButton::clicked,[=](){
//消息提⽰
msg->show();
QSound::play(":res/clicked.wav");
msg->exec(); //阻塞等待⽤⼾输⼊
if(msg->clickedButton() == okbtn) //点击了ok按钮
{
this->close();
//如果点击了 Ok 按钮,那么就会跳转到游戏关卡界⾯
GameSelect *s = new GameSelect;
s->show();
s->setGeometry(this->geometry());
QSound::play(":res/clicked.wav");
sound->stop(); //停⽌背景⾳乐
}
else
{
msg->close();
QSound::play(":res/clicked.wav");
}
});
获取历史战绩
int c = snakeList.size();
2 QFile file("C:/Users/Lenovo/Desktop/1.txt");
3 if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
4 QTextStream out(&file);
5 int num = c;
6 out << num; // 将int类型数据写⼊⽂件
7 file.close();
读⽂件:读取写⼊文件中蛇的长度
QFile file("C:/Users/Lenovo/Desktop/1.txt");
2 file.open(QIODevice::ReadOnly);
3
4 QTextStream in(&file); //创建⽂本流对象
5 int data = in.readLine().toInt(); //读取第⼀⾏作为整形数据