寻路算法是客户端程序的重点难点之一
普通的广度优先遍历可以找到最短路径
然后耗时较长
A*算法的意义边在于用更短的时间去找到最短路径
做这个例子之前参考了许多文章
http://blog.csdn.net/b2b160/article/details/4057781
http://blog.csdn.net/aerror/article/details/7023406
首先A星的概念引用一下第一篇文章里的描述
OPEN = priority queue containing START
CLOSED = empty set
while lowest rank in OPEN is not the GOAL:
current = remove lowest rank item from OPEN
add current to CLOSED
for neighbors of current:
cost = g(current) + movementcost(current, neighbor)
if neighbor in OPEN and cost less than g(neighbor):
remove neighbor from OPEN, because new path is better
if neighbor in CLOSED and cost less than g(neighbor): **
remove neighbor from CLOSED
if neighbor not in OPEN and neighbor not in CLOSED:
set g(neighbor) to cost
add neighbor to OPEN
set priority queue rank to g(neighbor) + h(neighbor)
set neighbor's parent to current
reconstruct reverse path from goal to start
by following parent pointers
(**) This should never happen if you have an admissible heuristic. However in games we often have inadmissible heuristics.
根据这个原理
自己手动用cocos写了一个A星搜索的例子
地图大小是128*70 = 9000左右格子
当地图完全随机时(所有格子的80%可以走,20%不能走)
沿对角线从左下角走到右上角寻路时间大概是0.05~0.07
这种时间消耗个人感觉还是比较理想的
AStarMap::findPath over path found! 286
AStarMap::findPath cost 0.046000
HelloWorld::drawPath steps = 131
主循环286次,用时0.046秒,路径长度131步
这里可以看出一个问题,路线并不是一条对角线!
主要因为计算F值时,我使H=abs(x-x)+abs(y-y),避免了使用平方开方耗时
当然如果在游戏中,允许斜线走格子时,H值要算的更准确
然而之后我在当前的地图上
又竖起了六道大墙
来测试复杂地形对A星带来的影响
如图所示
AStarMap::findPath over path found! 5686
HelloWorld::drawPath steps = 304
这时A星需要遍历5600多次(满屏9000个格子约有7000个白格子可以走,也就是说遍历的上限是7000次)
路径长度也增加了一倍
最初我的A星算法需要消耗2.6秒左右
最后再我的不断调整下缩短到0.5秒以内(还可以继续优化)
下面分享一下 优化的策略
1.降低循环消耗
A星每一次遍历需要拿到open集中的最优节点
因此首先想到的会是对open集合排序
在这个前提下
5600多次的排序,而且有时open集中的元素多达上百个
显然是非常耗时的
我所用的open集合是一个vector(十分无脑)
一个简单粗暴的方法可以大幅提升效率
就是放弃std::sort
因为std::sort把整个数组都sort了
我们其实只需要拎出来最优的那一个
因此自己写一个查找,一趟下来找到最优的,查到open集最前面!
只要理解了A星的意义,不难想出这个方法
这个方法有一个缺点,就是仍然做了5600多次查找!
如果继续使用sort而减少sort的次数
也能一定程度上提升效率
最简单的例子
当open集合sort一次之后
下一次遍历邻节点时
若邻节点的F值比当前open[0]的还好
那么直接插它前面!
这样这一次循环后,我们其实无需sort
因为我们知道(插过之后的)open[0]就是当前最好的节点
另一种情况
此次循环的邻节点都没有open[0]好
那么全部push_back
此时,仍然open[0]是当前最好的节点,还是可以不sort
基于这种思路,我使用了记数的办法
记录每时每刻从首元素开始,有序元素的长度
当有序元素长度==0时才sort
这样减少了80%的sort次数
效率提升翻倍
如果更进一步
判断open集的长度,仅对前十元素排序
动态的比较新节点的价值是否进入前十
并维护前十元素的有序性(只sort前十,小sort)
当前十一个个用完了也没补上的时候,再来一个大sort
当然这个思路实现起来比较麻烦
所以我也没去实现
2.设计数据结构
深入的优化必然需要考虑选择更合适的数据结构
普通vector局限性很强
sort浪费时间
然而如果使每一个新节点加入open时有序插入
那么sort可以省略了
这里看了其他大牛的经验
使用二元堆/二叉堆的思路非常好
这样的进一步优化十分高效
3.用空间换时间
A星其中一步需要检查新发现的邻居节点是否在open中已经有了
最初我的做法是遍历open表
后来我使用的一个新的二维数组来存放open(open中的元素存两遍,vector中的用于查找最优,二维数组中检测是否存在)
这样很纯的方法,证明也确实能提高一点效率
因为如果每次新节点都要去遍历open表(大部分结果是未找到)
十分无意义,浪费时间
这种空间换时间的策略
还可以用于H值的计算
因为H值是一定的,一次计算后并保存
省去以后每次遍历邻节点时的重复计算
4.人工智能与人类智能结合的究极优化
以上策略都是优化A星本身
然而我们只能提升每一步算法的效率
面对复杂地图,A星还是会犯错误
例如加了6面墙的地图
无论如何优化设计F值,我的A星都需要遍历5600次左右
即使我已经优化到了0.5秒以内
与没有墙时的286次循环耗时0.05秒依然有着10倍的差距
这时最好的策略就是用人脑来帮一帮A星
假设在游戏中有相似的情况
阻挡路线的是河而不是墙
若要过河必须走桥否则必然碰壁
那么可以把整张大地图划分为多个区域
在区域内的寻路必然超快,因为没有大的阻挡,路线也短
而跨区域的寻路
可以使用拆分策略
例如假设两区域间必然要过桥
那么就分别用A星计算
起点~桥
桥~终点
这两段路程
这样A星省去绝大部分错误的尝试
可以大幅提升效率
最后附上源代码
虽然不是最好的A星~
#pragma once #include <vector> struct Pos{ short x, y; Pos(short _x, short _y) :x(_x), y(_y) { } }; class Step { public: Step(short _x, short _y, Step* _parent) :x(_x), y(_y), parent(_parent) {}; ~Step(){}; short x, y; //出发消耗精确,斜线相邻距离为1.4 float g; //到达消耗估算,斜线相邻距离为2(省去开方计算) float h; //前节点 Step* parent; float getF() { //F越小越优先 return g + h; }; }; class AStarMap { public: AStarMap(short **_map, short _width, short _height); ~AStarMap() { delete[] map; }; std::vector<Step*> getNeighbors(Step* step, Pos origin, Pos destination, short direction); static Step* findPath(AStarMap * map , Pos origin, Pos destination, short direction = 8, bool isAStar = true); private: short **map; short width; short height; };
#include "AStar.h" #include "cocos2d.h" #include <vector> #include <set> #include <algorithm> static const int SORT_LENGTH = 10; static bool compare(Step* i, Step* j) { return i->getF() < j->getF(); } static bool compareReverse(Step* i, Step* j) { return i->getF() > j->getF(); } static void sort(std::vector<Step*> &vec) { // only care about the best one // do not sort the others size_t index = 0; for (size_t i = 1; i < vec.size(); i++) { if (vec[i]->getF() < vec[index]->getF()) index = i; } if (index != 0) std::swap(vec[0], vec[index]); } AStarMap::AStarMap(short **_map, short _width, short _height) { this->width = _width; this->height = _height; map = _map; } std::vector<Step*> AStarMap::getNeighbors(Step* step, Pos origin, Pos destination, short direction) { std::vector<Step*> neighbors; short x; short y; for (int i = -1; i <= 1; i++) { for (int j = -1; j <= 1; j++) { if (!(i == 0 && j == 0)) { //neighbor不能等于自己 if (i != 0 && j != 0 && direction == 4) { //direction 4 时不考虑对角线 } else { x = step->x + i; y = step->y + j; if (x >= 0 && x < width && y >= 0 && y < height) { //没有出界 if (map[x][y] == 1) { // whether this neighbor can get through Step * neighbor = new Step(x, y, step); float stepCost = (i == 0 || j == 0) ? 1.0f : 1.4f; neighbor->g = step->g + stepCost; neighbor->h = abs(destination.x - x) + abs(destination.y - y); neighbors.push_back(neighbor); } } } } } } return neighbors; } Step* AStarMap::findPath(AStarMap * map, Pos origin, Pos destination, short direction, bool isAStar) { //F越小越优先 CCLOG("AStarMap::findPath"); clock_t start, finish; float totaltime; start = clock(); // result Step* target = nullptr; // report value int loopOpens = 0; int loopOpenReduces = 0; int loopTimes = 0; int sortTimes = 0; int sortReduceInside = 0; int sortReduceOutside = 0; int sortReduceLast = 0; // open set std::vector<Step*> open; // close set bool **closed = new bool*[map->width]; float **G = new float*[map->width]; Step ***O = new Step**[map->width]; for (int i = 0; i < map->width; i++) { closed[i] = new bool[map->height]; G[i] = new float[map->height]; O[i] = new Step*[map->height]; for (int j = 0; j < map->height; j++) { closed[i][j] = false; G[i][j] = -1.0f; O[i][j] = nullptr; } } // make first step , add it to open Step* currentStep = new Step(origin.x, origin.y, nullptr); currentStep->g = 0; currentStep->h = abs(destination.x - origin.x) + abs(destination.y - origin.y); open.push_back(currentStep); bool bOver; bool bSort; bool bOpen; short frontSorted = 1; std::vector<Step*>::iterator n; while (open.size() > 0) { loopTimes++; bOver = false; currentStep = open[0]; open.erase(open.begin()); closed[currentStep->x][currentStep->y] = true; // neighbors u can go(without forbidden pos) std::vector<Step*> neighbors = map->getNeighbors(currentStep, origin, destination, direction); for (n = neighbors.begin(); n != neighbors.end();) { // whether this neighbor in closed if (closed[(*n)->x][(*n)->y]) { delete *n; n = neighbors.erase(n); continue; } // whether this neighbor in open bOpen = G[(*n)->x][(*n)->y] > 0.0f; if (!bOpen) { loopOpenReduces++; } if (bOpen) { if ((*n)->g < G[(*n)->x][(*n)->y]) { loopOpens++; // this neighbor in open got a better G then before G[(*n)->x][(*n)->y] = (*n)->g; O[(*n)->x][(*n)->y]->g = (*n)->g; O[(*n)->x][(*n)->y]->parent = (*n)->parent; } else { loopOpenReduces++; } delete *n; n = neighbors.erase(n); continue; } // whether this neighbor is TARGET if ((*n)->x == destination.x && (*n)->y == destination.y) { bOver = true; target = *n; // clear neighbors for (n = ++n; n != neighbors.end(); ++n) { delete *n; } neighbors.clear(); break; } // well , it's just a new neighbor // add to open open.push_back(*n); G[(*n)->x][(*n)->y] = (*n)->g; O[(*n)->x][(*n)->y] = *n; // go to next ++n; } // check whether the loop is over if (bOver) { for (std::vector<Step*>::iterator o = open.begin(); o != open.end(); ++o) { delete *o; } open.clear(); break; } else { neighbors.clear(); sort(open); } } if (target == nullptr) CCLOG("AStarMap::findPath over can't find path %d", loopTimes); else CCLOG("AStarMap::findPath over path found! %d", loopTimes); finish = clock(); totaltime = (double)(finish - start) / CLOCKS_PER_SEC; CCLOG("AStarMap::findPath sortTimes %d , loopOpens %d , loopOpenReduce %d ", loopTimes, loopOpens, loopOpenReduces); CCLOG("AStarMap::findPath cost %f", totaltime); delete[] closed; delete[] G; delete[] O; return target; }
#ifndef __HELLOWORLD_SCENE_H__ #define __HELLOWORLD_SCENE_H__ #include "cocos2d.h" #include "AStar.h" #define WIDTH 128 #define HEIGHT 70 #define CELL_WIDTH 10 #define CELL_HEIGHT 10 class HelloWorld : public cocos2d::Layer { public: HelloWorld(); virtual ~HelloWorld(); // there's no 'id' in cpp, so we recommend returning the class instance pointer static cocos2d::Scene* createScene(); // Here's a difference. Method 'init' in cocos2d-x returns bool, instead of returning 'id' in cocos2d-iphone virtual bool init(); // a selector callback void findCallback(cocos2d::Ref* pSender); void restartCallback(cocos2d::Ref* pSender); // implement the "static create()" method manually CREATE_FUNC(HelloWorld); Node* root; short** map; void drawMap(); void drawPath(Step*); }; #endif // __HELLOWORLD_SCENE_H__
#include "HelloWorldScene.h" USING_NS_CC; HelloWorld::HelloWorld() :map(nullptr) {} HelloWorld::~HelloWorld() { delete[] map; } Scene* HelloWorld::createScene() { // 'scene' is an autorelease object auto scene = Scene::create(); // 'layer' is an autorelease object auto layer = HelloWorld::create(); // add layer as a child to scene scene->addChild(layer); // return the scene return scene; } // on "init" you need to initialize your instance bool HelloWorld::init() { // // 1. super init first if ( !Layer::init() ) { return false; } Size visibleSize = Director::getInstance()->getVisibleSize(); Vec2 origin = Director::getInstance()->getVisibleOrigin(); auto findItem = MenuItemImage::create( "CloseNormal.png", "CloseSelected.png", CC_CALLBACK_1(HelloWorld::findCallback, this)); findItem->setPosition(Vec2(origin.x + visibleSize.width - findItem->getContentSize().width / 2, origin.y + findItem->getContentSize().height / 2)); auto restartItem = MenuItemImage::create( "CloseSelected.png", "CloseNormal.png", CC_CALLBACK_1(HelloWorld::restartCallback, this)); restartItem->setPosition(Vec2(origin.x + visibleSize.width - restartItem->getContentSize().width / 2, origin.y + restartItem->getContentSize().height * 4/ 2)); // create menu, it's an autorelease object auto menu = Menu::create(findItem, restartItem, NULL); menu->setPosition(Vec2::ZERO); this->addChild(menu, 1); CCLOG(""); root = Node::create(); this->addChild(root); //drawMap(); return true; } void HelloWorld::drawMap() { map = new short*[WIDTH]; //随机 for (int i = 0; i < WIDTH; i++) { map[i] = new short[HEIGHT]; for (int j = 0; j < HEIGHT; j++) { bool avaliable = CCRANDOM_MINUS1_1() > -0.6f; if (i <= 1 || i >= WIDTH - 2 || j <= 1 || j >= HEIGHT - 2) { //avaliable = true; } map[i][j] = avaliable ? 1 : 0; } } //墙 for (int i = 1; i < 6; i++) { for (int j = 0; j < HEIGHT - 10; j++) { auto x = WIDTH * i / 6; auto y = i % 2 == 0 ? j : HEIGHT - j - 1; map[x][y] = 0; } } map[0][0] = 1; map[WIDTH - 1][HEIGHT - 1] = 1; for (int i = 0; i < WIDTH; i++) { for (int j = 0; j < HEIGHT; j++) { bool avaliable = map[i][j] == 1; auto sprite = Sprite::create(avaliable ? "white.png" : "black.png"); sprite->setAnchorPoint(ccp(0, 0)); sprite->setPosition(ccp(i * CELL_WIDTH, j * CELL_HEIGHT)); root->addChild(sprite); } } } void HelloWorld::drawPath(Step* step) { if (step) { int steps = 0; while (step != nullptr) { auto x = step->x; auto y = step->y; Step* s = step; step = s->parent; delete s; auto sprite = Sprite::create("stars.png"); sprite->setAnchorPoint(ccp(0, 0)); sprite->setPosition(ccp(x * CELL_WIDTH, y * CELL_HEIGHT)); root->addChild(sprite); steps++; } CCLOG("HelloWorld::drawPath steps = %d", steps); } else { CCLOG("HelloWorld::drawPath step null , no path"); } } void HelloWorld::findCallback(Ref* pSender) { AStarMap* AStar = new AStarMap(map, WIDTH, HEIGHT); auto path = AStarMap::findPath(AStar, Pos(0, 0), Pos(WIDTH - 1, HEIGHT - 1) , 8); drawPath(path); } void HelloWorld::restartCallback(Ref* pSender) { root->removeAllChildren(); delete[] map; root->runAction(CCSequence::create( CCDelayTime::create(0.5f), CallFunc::create(CC_CALLBACK_0(HelloWorld::drawMap, this)), nullptr )); }