达永编程网

程序员技术分享与交流平台

Qt5 C++入门教程-第14章 小游戏(贪吃蛇)

大道至简,在 Qt5 C++入门教程的这一部分,我们将创建一个贪吃蛇小游戏。

贪吃蛇游戏

贪吃蛇是一款经典的老游戏,最早在 20 世纪 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 &);
};

这个头文件定义了贪吃蛇游戏的基本结构:

  • B_WIDTH 和 B_HEIGHT 常量决定了游戏界面的大小。
  • DOT_SIZE 是苹果和蛇身体每个点的大小。
  • ALL_DOTS 常量定义了游戏界面上可能出现的最大点数 (900 = (300300)/(1010))。
  • RAND_POS 常量用于计算苹果的随机位置。
  • DELAY 常量决定了游戏的速度。

x[ALL_DOTS] 和 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方法加载游戏所需的图片资源,使用QImage类来处理 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方法初始化游戏状态:创建一条初始长度为 3 的蛇,在游戏区域随机位置放置一个苹果,并启动游戏定时器。

运行

void Snake::checkApple() {
    if ((x[0] == apple_x) && (y[0] == apple_y)) {
        dots++;
        locateApple();
    }
}

checkApple方法检查蛇头是否与苹果碰撞。如果碰撞,蛇的长度加 1,并在新的随机位置生成一个苹果。

游戏的核心移动算法在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;
    }
}

如果蛇头碰到了自己的身体(除了前 4 个关节,因为刚开始移动时可能会短暂重叠),游戏结束。

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;
}

这段代码处理键盘输入。当玩家按下左方向键时,只要蛇当前不是向右移动(防止 180 度转向),就设置蛇向左移动。

// 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();
}

最后是程序的入口点,创建并显示游戏窗口。

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言