在学习本篇教程之前,如果你有cocos2d-x的开发经验,将会有所帮助。如果没有也没关系,因为你可以将这里讲解的例子迁移到其他的语言或者框架中。找到到达你键盘的最短路径,开始吧!Maze猫首先介绍下我们将要在本篇教程中开发的简单游戏。前往下载本篇教程的 工程代码 。编译运行工程,你将看到以下画面。在这款游戏中,你扮演着一只小偷猫,在一个由危险的狗守护着的地牢里小心穿行。如果你试图穿过一只狗,他会把你吃掉 – 除非你可以用骨头去贿赂它!所以在这款游戏中,你的任务是尝试以正确的顺序捡起骨头,然后 寻找路线 穿过狗逃离。注意到猫只能水平或者垂直的移动(例如不能斜线移动),并且会从一个方块的中心点移动到另一个中心点。每个方块既可以是可通行的也可以是不可通行的。尝试下这款游戏,看看你能否找到出路!建议你阅读代码以熟悉它的原理。这是一款相当普通的方块-地图式游戏,我们会在接下来的教程中修改它并使用上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.000000To: 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;}编译运行,在地图上进行点击,如下图所示:From: 24.000000, 0.000000To: 23.000000, 3.000000PATH FOUND :pos=[23;3] g=10 h=0 f=10pos=[22;3] g=9 h=1 f=10pos=[21;3] g=8 h=2 f=10pos=[20;3] g=7 h=3 f=10pos=[20;2] g=6 h=4 f=10pos=[20;1] g=5 h=5 f=10pos=[21;1] g=4 h=4 f=8pos=[22;1] g=3 h=3 f=6pos=[23;1] g=2 h=2 f=4pos=[24;1] g=1 h=3 f=4pos=[24;0] g=0 h=0 f=0注意该路径是从后面建立的,所以必须从下往上看猫选择了哪条路径。跟随路径前进现在已经找到了路径,只需让猫跟随前进即可。需要创建一个数组去存储路径,打开CatSprite.h文件,添加如下代码:cocos2d::Vector _shortestPath;打开CatSprite.cpp文件,更改moveToward方法,注释掉语句**bool pathFound = false**;,如下://bool pathFound = false;替换语句**pathFound = true;**为如下://pathFound = true;this ->constructPathAndStartAnimationFromStep(currentStep);并且注释掉下方的调试语句://ShortestPathStep *tmpStep = currentStep;//CCLOG("PATH FOUND :");//do//{// CCLOG("%s", tmpStep->getDescription().c_str());// tmpStep = tmpStep->getParent(); // 倒退/