最近项目里有用到,记录一下,方便以后拿来即用
基本思路:
仍然是两个集合,一个保存待寻路的列表,一个保存已经走过or不考虑的,每次遍历拿出最优路径点.
另外,对于六边形网格,这里使用的是简单的基于Y轴(三维Z轴)偏移的二维坐标,因此,在获取当前网格周围六个相邻格子时,对于偶数列,要做相应偏移。
另外有一些可以优化的点(用二叉搜索树代替待寻列表,布兰森汉姆先检查两点是否直接连通,对路径进行平滑处理,缓存等)
图
下面是思路和测试代码,正式工程禁止
寻路相关HexPathFinder.CS:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class HexPathFinder
{
/// <summary>
/// 该类表示寻路路径上的一个路径点
/// </summary>
internal class PathPoint
{
/// <summary>
/// constructor
/// </summary>
public PathPoint (Hex hex, Hex end,PathPoint prev)
{
_hex = hex;
//get manhattan distance
var h = Mathf.Abs( hex.X - end.X ) + Mathf.Abs( hex.Y - end.Y ) + Mathf.Abs( hex.Z - end.Z );
F = G + h / 2;
_parent = prev;
}
public int F { get; private set; } = -1;
public Hex _hex { get; private set; }
private PathPoint _parent;
public int X => _hex.X;
public int Y => _hex.Y;
/// <summary>
/// 这里可以不用parent,因为每个Parent没有保存其上一级Parent的引用
/// </summary>
public PathPoint Parent => _parent;
public int G => GetG();
public int GetG ()
{
var step = 0;
var temp = Parent;
while (temp != null)
{
temp = temp.Parent;
step++;
}
return step;
}
}
/// <summary>
/// 基于AStar的寻路算法,该函数接收一个起点,一个终点,返回由起点至终点按顺序排列的坐标集合,如果寻路失败则返回空列表
/// </summary>
public List<(int x, int y)> FindPath (Hex start, Hex end )//#todo如果无法到终点是否达到最近路径点?
{
//#优化 Bresenham检查两点是否可以直接连通,可以直接返回
//路径点集合
var pointDic = new Dictionary<int, PathPoint>();//检查引用集合
var openLst = new List<PathPoint>();//待寻列表
var closeDic = new Dictionary<int, Hex>();//走过的
var pathLst = new List<(int, int)>();//结果
var startPoint = new PathPoint( start,end,null );
openLst.Add( startPoint );
while (openLst.Count != 0)
{
var curr = openLst[0];
pathLst.Add( curr._hex.Coordinate );
if (openLst.Contains( curr ))
{
openLst.Remove( curr );
pointDic.Remove( curr._hex.ID );//如果有就删掉,没有也没关系
}
//for test
Entrance.Ins.GetHex( curr.X, curr.Y ).HighLight();
if (curr._hex == end)//终点
break;
else
{
if (!closeDic.ContainsKey( curr._hex.ID ))
closeDic.Add( curr._hex.ID, curr._hex );
}
PathPoint nearPoint = null;
Hex nearHex = null;
//拿周围六格
//这里使用的是简单的偏移二维坐标,具体需要结合实际地图
for (int i = 0; i < 6; i++)
{
var isEven = curr.Y % 2 == 0;
//六边形网格的偏移量
var xOffset = 0;
var yOffset = 0;
switch (i)
{
case 0://右上
xOffset = isEven ? 0 : 1;
yOffset = 1;
break;
case 1://正右
xOffset = 1;
break;
case 2://右下
xOffset = isEven ? 0 : 1;
yOffset = 1;
break;
case 3://左上
xOffset = isEven ? -1 : 0;
yOffset = 1;
break;
case 4://正左
xOffset = -1;
break;
case 5://左下
xOffset = isEven ? -1 : 0;
yOffset = -1;
break;
}
nearHex = Entrance.Ins.GetHex( curr.X + xOffset, curr.Y + yOffset );
if (nearHex is null || !nearHex.IsPassable)//边界或不可通行
continue;
if (closeDic.ContainsKey( nearHex.ID ))//走过了
continue;
if (!pointDic.ContainsKey( nearHex.ID ))
{
nearPoint = new PathPoint( nearHex, end, curr );
pointDic.Add( nearPoint._hex.ID, nearPoint );
}
else
nearPoint = pointDic[nearHex.ID];
if (!openLst.Contains( nearPoint ))
openLst.Add( nearPoint );
}
//#优化 改成BST效率更高
//保证每次都拿到最优路径点
openLst.Sort( (x,y) =>
{
return x.F <= y.F ? -1 : 1;
} );
}
if (openLst.Count == 0)//没有可走的路径
{
pathLst.Clear();
return pathLst;
}
return pathLst;
}
private static HexPathFinder _ins;
private static HexPathFinder GetIns ()
{
if (_ins is null)
_ins = new HexPathFinder();
return _ins;
}
/// <summary>
/// 单例
/// </summary>
public static HexPathFinder Ins => _ins is null ? GetIns() : _ins;
}
启动场景挂载加GameObject挂Entrance脚本以测试:
Entrance.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Entrance : MonoBehaviour
{
public static Entrance Ins = null;
//[SerializeField] private Hex[] _hexArr;
[SerializeField] GameObject _hexRoot;
/// <summary>
/// 六边形网格集合
/// </summary>
private Dictionary<int, Hex> _hexDic;
private HashSet<(int X,int Y)> _hexSet;
private List<List<Hex>> _hexList;
private void Awake ()
{
_hexDic = new Dictionary<int, Hex>();
_hexSet = new HashSet<(int,int)>();
_hexList = new List<List<Hex>>();
Ins = this;
}
private void Start ()
{
//测试代码。。。
var hexArr = _hexRoot.GetComponentsInChildren<Hex>();
//Init
foreach (var hex in hexArr)
{
if (_hexDic.ContainsKey( hex.ID ))
{
Debug.Log($"已经存在ID:{hex.ID}");
continue;
}
if (_hexSet.Contains( hex.Coordinate ))
{
Debug.Log($"已经存在坐标:{hex.Coordinate}");
continue;
}
_hexDic.Add( hex.ID, hex );
_hexSet.Add( hex.Coordinate );
}
Test();
}
private void Test ()
{
//test
var start = GetHex( 0,0);
var end = GetHex( 5,2);
if (start is null || end is null)
{
Debug.Log("<color=red>路径点不存在</color>");
return;
}
var lst = HexPathFinder.Ins.FindPath( start, end );
foreach (var item in lst)
{
GetHex( item.x, item.y ).HighLight();
Debug.Log( $"{item.x},{item.y}" );
}
}
private void Update ()
{
if (Input.GetKeyDown( KeyCode.Delete ))
Test();
}
}
网格类型Hex.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Hex : MonoBehaviour
{
//ID
public int ID => _id < 0 ? GetID() : _id;
/// <summary>
/// 坐标
/// </summary>
public (int X, int Y) Coordinate => (_x, _y);
public int X => _x;
public int Y => _y;
/// <summary>
/// 立方体Z轴坐标
/// </summary>
public int Z => CubeCoordinate.z;//对于六边形网格,在计算两点距离时需要额外的一个轴
/// <summary>
/// 立方体坐标
/// </summary>
public Vector3Int CubeCoordinate => GetCubeCoordinate();
/// <summary>
/// 生成立方体坐标
/// </summary>
private Vector3Int GetCubeCoordinate ()
{
if (_cubeCoordinate == Vector3Int.one)
{
var z = _x + _y;
z *= -1;
_cubeCoordinate = new Vector3Int( _x, _y, z );
}
return _cubeCoordinate;
}
public static bool operator + (Hex thisHex, Hex other) => thisHex.Coordinate == other.Coordinate;
/// <summary>
/// 是否可通行,true代表可以
/// </summary>
public bool IsPassable => _passable;
private int GetID ()
{
_id = IDPool.ID;
return _id;
}
[SerializeField] private int _x;
[SerializeField] private int _y;
[SerializeField] private int _id = int.MinValue;
[SerializeField] private bool _passable = true;
private SpriteRenderer _spRender;
public void HighLight ()
{
_text.color = Color.green;
}
/// <summary>
/// 立方体坐标
/// </summary>
private Vector3Int _cubeCoordinate = Vector3Int.one;
private Text _text;
private void OnEnable ()
{
_text = GetComponentInChildren<Text>();
_text.text = $"{_x},{_y},{Z}";//set text
_text.rectTransform.sizeDelta = new Vector2( 120f, 80f );
_spRender = GetComponentInChildren<SpriteRenderer>();
_text.color = _passable ? Color.white : Color.red;
gameObject.name = $"{_x}_{_y}_{Z}";
}
}
/// <summary>
/// ID池
/// </summary>
public class IDPool
{
private static int _id = int.MaxValue;
public static int ID => _id--;
}