目录
贪吃蛇小游戏是较简单的开发入门实战,通过该游戏的编码过程可以学习到很多的编程知识。
游戏说明
贪吃蛇游戏按键说明:
- 按方向键上下左右,可以实现蛇移动方向的改变。
- 按空格键可实现暂停,暂停后按任意键继续游戏。
- 按Esc键可直接退出游戏。
- 按R键可重新开始游戏。
游戏中会时时展示用户得分。
游戏效果展示
贪吃蛇的游戏移动速度是可以调整的,也可以自己实现关卡的方式在对应的分数中对小蛇进行加速提高游戏难度以及趣味性。
游戏代码详解
首先需要定义游戏界面的大小,需要定义两个部分:1.游戏的行数以及列数;2.定义每一个方框的大小。如下代码所示:
#define BLOCK_WIDHT 15 // 每一个格子的宽度
#define GAME_MAX_W 30 // 在水平方向上一共有多少个格子
#define GAME_MAX_H 30 // 在垂直方向上一共有多少个格子
小蛇的活动区域称之为游戏区域,右边为按钮区域以及分数显示区域。
小蛇的活动区域的宽度和高度可以定义为以下内容,宽的格子数量乘以每个格子的宽度等于游戏窗口实际宽度。
#define GAME_WINDOW_WIDTH BLOCK_WIDHT* GAME_MAX_W // 游戏窗口的宽度
#define GAME_WINDOW_HEIGHT BLOCK_WIDHT* GAME_MAX_H // 游戏窗口的高度
关键数据结构
小蛇的蛇头以及蛇身的都可以使用坐标来表示,并且小蛇的查询大于其数据变更,因此我们使用QVector<SnakeBoy>,定义结构如下:
struct Block {
int x; // 水平方向上的格子的坐标。
int y; // 垂直方向上的格子的坐标。
};
// 蛇的身体
struct SnakeBoy : Block {
};
// 食物
struct Food : Block {
};
这里的数据结构使用了集成关系,为了后期扩展所做的预留,将蛇身和食物都继承Block。
后期为了增加游戏的趣味性,可以在Food结构中加上食物类型等,在绘画时候根据食物类型绘画不同的食物。
游戏的状态以及运动方向。
enum MoveDirection {
moveRight, // 向右移动
moveLeft, // 向左移动
moveTop, // 向上移动
moveBottom // 向下移动
};
enum GameStatus {
gameInit,
gameStart,
gameOver
};
初始化游戏界面
游戏的界面主要分为两个部分,一个是游戏部分,一个是提示区域部分。
游戏部分
主要将边界画出来就可以了,由于QT在绘制的过程中画笔的宽度也占用像素,因此需要在右边界,以及下边界需要减掉画笔的宽度。当然也可以使用rect的绘制方式来进行绘制。
void GameWidget::paintBackground(QPainter *painter)
{
QPen pen = painter->pen();
pen.width();
// 绘制上边界
painter->drawLine(0, 0, 0, this->height());
// 绘制右边界
painter->drawLine(GAME_WINDOW_WIDTH - pen.width(), 0,
GAME_WINDOW_WIDTH - pen.width(), this->height());
// 绘制左边界
painter->drawLine(0, 0, this->width(), 0);
// 绘制下边界
painter->drawLine(0, GAME_WINDOW_HEIGHT - pen.width(), this->width(),
GAME_WINDOW_HEIGHT - pen.width());
}
提示部分
提示部分不在游戏绘制区域能,因此我们可以直接添加控件以及布局。下面是布局和控件添加的代码,这里并没有使用.UI拖拽的方式编写的代码,直接使用代码进行布局。
水平布局: layout_main ,包含了游戏界面(m_mainGameView),以及垂直布局(layout_btn)
垂直布局:layout_btn,包含分数(m_labScoreNum),开始按钮(m_btnStart)、结束按钮(m_btnStop)、暂停按钮(m_btnPause)。
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, m_mainGameView(new GameWidget(this))
, m_mainWidget(new QWidget(this))
, m_btnStart(new QPushButton(this))
, m_btnStop(new QPushButton(this))
, m_btnPause(new QPushButton(this))
, m_labScoreStr(new QLabel(this))
, m_labScoreNum(new QLabel(this))
{
m_btnStart->setText("开始");
m_btnStop->setText("结束");
m_btnPause->setText("暂停");
setWindowTitle("贪吃蛇");
QHBoxLayout *layout_main = new QHBoxLayout;
QVBoxLayout *layout_btn = new QVBoxLayout;
layout_main->addWidget(m_mainGameView);
layout_main->addLayout(layout_btn);
m_labScoreStr->setAlignment(Qt::AlignCenter);
m_labScoreNum->setAlignment(Qt::AlignCenter);
m_labScoreStr->setText("当前分数");
m_labScoreNum->setText("0");
QFont fontScoreNum;
fontScoreNum.setPixelSize(20);
m_labScoreNum->setFont(fontScoreNum);
QPalette pa;
pa.setColor(QPalette::WindowText, Qt::red);
m_labScoreNum->setPalette(pa);
layout_btn->setSpacing(10);
layout_btn->addWidget(m_labScoreStr);
layout_btn->addWidget(m_labScoreNum);
layout_btn->addWidget(m_btnStart);
layout_btn->addWidget(m_btnStop);
layout_btn->addWidget(m_btnPause);
layout_btn->addStretch();
m_mainWidget->setLayout(layout_main);
setCentralWidget(m_mainWidget);
connect(m_btnStart, &QPushButton::clicked, m_mainGameView,
&GameWidget::start);
connect(m_btnStop, &QPushButton::clicked, m_mainGameView,
&GameWidget::stop);
connect(m_btnPause, &QPushButton::clicked, m_mainGameView,
&GameWidget::pause);
connect(m_mainGameView, &GameWidget::sigUpdateScore, this,
&MainWindow::updateScore);
m_mainGameView->setFocus();
}
蛇逻辑
小蛇的初始化
小蛇的存储使用 QVector<SnakeBoy>结构进行存储,因此初始化时,需要将所有的数据清除,然后新生成数据。我这里为了方便因此仅仅只是创建了一个小蛇的头部数据,读者可以自己尝试创建两个小蛇的身体。
void snake::init()
{
// 第一步:清理小蛇的所有数据
m_bodys.clear();
// 第二部:创建小蛇的头部位置,注意这里将数据写死,如果游戏边界设置小于10就会出现问题。
// 因此初始化小蛇头部位置可以进行优化。读者可以自行优化。
SnakeBoy a;
a.x = 10;
a.y = 10;
m_bodys.push_back(a);
}
边界判断
因为小蛇的头部是最先移动的部分,所以只需要对小蛇的头部进行判断就可以了。
当小蛇的头部小于0时或者小蛇的头部坐标大于最大边界时都认为小蛇超出边界。
bool snake::isBeyoudBoundary()
{
if (m_bodys.at(0).x < 0 || m_bodys.at(0).x >= GAME_MAX_W ||
m_bodys.at(0).y < 0 || m_bodys.at(0).y >= GAME_MAX_H) {
return true;
}
return false;
}
蛇吃食物判断
是否吃到了食物,判断蛇头的坐标是否和食物的坐标重合,如果重合那么表示吃到了食物。
bool snake::isEatFood(Food food)
{
if (m_bodys.first().x == food.x && m_bodys.first().y == food.y) {
SnakeBoy body;
body.x = food.x;
body.y = food.y;
m_bodys.push_front(body);
return true;
}
return false;
}
随机生成食物
随机生成食物,不能生成在小蛇身体内部,因此我们可以使用while语句不断生成直到生成,符合要求的食物。
Food GameWidget::createFood()
{
Food food;
// 标识生成的食物不在小蛇内部
bool flat = false;
do {
flat = false;
// 尝试生成食物
food.x = getRandom(GAME_MAX_W);
food.y = getRandom(GAME_MAX_H);
// 判断是否与小蛇重合。
foreach (auto item, m_snake.m_bodys) {
if (food.x == item.x && food.y == item.y) {
flat = true;
}
}
} while (flat);
return food;
}
是否撞到自己
将小蛇的头部和自己的身体做比较,如果发现坐标重复那么意味着小蛇运动过程中给你碰撞到了自己的身体。
m_bodys.at(0)为小蛇的头部信息。
bool snake::isHitItself()
{
for (int i = m_bodys.size() - 1; i > 0; i--) {
if (i != 0) {
if (m_bodys.at(0).x == m_bodys[i].x &&
m_bodys.at(0).y == m_bodys[i].y) {
return true;
}
}
}
return false;
}
小蛇的移动
小蛇的移动分为两个部分
1. 首先移动小蛇的身体,小蛇的身体从后向前遍历,移动身体移动到最后可以看到,第一节身体其实是与小蛇的原始头部重合。
2. 最后移动小蛇的头部,根据传递的方向移动小蛇的头部。
void snake::move(MoveDirection diretion)
{
// 保持第一个蛇头不变,先移动蛇的身体
for (int i = m_bodys.size() - 1; i > 0; i--) {
if (i != 0) {
m_bodys[i] = m_bodys[i - 1];
}
}
// 根据移动方向,移动蛇的头部
switch (diretion) {
case moveRight:
m_bodys[0].x += 1;
break;
case moveLeft:
m_bodys[0].x -= 1;
break;
case moveTop:
m_bodys[0].y -= 1;
break;
case moveBottom:
m_bodys[0].y += 1;
break;
}
}
由于按钮没有改变小蛇的速度只是改变了小蛇的方向,因此为了避免小蛇自己碰撞到自己,因此需要进行方向判断,不允许直接方向操作。使其不能碰撞到自己。
按键响应代码如下:
void GameWidget::keyPressEvent(QKeyEvent *event)
{
qInfo() << "keyPressEvent start";
switch (event->key()) {
case Qt::Key_Up:
if (m_direction != moveBottom) m_direction = moveTop;
break;
case Qt::Key_Left:
qInfo() << "moveLeft";
if (m_direction != moveRight) m_direction = moveLeft;
break;
case Qt::Key_Right:
qInfo() << "moveRight";
if (m_direction != moveLeft) m_direction = moveRight;
break;
case Qt::Key_Down:
qInfo() << "moveBottom";
if (m_direction != moveTop) m_direction = moveBottom;
break;
}
}
其实这里还是有BUG的,就是当快速按动按键,还是会出现自己撞到身体的情况。这个BUG 其实在早期的手机游戏上面都有。例如:当小蛇向右移动,快速按 “下”、“左”就会自己撞到自己了,读者可以自己试试修复这个BUG。
游戏主体逻辑
定时器
定时器是很关键的函数,有了定时器才能使游戏能动起来,其实是一帧一帧的进行绘制,因此我们需要设置定时器,定时绘制逻辑代码。
定时器中我们需要做以下内容:
1、 判断小蛇是否吃到食物。
2、 小蛇移动。
3、 判断小蛇是否碰撞到自己的身体。
4、 判断小蛇是否超出了边界。
void GameWidget::slotTimeout()
{
if (m_snake.isEatFood(m_food)) {
m_food = createFood();
m_score++;
emit sigUpdateScore(m_score);
}
m_snake.move(m_direction);
// 判断是否是撞到自己
if (m_snake.isHitItself()) {
m_status = gameOver;
m_timer->stop();
} else if (m_snake.isBeyoudBoundary()) { // 判断是否超出游戏边框
m_status = gameOver;
m_timer->stop();
}
update();
}
分数记录
分数的记录我提供了一个信号,当用户游戏分数更新的时候会发送信号。在这里读者可以做一些处理,比如将其保存到文件内或者上传到服务器中做排名。
绑定消息如下:
connect(m_mainGameView, &GameWidget::sigUpdateScore, this,
&MainWindow::updateScore);
获取更新的分数,我这里仅仅知识做了一个展示,读者可以在这里升级代码,将其存放在服务器中或者文件中。
void MainWindow::updateScore(int score)
{
score *= 10;
QString str = QString::number(score);
m_labScoreNum->setText(str);
}
游戏绘制
绘制食物
void GameWidget::paintFood(QPainter *painter)
{
QPainterPath path;
path.addEllipse(QRectF(m_food.x * BLOCK_WIDHT + 2,
m_food.y * BLOCK_WIDHT + 2, BLOCK_WIDHT - 4,
BLOCK_WIDHT - 4));
painter->fillPath(path, QBrush(QColor(0, 255, 0)));
}
绘制边界
边界的绘制可以直接使用RECT来绘制,我这里使用4条线进行绘制。
void GameWidget::paintBackground(QPainter *painter)
{
QPen pen = painter->pen();
pen.width();
painter->drawLine(0, 0, 0, this->height());
painter->drawLine(GAME_WINDOW_WIDTH - pen.width(), 0,
GAME_WINDOW_WIDTH - pen.width(), this->height());
painter->drawLine(0, 0, this->width(), 0);
painter->drawLine(0, GAME_WINDOW_HEIGHT - pen.width(), this->width(),
GAME_WINDOW_HEIGHT - pen.width());
}
小蛇绘制
由于小蛇的头部和小蛇的身体是不一样的绘画方式,因此需要进行区分。
void GameWidget::paintSnakeBody(QPainter *painter)
{
QPainterPath path;
int i = 0;
foreach (auto item, m_snake.m_bodys) {
QRectF body = QRectF(item.x * BLOCK_WIDHT, item.y * BLOCK_WIDHT,
BLOCK_WIDHT, BLOCK_WIDHT);
// 绘画蛇头
if (i == 0) {
painter->setBrush(Qt::red);
painter->setPen(Qt::red);
painter->drawRect(body);
} else {
// 绘画蛇身
painter->setBrush(Qt::transparent);
painter->setPen(Qt::red);
painter->drawRect(body);
}
i = 1;
}
}
提示信息绘制
paintCentreStr函数计算绘制在游戏界面的中心未知。
void GameWidget::paintGameInit(QPainter *painter)
{
paintCentreStr(painter, "贪吃蛇");
}
void GameWidget::paintGameOver(QPainter *painter)
{
paintCentreStr(painter, "Game Over!");
}
void GameWidget::paintCentreStr(QPainter *painter, QString str)
{
QFont font;
font.setPixelSize(50);
painter->setFont(font);
QFontMetrics fm(font);
painter->drawText((GAME_WINDOW_WIDTH / 2) - (fm.width(str) / 2),
(GAME_WINDOW_HEIGHT / 2), str);
}
源码
git传送门
git clone git@gitcode.net:arv002/qt.git