前言
贪吃蛇相信大多数的人都玩过,本文记录了一个贪吃蛇的小游戏开发过程。
界面展示
一、实验要求
- 实现贪吃蛇游戏基本功能,屏幕上随机出现一个“食物”,称为豆子,上下左右控制“蛇”的移动,吃到“豆子”以后“蛇”的身体加长一点,得分增加,“蛇”碰到边界或,蛇头与蛇身相撞,蛇死亡,游戏结束。
- 为游戏设计初始欢迎界面,游戏界面,游戏结束界面。
- 进行交互界面的设计,要有开始键、暂停键和停止退出的选项。
- 对蛇吃到豆子进行分值计算,可以设置游戏速度,游戏音乐等拓展元素。
二、实验准备
- 首先设计好逻辑图,将要解决的问题分块处理.
- 一步步思考,解决豆子生成的规则,蛇的移动,碰撞等问题,学习相关算法.
- 解决图形界面的优化问题.
- 思考能否拓展的,比如蛇的颜色、形状,音乐等等.
- 学习Qt图形用户界面框架.
三、设计思路
思路图:
1、游戏界面设计
游戏主要通过绘图事件来实现,首先实现“开始游戏”和“退出游戏”,利用QPushBottom生成两个按钮,并且点击会有相应的操作。
Widget::Widget(QWidget *parent):
QWidget(parent)
{
this->setWindowTitle("快乐贪吃蛇");
this->setWindowIcon(QIcon("G:/Qt/Snake/Snake/snake.jpg"));//设置窗口标题的图片
resize(520, 520);//设置窗口大小
//setStyleSheet("QWidget{background:white}");
for(int i = 0; i < snake_long; i++)
{
snake_x.append(250);
snake_y.append(400 + i * 10);
}
food();//生成一个食物
QFont font("方正舒体",20,QFont::ExtraLight, false);
start=1;
/*设置按钮参数*/
pb_start = new QPushButton;
pb_start->setText("开始游戏");
pb_start->setFixedSize(200, 80);
pb_start->setStyleSheet("background-color:rgb(190,190,190)");
pb_start->setFont(font);
pb_quit = new QPushButton;
pb_quit->setText("退出游戏");
pb_quit->setFixedSize(200, 80);
pb_quit->setStyleSheet("background-color:rgb(190,190,190)");
pb_quit->setFont(font);
/*将开始界面的两个按钮所占空间平均分配*/
QHBoxLayout *hbox1 = new QHBoxLayout;//水平
hbox1->addStretch();
hbox1->addWidget(pb_start);
hbox1->addStretch();
QHBoxLayout *hbox2 = new QHBoxLayout;
hbox2->addStretch();
hbox2->addWidget(pb_quit);
hbox2->addStretch();
\
QVBoxLayout *vbox = new QVBoxLayout;//竖直
vbox->addStretch();
vbox->addLayout(hbox1);
vbox->addStretch();
vbox->addLayout(hbox2);
vbox->addStretch();
setLayout(vbox);
connect(pb_start, SIGNAL(clicked(bool)), this, SLOT(pb_Start()));
connect(pb_quit, SIGNAL(clicked(bool)), this, SLOT(pb_Quit()));
}
因为就只有两个按钮,所以就随便布了一个局,退出游戏只需要this->close();即可。
void Widget::pb_Quit()//退出游戏
{
this->close();
}
开始游戏之后,需要删除之前的Layout布局,但是不太会,网上找的相关代码,倒是实现了这个功能。
void Widget::pb_Start()//开始游戏
{
start=0;
deleteAllitemsOfLayout(this->layout());//删除布局
timestop = 1;//进入游戏
}
删除布局,即删除控件函数。
/*清除控件*/
void Widget::deleteAllitemsOfLayout(QLayout *layout)
{
QLayoutItem *child;
while ((child = layout->takeAt(0)) != nullptr)
{
//setParent为NULL,防止删除之后界面不消失
if(child->widget())
{
child->widget()->setParent(nullptr);
}else if(child->layout()){
deleteAllitemsOfLayout(child->layout());
}
delete child;
}
}
下一步先生成食物的坐标,坐标只需要生成一个点即可。
/*生成食物的坐标*/
void Widget::food()
{
food_x.append(qrand()%(this->width()/10) * 10);//生成随机食物的位置坐标,qrand:生成随机函数
food_y.append(qrand()%(this->height()/10) * 10);
}
这里用了一个定时器的东西,通过反复绘图实现蛇的运动。
/*为允许Qt来优化速度并且防止闪烁。*/
void Widget::timerEvent(QTimerEvent *event)
{
update();
}
蛇的定义是通过链表来实现的,Qt有现成的函数可以实现直接在第一个位置上添加和删除某一个位置上的数据。在移动的时候就需要这种函数,添加第一个位置,然后删除最后一个元素即可。
void Widget::moveLeft()//左移
{
snake_x.insert(0, (snake_x[0] - 10 + 500)%500);
snake_y.insert(0, snake_y[0]);//长度会加一
deleteLastRectF();
}
void Widget::moveRight()//右移
{
snake_x.insert(0, (snake_x[0] + 10 + 500)%500);
snake_y.insert(0, snake_y[0]);//长度会加一
deleteLastRectF();
}
void Widget::moveUp()//上移
{
snake_x.insert(0, snake_x[0]);
snake_y.insert(0, (snake_y[0] - 10 + 500)%500);//长度会加一
deleteLastRectF();
}
void Widget::moveDown()//下移
{
snake_x.insert(0, snake_x[0]);
snake_y.insert(0, (snake_y[0] + 10 + 500)%500);//长度会加一
deleteLastRectF();
}
然后删除结尾的数据。
/*删除结尾数据*/
void Widget::deleteLastRectF()
{
snake_x.removeAt(snake_x.length() - 1);//删除最后一个节点
snake_y.removeAt(snake_y.length() - 1);
}
2、游戏过程的设计
这个游戏,我设置了允许穿墙,所以只有在吃到自己的时候,蛇才会死亡,然后游戏结束。
/*判断游戏是否结束*/
bool Widget::isGameover()
{
for(int i=0; i<snake_x.length(); i++){
for(int j=i+1; j<snake_x.length(); j++){
if((snake_x.at(i) == snake_x.at(j)) && (snake_y.at(i) == snake_y.at(j))){
return true;
}
}
}
return false;
}
然后就是对是否吃到食物的判定。
/*判断是否吃到食物*/
void Widget::iseatFood()
{
for(int i = 0; i < food_x.length(); i++)
{
if((snake_x.at(0) == food_x.at(i)) && (snake_y.at(0) == food_y.at(i)))//如果吃到食物
{
food_x.removeAt(i);
food_y.removeAt(i);
snake_x.append(snake_x.last());
snake_y.append(snake_y.last());
break;
}
}
}
移动的时候,需要先判断当断蛇头移动的方向,对应的,按了向上键之后,按向下键,向下键会属于失效的状态,按不动,定义一个moveFlag方法,移动的时候更改他的值即可。
/*移动事件*/
void Widget::keyPressEvent(QKeyEvent *event)
{
switch (event->key()) {
case Qt::Key_Up:
if(moveFlag != 'D') {
moveFlag = 'U';
}
break;
case Qt::Key_Down:
if(moveFlag != 'U') {
moveFlag = 'D';
}
break;
case Qt::Key_Right:
if(moveFlag != 'L') {
moveFlag = 'R';
}
break;
case Qt::Key_Left:
if(moveFlag != 'R') {
moveFlag = 'L';
}
break;
}
}
3、画面构造及拓展功能
然后就是画图了,画图这里,定义画家,画笔,设置样式颜色等等,然后就是逻辑问题,我定义了一个timestop来判断是否结束绘图,timestop的初始值为0,不进行绘图。
开始游戏之后,更改timestop的值为1,开始绘图。通过moveFlag的值来回去当前移动的方向,然后更改链表中的数据,在后面画出蛇,画出食物等等。
最后当游戏结束的时候,将我定义的gameover设置为1,相当于最后一次绘图,将蛇死亡之前的画面中心绘制下来,显现在屏幕上,然后游戏结束。
/*画图*/
void Widget::paintEvent(QPaintEvent *event)
{
if(start)
{
QPainter painter(this);
painter.drawPixmap(0,0,width(),height(),QPixmap("G:/Qt/Snake/Snake/canyon.jpg"));
}
if(timestop == 1)
{
timerid = startTimer(220);//停0.22秒
//向某一方向一直移动
if(moveFlag == 'U') {
moveUp();
}
else if(moveFlag == 'D') {
moveDown();
}
else if(moveFlag == 'R') {
moveRight();
}
else if(moveFlag == 'L') {
moveLeft();
}
QPainter painter(this);
painter.drawPixmap(0,0,width(),height(),QPixmap("G:/Qt/Snake/Snake/sky.jpg"));
QPen pen;
painter.setPen(pen);
pen.setWidth(snake_size);
QBrush brs(Qt::green);
QFont font("方正舒体",12,QFont::ExtraLight,false);
painter.setFont(font);
painter.drawText(20,20,QString("当前得分:")+QString("%1").arg(snake_x.length()-4));
painter.setBrush(brs);
painter.drawEllipse(snake_x.at(0)-2, snake_y.at(0)-2, snake_size+4, snake_size+4);
for (int i = 1; i < snake_x.length(); i++) {//画出蛇的初始状态
painter.setBrush(brs);
//painter.drawRect(snake_x.at(i), snake_y.at(i), snake_size, snake_size);
painter.drawEllipse(snake_x.at(i), snake_y.at(i), snake_size, snake_size);
}
food_t++;
if(food_t == 20)//4.4秒生成一次食物
{
food();
food_t = 0;
}
for (int i = 0; i < food_x.length(); i++) {//画出食物
painter.setBrush(QColor(255,0,0));
painter.drawRect(food_x.at(i), food_y.at(i), snakzhe_size, snake_size);
}
if(isGameover())
{
timestop = 0;
killTimer(timerid);
gameover = 1;
}
iseatFood();
}
if(gameover == 1)//打印画面
{
QPainter painter(this);
painter.drawPixmap(0,0,width(),height(),QPixmap("G:/Qt/Snake/Snake/sky.jpg"));
QPen pen;
painter.setPen(pen);
pen.setWidth(snake_size);
QBrush brs(Qt::green);
QFont font("方正舒体",12,QFont::ExtraLight,false);
painter.setFont(font);
painter.drawText(20,20,QString("当前得分:")+QString("%1").arg(snake_x.length()-4));
painter.setBrush(brs);
painter.drawEllipse(snake_x.at(0)-2, snake_y.at(0)-2, snake_size+4, snake_size+4);
for (int i = 1; i < snake_x.length(); i++) {//画出蛇的初始状态
painter.setBrush(brs);
//painter.drawRect(snake_x.at(i), snake_y.at(i), snake_size, snake_size);
painter.drawEllipse(snake_x.at(i), snake_y.at(i), snake_size, snake_size);
}
for (int i = 0; i < food_x.length(); i++) {//画出食物
painter.setBrush(QColor(255,0,0));
painter.drawRect(food_x.at(i), food_y.at(i), snake_size, snake_size);
}
QFont font1("方正舒体",30,QFont::ExtraLight,false);
painter.setFont(font1);
painter.drawText((this->width()-200)/2,(this->height()-30)/2,QString("GAME OVER!"));
}
}
这里我给这个游戏添加了一个背景音乐,也是我非常喜欢的一首纯音乐。
void Widget::on_pushButton_clicked()//音乐播放
{
}
以下是头文件widget.h代码,
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QKeyEvent> //如果你的游戏可以用键盘操纵,需要包含
#include <QRectF>
#include <QPainter>
#include <QPen>
#include <QBrush>
#include <QDebug>
#include <QTimer>
#include <QTime>
#include <QDebug>
#include <QTime>
#include <QPaintEvent>
#include <QTimerEvent>
#include <QIcon>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPalette>
#include <QSound>
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
//char moveFlag = 'U';//定义初始方向
void deleteAllitemsOfLayout(QLayout* layout);//删除布局
protected:
void paintEvent(QPaintEvent *event);
void timerEvent(QTimerEvent *event);
void keyPressEvent(QKeyEvent *event);
private:
void moveLeft();//向左移动
void moveRight();//向右移动
void moveUp();//向上移动
void moveDown();//向下移动
void deleteLastRectF();//删除最后一个节点
void iseatFood();//吃到了食物
bool isGameover();//是否结束游戏
void on_pushButton_clicked();
protected slots:
void food();//生成食物
void pb_Start();//开始游戏
void pb_Quit();//退出游戏
private:
int snake_long = 5;//初始长度
QList<int> snake_x;//定义一个int类型的数据 x轴
QList<int> snake_y;//定义一个int类型的数据 y轴
int snake_size = 13;//定义单个身体大小
char moveFlag = 'U';//定义初始方向
QList<int> food_x;//食物
QList<int> food_y;//食物
int food_t = 0;//设置生成食物的时间
int timestop = 0;
int timerid;
//QTimer *foodTimer;
int gameover = 0;
int start;
private:
QPushButton *pb_start, *pb_quit;
};
#endif // WIDGET_H
main.cpp代码如下,
#include "widget.h"
#include <QApplication>
#include<QSound>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QSound::play("G:/Qt/Snake/build-Snake-Desktop_Qt_5_12_1_MinGW_64_bit-Debug/千坂 - Settle.wav");
Widget w;
w.show();
return a.exec();
}