├── .gitignore ├── Editor.meta ├── Editor ├── NavGen.meta ├── NavGen │ ├── NavEdUtil.cs │ ├── NavEdUtil.cs.meta │ ├── NavLinkGenerator_Editor.cs │ ├── NavLinkGenerator_Editor.cs.meta │ ├── NavNonWalkableCollection_Editor.cs │ └── NavNonWalkableCollection_Editor.cs.meta ├── NavGenEditor.asmdef └── NavGenEditor.asmdef.meta ├── LICENSE ├── LICENSE.meta ├── README.md ├── README.md.meta ├── Runtime.meta ├── Runtime ├── NavGen.asmdef ├── NavGen.asmdef.meta ├── NavGen.meta ├── NavGen │ ├── NavLinkCreationReason.cs │ ├── NavLinkCreationReason.cs.meta │ ├── NavLinkGenerator.cs │ ├── NavLinkGenerator.cs.meta │ ├── NavNonWalkableCollection.cs │ └── NavNonWalkableCollection.cs.meta ├── NavMeshAreas.meta └── NavMeshAreas │ ├── LICENSE │ ├── LICENSE.meta │ ├── NavMeshAreas.cs │ ├── NavMeshAreas.cs.meta │ ├── README.md │ └── README.md.meta ├── package.json └── package.json.meta /.gitignore: -------------------------------------------------------------------------------- 1 | # This .gitignore file should be placed at the root of your Unity project directory 2 | # 3 | # Get latest from https://github.com/github/gitignore/blob/master/Unity.gitignore 4 | # 5 | /[Ll]ibrary/ 6 | /[Tt]emp/ 7 | /[Oo]bj/ 8 | /[Bb]uild/ 9 | /[Bb]uilds/ 10 | /[Ll]ogs/ 11 | /[Uu]ser[Ss]ettings/ 12 | 13 | # MemoryCaptures can get excessive in size. 14 | # They also could contain extremely sensitive data 15 | /[Mm]emoryCaptures/ 16 | 17 | # Asset meta data should only be ignored when the corresponding asset is also ignored 18 | !/[Aa]ssets/**/*.meta 19 | 20 | # Uncomment this line if you wish to ignore the asset store tools plugin 21 | # /[Aa]ssets/AssetStoreTools* 22 | 23 | # Autogenerated Jetbrains Rider plugin 24 | /[Aa]ssets/Plugins/Editor/JetBrains* 25 | 26 | # Visual Studio cache directory 27 | .vs/ 28 | 29 | # Gradle cache directory 30 | .gradle/ 31 | 32 | # Autogenerated VS/MD/Consulo solution and project files 33 | ExportedObj/ 34 | .consulo/ 35 | *.csproj 36 | *.unityproj 37 | *.sln 38 | *.suo 39 | *.tmp 40 | *.user 41 | *.userprefs 42 | *.pidb 43 | *.booproj 44 | *.svd 45 | *.pdb 46 | *.mdb 47 | *.opendb 48 | *.VC.db 49 | 50 | # Unity3D generated meta files 51 | *.pidb.meta 52 | *.pdb.meta 53 | *.mdb.meta 54 | 55 | # Unity3D generated file on crash reports 56 | sysinfo.txt 57 | 58 | # Builds 59 | *.apk 60 | *.aab 61 | *.unitypackage 62 | 63 | # Crashlytics generated file 64 | crashlytics-build.properties 65 | 66 | # Packed Addressables 67 | /[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin* 68 | 69 | # Temporary auto-generated Android Assets 70 | /[Aa]ssets/[Ss]treamingAssets/aa.meta 71 | /[Aa]ssets/[Ss]treamingAssets/aa/* 72 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8984e06e8d14eed4e81e864bbc6b577f 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/NavGen.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 067afdbc7ccbebf488846c0ef8bdf146 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/NavGen/NavEdUtil.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections; 3 | using System.Linq; 4 | using System; 5 | using UnityEditor.SceneManagement; 6 | using UnityEditor; 7 | using UnityEngine; 8 | 9 | using Debug = UnityEngine.Debug; 10 | using Object = UnityEngine.Object; 11 | 12 | namespace idbrii.navgen 13 | { 14 | static class NavEdUtil 15 | { 16 | 17 | public static Object[] GetAllInActiveScene() where T : Component 18 | { 19 | var scene = EditorSceneManager.GetActiveScene(); 20 | return Resources.FindObjectsOfTypeAll() 21 | .Where(comp => comp.gameObject.scene == scene) 22 | .Select(comp => comp as Object) 23 | .ToArray(); 24 | } 25 | 26 | public static Transform GetNamedRoot(string root_name) 27 | { 28 | var root_objects = EditorSceneManager.GetActiveScene().GetRootGameObjects(); 29 | foreach (var obj in root_objects) 30 | { 31 | if (obj.name == root_name) 32 | { 33 | return obj.transform; 34 | } 35 | } 36 | return new GameObject(root_name).transform; 37 | } 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Editor/NavGen/NavEdUtil.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 191d05c1f0368824e89be0fdea326d38 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/NavGen/NavLinkGenerator_Editor.cs: -------------------------------------------------------------------------------- 1 | //~ #define NAVGEN_INCLUDE_TESTS 2 | 3 | using System.Collections.Generic; 4 | using System.Collections; 5 | using System.Linq; 6 | using UnityEditor.AI; 7 | using UnityEditor.SceneManagement; 8 | using UnityEditor; 9 | using UnityEngine.AI; 10 | using UnityEngine; 11 | 12 | using Debug = UnityEngine.Debug; 13 | using Object = UnityEngine.Object; 14 | 15 | 16 | namespace idbrii.navgen 17 | { 18 | [CustomEditor(typeof(NavLinkGenerator), true)] 19 | public class NavLinkGenerator_Editor : Editor 20 | { 21 | const float k_DrawDuration = 1f; 22 | const string k_LinkRootName = "Generated NavLinks"; 23 | 24 | [SerializeField] bool m_AttachDebugToLinks; 25 | [SerializeField] bool m_ShowCreatedLinks; 26 | [SerializeField] List m_CreatedLinks = new List(); 27 | 28 | public override void OnInspectorGUI() 29 | { 30 | DrawDefaultInspector(); 31 | 32 | var gen = target as NavLinkGenerator; 33 | 34 | m_AttachDebugToLinks = EditorGUILayout.Toggle("Attach Debug To Links", m_AttachDebugToLinks); 35 | 36 | EditorGUILayout.HelpBox("Workflow: click these buttons from left to right. See tooltips for more info.", MessageType.None); 37 | 38 | using (new GUILayout.HorizontalScope()) 39 | { 40 | if (GUILayout.Button(new GUIContent("Clear All", "Delete generated Interior Volumes, NavMesh, and NavMeshLinks."))) 41 | { 42 | NavNonWalkableCollection_Editor.ClearNonWalkableVolumes(); 43 | NavMeshAssetManager.instance.ClearSurfaces(NavEdUtil.GetAllInActiveScene()); 44 | RemoveLinks(); 45 | SceneView.RepaintAll(); 46 | Debug.Log($"Removed NavMesh and NavMeshLinks from all NavMeshSurfaces."); 47 | } 48 | 49 | if (GUILayout.Button(new GUIContent("Create Interior Volumes", "Create NonWalkable volumes to prevent navmesh generation inside of solid objects."))) 50 | { 51 | NavNonWalkableCollection_Editor.CreateNonWalkableVolumes(); 52 | } 53 | 54 | if (GUILayout.Button(new GUIContent("Bake NavMesh", "Build navmesh for all NavMeshSurface."))) 55 | { 56 | var surfaces = NavEdUtil.GetAllInActiveScene(); 57 | NavMeshAssetManager.instance.StartBakingSurfaces(surfaces); 58 | Debug.Log($"Baked NavMesh for {surfaces.Length} NavMeshSurfaces."); 59 | } 60 | 61 | if (GUILayout.Button(new GUIContent("Bake Links", "Create NavMeshLinks along your navmesh edges."))) 62 | { 63 | GenerateLinks(gen); 64 | Debug.Log($"Baked NavMeshLinks."); 65 | } 66 | 67 | if (GUILayout.Button(new GUIContent("Select NavMesh", "Selecting the navmesh makes it draw in the Scene view so you can evaluate the quality of the mesh and the links."))) 68 | { 69 | Selection.objects = NavEdUtil.GetAllInActiveScene(); 70 | } 71 | } 72 | 73 | EditorGUILayout.Space(); 74 | m_ShowCreatedLinks = EditorGUILayout.Foldout(m_ShowCreatedLinks, "Created Links", toggleOnLabelClick: true); 75 | if (m_ShowCreatedLinks) 76 | { 77 | foreach (var entry in m_CreatedLinks) 78 | { 79 | using (new GUILayout.HorizontalScope()) 80 | { 81 | EditorGUILayout.ObjectField(entry, typeof(NavMeshLink), allowSceneObjects: true); 82 | using (new EditorGUI.DisabledScope(!m_AttachDebugToLinks)) 83 | { 84 | if (GUILayout.Button("Draw")) 85 | { 86 | entry.GetComponent().Draw(); 87 | 88 | if (SceneView.lastActiveSceneView != null) 89 | { 90 | // Prevent losing focus (we'd lose our state). 91 | ActiveEditorTracker.sharedTracker.isLocked = true; 92 | 93 | var activeGameObject = Selection.activeGameObject; 94 | activeGameObject = entry.gameObject; 95 | EditorGUIUtility.PingObject(activeGameObject); 96 | SceneView.lastActiveSceneView.FrameSelected(); 97 | Selection.activeGameObject = activeGameObject; 98 | } 99 | } 100 | } 101 | } 102 | } 103 | } 104 | } 105 | 106 | void DrawEdges(float duration) 107 | { 108 | var tri = NavMesh.CalculateTriangulation(); 109 | var edge_list = CreateEdges(tri); 110 | foreach (var edge in edge_list) 111 | { 112 | edge.ComputeDerivedData(); 113 | Debug.DrawLine(edge.m_StartPos, edge.m_EndPos, Color.magenta, duration); 114 | var mid = edge.GetMidpoint(); 115 | Debug.DrawLine(mid, mid + edge.m_Normal, Color.blue, duration); 116 | } 117 | } 118 | 119 | class NavEdge 120 | { 121 | public Vector3 m_StartPos; 122 | public Vector3 m_EndPos; 123 | 124 | // Derived data 125 | public float m_Length; 126 | public Vector3 m_Normal; 127 | public Quaternion m_Away; 128 | 129 | public NavEdge(Vector3 start, Vector3 end) 130 | { 131 | m_StartPos = start; 132 | m_EndPos = end; 133 | } 134 | 135 | public Vector3 GetMidpoint() 136 | { 137 | return Vector3.Lerp(m_StartPos, m_EndPos, 0.5f); 138 | } 139 | 140 | public void ComputeDerivedData() 141 | { 142 | m_Length = Vector3.Distance(m_StartPos, m_EndPos); 143 | var normal = Vector3.Cross(m_EndPos - m_StartPos, Vector3.up).normalized; 144 | 145 | // Point it outside the nav poly. 146 | NavMeshHit nav_hit; 147 | var mid = GetMidpoint(); 148 | var end = mid - normal * 0.3f; 149 | bool hit = NavMesh.SamplePosition(end, out nav_hit, 0.2f, NavMesh.AllAreas); 150 | //~ Debug.DrawLine(mid, end, hit ? Color.red : Color.white); 151 | if (!hit) 152 | { 153 | normal *= -1f; 154 | } 155 | m_Normal = normal; 156 | m_Away = Quaternion.LookRotation(normal); 157 | } 158 | public bool IsPointOnEdge(Vector3 point) 159 | { 160 | return DistanceSqToPointOnLine(m_StartPos, m_EndPos, point) < 0.001f; 161 | } 162 | } 163 | 164 | // Using EqualityComparer on NavEdge didn't work, so use a comparer. 165 | class NavEdgeEqualityComparer : IEqualityComparer 166 | { 167 | public bool Equals(NavEdge lhs, NavEdge rhs) 168 | { 169 | return 170 | ( lhs.m_StartPos == rhs.m_StartPos && lhs.m_EndPos == rhs.m_EndPos) 171 | || (lhs.m_StartPos == rhs.m_EndPos && lhs.m_EndPos == rhs.m_StartPos); 172 | } 173 | 174 | public int GetHashCode(NavEdge e) 175 | { 176 | return e.m_StartPos.GetHashCode() ^ e.m_EndPos.GetHashCode(); 177 | } 178 | } 179 | 180 | #if NAVGEN_INCLUDE_TESTS 181 | [UnityEditor.MenuItem("Tools/Test/NavEdge Compare")] 182 | #endif 183 | static void Test_NavEdge_Compare() 184 | { 185 | var cmp = new NavEdgeEqualityComparer(); 186 | var edge = new NavEdge(new Vector3(10f, 20f, 30f), new Vector3(20f, 20f, 20f)); 187 | var edge_identical = new NavEdge(edge.m_StartPos, edge.m_EndPos); 188 | var edge_reverse = new NavEdge(edge.m_EndPos, edge.m_StartPos); 189 | 190 | Debug.Assert(cmp.Equals(edge, edge), "compare to self."); 191 | Debug.Assert(cmp.Equals(edge, edge_identical), "compare to identical."); 192 | Debug.Assert(cmp.Equals(edge, edge_reverse), "compare to mirrored."); 193 | 194 | var edge_list = new HashSet(cmp); 195 | edge_list.Add(edge); 196 | Debug.Assert(edge_list.Add(edge) == false, "Add failed to find duplicate"); 197 | Debug.Assert(edge_list.Remove(edge) == true, "Remove failed to find edge"); 198 | Debug.Assert(edge_list.Count == 0, "Must be empty now."); 199 | 200 | AddIfUniqueAndRemoveIfNot(edge_list, edge); 201 | Debug.Assert(edge_list.Count == 1, "AddIfUniqueAndRemoveIfNot should add edge to empty set."); 202 | AddIfUniqueAndRemoveIfNot(edge_list, edge_identical); 203 | Debug.Assert(edge_list.Count == 0, "AddIfUniqueAndRemoveIfNot should remove identical edge."); 204 | 205 | AddIfUniqueAndRemoveIfNot(edge_list, edge); 206 | Debug.Assert(edge_list.Count == 1, "AddIfUniqueAndRemoveIfNot failed to add edge"); 207 | AddIfUniqueAndRemoveIfNot(edge_list, edge_reverse); 208 | Debug.Assert(edge_list.Count == 0, "AddIfUniqueAndRemoveIfNot failed to find edge"); 209 | 210 | Debug.Log("Test complete: NavEdge"); 211 | } 212 | 213 | // Don't want inner edges (which match another existing edge). 214 | static void AddIfUniqueAndRemoveIfNot(HashSet set, NavEdge edge) 215 | { 216 | bool had_edge = set.Remove(edge); 217 | if (!had_edge) 218 | { 219 | set.Add(edge); 220 | } 221 | } 222 | 223 | static float DistanceSqToPointOnLine(Vector3 a, Vector3 b, Vector3 p) 224 | { 225 | Vector3 ab = b - a; 226 | Vector3 pa = a - p; 227 | var mag = ab.magnitude; 228 | Vector3 c = ab * (Vector3.Dot( pa, ab ) / (mag * mag)); 229 | Vector3 d = pa - c; 230 | return Vector3.Dot( d, d ); 231 | } 232 | 233 | #if NAVGEN_INCLUDE_TESTS 234 | [UnityEditor.MenuItem("Tools/Test/DistanceSqToPointOnLine")] 235 | #endif 236 | static void Test_DistanceSqToPointOnLine() 237 | { 238 | var a = new Vector3(0f, 0f, 0f); 239 | var b = new Vector3(1f, 1f, 0f); 240 | 241 | var not_on_line = new Vector3[]{ 242 | new Vector3(1f, 0f, 0f), 243 | new Vector3(0f, 1f, 0f), 244 | new Vector3(0f, 0f, 1f), 245 | }; 246 | foreach (var t in not_on_line) 247 | { 248 | var dist = DistanceSqToPointOnLine(a, b, t); 249 | UnityEngine.Assertions.Assert.IsTrue(dist > 0.001f, $"Didn't expect {t} to be colinear, but result was {dist}."); 250 | } 251 | 252 | var are_on_line = new Vector3[]{ 253 | new Vector3(0f, 0f, 0f), 254 | new Vector3(0.5f, 0.5f, 0f), 255 | new Vector3(0.75f, 0.75f, 0f), 256 | new Vector3(1f, 1f, 0f), 257 | new Vector3(2f, 2f, 0f), // line, not line segment 258 | }; 259 | foreach (var t in are_on_line) 260 | { 261 | UnityEngine.Assertions.Assert.AreApproximatelyEqual(DistanceSqToPointOnLine(a, b, t), 0f, $"Expected {t} to be colinear"); 262 | } 263 | 264 | Debug.Log("Test complete: DistanceSqToPointOnLine"); 265 | } 266 | 267 | 268 | void GenerateLinks(NavLinkGenerator gen) 269 | { 270 | var tri = NavMesh.CalculateTriangulation(); 271 | var edge_list = CreateEdges(tri); 272 | foreach (var edge in edge_list) 273 | { 274 | edge.ComputeDerivedData(); 275 | } 276 | if (edge_list.Count() == 0) 277 | { 278 | return; 279 | } 280 | 281 | RemoveLinks(); 282 | m_CreatedLinks.Clear(); 283 | var parent = NavEdUtil.GetNamedRoot(k_LinkRootName); 284 | 285 | foreach (var edge in edge_list) 286 | { 287 | var mid = edge.GetMidpoint(); 288 | var fwd = edge.m_Normal; 289 | var link = CreateNavLink(parent, gen, edge, mid, fwd); 290 | if (link != null) 291 | { 292 | m_CreatedLinks.Add(link); 293 | } 294 | } 295 | } 296 | 297 | 298 | NavMeshLink CreateNavLink(Transform parent, NavLinkGenerator gen, NavEdge edge, Vector3 mid, Vector3 fwd) 299 | { 300 | RaycastHit phys_hit; 301 | RaycastHit ignored; 302 | NavMeshHit nav_hit; 303 | var ground_found = Color.Lerp(Color.red, Color.white, 0.75f); 304 | var ground_missing = Color.Lerp(Color.red, Color.white, 0.35f); 305 | var navmesh_found = Color.Lerp(Color.cyan, Color.white, 0.75f); 306 | var navmesh_missing = Color.Lerp(Color.red, Color.white, 0.65f); 307 | var traverse_clear = Color.green; 308 | var traverse_hit = Color.red; 309 | for (int i = 0; i < gen.m_Steps; ++i) 310 | { 311 | float scale = (float)i / (float)gen.m_Steps; 312 | 313 | var top = mid + (fwd * gen.m_MaxHorizontalJump * scale); 314 | var down = top + (Vector3.down * gen.m_MaxVerticalFall); 315 | bool hit = Physics.Linecast(top, down, out phys_hit, gen.m_PhysicsMask.value, QueryTriggerInteraction.Ignore); 316 | //~ Debug.DrawLine(mid, top, hit ? ground_found : ground_missing, k_DrawDuration); 317 | //~ Debug.DrawLine(top, down, hit ? ground_found : ground_missing, k_DrawDuration); 318 | if (hit) 319 | { 320 | var max_distance = gen.m_MaxVerticalFall - phys_hit.distance; 321 | hit = NavMesh.SamplePosition(phys_hit.point, out nav_hit, max_distance, (int)gen.m_NavMask); 322 | // Only place downward links (to avoid back and forth double placement). 323 | hit = hit && (nav_hit.position.y <= mid.y); 324 | // Only accept 90 wedge in front of normal (prevent links 325 | // that other edges are already handling). 326 | hit = hit && Vector3.Dot(nav_hit.position - mid, edge.m_Normal) > Mathf.Cos(gen.m_MaxAngleFromEdgeNormal); 327 | bool is_original_edge = edge.IsPointOnEdge(nav_hit.position); 328 | hit &= !is_original_edge; // don't count self 329 | //~ Debug.DrawLine(phys_hit.point, nav_hit.position, hit ? navmesh_found : navmesh_missing, k_DrawDuration); 330 | if (hit) 331 | { 332 | var height_offset = Vector3.up * gen.m_AgentHeight; 333 | var transit_start = mid + height_offset; 334 | var transit_end = nav_hit.position + height_offset; 335 | // Raycast both ways to ensure we're not inside a collider. 336 | 337 | hit = Physics.Linecast(transit_start, transit_end, out ignored, gen.m_PhysicsMask.value, QueryTriggerInteraction.Ignore) 338 | || Physics.Linecast(transit_end, transit_start, out ignored, gen.m_PhysicsMask.value, QueryTriggerInteraction.Ignore); 339 | //~ Debug.DrawLine(transit_start, transit_end, hit ? traverse_clear : traverse_hit, k_DrawDuration); 340 | if (hit) 341 | { 342 | // Agent can't jump through here. 343 | continue; 344 | } 345 | var height_delta = mid.y - nav_hit.position.y; 346 | Debug.Assert(height_delta >= 0, "Not handling negative delta."); 347 | var prefab = gen.m_JumpLinkPrefab; 348 | if (height_delta > gen.m_MaxVerticalJump) 349 | { 350 | prefab = gen.m_FallLinkPrefab; 351 | } 352 | var t = PrefabUtility.InstantiatePrefab(prefab, parent.gameObject.scene) as Transform; 353 | Debug.Assert(t != null, $"Failed to instantiate {prefab}"); 354 | t.SetParent(parent); 355 | t.SetPositionAndRotation(mid, edge.m_Away); 356 | var link = t.GetComponent(); 357 | 358 | // Push endpoint out into the navmesh to ensure good 359 | // connection. Necessary to prevent invalid links. 360 | var inset = 0.05f; 361 | link.startPoint = link.transform.InverseTransformPoint(mid - fwd * inset); 362 | link.endPoint = link.transform.InverseTransformPoint(nav_hit.position) + (Vector3.forward * inset); 363 | link.width = edge.m_Length; 364 | link.UpdateLink(); 365 | Debug.Log("Created NavLink", link); 366 | Undo.RegisterCompleteObjectUndo(link.gameObject, "Create NavMeshLink"); 367 | 368 | if (m_AttachDebugToLinks) 369 | { 370 | // Attach a component that has the information we 371 | // used to decide how to create this navlink. Much 372 | // easier to go back and inspect it like this than 373 | // to try to examine the output as you generate 374 | // navlinks. Mostly useful for debugging 375 | // NavLinkGenerator. 376 | var reason = link.gameObject.AddComponent(); 377 | reason.gen = gen; 378 | reason.fwd = fwd; 379 | reason.mid = mid; 380 | reason.top = top; 381 | reason.down = down; 382 | reason.transit_start = transit_start; 383 | reason.transit_end = transit_end; 384 | reason.nav_hit_position = nav_hit.position; 385 | reason.phys_hit_point = phys_hit.point; 386 | } 387 | 388 | return link; 389 | } 390 | } 391 | } 392 | return null; 393 | } 394 | 395 | static IEnumerable CreateEdges(NavMeshTriangulation tri) 396 | { 397 | // use HashSet to ignore duplicate edges. 398 | var edges = new HashSet(new NavEdgeEqualityComparer()); 399 | for (int i = 0; i < tri.indices.Length - 1; i += 3) 400 | { 401 | AddIfUniqueAndRemoveIfNot(edges, TriangleToEdge(tri, i, i + 1)); 402 | AddIfUniqueAndRemoveIfNot(edges, TriangleToEdge(tri, i + 1, i + 2)); 403 | AddIfUniqueAndRemoveIfNot(edges, TriangleToEdge(tri, i + 2, i)); 404 | } 405 | return edges; 406 | } 407 | 408 | static NavEdge TriangleToEdge(NavMeshTriangulation tri, int start, int end) 409 | { 410 | var v1 = tri.vertices[tri.indices[start]]; 411 | var v2 = tri.vertices[tri.indices[end]]; 412 | return new NavEdge(v1, v2); 413 | } 414 | 415 | void RemoveLinks() 416 | { 417 | var nav_links = NavEdUtil.GetNamedRoot(k_LinkRootName).GetComponentsInChildren(); 418 | foreach (var link in nav_links) 419 | { 420 | GameObject.DestroyImmediate(link.gameObject); 421 | } 422 | } 423 | 424 | } 425 | } 426 | -------------------------------------------------------------------------------- /Editor/NavGen/NavLinkGenerator_Editor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e510b73387bb14e47ae499d063006f31 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/NavGen/NavNonWalkableCollection_Editor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections; 3 | using System.Linq; 4 | using UnityEditor.AI; 5 | using UnityEditor.SceneManagement; 6 | using UnityEditor; 7 | using UnityEngine.AI; 8 | using UnityEngine; 9 | 10 | using Debug = UnityEngine.Debug; 11 | using Object = UnityEngine.Object; 12 | 13 | 14 | namespace idbrii.navgen 15 | { 16 | [CustomEditor(typeof(NavNonWalkableCollection), true)] 17 | public class NavNonWalkableCollection_Editor : Editor 18 | { 19 | const float k_DrawDuration = 1f; 20 | 21 | internal const string k_NoMeshVolumeRootName = "NonWalkable Collection"; 22 | 23 | 24 | public override void OnInspectorGUI() 25 | { 26 | DrawDefaultInspector(); 27 | 28 | var collection = target as NavNonWalkableCollection; 29 | 30 | using (new GUILayout.HorizontalScope()) 31 | { 32 | if (GUILayout.Button("Clear Interior Volumes")) 33 | { 34 | ClearVolumes(collection); 35 | } 36 | 37 | if (GUILayout.Button("Create Interior Volumes")) 38 | { 39 | CreateNonWalkableVolumes(collection); 40 | } 41 | 42 | if (GUILayout.Button("Select NavMesh")) 43 | { 44 | Selection.objects = NavEdUtil.GetAllInActiveScene(); 45 | } 46 | } 47 | } 48 | 49 | static NavNonWalkableCollection Get() 50 | { 51 | var root = NavEdUtil.GetNamedRoot(k_NoMeshVolumeRootName); 52 | if (root == null) 53 | { 54 | root = new GameObject(k_NoMeshVolumeRootName).transform; 55 | } 56 | var collection = root.GetComponent(); 57 | if (collection == null) 58 | { 59 | collection = root.gameObject.AddComponent(); 60 | } 61 | return collection; 62 | } 63 | 64 | public static void CreateNonWalkableVolumes() 65 | { 66 | var collection = Get(); 67 | if (collection == null) 68 | { 69 | return; 70 | } 71 | CreateNonWalkableVolumes(collection); 72 | } 73 | 74 | public static void ClearNonWalkableVolumes() 75 | { 76 | var collection = Get(); 77 | if (collection == null) 78 | { 79 | return; 80 | } 81 | ClearVolumes(collection); 82 | } 83 | 84 | static void ClearVolumes(NavNonWalkableCollection collection) 85 | { 86 | foreach (var entry in collection.m_Volumes) 87 | { 88 | if (entry != null) 89 | { 90 | GameObject.DestroyImmediate(entry.gameObject); 91 | } 92 | } 93 | collection.m_Volumes.Clear(); 94 | } 95 | 96 | static void CreateNonWalkableVolumes(NavNonWalkableCollection collection) 97 | { 98 | if (collection.m_Volumes == null) 99 | { 100 | collection.m_Volumes = new List(); 101 | } 102 | 103 | ClearVolumes(collection); 104 | 105 | var surfaces = NavEdUtil.GetAllInActiveScene(); 106 | var colliders = surfaces 107 | .SelectMany(s => (s as NavMeshSurface).GetComponentsInChildren()); 108 | var threshold_sqr = 1.5f * 1.5f; 109 | foreach (Collider c in colliders) 110 | { 111 | if (c.GetComponentInChildren() != null) 112 | { 113 | // Skip so we can move them out of m_Volumes to make them persistent. 114 | Debug.Log($"[NavMesh] Skipping NonWalkable generation on {c.name} because it already has a NavMeshModifierVolume child.", c); 115 | continue; 116 | } 117 | // Get unrotated bounds. Hopefully these are tighter. 118 | var t = c.transform; 119 | var rot = t.rotation; 120 | var pos = t.position; 121 | t.position = Vector3.zero; 122 | t.rotation = Quaternion.identity; 123 | Physics.SyncTransforms(); 124 | var b = c.bounds; 125 | t.rotation = rot; 126 | t.position = pos; 127 | 128 | if (b.size.sqrMagnitude > threshold_sqr) 129 | { 130 | var obj = new GameObject("Block NavMesh - "+ t.name); 131 | obj.transform.SetParent(t); 132 | obj.transform.SetPositionAndRotation(pos, rot); 133 | obj.transform.localPosition = Vector3.zero; 134 | obj.transform.localRotation = Quaternion.identity; 135 | 136 | var vol = obj.AddComponent(); 137 | vol.area = (int)NavMeshAreaIndex.NotWalkable; 138 | 139 | var offset = 0.2f; 140 | var size = b.size; 141 | size -= Vector3.one * offset; 142 | vol.size = size; 143 | vol.center = b.center + Vector3.down * offset; 144 | Undo.RegisterCreatedObjectUndo(obj, "Create No Walk Volumes"); 145 | collection.m_Volumes.Add(vol); 146 | } 147 | } 148 | Undo.RecordObject(collection, "Create No Walk Volumes"); 149 | } 150 | 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Editor/NavGen/NavNonWalkableCollection_Editor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 28abfc9040737e54eb2019e55a7d7b69 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/NavGenEditor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "idbrii.NavGen.Editor", 3 | "references": [ 4 | "idbrii.NavGen", 5 | "NavMeshComponentsEditor", 6 | "NavMeshComponents" 7 | ], 8 | "optionalUnityReferences": [], 9 | "includePlatforms": [ 10 | "Editor" 11 | ], 12 | "excludePlatforms": [], 13 | "allowUnsafeCode": false, 14 | "overrideReferences": false, 15 | "precompiledReferences": [], 16 | "autoReferenced": true, 17 | "defineConstraints": [] 18 | } 19 | -------------------------------------------------------------------------------- /Editor/NavGenEditor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f1f804b7b219abe4c926dbf7a0f78bb8 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 David Briscoe 4 | Copyright (c) 2018 jeffvella 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /LICENSE.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 79f4265e3b03a6e449400882254b45f4 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # unity-navgen 2 | Tools for working with [Unity's 3 | NavMeshComponents](https://github.com/Unity-Technologies/NavMeshComponents) and 4 | generating navmesh: link generation, mesh cleanup, etc 5 | 6 | 7 | Default NavMesh Generation | Using NavLinkGenerator's Bake Links and Interior Volumes 8 | :-------------------------: | :-------------------------: 9 | ![Default navmesh generation](https://user-images.githubusercontent.com/43559/96373807-b0a40980-1123-11eb-8bec-a5921c9819f7.png) | ![Hiding cube platform to show navmesh island inside](https://user-images.githubusercontent.com/43559/96373809-b13ca000-1123-11eb-9431-e30834db4af1.png) 10 | ![After running Bake Links in NavLinkGenerator](https://user-images.githubusercontent.com/43559/96373808-b13ca000-1123-11eb-9e11-d1b2cb41cfba.png) | ![Hiding cube platform to show there's no navmesh island inside](https://user-images.githubusercontent.com/43559/96373810-b1d53680-1123-11eb-94e3-2c61b481973b.png) 11 | 12 | ## NavLinkGenerator 13 | 14 | NavLinkGenerator is an asset for generating 15 | [NavMeshLinks](https://docs.unity3d.com/Manual/class-NavMeshLink.html) across 16 | gaps in your navmesh. It also serves as the central hub for navgen. 17 | 18 | ![NavLinkGenerator](https://user-images.githubusercontent.com/43559/96361844-081f8680-10de-11eb-86ea-23157153d05e.png) 19 | 20 | NavLinkGenerator is a ScriptableObject -- so you need to create one to start 21 | using it (Assets > Create > Navigation > NavLinkGenerator). The asset contains 22 | settings and buttons for generating links. 23 | 24 | 25 | # NavNonWalkableCollection 26 | 27 | The "Create Interior Volumes" button in NavLinkGenerator creates a 28 | NavNonWalkableCollection which tracks the volumes so they can be rebuilt. 29 | Remove a volume from this component's list to prevent it from being modified. 30 | 31 | 32 | # NavMeshAreas 33 | 34 | You can assign enum values from `UnityEngine.AI.NavMeshAreas` to NavMeshAgent's 35 | AreaMask and `UnityEngine.AI.NavMeshAreaIndex` to area indexes in 36 | NavMeshSurface, NavMeshLink, NavMeshModifierVolume, etc. These enums are 37 | automatically updated from the areas defined in Navigation (Window > AI > 38 | Navigation). 39 | 40 | [NavMeshAreas generates two enums 41 | ](https://github.com/idbrii/unity-navgen/blob/16d4ba6c16228d7f7b9fe7a91ff8b8a837ba842c/Runtime/NavMeshAreas/NavMeshAreas.cs#L19-L32) 42 | that look something like this: 43 | 44 | ```cs 45 | // NavMeshAgent uses AreaMask. 46 | [Flags] 47 | public enum NavMeshAreas 48 | { 49 | None = 0, 50 | Walkable = 1, NotWalkable = 2, Jump = 4, Climb = 8, Blocked = 16, Hole = 32, Edge = 64, Fall = 128, New1 = 256, Stuff = 512, 51 | All = ~0, 52 | } 53 | 54 | // NavMeshSurface, NavMeshLink, NavMeshModifierVolume, etc. use indexes. 55 | public enum NavMeshAreaIndex 56 | { 57 | Walkable = 0, NotWalkable = 1, Jump = 2, Climb = 3, Blocked = 4, Hole = 5, Edge = 6, Fall = 7, New1 = 8, Stuff = 9, 58 | } 59 | ``` 60 | 61 | 62 | # Example 63 | See the [example branch](https://github.com/idbrii/unity-navgen/tree/example) for a demonstration project. 64 | 65 | 66 | # Installation 67 | 68 | 1. Install Unity [NavMeshComponents](https://github.com/Unity-Technologies/NavMeshComponents.git) from github. 69 | 2. Copy the code to your project or add a dependency to your manifest.json to install as a package: 70 | 71 | "com.github.idbrii.unity-navgen": "https://github.com/idbrii/unity-navgen.git#latest-release", 72 | 73 | 74 | # Alternatives 75 | 76 | * [NavMeshLinks_AutoPlacer by eDmitriy](https://forum.unity.com/threads/navmesh-links-generator-for-navmeshcomponents.515143/) 77 | * [Navmesh Cleaner](https://assetstore.unity.com/packages/tools/ai/navmesh-cleaner-151501) ([see here](http://answers.unity.com/answers/1781054/view.html) to use with NavMeshComponents) 78 | 79 | # Credits 80 | 81 | This project includes [UnityNavMeshAreas](https://github.com/jeffvella/UnityNavMeshAreas) Copyright (c) 2018 jeffvella. 82 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c5c5116746689af4c8194ef93587c307 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 37afdde1c50a7804cbab44595404a41d 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/NavGen.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "idbrii.NavGen", 3 | "references": [ 4 | "NavMeshComponents" 5 | ], 6 | "optionalUnityReferences": [], 7 | "includePlatforms": [], 8 | "excludePlatforms": [], 9 | "allowUnsafeCode": false, 10 | "overrideReferences": false, 11 | "precompiledReferences": [], 12 | "autoReferenced": true, 13 | "defineConstraints": [] 14 | } 15 | -------------------------------------------------------------------------------- /Runtime/NavGen.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8137360f1825c9e4ab5d05e246664a54 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime/NavGen.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4244add832dad7340a3804771a83a13d 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/NavGen/NavLinkCreationReason.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections; 3 | using System.Linq; 4 | using UnityEngine.AI; 5 | using UnityEngine; 6 | 7 | using Debug = UnityEngine.Debug; 8 | using Object = UnityEngine.Object; 9 | 10 | namespace idbrii.navgen 11 | { 12 | // Store the data we used to make decisions when generating NavMeshLinks. 13 | public class NavLinkCreationReason : MonoBehaviour 14 | { 15 | const float k_DrawDuration = 1f; 16 | 17 | public NavLinkGenerator gen; 18 | public Vector3 mid; 19 | public Vector3 fwd; 20 | public Vector3 top; 21 | public Vector3 down; 22 | public Vector3 transit_start; 23 | public Vector3 transit_end; 24 | public Vector3 nav_hit_position; 25 | public Vector3 phys_hit_point; 26 | 27 | //~ [NaughtyAttributes.Button] 28 | [ContextMenu("Draw")] 29 | public void Draw() 30 | { 31 | var ground_found = Color.Lerp(Color.green, Color.yellow, 0.75f); 32 | var ground_missing = Color.Lerp(Color.red, Color.white, 0.35f); 33 | var navmesh_found = Color.Lerp(Color.green, Color.cyan, 0.95f); 34 | var navmesh_missing = Color.Lerp(Color.red, Color.white, 0.65f); 35 | var traverse_clear = Color.green; 36 | var traverse_hit = Color.red; 37 | bool hit = true; 38 | 39 | // Find ground 40 | Debug.DrawLine(mid, top, hit ? ground_found : ground_missing, k_DrawDuration); 41 | Debug.DrawLine(top, down, hit ? ground_found : ground_missing, k_DrawDuration); 42 | // SamplePosition 43 | Debug.DrawLine(phys_hit_point, nav_hit_position, hit ? navmesh_found : navmesh_missing, k_DrawDuration); 44 | // Raycast both ways to ensure we're not inside a collider. 45 | Debug.DrawLine(transit_start, transit_end, hit ? traverse_clear : traverse_hit, k_DrawDuration); 46 | } 47 | 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Runtime/NavGen/NavLinkCreationReason.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e58fc2ef5efd2d74780b4cedc8360316 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/NavGen/NavLinkGenerator.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.AI; 3 | 4 | namespace idbrii.navgen 5 | { 6 | 7 | [CreateAssetMenu(fileName = "NavLinkGenerator", menuName = "Navigation/NavLinkGenerator", order = 1)] 8 | public class NavLinkGenerator : ScriptableObject 9 | { 10 | public Transform m_JumpLinkPrefab; 11 | public Transform m_FallLinkPrefab; 12 | public float m_MaxHorizontalJump = 5f; 13 | [Tooltip("Max distance we can jump up.")] 14 | public float m_MaxVerticalJump = 3f; 15 | [Tooltip("Max distance we are allowed to fall. Usually higher than m_MaxVerticalJump.")] 16 | public float m_MaxVerticalFall = 5f; 17 | public int m_Steps = 10; 18 | public LayerMask m_PhysicsMask = -1; 19 | public NavMeshAreas m_NavMask = NavMeshAreas.All; 20 | public float m_AgentHeight = 1.5f; 21 | public float m_AgentRadius = 0.5f; 22 | [Tooltip("Maximum degrees away from the normal pointing horizontally out of a navmesh edge. Larger values allow more awkward links, but may result in redundant or inappropriate links.")] 23 | [Range(0f, 60f)] 24 | public float m_MaxAngleFromEdgeNormal = 45f; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Runtime/NavGen/NavLinkGenerator.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a10d4c80d83fe1343bf16cdd5c250980 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/NavGen/NavNonWalkableCollection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine.AI; 3 | using UnityEngine; 4 | 5 | namespace idbrii.navgen 6 | { 7 | public class NavNonWalkableCollection : MonoBehaviour 8 | { 9 | [Tooltip("Volumes managed by NavNonWalkableCollection. Remove volumes from this list to avoid rebuilding them automatically.")] 10 | public List m_Volumes; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Runtime/NavGen/NavNonWalkableCollection.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 412bcb4952681664dbebe4d8e600b4b2 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/NavMeshAreas.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d814cf8a69f64574bbcda34840e32871 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/NavMeshAreas/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Runtime/NavMeshAreas/LICENSE.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 34429c5fa9d0b45438c88860d501a06e 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime/NavMeshAreas/NavMeshAreas.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using UnityEngine.AI; 5 | using System; 6 | using System.Linq; 7 | using System.IO; 8 | using System.Text; 9 | using System.Runtime.CompilerServices; 10 | 11 | #if UNITY_EDITOR 12 | using UnityEditor; 13 | #endif 14 | 15 | namespace UnityEngine.AI 16 | { 17 | #region Auto-Generated Content 18 | 19 | // NavMeshAgent uses AreaMask. 20 | [Flags] 21 | public enum NavMeshAreas 22 | { 23 | None = 0, 24 | Walkable = 1, NotWalkable = 2, Jump = 4, Climb = 8, Blocked = 16, Hole = 32, Edge = 64, Fall = 128, New1 = 256, Stuff = 512, 25 | All = ~0, 26 | } 27 | 28 | // NavMeshSurface, NavMeshLink, NavMeshModifierVolume, etc. use indexes. 29 | public enum NavMeshAreaIndex 30 | { 31 | Walkable = 0, NotWalkable = 1, Jump = 2, Climb = 3, Blocked = 4, Hole = 5, Edge = 6, Fall = 7, New1 = 8, Stuff = 9, 32 | } 33 | 34 | #endregion 35 | 36 | #if UNITY_EDITOR 37 | 38 | /// 39 | /// Auto-updates the enum in this file if it has changed, when scripts are compiled or assets saved. 40 | /// 41 | public static class NavMeshAreasGenerator 42 | { 43 | private const string IndexValuesToken = "#IndexValues"; 44 | private const string FlagValuesToken = "#FlagValues"; 45 | private const string HashSettingsKey = "NavMeshAreasHash"; 46 | 47 | private static void Update([CallerFilePath] string executingFilePath = "") 48 | { 49 | var areaNames = GameObjectUtility.GetNavMeshAreaNames(); 50 | var lastHash = EditorPrefs.GetInt(HashSettingsKey); 51 | var newHash = GetAreaHash(areaNames); 52 | 53 | if (newHash != lastHash) 54 | { 55 | Debug.Log($"{nameof(NavMeshAreas)} have changed, updating enum: '{executingFilePath}'"); 56 | GenerateFile(areaNames, newHash, executingFilePath); 57 | } 58 | } 59 | 60 | private static int GetAreaHash(string[] areaNames) 61 | { 62 | var input = areaNames.Aggregate((a, b) => a + b); 63 | var hash = 0; 64 | foreach (var t in input) 65 | hash = (hash << 5) + hash + t; 66 | return hash; 67 | } 68 | 69 | private static void GenerateFile(string[] areaNames = default, int hash = 0, string outputPath = null) 70 | { 71 | if (areaNames == null) 72 | areaNames = GameObjectUtility.GetNavMeshAreaNames(); 73 | 74 | if (hash == 0) 75 | hash = GetAreaHash(areaNames); 76 | 77 | var text = GetAreaEnumValuesAsText(ref areaNames, as_flags: true); 78 | var newEnumText = FlagContentTemplate.Replace(FlagValuesToken, text); 79 | var output = ReplaceEnumInFile(nameof(NavMeshAreas), File.ReadAllLines(outputPath), newEnumText); 80 | 81 | text = GetAreaEnumValuesAsText(ref areaNames, as_flags: false); 82 | newEnumText = IndexContentTemplate.Replace(IndexValuesToken, text); 83 | output = ReplaceEnumInFile(nameof(NavMeshAreaIndex), output.Trim().Split(new[]{Environment.NewLine}, StringSplitOptions.None), newEnumText); 84 | 85 | CreateScriptAssetWithContent(outputPath, string.Concat(output)); 86 | EditorPrefs.SetInt(HashSettingsKey, hash); 87 | AssetDatabase.Refresh(); 88 | } 89 | 90 | private static string GetAreaEnumValuesAsText(ref string[] areaNames, bool as_flags) 91 | { 92 | var increment = 0; 93 | var output = new StringBuilder(); 94 | var seenKeys = new HashSet(); 95 | 96 | foreach (var name in areaNames) 97 | { 98 | var enumKey = string.Concat(name.Where(char.IsLetterOrDigit)); 99 | var value = NavMesh.GetAreaFromName(name); 100 | if (as_flags) 101 | { 102 | value = 1 << value; 103 | } 104 | 105 | output.Append(seenKeys.Contains(name) 106 | ? $"{(enumKey + increment++)} = {value}, " 107 | : $"{enumKey} = {value}, "); 108 | 109 | seenKeys.Add(enumKey); 110 | } 111 | return output.ToString(); 112 | } 113 | 114 | private static readonly string FlagContentTemplate = 115 | $@" public enum {nameof(NavMeshAreas)} 116 | {{ 117 | None = 0, 118 | {FlagValuesToken} 119 | All = ~0, 120 | }} 121 | "; 122 | private static readonly string IndexContentTemplate = 123 | $@" public enum {nameof(NavMeshAreaIndex)} 124 | {{ 125 | {IndexValuesToken} 126 | }} 127 | "; 128 | 129 | private static int SkipToLineStartingWith(string pattern, string[] fileLines, int start_index, StringBuilder accumulated) 130 | { 131 | for (int i = start_index; i < fileLines.Length; i++) 132 | { 133 | string line = fileLines[i]; 134 | if (line.Trim().StartsWith(pattern)) 135 | { 136 | return i; 137 | } 138 | else if (accumulated != null) 139 | { 140 | accumulated.AppendLine(line); 141 | } 142 | } 143 | return -1; 144 | } 145 | 146 | private static string ReplaceEnumInFile(string enumName, string[] fileLines, string newEnum) 147 | { 148 | int enumStartLine = 0, enumEndLine = 0; 149 | var result = new StringBuilder(); 150 | enumStartLine = SkipToLineStartingWith("public enum " + enumName, fileLines, 0, result); 151 | if (enumStartLine > 0) 152 | { 153 | enumEndLine = SkipToLineStartingWith("}", fileLines, enumStartLine + 1, null); 154 | result.Append(newEnum); 155 | for (int i = enumEndLine + 1; i < fileLines.Length; i++) 156 | { 157 | result.AppendLine(fileLines[i]); 158 | } 159 | } 160 | return result.ToString(); 161 | } 162 | 163 | /// 164 | /// Create a new script asset. 165 | /// UnityEditor.ProjectWindowUtil.CreateScriptAssetWithContent (2019.1) 166 | /// 167 | /// the path to where the new file should be created 168 | /// the text to put inside 169 | /// 170 | private static UnityEngine.Object CreateScriptAssetWithContent(string pathName, string templateContent) 171 | { 172 | templateContent = SetLineEndings(templateContent, EditorSettings.lineEndingsForNewScripts); 173 | string fullPath = Path.GetFullPath(pathName); 174 | System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding(true); 175 | File.WriteAllText(fullPath, templateContent, encoding); 176 | string projectPath = GetPathRelativeToProject(fullPath); 177 | AssetDatabase.ImportAsset(projectPath); 178 | return AssetDatabase.LoadAssetAtPath(projectPath, typeof(UnityEngine.Object)); 179 | } 180 | 181 | /// 182 | /// Get a path relative to the project folder suitable for passing to 183 | /// AssetDatabase. 184 | /// 185 | /// the path to make relative 186 | /// a path relative to the project (including "Assets" folder) 187 | private static string GetPathRelativeToProject(string pathName) 188 | { 189 | string path = Path.GetFullPath(pathName); 190 | string project = Path.GetFullPath(Application.dataPath); 191 | if (path.StartsWith(project)) 192 | { 193 | path = "Assets" + path.Substring(project.Length); 194 | } 195 | return path; 196 | } 197 | 198 | /// 199 | /// Ensure correct OS specific line endings for saving file content. 200 | /// UnityEditor.ProjectWindowUtil.SetLineEndings (2019.1) 201 | /// 202 | /// a string to have line endings checked 203 | /// the type of line endings to use 204 | /// a cleaned string 205 | private static string SetLineEndings(string content, LineEndingsMode lineEndingsMode) 206 | { 207 | string replacement; 208 | switch (lineEndingsMode) 209 | { 210 | case LineEndingsMode.OSNative: 211 | replacement = Application.platform == RuntimePlatform.WindowsEditor ? "\r\n" : "\n"; 212 | break; 213 | case LineEndingsMode.Unix: 214 | replacement = "\n"; 215 | break; 216 | case LineEndingsMode.Windows: 217 | replacement = "\r\n"; 218 | break; 219 | default: 220 | replacement = "\n"; 221 | break; 222 | } 223 | content = System.Text.RegularExpressions.Regex.Replace(content, "\\r\\n?|\\n", replacement); 224 | return content; 225 | } 226 | 227 | 228 | /// 229 | /// Hook that runs the enum generator whenever assets are saved. 230 | /// 231 | private class UpdateOnAssetModification : UnityEditor.AssetModificationProcessor 232 | { 233 | public static string[] OnWillSaveAssets(string[] paths) 234 | { 235 | Update(); 236 | return paths; 237 | } 238 | } 239 | 240 | /// 241 | /// Hook that runs the enum generator whenever scripts are compiled. 242 | /// 243 | [UnityEditor.Callbacks.DidReloadScripts] 244 | private static void UpdateOnScriptCompile() 245 | { 246 | Update(); 247 | } 248 | 249 | /// 250 | /// Enables manually running the enum generator from the menus. 251 | /// 252 | [MenuItem("Tools/Update NavMeshAreas")] 253 | private static void UpdateOnMenuCommand() 254 | { 255 | UpdateOnScriptCompile(); 256 | } 257 | 258 | } 259 | 260 | /// 261 | /// Flags enum dropdown GUI for selecting properties in the inspector 262 | /// 263 | [CustomPropertyDrawer(typeof(NavMeshAreas))] 264 | public class NavMeshAreasDrawer : PropertyDrawer 265 | { 266 | public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) 267 | { 268 | label = EditorGUI.BeginProperty(position, label, property); 269 | var oldValue = (Enum)fieldInfo.GetValue(property.serializedObject.targetObject); 270 | var newValue = EditorGUI.EnumFlagsField(position, label, oldValue); 271 | if (!newValue.Equals(oldValue)) 272 | { 273 | property.intValue = (int)Convert.ChangeType(newValue, fieldInfo.FieldType); 274 | } 275 | EditorGUI.EndProperty(); 276 | } 277 | } 278 | 279 | #endif 280 | 281 | /// 282 | /// A helper for flag operations with NavMeshAreas 283 | /// 284 | public struct AreaMask 285 | { 286 | private readonly int _value; 287 | 288 | public int Value => _value; 289 | public NavMeshAreas Enum => (NavMeshAreas)_value; 290 | 291 | public AreaMask(int value) 292 | { 293 | _value = value; 294 | } 295 | 296 | public AreaMask(NavMeshAreas areas) 297 | { 298 | _value = (int)areas; 299 | } 300 | 301 | public static implicit operator AreaMask(int value) => new AreaMask(value); 302 | public static implicit operator AreaMask(string name) => new AreaMask(1 << NavMesh.GetAreaFromName(name)); 303 | public static implicit operator AreaMask(NavMeshAreas areas) => new AreaMask((int)areas); 304 | public static implicit operator NavMeshAreas(AreaMask flag) => (NavMeshAreas)flag._value; 305 | public static implicit operator int(AreaMask flag) => flag._value; 306 | 307 | public static bool operator ==(AreaMask a, int b) => a._value.Equals(b); 308 | public static bool operator !=(AreaMask a, int b) => !a._value.Equals(b); 309 | public static int operator +(AreaMask a, AreaMask b) => a.Add(b._value); 310 | public static int operator -(AreaMask a, AreaMask b) => a.Remove(b._value); 311 | public static int operator |(AreaMask a, AreaMask b) => a.Add(b._value); 312 | public static int operator ~(AreaMask a) => ~a._value; 313 | public static int operator +(int a, AreaMask b) => a |= b._value; 314 | public static int operator -(int a, AreaMask b) => a &= ~b._value; 315 | public static int operator |(int a, AreaMask b) => a |= b._value; 316 | public static int operator +(AreaMask a, int b) => a.Add(b); 317 | public static int operator -(AreaMask a, int b) => a.Remove(b); 318 | public static int operator |(AreaMask a, int b) => a.Add(b); 319 | 320 | public bool HasFlag(AreaMask flag) => (_value & flag._value) == flag; 321 | public bool HasFlag(int value) => (_value & value) == value; 322 | public AreaMask Add(AreaMask flag) => _value | flag._value; 323 | public AreaMask Remove(AreaMask flag) => _value & ~flag._value; 324 | public AreaMask Add(NavMeshAreas flags) => _value | (int)flags; 325 | public AreaMask Remove(NavMeshAreas flags) => _value & ~(int)flags; 326 | 327 | public bool Equals(AreaMask other) => _value == other._value; 328 | public override string ToString() => ((NavMeshAreas)_value).ToString(); 329 | public override int GetHashCode() => _value; 330 | public override bool Equals(object obj) 331 | => !ReferenceEquals(null, obj) && (obj is AreaMask other && Equals(other)); 332 | } 333 | 334 | } 335 | -------------------------------------------------------------------------------- /Runtime/NavMeshAreas/NavMeshAreas.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f470a1bb94f46fc4b9a7053ce7d55ac3 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/NavMeshAreas/README.md: -------------------------------------------------------------------------------- 1 | # UnityNavMeshAreas 2 | Self-updating Areas enum for NavMesh in Unity 3 | 4 | - Contains an **Enum called NavMeshAreas** so that you don't have to mess around with strings anymore. 5 | - The file **re-writes itself** whenever scripts compile or assets are saved in the editor. 6 | - Includes a **PropertyDrawer** so that the insepector will use a multiselect flags dropdown. 7 | - Contains a helper 'AreaMask' class to make working with flags easier. 8 | 9 | ###### Usage Examples 10 | ``` 11 | Agent.areaMask = (int)NavMeshAreas.Walkable; 12 | 13 | var mask1 = (AreaMask)(NavMeshAreas.Walkable | NavMeshAreas.Climb); 14 | var mask2 = mask1.Add(NavMeshAreas.Edge); 15 | var mask3 = mask2.Remove(NavMeshAreas.Climb); 16 | ``` 17 | 18 | Unity Version: 2018.3 19 | -------------------------------------------------------------------------------- /Runtime/NavMeshAreas/README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7df54b897d47a814793c04d654f85c94 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.github.idbrii.unity-navgen", 3 | "displayName": "NavGen", 4 | "version": "0.1.0", 5 | "unity": "2019.4", 6 | "description": "Tools for working with NavMeshComponents and generating navmesh: link generation, mesh cleanup, etc", 7 | "keywords": [ "navmesh", "editor" ], 8 | "category": "navigation", 9 | "dependencies": {}, 10 | "samples": [ 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f6b557b9b9458ba4b95f2f46f8cf5f5d 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------