A*算法的C#实现

简介: 在游戏开发中,AI的最基本问题之一就是寻路算法或称路径规划算法,在三年前,我曾实现过基于“图算法”的最短路径规划算法,然而在游戏中,我们通常将地图抽象为有单元格构成的矩形,如:           (本图源于这里)      这个微型地图由3*3的单元格构成,当然,实际游戏中的地图通常比它大很多,这里只是给出一个示例。

     在游戏开发中,AI的最基本问题之一就是寻路算法或称路径规划算法,在三年前,我曾实现过基于“图算法”的最短路径规划算法,然而在游戏中,我们通常将地图抽象为有单元格构成的矩形,如:

          (本图源于这里

     这个微型地图由3*3的单元格构成,当然,实际游戏中的地图通常比它大很多,这里只是给出一个示例。

     由于游戏地图通常由单元格构成,所以,基于“图算法”的路径规划便不再那么适用,我们需要采用基于单元格的路径规划算法。A*算法是如今游戏所采用的寻路算法中相当常用的一种算法,它可以保证在任何起点和任何终点之间找到最佳的路径(如果存在的话),而且,A*算法相当有效。

     关于A*算法的原理的详细介绍,可以参考这篇文章。当你明白A*算法的原理之后,在来看接下来的A*算法的实现就会比较容易了。

     现在,让我们转入正题,看看如何在C#中实现A*算法。

     首先,我们把地图划分为单元格,如此,一个地图就可以由(M行*N列)个单元格构成。我们使用AStarNode类来抽象单元格,表示一个节点,而节点的位置用Point表示,其X坐标表示Column Index,Y坐标表示Line Index。AStarNode的定义如下:

///   <summary>
    
///  AStarNode 用于保存规划到当前节点时的各个Cost值以及父节点。
    
///  zhuweisky 2008.10.18
    
///   </summary>
     public   class  AStarNode
    {
        
#region  Ctor
        
public  AStarNode(Point loc, AStarNode previous,  int  _costG,  int  _costH)
        {
            
this .location  =  loc;
            
this .previousNode  =  previous;
            
this .costG  =  _costG;
            
this .costH  =  _costH;
        }
        
#endregion

        
#region  Location
        
private  Point location  =   new  Point( 0 0 );
        
///   <summary>
        
///  Location 节点所在的位置,其X值代表ColumnIndex,Y值代表LineIndex
        
///   </summary>
         public  Point Location
        {
            
get  {  return  location; }
        } 
        
#endregion        

        
#region  PreviousNode
        
private  AStarNode previousNode  =   null ;
        
///   <summary>
        
///  PreviousNode 父节点,即是由哪个节点导航到当前节点的。
        
///   </summary>
         public  AStarNode PreviousNode
        {
            
get  {  return  previousNode; }
        }
        
#endregion

        
#region  CostF
        
///   <summary>
        
///  CostF 从起点导航经过本节点然后再到目的节点的估算总代价。
        
///   </summary>
         public   int  CostF
        {
            
get
            {
                
return   this .costG  +   this .costH;
            }
        }
        
#endregion

        
#region  CostG
        
private   int  costG  =   0 ;
        
///   <summary>
        
///  CostG 从起点导航到本节点的代价。
        
///   </summary>
         public   int  CostG
        {
            
get  {  return  costG; }
        }
        
#endregion

        
#region  CostH
        
private   int  costH  =   0 ;
        
///   <summary>
        
///  CostH 使用启发式方法估算的从本节点到目的节点的代价。
        
///   </summary>
         public   int  CostH
        {
            
get  {  return  costH; }
        }
        
#endregion

        
#region  ResetPreviousNode
        
///   <summary>
        
///  ResetPreviousNode 当从起点到达本节点有更优的路径时,调用该方法采用更优的路径。
        
///   </summary>         
         public   void  ResetPreviousNode(AStarNode previous,  int  _costG)
        {
            
this .previousNode  =  previous;
            
this .costG  =  _costG;         
        }
        
#endregion

        
public   override   string  ToString()
        {
            
return   this .location.ToString();
        }
    }

     如果,你看过上面提到的那篇参考文章,那么类中的各个属性的定义就不难理解了。

     我们假设,从某个节点出发,最多可以有8个方向移动,这8个方向定义为CompassDirections:

    public   enum  CompassDirections
    {
        NotSet 
=   0 ,
        North 
=   1 // UP
        NorthEast  =   2 // UP Right
        East  =   3 ,
        SouthEast 
=   4 ,
        South 
=   5 ,
        SouthWest 
=   6 ,
        West 
=   7 ,
        NorthWest 
=   8
    }

     CompassDirections遵守“左西右东,上北下南”的地图方位原则。

     而从某个节点出发,朝8个方向之中的某个方向移动是有代价(Cost)的,而且朝每个方向移动的代价可能是不相同的,而我们的寻路算法正是要找到起点和终点之间总代价最小的路径。我们使用一个接口ICostGetter来获取从某个节点开始朝8个方向移动的代价值。 

      ///   <summary>
    
///  ICostGetter 获取从当前节点向某个方向移动时的代价。
    
///   </summary>
     public   interface  ICostGetter
    {
        
int  GetCost(Point currentNodeLoaction, CompassDirections moveDirection);
    }

     之所以将其定义为接口,是因为不同的游戏中的对移动代价赋值是不一样的。不同的游戏可以自己实现这个接口,以表明自己的代价赋值策略。

     尽管如此,我们还是给出一个最简单的ICostGetter实现以方便我们测试,这个实现表示从当前节点向上、下、左、右四个方向的移动的代价是一样的。 

 

     ///   <summary>
    
///  SimpleCostGetter ICostGetter接口的简化实现。直线代价为10, 斜线为14。
    
///   </summary>
     public   class  SimpleCostGetter : ICostGetter
    {
        
#region  ICostGetter 成员

        
public   int  GetCost(Point currentNodeLoaction, CompassDirections moveDirection)
        {
            
if  (moveDirection  ==  CompassDirections.NotSet)
            {
                
return   0 ;
            }

            
if  (moveDirection  ==  CompassDirections.East  ||  moveDirection  ==  CompassDirections.West  ||  moveDirection  ==  CompassDirections.South  ||  moveDirection  ==  CompassDirections.North)
            {
                
return   10 ;
            }

            
return   14 ;
        }

        
#endregion
    }

 

     我们知道,如果定义上、下、左、右的代价为1,那么斜线的代价应为根号2,为了提高计算效率,我们将根号2取近似值为1.4,并将单位放大10倍(计算机对整数的运算比对浮点数的运算要快很多)。

     我们还需要一个结构来保存在路径规划过程中的中间结果:

    ///   <summary>
    
///  RoutePlanData 用于封装一次路径规划过程中的规划信息。
    
///   </summary>
     public   class  RoutePlanData
    {
        
#region  CellMap
        
private  Rectangle cellMap;
        
///   <summary>
        
///  CellMap 地图的矩形大小。经过单元格标准处理。
        
///   </summary>
         public  Rectangle CellMap
        {
            
get  {  return  cellMap; }
        } 
        
#endregion

        
#region  ClosedList
        
private  IList < AStarNode >  closedList  =   new  List < AStarNode > ();
        
///   <summary>
        
///  ClosedList 关闭列表,即存放已经遍历处理过的节点。
        
///   </summary>
         public  IList < AStarNode >  ClosedList
        {
            
get  {  return  closedList; }
        } 
        
#endregion

        
#region  OpenedList
        
private  IList < AStarNode >  openedList  =   new  List < AStarNode > ();
        
///   <summary>
        
///  OpenedList 开放列表,即存放已经开发但是还未处理的节点。
        
///   </summary>
         public  IList < AStarNode >  OpenedList
        {
            
get  {  return  openedList; }
        } 
        
#endregion

        
#region  Destination
        
private  Point destination;
        
///   <summary>
        
///  Destination 目的节点的位置。
        
///   </summary>
         public  Point Destination
        {
            
get  {  return  destination; }           
        } 
        
#endregion

        
#region  Ctor
        
public  RoutePlanData(Rectangle map, Point _destination)
        {
            
this .cellMap  =  map;
            
this .destination  =  _destination;
        } 
        
#endregion
    }

     

     有了上述这些基础结构,我们便可以开始实现算法的核心功能了:   

 

  ///   <summary>
    
///  AStarRoutePlanner A*路径规划。每个单元格Cell的位置用Point表示
    
///  F = G + H 。
    
///  G = 从起点A,沿着产生的路径,移动到网格上指定方格的移动耗费。
    
///  H = 从网格上那个方格移动到终点B的预估移动耗费。使用曼哈顿方法,它计算从当前格到目的格之间水平和垂直的方格的数量总和,忽略对角线方向。
    
///  zhuweisky 2008.10.18
    
///   </summary>
     public   class  AStarRoutePlanner
    {
        
private   int  lineCount  =   10 ;    // 反映地图高度,对应Y坐标
         private   int  columnCount  =   10 // 反映地图宽度,对应X坐标
         private  ICostGetter costGetter  =   new  SimpleCostGetter();
        
private   bool [][] obstacles  =   null // 障碍物位置,维度:Column * Line         

        
#region  Ctor
        
public  AStarRoutePlanner() : this ( 10  , 10  , new  SimpleCostGetter())
        {           
        }
        
public  AStarRoutePlanner( int  _lineCount,  int  _columnCount, ICostGetter _costGetter)
        {
            
this .lineCount  =  _lineCount;
            
this .columnCount  =  _columnCount;
            
this .costGetter  =  _costGetter;

            
this .InitializeObstacles();
        }

        
///   <summary>
        
///  InitializeObstacles 将所有位置均标记为无障碍物。
        
///   </summary>
         private   void  InitializeObstacles()
        {
            
this .obstacles  =   new   bool [ this .columnCount][];
            
for  ( int  i  =   0 ; i  <   this .columnCount; i ++ )
            {
                
this .obstacles[i]  =   new   bool [ this .lineCount];
            }

            
for  ( int  i  =   0 ; i  <   this .columnCount; i ++ )
            {
                
for  ( int  j  =   0 ; j  <   this .lineCount; j ++ )
                {
                    
this .obstacles[i][j]  =   false ;
                }
            }
        }
        
#endregion

        
#region  Initialize
        
///   <summary>
        
///  Initialize 在路径规划之前先设置障碍物位置。
        
///   </summary>         
         public   void  Initialize(IList < Point >  obstaclePoints)
        {
            
if  (obstacles  !=   null )
            {
                
foreach  (Point pt  in  obstaclePoints)
                {
                    
this .obstacles[pt.X][pt.Y]  =   true ;
                }
            }
        } 
        
#endregion

        
#region  Plan
        
public  IList < Point >  Plan(Point start, Point destination)
        {
            Rectangle map 
=   new  Rectangle( 0 0 this .columnCount,  this .lineCount);
            
if  (( ! map.Contains(start))  ||  ( ! map.Contains(destination)))
            {
                
throw   new  Exception( " StartPoint or Destination not in the current map! " );
            }

            RoutePlanData routePlanData 
=   new  RoutePlanData(map, destination);

            AStarNode startNode 
=   new  AStarNode(start,  null 0 0 );
            routePlanData.OpenedList.Add(startNode);

            AStarNode currenNode 
=  startNode;

            
// 从起始节点开始进行递归调用
             return  DoPlan(routePlanData, currenNode);
        } 
        
#endregion

        
#region  DoPlan
        
private  IList < Point >  DoPlan(RoutePlanData routePlanData, AStarNode currenNode)
        {
            IList
< CompassDirections >  allCompassDirections  =  CompassDirectionsHelper.GetAllCompassDirections();
            
foreach  (CompassDirections direction  in  allCompassDirections)
            {
                Point nextCell 
=  GeometryHelper.GetAdjacentPoint(currenNode.Location, direction);
                
if  ( ! routePlanData.CellMap.Contains(nextCell))  // 相邻点已经在地图之外
                {
                    
continue ;
                }

                
if  ( this .obstacles[nextCell.X][nextCell.Y])  // 下一个Cell为障碍物
                {
                    
continue ;
                }

                
int  costG  =   this .costGetter.GetCost(currenNode.Location, direction);
                
int  costH  =  Math.Abs(nextCell.X  -  routePlanData.Destination.X)  +  Math.Abs(nextCell.Y  -  routePlanData.Destination.Y);
                
if  (costH  ==   0 // costH为0,表示相邻点就是目的点,规划完成,构造结果路径
                {
                    IList
< Point >  route  =   new  List < Point > ();
                    route.Add(routePlanData.Destination);
                    route.Insert(0, currenNode.Location);
                    AStarNode tempNode  =  currenNode;
                    
while  (tempNode.PreviousNode  !=   null )
                    {
                        route.Insert(
0 , tempNode.PreviousNode.Location);
                        tempNode 
=  tempNode.PreviousNode;
                    }

                    
return  route;
                }

                AStarNode existNode 
=   this .GetNodeOnLocation(nextCell, routePlanData);
                
if  (existNode  !=   null )
                {
                    
if  (existNode.CostG  >  costG)
                    {
                        
// 如果新的路径代价更小,则更新该位置上的节点的原始路径
                        existNode.ResetPreviousNode(currenNode, costG);
                    }
                }
                
else
                {
                    AStarNode newNode 
=   new  AStarNode(nextCell, currenNode, costG, costH);
                    routePlanData.OpenedList.Add(newNode);
                }
            }

            
// 将已遍历过的节点从开放列表转移到关闭列表
            routePlanData.OpenedList.Remove(currenNode);
            routePlanData.ClosedList.Add(currenNode);

            AStarNode minCostNode 
=   this .GetMinCostNode(routePlanData.OpenedList);
            
if  (minCostNode  ==   null // 表明从起点到终点之间没有任何通路。
            {
                
return   null ;
            }

            
// 对开放列表中的下一个代价最小的节点作递归调用
             return   this .DoPlan(routePlanData, minCostNode);
        } 
        
#endregion

        
#region  GetNodeOnLocation
        
///   <summary>
        
///  GetNodeOnLocation 目标位置location是否已存在于开放列表或关闭列表中
        
///   </summary>        
         private  AStarNode GetNodeOnLocation(Point location, RoutePlanData routePlanData)
        {
            
foreach  (AStarNode temp  in  routePlanData.OpenedList)
            {
                
if  (temp.Location  ==  location)
                {
                    
return  temp;
                }
            }

            
foreach  (AStarNode temp  in  routePlanData.ClosedList)
            {
                
if  (temp.Location  ==  location)
                {
                    
return  temp;
                }
            }

            
return   null ;
        } 
        
#endregion

        
#region  GetMinCostNode
        
///   <summary>
        
///  GetMinCostNode 从开放列表中获取代价F最小的节点,以启动下一次递归
        
///   </summary>       
         private  AStarNode GetMinCostNode(IList < AStarNode >  openedList)
        {
            
if  (openedList.Count  ==   0 )
            {
                
return   null ;
            }

            AStarNode target 
=  openedList[ 0 ];
            
foreach  (AStarNode temp  in  openedList)
            {
                
if  (temp.CostF  <  target.CostF)
                {
                    target 
=  temp;
                }
            }

            
return  target;
        } 
        
#endregion        
    }

  

     代码中已经加了详尽的注释,要注意的有以下几点:

1.Initialize方法用于初始化障碍物的位置,所谓“障碍物”,是指导航时无法穿越的物体,比如,游戏中的墙、河流等。

2.标记为红色的ResetPreviousNode方法调用处,说明,到达当前节点的当前路径比已存在的路径代价更小,所以要选择更优的路径。

3.标记为黑体的 route.Insert(0, tempNode.PreviousNode.Location);方法调用,表示已经找到最优路径,此处便可通过反向迭代的方式整理出从起点到终点的最终路径。

4.CostH 的计算使用曼哈顿方法,它计算从当前格到目的格之间水平和垂直的方格的数量总和,忽略对角线方向。

5.在该算法中,至少还有一个地方可以优化,那就是GetMinCostNode方法所实现的内容,如果在路径搜索的过程中,我们就将OpenList中的各个节点按照Cost从小到大进行排序,那么每次GetMinCostNode时,就只需要取出第一个节点即可,而不必在遍历OpenList中的每一个节点了。在地图很大的时候,此法可以大幅提升算法效率。 

     最后,给出一个例子,感受一下:

            AStarRoutePlanner aStarRoutePlanner  =   new  AStarRoutePlanner();
            IList
< Point >  obstaclePoints  =   new  List < Point > ();
            obstaclePoints.Add(
new  Point( 2 4 ));
            obstaclePoints.Add(
new  Point( 3 4 ));
            obstaclePoints.Add(
new  Point( 4 4 ));
            obstaclePoints.Add(
new  Point( 5 4 ));
            obstaclePoints.Add(
new  Point( 6 4 ));
            aStarRoutePlanner.Initialize(obstaclePoints);

            IList
< Point >  route  =  aStarRoutePlanner.Plan( new  Point( 3 3 ),  new  Point( 4 6 ));

 

     运行后,返回的route结果如下:

     {3,3},  {2,3},  {1,3},   {1,4},  {1,5},  {2,5},  {2,6},  {3,6},  {4,6} 

 

2008-10-13:附上CompassDirectionsHelper和GeometryHelper源码。

     public   static   class  CompassDirectionsHelper
    {
        
private   static  IList < CompassDirections >  AllCompassDirections  =   new  List < CompassDirections > ();

        
#region  Static Ctor
        
static  CompassDirectionsHelper()
        {
            CompassDirectionsHelper.AllCompassDirections.Add(CompassDirections.East);
            CompassDirectionsHelper.AllCompassDirections.Add(CompassDirections.West);
            CompassDirectionsHelper.AllCompassDirections.Add(CompassDirections.South);
            CompassDirectionsHelper.AllCompassDirections.Add(CompassDirections.North);

            CompassDirectionsHelper.AllCompassDirections.Add(CompassDirections.SouthEast);
            CompassDirectionsHelper.AllCompassDirections.Add(CompassDirections.SouthWest);
            CompassDirectionsHelper.AllCompassDirections.Add(CompassDirections.NorthEast);
            CompassDirectionsHelper.AllCompassDirections.Add(CompassDirections.NorthWest);
        } 
        
#endregion

        
#region  GetAllCompassDirections
        
public   static  IList < CompassDirections >  GetAllCompassDirections()
        {
            
return  CompassDirectionsHelper.AllCompassDirections;
        }
        
#endregion
    }

 

     public   static   class  GeometryHelper
    {
        
#region  GetAdjacentPoint
        
///   <summary>
        
///  GetAdjacentPoint 获取某个方向上的相邻点
        
///   </summary>        
         public   static  Point GetAdjacentPoint(Point current, CompassDirections direction)
        {
            
switch  (direction)
            {
                
case  CompassDirections.North:
                    {
                        
return   new  Point(current.X, current.Y  -   1 );
                    }
                
case  CompassDirections.South:
                    {
                        
return   new  Point(current.X, current.Y  +   1 );
                    }
                
case  CompassDirections.East:
                    {
                        
return   new  Point(current.X  +   1 , current.Y);
                    }
                
case  CompassDirections.West:
                    {
                        
return   new  Point(current.X  -   1 , current.Y);
                    }
                
case  CompassDirections.NorthEast:
                    {
                        
return   new  Point(current.X  +   1 , current.Y  -   1 );
                    }
                
case  CompassDirections.NorthWest:
                    {
                        
return   new  Point(current.X  -   1 , current.Y  -   1 );
                    }
                
case  CompassDirections.SouthEast:
                    {
                        
return   new  Point(current.X  +   1 , current.Y  +   1 );
                    }
                
case  CompassDirections.SouthWest:
                    {
                        
return   new  Point(current.X  -   1 , current.Y  +   1 );
                    }
                
default :
                    {
                        
return  current;
                    }
            }
        }
        
#endregion      
    }

 

目录
相关文章
|
搜索推荐 算法 C#
【Unity 3D】C#中冒泡排序、选择排序、插入排序等算法的详解(附源码 超详细)
【Unity 3D】C#中冒泡排序、选择排序、插入排序等算法的详解(附源码 超详细)
546 1
|
7月前
|
存储 监控 算法
电脑监控管理中的 C# 哈希表进程资源索引算法
哈希表凭借O(1)查询效率、动态增删性能及低内存开销,适配电脑监控系统对进程资源数据的实时索引需求。通过定制哈希函数与链地址法冲突解决,实现高效进程状态追踪与异常预警。
327 10
|
存储 算法 安全
如何控制上网行为——基于 C# 实现布隆过滤器算法的上网行为管控策略研究与实践解析
在数字化办公生态系统中,企业对员工网络行为的精细化管理已成为保障网络安全、提升组织效能的核心命题。如何在有效防范恶意网站访问、数据泄露风险的同时,避免过度管控对正常业务运作的负面影响,构成了企业网络安全领域的重要研究方向。在此背景下,数据结构与算法作为底层技术支撑,其重要性愈发凸显。本文将以布隆过滤器算法为研究对象,基于 C# 编程语言开展理论分析与工程实践,系统探讨该算法在企业上网行为管理中的应用范式。
326 8
|
机器学习/深度学习 监控 算法
员工上网行为监控软件中基于滑动窗口的C#流量统计算法解析​
在数字化办公环境中,员工上网行为监控软件需要高效处理海量网络请求数据,同时实时识别异常行为(如高频访问非工作网站)。传统的时间序列统计方法因计算复杂度过高,难以满足低延迟需求。本文将介绍一种基于滑动窗口的C#统计算法,通过动态时间窗口管理,实现高效的行为模式分析与流量计数。
365 2
|
11月前
|
监控 算法 数据处理
内网实时监控中的 C# 算法探索:环形缓冲区在实时数据处理中的关键作用
本文探讨了环形缓冲区在内网实时监控中的应用,结合C#实现方案,分析其原理与优势。作为固定长度的循环队列,环形缓冲区通过FIFO机制高效处理高速数据流,具备O(1)时间复杂度的读写操作,降低延迟与内存开销。文章从设计逻辑、代码示例到实际适配效果展开讨论,并展望其与AI结合的潜力,为开发者提供参考。
440 2
|
11月前
|
监控 算法 安全
公司电脑监控软件关键技术探析:C# 环形缓冲区算法的理论与实践
环形缓冲区(Ring Buffer)是企业信息安全管理中电脑监控系统设计的核心数据结构,适用于高并发、高速率与短时有效的多源异构数据处理场景。其通过固定大小的连续内存空间实现闭环存储,具备内存优化、操作高效、数据时效管理和并发支持等优势。文章以C#语言为例,展示了线程安全的环形缓冲区实现,并结合URL访问记录监控应用场景,分析了其在流量削峰、关键数据保护和高性能处理中的适配性。该结构在日志捕获和事件缓冲中表现出色,对提升监控系统效能具有重要价值。
332 1
|
存储 监控 算法
局域网上网记录监控的 C# 基数树算法高效检索方案研究
在企业网络管理与信息安全领域,局域网上网记录监控是维护网络安全、规范网络行为的关键举措。随着企业网络数据量呈指数级增长,如何高效存储和检索上网记录数据成为亟待解决的核心问题。基数树(Trie 树)作为一种独特的数据结构,凭借其在字符串处理方面的卓越性能,为局域网上网记录监控提供了创新的解决方案。本文将深入剖析基数树算法的原理,并通过 C# 语言实现的代码示例,阐述其在局域网上网记录监控场景中的具体应用。
268 7
|
存储 监控 算法
企业内网监控系统中基于哈希表的 C# 算法解析
在企业内网监控系统中,哈希表作为一种高效的数据结构,能够快速处理大量网络连接和用户操作记录,确保网络安全与效率。通过C#代码示例展示了如何使用哈希表存储和管理用户的登录时间、访问IP及操作行为等信息,实现快速的查找、插入和删除操作。哈希表的应用显著提升了系统的实时性和准确性,尽管存在哈希冲突等问题,但通过合理设计哈希函数和冲突解决策略,可以确保系统稳定运行,为企业提供有力的安全保障。
|
算法 安全 机器人
Baumer工业相机堡盟工业相机如何联合BGAPISDK和OpenCVSharp实现图像的伽马变换算法增强(C#)
Baumer工业相机堡盟工业相机如何联合BGAPISDK和OpenCVSharp实现图像的伽马变换算法增强(C#)
426 0
|
算法 安全 机器人
Baumer工业相机堡盟工业相机如何联合BGAPISDK和OpenCVSharp实现图像的拉普拉斯算法增强(C#)
Baumer工业相机堡盟工业相机如何联合BGAPISDK和OpenCVSharp实现图像的拉普拉斯算法增强(C#)
290 0

热门文章

最新文章