mirror of
http://47.107.252.169:3000/Roguelite-Game-Developing-Team/Gen_Hack-and-Slash-Roguelite.git
synced 2025-11-20 08:27:12 +08:00
Co-authored-by: m0_75251201 <m0_75251201@noreply.gitcode.com> Reviewed-on: http://47.107.252.169:3000/Roguelite-Game-Developing-Team/Gen_Hack-and-Slash-Roguelite/pulls/60
418 lines
18 KiB
C#
418 lines
18 KiB
C#
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实例,用于排序(优先队列)。
|
||
/// 优先比较 FCost,FCost 相等时优先比较 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 方法
|
||
}
|
||
} |