提交代码

This commit is contained in:
PC-20230316NUNE\Administrator 2024-02-07 16:48:27 +08:00
parent 0a12c49bf1
commit 97b3671979
23 changed files with 3595 additions and 26 deletions

View File

@ -11,5 +11,45 @@
// 这通常用于防止某些字段被序列化,因为它们可能包含不需要或不能序列化的数据。 // 这通常用于防止某些字段被序列化,因为它们可能包含不需要或不能序列化的数据。
public NavGraph[] graphs = new NavGraph[0]; // 声明一个 NavGraph 类型的数组,初始化为长度为 0 的数组。 public NavGraph[] graphs = new NavGraph[0]; // 声明一个 NavGraph 类型的数组,初始化为长度为 0 的数组。
//数据初始化
public void Init()
{
graphs = new NavGraph[0];
DeserializeGraphs();
}
/// <summary>从<see cref="data"/>反序列化图形</summary>
/// <remarks>
/// 此方法从给定的数据源(可能是文件或内存中的对象)反序列化图形数据。
/// 如果<see cref="data"/>不为null它将调用另一个重载的DeserializeGraphs方法
/// 该方法可能包含具体的反序列化逻辑。
/// </remarks>
public void DeserializeGraphs () {
if (data != null) {
DeserializeGraphs(data);
}
}
/// <summary>销毁所有图形并将图形设置为null</summary>
/// <remarks>
/// 此方法负责销毁当前所有的图形对象并将图形数组重置为null。
/// 在销毁每个图形之前,它会调用图形的<see cref="IGraphInternals.OnDestroy"/>方法,
/// 该方法可能包含图形销毁前的清理逻辑。
/// 一旦所有图形都被销毁,它将重置<see cref="graphs"/>数组为一个新的空数组,
/// 并调用<see cref="UpdateShortcuts"/>方法来更新任何相关的快捷方式或缓存。
/// </remarks>
void ClearGraphs () {
if (graphs == null) return;
for (int i = 0; i < graphs.Length; i++) {
if (graphs[i] != null) {
((IGraphInternals)graphs[i]).OnDestroy();
graphs[i].active = null;
}
}
graphs = new NavGraph[0];
UpdateShortcuts();
}
} }
} }

View File

@ -3,8 +3,6 @@
/// <summary> /// <summary>
/// 为图形暴露内部方法。 /// 为图形暴露内部方法。
/// 这用于隐藏任何用户代码都不应使用但还必须为'public'或'internal'的方法(由于此库附带源代码,因此'internal'几乎与'public'相同)。
/// 隐藏内部方法可以清理文档和IntelliSense建议。
/// </summary> /// </summary>
public interface IGraphInternals { public interface IGraphInternals {
// // 获取或设置序列化后的编辑器设置。 // // 获取或设置序列化后的编辑器设置。

View File

@ -0,0 +1,74 @@
using Game.Plugins.JNGame.Sync.Frame.AStar.Util;
using UnityEngine;
namespace Plugins.JNGame.Sync.Frame.AStar.Entity
{
public abstract class GraphNode
{
/// <summary>
/// 内部唯一索引。同时存储一些位打包的值,如<see cref="TemporaryFlag1"/>和<see cref="TemporaryFlag2"/>。
/// </summary>
private int nodeIndex; // 定义一个私有整数变量nodeIndex用于存储节点的内部唯一索引并可能包含一些位打包的值如TemporaryFlag1和TemporaryFlag2。
/// <summary>
/// 位打包字段,包含多个数据片段。
/// 参见Walkable
/// 参见Area
/// 参见GraphIndex
/// 参见Tag
/// </summary>
protected uint flags; // 定义一个受保护的无符号整数变量flags用于存储位打包的多个字段。这些字段可能表示节点的不同属性如可走性(Walkable)、区域(Area)、图索引(GraphIndex)和标签(Tag)等。
/// <summary>
/// 节点在世界空间中的位置。
/// 注意:该位置以 Int3 类型存储,而不是 Vector3 类型。
/// 你可以使用显式转换将 Int3 转换为 Vector3。
/// 示例代码var v3 = (Vector3)node.position;
/// </summary>
public Int3 position;
// 如果任何人创建了超过大约2亿个节点那么事情就不会那么顺利了。然而到了那个时候人们肯定会遇到更紧迫的问题比如内存耗尽
// 定义了一个常量NodeIndexMask它是一个整数掩码用于从nodeIndex中提取索引值
// NodeIndexMask的值为0xFFFFFFF即31个连续的1这意味着索引值可以是从0到16,777,2152^24 - 1的任何整数
const int NodeIndexMask = 0xFFFFFFF;
// 定义了一个常量DestroyedNodeIndex表示被销毁的节点的索引值
// 它等于NodeIndexMask减去1因此其值为16,777,214
// 这意味着当一个节点的索引被设置为DestroyedNodeIndex时它可以被视为一个不再使用的、已被销毁的节点
const int DestroyedNodeIndex = NodeIndexMask - 1;
// 定义了一个常量TemporaryFlag1Mask它是一个位掩码用于表示TemporaryFlag1的状态
// 该掩码的最高位被设置为1即第28位而所有其他位都为0
// 通过将nodeIndex与TemporaryFlag1Mask进行按位与操作可以检查TemporaryFlag1是否被设置
// 如果结果是非零值那么TemporaryFlag1就被设置了否则它未被设置
const int TemporaryFlag1Mask = 0x10000000;
// 定义了一个常量TemporaryFlag2Mask它也是一个位掩码用于表示TemporaryFlag2的状态
// 该掩码的第二高位被设置为1即第29位而所有其他位都为0
// 通过将nodeIndex与TemporaryFlag2Mask进行按位与操作可以检查TemporaryFlag2是否被设置
// 如果结果是非零值那么TemporaryFlag2就被设置了否则它未被设置
const int TemporaryFlag2Mask = 0x20000000;
/// <summary>
/// 内部唯一索引。
/// 每个节点都会获得一个唯一索引。
/// 这个索引不一定与例如节点在图中的位置相关联。
/// </summary>
public int NodeIndex
{
get { return nodeIndex & NodeIndexMask; }
private set { nodeIndex = (nodeIndex & ~NodeIndexMask) | value; }
}
/// <summary>
/// 这个节点表面上距离点p最近的点
/// </summary>
/// <param name="p">给定的点</param>
/// <returns>返回节点表面上距离点p最近的点的坐标</returns>
public abstract Vector3 ClosestPointOnNode(Vector3 p);
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bfd38219aa2347bbb5fd2be73ac1d8d5
timeCreated: 1707288060

View File

@ -0,0 +1,165 @@
using Game.Plugins.JNGame.Sync.Frame.AStar.Util;
namespace Plugins.JNGame.Sync.Frame.AStar.Entity
{
/// <summary>
/// 存储单个路径查找请求的临时节点数据。
/// 每个节点在每个使用的线程中都有一个PathNode实例。
/// 它存储例如G分数、H分数和其他用于路径计算的临时变量
/// 这些变量不是图形结构的一部分。
///
/// 该类用于存储单个路径查找请求中的临时节点数据。这个类似乎与路径查找算法(如 A* 搜索算法)相关,用于在图形或网格中查找从起点到终点的最佳路径
///
/// 参见Pathfinding.PathHandler
/// 参见https://en.wikipedia.org/wiki/A*_search_algorithm
/// </summary>
public class PathNode
{
/// <summary>搜索树中的父节点</summary>
public PathNode parent;
/// <summary>上次使用此节点的路径请求(如果使用了多线程,则指当前线程中的请求)</summary>
public ushort pathID;
/// <summary>用于存储G分数的后备字段</summary>
private uint g; // 定义一个私有字段g类型为无符号整数uint用于存储G分数
/// <summary>用于存储H分数的后备字段</summary>
private uint h; // 定义一个私有字段h类型为无符号整数uint用于存储H分数
/// <summary>G分数表示到达此节点的成本</summary>
public uint G { get { return g; } set { g = value; } }
/// <summary>H分数表示估计到达目标的成本</summary>
public uint H { get { return h; } set { h = value; } }
/// <summary>F分数等于H分数与G分数的和</summary>
public uint F { get { return g + h; } }
/// <summary>
/// 位打包变量,用于存储多个字段
/// </summary>
private uint flags; // 定义一个无符号整数变量flags用于存储多个标志位和成本信息
/// <summary>
/// 成本使用前28位
/// </summary>
private const uint CostMask = (1U << 28) - 1U; // 定义一个常量CostMask用于提取flags变量中的成本信息它占据前28位
/// <summary>
/// 标志1位于第28位
/// </summary>
private const int Flag1Offset = 28; // 定义一个常量Flag1Offset表示标志1在flags变量中的位偏移量
private const uint Flag1Mask = (uint)(1 << Flag1Offset); // 定义一个常量Flag1Mask用于标记和提取flags变量中的标志1通过位运算将第28位设置为1其余位为0
/// <summary>
/// 标志2位于第29位
/// </summary>
private const int Flag2Offset = 29; // 定义一个常量Flag2Offset表示标志2在flags变量中的位偏移量
private const uint Flag2Mask = (uint)(1 << Flag2Offset); // 定义一个常量Flag2Mask用于标记和提取flags变量中的标志2通过位运算将第29位设置为1其余位为0
public uint cost {
get {
return flags & CostMask;
}
set {
flags = (flags & ~CostMask) | value;
}
}
/// <summary>
/// 在路径查找过程中用作临时标志。
/// 仅路径查找器Pathfinders在路径查找过程中可以使用此标志来标记节点。
/// 当完成路径查找后应将此标志恢复为其默认状态false以避免干扰其他路径查找请求。
/// </summary>
public bool flag1 {
get {
// 获取flag1的值。使用位运算检查flags变量中的Flag1Mask位是否被设置。
return (flags & Flag1Mask) != 0;
}
set {
// 设置flag1的值。使用位运算来设置或清除Flags变量中的Flag1Mask位。
flags = (flags & ~Flag1Mask) | (value ? Flag1Mask : 0U);
}
}
/// <summary>
/// 在路径查找过程中用作临时标志。
/// 仅路径查找器Pathfinders在路径查找过程中可以使用此标志来标记节点。
/// 当完成路径查找后应将此标志恢复为其默认状态false以避免干扰其他路径查找请求。
/// </summary>
public bool flag2 {
get {
// 获取flag2的值。使用位运算检查Flags变量中的Flag2Mask位是否被设置。
return (flags & Flag2Mask) != 0;
}
set {
// 设置flag2的值。使用位运算来设置或清除Flags变量中的Flag2Mask位。
flags = (flags & ~Flag2Mask) | (value ? Flag2Mask : 0U);
}
}
/// <summary>
/// 对实际图节点的引用
/// </summary>
public GraphNode node; // 这是一个对图节点的引用,用于指向实际存储节点信息的对象。通过这个属性,可以访问和操作这个节点。
}
public class PathHandler
{
private ushort pathID;
/// <summary>正在计算或最近一次计算过的路径的ID</summary>
public ushort PathID { get { return pathID; } }
/// <summary>
/// 用于追踪“开放列表”上的节点的二叉堆。
/// 参见https://en.wikipedia.org/wiki/A*_search_algorithm
/// </summary>
public readonly BinaryHeap heap = new BinaryHeap(128);
/// <summary>所有PathNode的数组</summary>
public PathNode[] nodes = new PathNode[0];
/// <summary>
/// 将所有节点的pathID设置为0。
/// 参见Pathfinding.PathNode.pathID
/// </summary>
public void ClearPathIDs () {
// 遍历nodes数组中的每个元素
for (int i = 0; i < nodes.Length; i++) {
// 如果当前节点不为null
if (nodes[i] != null)
// 将该节点的pathID设置为0
nodes[i].pathID = 0;
}
}
/// <summary>
/// 初始路线数据
/// </summary>
/// <param name="p"></param>
public void InitializeForPath (JNPath p) {
pathID = p.pathID;
heap.Clear();
}
/// <summary>
/// 根据指定的节点返回对应的PathNode。
/// 由于在多线程启用的情况下可能会同时使用多个PathHandler
/// 因此这个PathNode是特定于这个PathHandler的。
/// </summary>
/// <param name="node">要获取其PathNode的GraphNode。</param>
/// <returns>与指定节点对应的PathNode。</returns>
public PathNode GetPathNode(GraphNode node) {
// 使用节点索引从nodes数组中检索对应的PathNode
return nodes[node.NodeIndex];
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e753730811d84eb1b7793fd8d567baa9
timeCreated: 1707275292

View File

@ -14,6 +14,12 @@ namespace Plugins.JNGame.Sync.Frame.AStar.Entity
private JNAStarPath path; private JNAStarPath path;
public void Enqueue (JNPath path) {
lock (pathReturnQueue) {
pathReturnQueue.Enqueue(path);
}
}
public PathReturnQueue (JNAStarPath path) { public PathReturnQueue (JNAStarPath path) {
this.path = path; this.path = path;

View File

@ -1,4 +1,5 @@
using System; using System;
using Game.Plugins.JNGame.Sync.Frame.AStar.Navmesh;
using Plugins.JNGame.Sync.Frame.AStar.Entity; using Plugins.JNGame.Sync.Frame.AStar.Entity;
using Plugins.JNGame.Sync.Frame.AStar.Processor; using Plugins.JNGame.Sync.Frame.AStar.Processor;
using UnityEngine; using UnityEngine;
@ -19,20 +20,191 @@ namespace Plugins.JNGame.Sync.Frame.AStar
/// </summary> /// </summary>
public AStarData data = new AStarData(); public AStarData data = new AStarData();
/// <summary>Shortcut to Pathfinding.AstarData.graphs</summary> /// 网格数据
public NavGraph[] graphs => data.graphs; public NavGraph[] graphs => data.graphs;
/// <summary>保存所有已完成的路径,等待返回至请求它们的地方</summary> /// <summary>保存所有已完成的路径,等待返回至请求它们的地方</summary>
internal readonly PathReturnQueue pathReturnQueue; internal readonly PathReturnQueue pathReturnQueue;
/// <summary>
/// 处理导航网格的切割Navmesh cuts
/// 参见:<see cref="Pathfinding.NavmeshCut"/>
/// </summary>
public NavmeshUpdates navmeshUpdates;
// 图形更新处理器
GraphUpdateProcessor graphUpdates;
/// <summary>
/// 在搜索每个路径之前调用。当使用多线程时要小心,因为这将从另一个线程调用。
/// </summary>
public OnPathDelegate OnPathPreSearch;
/// <summary>
/// 在搜索每个路径之后调用。当使用多线程时要小心,因为这将从不同的线程调用。
/// </summary>
public static OnPathDelegate OnPathPostSearch;
public JNAStarPath() public JNAStarPath()
{ {
pathReturnQueue = new PathReturnQueue(this);; pathReturnQueue = new PathReturnQueue(this);;
navmeshUpdates = new NavmeshUpdates(this);
graphUpdates = new GraphUpdateProcessor(this);
}
/// <summary>
/// 检查是否有任何工作项需要执行,
/// 如果不使用多线程(因为计算会在其他线程中进行),
/// 则运行一段时间的路径查找,
/// 然后将计算出的路径返回给请求它们的脚本。
///
/// 参见PerformBlockingActions
/// 参见PathProcessor.TickNonMultithreaded
/// 参见PathReturnQueue.ReturnPaths
/// </summary>
private void Update () {
// 更新导航网格
// navmeshUpdates.Update();
// // 如果不使用多线程,则计算路径
// pathProcessor.TickNonMultithreaded();
// // 返回计算出的路径
// pathReturnQueue.ReturnPaths(true);
//初始化
Init();
} }
/// <summary>
/// 设置所有必要的变量并扫描图形。
/// 调用Initialize启动ReturnPaths协程并扫描所有图形。
/// 如果使用多线程,则启动线程
/// 参见:<see cref="OnAwakeSettings"/>
/// </summary>
protected void Init () {
// // 禁用GUILayout以提高性能因为它在OnGUI调用中未使用
// useGUILayout = false;
// // 确保在扫描之前已经启用了所有图形修饰符(以避免脚本执行顺序问题)
// GraphModifier.FindAllModifiers();
// RelevantGraphSurface.FindAllGraphSurfaces();
// 初始化路径处理器
InitializePathProcessor();
// // 配置内部引用
// ConfigureReferencesInternal();
// 初始化A*数据
InitializeAstarData();
// 刷新工作项这些工作项可能在InitializeAstarData中用于加载图形数据
FlushWorkItems();
// 标记euclideanEmbedding为需要更新
euclideanEmbedding.dirty = true;
// 启用导航网格更新
navmeshUpdates.OnEnable();
// 如果在启动时扫描且缓存未启用或缓存文件为空,则执行扫描
if (scanOnStartup && (!data.cacheStartup || data.file_cachedStartup == null)) {
Scan();
}
}
/// <summary>
/// 初始化AstarData类。
/// 搜索图类型,对<see cref="data"/>和所有图调用Awake方法。
///
/// 请参阅AstarData.FindGraphTypes
/// </summary>
void InitializeAstarData () {
// // 调用AstarData类的FindGraphTypes方法该方法负责搜索并识别可用的图类型。
// data.FindGraphTypes();
// 调用AstarData类的Awake方法该方法可能用于唤醒或初始化类的内部状态或资源。
data.Awake();
// 调用AstarData类的UpdateShortcuts方法该方法可能用于更新或创建图的快捷方式或优化路径。
data.UpdateShortcuts();
}
/// <summary>
/// 初始化 <see cref="pathProcessor"/> 字段
/// </summary>
void InitializePathProcessor() {
// // 确保线程数至少为1
// int numProcessors = Mathf.Max(numThreads, 1);
// // 判断是否使用多线程
// bool multithreaded = numThreads > 0;
// 创建 PathProcessor 实例,并传入相关的参数
pathProcessor = new PathProcessor(this, pathReturnQueue);
// 为 PathProcessor 的 OnPathPreSearch 事件添加事件处理器
pathProcessor.OnPathPreSearch += path => {
var tmp = OnPathPreSearch;
if (tmp != null) tmp(path);
};
// 为 PathProcessor 的 OnPathPostSearch 事件添加事件处理器
pathProcessor.OnPathPostSearch += path => {
LogPathResults(path);
var tmp = OnPathPostSearch;
if (tmp != null) tmp(path);
};
// // 为 PathProcessor 的 OnQueueUnblocked 事件添加事件处理器
// // 当路径队列被解锁时,此事件将被触发
// pathProcessor.OnQueueUnblocked += () => {
// if (euclideanEmbedding.dirty) {
// euclideanEmbedding.RecalculateCosts();
// }
// };
//
// // 如果使用多线程
// if (multithreaded) {
// // 启用图形更新的多线程支持
// graphUpdates.EnableMultithreading();
// }
}
/// <summary>
/// 将路径结果打印到日志中。打印的内容可以通过 <see cref="logPathResults"/> 进行控制。
/// 参见: <see cref="logPathResults"/>
/// 参见: PathLog
/// 参见: Pathfinding.Path.DebugString
/// </summary>
private void LogPathResults(JNPath path) {
// // 如果 logPathResults 不为 PathLog.None并且路径存在错误或者 logPathResults 不只为错误打印
// if (logPathResults != PathLog.None && (path.error || logPathResults != PathLog.OnlyErrors)) {
// // 获取路径的调试字符串,具体取决于 logPathResults 的值
// string debug = (path as IPathInternals).DebugString(logPathResults);
//
// // 如果 logPathResults 为 PathLog.InGame
// if (logPathResults == PathLog.InGame) {
// // 将调试字符串赋值给 inGameDebugPath
// inGameDebugPath = debug;
// // 如果路径存在错误
// } else if (path.error) {
// // 打印警告级别的调试信息
// Debug.LogWarning(debug);
// // 如果路径没有错误
// } else {
// // 打印普通的调试信息
// Debug.Log(debug);
// }
// }
if (path.error)
Debug.Log(path.errorLog);
}
/// <summary> /// <summary>
/// 将路径添加到队列中,以便尽快进行计算。 /// 将路径添加到队列中,以便尽快进行计算。

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 08e1307c23ba4d7abe40bb1c0f231817
timeCreated: 1707272932

View File

@ -0,0 +1,18 @@
using Plugins.JNGame.Sync.Frame.AStar;
namespace Game.Plugins.JNGame.Sync.Frame.AStar.Navmesh
{
public class GraphUpdateProcessor
{
private JNAStarPath AStar;
public event System.Action OnGraphsUpdated;
public GraphUpdateProcessor(JNAStarPath aStar)
{
AStar = aStar;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0803d041431149139fa784e73b6ab5b0
timeCreated: 1707272942

View File

@ -0,0 +1,18 @@
using Plugins.JNGame.Sync.Frame.AStar;
namespace Game.Plugins.JNGame.Sync.Frame.AStar.Navmesh
{
public class NavmeshUpdates
{
private JNAStarPath AStar;
public NavmeshUpdates(JNAStarPath aStar)
{
AStar = aStar;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d7461b466c81472fb94dc5b9bee79b00
timeCreated: 1707271423

View File

@ -1,4 +1,6 @@
using Game.Plugins.JNGame.Sync.Frame.AStar.Util; using System;
using Game.Plugins.JNGame.Sync.Frame.AStar.Util;
using Plugins.JNGame.Sync.Frame.AStar.Entity;
using UnityEngine; using UnityEngine;
namespace Plugins.JNGame.Sync.Frame.AStar namespace Plugins.JNGame.Sync.Frame.AStar
@ -15,6 +17,16 @@ namespace Plugins.JNGame.Sync.Frame.AStar
public class JNABPath : JNPath public class JNABPath : JNPath
{ {
/// <summary>
/// 路径的开始节点
/// </summary>
public GraphNode startNode;
/// <summary>
/// 路径的结束节点
/// </summary>
public GraphNode endNode;
/// <summary> /// <summary>
/// 与路径请求中完全相同的起点 /// 与路径请求中完全相同的起点
/// </summary> /// </summary>
@ -46,6 +58,44 @@ namespace Plugins.JNGame.Sync.Frame.AStar
/// </summary> /// </summary>
public ITraversalProvider traversalProvider; public ITraversalProvider traversalProvider;
/// <summary>
/// 如果目标节点无法到达,则计算部分路径。
/// 如果目标节点无法到达,将选择最接近(由启发式算法确定)的节点作为目标节点,
/// 并返回部分路径。
/// 这只在使用启发式算法时有效(这是默认设置)。
/// 如果找到了部分路径则将CompleteState设置为Partial。
/// 注意:其他路径类型不需要遵守此设置。
///
/// <see cref="endNode"/> 和 <see cref="endPoint"/> 将被修改并设置为最终最接近目标的节点。
///
/// 警告:如果您有一个大型图表,使用此功能可能会使路径计算明显变慢。原因是,
/// 当目标节点无法到达时,路径必须搜索它可以到达的每一个其他节点,
/// 以确定哪一个最接近。这可能是昂贵的,这就是为什么此选项默认被禁用。
/// </summary>
public bool calculatePartial;
/// <summary>
/// 部分路径的当前最佳目标。
/// 这是具有最低H分数的节点。
/// H分数通常是根据启发式算法计算得出的用于估计从当前节点到目标节点的代价。
/// 在计算部分路径时算法会不断寻找具有最低H分数的节点作为下一个目标
/// 以确保所选的路径是当前已知的最佳路径。
/// 当目标节点不可达时,此属性将存储最接近目标节点的节点,并用于构建部分路径。
/// </summary>
protected PathNode partialBestTarget;
/// <summary>
/// 用于路径查找算法此路径的总成本。
///
/// 成本受路径长度以及节点上的任何标签或惩罚的影响。
/// 默认情况下移动1个世界单位的成本是<see cref="Int3.Precision"/>。
///
/// 如果路径查找失败,成本将被设置为零。
///
/// 请参阅:标签(请在线文档中查看相关链接)
/// </summary>
public uint cost; // 这是一个无符号整型变量,用于存储路径查找算法中此路径的总成本
/// <summary> /// <summary>
/// 使用起始点和终点构造一个路径。 /// 使用起始点和终点构造一个路径。
/// 当路径计算完成后,将调用指定的委托。 /// 当路径计算完成后,将调用指定的委托。
@ -90,5 +140,158 @@ namespace Plugins.JNGame.Sync.Frame.AStar
startIntPoint = (Int3)start; startIntPoint = (Int3)start;
} }
/// <summary>
/// 完成部分路径计算,并将结果存储在路径处理器中。
/// </summary>
/// <param name="node">部分路径的最后一个节点。</param>
void CompletePartial(PathNode node) {
// 我们将改变结束节点,因此需要清理之前的结束节点,以避免留下过时数据。
var pathEndNode = pathHandler.GetPathNode(endNode);
pathEndNode.flag1 = false; // 清除结束节点的flag1标志
pathEndNode.flag2 = false; // 清除结束节点的flag2标志
// 设置完成状态为部分完成
CompleteState = PathCompleteState.Partial;
// 更新结束节点为传入的节点
endNode = node.node;
// 计算结束点,即传入节点上距离原始结束点最近的点
endPoint = endNode.ClosestPointOnNode(originalEndPoint);
// 更新路径成本为结束节点的G值从开始节点到结束节点的总成本
cost = pathEndNode.G;
// 追踪路径,可能是为了调试或可视化目的
Trace(node);
}
/// <summary>
/// 从终点节点回溯到起点节点,追踪计算出的路径。
/// 这将构建一个包含此路径经过的节点的数组(<see cref="path"/>),并将<see cref="vectorPath"/>数组设置为<see cref="path"/>数组的位置。
/// 假设<see cref="vectorPath"/>和<see cref="path"/>为空且不为null对于正确初始化的路径来说这是正确的
/// </summary>
protected virtual void Trace(PathNode from) {
// 当前正在处理的节点
PathNode c = from;
int count = 0;
// 从终点节点回溯到起点节点直到c为null
while (c != null) {
c = c.parent; // 移动到父节点
count++; // 计算路径中的节点数量
// 防止无限循环如果节点数量超过16384则输出警告并退出循环
if (count > 16384) {
Debug.LogWarning("Infinite loop? >16384 node path. Remove this message if you really have that long paths (Path.cs, Trace method)");
break;
}
}
// 确保列表的容量足够
if (path.Capacity < count) path.Capacity = count; // 调整path列表的容量
if (vectorPath.Capacity < count) vectorPath.Capacity = count; // 调整vectorPath列表的容量
c = from; // 重置c为起点节点
// 从起点节点到终点节点遍历并将节点添加到path列表中
for (int i = 0; i < count; i++) {
path.Add(c.node); // 添加节点到path列表
c = c.parent; // 移动到父节点
}
// 反转path列表因为我们是从终点回溯到起点的所以节点顺序是反的
int half = count / 2;
for (int i = 0; i < half; i++) {
var tmp = path[i]; // 临时变量用于交换
path[i] = path[count - i - 1]; // 交换节点位置
path[count - i - 1] = tmp;
}
// 将path列表中的节点位置添加到vectorPath列表中
for (int i = 0; i < count; i++) {
vectorPath.Add((Vector3)path[i].position); // 强制转换节点位置为Vector3并添加到vectorPath列表
}
}
/// <summary>
/// 计算路径直到完成或直到时间超过目标tick。
/// 通常只有在每500个节点并且时间超过目标tick时才会进行检查。
/// 时间/Ticks是从System.DateTime.UtcNow.Ticks获取的 帧同步不能使用本地时间 所以默认不能超过2000节点。
///
/// 对于标准路径Pathfinding.ABPath该函数的基本概述。
/// </summary>
protected override void CalculateStep()
{
// 计数器用于每500个节点检查一次时间
int counter = 0;
// 继续搜索,直到遇到错误或找到目标
while (CompleteState == PathCompleteState.NotCalculated)
{
// 增加已搜索节点的数量
searchedNodes++;
// 如果没有要搜索的节点了
if (pathHandler.heap.isEmpty)
{
// 如果需要计算部分路径,并且有一个部分最佳目标
if (calculatePartial && partialBestTarget != null)
{
// 完成部分路径的计算
CompletePartial(partialBestTarget);
}
else
{
// 报告错误,所有可达节点都已搜索,但找不到目标
FailWithError("Searched all reachable nodes, but could not find target. This can happen if you have nodes with a different tag blocking the way to the goal. You can enable path.calculatePartial to handle that case workaround (though this comes with a performance cost).");
}
// 退出方法
return;
}
// 选择F分数最低的节点并从开放列表中移除它
currentR = pathHandler.heap.Remove();
// 每500个节点检查一次时间通常大约是每0.5毫秒
if (counter > 2000)
{
// // 如果当前时间已经超过了目标tick
// if (System.DateTime.UtcNow.Ticks >= targetTick)
// {
// // 返回而不是yield一个单独的函数处理yieldCalculatePaths
// return;
// }
// // 重置计数器
// counter = 0;
return;
// // 主要用于开发阶段
// if (searchedNodes > 1000000)
// {
// // 抛出异常可能是无限循环搜索了超过1,000,000个节点
// throw new Exception("Probable infinite loop. Over 1,000,000 nodes searched");
// }
}
// 增加计数器
counter++;
}
// 如果路径计算完成
if (CompleteState == PathCompleteState.Complete)
{
// 记录或输出当前节点信息
Trace(currentR);
}
// 如果需要计算部分路径,并且有一个部分最佳目标
else if (calculatePartial && partialBestTarget != null)
{
// 完成部分路径的计算
CompletePartial(partialBestTarget);
}
}
} }
} }

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Game.Plugins.JNGame.Sync.Frame.AStar.Util; using Game.Plugins.JNGame.Sync.Frame.AStar.Util;
using Plugins.JNGame.Sync.Frame.AStar.Entity;
using UnityEngine; using UnityEngine;
using NotImplementedException = System.NotImplementedException; using NotImplementedException = System.NotImplementedException;
using Object = System.Object; using Object = System.Object;
@ -52,11 +53,11 @@ namespace Plugins.JNGame.Sync.Frame.AStar
/// </summary> /// </summary>
void ReturnPath(); void ReturnPath();
// /// <summary> /// <summary>
// /// 为基础路径处理器做准备 /// 为基础路径处理器做准备
// /// </summary> /// </summary>
// /// <param name="handler">路径处理器</param> /// <param name="handler">路径处理器</param>
// void PrepareBase(PathHandler handler); void PrepareBase(PathHandler handler);
/// <summary> /// <summary>
/// 准备路径以供使用 /// 准备路径以供使用
@ -76,8 +77,7 @@ namespace Plugins.JNGame.Sync.Frame.AStar
/// <summary> /// <summary>
/// 根据目标时间戳计算路径的下一步 /// 根据目标时间戳计算路径的下一步
/// </summary> /// </summary>
/// <param name="targetTick">目标时间戳</param> void CalculateStep();
void CalculateStep(long targetTick);
// /// <summary> // /// <summary>
// /// 获取用于调试的路径日志字符串 // /// 获取用于调试的路径日志字符串
@ -87,7 +87,7 @@ namespace Plugins.JNGame.Sync.Frame.AStar
// string DebugString(PathLog logMode); // string DebugString(PathLog logMode);
} }
public class JNPath : IPathInternals public abstract class JNPath : IPathInternals
{ {
/// <summary> /// <summary>
/// 此路径的ID。用于区分不同的路径 /// 此路径的ID。用于区分不同的路径
@ -176,6 +176,43 @@ namespace Plugins.JNGame.Sync.Frame.AStar
/// </summary> /// </summary>
public List<Vector3> vectorPath; public List<Vector3> vectorPath;
/// <summary>计算此路径的线程的数据</summary>
protected PathHandler pathHandler;
/// <summary>
/// 该路径已搜索的节点数量
/// </summary>
public int searchedNodes { get; protected set; }
/// <summary>
/// 记入错误信息
/// See: <see cref="AstarPath.logPathResults"/>
/// </summary>
public string errorLog { get; private set; }
/// <summary>
/// 以<see cref="GraphNode"/>列表的形式保存路径。
///
/// 这些节点是路径查找算法计算得出的路径上经过的所有节点。
/// 这可能与后处理路径遍历的节点不同。
///
/// 参见:<see cref="vectorPath"/>
/// </summary>
public List<GraphNode> path;
/// <summary>
/// 当前正在处理的节点
/// </summary>
protected PathNode currentR;
/// <summary>
/// 当路径完成时立即调用的回调函数。
/// 警告:这个函数可能在一个单独的线程中被调用。通常不建议使用此函数。
///
/// 参见callback
/// </summary>
public OnPathDelegate immediateCallback;
/// <summary> /// <summary>
/// 路径的当前状态。 /// 路径的当前状态。
/// Bug当前可能在路径完全计算之前就将此属性设置为Complete。特别是vectorPath和path列表可能尚未完全构建。 /// Bug当前可能在路径完全计算之前就将此属性设置为Complete。特别是vectorPath和path列表可能尚未完全构建。
@ -325,6 +362,79 @@ namespace Plugins.JNGame.Sync.Frame.AStar
} }
} }
/// <summary>
/// 为计算准备低级别的路径变量。
/// 在路径搜索之前调用。
/// 总是在Prepare、Initialize和CalculateStep函数之前调用。
/// </summary>
/// <param name="pathHandler">路径处理器对象,用于处理路径相关的操作。</param>
public void PrepareBase(PathHandler pathHandler) {
// 如果路径处理器的PathID大于当前的pathID则表明路径ID已经超过了65K需要进行清理。
// 由于pathIDs是顺序分配的我们可以这样做。
if (pathHandler.PathID > pathID) {
pathHandler.ClearPathIDs(); // 调用ClearPathIDs方法来清理pathIDs。
}
// 确保当前对象可能是一个路径对象对pathHandler有引用。
this.pathHandler = pathHandler;
// 将相关的路径数据分配给pathHandler以便后续使用。
pathHandler.InitializeForPath(this); // 调用InitializeForPath方法并传递当前对象为参数进行初始化。
// 确保internalTagPenalties是一个长度为32的数组。
// 如果internalTagPenalties为空或者其长度不为32则将其设置为ZeroTagPenalties。
if (internalTagPenalties == null || internalTagPenalties.Length != 32) {
internalTagPenalties = ZeroTagPenalties; // 将internalTagPenalties设置为默认的或零值的数组。
}
try {
ErrorCheck(); // 调用ErrorCheck方法进行错误检查。
} catch (Exception e) {
// 如果ErrorCheck方法抛出异常则捕获该异常并使用异常的消息调用FailWithError方法。
// 这通常用于记录错误或通知调用者出现了问题。
FailWithError(e.Message);
}
}
/// <summary>
/// 由于发生错误而中止路径。
/// 将 <see cref="error"/> 设置为 true。
/// 当发生错误时调用此函数(例如,找不到有效的路径)。
/// 请参见:<see cref="FailWithError"/>
/// </summary>
public void Error() {
CompleteState = PathCompleteState.Error;
}
/// <summary>
/// 使路径处理失败并将错误消息msg设置到<see cref="errorLog"/>中。
/// </summary>
/// <param name="msg">要记录的错误消息</param>
public void FailWithError(string msg) {
Error(); // 调用Error方法标记路径处理为错误状态
// 检查errorLog是否为空字符串
if (errorLog != "") {
// 如果不为空则在errorLog后添加换行符和新的错误消息msg
errorLog += "\n" + msg;
} else {
// 如果errorLog为空字符串则将errorLog设置为错误消息msg
errorLog = msg;
}
}
/// <summary>
/// 执行一些错误检查。
/// 确保用户没有使用旧的代码路径,并且没有犯下重大错误。
///
/// 如果发现任何错误,则使路径失败。
/// </summary>
private void ErrorCheck () {
// if (!hasBeenReset) FailWithError("请使用静态的Construct函数创建路径不要使用正常的构造函数。");
if (((IPathInternals)this).Pooled) FailWithError("该路径当前在路径池中。您是否将路径发送了两次以进行计算?");
if (pathHandler == null) FailWithError("pathHandler字段未设置。请报告此错误。");
if (PipelineState > PathState.Processing) FailWithError("此路径已被处理。不要使用相同的路径对象两次请求路径。");
}
public bool Pooled { get; set; } public bool Pooled { get; set; }
public void OnEnterPool() public void OnEnterPool()
@ -357,9 +467,8 @@ namespace Plugins.JNGame.Sync.Frame.AStar
throw new NotImplementedException(); throw new NotImplementedException();
} }
public void CalculateStep(long targetTick) /// <summary>计算直到它完成或时间超过targetTick</summary>
{ protected abstract void CalculateStep();
throw new NotImplementedException(); void IPathInternals.CalculateStep () { CalculateStep(); }
}
} }
} }

View File

@ -1,12 +1,183 @@
using System.Collections.Generic; using System;
using System.Collections;
using System.Collections.Generic;
using Plugins.JNGame.Sync.Frame.AStar.Entity;
using Plugins.JNGame.Sync.Frame.Entity;
namespace Plugins.JNGame.Sync.Frame.AStar.Processor namespace Plugins.JNGame.Sync.Frame.AStar.Processor
{ {
public class PathProcessor public class PathProcessor
{ {
// public Queue<JNPath> queue = new Queue<JNPath>(); public event Action<JNPath> OnPathPreSearch;
public LinkedList<JNPath> queue = new LinkedList<JNPath>(); public event Action<JNPath> OnPathPostSearch;
// public event Action OnQueueUnblocked;
public LinkedList<JNPath> queue = new LinkedList<JNPath>();
} public JNAStarPath AStar;
readonly PathReturnQueue returnQueue;
/// <summary>
/// (帧同步无法使用原始的多线程 IEnumerator 可以 所以去除了多线程代码)
/// 当不使用多线程时IEnumerator 将存储在这里。
/// 如果不使用多线程,则使用协程来代替。协程不是通过 StartCoroutine 直接调用的,
/// 而是有一个单独的函数,该函数仅包含一个 while 循环,用于递增主要的 IEnumerator。
/// 这样做是为了让其他函数能够在任何时候推动线程向前,而无需等待 Unity 更新它。
/// 参见CalculatePaths
/// 参见CalculatePathsHandler
/// </summary>
IEnumerator threadCoroutine;
public PathProcessor(JNAStarPath AStar, PathReturnQueue returnQueue)
{
this.AStar = this.AStar;
threadCoroutine = CalculatePaths(new PathHandler());
}
/// <summary>
/// 主要路径查找方法。
/// 该方法将在路径查找队列中计算路径。
///
/// 参考CalculatePathsThreaded
/// 参考StartPath
/// </summary>
IEnumerator CalculatePaths(PathHandler pathHandler)
{
// 允许的最大片数量
long maxTicks = 10;
while (true)
{
// 当前正在计算的路径
JNPath p = null;
// 尝试获取下一个要计算的路径
bool blockedBefore = false;
while (p == null)
{
try
{
// 尝试从队列中获取路径,如果队列为空则不阻塞
if (queue.First != null)
{
p = queue.First.Value;
}
else p = null;
queue.RemoveFirst();
// 如果队列为空则设置blockedBefore为true
blockedBefore |= p == null;
}
catch (Exception e)
{
// 如果队列被终止,则退出循环
yield break;
}
if (p == null)
{
// 如果路径为空则yield等待下一个帧
yield return null;
}
}
// 将Path对象转换为IPathInternals接口
IPathInternals ip = (IPathInternals)p;
// 准备路径的基础数据
ip.PrepareBase(pathHandler);
// 将路径状态设置为Processing
ip.AdvanceState(PathState.Processing);
// 调用一些回调函数
var tmpOnPathPreSearch = OnPathPreSearch;
if (tmpOnPathPreSearch != null) tmpOnPathPreSearch(p);
// // 记录路径开始计算的时间,用于计算计算时间
// long startTicks = System.DateTime.UtcNow.Ticks;
// long totalTicks = 0;
// 准备路径
ip.Prepare();
// 检查Prepare调用是否导致路径完成
// 如果发生这种情况,通常表示路径计算失败
if (p.CompleteState == PathCompleteState.NotCalculated)
{
// // 为了调试目的我们将最后计算的路径设置为p以便我们可以在编辑器场景视图中查看其调试信息
// AStar.debugPathData = ip.PathHandler;
// AStar.debugPathID = p.pathID;
// 初始化路径,现在已准备好开始搜索
ip.Initialize();
// 错误可能出现在Init函数中
while (p.CompleteState == PathCompleteState.NotCalculated)
{
// 对路径计算进行一些工作。
// 当花费的时间过多或计算完成时,该函数将返回
ip.CalculateStep();
// 如果路径已完成计算,我们可以直接在这里中断,而不是休眠
// 这将提高延迟性能
if (p.CompleteState != PathCompleteState.NotCalculated) break;
// yield/sleep以便其他线程可以工作
yield return null;
// // 如果不再接受更多路径,则取消函数(以及相应的线程)
// // 这在A*对象即将被销毁时完成
// // 路径会被返回然后该函数将被终止在函数的更高位置有类似的IF语句
// if (queue.IsTerminating)
// {
// p.FailWithError("AstarPath object destroyed");
// }
//
// // 更新目标时间片,基于当前时间和最大时间片
// targetTick = System.DateTime.UtcNow.Ticks + maxTicks;
}
// // 将从路径开始计算到现在所花费的总时间片添加到总时间中,并转换为秒
// totalTicks += System.DateTime.UtcNow.Ticks - startTicks;
// p.duration = totalTicks * 0.0001F;
#if ProfileAstar
// 如果启用了ProfileAstar则增加已完成路径的计数
System.Threading.Interlocked.Increment(ref AstarPath.PathsCompleted);
#endif
// 清理节点标记和其他事情
ip.Cleanup();
// 调用即时回调函数
// 需要将其存储在一个局部变量中以避免竞态条件
var tmpImmediateCallback = p.immediateCallback;
if (tmpImmediateCallback != null) tmpImmediateCallback(p);
// 调用路径搜索后的回调函数
// 需要将其存储在一个局部变量中以避免竞态条件
var tmpOnPathPostSearch = OnPathPostSearch;
if (tmpOnPathPostSearch != null) tmpOnPathPostSearch(p);
// 将路径推送到返回堆栈
// 它将被Unity的主线程检测到并尽快返回在下一个LateUpdate中
returnQueue.Enqueue(p);
// 将路径状态推进到返回队列中
ip.AdvanceState(PathState.ReturnQueue);
// // 如果我们计算了大量的路径,则稍作等待
// if (System.DateTime.UtcNow.Ticks > targetTick)
// {
// // yield/sleep以便其他线程可以工作
// yield return null;
// // 重置目标时间片为当前时间加上最大时间片
// targetTick = System.DateTime.UtcNow.Ticks + maxTicks;
// }
// yield/sleep以便其他线程可以工作
// yield return null;
}
}
}
}
} }

View File

@ -0,0 +1,448 @@
using Plugins.JNGame.Sync.Frame.AStar.Entity;
namespace Game.Plugins.JNGame.Sync.Frame.AStar.Util
{
/// <summary>
/// 二叉堆实现。
/// 二叉堆非常适合于以能够快速获取F值最低的节点的方式对节点进行排序。
/// 也称为优先队列。
///
/// 为了提高性能,这里实际上已经被重写为四叉堆,但原理相同。
///
/// 参见http://en.wikipedia.org/wiki/Binary_heap
/// 参见https://en.wikipedia.org/wiki/D-ary_heap
/// </summary>
public class BinaryHeap {
/// <summary>树中项目的数量</summary>
public int numberOfItems; // 定义了树中当前节点的数量
/// <summary>每次扩展树时,树至少会按此因子增长</summary>
public float growthFactor = 2; // 定义了树每次扩展时的增长因子默认为2
/// <summary>
/// 树中每个节点的子节点数量。
/// 已经测试了不同的值并经验性地发现4的性能最佳。
/// 参见https://en.wikipedia.org/wiki/D-ary_heap
/// </summary>
const int D = 4; // 定义了树中每个节点的子节点数量为4这符合四叉堆4-ary heap的定义
/// <summary>
/// 当比较F值时如果有相同的情况则按G值对节点进行排序。
/// 禁用此功能将提高路径查找性能约2.5%
/// 但可能会以不太理想的方式打破具有相同长度的路径之间的平衡(仅适用于网格图)。
/// </summary>
const bool SortGScores = true; // 定义了当F值相同时是否按G值对节点进行排序默认为true
public const ushort NotInHeap = 0xFFFF; // 定义了一个常量表示节点不在堆中使用ushort类型的最大值0xFFFF作为标识
/// <summary>堆的内部支持数组</summary>
private Tuple[] heap; // 定义了存储堆中节点的数组类型为Tuple[]
/// <summary>如果堆中不包含任何元素则为true</summary>
public bool isEmpty {
get {
return numberOfItems <= 0; // 定义了一个属性用于判断堆是否为空当numberOfItems小于等于0时表示堆为空
}
}
/// <summary>堆中的项</summary>
private struct Tuple {
// 堆中的元素包含一个PathNode节点和一个F值
// PathNode是路径节点通常包含位置、G值、H值等信息
// F值是A*算法中的关键值通常是G值从起点到当前节点的成本和H值从当前节点到目标的启发式估计成本的和
public PathNode node;
public uint F;
// Tuple的构造函数接受一个F值和一个PathNode节点作为参数
// 使用这些参数来初始化Tuple的F和node成员变量
public Tuple(uint f, PathNode node) {
this.F = f;
this.node = node;
}
}
/// <summary>
/// 向上取整v使得当它被D除时余数为1。
/// 即它是以n*D + 1的形式其中n是任何非负整数。
/// </summary>
static int RoundUpToNextMultipleMod1(int v) {
// 我感觉有更优雅的方式来实现这个
// 使用取模和加法运算将v调整到满足条件的下一个整数
// 这里要确保v + (4 - ((v-1) % D)) % D的结果在除以D后余数为1
// 这是因为D为4所以4 - ((v-1) % D)会确保余数为1
return v + (4 - ((v - 1) % D)) % D;
}
/// <summary>使用指定的初始容量创建一个新的堆</summary>
public BinaryHeap(int capacity) {
// 确保容量在除以D后余数为1
// 这允许我们始终保证在Remove方法中使用的索引
// 永远不会抛出越界异常
// 使用RoundUpToNextMultipleMod1方法来调整初始容量
capacity = RoundUpToNextMultipleMod1(capacity);
// 根据调整后的容量初始化堆数组
heap = new Tuple[capacity];
// 初始化堆中的项目数量为0
numberOfItems = 0;
}
/// <summary>从堆中移除所有元素</summary>
public void Clear() {
#if DECREASE_KEY
// 清除所有堆索引
// 这是为了避免出现错误
// 如果定义了DECREASE_KEY条件编译符号则执行以下代码块
for (int i = 0; i < numberOfItems; i++) {
// 将堆中每个元素的heapIndex设置为NotInHeap常量
// 这有助于在堆操作中避免错误使用已删除的节点
heap[i].node.heapIndex = NotInHeap;
}
#endif
// 将堆中的项目数量重置为0
numberOfItems = 0;
}
/// <summary>
/// 根据索引获取堆中的节点
/// </summary>
/// <param name="i">节点的索引</param>
/// <returns>返回指定索引处的PathNode节点</returns>
internal PathNode GetNode(int i) {
return heap[i].node;
}
/// <summary>
/// 设置堆中指定索引位置的元素的F值
/// </summary>
/// <param name="i">元素的索引</param>
/// <param name="f">新的F值</param>
internal void SetF(int i, uint f) {
heap[i].F = f;
}
/// <summary>
/// 当当前数组过小时,扩展为更大的支持数组
/// </summary>
void Expand() {
// 65533 除以4余1且略小于2的16次方即65536
// 计算新的数组大小基于当前数组长度增长一定的比例但不超过65533
// 同时确保新的大小是4的倍数加1以满足四叉堆的特性
int newSize = System.Math.Max(heap.Length + 4, System.Math.Min(65533, (int)System.Math.Round(heap.Length * growthFactor)));
// 确保新大小除以D余数为1
// 这保证了在Remove方法中使用的索引永远不会超出数组边界
newSize = RoundUpToNextMultipleMod1(newSize);
// 检查堆是否过大
// 注意大于此大小的堆不受支持因为PathNode.heapIndex是ushort类型
// 只能存储最大值到65535NotInHeap = 65535保留
if (newSize > (1 << 16) - 2) {
throw new System.Exception("Binary Heap Size really large (>65534). A heap size this large is probably the cause of pathfinding running in an infinite loop. ");
}
// 创建新的数组来存储扩展后的堆
var newHeap = new Tuple[newSize];
// 将旧堆的内容复制到新堆中
heap.CopyTo(newHeap, 0);
#if ASTARDEBUG
// 如果定义了ASTARDEBUG符号则输出调试信息
UnityEngine.Debug.Log("Resizing binary heap to " + newSize);
#endif
// 更新堆的引用为新数组
heap = newHeap;
}
/// <summary>
/// 将节点添加到堆中
/// </summary>
/// <param name="node">要添加的节点</param>
public void Add(PathNode node) {
// 如果传入的节点为null则抛出参数为空的异常
if (node == null) throw new System.ArgumentNullException("node");
#if DECREASE_KEY
// 检查节点是否已经在堆中
// 如果节点已经在堆中,则减小其关键值并返回,不再重复添加
if (node.heapIndex != NotInHeap) {
DecreaseKey(heap[node.heapIndex], node.heapIndex);
return;
}
#endif
// 如果堆已满,则扩展堆的大小
if (numberOfItems == heap.Length) {
Expand();
}
// 将新节点添加到堆中,并设置其关键值
DecreaseKey(new Tuple(0, node), (ushort)numberOfItems);
// 更新堆中的元素数量
numberOfItems++;
}
/// <summary>
/// 减小节点的关键值,并在堆中进行上浮调整
/// </summary>
/// <param name="node">要调整关键值的节点</param>
/// <param name="index">节点在堆中的索引</param>
void DecreaseKey(Tuple node, ushort index) {
// 'obj'在逻辑上位于二叉堆的哪个位置
// (出于性能考虑,我们实际上并不立即将其存储在那里,直到我们知道最终的索引为止,
// 因为提前存储只会浪费CPU周期
int bubbleIndex = index;
// 更新节点的F值因为它自从被添加到堆中以来可能已经改变
uint nodeF = node.F = node.node.F;
uint nodeG = node.node.G;
// 上浮调整,直到节点满足二叉堆的性质
while (bubbleIndex != 0) {
// 泡泡节点的父节点
int parentIndex = (bubbleIndex - 1) / D;
// 如果当前节点的F值小于其父节点的F值或者如果SortGScores为真且F值相等
// 当前节点的G值大于其父节点的G值则交换这两个节点
if (nodeF < heap[parentIndex].F || (SortGScores && nodeF == heap[parentIndex].F && nodeG > heap[parentIndex].node.G)) {
// 交换泡泡节点和其父节点
// (实际上我们并不需要立即存储泡泡节点,直到我们知道最终索引为止,
// 所以我们在循环之后执行此操作)
heap[bubbleIndex] = heap[parentIndex];
#if DECREASE_KEY
// 如果定义了DECREASE_KEY符号则更新父节点的堆索引
heap[bubbleIndex].node.heapIndex = (ushort)bubbleIndex;
#endif
// 将泡泡索引更新为父节点的索引,继续上浮调整
bubbleIndex = parentIndex;
} else {
// 如果当前节点的F值不小于其父节点的F值且如果SortGScores为真G值也不大于其父节点
// 则停止上浮调整
break;
}
}
// 将节点放置在最终的位置
heap[bubbleIndex] = node;
#if DECREASE_KEY
// 如果定义了DECREASE_KEY符号则更新节点的堆索引
node.node.heapIndex = (ushort)bubbleIndex;
#endif
}
/// <summary>
/// 从堆中移除并返回具有最低F分数的节点
/// </summary>
public PathNode Remove()
{
// 获取堆顶节点的节点对象
PathNode returnItem = heap[0].node;
#if DECREASE_KEY
// 如果定义了DECREASE_KEY符号则将堆顶节点的堆索引设置为NotInHeap
returnItem.heapIndex = NotInHeap;
#endif
// 减少堆中元素的数量
numberOfItems--;
// 如果堆为空,则直接返回堆顶节点
if (numberOfItems == 0) return returnItem;
// 获取堆数组中最后一个元素
var swapItem = heap[numberOfItems];
var swapItemG = swapItem.node.G;
// 定义交换索引和父节点索引
int swapIndex = 0, parent;
// 执行上浮操作
while (true)
{
// 设置父节点索引
parent = swapIndex;
// 获取交换元素的F分数
uint swapF = swapItem.F;
// 计算子节点的起始索引
int pd = parent * D + 1;
// 确保子节点的索引在有效范围内
if (pd <= numberOfItems)
{
// 预先加载所有子节点的F分数减少数据依赖并提高性能
uint f0 = heap[pd + 0].F;
uint f1 = heap[pd + 1].F;
uint f2 = heap[pd + 2].F;
uint f3 = heap[pd + 3].F;
// 常见的情况是节点的所有子节点都存在
// 因此下面的每个if语句中的第一个比较都将被很好地预测所以基本上是免费的
// (我尝试优化了常见情况但这对性能没有任何影响只是代码变得更长CPU的分支预测器非常强大)
// 检查第一个子节点如果其F分数更小或者如果SortGScores为真且F分数相等但G分数更小则更新交换元素
if (pd + 0 < numberOfItems &&
(f0 < swapF || (SortGScores && f0 == swapF && heap[pd + 0].node.G < swapItemG)))
{
swapF = f0;
swapIndex = pd + 0;
}
// 检查第二个子节点,更新逻辑同上
if (pd + 1 < numberOfItems && (f1 < swapF || (SortGScores && f1 == swapF &&
heap[pd + 1].node.G < (swapIndex == parent
? swapItemG
: heap[swapIndex].node.G))))
{
swapF = f1;
swapIndex = pd + 1;
}
// 检查第三个子节点,更新逻辑同上
if (pd + 2 < numberOfItems && (f2 < swapF || (SortGScores && f2 == swapF &&
heap[pd + 2].node.G < (swapIndex == parent
? swapItemG
: heap[swapIndex].node.G))))
{
swapF = f2;
swapIndex = pd + 2;
}
// 检查第四个子节点如果存在的F分数是否小于当前交换索引指向的节点的F分数
// 或者如果SortGScores为真表示需要根据G分数进行排序且F分数相同
// 那么比较第四个子节点的G分数是否小于当前交换索引指向的节点的G分数
// 如果是,则更新交换索引为第四个子节点的索引
if (pd + 3 < numberOfItems && (f3 < swapF || (SortGScores && f3 == swapF &&
heap[pd + 3].node.G < (swapIndex == parent
? swapItemG
: heap[swapIndex].node.G))))
{
swapIndex = pd + 3; // 将交换索引更新为第四个子节点的索引
}
}
// 如果父节点不是当前交换索引指向的节点(即存在更小的子节点)
// 则进行上浮操作,即将父节点与交换索引指向的节点(较小的子节点)交换位置
// 实际上这里并没有真正交换节点,而是通过局部变量保存交换的数据
// 直到我们找到最终的交换位置才进行赋值操作
if (parent != swapIndex)
{
// 将父节点的数据复制到交换索引指向的节点位置
heap[parent] = heap[swapIndex];
// 如果定义了DECREASE_KEY则更新父节点在堆中的索引
#if DECREASE_KEY
heap[parent].node.heapIndex = (ushort)parent;
#endif
}
else
{
// 如果父节点已经是最小的子节点,则跳出循环,上浮过程结束
break;
}
}
// 将最初从堆顶移除的节点swapItem放置到最终的交换位置swapIndex
heap[swapIndex] = swapItem;
// 如果定义了DECREASE_KEY则更新该节点在堆中的索引
#if DECREASE_KEY
swapItem.node.heapIndex = (ushort)swapIndex;
#endif
// 以下代码被注释掉了,可能是用于调试目的,用于验证堆结构的完整性
// Validate ();
// 返回从堆中移除的节点(即上浮操作后的堆顶节点)
return returnItem;
}
// 验证方法
void Validate() {
// 遍历堆中的每个元素从第二个元素开始索引为1因为数组通常从0开始
for (int i = 1; i < numberOfItems; i++) {
// 计算当前元素的父节点的索引
int parentIndex = (i-1)/D;
// 检查当前元素的F值是否小于其父节点的F值
// 如果不满足这个条件,则抛出异常,表示堆的状态无效
if (heap[parentIndex].F > heap[i].F) {
throw new System.Exception("在索引 " + i + " 处状态无效: 父节点索引 " + parentIndex + " ( " + heap[parentIndex].F + " > " + heap[i].F + " ) ");
}
// 如果定义了 DECREASE_KEY 符号(可能是编译时的条件编译符号),则执行下面的代码块
#if DECREASE_KEY
// 检查当前节点的堆索引是否与其在数组中的索引一致
// 如果不一致,则抛出异常,表示堆的状态无效
if (heap[i].node.heapIndex != i) {
throw new System.Exception("堆索引无效");
}
#endif
}
}
/// <summary>
/// 通过将所有项向下筛选来重建堆。
/// 通常在路径上的hTarget被更改后调用。
/// </summary>
public void Rebuild () {
#if ASTARDEBUG
// 如果定义了ASTARDEBUG符号则初始化一个变量来跟踪堆中的更改次数
int changes = 0;
#endif
// 从堆的第二个元素开始遍历索引为2因为数组通常从0开始
for (int i = 2; i < numberOfItems; i++) {
// 设置当前元素的索引为bubbleIndex
int bubbleIndex = i;
// 获取当前元素
var node = heap[i];
// 获取当前元素的F值
uint nodeF = node.F;
// 当bubbleIndex不是1时继续循环因为1是堆的根节点
while (bubbleIndex != 1) {
// 计算当前元素的父节点索引
int parentIndex = bubbleIndex / D;
// 如果当前元素的F值小于其父节点的F值则交换它们的位置
if (nodeF < heap[parentIndex].F) {
// 将父节点移动到当前位置
heap[bubbleIndex] = heap[parentIndex];
#if DECREASE_KEY
// 如果定义了DECREASE_KEY符号则更新父节点的堆索引
heap[bubbleIndex].node.heapIndex = (ushort)bubbleIndex;
#endif
// 将当前节点移动到父节点的位置
heap[parentIndex] = node;
#if DECREASE_KEY
// 如果定义了DECREASE_KEY符号则更新当前节点的堆索引
heap[parentIndex].node.heapIndex = (ushort)parentIndex;
#endif
// 更新bubbleIndex为父节点的索引继续向上筛选
bubbleIndex = parentIndex;
#if ASTARDEBUG
// 如果定义了ASTARDEBUG符号则记录一次更改
changes++;
#endif
} else {
// 如果当前元素的F值不小于其父节点的F值则停止筛选
break;
}
}
}
#if ASTARDEBUG
// 如果定义了ASTARDEBUG符号则打印调试信息显示重建堆过程中的更改次数
UnityEngine.Debug.Log("+++ Rebuilt Heap - " + changes + " changes +++");
#endif
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: dbaf79ef74ab4e55ad5d5c0d31856a82
timeCreated: 1707277030

View File

@ -56,8 +56,8 @@ namespace Plugins.JNGame.Sync.Frame.Entity
public JNTime(JNSyncFrame sync) public JNTime(JNSyncFrame sync)
{ {
Time = this;
this._sync = sync; this._sync = sync;
Time = this;
} }
public JNTime() public JNTime()
{ {

View File

@ -1 +1 @@
Build from PC-20230316NUNE at 2024/2/6 18:56:03 Build from PC-20230316NUNE at 2024/2/7 15:46:29

View File

@ -483,6 +483,11 @@
<Compile Include="Assets\Game\Plugins\JNGame\Sync\Frame\AStar\Pathfinders\Processor\PathProcessor.cs" /> <Compile Include="Assets\Game\Plugins\JNGame\Sync\Frame\AStar\Pathfinders\Processor\PathProcessor.cs" />
<Compile Include="Assets\Game\Plugins\JNGame\Sync\Frame\AStar\Entity\Base.cs" /> <Compile Include="Assets\Game\Plugins\JNGame\Sync\Frame\AStar\Entity\Base.cs" />
<Compile Include="Assets\Game\Plugins\JNGame\Sync\Frame\AStar\RVO\JNRVOController.cs" /> <Compile Include="Assets\Game\Plugins\JNGame\Sync\Frame\AStar\RVO\JNRVOController.cs" />
<Compile Include="Assets\Game\Plugins\JNGame\Sync\Frame\AStar\Navmesh\NavmeshUpdates.cs" />
<Compile Include="Assets\Game\Plugins\JNGame\Sync\Frame\AStar\Navmesh\GraphUpdateProcessor.cs" />
<Compile Include="Assets\Game\Plugins\JNGame\Sync\Frame\AStar\Entity\PathHandler.cs" />
<Compile Include="Assets\Game\Plugins\JNGame\Sync\Frame\AStar\Util\BinaryHeap.cs" />
<Compile Include="Assets\Game\Plugins\JNGame\Sync\Frame\AStar\Entity\GraphNode.cs" />
<None Include="Assets\Game\Plugins\JNGame\BepuPhysics\Core\BepuPhysics2\BepuUtilities.2.3.4\lib\netstandard2.0\BepuUtilities.xml" /> <None Include="Assets\Game\Plugins\JNGame\BepuPhysics\Core\BepuPhysics2\BepuUtilities.2.3.4\lib\netstandard2.0\BepuUtilities.xml" />
<None Include="Assets\Game\Plugins\JNGame\JNGame.asmdef" /> <None Include="Assets\Game\Plugins\JNGame\JNGame.asmdef" />
<None Include="Assets\Game\Plugins\JNGame\BepuPhysics\Core\BepuPhysics2\BepuPhysics.2.3.4\lib\netstandard2.0\BepuPhysics.xml" /> <None Include="Assets\Game\Plugins\JNGame\BepuPhysics\Core\BepuPhysics2\BepuPhysics.2.3.4\lib\netstandard2.0\BepuPhysics.xml" />

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff