diff options
author | chai <215380520@qq.com> | 2024-05-23 10:08:29 +0800 |
---|---|---|
committer | chai <215380520@qq.com> | 2024-05-23 10:08:29 +0800 |
commit | 8722a9920c1f6119bf6e769cba270e63097f8e25 (patch) | |
tree | 2eaf9865de7fb1404546de4a4296553d8f68cc3b /Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors | |
parent | 3ba4020b69e5971bb0df7ee08b31d10ea4d01937 (diff) |
+ astar project
Diffstat (limited to 'Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors')
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} |