diff options
Diffstat (limited to 'Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules')
10 files changed, 765 insertions, 0 deletions
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/GridGraphRules.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/GridGraphRules.cs new file mode 100644 index 0000000..174b7d2 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/GridGraphRules.cs @@ -0,0 +1,293 @@ +using System.Collections.Generic; + +namespace Pathfinding.Graphs.Grid.Rules { + using Pathfinding.Serialization; + using Pathfinding.Jobs; + using Unity.Jobs; + using Unity.Collections; + using Unity.Mathematics; + + public class CustomGridGraphRuleEditorAttribute : System.Attribute { + public System.Type type; + public string name; + public CustomGridGraphRuleEditorAttribute(System.Type type, string name) { + this.type = type; + this.name = name; + } + } + + /// <summary> + /// Container for all rules in a grid graph. + /// + /// <code> + /// // Get the first grid graph in the scene + /// var gridGraph = AstarPath.active.data.gridGraph; + /// + /// gridGraph.rules.AddRule(new Pathfinding.Graphs.Grid.Rules.RuleAnglePenalty { + /// penaltyScale = 10000, + /// curve = AnimationCurve.Linear(0, 0, 90, 1), + /// }); + /// </code> + /// + /// See: <see cref="Pathfinding.GridGraph.rules"/> + /// See: grid-rules (view in online documentation for working links) + /// </summary> + [JsonOptIn] + public class GridGraphRules { + List<System.Action<Context> >[] jobSystemCallbacks; + List<System.Action<Context> >[] mainThreadCallbacks; + + /// <summary>List of all rules</summary> + [JsonMember] + List<GridGraphRule> rules = new List<GridGraphRule>(); + + long lastHash; + + /// <summary>Context for when scanning or updating a graph</summary> + public class Context { + /// <summary>Graph which is being scanned or updated</summary> + public GridGraph graph; + /// <summary>Data for all the nodes as NativeArrays</summary> + public GridGraphScanData data; + /// <summary> + /// Tracks dependencies between jobs to allow parallelism without tediously specifying dependencies manually. + /// Always use when scheduling jobs. + /// </summary> + public JobDependencyTracker tracker => data.dependencyTracker; + } + + public void AddRule (GridGraphRule rule) { + rules.Add(rule); + lastHash = -1; + } + + public void RemoveRule (GridGraphRule rule) { + rules.Remove(rule); + lastHash = -1; + } + + public IReadOnlyList<GridGraphRule> GetRules () { + if (rules == null) rules = new List<GridGraphRule>(); + return rules.AsReadOnly(); + } + + long Hash () { + long hash = 196613; + + for (int i = 0; i < rules.Count; i++) { + if (rules[i] != null && rules[i].enabled) hash = hash * 1572869 ^ (long)rules[i].Hash; + } + return hash; + } + + public void RebuildIfNecessary () { + var newHash = Hash(); + + if (newHash == lastHash && jobSystemCallbacks != null && mainThreadCallbacks != null) return; + lastHash = newHash; + Rebuild(); + } + + public void Rebuild () { + rules = rules ?? new List<GridGraphRule>(); + jobSystemCallbacks = jobSystemCallbacks ?? new List<System.Action<Context> >[6]; + for (int i = 0; i < jobSystemCallbacks.Length; i++) { + if (jobSystemCallbacks[i] != null) jobSystemCallbacks[i].Clear(); + } + mainThreadCallbacks = mainThreadCallbacks ?? new List<System.Action<Context> >[6]; + for (int i = 0; i < mainThreadCallbacks.Length; i++) { + if (mainThreadCallbacks[i] != null) mainThreadCallbacks[i].Clear(); + } + for (int i = 0; i < rules.Count; i++) { + if (rules[i].enabled) rules[i].Register(this); + } + } + + public void DisposeUnmanagedData () { + if (rules != null) { + for (int i = 0; i < rules.Count; i++) { + if (rules[i] != null) { + rules[i].DisposeUnmanagedData(); + rules[i].SetDirty(); + } + } + } + } + + static void CallActions (List<System.Action<Context> > actions, Context context) { + if (actions != null) { + try { + for (int i = 0; i < actions.Count; i++) actions[i](context); + } catch (System.Exception e) { + UnityEngine.Debug.LogException(e); + } + } + } + + /// <summary> + /// Executes the rules for the given pass. + /// Call handle.Complete on, or wait for, all yielded job handles. + /// </summary> + public IEnumerator<JobHandle> ExecuteRule (GridGraphRule.Pass rule, Context context) { + if (jobSystemCallbacks == null) Rebuild(); + CallActions(jobSystemCallbacks[(int)rule], context); + + if (mainThreadCallbacks[(int)rule] != null && mainThreadCallbacks[(int)rule].Count > 0) { + if (!context.tracker.forceLinearDependencies) yield return context.tracker.AllWritesDependency; + CallActions(mainThreadCallbacks[(int)rule], context); + } + } + + public void ExecuteRuleMainThread (GridGraphRule.Pass rule, Context context) { + if (jobSystemCallbacks[(int)rule] != null && jobSystemCallbacks[(int)rule].Count > 0) throw new System.Exception("A job system pass has been added for the " + rule + " pass. " + rule + " only supports main thread callbacks."); + if (context.tracker != null) context.tracker.AllWritesDependency.Complete(); + CallActions(mainThreadCallbacks[(int)rule], context); + } + + /// <summary> + /// Adds a pass callback that uses the job system. + /// This rule should only schedule jobs using the `Context.tracker` dependency tracker. Data is not safe to access directly in the callback + /// + /// This method should only be called from rules in their Register method. + /// </summary> + public void AddJobSystemPass (GridGraphRule.Pass pass, System.Action<Context> action) { + var index = (int)pass; + + if (jobSystemCallbacks[index] == null) { + jobSystemCallbacks[index] = new List<System.Action<Context> >(); + } + jobSystemCallbacks[index].Add(action); + } + + /// <summary> + /// Adds a pass callback that runs in the main thread. + /// The callback may access and modify any data in the context. + /// You do not need to schedule jobs in order to access the data. + /// + /// Warning: Not all data in the Context is valid for every pass. For example you cannot access node connections in the BeforeConnections pass + /// since they haven't been calculated yet. + /// + /// This is a bit slower than <see cref="AddJobSystemPass"/> since parallelism and the burst compiler cannot be used. + /// But if you need to use non-thread-safe APIs or data then this is a good choice. + /// + /// This method should only be called from rules in their Register method. + /// </summary> + public void AddMainThreadPass (GridGraphRule.Pass pass, System.Action<Context> action) { + var index = (int)pass; + + if (mainThreadCallbacks[index] == null) { + mainThreadCallbacks[index] = new List<System.Action<Context> >(); + } + mainThreadCallbacks[index].Add(action); + } + + /// <summary>Deprecated: Use AddJobSystemPass or AddMainThreadPass instead</summary> + [System.Obsolete("Use AddJobSystemPass or AddMainThreadPass instead")] + public void Add (GridGraphRule.Pass pass, System.Action<Context> action) { + AddJobSystemPass(pass, action); + } + } + + /// <summary> + /// Custom rule for a grid graph. + /// See: <see cref="GridGraphRules"/> + /// See: grid-rules (view in online documentation for working links) + /// </summary> + [JsonDynamicType] + // Compatibility with old versions + [JsonDynamicTypeAlias("Pathfinding.RuleTexture", typeof(RuleTexture))] + [JsonDynamicTypeAlias("Pathfinding.RuleAnglePenalty", typeof(RuleAnglePenalty))] + [JsonDynamicTypeAlias("Pathfinding.RuleElevationPenalty", typeof(RuleElevationPenalty))] + [JsonDynamicTypeAlias("Pathfinding.RulePerLayerModifications", typeof(RulePerLayerModifications))] + public abstract class GridGraphRule { + /// <summary>Only enabled rules are executed</summary> + [JsonMember] + public bool enabled = true; + int dirty = 1; + + /// <summary> + /// Where in the scanning process a rule will be executed. + /// Check the documentation for <see cref="GridGraphScanData"/> to see which data fields are valid in which passes. + /// </summary> + public enum Pass { + /// <summary> + /// Before the collision testing phase but after height testing. + /// This is very early. Most data is not valid by this point. + /// + /// You can use this if you want to modify the node positions and still have it picked up by the collision testing code. + /// </summary> + BeforeCollision, + /// <summary> + /// Before connections are calculated. + /// At this point height testing and collision testing has been done (if they are enabled). + /// + /// This is the most common pass to use. + /// If you are modifying walkability here then connections and erosion will be calculated properly. + /// </summary> + BeforeConnections, + /// <summary> + /// After connections are calculated. + /// + /// If you are modifying connections directly you should do that in this pass. + /// + /// Note: If erosion is used then this pass will be executed twice. One time before erosion and one time after erosion + /// when the connections are calculated again. + /// </summary> + AfterConnections, + /// <summary> + /// After erosion is calculated but before connections have been recalculated. + /// + /// If no erosion is used then this pass will not be executed. + /// </summary> + AfterErosion, + /// <summary> + /// After everything else. + /// This pass is executed after everything else is done. + /// You should not modify walkability in this pass because then the node connections will not be up to date. + /// </summary> + PostProcess, + /// <summary> + /// After the graph update has been applied to the graph. + /// + /// This pass can only be added as a main-thread pass. + /// + /// Warning: No native data in the context is valid at this point. It has all been disposed. + /// You cannot modify any data in this pass. + /// </summary> + AfterApplied, + } + + /// <summary> + /// Hash of the settings for this rule. + /// The <see cref="Register"/> method will be called again whenever the hash changes. + /// If the hash does not change it is assumed that the <see cref="Register"/> method does not need to be called again. + /// </summary> + public virtual int Hash => dirty; + + /// <summary> + /// Call if you have changed any setting of the rule. + /// This will ensure that any cached data the rule uses is rebuilt. + /// If you do not do this then any settings changes may not affect the graph when it is rescanned or updated. + /// + /// The purpose of this method call is to cause the <see cref="Hash"/> property to change. If your custom rule overrides the Hash property to + /// return a hash of some settings, then you do not need to call this method for the changes the hash function already accounts for. + /// </summary> + public virtual void SetDirty () { + dirty++; + } + + /// <summary> + /// Called when the rule is removed or the graph is destroyed. + /// Use this to e.g. clean up any NativeArrays that the rule uses. + /// + /// Note: The rule should remain valid after this method has been called. + /// However the <see cref="Register"/> method is guaranteed to be called before the rule is executed again. + /// </summary> + public virtual void DisposeUnmanagedData () { + } + + /// <summary>Does preprocessing and adds callbacks to the <see cref="GridGraphRules"/> object</summary> + public virtual void Register (GridGraphRules rules) { + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/GridGraphRules.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/GridGraphRules.cs.meta new file mode 100644 index 0000000..a73e50c --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/GridGraphRules.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4d90c9a7bca49464796933f43b5506fc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RuleAnglePenalty.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RuleAnglePenalty.cs new file mode 100644 index 0000000..1322770 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RuleAnglePenalty.cs @@ -0,0 +1,81 @@ +namespace Pathfinding.Graphs.Grid.Rules { + using Pathfinding.Jobs; + using Unity.Jobs; + using Unity.Collections; + using Unity.Burst; + using UnityEngine; + using Unity.Mathematics; + + /// <summary> + /// Applies penalty based on the slope of the surface below the node. + /// + /// This is useful if you for example want to discourage agents from walking on steep slopes. + /// + /// The penalty applied is equivalent to: + /// + /// <code> + /// penalty = curve.evaluate(slope angle in degrees) * penaltyScale + /// </code> + /// + /// [Open online documentation to see images] + /// + /// See: grid-rules (view in online documentation for working links) + /// </summary> + [Pathfinding.Util.Preserve] + public class RuleAnglePenalty : GridGraphRule { + public float penaltyScale = 10000; + public AnimationCurve curve = AnimationCurve.Linear(0, 0, 90, 1); + NativeArray<float> angleToPenalty; + + public override void Register (GridGraphRules rules) { + if (!angleToPenalty.IsCreated) angleToPenalty = new NativeArray<float>(32, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); + for (int i = 0; i < angleToPenalty.Length; i++) { + angleToPenalty[i] = Mathf.Max(0, curve.Evaluate(90.0f * i / (angleToPenalty.Length - 1)) * penaltyScale); + } + + rules.AddJobSystemPass(Pass.BeforeConnections, context => { + new JobPenaltyAngle { + angleToPenalty = angleToPenalty, + up = context.data.up, + nodeNormals = context.data.nodes.normals, + penalty = context.data.nodes.penalties, + }.Schedule(context.tracker); + }); + } + + public override void DisposeUnmanagedData () { + if (angleToPenalty.IsCreated) angleToPenalty.Dispose(); + } + + [BurstCompile(FloatMode = FloatMode.Fast)] + public struct JobPenaltyAngle : IJob { + public Vector3 up; + + [ReadOnly] + public NativeArray<float> angleToPenalty; + + [ReadOnly] + public NativeArray<float4> nodeNormals; + + public NativeArray<uint> penalty; + + public void Execute () { + float4 up = new float4(this.up.x, this.up.y, this.up.z, 0); + + for (int i = 0; i < penalty.Length; i++) { + float4 normal = nodeNormals[i]; + if (math.any(normal)) { + float angle = math.acos(math.dot(normal, up)); + // Take the dot product to find out the cosinus of the angle it has + // Add penalty based on the angle from a precalculated array + float x = angle*(angleToPenalty.Length - 1)/math.PI; + int ix = (int)x; + float p1 = angleToPenalty[math.max(ix, 0)]; + float p2 = angleToPenalty[math.min(ix + 1, angleToPenalty.Length - 1)]; + penalty[i] += (uint)math.lerp(p1, p2, x - ix); + } + } + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RuleAnglePenalty.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RuleAnglePenalty.cs.meta new file mode 100644 index 0000000..d047088 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RuleAnglePenalty.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 40d5c4aeb2276457f8fe040e4c5d71fe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RuleElevationPenalty.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RuleElevationPenalty.cs new file mode 100644 index 0000000..6f62660 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RuleElevationPenalty.cs @@ -0,0 +1,76 @@ +namespace Pathfinding.Graphs.Grid.Rules { + using Pathfinding.Jobs; + using Unity.Jobs; + using Unity.Collections; + using Unity.Burst; + using UnityEngine; + using Unity.Mathematics; + + /// <summary> + /// Applies penalty based on the elevation of the node. + /// + /// This is useful if you for example want to discourage agents from walking high up in mountain regions. + /// + /// The penalty applied is equivalent to: + /// + /// <code> + /// penalty = curve.evaluate(Mathf.Clamp01(Mathf.InverseLerp(lower elevation range, upper elevation range, elevation))) * penaltyScale + /// </code> + /// + /// [Open online documentation to see images] + /// + /// See: grid-rules (view in online documentation for working links) + /// </summary> + [Pathfinding.Util.Preserve] + public class RuleElevationPenalty : GridGraphRule { + public float penaltyScale = 10000; + public Vector2 elevationRange = new Vector2(0, 100); + public AnimationCurve curve = AnimationCurve.Linear(0, 0, 1, 1); + NativeArray<float> elevationToPenalty; + + public override void Register (GridGraphRules rules) { + if (!elevationToPenalty.IsCreated) elevationToPenalty = new NativeArray<float>(64, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); + for (int i = 0; i < elevationToPenalty.Length; i++) { + elevationToPenalty[i] = Mathf.Max(0, penaltyScale * curve.Evaluate(i * 1.0f / (elevationToPenalty.Length - 1))); + } + + var clampedElevationRange = new Vector2(math.max(0, elevationRange.x), math.max(1, elevationRange.y)); + rules.AddJobSystemPass(Pass.BeforeConnections, context => { + //var elevationRangeScale = Matrix4x4.TRS(new Vector3(0, -clampedElevationRange.x, 0), Quaternion.identity, new Vector3(1, 1/(clampedElevationRange.y - clampedElevationRange.x), 1)); + var elevationRangeScale = Matrix4x4.Scale(new Vector3(1, 1/(clampedElevationRange.y - clampedElevationRange.x), 1)) * Matrix4x4.Translate(new Vector3(0, -clampedElevationRange.x, 0)); + new JobElevationPenalty { + elevationToPenalty = elevationToPenalty, + nodePositions = context.data.nodes.positions, + worldToGraph = elevationRangeScale * context.data.transform.matrix.inverse, + penalty = context.data.nodes.penalties, + }.Schedule(context.tracker); + }); + } + + public override void DisposeUnmanagedData () { + if (elevationToPenalty.IsCreated) elevationToPenalty.Dispose(); + } + + [BurstCompile(FloatMode = FloatMode.Fast)] + public struct JobElevationPenalty : IJob { + [ReadOnly] + public NativeArray<float> elevationToPenalty; + + [ReadOnly] + public NativeArray<Vector3> nodePositions; + + public Matrix4x4 worldToGraph; + public NativeArray<uint> penalty; + + public void Execute () { + for (int i = 0; i < penalty.Length; i++) { + float y = math.clamp(worldToGraph.MultiplyPoint3x4(nodePositions[i]).y, 0, 1) * (elevationToPenalty.Length - 1); + int iy = (int)y; + float p1 = elevationToPenalty[iy]; + float p2 = elevationToPenalty[math.min(iy + 1, elevationToPenalty.Length - 1)]; + penalty[i] += (uint)math.lerp(p1, p2, y - iy); + } + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RuleElevationPenalty.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RuleElevationPenalty.cs.meta new file mode 100644 index 0000000..5878475 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RuleElevationPenalty.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d2933158d922e49e39a332d795d26d68 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RulePerLayerModifications.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RulePerLayerModifications.cs new file mode 100644 index 0000000..1684b04 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RulePerLayerModifications.cs @@ -0,0 +1,79 @@ +using Pathfinding.Jobs; + +namespace Pathfinding.Graphs.Grid.Rules { + /// <summary> + /// Modifies nodes based on the layer of the surface under the node. + /// + /// You can for example make all surfaces with a specific layer make the nodes get a specific tag. + /// + /// [Open online documentation to see images] + /// + /// See: grid-rules (view in online documentation for working links) + /// </summary> + [Pathfinding.Util.Preserve] + public class RulePerLayerModifications : GridGraphRule { + public PerLayerRule[] layerRules = new PerLayerRule[0]; + const int SetTagBit = 1 << 30; + + public struct PerLayerRule { + /// <summary>Layer this rule applies to</summary> + public int layer; + /// <summary>The action to apply to matching nodes</summary> + public RuleAction action; + /// <summary> + /// Tag for the RuleAction.SetTag action. + /// Must be between 0 and <see cref="Pathfinding.GraphNode.MaxTagIndex"/> + /// </summary> + public int tag; + } + + public enum RuleAction { + /// <summary>Sets the tag of all affected nodes to <see cref="PerLayerRule.tag"/></summary> + SetTag, + /// <summary>Makes all affected nodes unwalkable</summary> + MakeUnwalkable, + } + + public override void Register (GridGraphRules rules) { + int[] layerToTag = new int[32]; + bool[] layerToUnwalkable = new bool[32]; + for (int i = 0; i < layerRules.Length; i++) { + var rule = layerRules[i]; + if (rule.action == RuleAction.SetTag) { + layerToTag[rule.layer] = SetTagBit | rule.tag; + } else { + layerToUnwalkable[rule.layer] = true; + } + } + + rules.AddMainThreadPass(Pass.BeforeConnections, context => { + if (!context.data.heightHits.IsCreated) { + UnityEngine.Debug.LogError("RulePerLayerModifications requires height testing to be enabled on the grid graph", context.graph.active); + return; + } + + var raycastHits = context.data.heightHits; + var nodeWalkable = context.data.nodes.walkable; + var nodeTags = context.data.nodes.tags; + var slice = new Slice3D(context.data.nodes.bounds, context.data.heightHitsBounds); + var size = slice.slice.size; + for (int y = 0; y < size.y; y++) { + for (int z = 0; z < size.z; z++) { + var rowOffset = y * size.x * size.z + z * size.x; + for (int x = 0; x < size.x; x++) { + var innerIndex = rowOffset + x; + var outerIndex = slice.InnerCoordinateToOuterIndex(x, y, z); + var coll = raycastHits[innerIndex].collider; + if (coll != null) { + var layer = coll.gameObject.layer; + if (layerToUnwalkable[layer]) nodeWalkable[outerIndex] = false; + var tag = layerToTag[layer]; + if ((tag & SetTagBit) != 0) nodeTags[outerIndex] = tag & 0xFF; + } + } + } + } + }); + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RulePerLayerModifications.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RulePerLayerModifications.cs.meta new file mode 100644 index 0000000..bfa3859 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RulePerLayerModifications.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4869e55551c0e4e1abaaf19bcc3d44a1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RuleTexture.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RuleTexture.cs new file mode 100644 index 0000000..ddc6a9c --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RuleTexture.cs @@ -0,0 +1,181 @@ +using UnityEngine; +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; +using Unity.Mathematics; + +namespace Pathfinding.Graphs.Grid.Rules { + using Pathfinding.Jobs; + + /// <summary> + /// Modifies nodes based on the contents of a texture. + /// + /// This can be used to "paint" penalties or walkability using an external program such as Photoshop. + /// + /// [Open online documentation to see images] + /// + /// See: grid-rules (view in online documentation for working links) + /// </summary> + [Pathfinding.Util.Preserve] + public class RuleTexture : GridGraphRule { + public Texture2D texture; + + public ChannelUse[] channels = new ChannelUse[4]; + public float[] channelScales = { 1000, 1000, 1000, 1000 }; + + public ScalingMode scalingMode = ScalingMode.StretchToFitGraph; + public float nodesPerPixel = 1; + + NativeArray<int> colors; + + public enum ScalingMode { + FixedScale, + StretchToFitGraph, + } + + public override int Hash { + get { + var h = base.Hash ^ (texture != null ? (int)texture.updateCount : 0); +#if UNITY_EDITOR + if (texture != null) h ^= (int)texture.imageContentsHash.GetHashCode(); +#endif + return h; + } + } + + public enum ChannelUse { + None, + /// <summary>Penalty goes from 0 to channelScale depending on the channel value</summary> + Penalty, + /// <summary>Node Y coordinate goes from 0 to channelScale depending on the channel value</summary> + Position, + /// <summary>If channel value is zero the node is made unwalkable, penalty goes from 0 to channelScale depending on the channel value</summary> + WalkablePenalty, + /// <summary>If channel value is zero the node is made unwalkable</summary> + Walkable, + } + + public override void Register (GridGraphRules rules) { + if (texture == null) return; + + if (!texture.isReadable) { + Debug.LogError("Texture for the texture rule on a grid graph is not marked as readable.", texture); + return; + } + + if (colors.IsCreated) colors.Dispose(); + colors = new NativeArray<Color32>(texture.GetPixels32(), Allocator.Persistent).Reinterpret<int>(); + + // Make sure this is done outside the delegate, just in case the texture is later resized + var textureSize = new int2(texture.width, texture.height); + + float4 channelPenaltiesCombined = float4.zero; + bool4 channelDeterminesWalkability = false; + float4 channelPositionScalesCombined = float4.zero; + for (int i = 0; i < 4; i++) { + channelPenaltiesCombined[i] = channels[i] == ChannelUse.Penalty || channels[i] == ChannelUse.WalkablePenalty ? channelScales[i] : 0; + channelDeterminesWalkability[i] = channels[i] == ChannelUse.Walkable || channels[i] == ChannelUse.WalkablePenalty; + channelPositionScalesCombined[i] = channels[i] == ChannelUse.Position ? channelScales[i] : 0; + } + + channelPositionScalesCombined /= 255.0f; + channelPenaltiesCombined /= 255.0f; + + if (math.any(channelPositionScalesCombined)) { + rules.AddJobSystemPass(Pass.BeforeCollision, context => { + new JobTexturePosition { + colorData = colors, + nodePositions = context.data.nodes.positions, + nodeNormals = context.data.nodes.normals, + bounds = context.data.nodes.bounds, + colorDataSize = textureSize, + scale = scalingMode == ScalingMode.FixedScale ? 1.0f/math.max(0.01f, nodesPerPixel) : textureSize / new float2(context.graph.width, context.graph.depth), + channelPositionScale = channelPositionScalesCombined, + graphToWorld = context.data.transform.matrix, + }.Schedule(context.tracker); + }); + } + + rules.AddJobSystemPass(Pass.BeforeConnections, context => { + new JobTexturePenalty { + colorData = colors, + penalty = context.data.nodes.penalties, + walkable = context.data.nodes.walkable, + nodeNormals = context.data.nodes.normals, + bounds = context.data.nodes.bounds, + colorDataSize = textureSize, + scale = scalingMode == ScalingMode.FixedScale ? 1.0f/math.max(0.01f, nodesPerPixel) : textureSize / new float2(context.graph.width, context.graph.depth), + channelPenalties = channelPenaltiesCombined, + channelDeterminesWalkability = channelDeterminesWalkability, + }.Schedule(context.tracker); + }); + } + + public override void DisposeUnmanagedData () { + if (colors.IsCreated) colors.Dispose(); + } + + [BurstCompile] + public struct JobTexturePosition : IJob, GridIterationUtilities.INodeModifier { + [ReadOnly] + public NativeArray<int> colorData; + [WriteOnly] + public NativeArray<Vector3> nodePositions; + [ReadOnly] + public NativeArray<float4> nodeNormals; + + public Matrix4x4 graphToWorld; + public IntBounds bounds; + public int2 colorDataSize; + public float2 scale; + public float4 channelPositionScale; + + public void ModifyNode (int dataIndex, int dataX, int dataLayer, int dataZ) { + var offset = bounds.min.xz; + int2 colorPos = math.clamp((int2)math.round((new float2(dataX, dataZ) + offset) * scale), int2.zero, colorDataSize - new int2(1, 1)); + int colorIndex = colorPos.y*colorDataSize.x + colorPos.x; + + int4 color = new int4((colorData[colorIndex] >> 0) & 0xFF, (colorData[colorIndex] >> 8) & 0xFF, (colorData[colorIndex] >> 16) & 0xFF, (colorData[colorIndex] >> 24) & 0xFF); + + float y = math.dot(channelPositionScale, color); + + nodePositions[dataIndex] = graphToWorld.MultiplyPoint3x4(new Vector3((bounds.min.x + dataX) + 0.5f, y, (bounds.min.z + dataZ) + 0.5f)); + } + + public void Execute () { + GridIterationUtilities.ForEachNode(bounds.size, nodeNormals, ref this); + } + } + + [BurstCompile] + public struct JobTexturePenalty : IJob, GridIterationUtilities.INodeModifier { + [ReadOnly] + public NativeArray<int> colorData; + public NativeArray<uint> penalty; + public NativeArray<bool> walkable; + [ReadOnly] + public NativeArray<float4> nodeNormals; + + public IntBounds bounds; + public int2 colorDataSize; + public float2 scale; + public float4 channelPenalties; + public bool4 channelDeterminesWalkability; + + public void ModifyNode (int dataIndex, int dataX, int dataLayer, int dataZ) { + var offset = bounds.min.xz; + int2 colorPos = math.clamp((int2)math.round((new float2(dataX, dataZ) + offset) * scale), int2.zero, colorDataSize - new int2(1, 1)); + int colorIndex = colorPos.y*colorDataSize.x + colorPos.x; + + int4 color = new int4((colorData[colorIndex] >> 0) & 0xFF, (colorData[colorIndex] >> 8) & 0xFF, (colorData[colorIndex] >> 16) & 0xFF, (colorData[colorIndex] >> 24) & 0xFF); + + penalty[dataIndex] += (uint)math.dot(channelPenalties, color); + walkable[dataIndex] = walkable[dataIndex] & !math.any(channelDeterminesWalkability & (color == 0)); + } + + public void Execute () { + GridIterationUtilities.ForEachNode(bounds.size, nodeNormals, ref this); + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RuleTexture.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RuleTexture.cs.meta new file mode 100644 index 0000000..6b3c4aa --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Rules/RuleTexture.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 42c128143490d447fa6420a4f35fe9bb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: |