昨天晚上閒得無聊,於是動手寫了一下貪喫蛇。開始想得比較多的就是蛇怎麼移動,我想到了用鏈表,而且想到了從尾到頭存儲,每次移動的時候,就把尾巴變成新的頭部。代碼大概是這個樣子:
// 頭文件
//
// Created by pikachu on 2019/12/26.
//
#ifndef SNAKE_SNAKE_H
#define SNAKE_SNAKE_H
struct SnakeNode {
int x;
int y;
SnakeNode *next;
explicit SnakeNode(int x = 0, int y = 0, SnakeNode *next = nullptr);
};
class Snake {
public:
Snake();
virtual ~Snake();
enum Direction{
UP,DOWN,LEFT,RIGHT
};
void move();
void changeDirection(Direction direction);
void grow();
private:
private:
SnakeNode *head;
SnakeNode *tail;
Direction direction = UP;
};
#endif //SNAKE_SNAKE_H
// 源文件
//
// Created by pikachu on 2019/12/26.
//
#include "Snake.h"
SnakeNode::SnakeNode(int x, int y, SnakeNode *next)
: x(x), y(y), next(next) {}
Snake::Snake() {
head = new SnakeNode(0, 1);
tail = new SnakeNode(0, 0);
tail->next = head;
}
Snake::~Snake() {
auto it = tail;
while(it != nullptr) {
auto t = it;
it = it->next;
delete t;
}
}
void Snake::move() {
auto tmp = tail;
tail = tail->next;
switch (direction) {
case UP:
tmp->y = head->y + 1;
break;
case DOWN:
tmp->y = head->y - 1;
break;
case LEFT:
tmp->x = head->x + 1;
break;
case RIGHT:
tmp->x = head->x - 1;
break;
}
head->next = tmp;
head = tmp;
head->next = nullptr;
}
void Snake::changeDirection(Snake::Direction direction) {
this->direction = direction;
}
void Snake::grow() {
auto tmp = new SnakeNode(head->x, head->y);
switch (direction) {
case UP:
tmp->y = head->y + 1;
break;
case DOWN:
tmp->y = head->y - 1;
break;
case LEFT:
tmp->x = head->x + 1;
break;
case RIGHT:
tmp->x = head->x - 1;
break;
}
head->next = tmp;
head = tmp;
head->next = nullptr;
}
我寫完後還挺得意的,這次終於輕鬆的寫完貪喫蛇了。於是用Qt簡單的寫了一個界面,其實就是把蛇化出來。代碼如下:
// 頭文件
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
// QWidget interface
protected:
void paintEvent(QPaintEvent *event) override;
void keyReleaseEvent(QKeyEvent *event) override;
private:
std::unique_ptr<Snake> snake;
constexpr static int snakeNodeWidth = 40;
constexpr static int snakeNodeHeight = 40;
constexpr static int screenWidth = 800;
constexpr static int screenHeight = 600;
constexpr static int totalWidth = screenWidth / snakeNodeWidth;
constexpr static int totalHeight = screenHeight / snakeNodeHeight;
private:
QPoint mapSnakeNodeToScreen(SnakeNode *node);
};
// 源文件
Widget::Widget(QWidget *parent)
: QWidget(parent), snake(std::make_unique<Snake>())
{
resize(800, 600);
QTimer *timer = new QTimer(this);
timer->setInterval(1000);
connect(timer, &QTimer::timeout, [this](){
this->snake->move();
this->update();
});
timer->start();
}
Widget::~Widget()
{
}
void Widget::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
qDebug() << "head:" << this->snake->head->x << this->snake->head->y;
auto it = this->snake->tail;
auto counter = 0;
while(it != nullptr) {
auto leftUpPoint = mapSnakeNodeToScreen(it);
painter.drawRect(leftUpPoint.x(), leftUpPoint.y(), snakeNodeWidth, snakeNodeHeight);
painter.drawText(leftUpPoint.x(), leftUpPoint.y(), snakeNodeWidth, snakeNodeHeight, Qt::AlignCenter, QString::number(counter));
it = it->next;
counter++;
}
}
void Widget::keyReleaseEvent(QKeyEvent *event)
{
switch (event->key()) {
case Qt::Key_Up:
case Qt::Key_W: {
qDebug() << "go up";
this->snake->changeDirection(Snake::UP);
break;
}
case Qt::Key_Down:
case Qt::Key_S: {
qDebug() << "go down";
this->snake->changeDirection(Snake::DOWN);
break;
}
case Qt::Key_Left:
case Qt::Key_A: {
qDebug() << "go left";
this->snake->changeDirection(Snake::LEFT);
break;
}
case Qt::Key_Right:
case Qt::Key_D: {
qDebug() << "go right";
this->snake->changeDirection(Snake::RIGHT);
break;
}
}
}
QPoint Widget::mapSnakeNodeToScreen(SnakeNode *node)
{
QPoint ret;
ret.setX(node->x * snakeNodeWidth);
ret.setY((totalHeight - node->y) * snakeNodeHeight);
return ret;
}
然後,令我不得不反思的東西就出現了。
首先,看一個運行的效果圖。
出現這個圖的時候,我就知道這次又完砸了。
- 沒有想到過轉彎回出現這種情況
- 沒有考慮過食物,如何出現食物,如何檢測喫到食物
- 沒有考慮過障礙物,如何設置障礙物,如何檢測與障礙物碰撞
- 沒有考慮過是否移動會碰到蛇的身體
- 沒有想過提供渲染蛇的方法或者接口,以後換一種ui就不得不改代碼
這個導致我不得不friend class Widget;
來保證圖形界面可以訪問蛇的信息。
功能沒做完不要緊,要緊的是,我居然開頭從來沒有想過這些事情,而是等代碼跑起來後,一邊改,才一邊寫。我知道,我後面寫肯定能把這些功能實現,但是,爲什麼我不一開始就想清楚呢?寫代碼之前把這些問題弄明白?
問題的解決
(這個不重要,重要的是上面的問題思考)
- 轉彎BUG
座標的變換應該是基於頭結點
void Snake::move() {
auto tmp = tail;
tail = tail->next;
switch (direction) {
case UP:
tmp->x = head->x;
tmp->y = head->y + 1;
break;
case DOWN:
tmp->x = head->x;
tmp->y = head->y - 1;
break;
case LEFT:
tmp->x = head->x - 1;
tmp->y = head->y;
break;
case RIGHT:
tmp->x = head->x + 1;
tmp->y = head->y;
break;
}
head->next = tmp;
head = tmp;
head->next = nullptr;
}
又花了點時間改了改,主要還是參考了
https://www.imooc.com/learn/487
這個時候的代碼爲:
// 頭文件
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <memory>
struct SnakeNode {
int x;
int y;
SnakeNode *next;
explicit SnakeNode(int x = 0, int y = 0, SnakeNode *next = nullptr);
};
struct Food {
int x;
int y;
};
class Snake;
class Game {
public:
Food generateFood(int xRange, int yRange);
bool isCollideWall(const std::unique_ptr<Snake> & snake, int xRange, int yRange);
};
class Snake {
public:
Snake();
virtual ~Snake();
enum Direction{
UP = -2, DOWN = 2, LEFT = 1, RIGHT = -1
};
void move();
void changeDirection(Direction direction);
void grow();
bool canEatFood(const Food & food);
bool isEatSelf();
private:
SnakeNode *head;
SnakeNode *tail;
Direction direction = UP;
friend class Widget;
friend class Game;
};
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
// QWidget interface
protected:
void paintEvent(QPaintEvent *event) override;
void keyReleaseEvent(QKeyEvent *event) override;
private:
std::unique_ptr<Snake> snake;
std::unique_ptr<Game> game;
bool hasFood = false;
bool isDead = false;
bool showSnake = true;
Food food;
int score = 0;
constexpr static int snakeNodeWidth = 40;
constexpr static int snakeNodeHeight = 40;
constexpr static int screenWidth = 800;
constexpr static int screenHeight = 600;
constexpr static int totalWidth = screenWidth / snakeNodeWidth;
constexpr static int totalHeight = screenHeight / snakeNodeHeight;
private:
QPoint mapSnakeNodeToScreen(SnakeNode *node);
QPoint mapFoodToScreen(const Food &food);
bool isCollideWall();
Food generateFood();
};
#endif // WIDGET_H
// 源文件
#include "widget.h"
#include <QtWidgets>
#include <utility>
Food Game::generateFood(int xRange, int yRange)
{
std::srand(unsigned(std::time(nullptr)));
return {std::rand() % xRange + 1, std::rand() % yRange + 1};
}
bool Game::isCollideWall(const std::unique_ptr<Snake> &snake, int xRange, int yRange)
{
auto x = snake->head->x;
auto y = snake->head->y;
return x == 0 || x == xRange - 1 || y == 0 || y == yRange - 1;
}
SnakeNode::SnakeNode(int x, int y, SnakeNode *next)
: x(x), y(y), next(next) {}
Snake::Snake() {
head = new SnakeNode(1, 3);
tail = new SnakeNode(1, 1);
tail->next = new SnakeNode(1, 2, head);
}
Snake::~Snake() {
auto it = tail;
while(it != nullptr) {
auto t = it;
it = it->next;
delete t;
}
}
void Snake::move() {
auto tmp = tail;
tail = tail->next;
switch (direction) {
case UP:
tmp->x = head->x;
tmp->y = head->y + 1;
break;
case DOWN:
tmp->x = head->x;
tmp->y = head->y - 1;
break;
case LEFT:
tmp->x = head->x - 1;
tmp->y = head->y;
break;
case RIGHT:
tmp->x = head->x + 1;
tmp->y = head->y;
break;
}
head->next = tmp;
head = tmp;
head->next = nullptr;
}
void Snake::changeDirection(Snake::Direction direction) {
if (this->direction + direction != 0)
this->direction = direction;
}
void Snake::grow() {
auto tmp = new SnakeNode(head->x, head->y);
switch (direction) {
case UP:
tmp->y = head->y + 1;
break;
case DOWN:
tmp->y = head->y - 1;
break;
case LEFT:
tmp->x = head->x - 1;
break;
case RIGHT:
tmp->x = head->x + 1;
break;
}
head->next = tmp;
head = tmp;
head->next = nullptr;
}
bool Snake::canEatFood(const Food &food)
{
int nextX = head->x;
int nextY = head->y;
switch (direction) {
case UP:
nextY = head->y + 1;
break;
case DOWN:
nextY = head->y - 1;
break;
case LEFT:
nextX = head->x - 1;
break;
case RIGHT:
nextX = head->x + 1;
break;
}
return nextX == food.x && nextY == food.y;
}
bool Snake::isEatSelf()
{
auto it = this->tail;
while(it != this->head) {
if (it->x == this->head->x && it->y == this->head->y) {
return true;
}
it = it->next;
}
return false;
}
Widget::Widget(QWidget *parent)
: QWidget(parent), snake(std::make_unique<Snake>()),
game(std::make_unique<Game>())
{
resize(800, 600);
QTimer *timer = new QTimer(this);
timer->setInterval(200);
connect(timer, &QTimer::timeout, [this](){
if (!isDead) {
if (!hasFood) {
this->food = generateFood();
qDebug() << "generate food:" << this->food.x << this->food.y;
this->hasFood = true;
this->snake->move();
} else {
if (this->snake->canEatFood(this->food)) {
qDebug() << "eat food";
this->score ++;
this->snake->grow();
this->hasFood = false;
} else {
this->snake->move();
}
}
if (this->snake->isEatSelf() || isCollideWall()) {
qDebug() << "game over";
isDead = true;
}
}
this->update();
});
timer->start();
}
Widget::~Widget()
{
}
void Widget::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
QPainter painter(this);
if (!isDead || showSnake) {
qDebug() << "head:" << this->snake->head->x << this->snake->head->y;
auto it = this->snake->tail;
auto counter = 0;
while(it != nullptr) {
auto leftUpPoint = mapSnakeNodeToScreen(it);
painter.drawRect(leftUpPoint.x(), leftUpPoint.y(), snakeNodeWidth, snakeNodeHeight);
painter.drawText(leftUpPoint.x(), leftUpPoint.y(), snakeNodeWidth, snakeNodeHeight, Qt::AlignCenter, QString::number(counter));
it = it->next;
counter++;
}
}
if (isDead)
showSnake = !showSnake;
qDebug() << "draw food:" << this->food.x << this->food.y;
auto foodPoint = mapFoodToScreen(this->food);
painter.drawRoundedRect(foodPoint.x(), foodPoint.y(), snakeNodeWidth, snakeNodeHeight, snakeNodeWidth/2, snakeNodeHeight/2);
// draw title
painter.drawText(0, 0, screenWidth, snakeNodeHeight, Qt::AlignCenter, "Snake Game");
// draw score
painter.drawText(0, screenHeight - 1 * snakeNodeHeight, screenWidth, snakeNodeHeight, Qt::AlignCenter, QString("Score: %1").arg(score));
// draw wall
painter.drawRect(0, 0, screenWidth, screenHeight);
painter.drawRect(snakeNodeWidth, snakeNodeHeight, screenWidth - 2 * snakeNodeWidth, screenHeight - 2 * snakeNodeHeight);
}
void Widget::keyReleaseEvent(QKeyEvent *event)
{
Q_UNUSED(event);
if (isDead) return;
switch (event->key()) {
case Qt::Key_Up:
case Qt::Key_W: {
qDebug() << "go up";
this->snake->changeDirection(Snake::UP);
break;
}
case Qt::Key_Down:
case Qt::Key_S: {
qDebug() << "go down";
this->snake->changeDirection(Snake::DOWN);
break;
}
case Qt::Key_Left:
case Qt::Key_A: {
qDebug() << "go left";
this->snake->changeDirection(Snake::LEFT);
break;
}
case Qt::Key_Right:
case Qt::Key_D: {
qDebug() << "go right";
this->snake->changeDirection(Snake::RIGHT);
break;
}
}
}
QPoint Widget::mapSnakeNodeToScreen(SnakeNode *node)
{
QPoint ret;
ret.setX(node->x * snakeNodeWidth);
ret.setY((totalHeight - node->y - 1) * snakeNodeHeight);
return ret;
}
QPoint Widget::mapFoodToScreen(const Food &food)
{
QPoint ret;
ret.setX(food.x * snakeNodeWidth);
ret.setY((totalHeight - food.y - 1) * snakeNodeHeight);
return ret;
}
bool Widget::isCollideWall()
{
return this->game->isCollideWall(this->snake, totalWidth, totalHeight);
}
Food Widget::generateFood()
{
return this->game->generateFood(totalWidth - 2, totalHeight - 2);
}
後面又改吧改吧,最後變成這個樣子
(網速太慢了,後面再加吧)