summaryrefslogtreecommitdiff
path: root/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/RecastGraphEditor.cs
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/RecastGraphEditor.cs
parent3ba4020b69e5971bb0df7ee08b31d10ea4d01937 (diff)
+ astar project
Diffstat (limited to 'Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/RecastGraphEditor.cs')
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Editor/GraphEditors/RecastGraphEditor.cs516
1 files changed, 516 insertions, 0 deletions
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);
+ }
+ }
+ }
+}