mirror of
http://47.107.252.169:3000/Roguelite-Game-Developing-Team/Gen_Hack-and-Slash-Roguelite.git
synced 2025-11-20 16:47:12 +08:00
442 lines
19 KiB
C#
442 lines
19 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 Vector3 WorldCoordinates; // 世界坐标 (中心点)
|
|||
|
|
public float GCost; // 从起点到当前节点的实际代价
|
|||
|
|
public float HCost; // 从当前节点到终点的估算代价 (启发式)
|
|||
|
|
public float FCost => GCost + HCost; // 总代价
|
|||
|
|
public PathNode Parent; // 父节点,用于回溯路径
|
|||
|
|
public PathNodeStatus Status; // 新增状态属性
|
|||
|
|
|
|||
|
|
/// <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; // 初始化状态
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <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>
|
|||
|
|
/// 比较两个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>
|
|||
|
|
/// 管理实体的路径生成和移动。
|
|||
|
|
/// </summary>
|
|||
|
|
public class EntityPathManager
|
|||
|
|
{
|
|||
|
|
|
|||
|
|
public bool IsPathComplete => _currentPath == null || _currentPath.Count == 0 || _pathIndex >= _currentPath.Count;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 最小优先队列实现,用于A*算法的OpenSet。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <typeparam name="T">队列中存储的元素类型,必须实现 IComparableT。</typeparam>
|
|||
|
|
private class PriorityQueue<T> where T : IComparable<T>
|
|||
|
|
{
|
|||
|
|
private readonly List<T> data; // 存储堆元素的列表
|
|||
|
|
|
|||
|
|
public int Count => data.Count; // 队列中的元素数量
|
|||
|
|
|
|||
|
|
public PriorityQueue()
|
|||
|
|
{
|
|||
|
|
data = new List<T>();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <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;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
private readonly Entity _entity; // 标记为 readonly
|
|||
|
|
private List<Vector2> _currentPath;
|
|||
|
|
private int _pathIndex;
|
|||
|
|
|
|||
|
|
private const float HEURISTIC_MULTIPLIER = 1.0f; // 启发式乘数
|
|||
|
|
private const float IMPASSABLE_TRAVEL_COST_THRESHOLD = 1.0f; // 不可通行成本阈值
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 构造一个新的EntityPathManager实例。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="entity">要管理路径的实体。</param>
|
|||
|
|
public EntityPathManager(Entity entity)
|
|||
|
|
{
|
|||
|
|
_entity = entity;
|
|||
|
|
_currentPath = new List<Vector2>();
|
|||
|
|
_pathIndex = 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 生成从实体当前位置到目标坐标的路径。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="targetCoordinate">目标世界坐标。</param>
|
|||
|
|
public void GeneratePath(Vector2 targetCoordinate)
|
|||
|
|
{
|
|||
|
|
_currentPath.Clear();
|
|||
|
|
_pathIndex = 0;
|
|||
|
|
|
|||
|
|
if (_entity == null)
|
|||
|
|
{
|
|||
|
|
Debug.LogError("实体在实体路径管理器中为空,无法生成路径。");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 假设 Program.Instance.GetDimension 和 currentDimension.landform 存在且有效
|
|||
|
|
// 否则在实际项目中需要更严格的空值检查和错误处理
|
|||
|
|
var currentDimension = Program.Instance.GetDimension(_entity.currentDimensionId);
|
|||
|
|
if (currentDimension == null)
|
|||
|
|
{
|
|||
|
|
Debug.LogError($"未找到维度: {_entity.currentDimensionId}");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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)
|
|||
|
|
{
|
|||
|
|
Debug.LogWarning($"目标位置 {targetCoordinate} 不可通行或超出地图边界,无法生成路径。");
|
|||
|
|
_currentPath.Clear();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 遍历邻居节点 (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);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果OpenSet为空且没有找到路径
|
|||
|
|
Debug.LogWarning($"未找到实体 {_entity.name} 到目标 {targetCoordinate} 的路径。可能目标不可达,或被完全包围。");
|
|||
|
|
_currentPath.Clear();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <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)
|
|||
|
|
{
|
|||
|
|
// 实体可以在当前帧内到达或超过当前路点。
|
|||
|
|
// 直接移动到路点,并将路径索引前进到下一个。
|
|||
|
|
Vector3 nextPosition = new Vector3(targetWaypoint2D.x, targetWaypoint2D.y, currentPosition.z);
|
|||
|
|
_pathIndex++; // 前进到下一个路点
|
|||
|
|
return nextPosition;
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
// 实体在当前帧内无法到达当前路点。
|
|||
|
|
// 以最大移动距离向当前路点移动。
|
|||
|
|
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) // 使用一个小的阈值判断是否“到达”路点
|
|||
|
|
{
|
|||
|
|
int lookAheadIndex = _pathIndex + 1;
|
|||
|
|
if (lookAheadIndex < _currentPath.Count)
|
|||
|
|
{
|
|||
|
|
var nextWaypoint2D = _currentPath[lookAheadIndex];
|
|||
|
|
return (nextWaypoint2D - currentPosition2D).normalized;
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
// 这是最后一个路点,或者没有下一个路点
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 移除了 GetSmoothTravelCost 方法
|
|||
|
|
}
|
|||
|
|
}
|