summaryrefslogtreecommitdiff
path: root/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors
diff options
context:
space:
mode:
authorchai <215380520@qq.com>2024-05-23 10:08:29 +0800
committerchai <215380520@qq.com>2024-05-23 10:08:29 +0800
commit8722a9920c1f6119bf6e769cba270e63097f8e25 (patch)
tree2eaf9865de7fb1404546de4a4296553d8f68cc3b /Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors
parent3ba4020b69e5971bb0df7ee08b31d10ea4d01937 (diff)
+ astar project
Diffstat (limited to 'Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors')
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/GraphEditor.cs160
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/GraphEditor.cs.meta7
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/GridGeneratorEditor.cs871
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/GridGeneratorEditor.cs.meta7
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/LayerGridGraphEditor.cs44
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/LayerGridGraphEditor.cs.meta7
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/NavMeshGeneratorEditor.cs49
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/NavMeshGeneratorEditor.cs.meta7
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/PointGeneratorEditor.cs56
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/PointGeneratorEditor.cs.meta7
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/RecastGraphEditor.cs516
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/RecastGraphEditor.cs.meta7
12 files changed, 1738 insertions, 0 deletions
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/GraphEditor.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/GraphEditor.cs
new file mode 100644
index 0000000..634f704
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/GraphEditor.cs
@@ -0,0 +1,160 @@
+using UnityEditor;
+using UnityEngine;
+
+namespace Pathfinding {
+ public class GraphEditor : GraphEditorBase {
+ public AstarPathEditor editor;
+
+ /// <summary>Stores if the graph is visible or not in the inspector</summary>
+ public FadeArea fadeArea;
+
+ /// <summary>Stores if the graph info box is visible or not in the inspector</summary>
+ public FadeArea infoFadeArea;
+
+ /// <summary>
+ /// Called by editor scripts to rescan the graphs e.g when the user moved a graph.
+ /// Will only scan graphs if not playing and time to scan last graph was less than some constant (to avoid lag with large graphs)
+ /// </summary>
+ public bool AutoScan () {
+ if (!Application.isPlaying && AstarPath.active != null && AstarPath.active.lastScanTime < 0.11F) {
+ AstarPath.active.Scan();
+ return true;
+ }
+ return false;
+ }
+
+ public virtual void OnEnable () {
+ }
+
+ /// <summary>Rounds a vector's components to multiples of 0.5 (i.e 0.5, 1.0, 1.5, etc.) if very close to them</summary>
+ public static Vector3 RoundVector3 (Vector3 v) {
+ const int Multiplier = 2;
+
+ if (Mathf.Abs(Multiplier*v.x - Mathf.Round(Multiplier*v.x)) < 0.001f) v.x = Mathf.Round(Multiplier*v.x)/Multiplier;
+ if (Mathf.Abs(Multiplier*v.y - Mathf.Round(Multiplier*v.y)) < 0.001f) v.y = Mathf.Round(Multiplier*v.y)/Multiplier;
+ if (Mathf.Abs(Multiplier*v.z - Mathf.Round(Multiplier*v.z)) < 0.001f) v.z = Mathf.Round(Multiplier*v.z)/Multiplier;
+ return v;
+ }
+
+ public static Object ObjectField (string label, Object obj, System.Type objType, bool allowSceneObjects, bool assetsMustBeInResourcesFolder) {
+ return ObjectField(new GUIContent(label), obj, objType, allowSceneObjects, assetsMustBeInResourcesFolder);
+ }
+
+ public static Object ObjectField (GUIContent label, Object obj, System.Type objType, bool allowSceneObjects, bool assetsMustBeInResourcesFolder) {
+ obj = EditorGUILayout.ObjectField(label, obj, objType, allowSceneObjects);
+
+ if (obj != null) {
+ if (allowSceneObjects && !EditorUtility.IsPersistent(obj)) {
+ // Object is in the scene
+ var com = obj as Component;
+ var go = obj as GameObject;
+ if (com != null) {
+ go = com.gameObject;
+ }
+ if (go != null && go.GetComponent<UnityReferenceHelper>() == null) {
+ if (FixLabel("Object's GameObject must have a UnityReferenceHelper component attached")) {
+ go.AddComponent<UnityReferenceHelper>();
+ }
+ }
+ } else if (EditorUtility.IsPersistent(obj)) {
+ if (assetsMustBeInResourcesFolder) {
+ string path = AssetDatabase.GetAssetPath(obj).Replace("\\", "/");
+ var rg = new System.Text.RegularExpressions.Regex(@"Resources/.*$");
+
+ if (!rg.IsMatch(path)) {
+ if (FixLabel("Object must be in the 'Resources' folder")) {
+ if (!System.IO.Directory.Exists(Application.dataPath+"/Resources")) {
+ System.IO.Directory.CreateDirectory(Application.dataPath+"/Resources");
+ AssetDatabase.Refresh();
+ }
+
+ string ext = System.IO.Path.GetExtension(path);
+ string error = AssetDatabase.MoveAsset(path, "Assets/Resources/"+obj.name+ext);
+
+ if (error == "") {
+ path = AssetDatabase.GetAssetPath(obj);
+ } else {
+ Debug.LogError("Couldn't move asset - "+error);
+ }
+ }
+ }
+
+ if (!AssetDatabase.IsMainAsset(obj) && obj.name != AssetDatabase.LoadMainAssetAtPath(path).name) {
+ if (FixLabel("Due to technical reasons, the main asset must\nhave the same name as the referenced asset")) {
+ string error = AssetDatabase.RenameAsset(path, obj.name);
+ if (error != "") {
+ Debug.LogError("Couldn't rename asset - "+error);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return obj;
+ }
+
+ /// <summary>Draws common graph settings</summary>
+ public void OnBaseInspectorGUI (NavGraph target) {
+ int penalty = EditorGUILayout.IntField(new GUIContent("Initial Penalty", "Initial Penalty for nodes in this graph. Set during Scan."), (int)target.initialPenalty);
+
+ if (penalty < 0) penalty = 0;
+ target.initialPenalty = (uint)penalty;
+ }
+
+ /// <summary>Override to implement graph inspectors</summary>
+ public virtual void OnInspectorGUI (NavGraph target) {
+ }
+
+ /// <summary>Override to implement scene GUI drawing for the graph</summary>
+ public virtual void OnSceneGUI (NavGraph target) {
+ }
+
+ public static void Header (string title) {
+ EditorGUILayout.LabelField(new GUIContent(title), EditorStyles.boldLabel);
+ GUILayout.Space(4);
+ }
+
+ /// <summary>Draws a thin separator line</summary>
+ public static void Separator () {
+ GUIStyle separator = AstarPathEditor.astarSkin.FindStyle("PixelBox3Separator") ?? new GUIStyle();
+
+ Rect r = GUILayoutUtility.GetRect(new GUIContent(), separator);
+
+ if (Event.current.type == EventType.Repaint) {
+ separator.Draw(r, false, false, false, false);
+ }
+ }
+
+ /// <summary>Draws a small help box with a 'Fix' button to the right. Returns: Boolean - Returns true if the button was clicked</summary>
+ public static bool FixLabel (string label, string buttonLabel = "Fix", int buttonWidth = 40) {
+ GUILayout.BeginHorizontal();
+ GUILayout.Space(14*EditorGUI.indentLevel);
+ GUILayout.BeginHorizontal(AstarPathEditor.helpBox);
+ GUILayout.Label(label, EditorGUIUtility.isProSkin ? EditorStyles.whiteMiniLabel : EditorStyles.miniLabel, GUILayout.ExpandWidth(true));
+ var returnValue = GUILayout.Button(buttonLabel, EditorStyles.miniButton, GUILayout.Width(buttonWidth));
+ GUILayout.EndHorizontal();
+ GUILayout.EndHorizontal();
+ return returnValue;
+ }
+
+ /// <summary>Draws a toggle with a bold label to the right. Does not enable or disable GUI</summary>
+ public bool ToggleGroup (string label, bool value) {
+ return ToggleGroup(new GUIContent(label), value);
+ }
+
+ /// <summary>Draws a toggle with a bold label to the right. Does not enable or disable GUI</summary>
+ public static bool ToggleGroup (GUIContent label, bool value) {
+ GUILayout.BeginHorizontal();
+ GUILayout.Space(13*EditorGUI.indentLevel);
+ value = GUILayout.Toggle(value, "", GUILayout.Width(10));
+ GUIStyle boxHeader = AstarPathEditor.astarSkin.FindStyle("CollisionHeader");
+ if (GUILayout.Button(label, boxHeader, GUILayout.Width(100))) {
+ value = !value;
+ }
+
+ GUILayout.EndHorizontal();
+ return value;
+ }
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/GraphEditor.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/GraphEditor.cs.meta
new file mode 100644
index 0000000..35c29f6
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/GraphEditor.cs.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: b0bbfd9d85ec946b2a05cdeaab0d6f5f
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/GridGeneratorEditor.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/GridGeneratorEditor.cs
new file mode 100644
index 0000000..9e75055
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/GridGeneratorEditor.cs
@@ -0,0 +1,871 @@
+using UnityEngine;
+using UnityEditor;
+using Pathfinding.Serialization;
+using System.Collections.Generic;
+
+namespace Pathfinding {
+ using Pathfinding.Graphs.Grid;
+ using Pathfinding.Graphs.Grid.Rules;
+ using Pathfinding.Util;
+
+ [CustomGraphEditor(typeof(GridGraph), "Grid Graph")]
+ public class GridGraphEditor : GraphEditor {
+ [JsonMember]
+ public bool locked = true;
+
+ [JsonMember]
+ public bool showExtra;
+
+ GraphTransform savedTransform;
+ Vector2 savedDimensions;
+ float savedNodeSize;
+
+ public bool isMouseDown;
+
+ [JsonMember]
+ public GridPivot pivot;
+
+ /// <summary>
+ /// Shows the preview for the collision testing options.
+ ///
+ /// [Open online documentation to see images]
+ ///
+ /// On the left you can see a top-down view of the graph with a grid of nodes.
+ /// On the right you can see a side view of the graph. The white line at the bottom is the base of the graph, with node positions indicated using small dots.
+ /// When using 2D physics, only the top-down view is visible.
+ ///
+ /// The green shape indicates the shape that will be used for collision checking.
+ /// </summary>
+ [JsonMember]
+ public bool collisionPreviewOpen;
+
+ [JsonMember]
+ public int selectedTilemap;
+
+ /// <summary>Cached gui style</summary>
+ static GUIStyle lockStyle;
+
+ /// <summary>Cached gui style</summary>
+ static GUIStyle gridPivotSelectBackground;
+
+ /// <summary>Cached gui style</summary>
+ static GUIStyle gridPivotSelectButton;
+
+ public GridGraphEditor() {
+ // Default value depends on if the game is running or not. Make it hidden in play mode by default.
+ collisionPreviewOpen = !Application.isPlaying;
+ }
+
+ public override void OnInspectorGUI (NavGraph target) {
+ var graph = target as GridGraph;
+
+ DrawFirstSection(graph);
+ Separator();
+ DrawMiddleSection(graph);
+ Separator();
+ DrawCollisionEditor(graph.collision);
+ DrawRules(graph);
+
+ Separator();
+ DrawLastSection(graph);
+ }
+
+ bool IsHexagonal (GridGraph graph) {
+ return graph.neighbours == NumNeighbours.Six && graph.uniformEdgeCosts;
+ }
+
+ bool IsIsometric (GridGraph graph) {
+ if (IsHexagonal(graph)) return false;
+ if (graph.aspectRatio != 1) return true;
+ return graph.isometricAngle != 0;
+ }
+
+ bool IsAdvanced (GridGraph graph) {
+ if (graph.inspectorGridMode == InspectorGridMode.Advanced) return true;
+ // Weird configuration
+ return (graph.neighbours == NumNeighbours.Six) != graph.uniformEdgeCosts;
+ }
+
+ InspectorGridMode DetermineGridType (GridGraph graph) {
+ bool hex = IsHexagonal(graph);
+ bool iso = IsIsometric(graph);
+ bool adv = IsAdvanced(graph);
+
+ if (adv || (hex && iso)) return InspectorGridMode.Advanced;
+ if (hex) return InspectorGridMode.Hexagonal;
+ if (iso) return InspectorGridMode.IsometricGrid;
+ return graph.inspectorGridMode;
+ }
+
+ void DrawInspectorMode (GridGraph graph) {
+ graph.inspectorGridMode = DetermineGridType(graph);
+ var newMode = (InspectorGridMode)EditorGUILayout.EnumPopup("Shape", (System.Enum)graph.inspectorGridMode);
+ if (newMode != graph.inspectorGridMode) graph.SetGridShape(newMode);
+ }
+
+ protected virtual void Draw2DMode (GridGraph graph) {
+ graph.is2D = EditorGUILayout.Toggle(new GUIContent("2D"), graph.is2D);
+ }
+
+ GUIContent[] hexagonSizeContents = {
+ new GUIContent("Hexagon Width", "Distance between two opposing sides on the hexagon"),
+ new GUIContent("Hexagon Diameter", "Distance between two opposing vertices on the hexagon"),
+ new GUIContent("Node Size", "Raw node size value, this doesn't correspond to anything particular on the hexagon."),
+ };
+
+ static List<GridLayout> cachedSceneGridLayouts;
+ static float cachedSceneGridLayoutsTimestamp = -float.PositiveInfinity;
+
+ static string GetPath (Transform current) {
+ if (current.parent == null)
+ return current.name;
+ return GetPath(current.parent) + "/" + current.name;
+ }
+
+ void DrawTilemapAlignment (GridGraph graph) {
+ if (cachedSceneGridLayouts == null || Time.realtimeSinceStartup - cachedSceneGridLayoutsTimestamp > 5f) {
+ var tilemaps = UnityCompatibility.FindObjectsByTypeSorted<UnityEngine.GridLayout>();
+ List<GridLayout> layouts = new List<GridLayout>(tilemaps);
+ for (int i = 0; i < tilemaps.Length; i++) {
+ if (tilemaps[i] is UnityEngine.Tilemaps.Tilemap tilemap) layouts.Remove(tilemap.layoutGrid);
+ }
+ cachedSceneGridLayouts = layouts;
+ cachedSceneGridLayoutsTimestamp = Time.realtimeSinceStartup;
+ }
+ for (int i = cachedSceneGridLayouts.Count - 1; i >= 0; i--) {
+ if (cachedSceneGridLayouts[i] == null) cachedSceneGridLayouts.RemoveAt(i);
+ }
+
+ if (cachedSceneGridLayouts.Count > 0) {
+ GUILayout.BeginHorizontal();
+ EditorGUILayout.PrefixLabel("Align to tilemap");
+
+ var tilemapNames = new GUIContent[cachedSceneGridLayouts.Count+1];
+ tilemapNames[0] = new GUIContent("Select...");
+ for (int i = 0; i < cachedSceneGridLayouts.Count; i++) tilemapNames[i+1] = new GUIContent(GetPath(cachedSceneGridLayouts[i].transform).Replace("/", "\u2215"));
+
+ var originalIndent = EditorGUI.indentLevel;
+ EditorGUI.indentLevel = 0;
+ selectedTilemap = EditorGUILayout.Popup(selectedTilemap, tilemapNames);
+ selectedTilemap = Mathf.Clamp(selectedTilemap, 0, tilemapNames.Length - 1);
+
+ EditorGUI.BeginDisabledGroup(selectedTilemap <= 0 || selectedTilemap - 1 >= cachedSceneGridLayouts.Count);
+ if (GUILayout.Button("Align To")) {
+ graph.AlignToTilemap(cachedSceneGridLayouts[selectedTilemap - 1]);
+ }
+ EditorGUI.EndDisabledGroup();
+ EditorGUI.indentLevel = originalIndent;
+
+ GUILayout.EndHorizontal();
+ }
+ }
+
+ void DrawFirstSection (GridGraph graph) {
+ float prevRatio = graph.aspectRatio;
+
+ DrawInspectorMode(graph);
+
+ Draw2DMode(graph);
+ DrawTilemapAlignment(graph);
+
+ var normalizedPivotPoint = NormalizedPivotPoint(graph, pivot);
+ var worldPoint = graph.CalculateTransform().Transform(normalizedPivotPoint);
+ int newWidth, newDepth;
+
+ DrawWidthDepthFields(graph, out newWidth, out newDepth);
+
+ EditorGUI.BeginChangeCheck();
+ float newNodeSize;
+ if (graph.inspectorGridMode == InspectorGridMode.Hexagonal) {
+ EditorGUILayout.BeginHorizontal();
+ EditorGUILayout.BeginVertical();
+ graph.inspectorHexagonSizeMode = (InspectorGridHexagonNodeSize)EditorGUILayout.EnumPopup(new GUIContent("Hexagon Dimension"), graph.inspectorHexagonSizeMode);
+ float hexagonSize = GridGraph.ConvertNodeSizeToHexagonSize(graph.inspectorHexagonSizeMode, graph.nodeSize);
+ hexagonSize = (float)System.Math.Round(hexagonSize, 5);
+ newNodeSize = GridGraph.ConvertHexagonSizeToNodeSize(graph.inspectorHexagonSizeMode, EditorGUILayout.FloatField(hexagonSizeContents[(int)graph.inspectorHexagonSizeMode], hexagonSize));
+ EditorGUILayout.EndVertical();
+ if (graph.inspectorHexagonSizeMode != InspectorGridHexagonNodeSize.NodeSize) GUILayout.Box("", AstarPathEditor.astarSkin.FindStyle(graph.inspectorHexagonSizeMode == InspectorGridHexagonNodeSize.Diameter ? "HexagonDiameter" : "HexagonWidth"));
+ EditorGUILayout.EndHorizontal();
+ } else {
+ newNodeSize = EditorGUILayout.FloatField(new GUIContent("Node size", "The size of a single node. The size is the side of the node square in world units"), graph.nodeSize);
+ }
+ bool nodeSizeChanged = EditorGUI.EndChangeCheck();
+
+ newNodeSize = newNodeSize <= 0.01F ? 0.01F : newNodeSize;
+
+ if (graph.inspectorGridMode == InspectorGridMode.IsometricGrid || graph.inspectorGridMode == InspectorGridMode.Hexagonal || graph.inspectorGridMode == InspectorGridMode.Advanced) {
+ graph.aspectRatio = EditorGUILayout.FloatField(new GUIContent("Aspect Ratio", "Ratio between a node's width and depth."), graph.aspectRatio);
+ }
+
+ if (graph.inspectorGridMode == InspectorGridMode.IsometricGrid || graph.inspectorGridMode == InspectorGridMode.Advanced) {
+ DrawIsometricField(graph);
+ }
+
+ if ((nodeSizeChanged && locked) || (newWidth != graph.width || newDepth != graph.depth) || prevRatio != graph.aspectRatio) {
+ graph.nodeSize = newNodeSize;
+ graph.SetDimensions(newWidth, newDepth, newNodeSize);
+
+ normalizedPivotPoint = NormalizedPivotPoint(graph, pivot);
+ var newWorldPoint = graph.CalculateTransform().Transform(normalizedPivotPoint);
+ // Move the center so that the pivot point stays at the same point in the world
+ graph.center += worldPoint - newWorldPoint;
+ graph.center = RoundVector3(graph.center);
+ graph.UpdateTransform();
+ }
+
+ if ((nodeSizeChanged && !locked)) {
+ graph.nodeSize = newNodeSize;
+ graph.UpdateTransform();
+ }
+
+ DrawPositionField(graph);
+
+ DrawRotationField(graph);
+ }
+
+ void DrawRotationField (GridGraph graph) {
+ if (graph.is2D) {
+ var right = Quaternion.Euler(graph.rotation) * Vector3.right;
+ var angle = Mathf.Atan2(right.y, right.x) * Mathf.Rad2Deg;
+ if (angle < 0) angle += 360;
+ if (Mathf.Abs(angle - Mathf.Round(angle)) < 0.001f) angle = Mathf.Round(angle);
+ EditorGUI.BeginChangeCheck();
+ angle = EditorGUILayout.FloatField("Rotation", angle);
+ if (EditorGUI.EndChangeCheck()) {
+ graph.rotation = RoundVector3(new Vector3(-90 + angle, 270, 90));
+ }
+ } else {
+ graph.rotation = RoundVector3(EditorGUILayout.Vector3Field("Rotation", graph.rotation));
+ }
+ }
+
+ void DrawWidthDepthFields (GridGraph graph, out int newWidth, out int newDepth) {
+ lockStyle = lockStyle ?? AstarPathEditor.astarSkin.FindStyle("GridSizeLock") ?? new GUIStyle();
+
+ GUILayout.BeginHorizontal();
+ GUILayout.BeginVertical();
+ newWidth = EditorGUILayout.IntField(new GUIContent("Width (nodes)", "Width of the graph in nodes"), graph.width);
+ newDepth = EditorGUILayout.IntField(new GUIContent("Depth (nodes)", "Depth (or height you might also call it) of the graph in nodes"), graph.depth);
+
+ // Clamping will be done elsewhere as well
+ // but this prevents negative widths from being converted to positive ones (since an absolute value will be taken)
+ newWidth = Mathf.Max(newWidth, 1);
+ newDepth = Mathf.Max(newDepth, 1);
+
+ GUILayout.EndVertical();
+
+ Rect lockRect = GUILayoutUtility.GetRect(lockStyle.fixedWidth, lockStyle.fixedHeight);
+
+ GUILayout.EndHorizontal();
+
+ // All the layouts mess up the margin to the next control, so add it manually
+ GUILayout.Space(2);
+
+ // Add a small offset to make it better centred around the controls
+ lockRect.y += 3;
+ lockRect.width = lockStyle.fixedWidth;
+ lockRect.height = lockStyle.fixedHeight;
+ lockRect.x += lockStyle.margin.left;
+ lockRect.y += lockStyle.margin.top;
+
+ locked = GUI.Toggle(lockRect, locked,
+ new GUIContent("", "If the width and depth values are locked, " +
+ "changing the node size will scale the grid while keeping the number of nodes consistent " +
+ "instead of keeping the size the same and changing the number of nodes in the graph"), lockStyle);
+ }
+
+ void DrawIsometricField (GridGraph graph) {
+ var isometricGUIContent = new GUIContent("Isometric Angle", "For an isometric 2D game, you can use this parameter to scale the graph correctly.\nIt can also be used to create a hexagonal grid.\nYou may want to rotate the graph 45 degrees around the Y axis to make it line up better.");
+ var isometricOptions = new [] { new GUIContent("None (0°)"), new GUIContent("Isometric (≈54.74°)"), new GUIContent("Dimetric (60°)"), new GUIContent("Custom") };
+ var isometricValues = new [] { 0f, GridGraph.StandardIsometricAngle, GridGraph.StandardDimetricAngle };
+ var isometricOption = isometricValues.Length;
+
+ for (int i = 0; i < isometricValues.Length; i++) {
+ if (Mathf.Approximately(graph.isometricAngle, isometricValues[i])) {
+ isometricOption = i;
+ }
+ }
+
+ var prevIsometricOption = isometricOption;
+ isometricOption = EditorGUILayout.IntPopup(isometricGUIContent, isometricOption, isometricOptions, new [] { 0, 1, 2, 3 });
+ if (prevIsometricOption != isometricOption) {
+ // Change to something that will not match the predefined values above
+ graph.isometricAngle = 45;
+ }
+
+ if (isometricOption < isometricValues.Length) {
+ graph.isometricAngle = isometricValues[isometricOption];
+ } else {
+ EditorGUI.indentLevel++;
+ // Custom
+ graph.isometricAngle = EditorGUILayout.FloatField(isometricGUIContent, graph.isometricAngle);
+ EditorGUI.indentLevel--;
+ }
+ }
+
+ static Vector3 NormalizedPivotPoint (GridGraph graph, GridPivot pivot) {
+ switch (pivot) {
+ case GridPivot.Center:
+ default:
+ return new Vector3(graph.width/2f, 0, graph.depth/2f);
+ case GridPivot.TopLeft:
+ return new Vector3(0, 0, graph.depth);
+ case GridPivot.TopRight:
+ return new Vector3(graph.width, 0, graph.depth);
+ case GridPivot.BottomLeft:
+ return new Vector3(0, 0, 0);
+ case GridPivot.BottomRight:
+ return new Vector3(graph.width, 0, 0);
+ }
+ }
+
+ void DrawPositionField (GridGraph graph) {
+ GUILayout.BeginHorizontal();
+ var normalizedPivotPoint = NormalizedPivotPoint(graph, pivot);
+ var worldPoint = RoundVector3(graph.CalculateTransform().Transform(normalizedPivotPoint));
+ var newWorldPoint = EditorGUILayout.Vector3Field(ObjectNames.NicifyVariableName(pivot.ToString()), worldPoint);
+ var delta = newWorldPoint - worldPoint;
+ if (delta.magnitude > 0.001f) {
+ graph.center += delta;
+ }
+
+ pivot = PivotPointSelector(pivot);
+ GUILayout.EndHorizontal();
+ }
+
+ protected virtual void DrawMiddleSection (GridGraph graph) {
+ DrawNeighbours(graph);
+ DrawMaxClimb(graph);
+ DrawMaxSlope(graph);
+ DrawErosion(graph);
+ }
+
+ protected virtual void DrawCutCorners (GridGraph graph) {
+ if (graph.inspectorGridMode == InspectorGridMode.Hexagonal) return;
+
+ graph.cutCorners = EditorGUILayout.Toggle(new GUIContent("Cut Corners", "Enables or disables cutting corners. See docs for image example"), graph.cutCorners);
+ }
+
+ protected virtual void DrawNeighbours (GridGraph graph) {
+ if (graph.inspectorGridMode == InspectorGridMode.Hexagonal) return;
+
+ var neighboursGUIContent = new GUIContent("Connections", "Sets how many connections a node should have to it's neighbour nodes.");
+ GUIContent[] neighbourOptions;
+ if (graph.inspectorGridMode == InspectorGridMode.Advanced) {
+ neighbourOptions = new [] { new GUIContent("Four"), new GUIContent("Eight"), new GUIContent("Six") };
+ } else {
+ neighbourOptions = new [] { new GUIContent("Four"), new GUIContent("Eight") };
+ }
+ graph.neighbours = (NumNeighbours)EditorGUILayout.Popup(neighboursGUIContent, (int)graph.neighbours, neighbourOptions);
+
+ EditorGUI.indentLevel++;
+
+ if (graph.neighbours == NumNeighbours.Eight) {
+ DrawCutCorners(graph);
+ }
+
+ if (graph.neighbours == NumNeighbours.Six) {
+ graph.uniformEdgeCosts = EditorGUILayout.Toggle(new GUIContent("Hexagon connection costs", "Tweak the edge costs in the graph to be more suitable for hexagon graphs"), graph.uniformEdgeCosts);
+ EditorGUILayout.HelpBox("You can set all settings to make this a hexagonal graph by changing the 'Shape' field above", MessageType.None);
+ } else {
+ graph.uniformEdgeCosts = false;
+ }
+
+ EditorGUI.indentLevel--;
+ }
+
+ protected virtual void DrawMaxClimb (GridGraph graph) {
+ if (!graph.collision.use2D) {
+ graph.maxStepHeight = EditorGUILayout.FloatField(new GUIContent("Max Step Height", "How high a step can be while still allowing the AI to go up/down it. A zero (0) indicates infinity. This affects for example how the graph is generated around ledges and stairs."), graph.maxStepHeight);
+ if (graph.maxStepHeight < 0) graph.maxStepHeight = 0;
+ if (graph.maxStepHeight > 0) {
+ EditorGUI.indentLevel++;
+ graph.maxStepUsesSlope = EditorGUILayout.Toggle(new GUIContent("Account for slopes", "Account for slopes when calculating the step sizes. See documentation for more info."), graph.maxStepUsesSlope);
+ EditorGUI.indentLevel--;
+ }
+ }
+ }
+
+ protected void DrawMaxSlope (GridGraph graph) {
+ if (!graph.collision.use2D) {
+ graph.maxSlope = EditorGUILayout.Slider(new GUIContent("Max Slope", "Sets the max slope in degrees for a point to be walkable. Only enabled if Height Testing is enabled."), graph.maxSlope, 0, 90F);
+ }
+ }
+
+ protected void DrawErosion (GridGraph graph) {
+ graph.erodeIterations = EditorGUILayout.IntField(new GUIContent("Erosion iterations", "Sets how many times the graph should be eroded. This adds extra margin to objects."), graph.erodeIterations);
+ graph.erodeIterations = graph.erodeIterations < 0 ? 0 : (graph.erodeIterations > 16 ? 16 : graph.erodeIterations); //Clamp iterations to [0,16]
+
+ if (graph.erodeIterations > 0) {
+ EditorGUI.indentLevel++;
+ graph.erosionUseTags = EditorGUILayout.Toggle(new GUIContent("Erosion Uses Tags", "Instead of making nodes unwalkable, " +
+ "nodes will have their tag set to a value corresponding to their erosion level, " +
+ "which is a quite good measurement of their distance to the closest wall.\nSee online documentation for more info."),
+ graph.erosionUseTags);
+ if (graph.erosionUseTags) {
+ EditorGUI.indentLevel++;
+ graph.erosionFirstTag = EditorGUILayoutHelper.TagField(new GUIContent("First Tag"), graph.erosionFirstTag, AstarPathEditor.EditTags);
+ var tagNames = AstarPath.FindTagNames().Clone() as string[];
+ var tagMsg = "";
+ for (int i = graph.erosionFirstTag; i < graph.erosionFirstTag + graph.erodeIterations; i++) {
+ tagMsg += (i > graph.erosionFirstTag ? (i == graph.erosionFirstTag + graph.erodeIterations - 1 ? " or " : ", ") : "") + tagNames[i];
+ }
+ EditorGUILayout.HelpBox("Tag " + tagMsg + " will be applied to nodes" + (graph.erodeIterations > 1 ? ", based on their distance to obstacles" : " when adjacent to obstacles"), MessageType.None);
+ for (int i = graph.erosionFirstTag; i < graph.erosionFirstTag + graph.erodeIterations; i++) tagNames[i] += " (used for erosion)";
+ graph.erosionTagsPrecedenceMask = EditorGUILayout.MaskField(
+ new GUIContent("Overwritable tags", "Nodes near unwalkable nodes will be marked with tags. " +
+ "If these nodes already have tags, you may want the custom tag to take precedence. This mask controls which tags are allowed to be replaced by the new erosion tags."),
+ graph.erosionTagsPrecedenceMask,
+ tagNames
+ );
+ if ((graph.erosionTagsPrecedenceMask & 0x1) == 0) {
+ EditorGUILayout.HelpBox("The " + tagNames[0] + " tag has been excluded. Since this is the default tag, erosion tags will likely be applied to very few, if any, nodes. This is likely not what you want", MessageType.Warning);
+ }
+ EditorGUI.indentLevel--;
+ }
+ EditorGUI.indentLevel--;
+ }
+ }
+
+ void DrawLastSection (GridGraph graph) {
+ GUILayout.BeginHorizontal();
+ GUILayout.Space(18);
+ graph.showMeshSurface = GUILayout.Toggle(graph.showMeshSurface, new GUIContent("Show surface", "Toggles gizmos for drawing the surface of the mesh"), EditorStyles.miniButtonLeft);
+ graph.showMeshOutline = GUILayout.Toggle(graph.showMeshOutline, new GUIContent("Show outline", "Toggles gizmos for drawing an outline of the nodes"), EditorStyles.miniButtonMid);
+ graph.showNodeConnections = GUILayout.Toggle(graph.showNodeConnections, new GUIContent("Show connections", "Toggles gizmos for drawing node connections"), EditorStyles.miniButtonRight);
+ GUILayout.EndHorizontal();
+ }
+
+ /// <summary>Draws the inspector for a <see cref="GraphCollision"/> class</summary>
+ protected virtual void DrawCollisionEditor (GraphCollision collision) {
+ collision = collision ?? new GraphCollision();
+
+ DrawUse2DPhysics(collision);
+
+ collision.collisionCheck = ToggleGroup("Collision testing", collision.collisionCheck);
+ if (collision.collisionCheck) {
+ EditorGUI.indentLevel++;
+ string[] colliderOptions = collision.use2D ? new [] { "Circle", "Point" } : new [] { "Sphere", "Capsule", "Ray" };
+ int[] colliderValues = collision.use2D ? new [] { 0, 2 } : new [] { 0, 1, 2 };
+ // In 2D the Circle (Sphere) mode will replace both the Sphere and the Capsule modes
+ // However make sure that the original value is still stored in the grid graph in case the user changes back to the 3D mode in the inspector.
+ var tp = collision.type;
+ if (tp == ColliderType.Capsule && collision.use2D) tp = ColliderType.Sphere;
+ EditorGUI.BeginChangeCheck();
+ tp = (ColliderType)EditorGUILayout.IntPopup("Collider type", (int)tp, colliderOptions, colliderValues);
+ if (EditorGUI.EndChangeCheck()) collision.type = tp;
+
+ // Only spheres and capsules have a diameter
+ if (collision.type == ColliderType.Capsule || collision.type == ColliderType.Sphere) {
+ collision.diameter = EditorGUILayout.FloatField(new GUIContent("Diameter", "Diameter of the capsule or sphere. 1 equals one node width"), collision.diameter);
+ collision.diameter = Mathf.Max(collision.diameter, 0.01f);
+ }
+
+ if (!collision.use2D) {
+ if (collision.type == ColliderType.Capsule || collision.type == ColliderType.Ray) {
+ collision.height = EditorGUILayout.FloatField(new GUIContent("Height/Length", "Height of cylinder or length of ray in world units"), collision.height);
+ collision.height = Mathf.Max(collision.height, 0.01f);
+ }
+
+ collision.collisionOffset = EditorGUILayout.FloatField(new GUIContent("Offset", "Offset upwards from the node. Can be used so that obstacles can be used as ground and at the same time as obstacles for lower positioned nodes"), collision.collisionOffset);
+ }
+
+ collision.mask = EditorGUILayoutx.LayerMaskField("Obstacle Layer Mask", collision.mask);
+
+ DrawCollisionPreview(collision);
+ EditorGUI.indentLevel--;
+ }
+
+ GUILayout.Space(2);
+
+ if (collision.use2D) {
+ EditorGUI.BeginDisabledGroup(collision.use2D);
+ ToggleGroup("Height testing", false);
+ EditorGUI.EndDisabledGroup();
+ } else {
+ collision.heightCheck = ToggleGroup("Height testing", collision.heightCheck);
+ if (collision.heightCheck) {
+ EditorGUI.indentLevel++;
+ collision.fromHeight = EditorGUILayout.FloatField(new GUIContent("Ray length", "The height from which to check for ground"), collision.fromHeight);
+
+ collision.heightMask = EditorGUILayoutx.LayerMaskField("Mask", collision.heightMask);
+
+ collision.thickRaycast = EditorGUILayout.Toggle(new GUIContent("Thick Raycast", "Use a thick line instead of a thin line"), collision.thickRaycast);
+
+ if (collision.thickRaycast) {
+ EditorGUI.indentLevel++;
+ collision.thickRaycastDiameter = EditorGUILayout.FloatField(new GUIContent("Diameter", "Diameter of the thick raycast"), collision.thickRaycastDiameter);
+ EditorGUI.indentLevel--;
+ }
+
+ collision.unwalkableWhenNoGround = EditorGUILayout.Toggle(new GUIContent("Unwalkable when no ground", "Make nodes unwalkable when no ground was found with the height raycast. If height raycast is turned off, this doesn't affect anything"), collision.unwalkableWhenNoGround);
+ EditorGUI.indentLevel--;
+ }
+ }
+ }
+
+ Vector3[] arcBuffer = new Vector3[21];
+ Vector3[] lineBuffer = new Vector3[2];
+ void DrawArc (Vector2 center, float radius, float startAngle, float endAngle) {
+ // The AA line doesn't always properly close the gap even for full circles
+ endAngle += 1*Mathf.Deg2Rad;
+ var width = 4;
+ // The DrawAAPolyLine method does not draw a centered line unfortunately
+ //radius -= width/2;
+ for (int i = 0; i < arcBuffer.Length; i++) {
+ float t = i * 1.0f / (arcBuffer.Length-1);
+ float angle = Mathf.Lerp(startAngle, endAngle, t);
+ arcBuffer[i] = new Vector3(center.x + radius * Mathf.Cos(angle), center.y + radius * Mathf.Sin(angle), 0);
+ }
+ Handles.DrawAAPolyLine(EditorResourceHelper.HandlesAALineTexture, width, arcBuffer);
+ }
+
+ void DrawLine (Vector2 a, Vector2 b) {
+ lineBuffer[0] = a;
+ lineBuffer[1] = b;
+ Handles.DrawAAPolyLine(EditorResourceHelper.HandlesAALineTexture, 4, lineBuffer);
+ }
+
+ void DrawDashedLine (Vector2 a, Vector2 b, float dashLength) {
+ if (dashLength == 0) {
+ DrawLine(a, b);
+ } else {
+ var dist = (b - a).magnitude;
+ int steps = Mathf.RoundToInt(dist / dashLength);
+ for (int i = 0; i < steps; i++) {
+ var t1 = i * 1.0f / (steps-1);
+ var t2 = (i + 0.5f) * 1.0f / (steps-1);
+ DrawLine(Vector2.Lerp(a, b, t1), Vector2.Lerp(a, b, t2));
+ }
+ }
+ }
+
+ static int RoundUpToNextOddNumber (float x) {
+ return Mathf.CeilToInt((x - 1)/2.0f)*2 + 1;
+ }
+
+ float interpolatedGridWidthInNodes = -1;
+ float lastTime = 0;
+
+ void DrawCollisionPreview (GraphCollision collision) {
+ EditorGUILayout.BeginHorizontal();
+ GUILayout.Space(2);
+ collisionPreviewOpen = EditorGUILayout.Foldout(collisionPreviewOpen, "Preview");
+ EditorGUILayout.EndHorizontal();
+ if (!collisionPreviewOpen) return;
+
+ EditorGUILayout.Separator();
+ var rect = EditorGUI.IndentedRect(GUILayoutUtility.GetRect(10, 100));
+ var m = Handles.matrix;
+ Handles.matrix = Handles.matrix * Matrix4x4.Translate(new Vector3(rect.xMin, rect.yMin));
+
+ // Draw NxN grid with circle in the middle
+ // Draw Flat plane with capsule/sphere/line above
+
+ Handles.color = Color.white;
+ int gridWidthInNodes = collision.type == ColliderType.Ray ? 3 : Mathf.Max(3, RoundUpToNextOddNumber(collision.diameter + 0.5f));
+ if (interpolatedGridWidthInNodes == -1) interpolatedGridWidthInNodes = gridWidthInNodes;
+ if (Mathf.Abs(interpolatedGridWidthInNodes - gridWidthInNodes) < 0.01f) interpolatedGridWidthInNodes = gridWidthInNodes;
+ else editor.Repaint();
+
+ var dt = Time.realtimeSinceStartup - lastTime;
+ lastTime = Time.realtimeSinceStartup;
+ interpolatedGridWidthInNodes = Mathf.Lerp(interpolatedGridWidthInNodes, gridWidthInNodes, 5 * dt);
+
+ var gridCenter = collision.use2D ? new Vector2(rect.width / 2.0f, rect.height * 0.5f) : new Vector2(rect.width / 3.0f, rect.height * 0.5f);
+ var gridWidth = Mathf.Min(rect.width / 3, rect.height);
+ var nodeSize = (this.target as GridGraph).nodeSize;
+ var scale = gridWidth / (nodeSize * interpolatedGridWidthInNodes);
+ var diameter = collision.type == ColliderType.Ray ? 0.05f : collision.diameter * nodeSize;
+ var interpolatedGridScale = gridWidthInNodes * nodeSize * scale;
+ for (int i = 0; i <= gridWidthInNodes; i++) {
+ var c = i*1.0f/gridWidthInNodes;
+ DrawLine(gridCenter + new Vector2(c - 0.5f, -0.5f) * interpolatedGridScale, gridCenter + new Vector2(c - 0.5f, 0.5f) * interpolatedGridScale);
+ DrawLine(gridCenter + new Vector2(-0.5f, c - 0.5f) * interpolatedGridScale, gridCenter + new Vector2(0.5f, c - 0.5f) * interpolatedGridScale);
+ }
+
+ var sideBase = new Vector2(2*rect.width / 3f, rect.height);
+ float sideScale;
+ if (collision.type == ColliderType.Sphere) {
+ sideScale = scale;
+ // A high collision offset should not cause it to break
+ sideScale = Mathf.Min(sideScale, sideBase.y / (Mathf.Max(0, collision.collisionOffset) + diameter));
+ } else {
+ sideScale = Mathf.Max(scale * 0.5f, Mathf.Min(scale, sideBase.y / (collision.height + collision.collisionOffset + diameter * 0.5f)));
+ // A high collision offset should not cause it to break
+ sideScale = Mathf.Min(sideScale, sideBase.y / (Mathf.Max(0, collision.collisionOffset) + diameter));
+ }
+
+ var darkGreen = new Color(9/255f, 150/255f, 23/255f);
+ var lightGreen = new Color(12/255f, 194/255f, 30/255f);
+ var green = EditorGUIUtility.isProSkin ? lightGreen : darkGreen;
+
+ Handles.color = green;
+ DrawArc(gridCenter, diameter * 0.5f * scale, 0, Mathf.PI*2);
+
+ if (!collision.use2D) {
+ Handles.color = Color.white;
+ var interpolatedGridSideScale = gridWidthInNodes * nodeSize * sideScale;
+
+ DrawLine(sideBase + new Vector2(-interpolatedGridSideScale * 0.5f, 0), sideBase + new Vector2(interpolatedGridSideScale * 0.5f, 0));
+ for (int i = 0; i <= gridWidthInNodes; i++) {
+ var c = i*1.0f/gridWidthInNodes;
+ DrawArc(sideBase + new Vector2(c - 0.5f, 0) * interpolatedGridSideScale, 2, 0, Mathf.PI*2);
+ }
+
+ Handles.color = green;
+
+ if (collision.type == ColliderType.Ray) {
+ var height = collision.height;
+ var maxHeight = sideBase.y / sideScale - (collision.collisionOffset + diameter*0.5f);
+ float dashLength = 0;
+ if (collision.height > maxHeight + 0.01f) {
+ height = maxHeight;
+ dashLength = 6;
+ }
+
+ var offset = sideBase + new Vector2(0, -collision.collisionOffset) * sideScale;
+ DrawLine(offset + new Vector2(0, -height*0.75f) * sideScale, offset);
+ DrawDashedLine(offset + new Vector2(0, -height) * sideScale, offset + new Vector2(0, -height * 0.75f) * sideScale, dashLength);
+ DrawLine(offset, offset + new Vector2(6, -6));
+ DrawLine(offset, offset + new Vector2(-6, -6));
+ } else {
+ var height = collision.type == ColliderType.Capsule ? collision.height : 0;
+ // sideBase.y - (collision.collisionOffset + height + diameter * 0.5f) * scale < 0
+ var maxHeight = sideBase.y / sideScale - (collision.collisionOffset + diameter*0.5f);
+ float dashLength = 0;
+ if (height > maxHeight + 0.01f) {
+ height = maxHeight;
+ dashLength = 6;
+ }
+ DrawArc(sideBase + new Vector2(0, -collision.collisionOffset * sideScale), diameter * 0.5f * sideScale, 0, Mathf.PI);
+ DrawArc(sideBase + new Vector2(0, -(height + collision.collisionOffset) * sideScale), diameter * 0.5f * sideScale, Mathf.PI, 2*Mathf.PI);
+ DrawDashedLine(sideBase + new Vector2(-diameter * 0.5f, -collision.collisionOffset) * sideScale, sideBase + new Vector2(-diameter * 0.5f, -(height + collision.collisionOffset)) * sideScale, dashLength);
+ DrawDashedLine(sideBase + new Vector2(diameter * 0.5f, -collision.collisionOffset) * sideScale, sideBase + new Vector2(diameter * 0.5f, -(height + collision.collisionOffset)) * sideScale, dashLength);
+ }
+ }
+ Handles.matrix = m;
+ EditorGUILayout.Separator();
+ }
+
+ protected virtual void DrawUse2DPhysics (GraphCollision collision) {
+ collision.use2D = EditorGUILayout.Toggle(new GUIContent("Use 2D Physics", "Use the Physics2D API for collision checking"), collision.use2D);
+
+ if (collision.use2D) {
+ var graph = target as GridGraph;
+ if (Mathf.Abs(Vector3.Dot(Vector3.forward, Quaternion.Euler(graph.rotation) * Vector3.up)) < 0.9f) {
+ EditorGUILayout.HelpBox("When using 2D physics it is recommended to rotate the graph so that it aligns with the 2D plane.", MessageType.Warning);
+ }
+ }
+ }
+
+ static Dictionary<System.Type, System.Type> ruleEditors;
+ static Dictionary<System.Type, string> ruleHeaders;
+ static List<System.Type> ruleTypes;
+ Dictionary<GridGraphRule, IGridGraphRuleEditor> ruleEditorInstances = new Dictionary<GridGraphRule, IGridGraphRuleEditor>();
+
+ static void FindRuleEditors () {
+ ruleEditors = new Dictionary<System.Type, System.Type>();
+ ruleHeaders = new Dictionary<System.Type, string>();
+ ruleTypes = new List<System.Type>();
+ foreach (var type in TypeCache.GetTypesWithAttribute<CustomGridGraphRuleEditorAttribute>()) {
+ var attrs = type.GetCustomAttributes(typeof(CustomGridGraphRuleEditorAttribute), false);
+ foreach (CustomGridGraphRuleEditorAttribute attr in attrs) {
+ ruleEditors[attr.type] = type;
+ ruleHeaders[attr.type] = attr.name;
+ }
+ }
+
+ foreach (var type in TypeCache.GetTypesDerivedFrom<GridGraphRule>()) {
+ if (!type.IsAbstract) ruleTypes.Add(type);
+ }
+ }
+
+ IGridGraphRuleEditor GetEditor (GridGraphRule rule) {
+ if (ruleEditors == null) FindRuleEditors();
+ IGridGraphRuleEditor ruleEditor;
+ if (!ruleEditorInstances.TryGetValue(rule, out ruleEditor)) {
+ if (ruleEditors.ContainsKey(rule.GetType())) {
+ ruleEditor = ruleEditorInstances[rule] = (IGridGraphRuleEditor)System.Activator.CreateInstance(ruleEditors[rule.GetType()]);
+ }
+ }
+ return ruleEditor;
+ }
+
+ protected virtual void DrawRules (GridGraph graph) {
+ var rules = graph.rules.GetRules();
+
+ for (int i = 0; i < rules.Count; i++) {
+ var rule = rules[i];
+ if (rule != null) {
+ var ruleEditor = GetEditor(rule);
+ var ruleType = rule.GetType();
+ GUILayout.BeginHorizontal();
+ rule.enabled = ToggleGroup(ruleHeaders.TryGetValue(ruleType, out var header) ? header : ruleType.Name, rule.enabled);
+ if (GUILayout.Button("", AstarPathEditor.astarSkin.FindStyle("SimpleDeleteButton"))) {
+ graph.rules.RemoveRule(rule);
+ ruleEditorInstances.Remove(rule);
+ rule.enabled = false;
+ rule.DisposeUnmanagedData();
+ }
+ GUILayout.EndHorizontal();
+
+ if (rule.enabled) {
+ if (ruleEditor != null) {
+ EditorGUI.indentLevel++;
+ EditorGUI.BeginChangeCheck();
+ ruleEditor.OnInspectorGUI(graph, rule);
+ if (EditorGUI.EndChangeCheck()) rule.SetDirty();
+ EditorGUI.indentLevel--;
+ } else {
+ EditorGUILayout.HelpBox("No editor found for " + rule.GetType().Name, MessageType.Error);
+ }
+ }
+ }
+ }
+
+ EditorGUILayout.Separator();
+
+ GUILayout.BeginHorizontal();
+ GUILayout.Space(10);
+ if (GUILayout.Button("Add Rule", GUILayout.Height(30))) {
+ if (ruleEditors == null) FindRuleEditors();
+ GenericMenu menu = new GenericMenu();
+ foreach (var type in ruleTypes) {
+ menu.AddItem(new GUIContent(ruleHeaders.TryGetValue(type, out var header) ? header : type.Name), false, ruleType => graph.rules.AddRule(System.Activator.CreateInstance((System.Type)ruleType) as GridGraphRule), type);
+ }
+ menu.ShowAsContext();
+ }
+ GUILayout.Space(10);
+ GUILayout.EndHorizontal();
+ }
+
+ public static GridPivot PivotPointSelector (GridPivot pivot) {
+ // Find required styles
+ gridPivotSelectBackground = gridPivotSelectBackground ?? AstarPathEditor.astarSkin.FindStyle("GridPivotSelectBackground");
+ gridPivotSelectButton = gridPivotSelectButton ?? AstarPathEditor.astarSkin.FindStyle("GridPivotSelectButton");
+
+ Rect r = GUILayoutUtility.GetRect(19, 19, gridPivotSelectBackground);
+
+ // I have no idea why... but this is required for it to work well
+ r.y -= 14;
+
+ r.width = 19;
+ r.height = 19;
+
+ if (gridPivotSelectBackground == null) {
+ return pivot;
+ }
+
+ if (Event.current.type == EventType.Repaint) {
+ gridPivotSelectBackground.Draw(r, false, false, false, false);
+ }
+
+ if (GUI.Toggle(new Rect(r.x, r.y, 7, 7), pivot == GridPivot.TopLeft, "", gridPivotSelectButton))
+ pivot = GridPivot.TopLeft;
+
+ if (GUI.Toggle(new Rect(r.x+12, r.y, 7, 7), pivot == GridPivot.TopRight, "", gridPivotSelectButton))
+ pivot = GridPivot.TopRight;
+
+ if (GUI.Toggle(new Rect(r.x+12, r.y+12, 7, 7), pivot == GridPivot.BottomRight, "", gridPivotSelectButton))
+ pivot = GridPivot.BottomRight;
+
+ if (GUI.Toggle(new Rect(r.x, r.y+12, 7, 7), pivot == GridPivot.BottomLeft, "", gridPivotSelectButton))
+ pivot = GridPivot.BottomLeft;
+
+ if (GUI.Toggle(new Rect(r.x+6, r.y+6, 7, 7), pivot == GridPivot.Center, "", gridPivotSelectButton))
+ pivot = GridPivot.Center;
+
+ return pivot;
+ }
+
+ static readonly Vector3[] handlePoints = new [] { new Vector3(0.0f, 0, 0.5f), new Vector3(1.0f, 0, 0.5f), new Vector3(0.5f, 0, 0.0f), new Vector3(0.5f, 0, 1.0f) };
+
+ public override void OnSceneGUI (NavGraph target) {
+ Event e = Event.current;
+
+ var graph = target as GridGraph;
+
+ graph.UpdateTransform();
+ var currentTransform = graph.transform * Matrix4x4.Scale(new Vector3(graph.width, 1, graph.depth));
+
+ if (e.type == EventType.MouseDown) {
+ isMouseDown = true;
+ } else if (e.type == EventType.MouseUp) {
+ isMouseDown = false;
+ }
+
+ if (!isMouseDown) {
+ savedTransform = currentTransform;
+ savedDimensions = new Vector2(graph.width, graph.depth);
+ savedNodeSize = graph.nodeSize;
+ }
+
+ Handles.matrix = Matrix4x4.identity;
+ Handles.color = AstarColor.BoundsHandles;
+ Handles.CapFunction cap = Handles.CylinderHandleCap;
+
+ var center = currentTransform.Transform(new Vector3(0.5f, 0, 0.5f));
+ if (Tools.current == Tool.Scale) {
+ const float HandleScale = 0.1f;
+
+ Vector3 mn = Vector3.zero;
+ Vector3 mx = Vector3.zero;
+ EditorGUI.BeginChangeCheck();
+ for (int i = 0; i < handlePoints.Length; i++) {
+ var ps = currentTransform.Transform(handlePoints[i]);
+ Vector3 p = savedTransform.InverseTransform(Handles.Slider(ps, ps - center, HandleScale*HandleUtility.GetHandleSize(ps), cap, 0));
+
+ // Snap to increments of whole nodes
+ p.x = Mathf.Round(p.x * savedDimensions.x) / savedDimensions.x;
+ p.z = Mathf.Round(p.z * savedDimensions.y) / savedDimensions.y;
+
+ if (i == 0) {
+ mn = mx = p;
+ } else {
+ mn = Vector3.Min(mn, p);
+ mx = Vector3.Max(mx, p);
+ }
+ }
+
+ if (EditorGUI.EndChangeCheck()) {
+ graph.center = savedTransform.Transform((mn + mx) * 0.5f);
+ graph.unclampedSize = Vector2.Scale(new Vector2(mx.x - mn.x, mx.z - mn.z), savedDimensions) * savedNodeSize;
+ }
+ } else if (Tools.current == Tool.Move) {
+ EditorGUI.BeginChangeCheck();
+ center = Handles.PositionHandle(graph.center, Tools.pivotRotation == PivotRotation.Global ? Quaternion.identity : Quaternion.Euler(graph.rotation));
+
+ if (EditorGUI.EndChangeCheck() && Tools.viewTool != ViewTool.Orbit) {
+ graph.center = center;
+ }
+ } else if (Tools.current == Tool.Rotate) {
+ EditorGUI.BeginChangeCheck();
+ var rot = Handles.RotationHandle(Quaternion.Euler(graph.rotation), graph.center);
+
+ if (EditorGUI.EndChangeCheck() && Tools.viewTool != ViewTool.Orbit) {
+ graph.rotation = rot.eulerAngles;
+ }
+ }
+
+ var rules = graph.rules.GetRules();
+ for (int i = 0; i < rules.Count; i++) {
+ var rule = rules[i];
+ if (rule != null && rule.enabled) {
+ var ruleEditor = GetEditor(rule);
+ if (ruleEditor != null) {
+ ruleEditor.OnSceneGUI(graph, rule);
+ }
+ }
+ }
+ }
+
+ public enum GridPivot {
+ Center,
+ TopLeft,
+ TopRight,
+ BottomLeft,
+ BottomRight
+ }
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/GridGeneratorEditor.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/GridGeneratorEditor.cs.meta
new file mode 100644
index 0000000..495f7e9
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/GridGeneratorEditor.cs.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 22fe5ef0fc7b34922bb13d1961c9f444
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/LayerGridGraphEditor.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/LayerGridGraphEditor.cs
new file mode 100644
index 0000000..bedf8b8
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/LayerGridGraphEditor.cs
@@ -0,0 +1,44 @@
+using UnityEngine;
+using UnityEditor;
+using Pathfinding.Graphs.Grid;
+
+namespace Pathfinding {
+ [CustomGraphEditor(typeof(LayerGridGraph), "Layered Grid Graph")]
+ public class LayerGridGraphEditor : GridGraphEditor {
+ protected override void DrawMiddleSection (GridGraph graph) {
+ var layerGridGraph = graph as LayerGridGraph;
+
+ DrawNeighbours(graph);
+
+ layerGridGraph.characterHeight = EditorGUILayout.DelayedFloatField("Character Height", layerGridGraph.characterHeight);
+ DrawMaxClimb(graph);
+
+ DrawMaxSlope(graph);
+ DrawErosion(graph);
+ }
+
+ protected override void DrawMaxClimb (GridGraph graph) {
+ var layerGridGraph = graph as LayerGridGraph;
+
+ base.DrawMaxClimb(graph);
+ layerGridGraph.maxStepHeight = Mathf.Clamp(layerGridGraph.maxStepHeight, 0, layerGridGraph.characterHeight);
+
+ if (layerGridGraph.maxStepHeight >= layerGridGraph.characterHeight) {
+ EditorGUILayout.HelpBox("Max step height needs to be smaller or equal to character height", MessageType.Info);
+ }
+ }
+
+ protected override void DrawCollisionEditor (GraphCollision collision) {
+ base.DrawCollisionEditor(collision);
+
+ if (collision.thickRaycast) {
+ EditorGUILayout.HelpBox("Note: Thick raycast cannot be used with this graph type", MessageType.Error);
+ }
+ }
+
+ protected override void DrawUse2DPhysics (GraphCollision collision) {
+ // 2D physics does not make sense for a layered grid graph
+ collision.use2D = false;
+ }
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/LayerGridGraphEditor.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/LayerGridGraphEditor.cs.meta
new file mode 100644
index 0000000..3b03a7e
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/LayerGridGraphEditor.cs.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 889543b216eb24cca9740e3cca767f64
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/NavMeshGeneratorEditor.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/NavMeshGeneratorEditor.cs
new file mode 100644
index 0000000..84c8f89
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/NavMeshGeneratorEditor.cs
@@ -0,0 +1,49 @@
+using UnityEngine;
+using UnityEditor;
+
+namespace Pathfinding {
+ [CustomGraphEditor(typeof(NavMeshGraph), "Navmesh Graph")]
+ public class NavMeshGraphEditor : GraphEditor {
+ public override void OnInspectorGUI (NavGraph target) {
+ var graph = target as NavMeshGraph;
+
+ graph.sourceMesh = ObjectField("Source Mesh", graph.sourceMesh, typeof(Mesh), false, true) as Mesh;
+
+ graph.offset = EditorGUILayout.Vector3Field("Offset", graph.offset);
+
+ graph.rotation = EditorGUILayout.Vector3Field("Rotation", graph.rotation);
+
+ graph.scale = EditorGUILayout.FloatField(new GUIContent("Scale", "Scale of the mesh"), graph.scale);
+ graph.scale = Mathf.Abs(graph.scale) < 0.01F ? (graph.scale >= 0 ? 0.01F : -0.01F) : graph.scale;
+
+ #pragma warning disable 618
+ if (graph.nearestSearchOnlyXZ) {
+ graph.nearestSearchOnlyXZ = EditorGUILayout.Toggle(new GUIContent("Nearest node queries in XZ space",
+ "Recomended for single-layered environments.\nFaster but can be inacurate esp. in multilayered contexts."), graph.nearestSearchOnlyXZ);
+
+ EditorGUILayout.HelpBox("The global toggle for node queries in XZ space has been deprecated. Use the NNConstraint settings instead.", MessageType.Warning);
+ }
+ #pragma warning restore 618
+
+ graph.recalculateNormals = EditorGUILayout.Toggle(new GUIContent("Recalculate Normals", "Disable for spherical graphs or other complicated surfaces that allow the agents to e.g walk on walls or ceilings. See docs for more info."), graph.recalculateNormals);
+ graph.enableNavmeshCutting = EditorGUILayout.Toggle(new GUIContent("Affected by navmesh cuts", "Makes this graph affected by NavmeshCut and NavmeshAdd components. See the documentation for more info."), graph.enableNavmeshCutting);
+ if (graph.enableNavmeshCutting) {
+ EditorGUI.indentLevel++;
+ EditorGUI.BeginChangeCheck();
+ var newValue = EditorGUILayout.FloatField(new GUIContent("Agent radius", "Navmesh cuts can optionally be expanded by the agent radius"), graph.navmeshCuttingCharacterRadius);
+ if (EditorGUI.EndChangeCheck()) {
+ graph.navmeshCuttingCharacterRadius = Mathf.Max(0, newValue);
+ graph.navmeshUpdateData.ReloadAllTiles();
+ }
+ EditorGUI.indentLevel--;
+ }
+
+ GUILayout.BeginHorizontal();
+ GUILayout.Space(18);
+ graph.showMeshSurface = GUILayout.Toggle(graph.showMeshSurface, new GUIContent("Show surface", "Toggles gizmos for drawing the surface of the mesh"), EditorStyles.miniButtonLeft);
+ graph.showMeshOutline = GUILayout.Toggle(graph.showMeshOutline, new GUIContent("Show outline", "Toggles gizmos for drawing an outline of the nodes"), EditorStyles.miniButtonMid);
+ graph.showNodeConnections = GUILayout.Toggle(graph.showNodeConnections, new GUIContent("Show connections", "Toggles gizmos for drawing node connections"), EditorStyles.miniButtonRight);
+ GUILayout.EndHorizontal();
+ }
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/NavMeshGeneratorEditor.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/NavMeshGeneratorEditor.cs.meta
new file mode 100644
index 0000000..7e33f80
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/NavMeshGeneratorEditor.cs.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: c4ad80e9395de47c78e55cbaae437660
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/PointGeneratorEditor.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/PointGeneratorEditor.cs
new file mode 100644
index 0000000..b573c58
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/PointGeneratorEditor.cs
@@ -0,0 +1,56 @@
+using UnityEngine;
+using UnityEditor;
+
+namespace Pathfinding {
+ [CustomGraphEditor(typeof(PointGraph), "Point Graph")]
+ public class PointGraphEditor : GraphEditor {
+ static readonly GUIContent[] nearestNodeDistanceModeLabels = {
+ new GUIContent("Node"),
+ new GUIContent("Connection (slower)"),
+ };
+
+ public override void OnInspectorGUI (NavGraph target) {
+ var graph = target as PointGraph;
+
+ graph.root = ObjectField(new GUIContent("Root", "All childs of this object will be used as nodes, if it is not set, a tag search will be used instead (see below)"), graph.root, typeof(Transform), true, false) as Transform;
+
+ graph.recursive = EditorGUILayout.Toggle(new GUIContent("Recursive", "Should childs of the childs in the root GameObject be searched"), graph.recursive);
+ graph.searchTag = EditorGUILayout.TagField(new GUIContent("Tag", "If root is not set, all objects with this tag will be used as nodes"), graph.searchTag);
+
+ if (graph.root != null) {
+ EditorGUILayout.HelpBox("All childs "+(graph.recursive ? "and sub-childs " : "") +"of 'root' will be used as nodes\nSet root to null to use a tag search instead", MessageType.None);
+ } else {
+ EditorGUILayout.HelpBox("All object with the tag '"+graph.searchTag+"' will be used as nodes"+(graph.searchTag == "Untagged" ? "\nNote: the tag 'Untagged' cannot be used" : ""), MessageType.None);
+ }
+
+ graph.maxDistance = EditorGUILayout.FloatField(new GUIContent("Max Distance", "The max distance in world space for a connection to be valid. A zero counts as infinity"), graph.maxDistance);
+
+ graph.limits = EditorGUILayout.Vector3Field("Max Distance (axis aligned)", graph.limits);
+
+ graph.raycast = EditorGUILayout.Toggle(new GUIContent("Raycast", "Use raycasting to check if connections are valid between each pair of nodes"), graph.raycast);
+
+ if (graph.raycast) {
+ EditorGUI.indentLevel++;
+
+ graph.use2DPhysics = EditorGUILayout.Toggle(new GUIContent("Use 2D Physics", "If enabled, all raycasts will use the Unity 2D Physics API instead of the 3D one."), graph.use2DPhysics);
+ graph.thickRaycast = EditorGUILayout.Toggle(new GUIContent("Thick Raycast", "A thick raycast checks along a thick line with radius instead of just along a line"), graph.thickRaycast);
+
+ if (graph.thickRaycast) {
+ EditorGUI.indentLevel++;
+ graph.thickRaycastRadius = EditorGUILayout.FloatField(new GUIContent("Raycast Radius", "The radius in world units for the thick raycast"), graph.thickRaycastRadius);
+ EditorGUI.indentLevel--;
+ }
+
+ graph.mask = EditorGUILayoutx.LayerMaskField("Mask", graph.mask);
+ EditorGUI.indentLevel--;
+ }
+
+ graph.optimizeForSparseGraph = EditorGUILayout.Toggle(new GUIContent("Optimize For Sparse Graph", "Check online documentation for more information."), graph.optimizeForSparseGraph);
+ graph.nearestNodeDistanceMode = (PointGraph.NodeDistanceMode)EditorGUILayout.Popup(new GUIContent("Nearest node queries find closest"), (int)graph.nearestNodeDistanceMode, nearestNodeDistanceModeLabels);
+
+ if (graph.nearestNodeDistanceMode == PointGraph.NodeDistanceMode.Connection && !graph.optimizeForSparseGraph) {
+ EditorGUILayout.HelpBox("Connection mode can only be used if Optimize For Sparse Graph is enabled", MessageType.Error);
+ }
+ }
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/PointGeneratorEditor.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/PointGeneratorEditor.cs.meta
new file mode 100644
index 0000000..cf96f37
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/PointGeneratorEditor.cs.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: b563cffdf75ee4497b91d4ac8e106260
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/RecastGraphEditor.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/RecastGraphEditor.cs
new file mode 100644
index 0000000..52880dc
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/RecastGraphEditor.cs
@@ -0,0 +1,516 @@
+using UnityEngine;
+using UnityEditor;
+using Pathfinding.Graphs.Navmesh;
+using UnityEditorInternal;
+
+namespace Pathfinding {
+ /// <summary>Editor for the RecastGraph.</summary>
+ [CustomGraphEditor(typeof(RecastGraph), "Recast Graph")]
+ public class RecastGraphEditor : GraphEditor {
+ public static bool tagMaskFoldout;
+ public static bool meshesUnreadableAtRuntimeFoldout;
+ ReorderableList tagMaskList;
+ ReorderableList perLayerModificationsList;
+
+ public enum UseTiles {
+ UseTiles = 0,
+ DontUseTiles = 1
+ }
+
+ static readonly GUIContent[] DimensionModeLabels = new [] {
+ new GUIContent("2D"),
+ new GUIContent("3D"),
+ };
+
+ static Rect SliceColumn (ref Rect rect, float width, float spacing = 0) {
+ return GUIUtilityx.SliceColumn(ref rect, width, spacing);
+ }
+
+ static void DrawIndentedList (ReorderableList list) {
+ GUILayout.BeginHorizontal();
+ GUILayout.Space(EditorGUI.IndentedRect(default).xMin);
+ list.DoLayoutList();
+ GUILayout.Space(3);
+ GUILayout.EndHorizontal();
+ }
+
+ static void DrawColliderDetail (RecastGraph.CollectionSettings settings) {
+ const float LowestApproximationError = 0.5f;
+ settings.colliderRasterizeDetail = EditorGUILayout.Slider(new GUIContent("Round Collider Detail", "Controls the detail of the generated sphere and capsule meshes. "+
+ "Higher values may increase navmesh quality slightly, and lower values improve graph scanning performance."), Mathf.Round(10*settings.colliderRasterizeDetail)*0.1f, 0, 1.0f / LowestApproximationError);
+ }
+
+ void DrawCollectionSettings (RecastGraph.CollectionSettings settings, RecastGraph.DimensionMode dimensionMode) {
+ settings.collectionMode = (RecastGraph.CollectionSettings.FilterMode)EditorGUILayout.EnumPopup("Filter objects by", settings.collectionMode);
+
+ if (settings.collectionMode == RecastGraph.CollectionSettings.FilterMode.Layers) {
+ settings.layerMask = EditorGUILayoutx.LayerMaskField("Layer Mask", settings.layerMask);
+ } else {
+ DrawIndentedList(tagMaskList);
+ }
+
+ if (dimensionMode == RecastGraph.DimensionMode.Dimension3D) {
+ settings.rasterizeTerrain = EditorGUILayout.Toggle(new GUIContent("Rasterize Terrains", "Should a rasterized terrain be included"), settings.rasterizeTerrain);
+ if (settings.rasterizeTerrain) {
+ EditorGUI.indentLevel++;
+ settings.rasterizeTrees = EditorGUILayout.Toggle(new GUIContent("Rasterize Trees", "Rasterize tree colliders on terrains. " +
+ "If the tree prefab has a collider, that collider will be rasterized. " +
+ "Otherwise a simple box collider will be used and the script will " +
+ "try to adjust it to the tree's scale, it might not do a very good job though so " +
+ "an attached collider is preferable."), settings.rasterizeTrees);
+ settings.terrainHeightmapDownsamplingFactor = EditorGUILayout.IntField(new GUIContent("Heightmap Downsampling", "How much to downsample the terrain's heightmap. A lower value is better, but slower to scan"), settings.terrainHeightmapDownsamplingFactor);
+ settings.terrainHeightmapDownsamplingFactor = Mathf.Max(1, settings.terrainHeightmapDownsamplingFactor);
+ EditorGUI.indentLevel--;
+ }
+
+ settings.rasterizeMeshes = EditorGUILayout.Toggle(new GUIContent("Rasterize Meshes", "Should meshes be rasterized and used for building the navmesh"), settings.rasterizeMeshes);
+ settings.rasterizeColliders = EditorGUILayout.Toggle(new GUIContent("Rasterize Colliders", "Should colliders be rasterized and used for building the navmesh"), settings.rasterizeColliders);
+ } else {
+ // Colliders are always rasterized in 2D mode
+ EditorGUI.BeginDisabledGroup(true);
+ EditorGUILayout.Toggle(new GUIContent("Rasterize Colliders", "Should colliders be rasterized and used for building the navmesh. In 2D mode, this is always enabled."), true);
+ EditorGUI.EndDisabledGroup();
+ }
+
+ if (settings.rasterizeMeshes && settings.rasterizeColliders && dimensionMode == RecastGraph.DimensionMode.Dimension3D) {
+ EditorGUILayout.HelpBox("You are rasterizing both meshes and colliders. This is likely just duplicate work if the colliders and meshes are similar in shape. You can use the RecastMeshObj component" +
+ " to always include some specific objects regardless of what the above settings are set to.", MessageType.Info);
+ }
+ }
+
+ public override void OnEnable () {
+ base.OnEnable();
+ var graph = target as RecastGraph;
+ tagMaskList = new ReorderableList(graph.collectionSettings.tagMask, typeof(string), true, true, true, true) {
+ drawElementCallback = (Rect rect, int index, bool active, bool isFocused) => {
+ graph.collectionSettings.tagMask[index] = EditorGUI.TagField(rect, graph.collectionSettings.tagMask[index]);
+ },
+ drawHeaderCallback = (Rect rect) => {
+ GUI.Label(rect, "Tag mask");
+ },
+ elementHeight = EditorGUIUtility.singleLineHeight,
+ onAddCallback = (ReorderableList list) => {
+ graph.collectionSettings.tagMask.Add("Untagged");
+ }
+ };
+
+ perLayerModificationsList = new ReorderableList(graph.perLayerModifications, typeof(RecastGraph.PerLayerModification), true, true, true, true) {
+ drawElementCallback = (Rect rect, int index, bool active, bool isFocused) => {
+ var element = graph.perLayerModifications[index];
+ var w = rect.width;
+ var spacing = EditorGUIUtility.standardVerticalSpacing;
+ element.layer = EditorGUI.LayerField(SliceColumn(ref rect, w * 0.3f, spacing), element.layer);
+
+ if (element.mode == RecastMeshObj.Mode.WalkableSurfaceWithTag) {
+ element.mode = (RecastMeshObj.Mode)EditorGUI.EnumPopup(SliceColumn(ref rect, w * 0.4f, spacing), element.mode);
+ element.surfaceID = Util.EditorGUILayoutHelper.TagField(rect, GUIContent.none, element.surfaceID, AstarPathEditor.EditTags);
+ element.surfaceID = Mathf.Clamp(element.surfaceID, 0, GraphNode.MaxTagIndex);
+ } else if (element.mode == RecastMeshObj.Mode.WalkableSurfaceWithSeam) {
+ element.mode = (RecastMeshObj.Mode)EditorGUI.EnumPopup(SliceColumn(ref rect, w * 0.4f, spacing), element.mode);
+ string helpTooltip = "All surfaces on this mesh will be walkable and a " +
+ "seam will be created between the surfaces on this mesh and the surfaces on other meshes (with a different surface id)";
+ GUI.Label(SliceColumn(ref rect, 70, spacing), new GUIContent("Surface ID", helpTooltip));
+ element.surfaceID = Mathf.Max(0, EditorGUI.IntField(rect, new GUIContent("", helpTooltip), element.surfaceID));
+ } else {
+ element.mode = (RecastMeshObj.Mode)EditorGUI.EnumPopup(rect, element.mode);
+ }
+
+ graph.perLayerModifications[index] = element;
+ },
+ drawHeaderCallback = (Rect rect) => {
+ GUI.Label(rect, "Per Layer Modifications");
+ },
+ elementHeight = EditorGUIUtility.singleLineHeight,
+ onAddCallback = (ReorderableList list) => {
+ // Find the first layer that is not already modified
+ var availableLayers = graph.collectionSettings.layerMask;
+ foreach (var mod in graph.perLayerModifications) {
+ availableLayers &= ~(1 << mod.layer);
+ }
+ var newMod = RecastGraph.PerLayerModification.Default;
+ for (int i = 0; i < 32; i++) {
+ if ((availableLayers & (1 << i)) != 0) {
+ newMod.layer = i;
+ break;
+ }
+ }
+ graph.perLayerModifications.Add(newMod);
+ }
+ };
+ }
+
+ public override void OnInspectorGUI (NavGraph target) {
+ var graph = target as RecastGraph;
+
+ Header("Shape");
+
+ graph.dimensionMode = (RecastGraph.DimensionMode)EditorGUILayout.Popup(new GUIContent("Dimensions", "Should the graph be for a 2D or 3D world?"), (int)graph.dimensionMode, DimensionModeLabels);
+ if (graph.dimensionMode == RecastGraph.DimensionMode.Dimension2D && Mathf.Abs(Vector3.Dot(Quaternion.Euler(graph.rotation) * Vector3.up, Vector3.forward)) < 0.99999f) {
+ EditorGUI.indentLevel++;
+ EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
+ GUILayout.Label(EditorGUIUtility.IconContent("console.warnicon"), GUILayout.ExpandWidth(false));
+ GUILayout.BeginVertical();
+ GUILayout.FlexibleSpace();
+ GUILayout.BeginHorizontal();
+ GUILayout.Label("Your graph is not in the XY plane");
+ if (GUILayout.Button("Align")) {
+ graph.rotation = new Vector3(-90, 0, 0);
+ graph.forcedBoundsCenter = new Vector3(graph.forcedBoundsCenter.x, graph.forcedBoundsCenter.y, -graph.forcedBoundsSize.y * 0.5f);
+ }
+ GUILayout.EndHorizontal();
+ GUILayout.FlexibleSpace();
+ GUILayout.EndVertical();
+ EditorGUILayout.EndHorizontal();
+ EditorGUI.indentLevel--;
+ }
+
+ // In 3D mode, we use the graph's center as the pivot point, but in 2D mode, we use the center of the base plane of the graph as the pivot point.
+ // This makes sense because in 2D mode, you typically want to set the base plane's center to Z=0, and you don't care much about the height of the graph.
+ var pivot = graph.dimensionMode == RecastGraph.DimensionMode.Dimension2D ? new Vector3(0.0f, -0.5f, 0.0f) : Vector3.zero;
+ var centerOffset = Quaternion.Euler(graph.rotation) * Vector3.Scale(graph.forcedBoundsSize, pivot);
+ var newCenter = EditorGUILayout.Vector3Field("Center", graph.forcedBoundsCenter + centerOffset);
+ var newSize = EditorGUILayout.Vector3Field("Size", graph.forcedBoundsSize);
+
+ // Make sure the bounding box is not infinitely thin along any axis
+ newSize = Vector3.Max(newSize, Vector3.one * 0.001f);
+
+ // Recalculate the center offset with the new size, and then adjust the center so that the pivot point stays the same if the size changes
+ centerOffset = Quaternion.Euler(graph.rotation) * Vector3.Scale(newSize, pivot);
+ graph.forcedBoundsCenter = RoundVector3(newCenter) - centerOffset;
+ graph.forcedBoundsSize = RoundVector3(newSize);
+
+ graph.rotation = RoundVector3(EditorGUILayout.Vector3Field("Rotation", graph.rotation));
+
+ long estWidth = Mathf.RoundToInt(Mathf.Ceil(graph.forcedBoundsSize.x / graph.cellSize));
+ long estDepth = Mathf.RoundToInt(Mathf.Ceil(graph.forcedBoundsSize.z / graph.cellSize));
+
+ EditorGUI.BeginDisabledGroup(true);
+ var estTilesX = (estWidth + graph.editorTileSize - 1) / graph.editorTileSize;
+ var estTilesZ = (estDepth + graph.editorTileSize - 1) / graph.editorTileSize;
+ var label = estWidth.ToString() + " x " + estDepth.ToString();
+ if (graph.useTiles) {
+ label += " voxels, divided into " + (estTilesX*estTilesZ) + " tiles";
+ }
+ EditorGUILayout.LabelField(new GUIContent("Size", "Based on the voxel size and the bounding box"), new GUIContent(label));
+ EditorGUI.EndDisabledGroup();
+
+ // Show a warning if the number of voxels is too large
+ if (estWidth*estDepth >= 3000*3000) {
+ GUIStyle helpBox = GUI.skin.FindStyle("HelpBox") ?? GUI.skin.FindStyle("Box");
+
+ Color preColor = GUI.color;
+ if (estWidth*estDepth >= 8192*8192) {
+ GUI.color = Color.red;
+ } else {
+ GUI.color = Color.yellow;
+ }
+
+ GUILayout.Label("Warning: Might take some time to calculate", helpBox);
+ GUI.color = preColor;
+ }
+
+ if (!editor.isPrefab) {
+ if (GUILayout.Button(new GUIContent("Snap bounds to scene", "Will snap the bounds of the graph to exactly contain all meshes in the scene that matches the masks."))) {
+ graph.SnapForceBoundsToScene();
+ GUI.changed = true;
+ }
+ }
+
+ Separator();
+ Header("Input Filtering");
+
+ DrawCollectionSettings(graph.collectionSettings, graph.dimensionMode);
+
+ Separator();
+ Header("Rasterization");
+
+ graph.cellSize = EditorGUILayout.FloatField(new GUIContent("Voxel Size", "Size of one voxel in world units"), graph.cellSize);
+ if (graph.cellSize < 0.001F) graph.cellSize = 0.001F;
+
+ graph.useTiles = (UseTiles)EditorGUILayout.EnumPopup("Use Tiles", graph.useTiles ? UseTiles.UseTiles : UseTiles.DontUseTiles) == UseTiles.UseTiles;
+
+ if (graph.useTiles) {
+ EditorGUI.indentLevel++;
+ graph.editorTileSize = EditorGUILayout.IntField(new GUIContent("Tile Size (voxels)", "Size in voxels of a single tile.\n" +
+ "This is the width of the tile.\n" +
+ "\n" +
+ "A large tile size can be faster to initially scan (but beware of out of memory issues if you try with a too large tile size in a large world)\n" +
+ "smaller tile sizes are (much) faster to update.\n" +
+ "\n" +
+ "Different tile sizes can affect the quality of paths. It is often good to split up huge open areas into several tiles for\n" +
+ "better quality paths, but too small tiles can lead to effects looking like invisible obstacles.\n\n" +
+ "Typical values are between 64 and 256"), graph.editorTileSize);
+ graph.editorTileSize = Mathf.Max(10, graph.editorTileSize);
+ EditorGUI.indentLevel--;
+ }
+
+ if (graph.dimensionMode == RecastGraph.DimensionMode.Dimension3D) {
+ graph.walkableHeight = EditorGUILayout.DelayedFloatField(new GUIContent("Character Height", "Minimum distance to the roof for an area to be walkable"), graph.walkableHeight);
+ graph.walkableHeight = Mathf.Max(graph.walkableHeight, 0);
+
+ graph.characterRadius = EditorGUILayout.FloatField(new GUIContent("Character Radius", "Radius of the character. It's good to add some margin.\nIn world units."), graph.characterRadius);
+ graph.characterRadius = Mathf.Max(graph.characterRadius, 0);
+
+ if (graph.characterRadius < graph.cellSize * 2) {
+ EditorGUILayout.HelpBox("For best navmesh quality, it is recommended to keep the character radius at least 2 times as large as the voxel size. Smaller voxels will give you higher quality navmeshes, but it will take more time to scan the graph.", MessageType.Warning);
+ }
+
+ graph.walkableClimb = EditorGUILayout.FloatField(new GUIContent("Max Step Height", "How high can the character step"), graph.walkableClimb);
+
+ // A walkableClimb higher than this can cause issues when generating the navmesh since then it can in some cases
+ // Both be valid for a character to walk under an obstacle and climb up on top of it (and that cannot be handled with a navmesh without links)
+ if (graph.walkableClimb >= graph.walkableHeight) {
+ graph.walkableClimb = graph.walkableHeight;
+ EditorGUILayout.HelpBox("Max Step Height should be less than Character Height. Clamping to " + graph.walkableHeight+".", MessageType.Warning);
+ } else if (graph.walkableClimb < 0) {
+ graph.walkableClimb = 0;
+ }
+ }
+
+ if (graph.dimensionMode == RecastGraph.DimensionMode.Dimension3D) {
+ graph.maxSlope = EditorGUILayout.Slider(new GUIContent("Max Slope", "Approximate maximum slope"), graph.maxSlope, 0F, 90F);
+ }
+
+ graph.maxEdgeLength = EditorGUILayout.FloatField(new GUIContent("Max Border Edge Length", "Maximum length of one border edge in the completed navmesh before it is split. A lower value can often yield better quality graphs, but don't use so low values so that you get a lot of thin triangles."), graph.maxEdgeLength);
+ graph.maxEdgeLength = graph.maxEdgeLength < graph.cellSize ? graph.cellSize : graph.maxEdgeLength;
+
+ // This is actually a float, but to make things easier for the user, we only allow picking integers. Small changes don't matter that much anyway.
+ graph.contourMaxError = EditorGUILayout.IntSlider(new GUIContent("Edge Simplification", "Simplifies the edges of the navmesh such that it is no more than this number of voxels away from the true value.\nIn voxels."), Mathf.RoundToInt(graph.contourMaxError), 0, 5);
+ graph.minRegionSize = EditorGUILayout.FloatField(new GUIContent("Min Region Size", "Small regions will be removed. In voxels"), graph.minRegionSize);
+
+ if (graph.dimensionMode == RecastGraph.DimensionMode.Dimension2D) {
+ graph.backgroundTraversability = (RecastGraph.BackgroundTraversability)EditorGUILayout.EnumPopup("Background traversability", graph.backgroundTraversability);
+ }
+
+ if (graph.collectionSettings.rasterizeColliders || (graph.dimensionMode == RecastGraph.DimensionMode.Dimension3D && graph.collectionSettings.rasterizeTerrain && graph.collectionSettings.rasterizeTrees)) {
+ DrawColliderDetail(graph.collectionSettings);
+ }
+
+ DrawIndentedList(perLayerModificationsList);
+
+ int seenLayers = 0;
+ for (int i = 0; i < graph.perLayerModifications.Count; i++) {
+ if ((seenLayers & 1 << graph.perLayerModifications[i].layer) != 0) {
+ EditorGUILayout.HelpBox("Duplicate layers. Each layer can only be modified by a single rule.", MessageType.Error);
+ break;
+ }
+ seenLayers |= 1 << graph.perLayerModifications[i].layer;
+ }
+
+ var countStillUnreadable = 0;
+ for (int i = 0; graph.meshesUnreadableAtRuntime != null && i < graph.meshesUnreadableAtRuntime.Count; i++) {
+ countStillUnreadable += graph.meshesUnreadableAtRuntime[i].Item2.isReadable ? 0 : 1;
+ }
+ if (countStillUnreadable > 0) {
+ GUILayout.BeginHorizontal();
+ GUILayout.Space(EditorGUI.IndentedRect(new Rect(0, 0, 0, 0)).xMin);
+ EditorGUILayout.BeginVertical(EditorStyles.helpBox);
+ GUILayout.BeginHorizontal();
+ GUILayout.BeginVertical();
+ GUILayout.FlexibleSpace();
+ meshesUnreadableAtRuntimeFoldout = GUILayout.Toggle(meshesUnreadableAtRuntimeFoldout, "", EditorStyles.foldout, GUILayout.Width(10));
+ GUILayout.FlexibleSpace();
+ GUILayout.EndVertical();
+
+ GUILayout.Label(EditorGUIUtility.IconContent("console.warnicon"), GUILayout.ExpandWidth(false));
+ GUILayout.Label(graph.meshesUnreadableAtRuntime.Count + " " + (graph.meshesUnreadableAtRuntime.Count > 1 ? "meshes" : "mesh") + " will be ignored if scanned in a standalone build, because they are marked as not readable." +
+ "If you plan to scan the graph in a standalone build, all included meshes must be marked as read/write in their import settings.", EditorStyles.wordWrappedMiniLabel);
+ // EditorGUI.DrawTextureTransparent() EditorGUIUtility.IconContent("console.warnicon")
+ GUILayout.EndHorizontal();
+
+ if (meshesUnreadableAtRuntimeFoldout) {
+ EditorGUILayout.Separator();
+ for (int i = 0; i < graph.meshesUnreadableAtRuntime.Count; i++) {
+ var(source, mesh) = graph.meshesUnreadableAtRuntime[i];
+ if (!mesh.isReadable) {
+ GUILayout.BeginHorizontal();
+ EditorGUI.BeginDisabledGroup(true);
+ EditorGUILayout.ObjectField(source, typeof(Mesh), true);
+ EditorGUILayout.ObjectField(mesh, typeof(Mesh), false);
+ EditorGUI.EndDisabledGroup();
+ if (GUILayout.Button("Make readable")) {
+ var importer = ModelImporter.GetAtPath(AssetDatabase.GetAssetPath(mesh)) as ModelImporter;
+ if (importer != null) {
+ importer.isReadable = true;
+ importer.SaveAndReimport();
+ }
+ }
+ GUILayout.EndHorizontal();
+ }
+ }
+ }
+ EditorGUILayout.EndVertical();
+ GUILayout.EndHorizontal();
+ }
+
+ Separator();
+ Header("Runtime Settings");
+
+ graph.enableNavmeshCutting = EditorGUILayout.Toggle(new GUIContent("Affected by Navmesh Cuts", "Makes this graph affected by NavmeshCut and NavmeshAdd components. See the documentation for more info."), graph.enableNavmeshCutting);
+
+ Separator();
+ Header("Debug");
+ GUILayout.BeginHorizontal();
+ GUILayout.Space(18);
+ graph.showMeshSurface = GUILayout.Toggle(graph.showMeshSurface, new GUIContent("Show surface", "Toggles gizmos for drawing the surface of the mesh"), EditorStyles.miniButtonLeft);
+ graph.showMeshOutline = GUILayout.Toggle(graph.showMeshOutline, new GUIContent("Show outline", "Toggles gizmos for drawing an outline of the nodes"), EditorStyles.miniButtonMid);
+ graph.showNodeConnections = GUILayout.Toggle(graph.showNodeConnections, new GUIContent("Show connections", "Toggles gizmos for drawing node connections"), EditorStyles.miniButtonRight);
+ GUILayout.EndHorizontal();
+
+
+ Separator();
+ Header("Advanced");
+
+ graph.relevantGraphSurfaceMode = (RecastGraph.RelevantGraphSurfaceMode)EditorGUILayout.EnumPopup(new GUIContent("Relevant Graph Surface Mode",
+ "Require every region to have a RelevantGraphSurface component inside it.\n" +
+ "A RelevantGraphSurface component placed in the scene specifies that\n" +
+ "the navmesh region it is inside should be included in the navmesh.\n\n" +
+ "If this is set to OnlyForCompletelyInsideTile\n" +
+ "a navmesh region is included in the navmesh if it\n" +
+ "has a RelevantGraphSurface inside it, or if it\n" +
+ "is adjacent to a tile border. This can leave some small regions\n" +
+ "which you didn't want to have included because they are adjacent\n" +
+ "to tile borders, but it removes the need to place a component\n" +
+ "in every single tile, which can be tedious (see below).\n\n" +
+ "If this is set to RequireForAll\n" +
+ "a navmesh region is included only if it has a RelevantGraphSurface\n" +
+ "inside it. Note that even though the navmesh\n" +
+ "looks continous between tiles, the tiles are computed individually\n" +
+ "and therefore you need a RelevantGraphSurface component for each\n" +
+ "region and for each tile."),
+ graph.relevantGraphSurfaceMode);
+
+ #pragma warning disable 618
+ if (graph.nearestSearchOnlyXZ) {
+ graph.nearestSearchOnlyXZ = EditorGUILayout.Toggle(new GUIContent("Nearest node queries in XZ space",
+ "Recomended for single-layered environments.\nFaster but can be inacurate esp. in multilayered contexts."), graph.nearestSearchOnlyXZ);
+
+ EditorGUILayout.HelpBox("The global toggle for node queries in XZ space has been deprecated. Use the NNConstraint settings instead.", MessageType.Warning);
+ }
+ #pragma warning restore 618
+
+ if (GUILayout.Button("Export to .obj file")) {
+ editor.RunTask(() => ExportToFile(graph));
+ }
+ }
+
+ static readonly Vector3[] handlePoints = new [] { new Vector3(-1, 0, 0), new Vector3(1, 0, 0), new Vector3(0, 0, -1), new Vector3(0, 0, 1), new Vector3(0, 1, 0), new Vector3(0, -1, 0) };
+
+ public override void OnSceneGUI (NavGraph target) {
+ var graph = target as RecastGraph;
+
+ Handles.matrix = Matrix4x4.identity;
+ Handles.color = AstarColor.BoundsHandles;
+ Handles.CapFunction cap = Handles.CylinderHandleCap;
+
+ var center = graph.forcedBoundsCenter;
+ Matrix4x4 matrix = Matrix4x4.TRS(center, Quaternion.Euler(graph.rotation), graph.forcedBoundsSize * 0.5f);
+
+ if (Tools.current == Tool.Scale) {
+ const float HandleScale = 0.1f;
+
+ Vector3 mn = Vector3.zero;
+ Vector3 mx = Vector3.zero;
+ EditorGUI.BeginChangeCheck();
+ for (int i = 0; i < handlePoints.Length; i++) {
+ var ps = matrix.MultiplyPoint3x4(handlePoints[i]);
+ Vector3 p = matrix.inverse.MultiplyPoint3x4(Handles.Slider(ps, ps - center, HandleScale*HandleUtility.GetHandleSize(ps), cap, 0));
+
+ if (i == 0) {
+ mn = mx = p;
+ } else {
+ mn = Vector3.Min(mn, p);
+ mx = Vector3.Max(mx, p);
+ }
+ }
+
+ if (EditorGUI.EndChangeCheck()) {
+ graph.forcedBoundsCenter = matrix.MultiplyPoint3x4((mn + mx) * 0.5f);
+ graph.forcedBoundsSize = Vector3.Scale(graph.forcedBoundsSize, (mx - mn) * 0.5f);
+ }
+ } else if (Tools.current == Tool.Move) {
+ EditorGUI.BeginChangeCheck();
+ center = Handles.PositionHandle(center, Tools.pivotRotation == PivotRotation.Global ? Quaternion.identity : Quaternion.Euler(graph.rotation));
+
+ if (EditorGUI.EndChangeCheck() && Tools.viewTool != ViewTool.Orbit) {
+ graph.forcedBoundsCenter = center;
+ }
+ } else if (Tools.current == Tool.Rotate) {
+ EditorGUI.BeginChangeCheck();
+ var rot = Handles.RotationHandle(Quaternion.Euler(graph.rotation), graph.forcedBoundsCenter);
+
+ if (EditorGUI.EndChangeCheck() && Tools.viewTool != ViewTool.Orbit) {
+ graph.rotation = rot.eulerAngles;
+ }
+ }
+ }
+
+ /// <summary>Exports the INavmesh graph to a .obj file</summary>
+ public static void ExportToFile (NavmeshBase target) {
+ if (target == null) return;
+
+ NavmeshTile[] tiles = target.GetTiles();
+
+ if (tiles == null) {
+ if (EditorUtility.DisplayDialog("Scan graph before exporting?", "The graph does not contain any mesh data. Do you want to scan it?", "Ok", "Cancel")) {
+ AstarPathEditor.MenuScan();
+ tiles = target.GetTiles();
+ if (tiles == null) return;
+ } else {
+ return;
+ }
+ }
+
+ string path = EditorUtility.SaveFilePanel("Export .obj", "", "navmesh.obj", "obj");
+ if (path == "") return;
+
+ //Generate .obj
+ var sb = new System.Text.StringBuilder();
+
+ string name = System.IO.Path.GetFileNameWithoutExtension(path);
+
+ sb.Append("g ").Append(name).AppendLine();
+
+ //Vertices start from 1
+ int vCount = 1;
+
+ //Define single texture coordinate to zero
+ sb.Append("vt 0 0\n");
+
+ for (int t = 0; t < tiles.Length; t++) {
+ NavmeshTile tile = tiles[t];
+
+ if (tile == null) continue;
+
+ var vertices = tile.verts;
+
+ //Write vertices
+ for (int i = 0; i < vertices.Length; i++) {
+ var v = (Vector3)vertices[i];
+ sb.Append(string.Format("v {0} {1} {2}\n", -v.x, v.y, v.z));
+ }
+
+ //Write triangles
+ TriangleMeshNode[] nodes = tile.nodes;
+ for (int i = 0; i < nodes.Length; i++) {
+ TriangleMeshNode node = nodes[i];
+ if (node == null) {
+ Debug.LogError("Node was null or no TriangleMeshNode. Critical error. Graph type " + target.GetType().Name);
+ return;
+ }
+ if (node.GetVertexArrayIndex(0) < 0 || node.GetVertexArrayIndex(0) >= vertices.Length) throw new System.Exception("ERR");
+
+ sb.Append(string.Format("f {0}/1 {1}/1 {2}/1\n", (node.GetVertexArrayIndex(0) + vCount), (node.GetVertexArrayIndex(1) + vCount), (node.GetVertexArrayIndex(2) + vCount)));
+ }
+
+ vCount += vertices.Length;
+ }
+
+ string obj = sb.ToString();
+
+ using (var sw = new System.IO.StreamWriter(path)) {
+ sw.Write(obj);
+ }
+ }
+ }
+}
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/RecastGraphEditor.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/RecastGraphEditor.cs.meta
new file mode 100644
index 0000000..ed531b4
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/RecastGraphEditor.cs.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 1e335b837bf0e437f865b313cd5ac11c
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}