Files

418 lines
18 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using UnityEngine;
namespace Entity
{
public enum PathNodeStatus // 新增枚举
{
Unvisited,
InOpenSet,
InClosedSet
}
/// <summary>
/// 表示寻路算法中的一个节点。
/// </summary>
public class PathNode : IComparable<PathNode> // 实现 IComparable 接口
{
public Vector3Int CellCoordinates; // 格子坐标
public float GCost; // 从起点到当前节点的实际代价
public float HCost; // 从当前节点到终点的估算代价 (启发式)
public PathNode Parent; // 父节点,用于回溯路径
public PathNodeStatus Status; // 新增状态属性
public Vector3 WorldCoordinates; // 世界坐标 (中心点)
/// <summary>
/// 构造一个新的PathNode实例。
/// </summary>
/// <param name="cellCoords">节点的格子坐标。</param>
/// <param name="worldCoords">节点的世界坐标。</param>
public PathNode(Vector3Int cellCoords, Vector3 worldCoords)
{
CellCoordinates = cellCoords;
WorldCoordinates = worldCoords;
GCost = float.MaxValue; // 初始化 GCost 为最大值
HCost = 0;
Parent = null; // 确保 Parent 默认是 null
Status = PathNodeStatus.Unvisited; // 初始化状态
}
public float FCost => GCost + HCost; // 总代价
/// <summary>
/// 比较两个PathNode实例用于排序优先队列
/// 优先比较 FCostFCost 相等时优先比较 HCost避免路径过长
/// </summary>
public int CompareTo(PathNode other)
{
if (other == null) return 1;
var fCostComparison = FCost.CompareTo(other.FCost);
if (fCostComparison != 0) return fCostComparison;
// FCost 相等时HCost 小的优先级更高(更接近目标)
return HCost.CompareTo(other.HCost);
}
/// <summary>
/// 判断两个PathNode实例是否相等基于它们的格子坐标。
/// </summary>
/// <param name="obj">要比较的对象。</param>
/// <returns>如果相等则返回true否则返回false。</returns>
public override bool Equals(object obj)
{
if (obj is PathNode other) return CellCoordinates.Equals(other.CellCoordinates);
return false;
}
/// <summary>
/// 获取PathNode实例的哈希码基于其格子坐标。
/// </summary>
/// <returns>PathNode实例的哈希码。</returns>
public override int GetHashCode()
{
return CellCoordinates.GetHashCode();
}
/// <summary>
/// 返回表示当前节点的字符串。
/// </summary>
/// <returns>节点的字符串表示。</returns>
public override string ToString()
{
return $"Node({CellCoordinates}, F:{FCost:F1}, G:{GCost:F1}, H:{HCost:F1}, Status:{Status})";
}
}
/// <summary>
/// 管理实体的路径生成和移动。
/// </summary>
public class EntityPathManager
{
private const float HEURISTIC_MULTIPLIER = 1.0f; // 启发式乘数
private const float IMPASSABLE_TRAVEL_COST_THRESHOLD = 1.0f; // 不可通行成本阈值
private readonly Entity _entity; // 标记为 readonly
private List<Vector2> _currentPath;
private int _pathIndex;
/// <summary>
/// 构造一个新的EntityPathManager实例。
/// </summary>
/// <param name="entity">要管理路径的实体。</param>
public EntityPathManager(Entity entity)
{
_entity = entity;
_currentPath = new List<Vector2>();
_pathIndex = 0;
}
public bool IsPathComplete =>
_currentPath == null || _currentPath.Count == 0 || _pathIndex >= _currentPath.Count;
/// <summary>
/// 生成从实体当前位置到目标坐标的路径。
/// </summary>
/// <param name="targetCoordinate">目标世界坐标。</param>
public bool GeneratePath(Vector2 targetCoordinate)
{
_currentPath.Clear();
_pathIndex = 0;
if (!_entity)
{
Debug.LogWarning("实体在实体路径管理器中为空,无法生成路径。");
return false;
}
// 假设 Program.Instance.GetDimension 和 currentDimension.landform 存在且有效
// 否则在实际项目中需要更严格的空值检查和错误处理
var currentDimension = Program.Instance.GetDimension(_entity.currentDimensionId);
if (!currentDimension)
{
Debug.LogWarning($"未找到维度: {_entity.currentDimensionId}");
return false;
}
var landform = currentDimension.landform;
// 起点和终点的格子坐标
var startCell = landform.GetCellCoordinates(_entity.Position);
var targetWorldPosition3D = new Vector3(targetCoordinate.x, targetCoordinate.y, _entity.Position.z);
var targetCell = landform.GetCellCoordinates(targetWorldPosition3D);
var entitySize =
new Vector3Int(Mathf.CeilToInt(_entity.Size.x), Mathf.CeilToInt(_entity.Size.y), 1);
// A* 算法
var openSet = new PriorityQueue<PathNode>(); // 使用优先队列
var allNodes = new Dictionary<Vector3Int, PathNode>(); // 用于快速查找已创建的节点
var startNode = new PathNode(startCell, _entity.Position)
{
GCost = 0,
HCost = CalculateHeuristic(_entity.Position, targetCoordinate),
Status = PathNodeStatus.InOpenSet // 设置起始节点状态
};
openSet.Enqueue(startNode);
allNodes.Add(startCell, startNode);
// 检查目标点是否可通行
// 如果目标点在地图边界外landform.GetTravelCostForArea 应该返回一个不可通行值
var targetCellTravelCost = landform.GetTravelCostForArea(targetCell, entitySize);
if (targetCellTravelCost >= IMPASSABLE_TRAVEL_COST_THRESHOLD)
{
_currentPath.Clear();
return false;
}
while (openSet.Count > 0)
{
var currentNode = openSet.Dequeue(); // 从优先队列中取出FCost最小的节点
// 如果这个节点已经以最优路径处理过 (通过另一个更优的路径被Dequeued并Closed)
if (currentNode.Status == PathNodeStatus.InClosedSet) continue; // 跳过旧的或重复的队列项
// 将当前节点标记为已处理
currentNode.Status = PathNodeStatus.InClosedSet;
// 如果找到目标节点
if (currentNode.CellCoordinates == targetCell)
{
_currentPath = ReconstructPath(currentNode);
return true;
}
// 遍历邻居节点 (8个方向包括对角线)
foreach (var neighborCellOffset in new[]
{
new Vector3Int(0, 1, 0), new Vector3Int(0, -1, 0), new Vector3Int(1, 0, 0),
new Vector3Int(-1, 0, 0),
new Vector3Int(1, 1, 0), new Vector3Int(1, -1, 0), new Vector3Int(-1, 1, 0),
new Vector3Int(-1, -1, 0)
})
{
var neighborCell = currentNode.CellCoordinates + neighborCellOffset;
// 获取邻居节点的世界坐标中心点
var neighborWorldPosition = landform.GetWorldCoordinates(neighborCell);
// 获取邻居区域的通行成本,考虑到实体大小
var travelCost = landform.GetTravelCostForArea(neighborCell, entitySize);
// 如果邻居不可通行,则跳过
if (travelCost >= IMPASSABLE_TRAVEL_COST_THRESHOLD) continue;
PathNode neighborNode;
// 尝试从 allNodes 字典中获取邻居节点
if (!allNodes.TryGetValue(neighborCell, out neighborNode))
{
// 如果节点未被发现,则创建它并添加到 allNodes 字典
neighborNode = new PathNode(neighborCell, neighborWorldPosition);
allNodes.Add(neighborCell, neighborNode);
}
// 【逻辑修改1】A* 状态管理一致性改进
// 如果邻居节点已经在closedSet中则意味着我们已经以最优路径处理过它跳过。
// 此处不进行“重新开启已关闭节点”的操作。
if (neighborNode.Status == PathNodeStatus.InClosedSet) continue;
// 从当前节点到邻居节点的实际代价
// Distance 乘以 (1 + travelCost) 作为难度系数
var newGCost = currentNode.GCost +
Vector3.Distance(currentNode.WorldCoordinates, neighborNode.WorldCoordinates) *
(1f + travelCost);
// 如果找到更短的路径,则更新邻居节点信息
if (newGCost < neighborNode.GCost) // 路径更优
{
neighborNode.GCost = newGCost;
neighborNode.HCost = CalculateHeuristic(neighborNode.WorldCoordinates, targetCoordinate);
neighborNode.Parent = currentNode;
// 【逻辑修改1】A* 状态管理一致性改进
// 只有当邻居节点是Unvisited时才将其加入OpenSet。
// 如果节点已经是InOpenSet它的GCost已经被更新优先队列会处理其优先级变化。
// 如果节点是InClosedSet则由于前面的continue语句不会到达此处。
if (neighborNode.Status == PathNodeStatus.Unvisited)
{
neighborNode.Status = PathNodeStatus.InOpenSet;
openSet.Enqueue(neighborNode);
}
}
}
}
_currentPath.Clear();
return false;
}
/// <summary>
/// 获取实体下一帧应该移动到的位置。
/// </summary>
/// <param name="currentPosition">实体当前的世界坐标。</param>
/// <param name="moveSpeed">实体的移动速度(单位:世界单位/秒)。</param>
/// <returns>下一帧实体的位置。</returns>
public Vector3 GetNextPosition(Vector3 currentPosition, float moveSpeed)
{
if (IsPathComplete) return currentPosition; // 没有路径或路径已完成,停留在原地
var targetWaypoint2D = _currentPath[_pathIndex];
var currentPosition2D = new Vector2(currentPosition.x, currentPosition.y);
// 计算到当前目标路点的距离
var distanceToWaypoint = Vector2.Distance(currentPosition2D, targetWaypoint2D);
// 计算实体在当前帧内可以移动的最大距离
var maxMoveDistance = moveSpeed * Time.deltaTime;
if (distanceToWaypoint <= maxMoveDistance)
{
// 实体可以在当前帧内到达或超过当前路点。
// 直接移动到路点,并将路径索引前进到下一个。
var nextPosition = new Vector3(targetWaypoint2D.x, targetWaypoint2D.y, currentPosition.z);
_pathIndex++; // 前进到下一个路点
return nextPosition;
}
// 实体在当前帧内无法到达当前路点。
// 以最大移动距离向当前路点移动。
var direction = (targetWaypoint2D - currentPosition2D).normalized;
var newPosition2D = currentPosition2D + direction * maxMoveDistance;
return new Vector3(newPosition2D.x, newPosition2D.y, currentPosition.z);
}
// 【新增函数】获取实体下一帧的方向
/// <summary>
/// 获取实体应该移动的方向2D
/// </summary>
/// <param name="currentPosition">实体的当前世界坐标。</param>
/// <returns>标准化后的移动方向Vector2如果没有路径或路径完成则返回Vector2.zero。</returns>
public Vector2 GetNextDirection(Vector3 currentPosition)
{
if (IsPathComplete) return Vector2.zero; // 没有路径或路径已完成,没有移动方向
var targetWaypoint2D = _currentPath[_pathIndex];
var currentPosition2D = new Vector2(currentPosition.x, currentPosition.y);
var direction = targetWaypoint2D - currentPosition2D;
// 如果实体当前已非常接近目标路点,预判性地查看下一个路点的方向
// 这样做有助于动画平滑过渡,避免在到达路点瞬间方向变为零
if (direction.sqrMagnitude < 0.001f) // 使用一个小的阈值判断是否“到达”路点
{
var lookAheadIndex = _pathIndex + 1;
if (lookAheadIndex < _currentPath.Count)
{
var nextWaypoint2D = _currentPath[lookAheadIndex];
return (nextWaypoint2D - currentPosition2D).normalized;
}
// 这是最后一个路点,或者没有下一个路点
return Vector2.zero;
}
return direction.normalized;
}
/// <summary>
/// 计算从当前节点到目标节点的启发式代价。
/// </summary>
/// <param name="currentWorldPosition">当前世界坐标。</param>
/// <param name="targetWorldPosition">目标世界坐标。</param>
/// <returns>启发式代价。</returns>
private float CalculateHeuristic(Vector3 currentWorldPosition, Vector2 targetWorldPosition)
{
return Vector2.Distance(new Vector2(currentWorldPosition.x, currentWorldPosition.y), targetWorldPosition) *
HEURISTIC_MULTIPLIER;
}
/// <summary>
/// 从终点回溯并重建路径。
/// </summary>
/// <param name="endNode">路径的终点节点。</param>
/// <returns>世界坐标点的路径列表。</returns>
private List<Vector2> ReconstructPath(PathNode endNode)
{
var path = new List<Vector2>();
var currentNode = endNode;
while (currentNode != null)
{
path.Add(new Vector2(currentNode.WorldCoordinates.x, currentNode.WorldCoordinates.y));
currentNode = currentNode.Parent;
}
path.Reverse(); // 将路径反转,使其从起点到终点
return path;
}
/// <summary>
/// 最小优先队列实现用于A*算法的OpenSet。
/// </summary>
/// <typeparam name="T">队列中存储的元素类型,必须实现 IComparableT。</typeparam>
private class PriorityQueue<T> where T : IComparable<T>
{
private readonly List<T> data; // 存储堆元素的列表
public PriorityQueue()
{
data = new List<T>();
}
public int Count => data.Count; // 队列中的元素数量
/// <summary>
/// 将元素添加到队列中。
/// </summary>
/// <param name="item">要添加的元素。</param>
public void Enqueue(T item)
{
data.Add(item);
var ci = data.Count - 1; // child index
while (ci > 0)
{
var pi = (ci - 1) / 2; // parent index
if (data[ci].CompareTo(data[pi]) >= 0) // 如果子节点不比父节点小,则满足堆属性
break;
(data[ci], data[pi]) = (data[pi], data[ci]);
ci = pi;
}
}
/// <summary>
/// 移除并返回队列中最小的元素。
/// </summary>
/// <returns>最小的元素。</returns>
public T Dequeue()
{
// 假设队列不为空,调用前应检查 Count > 0
var li = data.Count - 1; // last index
var frontItem = data[0]; // 最小元素
data[0] = data[li]; // 将最后一个元素移到根部
data.RemoveAt(li); // 移除最后一个元素
--li; // last index (after removal)
var pi = 0; // parent index
while (true)
{
var ci = pi * 2 + 1; // child index (left child)
if (ci > li) break; // 如果左子节点超出范围,说明没有子节点了
var rc = ci + 1; // right child index
if (rc <= li && data[rc].CompareTo(data[ci]) < 0) // 如果右子节点存在且比左子节点小
ci = rc; // 选择右子节点作为要比较的子节点
if (data[pi].CompareTo(data[ci]) <= 0) break; // 如果父节点不比选中的子节点大,则满足堆属性
(data[pi], data[ci]) = (data[ci], data[pi]);
pi = ci;
}
return frontItem;
}
}
// 移除了 GetSmoothTravelCost 方法
}
}