如何基于cocos2dx3.x实现A星寻路算法

在这款游戏中,你扮演着一只小偷猫,在一个由危险的狗守护着的地牢里小心穿行。如果你试图穿过一只狗,他会把你吃掉 –

除非你可以用骨头去贿赂它!

所以在这款游戏中,你的任务是尝试以正确的顺序捡起骨头,然后 寻找路线

穿过狗逃离。

注意到猫只能水平或者垂直的移动(例如不能斜线移动),并且会从一个方块的中心点移动到另一个中心点。每个方块既可以是可通行的也可以是不可通行的。

尝试下这款游戏,看看你能否找到出路!建议你阅读代码以熟悉它的原理。这是一款相当普通的方块-地图式游戏,我们会在接下来的教程中修改它并使用上A星寻路算法。

Maze猫和A星概览

正如你所看到的,当你点击地图某处时,猫会沿着你点击的方向跳到相邻的方块上。

我们想对程序做修改,让猫持续的往你点击的方块方向前进,就像许多RPGs或者point-and-click冒险类游戏。

让我们看下控制触摸事件代码的工作原理。如果你打开HelloWorldScene.cpp文件,你将看到像下面这样去实现触摸操作:

auto

listener =

EventListenerTouchOneByOne::create();

listener->setSwallowTouches( true

);

listener->onTouchBegan = [ this ](Touch *touch, Event *event){

if

(_gameOver)

{

return false ;

}

Point touchLocation =

_tileMap->convertTouchToNodeSpace(touch);

_cat->moveToward(touchLocation);

return

true

};

_eventDispatcher->addEventListenerWithSceneGraphPriority(listener,

this

);

你可以看到这里只是对猫精灵调用了一个方法,让猫在方块地图上往你点击的地方移动。

我们现在要做的是修改在CatSprite.m文件中的以下方法,寻找到达该点的最短路径,并且开始前进:

void

CatSprite::moveToward( const Point &target)

{

}

创建ShortestPathStep类

我们开始创建一个内部类,代表路径上的一步操作。在这种情况下,它是一个方块和由A星算法计算出来的的F,G和H

scores。

class ShortestPathStep : public cocos2d::Object

{

public

:

ShortestPathStep();

~ShortestPathStep();

static ShortestPathStep

*createWithPosition( const cocos2d::Point &pos);

bool initWithPosition(

const cocos2d::Point &pos);

int getFScore() const ;

bool isEqual(

const ShortestPathStep *other) const ;

std::string getDescription() const

CC_SYNTHESIZE(cocos2d::Point, _position, Position);

CC_SYNTHESIZE( int ,

_gScore, GScore);

CC_SYNTHESIZE( int , _hScore,

HScore);

CC_SYNTHESIZE(ShortestPathStep*, _parent,

Parent);

};

现在添加以下代码到CatSprite.cpp文件的顶部。

CatSprite::ShortestPathStep::ShortestPathStep()

:

_position(Point::ZERO),

_gScore(0),

_hScore(0),

_parent(nullptr)

{

}

CatSprite::ShortestPathStep::~ShortestPathStep()

{

}

CatSprite::ShortestPathStep

*CatSprite::ShortestPathStep::createWithPosition( const Point

&pos)

{

ShortestPathStep *pRet = new ShortestPathStep();

if (pRet

&&

pRet->initWithPosition(pos))

{

pRet->autorelease();

return

pRet;

}

else

{

CC_SAFE_DELETE(pRet);

return

nullptr;

}

}

bool CatSprite::ShortestPathStep::initWithPosition( const

Point &pos)

{

bool bRet = false ;

do

{

this

->setPosition(pos);

bRet = true ;

} while (0);

return

bRet;

}

int CatSprite::ShortestPathStep::getFScore() const

{

return

this ->getGScore() + this ->getHScore();

}

bool

CatSprite::ShortestPathStep::isEqual( const CatSprite::ShortestPathStep *other)

const

{

return this ->getPosition() ==

other->getPosition();

}

std::string

CatSprite::ShortestPathStep::getDescription() const

{

return

StringUtils::format( "pos=[%.0f;%.0f] g=%d h=%d f=%d" ,

this

->getPosition().x, this ->getPosition().y,

this ->getGScore(), this

->getHScore(), this ->getFScore());

}

正如所见,这是一个很简单的类,记录了以下内容:

-

方块的坐标

- G值(记住,这是开始点到当前点的方块数量)

- H值(记住,这是当前点到目标点的方块估算数量)

-

Parent是它的上一步操作

-

F值,这是方块的和值(它是G+H的值)

这里定义了getDescription方法,以方便调试。创建了isEquals方法,当且仅当两个ShortestPathSteps的方块坐标相同时,它们相等(例如它们代表着相同的方块)。

创建Open和Closed列表

打开CatSprite.h文件,添加如下代码:

cocos2d::Vector

_spOpenSteps;

cocos2d::Vector

_spClosedSteps;

检查开始和结束点

重新实现moveToward方法,获取当前方块坐标和目标方块坐标,然后检查是否需要计算一条路径,最后测试目标方块坐标是否可行走的(在这里只有墙壁是不可行走的)。打开CatSprite.cpp文件,修改moveToward方法,为如下:

void

CatSprite::moveToward( const Point &target)

{

Point fromTileCoord =

_layer->tileCoordForPosition( this ->getPosition());

Point toTileCoord

= _layer->tileCoordForPosition(target);

if (fromTileCoord ==

toTileCoord)

{

CCLOG( "You're already there! :P" );

return ;

}

if

(!_layer->isValidTileCoord(toTileCoord) ||

_layer->isWallAtTileCoord(toTileCoord))

{

SimpleAudioEngine::getInstance()->playEffect(

"hitWall.wav" );

return ;

}

CCLOG( "From: %f, %f" , fromTileCoord.x,

fromTileCoord.y);

CCLOG( "To: %f, %f" , toTileCoord.x,

toTileCoord.y);

}

编译运行,在地图上进行点击,如果不是点击到墙壁的话,可以在控制台看到如下信息:

From:

24.000000, 0.000000

To: 20.000000, 0.000000

其中 **From**

就是猫的方块坐标,**To**就是所点击的方块坐标。

实现A星算法

根据算法,第一步是添加当前坐标到open列表。还需要三个辅助方法:

-

一个方法用来插入一个ShortestPathStep对象到适当的位置(有序的F值)

- 一个方法用来计算从一个方块到相邻方块的移动数值

-

一个方法是根据"曼哈顿距离"算法,计算方块的H值

打开CatSprite.cpp文件,添加如下方法:

void

CatSprite::insertInOpenSteps(CatSprite::ShortestPathStep *step)

{

int

stepFScore = step->getFScore();

ssize_t count =

_spOpenSteps.size();

ssize_t i = 0;

for (; i < count; ++i)

{

if

(stepFScore <= _spOpenSteps.at(i)->getFScore())

{

break

}

}

_spOpenSteps.insert(i, step);

}

int

CatSprite::computeHScoreFromCoordToCoord( const Point &fromCoord, const

Point &toCoord)

{

// 忽略了可能在路上的各种障碍

return abs(toCoord.x - fromCoord.x) + abs(toCoord.y -

fromCoord.y);

}

int CatSprite::costToMoveFromStepToAdjacentStep( const

ShortestPathStep *fromStep, const ShortestPathStep *toStep)

{

//

因为不能斜着走,而且由于地形就是可行走和不可行走的成本都是一样的

// 如果能够对角移动,或者有沼泽、山丘等等,那么它必须是不同的

return

1;

}

接下来,需要一个方法去获取给定方块的所有相邻可行走方块。因为在这个游戏中,HelloWorld管理着地图,所以在那里添加方法。打开HelloWorldScene.cpp文件,添加如下方法:

PointArray

*HelloWorld::walkableAdjacentTilesCoordForTileCoord( const Point &tileCoord)

const

{

PointArray *tmp = PointArray::create(4);

// 上

Point

p(tileCoord.x, tileCoord.y - 1);

if ( this ->isValidTileCoord(p)

&& ! this

->isWallAtTileCoord(p))

{

tmp->addControlPoint(p);

}

//

p.setPoint(tileCoord.x - 1, tileCoord.y);

if ( this

->isValidTileCoord(p) && ! this

->isWallAtTileCoord(p))

{

tmp->addControlPoint(p);

}

//

p.setPoint(tileCoord.x, tileCoord.y + 1);

if ( this

->isValidTileCoord(p) && ! this

->isWallAtTileCoord(p))

{

tmp->addControlPoint(p);

}

//

p.setPoint(tileCoord.x + 1, tileCoord.y);

if ( this

->isValidTileCoord(p) && ! this

->isWallAtTileCoord(p))

{

tmp->addControlPoint(p);

}

return

tmp;

}

可以继续CatSprite.cpp中的moveToward方法了,在moveToward方法的后面,添加如下代码:

bool

pathFound = false ;

_spOpenSteps.clear();

_spClosedSteps.clear();

//

首先,添加猫的方块坐标到open列表

this

->insertInOpenSteps(ShortestPathStep::createWithPosition(fromTileCoord));

do

{

//

得到最小的F值步骤

// 因为是有序列表,第一个步骤总是最小的F值

ShortestPathStep *currentStep =

_spOpenSteps.at(0);

//

添加当前步骤到closed列表

_spClosedSteps.pushBack(currentStep);

//

将它从open列表里面移除

//

需要注意的是,如果想要先从open列表里面移除,应小心对象的内存

_spOpenSteps.erase(0);

//

如果当前步骤是目标方块坐标,那么就完成了

if (currentStep->getPosition() ==

toTileCoord)

{

pathFound = true ;

ShortestPathStep *tmpStep =

currentStep;

CCLOG( "PATH FOUND :" );

do

{

CCLOG( "%s" ,

tmpStep->getDescription().c_str());

tmpStep = tmpStep->getParent(); //

倒退

} while (tmpStep); //

直到没有上一步

_spOpenSteps.clear();

_spClosedSteps.clear();

break

}

// 得到当前步骤的相邻方块坐标

PointArray *adjSteps =

_layer->walkableAdjacentTilesCoordForTileCoord(currentStep->getPosition());

for

(ssize_t i = 0; i < adjSteps->count(); ++i)

{

ShortestPathStep *step

=

ShortestPathStep::createWithPosition(adjSteps->getControlPointAtIndex(i));

//

检查步骤是不是已经在closed列表

if ( this ->getStepIndex(_spClosedSteps, step) !=

-1)

{

continue ;

}

// 计算从当前步骤到此步骤的成本

int moveCost = this

->costToMoveFromStepToAdjacentStep(currentStep, step);

//

检查此步骤是否已经在open列表

ssize_t index = this ->getStepIndex(_spOpenSteps,

step);

// 不在open列表,添加它

if (index == -1)

{

//

设置当前步骤作为上一步操作

step->setParent(currentStep);

// G值等同于上一步的G值 +

从上一步到这里的成本

step->setGScore(currentStep->getGScore() + moveCost);

//

H值即是从此步骤到目标方块坐标的移动量估算值

step->setHScore( this

->computeHScoreFromCoordToCoord(step->getPosition(), toTileCoord));

//

按序添加到open列表

this ->insertInOpenSteps(step);

}

else

{

//

获取旧的步骤,其值已经计算过

step = _spOpenSteps.at(index);

// 检查G值是否低于当前步骤到此步骤的值

if

((currentStep->getGScore() + moveCost) < step->getGScore())

{

//

G值等同于上一步的G值 + 从上一步到这里的成本

step->setGScore(currentStep->getGScore() +

moveCost);

// 因为G值改变了,F值也会跟着改变

// 所以为了保持open列表有序,需要将此步骤移除,再重新按序插入

//

在移除之前,需要先保持引用

step->retain();

//

现在可以放心移除,不用担心被释放

_spOpenSteps.erase(index);

// 重新按序插入

this

->insertInOpenSteps(step);

//

现在可以释放它了,因为open列表应该持有它

step->release();

}

}

}

} while

(_spOpenSteps.size() > 0);

if

(!pathFound)

{

SimpleAudioEngine::getInstance()->playEffect(

"hitWall.wav" );

}

添加以下方法:

ssize_t CatSprite::getStepIndex( const

cocos2d::Vector &steps, const CatSprite::ShortestPathStep *step)

{

for

(ssize_t i = 0; i < steps.size(); ++i)

{

if

(steps.at(i)->isEqual(step))

{

return i;

}

}

return

-1;

}