贪吃蛇的寻路问题是最简单的一类路径搜索问题, 几乎所有的路径搜索算法都能帮助我们解决这一问题,接下来,我将基于广度优先搜索算法,在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 <iostream>
#include <memory>
class Node
{
public:
int x;
int y;
int type; //用于区分节点的类型
std::shared_ptr<Node> prenode; //父节点
std::shared_ptr<Node> nextnode; //子节点
public:
Node();
//~Node();
Node(int x,int y);
Node(int x,int y, int type);
Node(int x,int y, std::shared_ptr<Node> prenode, std::shared_ptr<Node> nextnode);
void setType(int type);
friend std::ostream& operator<< (std::ostream &os,std::shared_ptr<Node> nd)
{
os<<"X: "<<nd->x<<" "<<"Y: "<<nd->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 <blockmap.h>
#include <queue>
#include <time.h>//用时间实现伪随即
class BlockMap;
class Snake
{
public:
BlockMap * blockmap;
int length = 1;
std::shared_ptr<Node> head = NULL;
std::shared_ptr<Node> path = NULL; // 路径的头结点
public:
Snake();
Snake(int x,int y,int type);
void setBlockMap(BlockMap * bm); //用于解决循环引用
bool nextStep();
std::shared_ptr<Node> search();
void snakeReflush();
void generateTarget();
bool checkNodeAvailability(int, int);
int getDirection(std::shared_ptr<Node> n1, std::shared_ptr<Node> n2);
void reSearchPath();
bool checkSnake(std::shared_ptr<Node> 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<Node>(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<<" ActualLength: "<<this->length<<std::endl;
// 处理可能的异常长度变化
if(this->length != length-1){
std::shared_ptr<Node> fixTailNode = this->head;
for(int i=0;i<this->length;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::queue<std::shared_ptr<Node>> 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<Node>(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<Node> 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 <stdio.h>
#include <node.h>
#include <snake.h>
#include <stdlib.h>//rand函数和srand函数
#include <time.h>//用时间实现伪随即
#include <queue> //队列,用于实现BFS
#include <memory> //用于使用std::shared_ptr
class Snake;
class BlockMap
{
public:
Snake ** sa;
int **blockMap;
int **visit; //记录搜索过的点,减少广度搜索个数
int width;
int height;
int snakeNum;
std::shared_ptr<Node> 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;i<height;i++){
newBlockMap[i] = new int[width];
}
int **newVisit = new int*[height];
for(int i=0;i<height;i++){
newVisit[i] = new int[width];
}
//初始化地图
/*
* 0: Target
* 1: Body
* 2: HeadUp
* 3: HeadRight
* 4: HeadDown
* 5: HeadLeft
* 6: Free Block
* 7: Banned Block
*/
for(int i=0;i<height;i++){
for(int j=0;j<width;j++){
if(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;i<ptrNum;i++){
newBlockMap[sa[i]->head->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;i<height;i++){
for(int j=0;j<width;j++){
switch (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;i<height;i++){
for(int j=0;j<width;j++){
this->visit[i][j] = 0;
}
}
}
BlockMap::noticeAllSnakesForNewPath
- 通知所有的蛇重新计算路线
void BlockMap::noticeAllSnakesForNewPath()
{
for(int i=0;i <this->snakeNum ; i++)
{
this->sa[i]->reSearchPath();
}
}
4. Main
#include <QCoreApplication>
#include <snake.h>
#include <blockmap.h>
#include <iostream>
#include <stdio.h>
#include <windows.h>
#include <locale.h>
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; i<numOfSnake; i++)
{
std::cout<<"Snake "<<i+1<<" ";
sa[i]->nextStep();
if(i==numOfSnake-1)std::cout<<"Target:"<<bm->target;
}
Sleep(50);
}
system("cls");
}
return a.exec();
}
3. 总结
至此,我们已经完成了一个多条蛇同时搜索食物的动画,其中我们处理了蛇路径冲突,头方向计算,并且使用智能指针来避免了内存泄漏,使用visit数组减小了计算量。
以下是程序运行实例图
接下来,我将研究带权重的贪吃蛇搜索,也就更加接近于现实状况,使用的算法也可以选择A*等启发式算法来进行,第一次写这种教程,心虚的很,但也是我学习的一种记录,有错误或能够提升效率的方法请联系我,不胜感激 :)