隨手寫貪喫蛇有感

昨天晚上閒得無聊,於是動手寫了一下貪喫蛇。開始想得比較多的就是蛇怎麼移動,我想到了用鏈表,而且想到了從尾到頭存儲,每次移動的時候,就把尾巴變成新的頭部。代碼大概是這個樣子:

// 頭文件
//
// 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);
}



後面又改吧改吧,最後變成這個樣子
(網速太慢了,後面再加吧)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章