using System;
using System.Collections.Generic;
using UnityEngine;
public sealed class JPS
{
///
/// 八个方向(根据实际情况制定方向规则)
///
private static readonly int[ , ] m_Direction = new int[8, 2]{{1, 0}, {1, 1}, {0, 1}, {-1, 1}, {-1, 0}, {-1, -1}, {0, -1}, {1, -1}};
///
/// 启发式枚举与启发式函数之间的映射
///
private static readonly Dictionary> m_DistanceCalculators = new Dictionary>()
{
{ GlobalEnum.DistanceCalculationMethod.Manhattan, (v1, v2) => ManhattanDistance(v1, v2) },
{ GlobalEnum.DistanceCalculationMethod.Euclidean, (v1, v2) => EuclideanDistance(v1, v2, true) },
{ GlobalEnum.DistanceCalculationMethod.Chebyshev, (v1, v2) => ChebyshevDistance(v1, v2) },
};
///
/// 曼哈顿距离
///
/// 坐标1
/// 坐标2
/// 两个坐标之间的曼哈顿距离
private static float ManhattanDistance(Vector2 v1, Vector2 v2)
{
return Mathf.Abs(v1.x - v2.x) + Mathf.Abs(v1.y - v2.y);
}
///
/// 欧几里得距离
///
/// 坐标1
/// 坐标2
/// 欧几里得距离是否需要开根(不开根性能更好, 但会导致算法无法找到最优路径)
/// 两个坐标之间的欧几里得距离或欧几里得距离的平方
private static float EuclideanDistance(Vector2 v1, Vector2 v2, bool isSquareRootRequired = true)
{
Vector2 difference = v1 - v2;
float euclideanDistance = Vector2.Dot(difference, difference);
return isSquareRootRequired ? Mathf.Sqrt(euclideanDistance) : euclideanDistance;
}
///
/// 切比雪夫距离
///
/// 坐标1
/// 坐标2
/// 两个坐标之间的切比雪夫距离
private static float ChebyshevDistance(Vector2 v1, Vector2 v2)
{
return Mathf.Max(Mathf.Abs(v1.x - v2.x), Mathf.Abs(v1.y - v2.y));
}
///
/// 寻路图
///
private JPSNode[ , ] m_Map;
///
/// 地图的宽
///
private int m_MapWidth;
///
/// 地图的高
///
private int m_MapHeight;
///
/// 待评估的节点
///
private readonly PriorityQueue m_OpenList = new PriorityQueue();
///
/// 已评估的节点
///
private readonly HashSet m_CloseList = new HashSet();
///
/// 通过 JPS 获取两点之间的最短路径
///
/// 起点
/// 终点
/// 寻路图
/// 通过 JPS 算出的最短路径, 如果找不到最短路径, 返回 null
public List FindPath2D(Vector2 startPosition, Vector2 endPosition, JPSNode[ , ] map)
{
// 判断起始点和终点是否合法
m_Map = map;
m_MapWidth = map.GetLength(0);
m_MapHeight = map.GetLength(1);
(int startX, int startY) = WorldPositionToJPSPosition(startPosition);
(int endX, int endY) = WorldPositionToJPSPosition(endPosition);
if (!IsWalkable(startX, startY) || !IsWalkable(endX, endY)) return null;
JPSNode startNode = map[startX, startY];
JPSNode endNode = map[endX, endY];
if (startNode == endNode) return null;
startNode.father = null;
startNode.g = 0;
startNode.h = CalculateH(startNode, endNode, GlobalEnum.DistanceCalculationMethod.Euclidean);
startNode.f = startNode.g + startNode.h;
m_OpenList.Enqueue(startNode, startNode);
while (!m_OpenList.IsEmpty)
{
JPSNode currentNode = m_OpenList.Dequeue();
m_CloseList.Add(currentNode);
// 如果找到终点,构建路径并返回
if (currentNode == endNode)
{
List result = new List() { currentNode };
JPSNode pathNode = currentNode;
while (pathNode.father != null)
{
pathNode = pathNode.father;
result.Add(pathNode);
}
result.Reverse();
return result;
}
// 评估邻居
(int currentNodeX, int currentNodeY) = WorldPositionToJPSPosition(currentNode.position);
List<(int dx, int dy)> neighbors = EvaluateNeighbor(currentNodeX, currentNodeY, currentNode);
// 寻找跳跃点
for (int i = 0; i < neighbors.Count; ++i)
{
JPSNode jumpNode = SearchJumpNode(currentNodeX, currentNodeY, neighbors[i].dx, neighbors[i].dy, endNode);
if (jumpNode != null && !m_CloseList.Contains(jumpNode))
{
jumpNode.father = currentNode;
// 计算新的 G 值
float newG = CalculateG(currentNode, jumpNode, GlobalEnum.DistanceCalculationMethod.Euclidean);
// 如果跳跃点已在开放列表中,且新的 G 值更小,则更新节点信息
if (m_OpenList.Contains(jumpNode))
{
if (newG < jumpNode.g)
{
jumpNode.g = newG;
jumpNode.f = jumpNode.g + jumpNode.h;
m_OpenList.UpdatePriority(jumpNode, jumpNode);
}
}
// 新节点:初始化并加入开放列表
else
{
jumpNode.g = newG;
jumpNode.h = CalculateH(jumpNode, endNode, GlobalEnum.DistanceCalculationMethod.Euclidean);
jumpNode.f = jumpNode.g + jumpNode.h;
m_OpenList.Enqueue(jumpNode, jumpNode);
}
}
}
}
// 没有找到路径
return null;
}
///
/// 评估当前节点的邻居方向
///
/// 当前需要评估的节点的 X 坐标
/// 当前需要评估的节点的 Y 坐标
/// 当前需要评估的节点
/// 需要探索的邻居方向偏移量列表(dx, dy)
private List<(int dx, int dy)> EvaluateNeighbor(int nodeX, int nodeY, JPSNode node)
{
List<(int dx, int dy)> neighbors = new List<(int dx, int dy)>();
// 如果是起点(没有父节点),返回所有 8 个可走方向
if (node.father == null)
{
for (int i = 0; i < 8; i++)
{
int dx = m_Direction[i, 0];
int dy = m_Direction[i, 1];
if (IsWalkable(nodeX + dx, nodeY + dy)) neighbors.Add((dx, dy));
}
return neighbors;
}
// 计算移动方向
(int nodeFatherX, int nodeFatherY) = WorldPositionToJPSPosition(node.father.position);
int dxDir = Mathf.Clamp(nodeX - nodeFatherX, -1, 1);
int dyDir = Mathf.Clamp(nodeY - nodeFatherY, -1, 1);
// 辅助函数:防止穿墙判定
bool CanMoveDiagonal(int cx, int cy, int dx, int dy)
{
// 目标必须可走
if (!IsWalkable(cx + dx, cy + dy)) return false;
// 防穿墙:水平或垂直方向至少有一个可走(宽松模式)
// 若要严格模式(不能蹭墙角),改为 &&
return IsWalkable(cx + dx, cy) || IsWalkable(cx, cy + dy);
}
// 1. 对角线移动
if (dxDir != 0 && dyDir != 0)
{
// 自然邻居:前方,水平分量,垂直分量
if (CanMoveDiagonal(nodeX, nodeY, dxDir, dyDir)) neighbors.Add((dxDir, dyDir));
if (IsWalkable(nodeX + dxDir, nodeY)) neighbors.Add((dxDir, 0));
if (IsWalkable(nodeX, nodeY + dyDir)) neighbors.Add((0, dyDir));
// 强迫邻居 (Forced Neighbor)
// 只有当反方向水平阻挡,且该方向的对角线可达时,该对角线是强制邻居
if (!IsWalkable(nodeX - dxDir, nodeY) && CanMoveDiagonal(nodeX, nodeY, -dxDir, dyDir)) neighbors.Add((-dxDir, dyDir));
// 只有当反方向垂直阻挡,且该方向的对角线可达时,该对角线是强制邻居
if (!IsWalkable(nodeX, nodeY - dyDir) && CanMoveDiagonal(nodeX, nodeY, dxDir, -dyDir)) neighbors.Add((dxDir, -dyDir));
}
// 2. 直线移动
else
{
if (dxDir != 0) // 水平移动
{
if (IsWalkable(nodeX + dxDir, nodeY))
{
neighbors.Add((dxDir, 0));
// 检查上方强迫邻居
if (!IsWalkable(nodeX, nodeY + 1) && CanMoveDiagonal(nodeX, nodeY, dxDir, 1)) neighbors.Add((dxDir, 1));
// 检查下方强迫邻居
if (!IsWalkable(nodeX, nodeY - 1) && CanMoveDiagonal(nodeX, nodeY, dxDir, -1)) neighbors.Add((dxDir, -1));
}
}
else // 垂直移动
{
if (IsWalkable(nodeX, nodeY + dyDir))
{
neighbors.Add((0, dyDir));
// 检查右侧强迫邻居
if (!IsWalkable(nodeX + 1, nodeY) && CanMoveDiagonal(nodeX, nodeY, 1, dyDir)) neighbors.Add((1, dyDir));
// 检查左侧强迫邻居
if (!IsWalkable(nodeX - 1, nodeY) && CanMoveDiagonal(nodeX, nodeY, -1, dyDir)) neighbors.Add((-1, dyDir));
}
}
}
return neighbors;
}
///
/// JPS 核心:跳跃函数
///
/// 当前位置 x
/// 当前位置 y
/// 方向 x
/// 方向 y
/// 终点
/// 找到的跳点或 null
private JPSNode SearchJumpNode(int cx, int cy, int dx, int dy, JPSNode endNode)
{
while (true)
{
// --- 1. 移动前置检查(防穿墙逻辑) ---
// 如果是对角线移动,必须确保没有“卡墙角”
if (dx != 0 && dy != 0)
{
// 规则:前往 (cx+dx, cy+dy) 时,(cx+dx, cy) 和 (cx, cy+dy) 不能同时阻挡(宽松)
// 或者:只要有一个阻挡就不能走(严格)
// 这里使用相对宽松的规则:只要有一个方向是通的就可以走,但如果都堵住了就不能走
// 同时也隐含检查了目标点 (cx+dx, cy+dy) 是否可走
// 检查目标点是否可走
if (!IsWalkable(cx + dx, cy + dy)) return null;
// 检查墙角阻挡 (防穿墙)
// 如果右边是墙 且 下边是墙,则不能向右下穿过
if (!IsWalkable(cx + dx, cy) && !IsWalkable(cx, cy + dy)) return null;
// 如果需要更严格的物理碰撞(只要蹭到墙角就不能走),使用下面这行:
// if (!IsWalkable(cx + dx, cy) || !IsWalkable(cx, cy + dy)) return null;
}
else
{
// 直线移动,仅检查目标点
if (!IsWalkable(cx + dx, cy + dy)) return null;
}
// --- 2. 执行移动 ---
cx += dx;
cy += dy;
JPSNode currentNode = m_Map[cx, cy];
// --- 3. 检查是否到达终点 ---
if (currentNode == endNode) return currentNode;
// --- 4. 检查强制邻居 (Forced Neighbor) ---
// 4.1 对角线移动 (dx != 0, dy != 0)
if (dx != 0 && dy != 0)
{
// 检查逻辑:
// 如果反方向水平受阻 (x-dx, y),但该受阻点的“前方” (x-dx, y+dy) 可走 -> 发现强制邻居
bool forcedH = !IsWalkable(cx - dx, cy) && IsWalkable(cx - dx, cy + dy);
// 如果反方向垂直受阻 (x, y-dy),但该受阻点的“前方” (x+dx, y-dy) 可走 -> 发现强制邻居
bool forcedV = !IsWalkable(cx, cy - dy) && IsWalkable(cx + dx, cy - dy);
if (forcedH || forcedV)
{
return currentNode;
}
// --- 递归检查水平和垂直分量 ---
// 只有当当前点本身是合法的位置时,才有资格去检查分量
// (dx, 0) 和 (0, dy) 的检查不会产生无限递归,深度为 1
if (SearchJumpNode(cx, cy, dx, 0, endNode) != null ||
SearchJumpNode(cx, cy, 0, dy, endNode) != null)
{
return currentNode;
}
}
// 4.2 直线移动
else
{
// 水平移动 (dx != 0)
if (dx != 0)
{
// 上方有墙 且 dx 方向的上方可走 || 下方有墙 且 dx 方向的下方可走
if ((!IsWalkable(cx, cy + 1) && IsWalkable(cx + dx, cy + 1)) ||
(!IsWalkable(cx, cy - 1) && IsWalkable(cx + dx, cy - 1)))
{
return currentNode;
}
}
// 垂直移动 (dy != 0)
else
{
// 右侧有墙 且 右侧 dy 方向可走 || 左侧有墙 且 左侧 dy 方向可走
if ((!IsWalkable(cx + 1, cy) && IsWalkable(cx + 1, cy + dy)) ||
(!IsWalkable(cx - 1, cy) && IsWalkable(cx - 1, cy + dy)))
{
return currentNode;
}
}
}
}
}
///
/// 判断坐标点是否合法并且可行走
///
/// x轴
/// y轴
/// 坐标点是否合法并且可行走
private bool IsWalkable(int x, int y)
{
return x >= 0 && x < m_MapWidth && y >= 0 && y < m_MapHeight && m_Map[x, y].nodeStatu != GlobalEnum.NodeStatu.Stop;
}
///
/// 计算当前节点的g值
///
/// 当前节点的父节点
/// 当前节点
/// 节点间的距离计算方式
/// 当前节点的g值
private static float CalculateG(JPSNode father, JPSNode currentNode, GlobalEnum.DistanceCalculationMethod methodName = GlobalEnum.DistanceCalculationMethod.Manhattan)
{
return father.g + m_DistanceCalculators[methodName](father.position, currentNode.position);
}
///
/// 计算当前节点的h值
///
/// 当前节点
/// 终点
/// 节点间的距离计算方式
/// 当前节点的h值
private static float CalculateH(JPSNode currentNode, JPSNode endNode, GlobalEnum.DistanceCalculationMethod methodName = GlobalEnum.DistanceCalculationMethod.Manhattan)
{
return m_DistanceCalculators[methodName](currentNode.position, endNode.position);
}
///
/// 将世界坐标转化为 JPS 算法中寻路图的下标(下标的转换需根据实际情况制定)
///
/// 世界坐标
/// 转换后的 x,y 坐标
private static (int, int) WorldPositionToJPSPosition(Vector2 worldPosition)
{
return ((int)worldPosition.x, (int)worldPosition.y);
}
}