/* This file is part of the "NavMesh Extension" project by Rebound Games. * You are only allowed to use these resources if you've bought them directly or indirectly * from Rebound Games. You shall not license, sublicense, sell, resell, transfer, assign, * distribute or otherwise make available to any third party the Service or the Content. */ using UnityEngine; using UnityEditor; using System.Linq; using System.Collections; using System.Collections.Generic; using System.IO; namespace NavMeshExtension { /// /// Custom Editor for editing vertices and exporting the mesh. /// [CustomEditor(typeof(NavMeshObject))] public class NavMeshObjectEditor : Editor { //navmesh object reference private NavMeshObject script; //export path private const string assetPath = "Assets/NavMeshExtension/Prefabs/"; //scene view help texts private const string editOnText = "Click: Place Vertices\n" + "Ctrl+Click: Submesh\n" + "Always exit Edit Mode!"; private const string editOffText = "Click vertices: Select\n" + "Right-click: Deselect\n" + "BS: Delete Selection"; //converted array of vertex positions private Vector3[] allPoints; //indices of selected vertices private List selected = new List(); //whether or not placement mode is active private static bool placing = false; //undo/redo flag for eventually disabling placing private static bool undoRedo = false; //clicked mouse position in the scene private Vector3 mousePosition; //vertex index near mouse position private int dragIndex; void OnEnable() { script = (NavMeshObject)target; } void OnDisable() { //if undo or redo was performed, //don't toggle placing - and vice versa if (undoRedo) PerformUndoRedo(); else placing = false; } /// /// Custom inspector override for buttons. /// public override void OnInspectorGUI() { DrawDefaultInspector(); EditorGUILayout.Space(); //display current count of placed vertices in the active submesh EditorGUILayout.LabelField("Current Vertices: " + script.current.Count); //enter placement mode if (!placing && GUILayout.Button("Edit Mode: Off")) { Undo.RegisterCompleteObjectUndo(script, "Edit On"); //if possible, create new submesh if (CheckSubMesh()) { GameObject newObj = script.CreateSubMesh(); Undo.RegisterCreatedObjectUndo(newObj, "Edit On"); } //clear handle selections selected.Clear(); placing = true; } GUI.color = Color.yellow; //leave placement mode and try to combine submeshes if (placing && GUILayout.Button("Edit Mode: On")) { Undo.RegisterCompleteObjectUndo(script, "Edit Off"); //if possible, combine submeshes if (CheckCombine()) { //get all mesh filters MeshFilter[] meshFilters = script.GetComponentsInChildren(); Undo.RecordObject(meshFilters[0], "Edit Off"); //let the script combine them script.Combine(); for (int i = 1; i < meshFilters.Length; i++) { Undo.DestroyObjectImmediate(meshFilters[i].gameObject); } } placing = false; } GUI.color = Color.white; //export mesh to asset and prefab if (GUILayout.Button("Save as Prefab")) { //get gameobject and its mesh filter GameObject gObj = script.gameObject; Mesh mesh = gObj.GetComponent().sharedMesh; if (!mesh) { Debug.LogWarning("Could not save as prefab, asset does not have a Mesh."); return; } if (AssetDatabase.Contains(mesh)) { Debug.Log("Mesh asset already exists. Cancelling."); return; } //get the current unix timestamp for a unique naming scheme var epochStart = new System.DateTime(1970, 1, 1, 0, 0, 0, System.DateTimeKind.Utc); string timestamp = (System.DateTime.UtcNow - epochStart).TotalSeconds.ToString("F0"); string assetName = "NavMesh_"; //check that the folder does exist string dir = Path.GetDirectoryName(assetPath); if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); AssetDatabase.ImportAsset(dir); } //create the mesh asset at the path specified, with unique name AssetDatabase.CreateAsset(mesh, assetPath + assetName + timestamp + ".asset"); AssetDatabase.SaveAssets(); //create the prefab of this gameobject at the path specified, with the same name PrefabUtility.CreatePrefab(assetPath + assetName + timestamp + ".prefab", gObj, ReplacePrefabOptions.ConnectToPrefab); //rename instance to prefab name gObj.name = assetName + timestamp; } if (GUILayout.Button("Create Mesh")) { script.CreateMeshFromPoints(); } } /// /// Draw Scene GUI handles, circles and outlines for submesh vertices. /// public void OnSceneGUI() { //get world positions of vertices allPoints = ConvertAllPoints(); //create a ray to get where we clicked in the scene view and pass in mouse position Ray worldRay = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition); RaycastHit hitInfo; Event e = Event.current; //this prevents selecting other objects in the scene int controlID = GUIUtility.GetControlID(FocusType.Passive); HandleUtility.AddDefaultControl(controlID); //find index of closest vertex, if any dragIndex = FindClosest(); if (!placing && e.type == EventType.KeyDown && e.keyCode == KeyCode.Backspace) { e.Use(); Undo.RegisterCompleteObjectUndo(script, "Delete Vertex"); Undo.RecordObject(script.GetComponent().sharedMesh, "Delete Vertex"); DeleteSelected(); return; } //in the edit mode, ray hit something if (placing) { Tools.current = Tool.None; Handles.BeginGUI(); GUILayout.Window(2, new Rect(Screen.width - 157, Screen.height - 100, 100, 50), (id) => { GUILayout.Label(editOnText); }, "Control Info Box"); Handles.EndGUI(); if (Physics.Raycast(worldRay, out hitInfo)) { //the actual hit position mousePosition = hitInfo.point; //place new point if the left mouse button was clicked if (e.type == EventType.MouseUp && e.button == 0 && !e.alt) { Undo.RegisterCompleteObjectUndo(script, "Add Vertex"); //get current submesh vertex count int currentCount = script.current.Count; //create a new submesh, if control was hold in addition to the mouse click //or if autoSplit is true and the current count exceeds splitAt if ((e.control && currentCount >= 3) || (script.autoSplit && currentCount >= script.splitAt)) { GameObject newObj = script.CreateSubMesh(); Undo.RegisterCreatedObjectUndo(newObj, "Add Vertex"); } //call this method when you've used an event. //the event's type will be set to EventType.Used, //causing other GUI elements to ignore it e.Use(); //add point to existing vertex, if near any //but don't add it to the same submesh (closed mesh) //otherwise just add a new vertex position if (script.current.Contains(dragIndex)) script.AddPoint(script.transform.TransformPoint(script.list[dragIndex])); else if (dragIndex >= 0) script.AddPoint(dragIndex); else script.AddPoint(mousePosition); //invoke new mesh calculation Undo.RegisterCompleteObjectUndo(script.subMesh, "Add Vertex"); script.CreateMesh(); return; } } } //not in edit mode if (!placing) { Handles.BeginGUI(); GUILayout.Window(2, new Rect(Screen.width - 150, Screen.height - 100, 100, 50), (id) => { GUILayout.Label(editOffText); }, "Control Info Box"); Handles.EndGUI(); //clicking near vertices will select them and show handles if (e.type == EventType.MouseUp && e.button == 0 && !e.alt) { //select/unselect vertex point if (dragIndex >= 0) { if (!selected.Contains(dragIndex)) selected.Add(dragIndex); else selected.Remove(dragIndex); SceneView.RepaintAll(); } else if(selected.Count == 0) Selection.activeObject = null; } //unselect all vertices else if (e.type == EventType.MouseUp && e.button == 1 && !e.alt) { selected.Clear(); } } //draw scene gizmos DrawSelectedHandles(); DrawPolygonOutline(); //handle undo of vertex modifications (redraw mesh) if (e.type == EventType.ValidateCommand && e.commandName == "UndoRedoPerformed") undoRedo = true; HandleUtility.AddDefaultControl(-1); HandleUtility.Repaint(); } private void PerformUndoRedo() { undoRedo = false; if (script.current.Count > 1 && script.subMesh) { script.subMesh.triangles = script.RecalculateTriangles(null); OptimizeMesh(script.subMesh); return; } Mesh sharedMesh = script.GetComponent().sharedMesh; if (!sharedMesh) return; List triangles = new List(); for (int i = 0; i < script.subPoints.Count; i++) triangles.AddRange(script.RecalculateTriangles(script.subPoints[i].list)); sharedMesh.triangles = new int[sharedMesh.vertexCount * 3]; script.UpdateMesh(ConvertAllPoints()); sharedMesh.triangles = triangles.ToArray(); if (!placing) script.Combine(); } //check if a combine of submeshes is possible private bool CheckCombine() { NavMeshManagerEditor.GetSceneView().Focus(); //get count of all submeshes int subPointsCount = script.subPoints.Count; if (subPointsCount > 0) { //remove submesh without references if (script.subPoints[subPointsCount - 1].list.Count == 0) script.subPoints.RemoveAt(subPointsCount - 1); else if (script.subPoints[subPointsCount - 1].list.Count <= 2) { NavMeshManagerEditor.ShowNotification("Can't combine submeshes.\nYou haven't placed enough points."); return false; } } return true; } //check if creating a new submesh is possible private bool CheckSubMesh() { NavMeshManagerEditor.GetSceneView().Focus(); //get count of all submeshes int subPointsCount = script.subPoints.Count; if (subPointsCount > 0 && script.subPoints[subPointsCount - 1].list.Count <= 2) { NavMeshManagerEditor.ShowNotification("Resuming current submesh."); return false; } return true; } //find closest vertex to the mouse position private int FindClosest() { //initialize variables List closest = new List(); Vector2 mousePos = Event.current.mousePosition; int near = -1; //loop over vertices to find the nearest ones for (int i = 0; i < allPoints.Length; i++) { Vector2 screenPoint = HandleUtility.WorldToGUIPoint(allPoints[i]); if (Vector2.Distance(screenPoint, mousePos) < 10) closest.Add(i); } //don't do further calculation in some cases if (closest.Count == 0) return near; else if (closest.Count == 1) return closest[0]; else { //there are more than a few vertices near the mouse position, //here only the closest vertex to the camera should matter Vector3 camPos = Camera.current.transform.position; float nearDist = float.MaxValue; //loop over all vertices and get the one near to the camera for (int i = 0; i < closest.Count; i++) { float dist = Vector3.Distance(allPoints[closest[i]], camPos); if (dist < nearDist) { nearDist = dist; near = closest[i]; } } } closest.Clear(); return near; } //convert relative vertex positions to world positions private Vector3[] ConvertAllPoints() { int count = script.list.Count; List all = new List(); for (int i = 0; i < count; i++) all.Add(script.transform.TransformPoint(script.list[i])); return all.ToArray(); } //delete previously selected vertices and rebuild mesh private void DeleteSelected() { //get mesh references MeshFilter filter = script.GetComponent(); List vertices = new List(filter.sharedMesh.vertices); //filter selected list for unique entries selected = selected.Distinct().ToList(); selected = selected.OrderByDescending(x => x).ToList(); //loop over selected vertex indices for (int i = 0; i < selected.Count; i++) { //remove index from mesh vertices int index = selected[i]; vertices.RemoveAt(index); script.list.RemoveAt(index); //loop over submeshes and remove it there too for (int j = 0; j < script.subPoints.Count; j++) { script.subPoints[j].list.Remove(index); //decrease higher entries, as the array is smaller now for (int k = 0; k < script.subPoints[j].list.Count; k++) { if (script.subPoints[j].list[k] >= index) script.subPoints[j].list[k] -= 1; } } } //clear selection selected.Clear(); //loop over submeshes to remove obsolete indices, //e.g. if a submesh has only 2 vertices after removal for (int i = script.subPoints.Count - 1; i >= 0; i--) { //check for vertex count if (script.subPoints[i].list.Count <= 2) { //construct a combined list with all indices List allIndices = new List(); for (int j = 0; j < script.subPoints.Count; j++) allIndices.AddRange(script.subPoints[j].list); //check whether an index occurs more than once List duplicates = allIndices.GroupBy(x => x) .Where(x => x.Count() > 1) .Select(x => x.Key) .ToList(); //if an index in this submesh is not being used in other //submeshes anymore, this means that we can remove it too for (int j = 0; j < script.subPoints[i].list.Count; j++) if (!duplicates.Contains(script.subPoints[i].list[j])) selected.Add(script.subPoints[i].list[j]); //delete this submesh entry script.subPoints.RemoveAt(i); } } //recalculate triangles for complete mesh List triangles = new List(); for (int i = 0; i < script.subPoints.Count; i++) triangles.AddRange(script.RecalculateTriangles(script.subPoints[i].list)); //assign triangles and update vertices filter.sharedMesh.triangles = triangles.ToArray(); script.list = vertices; script.UpdateMesh(ConvertAllPoints()); //recursively delete the remaining obsolete indices //which were found by looking through all submeshes if (selected.Count > 0) { DeleteSelected(); return; } //deletion done - optimize mesh OptimizeMesh(filter.sharedMesh); } //rebuild mesh properties private void OptimizeMesh(Mesh mesh) { mesh.RecalculateNormals(); mesh.RecalculateBounds(); ; } //draw selection and move handles private void DrawSelectedHandles() { //don't draw anything without mesh if (!script.GetComponent().sharedMesh) return; Handles.color = new Color(1, 0, 0, 0.2f); //get current vertex count of the active submesh int currentCount = script.current.Count; //draw a solid disc at the lastest position we clicked in edit mode if (placing && currentCount > 0 && (!script.autoSplit || (script.autoSplit && currentCount != script.splitAt))) { Vector3 pos = allPoints[script.current[currentCount - 1]]; Handles.DrawSolidDisc(pos, Vector3.up, HandleUtility.GetHandleSize(pos) * 0.1f); } //don't continue without selected vertices if (selected.Count == 0) return; //draw a solid disc for each vertex we selected Vector3[] selectedHandles = new Vector3[selected.Count]; for (int i = 0; i < selected.Count; i++) { Vector3 pos = allPoints[selected[i]]; selectedHandles[i] = pos; Handles.DrawSolidDisc(pos, Vector3.up, HandleUtility.GetHandleSize(pos) * 0.1f); } //get the center position of selected vertices and draw a handle there Vector3 center = GetCenterOfVector3(selectedHandles); Vector3 handle = Handles.PositionHandle(center, Quaternion.identity); Vector3 diff = handle - center; //if the handle moved if (diff != Vector3.zero) { Undo.RegisterCompleteObjectUndo(script, "Handle Moved"); Mesh myMesh = script.gameObject.GetComponent().sharedMesh; if (myMesh) Undo.RecordObject(myMesh, "Handle Moved"); //adjust corresponding mesh vertices for (int i = 0; i < selected.Count; i++) script.list[selected[i]] += diff; //update mesh with new vertex positions script.UpdateMesh(ConvertAllPoints()); } } //draws submesh outlines private void DrawPolygonOutline() { List sub = script.subPoints; Handles.color = Color.yellow; //for each submesh for (int i = 0; i < sub.Count; i++) { //get actual vertex positions from indices Vector3[] points = new Vector3[sub[i].list.Count]; for (int j = 0; j < points.Length; j++) points[j] = allPoints[sub[i].list[j]]; //draw polyline with these points Handles.DrawPolyLine(points); //connect the first and last point to a loop if (points.Length >= 2) { Vector3[] p = { points[0], points[points.Length - 1] }; Handles.DrawPolyLine((p)); } } Handles.color = Color.red; //draw a circle at each vertex position for (int i = 0; i < allPoints.Length; i++) { Handles.CircleCap(0, allPoints[i], Quaternion.LookRotation(Vector3.up, Vector3.forward), HandleUtility.GetHandleSize(allPoints[i]) * 0.1f); } } //returns the center point of an array of Vector3s private Vector3 GetCenterOfVector3(Vector3[] points) { Vector3 sum = Vector3.zero; if (points.Length == 0) return sum; for (int i = 0; i < points.Length; i++) sum += points[i]; return sum / points.Length; } } }