A*算法实现游戏寻路及优化思路

寻路算法是客户端程序的重点难点之一

普通的广度优先遍历可以找到最短路径

然后耗时较长

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;
};
View Code
#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;
}
View Code
#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__
View Code
#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
        ));
}
View Code

 

转载于:https://www.cnblogs.com/billyrun/articles/5498802.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值