UnityNavMeshAdapter.cs 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. #if UNITY_NAVIGATION_COMPONENTS
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using Unity.AI.Navigation;
  6. using UnityEngine;
  7. using UnityEngine.AI;
  8. namespace DunGen.Adapters
  9. {
  10. [AddComponentMenu("DunGen/NavMesh/Unity NavMesh Adapter")]
  11. public class UnityNavMeshAdapter : NavMeshAdapter
  12. {
  13. #region Nested Types
  14. public enum RuntimeNavMeshBakeMode
  15. {
  16. /// <summary>
  17. /// Uses only existing baked surfaces found in the dungeon tiles, no runtime baking is performed
  18. /// </summary>
  19. PreBakedOnly,
  20. /// <summary>
  21. /// Uses existing baked surfaces in the tiles if any are found, otherwise new surfaces will be added and baked at runtime
  22. /// </summary>
  23. AddIfNoSurfaceExists,
  24. /// <summary>
  25. /// Adds new surfaces where they don't already exist. Rebakes all at runtime
  26. /// </summary>
  27. AlwaysRebake,
  28. /// <summary>
  29. /// Bakes a single surface for the entire dungeon at runtime. No links will be made
  30. /// </summary>
  31. FullDungeonBake,
  32. }
  33. [Serializable]
  34. public sealed class NavMeshAgentLinkInfo
  35. {
  36. public int AgentTypeID = 0;
  37. public int AreaTypeID = 0;
  38. public bool DisableLinkWhenDoorIsClosed = true;
  39. }
  40. #endregion
  41. public RuntimeNavMeshBakeMode BakeMode = RuntimeNavMeshBakeMode.AddIfNoSurfaceExists;
  42. public LayerMask LayerMask = ~0;
  43. public bool AddNavMeshLinksBetweenRooms = true;
  44. public List<NavMeshAgentLinkInfo> NavMeshAgentTypes = new List<NavMeshAgentLinkInfo>() { new NavMeshAgentLinkInfo() };
  45. public float NavMeshLinkDistanceFromDoorway = 2.5f;
  46. public bool AutoGenerateFullRebakeSurfaces = true;
  47. public List<NavMeshSurface> FullRebakeTargets = new List<NavMeshSurface>();
  48. public bool UseAutomaticLinkDistance = false;
  49. public float AutomaticLinkDistanceOffset = 0.1f;
  50. private List<NavMeshSurface> addedSurfaces = new List<NavMeshSurface>();
  51. private List<NavMeshSurface> fullBakeSurfaces = new List<NavMeshSurface>();
  52. public override void Generate(Dungeon dungeon)
  53. {
  54. if (BakeMode == RuntimeNavMeshBakeMode.FullDungeonBake)
  55. {
  56. BakeFullDungeon(dungeon);
  57. return;
  58. }
  59. // Bake Surfaces
  60. if (BakeMode != RuntimeNavMeshBakeMode.PreBakedOnly)
  61. {
  62. foreach (var tile in dungeon.AllTiles)
  63. {
  64. // Find existing surfaces
  65. var existingSurfaces = tile.gameObject.GetComponentsInChildren<NavMeshSurface>();
  66. // Add surfaces for any agent type that is missing one
  67. var addedSurfaces = AddMissingSurfaces(tile, existingSurfaces);
  68. // Gather surfaces to bake
  69. IEnumerable<NavMeshSurface> surfacesToBake = addedSurfaces;
  70. // Append all existing surfaces if mode is set to "Always Rebake"
  71. if (BakeMode == RuntimeNavMeshBakeMode.AlwaysRebake)
  72. surfacesToBake = surfacesToBake.Concat(existingSurfaces);
  73. // Append only unbaked surfaces if mode is set to "Add if no Surface Exists"
  74. else if (BakeMode == RuntimeNavMeshBakeMode.AddIfNoSurfaceExists)
  75. {
  76. var existingUnbakedSurfaces = existingSurfaces.Where(x => x.navMeshData == null);
  77. surfacesToBake = surfacesToBake.Concat(existingUnbakedSurfaces);
  78. }
  79. // Bake
  80. foreach (var surface in surfacesToBake.Distinct())
  81. surface.BuildNavMesh();
  82. }
  83. }
  84. // Add links between rooms
  85. if (AddNavMeshLinksBetweenRooms)
  86. {
  87. foreach (var connection in dungeon.Connections)
  88. foreach (var linkInfo in NavMeshAgentTypes)
  89. AddNavMeshLink(connection, linkInfo);
  90. }
  91. if (OnProgress != null)
  92. OnProgress(new NavMeshGenerationProgress() { Description = "Done", Percentage = 1.0f });
  93. }
  94. private void BakeFullDungeon(Dungeon dungeon)
  95. {
  96. if (AutoGenerateFullRebakeSurfaces)
  97. {
  98. foreach (var surface in fullBakeSurfaces)
  99. if (surface != null)
  100. surface.RemoveData();
  101. fullBakeSurfaces.Clear();
  102. int settingsCount = NavMesh.GetSettingsCount();
  103. for (int i = 0; i < settingsCount; i++)
  104. {
  105. var settings = NavMesh.GetSettingsByIndex(i);
  106. // Find a surface if it already exists
  107. var surface = dungeon.gameObject.GetComponents<NavMeshSurface>()
  108. .Where(s => s.agentTypeID == settings.agentTypeID)
  109. .FirstOrDefault();
  110. if (surface == null)
  111. {
  112. surface = dungeon.gameObject.AddComponent<NavMeshSurface>();
  113. surface.agentTypeID = settings.agentTypeID;
  114. surface.collectObjects = CollectObjects.Children;
  115. surface.layerMask = LayerMask;
  116. }
  117. fullBakeSurfaces.Add(surface);
  118. surface.BuildNavMesh();
  119. }
  120. // Disable all other surfaces to avoid overlapping navmeshes
  121. foreach (var surface in dungeon.gameObject.GetComponentsInChildren<NavMeshSurface>())
  122. if (!fullBakeSurfaces.Contains(surface))
  123. surface.enabled = false;
  124. }
  125. else
  126. {
  127. foreach (var surface in FullRebakeTargets)
  128. surface.BuildNavMesh();
  129. }
  130. if (OnProgress != null)
  131. OnProgress(new NavMeshGenerationProgress() { Description = "Done", Percentage = 1.0f });
  132. }
  133. private NavMeshSurface[] AddMissingSurfaces(Tile tile, NavMeshSurface[] existingSurfaces)
  134. {
  135. addedSurfaces.Clear();
  136. int settingsCount = NavMesh.GetSettingsCount();
  137. for (int i = 0; i < settingsCount; i++)
  138. {
  139. var settings = NavMesh.GetSettingsByIndex(i);
  140. // We already have a surface for this agent type
  141. if (existingSurfaces.Where(x => x.agentTypeID == settings.agentTypeID).Any())
  142. continue;
  143. var surface = tile.gameObject.AddComponent<NavMeshSurface>();
  144. surface.agentTypeID = settings.agentTypeID;
  145. surface.collectObjects = CollectObjects.Children;
  146. surface.layerMask = LayerMask;
  147. addedSurfaces.Add(surface);
  148. }
  149. return addedSurfaces.ToArray();
  150. }
  151. private void AddNavMeshLink(DoorwayConnection connection, NavMeshAgentLinkInfo agentLinkInfo)
  152. {
  153. var doorway = connection.A.gameObject;
  154. var agentSettings = NavMesh.GetSettingsByID(agentLinkInfo.AgentTypeID);
  155. // We need to account for the agent's radius when setting the link's width
  156. float linkWidth = Mathf.Max(connection.A.Socket.Size.x - (agentSettings.agentRadius * 2), 0.01f);
  157. // Add NavMeshLink to one of the doorways
  158. var link = doorway.AddComponent<NavMeshLink>();
  159. link.agentTypeID = agentLinkInfo.AgentTypeID;
  160. link.bidirectional = true;
  161. link.area = agentLinkInfo.AreaTypeID;
  162. link.width = linkWidth;
  163. if (UseAutomaticLinkDistance)
  164. {
  165. link.startPoint = doorway.transform.InverseTransformPoint(GetClosestPointOnNavMesh(doorway.transform.position, doorway.transform.forward)) + new Vector3(0f, 0f, AutomaticLinkDistanceOffset);
  166. link.endPoint = doorway.transform.InverseTransformPoint(GetClosestPointOnNavMesh(doorway.transform.position, -doorway.transform.forward)) - new Vector3(0f, 0f, AutomaticLinkDistanceOffset);
  167. }
  168. else
  169. {
  170. link.startPoint = new Vector3(0, 0, -NavMeshLinkDistanceFromDoorway);
  171. link.endPoint = new Vector3(0, 0, NavMeshLinkDistanceFromDoorway);
  172. }
  173. if (agentLinkInfo.DisableLinkWhenDoorIsClosed)
  174. {
  175. // If there is a door in this doorway, hookup event listeners to enable/disable the link when the door is opened/closed respectively
  176. GameObject doorObj = (connection.A.UsedDoorPrefabInstance != null) ? connection.A.UsedDoorPrefabInstance : (connection.B.UsedDoorPrefabInstance != null) ? connection.B.UsedDoorPrefabInstance : null;
  177. if (doorObj != null)
  178. {
  179. var door = doorObj.GetComponent<Door>();
  180. link.enabled = door.IsOpen;
  181. if (door != null)
  182. door.OnDoorStateChanged += (d, o) => link.enabled = o;
  183. }
  184. }
  185. }
  186. /// <summary>
  187. /// Finds the closest point on the navigation mesh. Unlike NavMesh.FindClosestEdge and
  188. /// NavMesh.Raycast, this method works reliably when given a point that is not on the navmesh
  189. /// </summary>
  190. /// <param name="point">The position we want to find the nearest point to</param>
  191. /// <param name="distanceDirection">An optional direction. If specified, this will ignore points in the opposite direction</param>
  192. /// <returns></returns>
  193. private Vector3 GetClosestPointOnNavMesh(Vector3 point, Vector3? distanceDirection = null)
  194. {
  195. float CalculateDistance(Vector3 p0, Vector3 p1)
  196. {
  197. if (distanceDirection == null)
  198. return (p0 - p1).magnitude;
  199. else
  200. {
  201. float distance = Vector3.Dot(p1 - p0, distanceDirection.Value);
  202. if (distance <= 0f)
  203. return float.PositiveInfinity;
  204. else
  205. return (p0 - p1).magnitude;
  206. }
  207. }
  208. var triangulation = NavMesh.CalculateTriangulation();
  209. Vector3 closestPoint = point;
  210. float closestDistance = float.PositiveInfinity;
  211. for (int i = 2; i < triangulation.indices.Length; i += 3)
  212. {
  213. Vector3 v0 = triangulation.vertices[triangulation.indices[i - 2]];
  214. Vector3 v1 = triangulation.vertices[triangulation.indices[i - 1]];
  215. Vector3 v2 = triangulation.vertices[triangulation.indices[i]];
  216. Vector3 p0 = GetClosestPointOnEdge(point, v0, v1);
  217. Vector3 p1 = GetClosestPointOnEdge(point, v1, v2);
  218. Vector3 p2 = GetClosestPointOnEdge(point, v2, v0);
  219. float p0Dist = CalculateDistance(point, p0);
  220. float p1Dist = CalculateDistance(point, p1);
  221. float p2Dist = CalculateDistance(point, p2);
  222. if (p0Dist < closestDistance)
  223. {
  224. closestDistance = p0Dist;
  225. closestPoint = p0;
  226. }
  227. if (p1Dist < closestDistance)
  228. {
  229. closestDistance = p1Dist;
  230. closestPoint = p1;
  231. }
  232. if (p2Dist < closestDistance)
  233. {
  234. closestDistance = p2Dist;
  235. closestPoint = p2;
  236. }
  237. }
  238. return closestPoint;
  239. }
  240. private Vector3 GetClosestPointOnEdge(Vector3 referencePoint, Vector3 edgePointA, Vector3 edgePointB)
  241. {
  242. Vector3 direction = edgePointB - edgePointA;
  243. float lineLength = direction.magnitude;
  244. direction.Normalize();
  245. float projectDistance = Mathf.Clamp(Vector3.Dot(referencePoint - edgePointA, direction), 0f, lineLength);
  246. return edgePointA + direction * projectDistance;
  247. }
  248. }
  249. }
  250. #endif