ZetCode

Qt5 里的贪吃蛇

最后修改于 2023 年 10 月 18 日

在本 Qt5 教程中,我们创建一个贪吃蛇游戏的克隆版。

贪吃蛇

贪吃蛇 是一款经典的旧视频游戏。它最初于 70 年代末被创造出来。后来它被移植到 PC 上。在这个游戏中,玩家控制一条蛇。目标是吃掉尽可能多的苹果。每次蛇吃掉一个苹果,它的身体就会变长。蛇必须避开墙壁和自己的身体。这个游戏有时也被称为 Nibbles

开发

蛇的每个关节的大小是 10 像素。蛇用光标键控制。最初,蛇有三个关节。如果游戏结束,"Game Over" 消息将显示在游戏板的中间。

snake.h
#pragma once

#include <QWidget>
#include <QKeyEvent>

class Snake : public QWidget {

  public:
      Snake(QWidget *parent = nullptr);

  protected:
      void paintEvent(QPaintEvent *);
      void timerEvent(QTimerEvent *);
      void keyPressEvent(QKeyEvent *);

  private:
      QImage dot;
      QImage head;
      QImage apple;

      static const int B_WIDTH = 300;
      static const int B_HEIGHT = 300;
      static const int DOT_SIZE = 10;
      static const int ALL_DOTS = 900;
      static const int RAND_POS = 29;
      static const int DELAY = 140;

      int timerId;
      int dots;
      int apple_x;
      int apple_y;

      int x[ALL_DOTS];
      int y[ALL_DOTS];

      bool leftDirection;
      bool rightDirection;
      bool upDirection;
      bool downDirection;
      bool inGame;

      void loadImages();
      void initGame();
      void locateApple();
      void checkApple();
      void checkCollision();
      void move();
      void doDrawing();
      void gameOver(QPainter &);
};

这是头文件。

static const int B_WIDTH = 300;
static const int B_HEIGHT = 300;
static const int DOT_SIZE = 10;
static const int ALL_DOTS = 900;
static const int RAND_POS = 29;
static const int DELAY = 140;

B_WIDTHB_HEIGHT 常量确定游戏板的大小。DOT_SIZE 是苹果和蛇的点的尺寸。ALL_DOTS 常量定义了游戏板上可能的最大点数(900 = (300*300)/(10*10))。RAND_POS 常量用于计算苹果的随机位置。DELAY 常量决定了游戏的速度。

int x[ALL_DOTS];
int y[ALL_DOTS];

这两个数组保存了蛇的所有关节的 x 和 y 坐标。

snake.cpp
#include <QPainter>
#include <QTime>
#include "snake.h"

Snake::Snake(QWidget *parent) : QWidget(parent) {

    setStyleSheet("background-color:black;");
    leftDirection = false;
    rightDirection = true;
    upDirection = false;
    downDirection = false;
    inGame = true;

    setFixedSize(B_WIDTH, B_HEIGHT);
    loadImages();
    initGame();
}

void Snake::loadImages() {

    dot.load("dot.png");
    head.load("head.png");
    apple.load("apple.png");
}

void Snake::initGame() {

    dots = 3;

    for (int z = 0; z < dots; z++) {
        x[z] = 50 - z * 10;
        y[z] = 50;
    }

    locateApple();

    timerId = startTimer(DELAY);
}

void Snake::paintEvent(QPaintEvent *e) {

    Q_UNUSED(e);

    doDrawing();
}

void Snake::doDrawing() {

    QPainter qp(this);

    if (inGame) {

        qp.drawImage(apple_x, apple_y, apple);

        for (int z = 0; z < dots; z++) {
            if (z == 0) {
                qp.drawImage(x[z], y[z], head);
            } else {
                qp.drawImage(x[z], y[z], dot);
            }
        }

    } else {

        gameOver(qp);
    }
}

void Snake::gameOver(QPainter &qp) {

    QString message = "Game over";
    QFont font("Courier", 15, QFont::DemiBold);
    QFontMetrics fm(font);
    int textWidth = fm.horizontalAdvance(message);

    qp.setFont(font);
    int h = height();
    int w = width();

    qp.translate(QPoint(w/2, h/2));
    qp.drawText(-textWidth/2, 0, message);
}

void Snake::checkApple() {

    if ((x[0] == apple_x) && (y[0] == apple_y)) {

        dots++;
        locateApple();
    }
}

void Snake::move() {

    for (int z = dots; z > 0; z--) {
        x[z] = x[(z - 1)];
        y[z] = y[(z - 1)];
    }

    if (leftDirection) {
        x[0] -= DOT_SIZE;
    }

    if (rightDirection) {
        x[0] += DOT_SIZE;
    }

    if (upDirection) {
        y[0] -= DOT_SIZE;
    }

    if (downDirection) {
        y[0] += DOT_SIZE;
    }
}

void Snake::checkCollision() {

    for (int z = dots; z > 0; z--) {

        if ((z > 4) && (x[0] == x[z]) && (y[0] == y[z])) {
            inGame = false;
        }
    }

    if (y[0] >= B_HEIGHT) {
        inGame = false;
    }

    if (y[0] < 0) {
        inGame = false;
    }

    if (x[0] >= B_WIDTH) {
        inGame = false;
    }

    if (x[0] < 0) {
        inGame = false;
    }

    if(!inGame) {
        killTimer(timerId);
    }
}

void Snake::locateApple() {

    QTime time = QTime::currentTime();
    qsrand((uint) time.msec());

    int r = qrand() % RAND_POS;
    apple_x = (r * DOT_SIZE);

    r = qrand() % RAND_POS;
    apple_y = (r * DOT_SIZE);
}

void Snake::timerEvent(QTimerEvent *e) {

    Q_UNUSED(e);

    if (inGame) {

        checkApple();
        checkCollision();
        move();
    }

    repaint();
}

void Snake::keyPressEvent(QKeyEvent *e) {

    int key = e->key();

    if ((key == Qt::Key_Left) && (!rightDirection)) {
        leftDirection = true;
        upDirection = false;
        downDirection = false;
    }

    if ((key == Qt::Key_Right) && (!leftDirection)) {
        rightDirection = true;
        upDirection = false;
        downDirection = false;
    }

    if ((key == Qt::Key_Up) && (!downDirection)) {
        upDirection = true;
        rightDirection = false;
        leftDirection = false;
    }

    if ((key == Qt::Key_Down) && (!upDirection)) {
        downDirection = true;
        rightDirection = false;
        leftDirection = false;
    }

    QWidget::keyPressEvent(e);
}

snake.cpp 文件中,我们有游戏的逻辑。

void Snake::loadImages() {

    dot.load("dot.png");
    head.load("head.png");
    apple.load("apple.png");
}

loadImages 方法中,我们获取游戏的图像。ImageIcon 类用于显示 PNG 图像。

void Snake::initGame() {

    dots = 3;

    for (int z = 0; z < dots; z++) {
        x[z] = 50 - z * 10;
        y[z] = 50;
    }

    locateApple();

    timerId = startTimer(DELAY);
}

initGame 方法中,我们创建蛇,随机地在游戏板上定位一个苹果,并启动计时器。

void Snake::checkApple() {

    if ((x[0] == apple_x) && (y[0] == apple_y)) {

        dots++;
        locateApple();
    }
}

如果苹果与蛇头碰撞,我们增加蛇的关节数量。我们调用 locateApple 方法,该方法随机定位一个新的苹果对象。

move 方法中,我们有游戏的关键算法。要理解它,请看蛇是如何移动的。我们控制蛇的头部。我们可以用光标键改变它的方向。其余的关节沿链向上移动一个位置。第二个关节移动到第一个关节的位置,第三个关节移动到第二个关节的位置,以此类推。

for (int z = dots; z > 0; z--) {
    x[z] = x[(z - 1)];
    y[z] = y[(z - 1)];
}

此代码沿链移动关节。

if (leftDirection) {
    x[0] -= DOT_SIZE;
}

此行将头部向左移动。

checkCollision 方法中,我们确定蛇是否撞到了自己或墙壁之一。

for (int z = dots; z > 0; z--) {

    if ((z > 4) && (x[0] == x[z]) && (y[0] == y[z])) {
        inGame = false;
    }
}

如果蛇用它的头撞到了它的一个关节,游戏就结束了。

if (y[0] >= B_HEIGHT) {
    inGame = false;
}

如果蛇撞到了游戏板的底部,游戏就结束了。

void Snake::timerEvent(QTimerEvent *e) {

    Q_UNUSED(e);

    if (inGame) {

        checkApple();
        checkCollision();
        move();
    }

    repaint();
}

timerEvent 方法形成一个游戏循环。假设游戏尚未结束,我们执行碰撞检测并进行移动。repaint 会导致窗口被重绘。

if ((key == Qt::Key_Left) && (!rightDirection)) {
    leftDirection = true;
    upDirection = false;
    downDirection = false;
}

如果我们按下左光标键,我们将 leftDirection 变量设置为 true。此变量用于 move 函数以更改蛇对象的坐标。还要注意,当蛇向右移动时,我们不能立即向左转。

main.cpp
#include <QApplication>
#include "snake.h"

int main(int argc, char *argv[]) {

  QApplication app(argc, argv);

  Snake window;

  window.setWindowTitle("Snake");
  window.show();

  return app.exec();
}

这是主类。

Snake
图:贪吃蛇

这是在 Qt5 中开发的贪吃蛇游戏。