using UnityEngine;
using UnityEditor;
using Pathfinding.Graphs.Navmesh;
using UnityEditorInternal;
namespace Pathfinding {
/// Editor for the RecastGraph.
[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;
}
}
}
/// Exports the INavmesh graph to a .obj file
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);
}
}
}
}