基于广度优先搜索的自适应贪吃蛇实现

贪吃蛇的寻路问题是最简单的一类路径搜索问题, 几乎所有的路径搜索算法都能帮助我们解决这一问题,接下来,我将基于广度优先搜索算法,在C++平台上实现多条蛇同时进行竞争运动。

注:本文偏新手向,如有错漏请多指教


1. 系统配置

Key Value
系统 Windows 10
语言 C++ 11
IDE Qt 5.6.1
编译器 Clang

- Qt 的Pro配置文件如下所示,注意这里要加上c++11的配置选项,因为接下来要用到新的feature

QT += core
QT -= gui

CONFIG += c++11

TARGET = Snakes_m
CONFIG += console
CONFIG -= app_bundle

TEMPLATE = app

SOURCES += main.cpp \
    node.cpp \
    blockmap.cpp \
    snake.cpp

HEADERS += \
    node.h \
    blockmap.h \
    snake.h

为了叙述简便,以下章节中将广度优先搜索简称为BFS。
我们对节点类型做如下规定:

//Node :: Type
/*
 * 0: Target
 * 1: Body
 * 2: HeadUp
 * 3: HeadRight
 * 4: HeadDown
 * 5: HeadLeft
 * 6: Free Block
 * 7: Banned Block
 */

2. 系统架构

1. Class: Node

贪吃蛇问题说白了就是图论中的最短路问题,而图论问题的两个主要变量就是点和边,在本问题中,边可以被抽象为权重为1的边,因此可以被忽略,我们只需要定义一个表示点的Node类就可以了。

#include 
#include 


class Node
{
public:
    int x;
    int y;
    int type; //用于区分节点的类型
    std::shared_ptr prenode;  //父节点
    std::shared_ptr nextnode; //子节点

public:
    Node();
    //~Node();
    Node(int x,int y);
    Node(int x,int y, int type);
    Node(int x,int y, std::shared_ptr prenode, std::shared_ptr nextnode);

    void setType(int type);

    friend std::ostream& operator<< (std::ostream &os,std::shared_ptr nd)
    {
        os<<"X: "<x<<"  "<<"Y: "<y<<"\n";
        return os;
    }
};
  • 细心的小伙伴可能发现了,在我所使用的函数中,都没有直接使用Node指针作为参数,而是使用了一个std 模板类shared_ptr,这个指针可以实现类似于Swift或Java中的ARC(Auto Reference Counting)机制,当指针的被引次数为0时自动删除,因为我们的广搜算法在找到一条路径之后,只会返回一个单向的解路径,要想删除其余的无用解非常麻烦,不如交给shared_ptr帮助我们来完成。
  • Node::setType用于设置节点类型。
  • 除此之外,这个Node类重载了ostream操作符号,方便我们进行调试。

2. Class: Snake

在本系统中,我们要实现的是多条蛇同时进行搜索,因此可以抽象为一个Snake类,每个Snake管理自己的头结点,并且自主进行路径搜索,搜索完成后将状态更新至BlockMap(接下来会讲到)。

#include 
#include 
#include //用时间实现伪随即

class BlockMap;
class Snake
{
public:
    BlockMap * blockmap;
    int length = 1;
    std::shared_ptr head = NULL;
    std::shared_ptr path = NULL; // 路径的头结点
public:
    Snake();
    Snake(int x,int y,int type);
    void setBlockMap(BlockMap * bm); //用于解决循环引用

    bool nextStep();
    std::shared_ptr search();
    void snakeReflush();
    void generateTarget();
    bool checkNodeAvailability(int, int);
    int getDirection(std::shared_ptr n1, std::shared_ptr n2);
    void reSearchPath();
    bool checkSnake(std::shared_ptr n);
};
  • Snake::length用于记录蛇的长度,在每一次吃食物时增加,在结尾处用于验算长度,防止因不明原因导致的恶性增长;
  • shared_ptr::head用于记录当前蛇的头结点,并可以通过列表找到蛇的尾节点(尾节点的nextnode为null)
  • shared_ptr::path用于记录当前路径的头结点,当一条路径被计算出来后,路径将作为链表存储,path变量存储了这一链表的头结点,它与head的关系如下图所示:

null->snakeBody2->snakeBody1->head->path->pathNode1->pathNode2-> … ->target

这么做的好处是可以在搜索出一条路径后,系统不需要在每一次循环中重新搜索一次,减少了计算量,path 只有在以下情况下才会重新计算:1. 某一条蛇吃到了食物,也就是target消失;2. 当前path的下一节点与其他蛇冲突

接下来,我对Snake类的函数进行功能解析

构造函数
  • 用shared_ptr构造一个head节点就可以了,需要传入蛇的头起始的x,y坐标以及类型
Snake::Snake(int x, int y, int type) : head(std::shared_ptr(new Node(x,y,type))){}
Snake::nextStep
  • 用于计算蛇的下一状态
bool Snake::nextStep()
{
    //1. 判断是否还有target,没有则给定新的target
    if(!this->blockmap->target){
        this->generateTarget();
    }

    //2. 计算path头结点
    if(!this->path){
        this->path = this->search();
        this->visitReflush();
    }

    //3. 沿着path前进一格
    std::shared_ptr<Node> nextNode = this->path;
    if(!nextNode){ // 当蛇被block时,保持不动
        return true;
    }

    //4. 如果下一结点正好是别的蛇的一部分,则重新计算路线
    if(!this->checkSnake(this->path)){
        if(nextNode->x == this->blockmap->target->x && nextNode->y == this->blockmap->target->y){
            //蛇长度+1
            this->length++;

            //处理头部方向问题
            int direction = this->getDirection(nextNode,this->head);
            this->head->setType(1);
            nextNode->setType(direction);
            this->head = nextNode;

            //重新指定target,发送notice给所有snake,重新计算路线
            this->blockmap->target = nullptr;
            this->generateTarget();
            this->blockmap->noticeAllSnakesForNewPath();
            return true;
        }
        this->blockmap->noticeAllSnakesForNewPath();
        return true;
    }

    this->path = this->path->prenode;
    //如果下一节点是target
    if(nextNode->x == this->blockmap->target->x && nextNode->y == this->blockmap->target->y){ 
        //蛇长度+1
        this->length++;

        //处理头部方向问题
        int direction = this->getDirection(nextNode,this->head);
        this->head->setType(1);
        nextNode->setType(direction);
        this->head = nextNode;

        //重新指定target,发送notice给所有snake,重新计算路线
        this->blockmap->target = nullptr;
        this->generateTarget();
        this->blockmap->noticeAllSnakesForNewPath();
        return true;
    }
    else{
        int direction = this->getDirection(nextNode,this->head);
        this->head->setType(1);
        nextNode->nextnode = this->head;
        this->head->prenode = nextNode;
        this->head = nextNode;
        this->head->setType(direction);

        //1.删除蛇的尾结点
        std::shared_ptr<Node> tailNode = this->head;
        int length = 1;
        while(tailNode){
            if(!tailNode->nextnode){
                tailNode->prenode->nextnode = nullptr;
                this->blockmap->blockMap[tailNode->x][tailNode->y] = 6;
                break;
            }
            tailNode = tailNode->nextnode;
            length++;
        }
        std::cout<<"length :"<length<length != length-1){
            std::shared_ptr fixTailNode = this->head;
            for(int i=0;ilength;i++){
                if(i == this->length-1){
                    this->blockmap->blockMap[fixTailNode->nextnode->x][fixTailNode->nextnode->y] = 6;
                    fixTailNode->nextnode = nullptr;
                    break;
                }
                fixTailNode = fixTailNode->nextnode;
            }
        }
    }
    //将当前snake的最新位置信息更新到blockmap中
    this->snakeReflush();
}
Snake::search
  • 使用BFS算法计算下一路径点
std::shared_ptr<Node> Snake::search()
{
    std::queueNode>> q;
    q.push(this->head);
    while(!q.empty()){
        std::shared_ptr<Node> node = q.front();
        q.pop();

        //如果当期结点是target,则回溯并返回pathHead指向的下一结点
        if(node->x==this->blockmap->target->x && node->y==this->blockmap->target->y){
            //对node链表的pre和next进行重置
            node->nextnode=node->prenode;
            node->prenode = nullptr;
            std::shared_ptr<Node> temp_node = node;
            if(node->nextnode == this->head){
                return node;
            }else{
                node = node->nextnode;
            }
            while(node){
                node->nextnode = node->prenode;
                if(node->nextnode == this->head){
                    node->prenode = temp_node;
                    return node;
                }else{
                    node->prenode = temp_node;
                    temp_node = node;
                    node = node->nextnode;
                }
            }
        }
        else{
            if(checkNodeAvailability(node->x-1,node->y) && this->blockmap->visit[node->x-1][node->y]==0){
                q.push(std::shared_ptr<Node>(new Node(node->x-1,node->y,node,nullptr)));
                this->blockmap->visit[node->x-1][node->y]=1;
            };
            if(checkNodeAvailability(node->x+1,node->y) && this->blockmap->visit[node->x+1][node->y]==0){
                q.push(std::shared_ptr<Node>(new Node(node->x+1,node->y,node,nullptr)));
                this->blockmap->visit[node->x+1][node->y]=1;
            };
            if(checkNodeAvailability(node->x,node->y+1) && this->blockmap->visit[node->x][node->y+1]==0){
                q.push(std::shared_ptr<Node>(new Node(node->x,node->y+1,node,nullptr)));
                this->blockmap->visit[node->x][node->y+1]=1;
            };
            if(checkNodeAvailability(node->x,node->y-1) && this->blockmap->visit[node->x][node->y-1]==0){
                q.push(std::shared_ptr<Node>(new Node(node->x,node->y-1,node,nullptr)));
                this->blockmap->visit[node->x][node->y-1]=1;
            };
        }
    }
    return nullptr;
}

上述的链表重置是为了将path链表的父节点和子节点进行完善:

//原path链表
head  <-  path  <-  pathnode2  <- ... <-target
      ↑         ↑              ↑
   prenode   prenode        prenode

//处理后path链表
head  <-  path  <-  pathnode2  <- ... <-target
      ↑         ↑              ↑
  nextnode   nextnode      nextnode   

head  ->  path  ->  pathnode2  -> ... ->target
      ↑         ↑              ↑
   prenode   prenode        prenode

这样处理后,在进行蛇的头结点移动时就方便的多了:)

Snake::visitReflush
  • 在BlockMap中我加入了一个visit数组,用于记录BFS中搜索过的点,以减少搜索量,这个函数用于重置visit数组
void Snake::visitReflush()
{
    this->blockmap->visitReflush();
}
Snake::snakeReflush
  • 在当前蛇的状态更新之后,需要将新状态更新到我们的舞台上去
void Snake::snakeReflush()
{
    std::shared_ptr<Node> cn = this->head; //cn=CurrentNode
    this->blockmap->blockMap[cn->x][cn->y]=cn->type;
    while(cn=cn->nextnode){
        this->blockmap->blockMap[cn->x][cn->y]=1;
    }
}
Snake::generateTarget
  • 生成食物,本程序中我们只生成一个食物,且对于所有蛇的权重均为1,生成时,用随机数随机选择一个free block进行生成。
void Snake::generateTarget()
{
    int a = this->blockmap->width;
    int b = this->blockmap->height;
    srand((unsigned)time(NULL)); //初始化随机数种子
    int rw,rh;
    for(int i=0;i<20;i++){
        rw = (rand() % (a));
        rh = (rand() % (b));
        if(this->blockmap->blockMap[rh][rw]==6){
            break;
        }
    }
    this->blockmap->target = std::shared_ptr(new Node(rh,rw,0));
    this->blockmap->blockMap[rh][rw]=0;
}
Snake::checkNodeAvailability
  • 用于BFS中检测下一节点是否可行,可行状态只有free block和target
bool Snake::checkNodeAvailability(int x, int y)
{
    if(this->blockmap->blockMap[x][y]==6 || this->blockmap->blockMap[x][y]==0){
        return true;
    }
    else{
        return false;
    }
}
Snake::getDirection
  • 用于检测蛇头部方向
int Snake::getDirection(std::shared_ptr<Node> n1, std::shared_ptr<Node> n2)
{
    if(n1->x == n2->x && (n1->y -n2->y) == 1){
        return 3; // 右
    }
    if(n1->x == n2->x && (n1->y - n2->y) == -1){
        return 5; // 左
    }
    if(n1->y == n2->y && (n1->x - n2->x) == 1){
        return 4; // 右
    }
    if(n1->y == n2->y && (n1->x - n2->x) == -1){
        return 2; // 左
    }
}
Snake::checkSnake
  • 检测下一路径点是否与其他蛇有碰撞
bool Snake::checkSnake(std::shared_ptr n)
{
    int x = n->x;
    int y = n->y;
    if(this->blockmap->blockMap[x][y] == 6 || this->blockmap->blockMap[x][y] == 0){
        return true;
    }
    else{
        return false;
    }
}
Snake::reSearchPath
  • 重新计算路线
void Snake::reSearchPath()
{
    this->path = this->search();
    this->visitReflush();
}

3. Class: BlockMap

  • 我们的蛇蛇需要一个可以运动的二维公共舞台,通常我们可以使用二维数组或者二重指针来实现,这里我习惯使用二重指针来实现,因为可以使用变量来初始化比较方便,其实也可以使用enum sack来使数组也可以使用变量初始化,感兴趣的小伙伴可以去看看《Effective C++》。
  • 同时,我们发现,BlockMap类和Snake出现了互相引用的问题,为了解决这个问题,我们需要在Class定义前需要手动定义一个Class,并且该Class需要在实例构造之后传入,不可写入构造函数中,因为C++不确定哪一个类先被构造成功,容易Crash,Compiler也不会让你过的。我们要做的就是在头文件中定义好它,然后设置一个set函数传入即可(例如Snake::setBlockMap方法)。
#include 
#include 
#include 
#include //rand函数和srand函数
#include //用时间实现伪随即
#include  //队列,用于实现BFS
#include  //用于使用std::shared_ptr

class Snake;
class BlockMap
{
public:
    Snake ** sa;
    int **blockMap;
    int **visit; //记录搜索过的点,减少广度搜索个数
    int width;
    int height;
    int snakeNum;
    std::shared_ptr target=NULL;
public:
    BlockMap();
    BlockMap(Snake ** sa,int ptrNum, int width, int height);

    bool autoReflush();
    void mapReflush();
    void snakeReflush();
    void visitReflush();
    void noticeAllSnakesForNewPath();

};

相比于Snake类来说,BlockMap类的功能较为简单,这里我就贴上代码

构造函数
BlockMap::BlockMap(Snake **sa, int ptrNum, int width, int height) : sa(sa),snakeNum(ptrNum),width(width),height(height)
{
    setlocale(LC_CTYPE, ""); // 配合wprintf使用,输出特殊符号

    int **newBlockMap = new int*[height];
    for(int i=0;inew int[width];
    }

    int **newVisit = new int*[height];
    for(int i=0;inew int[width];
    }

    //初始化地图
    /*
     * 0: Target
     * 1: Body
     * 2: HeadUp
     * 3: HeadRight
     * 4: HeadDown
     * 5: HeadLeft
     * 6: Free Block
     * 7: Banned Block
     */
    for(int i=0;ifor(int j=0;jif(i==0||j==0||i==height-1||j==width-1){
                newBlockMap[i][j] = 7;
            }
            else{
                newBlockMap[i][j] = 6;
            }
            newVisit[i][j] = 0;
        }
    }

    for(int i=0;ihead->x][sa[i]->head->y] = 4;sa[i]->head->type;
        newVisit[sa[i]->head->x][sa[i]->head->y] = 1;
    }

    this->blockMap = newBlockMap;
    this->visit = newVisit;
}
BlockMap::autoReflush
  • 用于main前段控制入口
bool BlockMap::autoReflush()
{
    this->snakeReflush();
    this->mapReflush();
    return true;
}
BlockMap::mapReflush
  • 根据当前的block数组输出图形
void BlockMap::mapReflush()
{
    if(this->blockMap){
        int width = this->width;
        int height = this->height;
        for(int i=0;ifor(int j=0;jswitch (this->blockMap[i][j]) {
                case 0:
                    wprintf(L"⊙");
                    break;
                case 1:
                    wprintf(L"■");
                    break;
                case 2:
                    wprintf(L"↑");
                    break;
                case 3:
                    wprintf(L"→");
                    break;
                case 4:
                    wprintf(L"↓");
                    break;
                case 5:
                    wprintf(L"←");
                    break;
                case 6:
                    wprintf(L"□");
                    break;
                case 7:
                    wprintf(L"■");
                    break;
                default:
                    break;
                }
            }
            std::cout<<"\n";
        }
    }
}
BlockMap::visitReflush
  • 重置visit数组
void BlockMap::visitReflush()
{
    int height = this->height;
    int width = this->width;

    for(int i=0;ifor(int j=0;jthis->visit[i][j] = 0;
        }
    }
}
BlockMap::noticeAllSnakesForNewPath
  • 通知所有的蛇重新计算路线
void BlockMap::noticeAllSnakesForNewPath()
{
    for(int i=0;i <this->snakeNum ; i++)
    {
        this->sa[i]->reSearchPath();
    }
}

4. Main

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    /* Node的type值
     * 0: Target
     * 1: Body
     * 2: HeadUp
     * 3: HeadRight
     * 4: HeadDown
     * 5: HeadLeft
     * 6: Free Block
     * 7: Banned Block
     */
    Snake * s1 = new Snake(1,1,4);
    Snake * s2 = new Snake(5,5,4);
    Snake * s3 = new Snake(10,10,4);
    Snake * s4 = new Snake(15,15,4);
    Snake **sa = new Snake*[4];
    sa[0] = s1;
    sa[1] = s2;
    sa[2] = s3;
    sa[3] = s4;

    int width = 30;
    int height = 30;
    int numOfSnake = 4;
    BlockMap * bm = new BlockMap(sa,numOfSnake,width,height);

    sa[0]->setBlockMap(bm);
    sa[1]->setBlockMap(bm);
    sa[2]->setBlockMap(bm);
    sa[3]->setBlockMap(bm);


    while(1){

        if(bm->autoReflush()){

            for(int i=0; istd::cout<<"Snake "<1<<" ";
                sa[i]->nextStep();
                if(i==numOfSnake-1)std::cout<<"Target:"<target;
            }
            Sleep(50);
        }
        system("cls");
    }

    return a.exec();
}

3. 总结

  • 至此,我们已经完成了一个多条蛇同时搜索食物的动画,其中我们处理了蛇路径冲突,头方向计算,并且使用智能指针来避免了内存泄漏,使用visit数组减小了计算量。

  • 以下是程序运行实例图
    基于广度优先搜索的自适应贪吃蛇实现_第1张图片

  • 接下来,我将研究带权重的贪吃蛇搜索,也就更加接近于现实状况,使用的算法也可以选择A*等启发式算法来进行,第一次写这种教程,心虚的很,但也是我学习的一种记录,有错误或能够提升效率的方法请联系我,不胜感激 :)

你可能感兴趣的:(C++,windows,qt,c++)