Qt5 里的贪吃蛇
最后修改于 2023 年 10 月 18 日
在本 Qt5 教程中,我们创建一个贪吃蛇游戏的克隆版。
贪吃蛇
贪吃蛇 是一款经典的旧视频游戏。它最初于 70 年代末被创造出来。后来它被移植到 PC 上。在这个游戏中,玩家控制一条蛇。目标是吃掉尽可能多的苹果。每次蛇吃掉一个苹果,它的身体就会变长。蛇必须避开墙壁和自己的身体。这个游戏有时也被称为 Nibbles。
开发
蛇的每个关节的大小是 10 像素。蛇用光标键控制。最初,蛇有三个关节。如果游戏结束,"Game Over" 消息将显示在游戏板的中间。
#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_WIDTH 和 B_HEIGHT 常量确定游戏板的大小。DOT_SIZE 是苹果和蛇的点的尺寸。ALL_DOTS 常量定义了游戏板上可能的最大点数(900 = (300*300)/(10*10))。RAND_POS 常量用于计算苹果的随机位置。DELAY 常量决定了游戏的速度。
int x[ALL_DOTS]; int y[ALL_DOTS];
这两个数组保存了蛇的所有关节的 x 和 y 坐标。
#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 函数以更改蛇对象的坐标。还要注意,当蛇向右移动时,我们不能立即向左转。
#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();
}
这是主类。
这是在 Qt5 中开发的贪吃蛇游戏。