提交寻路

This commit is contained in:
PC-20230316NUNE\Administrator
2024-02-06 18:57:13 +08:00
parent 16d943ab6b
commit 0a12c49bf1
49 changed files with 9046 additions and 38 deletions

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ea15d1984de842cba18584da709524e7
timeCreated: 1707188365

View File

@@ -0,0 +1,263 @@
using System.Collections.Generic;
using Plugins.JNGame.Sync.Frame.AStar;
using Plugins.JNGame.Sync.Frame.AStar.Modifier;
using UnityEngine;
using UnityEngine.Profiling;
namespace Game.Plugins.JNGame.Sync.Frame.AstarPath.AI
{
public class JNAISeeker
{
public JNAISeeker(JNAStarPath Atar)
{
this.Atar = Atar;
}
//A*系统
public JNAStarPath Atar;
/// <summary>
/// 使用Gizmos绘制最后计算出的路径。
/// 路径将以绿色显示。
///
/// 参见OnDrawGizmos
/// </summary>
public bool drawGizmos = true;
/// <summary>
/// 启用使用Gizmos绘制非后处理路径。
/// 路径将以橙色显示。
///
/// 要求 <see cref="drawGizmos"/> 为 true。
///
/// 这将显示在应用任何后处理(如平滑)之前的路径。
///
/// 参见drawGizmos
/// 参见OnDrawGizmos
/// </summary>
public bool detailedGizmos;
// /// <summary>路径修改器,用于微调路径的起点和终点</summary>
// [HideInInspector] // 在Inspector面板中隐藏此属性
// public StartEndModifier startEndModifier = new StartEndModifier(); // 创建一个StartEndModifier实例用于修改路径的起点和终点
/// <summary>
/// Seeker可以遍历的标签。
///
/// 注意:此字段是一个位掩码。
/// 请参见:位掩码(查看在线文档以获取工作链接)
/// </summary>
public int traversableTags = -1; // 表示Seeker可以遍历的所有标签。初始值为-1可能意味着没有限制。
/// <summary>
/// 每个标签的惩罚值。
/// 默认标签即标签0将有一个惩罚值该值由tagPenalties[0]给出。
/// 这些值应该是正数因为A*算法无法处理负惩罚值。
///
/// 注意此数组的长度应始终为32否则系统将忽略它。
///
/// 请参见Pathfinding.Path.tagPenalties
/// </summary>
public int[] tagPenalties = new int[32]; // 创建一个长度为32的整数数组用于存储每个标签的惩罚值
/// <summary>
/// 自定义遍历提供者,用于计算哪些节点可遍历以及它们的惩罚值。
///
/// 这可以用于覆盖内置的寻路逻辑。(请在线文档中查看工作链接)
/// </summary>
public ITraversalProvider traversalProvider;
/// <summary>
/// 该搜索器可以使用的图形。
/// 此字段决定了在搜索路径的起始和结束节点时应该考虑哪些图形。
/// 在许多情况下都很有用,例如,如果你想为小型单位创建一个图形,为大型单位创建另一个图形。
///
/// 这是一个位掩码因此例如如果你只想让代理仅使用图形索引3你可以这样设置
/// <code> seeker.graphMask = 1 << 3; </code>
///
/// 请参阅:位掩码(查看在线文档以获取工作链接)
///
/// 请注意,此字段仅存储允许的图形索引。这意味着如果图形的顺序发生变化,
/// 则此掩码可能不再正确。 GraphMask mask1 = GraphMask.FromGraphName("My Grid Graph");
/// GraphMask mask2 = GraphMask.FromGraphName("My Other Grid Graph");
///
/// NNConstraint nn = NNConstraint.Walkable;
///
/// nn.graphMask = mask1 | mask2;
///
/// // 查找与somePoint最近的节点该节点位于'My Grid Graph'或'My Other Grid Graph'中
/// var info = AstarPath.active.GetNearest(somePoint, nn);
/// </code>
///
/// <see cref="StartPath"/>方法的一些重载接受一个graphMask参数。如果使用这些重载
/// 它们将覆盖该路径请求的图形掩码。
///
/// [打开在线文档以查看图像]
///
/// 请参阅:多种代理类型(查看在线文档以获取工作链接)
/// </summary>
public GraphMask graphMask = GraphMask.everything; // 默认情况下,允许所有图形
/// <summary>当前路径</summary>
protected JNPath path;
/// <summary>
/// 上次查询的路径的ID
/// </summary>
protected uint lastPathID;
/// <summary>
/// 仅对当前路径调用的临时回调函数。该值由StartPath函数设置
/// </summary>
private OnPathDelegate tmpPathCallback;
/// <summary>
/// 缓存的委托,用于避免每次启动路径时都分配一个新的委托实例
/// </summary>
private readonly OnPathDelegate onPathDelegate;
/// <summary>
/// 缓存的委托,用于避免每次启动部分路径时都分配一个新的委托实例
/// </summary>
private readonly OnPathDelegate onPartialPathDelegate;
/// <summary>在路径查找开始之前调用</summary>
public OnPathDelegate preProcessPath;
/// <summary>
/// 所有修饰符的内部列表
/// </summary>
readonly List<IPathModifier> modifiers = new List<IPathModifier>();
/// <summary>
/// 在路径计算完成后、修饰符执行之前调用。
/// </summary>
public OnPathDelegate postProcessPath;
public enum ModifierPass {
PreProcess,
PostProcess = 2,
}
/// <summary>
/// 调用此函数开始计算路径。
///
/// 当路径计算完成(可能是在未来几个帧中)时,将调用回调。
/// 如果路径被取消(例如,在之前的路径完成之前请求新的路径),则不会调用回调。
/// </summary>
/// <param name="start">路径的起点</param>
/// <param name="end">路径的终点</param>
/// <param name="callback">路径计算完成后要调用的函数</param>
public JNPath StartPath(Vector3 start, Vector3 end, OnPathDelegate callback) {
return StartPath(JNABPath.Construct(start, end, null), callback);
}
/// <summary>
/// 调用此函数以开始计算路径。
///
/// 当路径计算完成后(可能是在未来的多个帧中),将调用回调函数。
/// 如果在新的路径请求计算完成之前开始了一个新的路径请求,则不会调用回调函数。
///
/// 版本从3.8.3版本开始如果使用了MultiTargetPath则此方法将正常工作。
/// 它的行为现在与StartMultiTargetPath(MultiTargetPath)方法完全相同。
///
/// 版本从4.1.x版本开始除非明确将其作为参数传递请参阅此方法的其他重载否则此方法将不再覆盖路径上的graphMask。
/// </summary>
/// <param name="p">要开始计算的路径</param>
/// <param name="callback">路径计算完成时要调用的函数</param>
public JNABPath StartPath(JNABPath p, OnPathDelegate callback = null) {
// 仅当用户未从默认值更改它时才设置图掩码。
// 这不是完美的,因为用户可能希望它精确为-1
// 但是这是我能做的最佳检测。
// 非默认检查主要是为了兼容性原因,以避免破坏人们现有的代码。
// 应使用具有显式graphMask字段的StartPath重载来设置graphMask。
if (p.nnConstraint.graphMask == -1)
{
p.nnConstraint.graphMask = graphMask;
}
StartPathInternal(p, callback);
return p;
}
/// <summary>
/// 在路径上运行修改器
/// </summary>
/// <param name="pass">修改器传递类型(预处理或后处理)</param>
/// <param name="path">要修改的路径</param>
public void RunModifiers(ModifierPass pass, JNPath path) {
// 如果传递类型是预处理
if (pass == ModifierPass.PreProcess) {
// 如果预处理路径委托存在,则调用它
if (preProcessPath != null) preProcessPath(path);
// 遍历所有修改器,并对其进行预处理
for (int i = 0; i < modifiers.Count; i++)
modifiers[i].PreProcess(path);
}
// 如果传递类型是后处理
else if (pass == ModifierPass.PostProcess) {
// 开始性能分析样本,记录运行路径修改器的时间
Profiler.BeginSample("Running Path Modifiers");
// 如果后处理路径委托存在,则调用它
if (postProcessPath != null) postProcessPath(path);
// 遍历所有修改器,并应用后处理
for (int i = 0; i < modifiers.Count; i++)
modifiers[i].Apply(path);
// 结束性能分析样本
Profiler.EndSample();
}
}
/// <summary>内部方法,用于启动一个路径并将其标记为当前活动路径</summary>
void StartPathInternal (JNABPath p, OnPathDelegate callback) {
var mtp = p as JNMultiTargetPath;
if (mtp != null) {
// // TODO: Allocation, cache
// var callbacks = new OnPathDelegate[mtp.targetPoints.Length];
//
// for (int i = 0; i < callbacks.Length; i++) {
// callbacks[i] = onPartialPathDelegate;
// }
//
// mtp.callbacks = callbacks;
// p.callback += OnMultiPathComplete;
} else {
p.callback += onPathDelegate;
}
p.enabledTags = traversableTags;
p.tagPenalties = tagPenalties;
if (traversalProvider != null) p.traversalProvider = traversalProvider;
// 如果之前请求的路径尚未处理,并且确保它没有被回收并在其他地方使用,则取消它
if (path != null && path.PipelineState <= PathState.Processing && path.CompleteState != PathCompleteState.Error && lastPathID == path.pathID) {
// path.FailWithError("Canceled path because a new one was requested.\n" +
// "This happens when a new path is requested from the seeker when one was already being calculated.\n" +
// "For example if a unit got a new order, you might request a new path directly instead of waiting for the now" +
// " invalid path to be calculated. Which is probably what you want.\n" +
// "If you are getting this a lot, you might want to consider how you are scheduling path requests.");
// 取消的路径将不会发送回调
}
// 将p设置为活动路径
path = p;
tmpPathCallback = callback;
// 保存路径ID以便我们可以确保如果取消路径如上所述它尚未被回收
lastPathID = path.pathID;
// 预处理路径
RunModifiers(ModifierPass.PreProcess, path);
// 将请求发送到寻路器
Atar.StartPath(path);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 66618768278f4430b6d96c6a15814be6
timeCreated: 1707188365

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bd795436e6ce4872a3eb7bbd8f7a6e61
timeCreated: 1707206557

View File

@@ -0,0 +1,15 @@
namespace Plugins.JNGame.Sync.Frame.AStar.Entity
{
public class AStarData
{
/// <summary>
/// 所有Graph。
/// 该数组仅在反序列化完成后填充。
/// 如果图形已被移除,则可能包含空条目。
/// </summary>
// 这通常用于防止某些字段被序列化,因为它们可能包含不需要或不能序列化的数据。
public NavGraph[] graphs = new NavGraph[0]; // 声明一个 NavGraph 类型的数组,初始化为长度为 0 的数组。
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a5994a8804fe405bb692cda49728167c
timeCreated: 1707206826

View File

@@ -0,0 +1,39 @@
namespace Plugins.JNGame.Sync.Frame.AStar.Entity
{
/// <summary>
/// 为图形暴露内部方法。
/// 这用于隐藏任何用户代码都不应使用但还必须为'public'或'internal'的方法(由于此库附带源代码,因此'internal'几乎与'public'相同)。
/// 隐藏内部方法可以清理文档和IntelliSense建议。
/// </summary>
public interface IGraphInternals {
// // 获取或设置序列化后的编辑器设置。
// string SerializedEditorSettings { get; set; }
//
// // 当对象被销毁时调用此方法。
// void OnDestroy();
//
// // 销毁所有节点。
// void DestroyAllNodes();
//
// // 扫描图形并返回进度的集合。
// IEnumerable<Progress> ScanInternal();
//
// // 序列化额外的信息到图形序列化上下文中。
// void SerializeExtraInfo(GraphSerializationContext ctx);
//
// // 从图形序列化上下文中反序列化额外的信息。
// void DeserializeExtraInfo(GraphSerializationContext ctx);
//
// // 在反序列化后执行一些后处理操作。
// void PostDeserialization(GraphSerializationContext ctx);
//
// // 为了兼容性,从图形序列化上下文中反序列化设置。
// void DeserializeSettingsCompatibility(GraphSerializationContext ctx);
}
public abstract class NavGraph : IGraphInternals {
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bb12233f444c4d7689972e08336c0fc0
timeCreated: 1707206578

View File

@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
namespace Plugins.JNGame.Sync.Frame.AStar.Entity
{
public class PathReturnQueue
{
/// <summary>
/// 保存所有等待标记为已完成的路径。
/// 参见:<see cref="ReturnPaths"/>
/// </summary>
Queue<JNPath> pathReturnQueue = new Queue<JNPath>();
private JNAStarPath path;
public PathReturnQueue (JNAStarPath path) {
this.path = path;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 01b5994c772b486196f2524e7fa2a632
timeCreated: 1707208704

View File

@@ -1,7 +0,0 @@
namespace Game.Plugins.JNGame.Sync.Frame.AstarPath
{
public class JNAStarBase
{
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: a3417f25b3114ecc8e596be23f70851f
timeCreated: 1707104675

View File

@@ -0,0 +1,267 @@
using System;
namespace Plugins.JNGame.Sync.Frame.AStar
{
public delegate void OnPathDelegate(JNPath p);
// public delegate void OnGraphDelegate(NavGraph graph);
// public delegate void OnScanDelegate(AstarPath script);
/// <summary>
/// 持有图形的位掩码。
/// 这个位掩码可以容纳最多32个图形。
///
/// 位掩码可以隐式地转换为整数,也可以从整数隐式转换。.FromGraphName("My Other Grid Graph");
///
/// NNConstraint nn = NNConstraint.Walkable;
///
/// nn.graphMask = mask1 | mask2;
///
/// // 查找与somePoint最近的节点该节点位于'My Grid Graph'或'My Other Grid Graph'中
/// var info = AstarPath.active.GetNearest(somePoint, nn);
/// </code>
///
/// 参见:位掩码(查看在线文档以获取工作链接)
/// </summary>
[Serializable]
public struct GraphMask {
/// <summary>表示掩码的位掩码</summary>
public int value;
/// <summary>包含所有图形的掩码</summary>
/// 这是一个静态属性,它返回一个包含所有图形的掩码。在二进制表示中,-1 的所有位都是 1因此它表示所有图形。
public static GraphMask everything { get { return new GraphMask(-1); } }
public GraphMask(int value) {
this.value = value;
}
// 这是一个隐式转换运算符,它允许将 GraphMask 实例隐式转换为整数。转换的结果是 GraphMask 实例的 value 字段的值。
public static implicit operator int(GraphMask mask) {
return mask.value;
}
// 这是另一个隐式转换运算符,它允许将整数隐式转换为 GraphMask 实例。转换的结果是一个新的 GraphMask 实例,其 value 字段的值为传入的整数。
public static implicit operator GraphMask(int mask) {
return new GraphMask(mask);
}
/// <summary>组合两个掩码以形成它们之间的交集</summary>
/// 这是一个重载的按位与运算符,它接受两个 GraphMask 实例作为参数,并返回一个新的 GraphMask 实例,表示这两个掩码的交集。
public static GraphMask operator &(GraphMask lhs, GraphMask rhs) {
return new GraphMask(lhs.value & rhs.value);
}
/// <summary>组合两个掩码以形成它们的并集</summary>
/// 这是一个重载的按位或运算符,它接受两个 GraphMask 实例作为参数,并返回一个新的 GraphMask 实例,表示这两个掩码的并集。
public static GraphMask operator |(GraphMask lhs, GraphMask rhs) {
return new GraphMask(lhs.value | rhs.value);
}
/// <summary>反转掩码</summary>
/// 这是一个重载的按位非运算符,它接受一个 GraphMask 实例作为参数,并返回一个新的 GraphMask 实例,表示该掩码的反转。
public static GraphMask operator ~(GraphMask lhs) {
return new GraphMask(~lhs.value);
}
/// <summary>如果此掩码包含具有给定图形索引的图形,则为真</summary>
/// 这是一个方法,它接受一个整数作为参数(表示图形索引),并返回一个布尔值,指示该掩码是否包含具有给定索引的图形。
/// 它通过将 value 字段右移 graphIndex 位,然后与 1 进行按位与运算,来检查特定位是否为 1。
public bool Contains(int graphIndex) {
return ((value >> graphIndex) & 1) != 0;
}
// /// <summary>包含给定图形的位掩码</summary>
// /// 这是一个静态方法,它接受一个 NavGraph 实例作为参数,并返回一个新的 GraphMask 实例,其中只有与给定图形的索引对应的位被设置为 1。
// /// 它通过将 1 左移 graph.graphIndex 位来创建这个掩码。
// public static GraphMask FromGraph(NavGraph graph) {
// return 1 << (int)graph.graphIndex;
// }
// 这是 ToString 方法的重写,它返回 value 字段的字符串表示形式。这允许 GraphMask 实例以字符串形式输出,通常用于调试或日志记录。
public override string ToString() {
return value.ToString();
}
/// <summary>包含给定图形索引的位掩码。</summary>
/// 这是一个静态方法它接受一个无符号整数表示图形索引作为参数并返回一个GraphMask实例。
/// 它通过将1左移graphIndex位来创建一个位掩码其中只有与给定索引对应的位被设置为1。
public static GraphMask FromGraphIndex(uint graphIndex) => new GraphMask(1 << (int)graphIndex);
// /// <summary>
// /// 包含具有给定名称的第一个图形的位掩码。FromGraphName("My Other Grid Graph");
// ///
// /// NNConstraint nn = NNConstraint.Walkable;
// ///
// /// nn.graphMask = mask1 | mask2;
// ///
// /// // 在'My Grid Graph'或'My Other Grid Graph'中找到离somePoint最近的节点
// /// var info = AstarPath.active.GetNearest(somePoint, nn);
// /// </code>
// /// </summary>
// /// 这是一个静态方法它接受一个字符串表示图形名称作为参数并返回一个GraphMask实例。
// /// 它首先尝试在活动的Astar数据集中找到具有给定名称的图形。
// /// 如果找到图形它将使用FromGraph方法创建一个包含该图形的位掩码。
// /// 如果没有找到匹配的图形,它将抛出一个异常。
// public static GraphMask FromGraphName(string graphName) {
// // 尝试从活动数据集中找到具有给定名称的图形
// var graph = AstarData.active.data.FindGraph(g => g.name == graphName);
//
// // 如果没有找到与名称匹配的图形,则抛出异常
// if (graph == null)
// throw new System.ArgumentException("Could not find any graph with the name '" + graphName + "'");
//
// // 如果找到了图形使用FromGraph方法创建并返回一个包含该图形的位掩码
// return FromGraph(graph);
// }
}
/// <summary>
/// 最近节点约束。约束 <see cref="AstarPath.GetNearest"/> 函数返回的节点。
/// </summary>
public class NNConstraint {
/// <summary>
/// 可用于搜索的图。
/// 这是一个位掩码bitmask意味着位0指定是否应将图形列表中的第一个图形包含在搜索中
/// 位1指定是否应将第二个图形包含在搜索中依此类推。 GraphMask.FromGraphName("My Grid Graph");
/// GraphMask mask2 = GraphMask.FromGraphName("My Other Grid Graph");
///
/// NNConstraint nn = NNConstraint.Walkable;
///
/// nn.graphMask = mask1 | mask2;
///
/// // 查找与somePoint最近的节点该节点位于'My Grid Graph'或'My Other Grid Graph'中
/// var info = AstarPath.active.GetNearest(somePoint, nn);
/// </code>
///
/// 注意:这仅影响从 <see cref="AstarPath.GetNearest"/> 调用返回的节点,
/// 如果一个有效的图通过节点链接连接到一个无效的图,则可能仍然会进行搜索。
///
/// 参见:<see cref="AstarPath.GetNearest"/>
/// 参见:<see cref="SuitableGraph"/>
/// 参见:位掩码(在线文档中查看工作链接)
/// </summary>
public GraphMask graphMask = -1;
/// <summary>
/// 仅将区域 <see cref="area"/> 内的节点视为合适的。如果 <see cref="area"/> 小于 0则不会影响任何内容。
/// </summary>
public bool constrainArea;
/// <summary>
/// 一个NNConstraint用于过滤掉不可通过的节点。
/// 这是最常用的NNConstraint。
///
/// 它还约束最近节点必须在A* Inspector -> Settings -> Max Nearest Node Distance设置的最大距离内。
/// </summary>
public static NNConstraint Walkable {
get {
return new NNConstraint();
}
}
}
/// <summary>
/// 一个特殊的NNConstraint可以在路径中的起始节点和结束节点使用不同的逻辑。
/// PathNNConstraint可以被分配给Path.nnConstraint字段路径将首先搜索起始节点然后它将调用<see cref="SetStart"/>并继续搜索结束节点对于MultiTargetPath则是多个节点
/// 默认的PathNNConstraint将约束终点位于与起点相同的区域内。
/// </summary>
public class PathNNConstraint : NNConstraint {
// 定义了一个静态的默认PathNNConstraint实例该实例在创建时默认将constrainArea设置为true
public static new PathNNConstraint Default {
get {
return new PathNNConstraint {
constrainArea = true
};
}
}
// /// <summary>在找到起始节点后被调用。这用于在路径中的起始节点和结束节点使用不同的搜索逻辑</summary>
// /// <param name="node">找到的起始节点</param>
// public virtual void SetStart (GraphNode node) {
// // 如果找到的节点不为空则设置area为节点的Area属性值
// if (node != null) {
// area = (int)node.Area;
// } else {
// // 如果节点为空,则禁用区域约束
// constrainArea = false;
// }
// }
}
/// <summary>
/// 路径请求的状态
/// </summary>
public enum PathCompleteState {
/// <summary>
/// 路径尚未计算。
/// 请参考:<see cref="Pathfinding.Path.IsDone()"/>
/// </summary>
NotCalculated = 0,
/// <summary>
/// 路径计算已完成,但失败了。
/// 请参考:<see cref="Pathfinding.Path.error"/>
/// </summary>
Error = 1,
/// <summary>
/// 路径已成功计算
/// </summary>
Complete = 2,
/// <summary>
/// 路径已计算,但只找到了部分路径。
/// 请参考:<see cref="Pathfinding.ABPath.calculatePartial"/>
/// </summary>
Partial = 3,
}
/// <summary>
/// 路径在管道中的内部状态
/// </summary>
public enum PathState {
/// <summary>
/// 路径已创建但尚未安排
/// </summary>
Created = 0,
/// <summary>
/// 路径正在等待计算
/// </summary>
PathQueue = 1,
/// <summary>
/// 路径正在计算中
/// </summary>
Processing = 2,
/// <summary>
/// 路径已计算完成,正在等待调用回调函数
/// </summary>
ReturnQueue = 3,
/// <summary>
/// 目前正在调用路径的回调函数(仅在回调函数内部设置)
/// </summary>
Returning = 4,
/// <summary>
/// 路径已计算完成且回调函数已调用
/// </summary>
Returned = 5,
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f220b46018bc4401ab01f5724b4cfdba
timeCreated: 1707189107

View File

@@ -1,13 +1,84 @@

namespace Game.Plugins.JNGame.Sync.Frame.AstarPath
using System;
using Plugins.JNGame.Sync.Frame.AStar.Entity;
using Plugins.JNGame.Sync.Frame.AStar.Processor;
using UnityEngine;
namespace Plugins.JNGame.Sync.Frame.AStar
{
/**
* 寻路类
*/
//A* 寻路
public class JNAStarPath
{
/// <summary>
/// 存储所有等待计算的路径并计算它们
/// </summary>
public PathProcessor pathProcessor;
/// <summary>
/// 导航数据
/// </summary>
public AStarData data = new AStarData();
/// <summary>Shortcut to Pathfinding.AstarData.graphs</summary>
public NavGraph[] graphs => data.graphs;
/// <summary>保存所有已完成的路径,等待返回至请求它们的地方</summary>
internal readonly PathReturnQueue pathReturnQueue;
public JNAStarPath()
{
pathReturnQueue = new PathReturnQueue(this);;
}
/// <summary>
/// 将路径添加到队列中,以便尽快进行计算。
/// 当路径计算完成时,将调用在构造路径时指定的回调。
/// 通常你应该使用Seeker组件而不是直接调用此函数。
/// </summary>
/// <param name="path">应该入队的路径。</param>
/// <param name="pushToFront">如果为true则将路径推送到队列的前面绕过所有等待的路径使其成为下一个要计算的路径。
/// 如果你有一个想要优先于其他所有路径的路径,这可能会很有用。但是,请注意不要过度使用它。
/// 如果经常将太多路径放在队列的前面,这可能会导致正常路径需要等待很长时间才能进行计算。</param>
public void StartPath(JNPath path, bool pushToFront = false)
{
// 如果路径的状态不是已创建,则抛出异常
if (path.PipelineState != PathState.Created)
{
Debug.LogError("路径的状态无效。预期值为 " + PathState.Created + ",找到的值为 " + path.PipelineState + "\n" +
"确保你没有请求同一个路径两次");
}
// 如果没有在场景中的图形,则记录错误并使路径失败
if (this.graphs == null || this.graphs.Length == 0)
{
Debug.LogError("场景中没有图形");
return;
}
// 声明该路径属于AStarPath对象
path.Claim(this);
// 将路径的状态从已创建更改为在路径队列中
((IPathInternals)path).AdvanceState(PathState.PathQueue);
// 如果指定了pushToFront参数为true则将路径推送到队列的前面
// 否则,将路径推送到队列的末尾
if (pushToFront){
this.pathProcessor.queue.AddFirst(path);
}else{
this.pathProcessor.queue.AddLast(path);
}
// // 在播放模式之外,所有路径请求都是同步的
// if (!Application.isPlaying) {
// BlockUntilCalculated(path);
// }
}
}
}

View File

@@ -1,3 +1,3 @@
fileFormatVersion: 2
guid: 79d7cad08d874b6aba4388770d624c79
timeCreated: 1707104391
guid: 16e7d844387d4a26a28eb48731f86a00
timeCreated: 1707204238

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bff5409c229d40ccba5802f6d14c962d
timeCreated: 1707203362

View File

@@ -0,0 +1,27 @@
namespace Plugins.JNGame.Sync.Frame.AStar.Modifier
{
/// <summary>
/// 所有路径修饰符的基础接口。
/// 参见MonoModifier
/// Modifier
/// </summary>
public interface IPathModifier
{
/// <summary>
/// 获取修饰符的处理顺序。
/// </summary>
int Order { get; }
/// <summary>
/// 将修饰符应用于给定的路径。
/// </summary>
/// <param name="path">要应用修饰符的路径。</param>
void Apply(JNPath path);
/// <summary>
/// 在处理路径之前进行预处理。
/// </summary>
/// <param name="path">要进行预处理的路径。</param>
void PreProcess(JNPath path);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2ed6304696424c04b5f813c80a1caf7c
timeCreated: 1707203397

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2d59f34978e848e791c4d0e3d43950fe
timeCreated: 1707189548

View File

@@ -0,0 +1,94 @@
using Game.Plugins.JNGame.Sync.Frame.AStar.Util;
using UnityEngine;
namespace Plugins.JNGame.Sync.Frame.AStar
{
/// <summary>
/// 基础路径类用于从A点找到到B点的最短路径。
///
/// 这是最基本的路径对象,它将尝试找到两点之间的最短路径。
/// 许多其他路径类型都继承自这个类。
/// 请参见Seeker.StartPath
/// 请参见calling-pathfinding请在线文档查看有效链接
/// 请参见getstarted请在线文档查看有效链接
/// </summary>
public class JNABPath : JNPath
{
/// <summary>
/// 与路径请求中完全相同的起点
/// </summary>
public Vector3 originalStartPoint;
/// <summary>
/// 路径的起点。
/// 这是 <see cref="startNode"/> 上最接近 <see cref="originalStartPoint"/> 的点。
/// </summary>
public Vector3 startPoint;
/// <summary>
/// 与路径请求中完全相同的终点
/// </summary>
public Vector3 originalEndPoint;
/// <summary>
/// 路径的终点。
/// 这是 <see cref="endNode"/> 上最接近 <see cref="originalEndPoint"/> 的点。
/// </summary>
public Vector3 endPoint;
/// <summary>整数坐标下的起点</summary>
public Int3 startIntPoint;
/// <summary>
/// 为路径请求提供额外的遍历信息。
/// 参见traversal_provider请在线文档中查看以获取有效链接
/// </summary>
public ITraversalProvider traversalProvider;
/// <summary>
/// 使用起始点和终点构造一个路径。
/// 当路径计算完成后,将调用指定的委托。
/// 不要将其与 Seeker 的回调混淆,因为它们在不同的时间点发送。
/// 如果你使用 Seeker 来启动路径,你可以将回调设置为 null。
///
/// 返回值:构造的路径对象
/// </summary>
public static JNABPath Construct(Vector3 start, Vector3 end, OnPathDelegate callback = null) {
// // 从路径池中获取一个ABPath对象。路径池是一种优化技术用于重用和减少内存分配。
// var p = PathPool.GetPath<ABPath>();
//前期直接 New 因为不确定对象池是否可以帧同步
var p = new JNABPath();
// 设置路径的起始点、终点和回调委托。
p.Setup(start, end, callback);
// 返回构造好的路径对象。
return p;
}
protected void Setup (Vector3 start, Vector3 end, OnPathDelegate callbackDelegate) {
callback = callbackDelegate;
UpdateStartEnd(start, end);
}
/// <summary>
/// 设置起始点和终点。
/// 设置 <see cref="originalStartPoint"/>, <see cref="originalEndPoint"/>, <see cref="startPoint"/>, <see cref="endPoint"/>, <see cref="startIntPoint"/> 和 <see cref="hTarget"/>(至终点)
/// </summary>
protected void UpdateStartEnd(Vector3 start, Vector3 end) {
// 将传入的起始点赋值给 originalStartPoint 和 startPoint 变量
originalStartPoint = start;
startPoint = start;
// 将传入的终点赋值给 originalEndPoint, endPoint 和 hTarget 变量
originalEndPoint = end;
endPoint = end;
hTarget = (Int3)end; // 注意这里将 Vector3 类型的 end 强制转换为 Int3 类型
// 将起始点 start 转换为 Int3 类型并赋值给 startIntPoint 变量
startIntPoint = (Int3)start;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7ab89baa2672478ba1aaf8824ea5ccfa
timeCreated: 1707189578

View File

@@ -0,0 +1,29 @@
namespace Plugins.JNGame.Sync.Frame.AStar
{
/// <summary>
/// 一个路径,用于在一次搜索中从一个点到多个不同的目标点搜索,或者从多个不同的起点到单个目标点搜索。
///
/// 如果pathsForAll为true这比为每个目标使用ABPath进行搜索更快。
/// 这种路径类型可用于例如当你想让一个代理从几个不同的选项中找到最近的目标时。
///
/// 当pathsForAll为true时它将为每个目标点计算路径但不同的路径可以共享大量的计算因此
/// 这比单独请求它们要快。
///
/// 当pathsForAll为false时它将使用启发式设置为None进行搜索并在找到第一个目标时停止。
/// 这可能比单独请求每个路径更快或更慢。
/// 它将运行一个Dijkstra搜索搜索起点周围的所有节点直到找到最近的目标。
/// 请注意,如果一些目标点非常接近起点,而另一些非常远,那么这通常更快,但
/// 如果所有目标点都相对较远,那么由于不使用任何启发式,它将不得不搜索一个更大的
/// 区域,因此可能会更慢。
///
/// 参见Seeker.StartMultiTargetPath
/// 参见MultiTargetPathExample.cs查看在线文档以获取工作链接“多目标路径使用示例”
///
/// 版本从3.7.1版本开始即使pathsForAll为truevectorPath和path字段也始终设置为最短路径。
/// </summary>
// MultiTargetPath类继承自ABPath类用于表示从一个点到多个目标的路径或多起点到单一目标的路径
public class JNMultiTargetPath : JNABPath
{
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b30abfb6dce3449491d3059e448c0823
timeCreated: 1707199274

View File

@@ -0,0 +1,365 @@
using System;
using System.Collections.Generic;
using Game.Plugins.JNGame.Sync.Frame.AStar.Util;
using UnityEngine;
using NotImplementedException = System.NotImplementedException;
using Object = System.Object;
namespace Plugins.JNGame.Sync.Frame.AStar
{
/// <summary>
/// 为路径请求提供额外的遍历信息。
/// 请在线文档中查看 traversal_provider以获取有效的链接
/// </summary>
public interface ITraversalProvider {
// bool CanTraverse(JNAStarPath path, GraphNode node);
// uint GetTraversalCost(JNAStarPath path, GraphNode node);
}
/// <summary>
/// 用于隐藏Path类的内部方法
/// </summary>
internal interface IPathInternals
{
// /// <summary>
// /// 获取路径处理器
// /// </summary>
// PathHandler PathHandler { get; }
/// <summary>
/// 获取或设置路径是否已被池化(即是否被复用)
/// </summary>
bool Pooled { get; set; }
/// <summary>
/// 将路径状态向前推进到指定的状态
/// </summary>
/// <param name="s">目标路径状态</param>
void AdvanceState(PathState s);
/// <summary>
/// 当路径被加入池化时调用
/// </summary>
void OnEnterPool();
/// <summary>
/// 重置路径的状态和属性
/// </summary>
void Reset();
/// <summary>
/// 将路径返回给路径池
/// </summary>
void ReturnPath();
// /// <summary>
// /// 为基础路径处理器做准备
// /// </summary>
// /// <param name="handler">路径处理器</param>
// void PrepareBase(PathHandler handler);
/// <summary>
/// 准备路径以供使用
/// </summary>
void Prepare();
/// <summary>
/// 初始化路径
/// </summary>
void Initialize();
/// <summary>
/// 清理路径资源和状态
/// </summary>
void Cleanup();
/// <summary>
/// 根据目标时间戳计算路径的下一步
/// </summary>
/// <param name="targetTick">目标时间戳</param>
void CalculateStep(long targetTick);
// /// <summary>
// /// 获取用于调试的路径日志字符串
// /// </summary>
// /// <param name="logMode">日志模式</param>
// /// <returns>调试字符串</returns>
// string DebugString(PathLog logMode);
}
public class JNPath : IPathInternals
{
/// <summary>
/// 此路径的ID。用于区分不同的路径
/// </summary>
public ushort pathID { get; private set; }
/// <summary>
/// 用于指定如何搜索节点的约束条件
/// </summary>
public NNConstraint nnConstraint = PathNNConstraint.Walkable;
/// <summary>
/// 当路径计算完成时调用的回调。
/// 这个回调通常被发送给 Seeker 组件,该组件对路径进行后处理,然后调用请求路径的脚本的回调。
/// </summary>
public OnPathDelegate callback;
/// <summary>用于H分数计算的目标点。参见Pathfinding.Node.H</summary>
protected Int3 hTarget;
/// <summary>
/// 可遍历的图标签。
/// 这是一个位掩码,因此-1表示所有位都设置为可遍历的所有标签。); </code>
///
/// 搜索器(Seeker)有一个弹出字段,您可以在其中设置要使用的标签。
/// 注意:如果您正在使用搜索器(Seeker)。在StartPath时搜索器(Seeker)会将此值设置为检查器字段中设置的值。
/// 因此,如果您想通过脚本更改它,需要通过脚本更改搜索器(Seeker)的值,而不是设置此值。
///
/// 参考CanTraverse
/// </summary>
public int enabledTags = -1;
/// <summary>
/// 由其他脚本设置的标签惩罚值
/// 参见tagPenalties
/// </summary>
protected int[] manualTagPenalties;
/// <summary>
/// 实际使用的标签惩罚值。
/// 如果 manualTagPenalties 为 null那么这将是 ZeroTagPenalties。
/// 参见tagPenalties
/// </summary>
protected int[] internalTagPenalties;
/// <summary>用作默认标签惩罚值的零值列表</summary>
static readonly int[] ZeroTagPenalties = new int[32];
/// <summary>
/// 返回路径查找管道中路径的状态
/// </summary>
public PathState PipelineState { get; private set; }
/// <summary>
/// 支持<see cref="CompleteState"/>属性的后备字段
/// </summary>
protected PathCompleteState completeState;
/// <summary>
/// 包含引用对象的此路径上的声明列表
/// </summary>
private List<Object> claimed = new();
/// <summary>
/// 如果路径已经通过非静默调用被释放则为True。
///
/// 参见Release
/// 参见Claim
/// </summary>
private bool releasedNotSilent;
/// <summary>
/// 如果路径失败,此属性为 true。
/// 参见:<see cref="errorLog"/>
/// 参见:这相当于检查 path.CompleteState == PathCompleteState.Error
/// </summary>
public bool error { get { return CompleteState == PathCompleteState.Error; } }
/// <summary>
/// 存储(可能经过后处理)的路径作为 Vector3 列表。
///
/// 该列表可能会被路径修饰器修改,与路径查找算法生成的原始路径相比,可能会更平滑或更简单。
///
/// 参见:修饰器(请查阅在线文档以获取有效链接)
/// 参见:<see cref="path"/>
/// </summary>
public List<Vector3> vectorPath;
/// <summary>
/// 路径的当前状态。
/// Bug当前可能在路径完全计算之前就将此属性设置为Complete。特别是vectorPath和path列表可能尚未完全构建。
/// 在使用多线程时,这可能导致竞态条件。尽量避免使用此方法检查路径是否已计算完成,而应使用<see cref="IsDone"/>。
/// </summary>
public PathCompleteState CompleteState {
get { return completeState; }
protected set {
// // 使用锁定来避免多线程竞态条件,
// // 在这种情况下,主线程将错误状态设置为取消路径,然后寻路线程将路径标记为完成,
// // 这将替换错误状态(如果没有使用锁定和检查)。
// lock (stateLock) {
// 一旦路径进入错误状态,就不能将其设置为任何其他状态
if (completeState != PathCompleteState.Error) completeState = value;
// }
}
}
/// <summary>
/// 每个标签的惩罚值。
/// 默认标签即标签0将添加tagPenalties[0]的惩罚值。
/// 这些值应为正数因为A*算法无法处理负惩罚值。
///
/// 当为这个属性分配数组时它必须长度为32。
///
/// 注意将此属性设置为null或者尝试分配一个长度不为32的数组将使得所有标签的惩罚值被视为零。
///
/// 注意如果你正在使用Seeker。当你调用seeker.StartPath时Seeker会将此值设置为检查器字段中设置的值。
/// 因此你需要通过脚本来更改Seeker的值而不是设置这个值。
///
/// 参见Seeker.tagPenalties
/// </summary>
public int[] tagPenalties {
get {
// 获取器(getter),返回当前的手动设置的标签惩罚值数组
return manualTagPenalties;
}
set {
// 设置器(setter),允许外部设置标签惩罚值数组
if (value == null || value.Length != 32) {
// 如果传入的数组为null或长度不为32则将手动设置的标签惩罚值数组设为null
// 同时将内部使用的标签惩罚值数组设为默认零惩罚值数组
manualTagPenalties = null;
internalTagPenalties = ZeroTagPenalties;
} else {
// 如果传入的数组有效非null且长度为32则更新手动设置的标签惩罚值数组
// 同时更新内部使用的标签惩罚值数组
manualTagPenalties = value;
internalTagPenalties = value;
}
}
}
/// <summary>
/// 将此路径的引用计数增加1用于池化
/// 对路径的声明将确保它不会被池化。
/// 如果您正在使用路径,则应在首次获取它时声明它,然后在不再使用它时释放它。当路径上没有声明时,它将被重置并放入池中。
///
/// 这本质上是引用计数。
///
/// 传递给此方法的对象仅用于更容易地检测池化是否未正确完成。
/// 它可以是任何对象当从移动脚本中使用时您可以只传递“this”。如果尝试使用相同的对象对同一路径进行两次声明这通常不是您想要的
/// 或者如果尝试使用未在该路径的Claim调用中使用的对象进行释放则此类将引发异常。
/// 传递给Claim方法的对象需要与您传递给此方法的对象相同。
///
/// 参见Release
/// 参见Pool
/// 参见pooling请在线文档中查看工作链接
/// 参见https://en.wikipedia.org/wiki/Reference_counting
/// </summary>
public void Claim(Object o)
{
// 如果传递的对象为空,则抛出空引用异常
if (Object.ReferenceEquals(o, null)) throw new ArgumentNullException("o");
// 遍历已声明的对象列表
for (int i = 0; i < claimed.Count; i++)
{
// 需要使用ReferenceEquals因为它可能从另一个线程调用
// 如果已声明的列表中存在相同的对象,则抛出参数异常
if (Object.ReferenceEquals(claimed[i], o))
throw new ArgumentException("您已经使用该对象 (" + o + ") 声明了路径。您是否尝试使用相同的对象两次来声明路径?");
}
// 将对象添加到已声明的列表中
claimed.Add(o);
// // 如果定义了ASTAR_POOL_DEBUG则记录声明信息包括对象的字符串表示和堆栈跟踪
// claimInfo.Add(o.ToString() + "\n\nClaimed from:\n" + System.Environment.StackTrace);
}
/// <summary>
/// 线程安全地增加状态
/// </summary>
void IPathInternals.AdvanceState(PathState s) {
// lock (stateLock) {
// // 使用lock关键字确保在修改PipelineState时的线程安全
// // 将PipelineState更新为PipelineState和s中较大的那个值
// PipelineState = (PathState)System.Math.Max((int)PipelineState, (int)s);
// }
PipelineState = (PathState)Math.Max((int)PipelineState, (int)s);
}
/// <summary>
/// 减少路径的引用计数(对象池技术)。
/// 移除指定对象对路径的声明。
/// 当引用计数达到零时,路径将被放回对象池,所有变量将被清除,以便再次使用。
/// 这对于性能提升很有帮助,因为减少了对象的分配。
///
/// 如果silent参数为true则此方法将仅移除指定对象的声明
/// 但如果声明计数达到零路径也不会被放回对象池除非之前已经有一个非silent的Release调用。
/// 这被内部路径查找组件如Seeker和AstarPath所使用以便它们不会导致路径被放回对象池
/// 从而避免在用户跳过声明/释放调用时产生奇怪的bug。
///
/// 参见Claim
/// 参见PathPool
/// </summary>
/// <param name="o">要释放路径声明的对象。</param>
/// <param name="silent">是否静默释放默认为false。</param>
public void Release (Object o, bool silent = false) {
// 检查传入的对象是否为null如果是则抛出异常
if (o == null) throw new ArgumentNullException("o");
// 遍历当前所有已声明的对象
for (int i = 0; i < claimed.Count; i++) {
// 使用ReferenceEquals是因为这个方法可能会从另一个线程被调用
// 如果当前遍历到的对象与传入的对象是同一个对象(引用相同)
if (ReferenceEquals(claimed[i], o)) {
// 从已声明列表中移除该对象
claimed.RemoveAt(i);
// 如果不是静默释放,则标记为非静默已释放
if (!silent) {
releasedNotSilent = true;
}
// 如果已声明列表为空且之前有过非静默释放,则将当前路径对象放回对象池
if (claimed.Count == 0 && releasedNotSilent) {
// PathPool.Pool(this);
}
// 找到并处理完目标对象后,提前返回
return;
}
}
}
public bool Pooled { get; set; }
public void OnEnterPool()
{
throw new NotImplementedException();
}
public void Reset()
{
throw new NotImplementedException();
}
public void ReturnPath()
{
throw new NotImplementedException();
}
public void Prepare()
{
throw new NotImplementedException();
}
public void Initialize()
{
throw new NotImplementedException();
}
public void Cleanup()
{
throw new NotImplementedException();
}
public void CalculateStep(long targetTick)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 37fceeaba5504147a7966ad31bbdf3bf
timeCreated: 1707189234

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 31a221f8276442f69b19987aed2f148f
timeCreated: 1707205010

View File

@@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace Plugins.JNGame.Sync.Frame.AStar.Processor
{
public class PathProcessor
{
// public Queue<JNPath> queue = new Queue<JNPath>();
public LinkedList<JNPath> queue = new LinkedList<JNPath>();
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1a93001ec12e445c9fd37c274dec33ed
timeCreated: 1707205024

View File

@@ -0,0 +1,369 @@
using UnityEngine;
namespace Game.Plugins.JNGame.Sync.Frame.AstarPath.RVO
{
/// <summary>
/// RVO角色控制器 用于处理基于RVO算法的局部避让允许开发者在复杂的动态环境中控制角色的移动同时避免与其他角色发生碰撞。。
/// 与Unity的CharacterController类似。它处理移动计算并考虑其他代理。
/// 它本身不处理移动,但允许调用脚本获取计算出的速度,
/// 并使用适合的方法移动对象例如使用CharacterController使用Point = transform.position + transform.forward * 100;
///
/// // 使用期望的速度10和最大速度12设置要移动到的目标点
/// controller.SetTarget(targetPoint, 10, 12);
///
/// // 计算这一帧中要移动的距离
/// // 此信息基于之前帧的移动命令
/// // 因为局部避让是由RVOSimulator组件定期全局计算的
/// var delta = controller.CalculateMovementDelta(transform.position, Time.deltaTime);
/// transform.position = transform.position + delta;
/// }
///
/// 关于这个类中许多变量的文档请参阅Pathfinding.RVO.IAgent接口。
///
/// 注意场景中需要一个RVOSimulator组件
///
/// 请参阅Pathfinding.RVO.IAgent
/// 请参阅RVOSimulator
/// 请参阅local-avoidance在在线文档中查看工作链接
/// </summary>
public class JNRVOController
{
// 内部浮点型字段用于存储半径值初始化为0.5
internal float radiusBackingField = 0.5f;
// 浮点型字段用于存储高度值初始化为2
float heightBackingField = 2;
// 浮点型字段用于存储中心点值初始化为1
float centerBackingField = 1;
/// <summary>
/// 代理在世界单位中的半径。
/// 注意如果同一个GameObject上附加了移动脚本AIPath/RichAI/AILerp任何实现IAstarAI接口的内容则该值将由该脚本驱动。
/// </summary>
// 定义一个公共的浮点型属性radius用于表示代理在世界单位中的半径。
// 当一个GameObject同时附加了移动脚本实现了IAstarAI接口的脚本radius的值将由该脚本决定。
public float radius {
get => radiusBackingField;
set => radiusBackingField = value;
}
/// <summary>
/// 代理在世界单位中的高度。
/// </summary>
public float height
{
get => heightBackingField;
set => heightBackingField = value;
}
/// <summary>
/// 代理相对于此游戏对象枢转点的中心位置。
/// </summary>
public float center
{
get => centerBackingField;
// 如果有 AI 脚本附加,该值将始终被驱动为 height/2因为移动脚本期望对象的位置在其脚下
// 如果没有 AI 脚本附加,则返回 centerBackingField 的值
set => centerBackingField = value;
}
/// <summary>内部代理的引用</summary>
public JNIAgent rvoAgent { get; private set; }
/// <summary>缓存的对象</summary>
protected GameObject obj;
///避障核心类
private JNRVOSimulator Simulator;
/// <summary>
/// 在未来多少秒内寻找与其他代理的碰撞(以秒为单位)。这个变量表示在预测未来的移动路径时,应该考虑多少秒内的碰撞检测。
/// </summary>
public float agentTimeHorizon = 2;
/// <summary>
/// 在未来多少秒内寻找与障碍物的碰撞(以秒为单位)。 这个变量表示在预测未来的移动路径时,应该考虑多少秒内的与障碍物的碰撞检测。
/// </summary>
public float obstacleTimeHorizon = 2;
/// <summary>
/// 一个被锁定的单位不能移动。其他单位仍然会避开它,但避开的质量不是最好的。
/// </summary>
public bool locked = false;
/// <summary>
/// 考虑在内的其他代理的最大数量。
/// 较小的值可以减少CPU负载而较高的值可以提高局部避让的质量。
/// 如果将maxNeighbours设置得较小可以减少代理需要计算的碰撞避免逻辑从而减轻CPU的负担。
/// 如果将maxNeighbours设置得较高代理将考虑更多的其他代理这可能会导致更精确的避让行为但也会增加CPU的负载。
/// </summary>
public int maxNeighbours = 10;
/// <summary>DEBUG 绘制</summary>
public bool debug;
/// <summary>
/// 指定此代理的避让层。
/// 其他代理的<see cref="collidesWith"/>掩码将确定它们是否会避开此代理。
/// </summary>
public JNRVOLayer layer = JNRVOLayer.DefaultAgent;
/// <summary>
/// 一个层掩码,指定此代理将避免哪些层。
/// 你可以这样设置它CollidesWith = RVOLayer.DefaultAgent | RVOLayer.Layer3 | RVOLayer.Layer6 ...
/// 在具有多个团队的游戏中,这非常有用。例如,你通常希望一个团队中的代理避免彼此,但你不希望他们避免敌人。
/// 此字段仅影响此代理将避免哪些其他代理,它不影响其他代理如何对此代理做出反应。
/// 参见:位掩码(在线文档中查看相关链接)
/// 参见http://en.wikipedia.org/wiki/Mask_(computing)
/// </summary>
public JNRVOLayer collidesWith = (JNRVOLayer)(-1);
/// <summary>
/// 使用Tooltip特性为属性提供工具提示说明其他代理将如何强烈地避免此代理。
/// 使用UnityEngine.Range特性限制priority属性的值范围在0到1之间。
/// 这个变量表示其他代理在避让此代理时的优先级或强度。值越高,其他代理将更强烈地避免与此代理碰撞。
/// </summary>
public float priority = 0.5f;
/// <summary>
/// 当期望的速度接近于零时,自动将 <see cref="locked"/> 设置为 true。
/// 这可以防止其他单位在它们应该执行例如阻塞瓶颈等操作时将它们推开。
///
/// 当此属性为 true 时,每次调用 <see cref="SetTarget"/> 或 <see cref="Move"/> 方法时,
/// 如果期望的速度不为零,则将 <see cref="locked"/> 字段设置为 true否则设置为 false。
/// </summary>
public bool lockWhenNotMoving = false;
/// <summary>
/// 将3D向量转换为运动平面上的2D向量。
/// 如果运动平面是XZ则将其投影到XZ平面上
/// 否则将其投影到XY平面上。
/// </summary>
/// <param name="p">要转换的3D向量</param>
/// <returns>转换后的2D向量</returns>
public Vector2 To2D(Vector3 p) {
float dummy; // 定义一个临时变量dummy用于接收不需要的输出值
// 调用重载的To2D方法将3D向量p和dummy作为参数传递并返回转换后的2D向量
return To2D(p, out dummy);
}
/// <summary>
/// 将运动平面上的二维向量及其高度转换为三维坐标。
/// 参见To2D 方法(用于将三维坐标转换为二维向量和高度)
/// 参见movementPlane 属性(定义代理的运动平面)
/// </summary>
/// <param name="p">二维向量,表示运动平面上的坐标。</param>
/// <param name="elevationCoordinate">高度坐标,根据运动平面的不同,它可能表示不同的轴。</param>
/// <returns>返回转换后的三维坐标。</returns>
public Vector3 To3D(Vector2 p, float elevationCoordinate)
{
// 根据 movementPlane 的值,将二维向量和高度转换为三维坐标
if (movementPlane == MovementPlane.XY)
{
// 如果运动平面是 XY 平面,则 Z 轴表示高度(但此处使用负值,可能是根据具体应用场景而定)
return new Vector3(p.x, p.y, -elevationCoordinate);
}
else
{
// 如果运动平面不是 XY 平面(可能是 XZ 平面),则 Y 轴表示高度
return new Vector3(p.x, elevationCoordinate, p.y);
}
}
/// <summary>
/// 将3D向量转换为运动平面上的2D向量。
/// 如果运动平面是XZ则将其投影到XZ平面上并将高度坐标设置为Y坐标
/// 否则将其投影到XY平面上并将高度设置为Z坐标。
/// </summary>
/// <param name="p">要转换的3D向量</param>
/// <param name="elevation">转换后的高度坐标</param>
/// <returns>转换后的2D向量</returns>
public Vector2 To2D(Vector3 p, out float elevation) {
// 判断当前的运动平面
if (movementPlane == MovementPlane.XY) {
// 如果运动平面是XY则将Y坐标作为高度并返回X和Y组成的2D向量
elevation = -p.z; // 高度设置为-Z坐标通常用于表示高度或海拔
return new Vector2(p.x, p.y); // 返回X和Y组成的2D向量
} else {
// 如果运动平面不是XY即XZ则将Y坐标作为高度并返回X和Z组成的2D向量
elevation = p.y; // 高度设置为Y坐标
return new Vector2(p.x, p.z); // 返回X和Z组成的2D向量
}
}
/// <summary>
/// 确定是否使用XY2D或XZ3D平面进行移动
/// </summary>
public MovementPlane movementPlane {
get => Simulator.movementPlane;
}
public JNRVOController(JNRVOSimulator Simulator,GameObject obj)
{
this.obj = obj;
this.Simulator = Simulator;
// 如果是这样的话,我们可以简单地将它再次添加到模拟中
if (rvoAgent != null) {
// 我们将其再次添加到模拟器中
Simulator.AddAgent(rvoAgent);
} else {
// 如果 rvoAgent 为空,表示之前没有实例或者实例已被销毁
// 我们创建一个新的代理实例并添加到模拟器中
// 使用初始位置 Vector2.zero通常代表坐标(0,0))和初始方向 0通常代表面向上来创建新的代理
rvoAgent = Simulator.AddAgent(Vector2.zero, 0);
// 设置 rvoAgent 的 PreCalculationCallback 为 UpdateAgentProperties 方法
// 这意味着在每次模拟计算之前,都会调用 UpdateAgentProperties 方法来更新代理的属性
rvoAgent.PreCalculationCallback = UpdateAgentProperties;
}
}
/// <summary>
/// 更新代理的属性
/// </summary>
protected void UpdateAgentProperties()
{
// 获取当前代理的本地缩放比例
var scale = obj.transform.localScale;
// 设置代理的半径至少为0.001通常根据半径与X轴缩放比例的乘积来确定
rvoAgent.Radius = Mathf.Max(0.001f, radius * scale.x);
// 设置代理的时间预测范围
rvoAgent.AgentTimeHorizon = agentTimeHorizon;
// 设置代理与障碍物的时间预测范围
rvoAgent.ObstacleTimeHorizon = obstacleTimeHorizon;
// 锁定代理,使其不参与模拟计算
rvoAgent.Locked = locked;
// 设置代理的最大邻居数量
rvoAgent.MaxNeighbours = maxNeighbours;
// 是否开启代理的调试绘制
rvoAgent.DebugDraw = debug;
// 设置代理所在的层级
rvoAgent.Layer = layer;
// 设置代理与哪些类型的代理发生碰撞
rvoAgent.CollidesWith = collidesWith;
// 设置代理的优先级
rvoAgent.Priority = priority;
float elevation;
// 如果存在移动脚本,则使用移动脚本的位置作为代理的位置
// 因为移动脚本的位置可能与Transform组件的位置不同特别是在IAstarAI.updatePosition为false时
rvoAgent.Position = To2D(obj.transform.position, out elevation);
// 根据移动平面设置代理的高度和高度坐标
if (movementPlane == MovementPlane.XZ)
{
// 如果移动平面是XZ平面则设置代理的高度为缩放后的高度值
rvoAgent.Height = height * scale.y;
// 设置代理的高度坐标,根据中心点和高度计算得出
rvoAgent.ElevationCoordinate = elevation + (center - 0.5f * height) * scale.y;
}
else
{
// 如果移动平面不是XZ平面则将代理的高度设置为1高度坐标设置为0
rvoAgent.Height = 1;
rvoAgent.ElevationCoordinate = 0;
}
}
/// <summary>
/// 为代理设置移动的目标点。
/// 与 <see cref="Move"/> 方法类似,但更加灵活。
/// 在路径的末尾使用此方法更好,因为使用 Move 方法时,代理不知道应该在何处停止,因此可能会超过目标点。
/// 使用此方法时,代理不会超过目标点。
/// 代理将假设当它到达目标点时会停止,因此请确保如果你只是想朝特定方向移动,则不要将点设置得太靠近代理。
///
/// 目标点将保持不变,直到请求其他点(而不是每帧重置)。
///
/// 请参阅:也请查看 <see cref="IAgent.SetTarget"/> 的文档,其中有更多详细信息。
/// 请参阅:<see cref="Move"/>
/// </summary>
/// <param name="pos">要移动到的世界空间中的点。</param>
/// <param name="speed">每秒在世界单位中期望的速度。</param>
/// <param name="maxSpeed">每秒在世界单位中的最大速度。
/// 如果必要,代理将使用此速度以避免与其他代理发生碰撞。
/// 应该至少与 speed 一样高,但建议使用比 speed 略高的值(例如 speed*1.2)。</param>
public void SetTarget(Vector3 pos, float speed, float maxSpeed) {
// 将 3D 位置转换为 2D 位置,并设置给 rvoAgent
rvoAgent.SetTarget(To2D(pos), speed, maxSpeed);
// 如果 lockWhenNotMoving 为 true则根据速度是否低于某个阈值来锁定代理
if (lockWhenNotMoving) {
locked = speed < 0.001f;
}
}
/// <summary>
/// 设置代理的期望速度。
/// 请注意,这是一个速度(单位/秒),而不是移动增量(单位/帧)。
///
/// 此速度将保持不变,直到请求其他值(而不是每帧重置)。
///
/// 注意:在大多数情况下,使用 SetTarget 方法是更好的选择。
/// 实际上,此方法将调用 SetTarget 并传递(位置 + 速度)作为目标点。
/// 请参阅 IAgent.SetTarget 文档中的说明,了解这可能导致的潜在问题(特别是可能很难让代理在精确的点停止)。
/// </summary>
public void Move(Vector3 vel) {
// 将 3D 速度转换为 2D 速度
var velocity2D = To2D(vel);
// 获取速度的大小(即速度的模)
var speed = velocity2D.magnitude;
// 调用 rvoAgent 的 SetTarget 方法,设置目标点为当前位置(如果 ai 不为 null则使用 ai.position否则使用 tr.position加上 2D 速度
// 最大速度和期望速度都设置为 speed
rvoAgent.SetTarget(To2D(obj.transform.position) + velocity2D, speed, speed);
// 如果 lockWhenNotMoving 为 true则根据速度是否低于 0.001f 来设置 locked 标志
if (lockWhenNotMoving) {
locked = speed < 0.001f;
}
}
/// <summary>
/// 计算单个帧内为避免障碍物而移动的方向和距离。
///
/// 代理的位置取自附加的移动脚本的位置(参见 <see cref="Pathfinding.IAstarAI.position"/>),如果没有附加,则取自 transform.position。
/// </summary>
/// <param name="deltaTime">移动的时间长度[秒]。
/// 通常设置为 Time.deltaTime。</param>
/// <returns>返回一个 Vector3 类型的值,表示代理在单个帧内为避免障碍物而移动的方向和距离。</returns>
public Vector3 CalculateMovementDelta(float deltaTime)
{
// 如果 rvoAgent 为空,则返回零向量,即代理不进行移动
if (rvoAgent == null) return Vector3.zero;
// 计算目标点和代理当前位置之间的二维向量差
Vector2 movementVector = rvoAgent.CalculatedTargetPoint - To2D(obj.transform.position);
// 限制向量的幅度(即长度),确保不超过代理在给定时间内可以移动的最大距离
Vector2 clampedVector = Vector2.ClampMagnitude(movementVector, rvoAgent.CalculatedSpeed * deltaTime);
// 将二维向量转换为三维向量,其中 Z 分量为 0
Vector3 movementDelta = To3D(clampedVector, 0);
// 返回计算出的移动方向和距离
return movementDelta;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3f94234bdfb64d4aa45c5a19466414e7
timeCreated: 1707210242

View File

@@ -306,7 +306,7 @@ namespace Game.Plugins.JNGame.Sync.Frame.AstarPath.RVO
/// Inverse desired simulation fps.
/// See: DesiredDeltaTime
/// </summary>
private float desiredDeltaTime = 0.05f;
private float desiredDeltaTime = 0.03f;
/// <summary>
/// Time in seconds between each simulation step.