尋路算法是客戶端程序的重點難點之一
普通的廣度優先遍歷可以找到最短路徑
然后耗時較長
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 )); }