大道至简,在 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();
}
最后是程序的入口点,创建并显示游戏窗口。