在Qt5教程的这一部分,我们创建了一个贪吃蛇游戏。
贪吃蛇
贪吃蛇是一个古老的经典视频游戏。它是在70年代末首次创建的。后来,它被带到了PC上。在这个游戏中,玩家控制一条蛇。游戏的目的是吃尽可能多的苹果。每当蛇吃下一个苹果,它的身体就会增长。蛇必须避开墙壁和自己的身体。这个游戏有时被称为Nibbles。
发展
蛇的每个关节的大小为10px。蛇是用光标键控制的。最初,蛇有三个关节。如果游戏结束,"游戏结束 "的信息会显示在棋盘的中间。
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_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坐标。
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方法,随机定位一个新的苹果对象。
在移动方法中,我们有这个游戏的关键算法。为了理解它,看看蛇是如何移动的。我们控制蛇的头部。我们可以用光标键改变它的方向。其余的关节在链条上移动一个位置。第二个关节移动到第一个关节的位置,第三个关节移动到第二个关节的位置,等等。
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;
}
}
如果蛇用头撞到它的一个关节,游戏就结束了。
如果(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。这个变量在移动函数中被用来改变蛇对象的坐标。还要注意的是,当蛇向右移动时,我们不能立即向左转。
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();
}
这是主类。