Cocos2d-x 地图行走的实现3:A*算法
本文乃Siliphen原创,转载请注明出处:http://blog.csdn.net/stevenkylelee
上一节《Cocos2d-x
地图行走的实现2:SPFA算法》:
http://blog.csdn.net/stevenkylelee/article/details/38440663
假设读者忘记了之前我们的Dijkstra的实现。请顺藤摸瓜翻到第一节文章回想一下。为什么要这样做呢?由于本节要讲的A*算法事实上是Dijkstra的一种改进,仅仅有理解了Dijkstra才干更好地理解A*。
本节。我们先改动一下之前的Dijkstra的实现,让它变得更像A*的结构。然后。我们再把Dijkstra改成A*。
1.回想和改动一下之前的Dijkstra的实现
回想一下之前Dijkstra的实现。Dijkstra须要从一个表Q中选出一个路径代价最小的顶点。
之前我们的实现是。一開始就把全部的顶点都放入这个表Q中。
细致想下就会发现,那些被初始化为路径代价最大值0x0FFFFFFF的顶点是不可能会被选中的,对于这些顶点不须要遍历。从表中取出的路径代价最小的顶点。取出一个就表示从起点找到了到这个顶点的最短路径,这些顶点不须要再放回列表中。
我们能够对Dijkstra做这样一个小小的优化。尽管还是O(N^2)。时间复杂度没有改变:
一開始仅仅把起始顶点放入表中。
假设松弛成功。就把边终点指向的顶点放入表中。
这样做的话,Relax就要返回结果了。
实现代码例如以下:
void Dijkstra::Execute( const Graph& Graph , const string& VetexId )
{
m_Ret.PathTree.clear( ) ; const auto& Vertexes = Graph.GetVertexes( ) ;
Vertex* pVertexStart = Vertexes.find( VetexId )->second ;
vector< Vertex* > Q ; // 初始化顶点
for ( auto& it : Vertexes )
{
it.second->PathfindingData.Cost = 0x0FFFFFFF ;
pVertexStart->PathfindingData.pParent = 0 ;
}
// 初始化起始顶点
pVertexStart->PathfindingData.Cost = 0 ;
pVertexStart->PathfindingData.pParent = 0 ;
// 把起始顶点放入列表中
Q.push_back( pVertexStart ) ;
pVertexStart->PathfindingData.Flag = true ; for ( ; Q.size() > 0 ; )
{
// 选出最小路径预计的顶点
auto v = ExtractMin( Q ) ;
v->PathfindingData.Flag = false ; // 对全部的出边进行“松弛”
const auto& EO = v->GetEdgesOut( ) ;
for ( auto& it : EO )
{
Edge* pEdge = it.second ;
Vertex* pVEnd = pEdge->GetEndVertex( ) ; bool bRet = Relax( v , pVEnd , pEdge->GetWeight( ) ) ;
// 假设松弛成功,增加列表中。
if ( bRet && pVEnd->PathfindingData.Flag == false )
{
Q.push_back( pVEnd ) ;
pVEnd->PathfindingData.Flag = true ;
}
}
// end for
}
// end for }
Dijkstra要比BFS聪明,BFS仅仅是“盲目地”从队列中取出元素出来扩展,Dijkstra则知道每次应该选取路径代价最短的节点扩展。
2.A*算法
Dijkstra比BFS聪明,A*则比Dijkstra更聪明,执行更快。A*通过一个叫“启示式函数”的东东来改进扩展规则。它会尽量避免扩展其它没用的顶点。它的目标就是朝着目的地直奔而去的。这样说,好像A*长了眼睛一样能看到当前位置距离目标点还有多远。A*和上面的Dijkstra最大的差别就是有“眼睛”:启示式函数。
启示式函数会告诉A*应该优先扩展哪个顶点。启示式函数是怎么回事呢?公式表示是:F = G + H。
简单地说。就是:当前顶点的路径代价(G) + 当前顶点距离目标顶点预计花费的代价(F)
之前对Dijkstra做改动优化。就是为了让它更加像A*算法。这里。把Dijkstra的启示式数据从选拥有最小路径代价的顶点改成选拥有最小的F(启示式函数的值)的顶点就变成了A*。估价函数H怎么设计呢?这里取顶点到目标顶点的距离就可以。
我们须要对上面的Dijkstra和数据结构做例如以下改造:
1.顶点类的寻路数据结构体添加一个Heuristic字段。该字段用于A*算法,保存启示式函数计算出来的值。例如以下所看到的:
class Vertex
{
// ... 省略了一些无关函数和字段
// 和曾经一样 public : // 寻路算法须要的数据
struct Pathfinding
{
// 顶点的前驱顶点。
Vertex * pParent ; // 路径代价预计
int Cost ; // 标识符
int Flag ; // 启示式函数的计算出来的值
int Heuristic ; Pathfinding( )
{
pParent = 0 ;
Cost = 0 ;
Flag = 0 ;
Heuristic = 0 ;
}
}
PathfindingData ;
}
2.Dijkstra的Relax松弛函数,改成限制启示式函数F的值。假设计算出来的F值小于这个顶点原先的F值,就更新该顶点的父节点、实际路径代价、F值。
3.每次循环都推断下,找出来的最小F值的顶点是不是目标顶点。假设是目标顶点,说明找到了路径。算法结束。
用在这里的A*伪代码例如以下:
AStar( 图G。起始顶点S。目标顶点T)
{
把起点S放入Open表中 while( Open表不为空)
{
从Open表中取出估价值F最小的顶点v
标记v不在Open表中 if( v 等于 目标顶点T)
{
// 找到了路径
retrun ;
} foreach( v的全部出边的终点顶点vEnd )
{
Relax( v , vEnd , 边的权值 )
if( Relax松弛成功 且 顶点vEnd不在Open表中 )
{
把vEnd放入Open表中 ;
标记vEnd在Open表中 ;
}
}
} } bool Relax( 顶点from , 顶点to , 边上的权值 )
{
// A*启示式函数计算 F = G + H
G = 顶点from的路径代价 + 边上的权值 ;
H = 顶点to到目标顶点T的预计代价 ;
F = G + H ; if( F < 顶点to的F估价值)
{
记录to的父路径为from ;
顶点to的路径代价值更新为G ;
顶点to的启示式估价值F更新为F ; return true ;
} return false ;
}
能够看到。A*和我们改造的Dijkstra算法。是非常像的。假设我们让 A* 的启示式函数 F=G+H 的 H 一直返回 0。那就是一个 Dijkstra 。道理非常easy, H = 0 ,那就是 F = G + 0 ,F 直接等于 G 了,选拥有最小启示式函数值F的顶点就变成了选拥有最小路径代价的顶点,可见失去估价函数H的 A* 就和 Dijkstra 是一样的。所以,在选顶点方面,优化Dijkstra的方案也是优化A*的方案。
Dijkstra 基于实际的路径代价进行扩展,一定能找到最优解。A*则是基于某种预计。假设你让估价函数H预计得太离谱,A* 就不一定能找到最优解了。
估价值 <= 实际值A*才干找到最优解。
以下是我实现的A*算法。
AStar.h
#pragma once #include "GraphPathfinding.h"
#include <functional> class AStar : public GraphPathfinding
{
public:
AStar( );
~AStar( ); public : // 预计顶点到目标顶点的代价
std::function<int( const Vertex* pVCurrent , const Vertex* pVTarget ) > Estimate ; public: virtual void Execute( const Graph& Graph , const string& VetexId ) override ; private : // 抽出最小路径估值的顶点
inline Vertex* ExtractMin( vector< Vertex* >& Q ) ; // 松弛
inline bool Relax( Vertex* v1 , Vertex* v2 , int Weight ) ; public: void SetTarget( Vertex* pVTarget ) { m_pVTarget = pVTarget ; } private: Vertex* m_pVTarget ; };
AStar.cpp
#include "AStar.h" AStar::AStar( )
{
} AStar::~AStar( )
{
} void AStar::Execute( const Graph& Graph , const string& VetexId )
{
const auto& Vertexes = Graph.GetVertexes( ) ;
Vertex* pVertexStart = Vertexes.find( VetexId )->second ;
vector< Vertex* > Q ; // 初始化顶点
for ( auto& it : Vertexes )
{
Vertex* pV = it.second ; pV->PathfindingData.Cost = 0 ;
pV->PathfindingData.pParent = 0 ;
pV->PathfindingData.Heuristic = 0x0FFFFFFF ;
pV->PathfindingData.Flag = false ;
}
// 初始化起始顶点
pVertexStart->PathfindingData.pParent = 0 ;
pVertexStart->PathfindingData.Cost = 0 ;
pVertexStart->PathfindingData.Heuristic = Estimate( pVertexStart , m_pVTarget ) ;
// 把起始顶点放入列表中
Q.push_back( pVertexStart ) ;
pVertexStart->PathfindingData.Flag = true ; for ( ; Q.size( ) > 0 ; )
{
// 选出最小路径预计的顶点
auto v = ExtractMin( Q ) ;
v->PathfindingData.Flag = false ;
if ( v == m_pVTarget )
{
return ;
} // 对全部的出边进行“松弛”
const auto& EO = v->GetEdgesOut( ) ;
for ( auto& it : EO )
{
Edge* pEdge = it.second ;
Vertex* pVEnd = pEdge->GetEndVertex( ) ; bool bRet = Relax( v , pVEnd , pEdge->GetWeight( ) ) ;
// 假设松弛成功,增加列表中。
if ( bRet && pVEnd->PathfindingData.Flag == false )
{
Q.push_back( pVEnd ) ;
pVEnd->PathfindingData.Flag = true ; }
}
// end for
}
// end for } Vertex* AStar::ExtractMin( vector< Vertex* >& Q )
{
Vertex* Ret = 0 ; Ret = Q[ 0 ] ;
int pos = 0 ;
for ( int i = 1 , size = Q.size( ) ; i < size ; ++i )
{
if ( Ret->PathfindingData.Heuristic > Q[ i ]->PathfindingData.Heuristic )
{
Ret = Q[ i ] ;
pos = i ;
}
} Q.erase( Q.begin( ) + pos ) ; return Ret ;
} bool AStar::Relax( Vertex* v1 , Vertex* v2 , int Weight )
{
// 这里就是启示式函数
int G = v1->PathfindingData.Cost + Weight ; // 取得从V1到V2的实际路径代价
int H = Estimate( v2 , m_pVTarget ) ; // 预计V2到目标节点的路径代价
int nHeuristic = G + H ; // 实际 + 估算 = 启示式函数的值 // 假设从此路径达到目标会被之前计算的更短。就更新
if ( nHeuristic < v2->PathfindingData.Heuristic )
{
v2->PathfindingData.Cost = G ;
v2->PathfindingData.pParent = v1 ; v2->PathfindingData.Heuristic = nHeuristic ; return true ;
} return false ;
}
H函数(预计当前顶点到目标顶点的代价)”外包“到外部运行了。由于AStart类是不知道MapWalkVertex顶点类的存在的。为什么要”外包“运行,而不是在AStar类中做呢?假设要在AStar类中做。就须要知道每一个顶点的几何位置。而顶点的几何位置是Cocos2D-x的Node类的属性。AStar类不应该和其它东西耦合。为了”独立“,”通用“,计算H就用观察者模式思想,”外包“运行了。
AStar类的使用,例如以下:
// A*的H估价函数
auto Estimate = [ ]( const Vertex* pVCurrent , const Vertex* pVTarget )->int
{
MapWalkVertex * pMwv1 = ( MapWalkVertex* )pVCurrent->UserData.find( "mwv" )->second ;
MapWalkVertex * pMwv2 = ( MapWalkVertex* )pVTarget->UserData.find( "mwv" )->second ;
Point v = pMwv1->getPosition( ) - pMwv2->getPosition( ) ;
int H = v.getLength( ) ;
return H ; } ; AStar AStar ;
// 设置目的顶点
AStar.SetTarget( pVertexTarget ) ;
// 设置H估价函数
AStar.Estimate = Estimate ;
// 開始运行
AStar.Execute( *m_pGraph , pMwvStart->GetGraphVertex( )->GetId( ) ) ;
OK ,A* 完毕了。測试执行一下:
经过測试。我们的A*能找到最短路径。而且运行速度比Dijkstra和Spfa都快。
4.简要总结Djikstra。SPFA。A*算法
Dijsktra : 选出一个具有最小路径代价的顶点,松弛其全部的边。
SPFA : 用一个队列存放顶点。从队列中取出队头顶点,松弛其全部边,假设松弛成功,边上顶点入队。
A* : 是Djikstra的改进版。选出具有启示式函数值最小的顶点,松弛其全部的边。
4.本文源码project下载:
Cocos2d-x 地图行走的实现3:A*算法的更多相关文章
- 百度地图API位置偏移的校准算法
转自极客人原文 百度地图API位置偏移的校准算法 在开始使用百度地图API进行开发时可能会遇到一件相当奇怪的事情,使用百度定位的经纬度在地图上显示相当不准确,这一问题我在微信开发和安卓开始时都遇到过. ...
- Cocos2D实现RPG游戏人物地图行走的跟随效果
大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请多提意见,如果觉得不错请多多支持点赞.谢谢! hopy ;) 在一些RPG游戏中,人物队列在地图中行走的时候有时需要实现一个 ...
- Cocos2D瓦块地图高清屏(retina)显示比例问题的解决
大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请多提意见,如果觉得不错请多多支持点赞.谢谢! hopy ;) 在Cocos2D的游戏编程里,常用到瓦块地图.而cocos2D ...
- Cocos2d-x 地图步行实现1:图论Dijkstra算法
下一节<Cocos2d-x 地图行走的实现2:SPFA算法>: http://blog.csdn.net/stevenkylelee/article/details/38440663 本文 ...
- Cocos2d-x 2地图步行实现:SPFA算法
本文乃Siliphen原创,转载请注明出处:http://blog.csdn.net/stevenkylelee 上一节<Cocos2d-x 地图行走的实现1:图论与Dijkstra算法> ...
- TileMap地图
参考资料: http://8287044.blog.51cto.com/5179921/1045274 TileMap编辑器使用 1.认识TileMap TileMap是一款开源的地图编辑 ...
- Slam(即时定位与地图构建) 知识篇
Slam即时定位与地图构建 技术解释 同步定位与地图构建(SLAM或Simultaneous localization and mapping)是一种概念:希望机器人从未知环境的未知地点出发,在运动过 ...
- roguelike地图的随机生成算法
如果要想自己设计一个roguelike游戏,那么需要你有一个随机地图生成,我在indienova上看到一篇文章,描述了一个roguelike算法,然后自己用unity实现了一个下. 原文地址:随机生成 ...
- 地图POI类别标签体系建设实践
导读 POI是“Point of interest”的缩写,中文可以翻译为“兴趣点”.在地图上,一个POI可以是一栋房子.一个商铺.一个公交站.一个湖泊.一条道路等.在地图搜索场景,POI是检索对象, ...
随机推荐
- PB数据管道
数据管道提供了一种不同数据库之间传递数据和(或)表结构的方法. 数据管道对象 要完毕数据管道的功能须要提供例如以下内容: 须要数据源和目标数据库,并可以和这两个数据库正常联接 须要源数据库中的哪些表: ...
- 宽屏手机显示9.png的图片拉伸不均衡
制作的一个.9的背景图片,在一般的480宽的手机上显示没有问题,正常拉伸,用三星的一个宽屏手机测试时,没有完全拉伸,一边拉伸多一点,一边拉伸少一点 决绝办法:就是在制作.9的时候,我在横向拉伸的地方, ...
- boost uuid
uuid: uuid库是一个小的使用工具,可以表示和生成UUID UUID是University Unique Identifier的缩写,它是一个128位的数字(16字节),不需要有一个中央认证机构 ...
- 教师简介 (Alma Del Tango的小站)
教师简介 (Alma Del Tango的小站) Esteban Peng (TT) & Emilia Jia (Amy) TT和Amy是北京极具影响力的专业舞者,他们从07年开始推广阿根廷探 ...
- Recipes — Bottle 0.13-dev documentation
Recipes - Bottle 0.13-dev documentation Recipes¶ This is a collection of code snippets and examples ...
- DDD领域驱动设计的理解
DDD领域驱动设计的理解 从遇到问题开始 当人们要做一个软件系统时,一般总是因为遇到了什么问题,然后希望通过一个软件系统来解决. 比如,我是一家企业,然后我觉得我现在线下销售自己的产品还不够,我希望能 ...
- 讲解下for循环的用法,加深记忆
引子 这是一段很简单的代码,但是即便是这么简单的东西,这里我们还是需要说一下. 关于for循环整个执行流程就是,先执行var i=10,然后到了第二个语句,判断10是否大于0,很明显为true,所以此 ...
- window.name 跨域
跨域的由来 JavaScript出于安全方面的考虑,不允许跨域调用其他页面的对象.但是我们常常会遇到无法避免跨域的情况,如普通文章站点(article.xxx.com)需要评论,而评论站点却在chea ...
- Delphi下用API代码创建Form
program PMyWindowClass; uses Windows, Messages, SysUtils; type TMyWindow = class(TObject) priva ...
- HDU 4870 Rating (2014 多校联合第一场 J)(概率)
题意: 一个人有两个TC的账号,一开始两个账号rating都是0,然后每次它会选择里面rating较小的一个账号去打比赛,每次比赛有p的概率+1分,有1-p的概率-2分,当然如果本身是<=2分的 ...