#if UNITY_NAVIGATION_COMPONENTS
using System;
using System.Collections.Generic;
using System.Linq;
using Unity.AI.Navigation;
using UnityEngine;
using UnityEngine.AI;
namespace DunGen.Adapters
{
[AddComponentMenu("DunGen/NavMesh/Unity NavMesh Adapter")]
public class UnityNavMeshAdapter : NavMeshAdapter
{
#region Nested Types
public enum RuntimeNavMeshBakeMode
{
///
/// Uses only existing baked surfaces found in the dungeon tiles, no runtime baking is performed
///
PreBakedOnly,
///
/// Uses existing baked surfaces in the tiles if any are found, otherwise new surfaces will be added and baked at runtime
///
AddIfNoSurfaceExists,
///
/// Adds new surfaces where they don't already exist. Rebakes all at runtime
///
AlwaysRebake,
///
/// Bakes a single surface for the entire dungeon at runtime. No links will be made
///
FullDungeonBake,
}
[Serializable]
public sealed class NavMeshAgentLinkInfo
{
public int AgentTypeID = 0;
public int AreaTypeID = 0;
public bool DisableLinkWhenDoorIsClosed = true;
}
#endregion
public RuntimeNavMeshBakeMode BakeMode = RuntimeNavMeshBakeMode.AddIfNoSurfaceExists;
public LayerMask LayerMask = ~0;
public bool AddNavMeshLinksBetweenRooms = true;
public List NavMeshAgentTypes = new List() { new NavMeshAgentLinkInfo() };
public float NavMeshLinkDistanceFromDoorway = 2.5f;
public bool AutoGenerateFullRebakeSurfaces = true;
public List FullRebakeTargets = new List();
public bool UseAutomaticLinkDistance = false;
public float AutomaticLinkDistanceOffset = 0.1f;
private List addedSurfaces = new List();
private List fullBakeSurfaces = new List();
public override void Generate(Dungeon dungeon)
{
if (BakeMode == RuntimeNavMeshBakeMode.FullDungeonBake)
{
BakeFullDungeon(dungeon);
return;
}
// Bake Surfaces
if (BakeMode != RuntimeNavMeshBakeMode.PreBakedOnly)
{
foreach (var tile in dungeon.AllTiles)
{
// Find existing surfaces
var existingSurfaces = tile.gameObject.GetComponentsInChildren();
// Add surfaces for any agent type that is missing one
var addedSurfaces = AddMissingSurfaces(tile, existingSurfaces);
// Gather surfaces to bake
IEnumerable surfacesToBake = addedSurfaces;
// Append all existing surfaces if mode is set to "Always Rebake"
if (BakeMode == RuntimeNavMeshBakeMode.AlwaysRebake)
surfacesToBake = surfacesToBake.Concat(existingSurfaces);
// Append only unbaked surfaces if mode is set to "Add if no Surface Exists"
else if (BakeMode == RuntimeNavMeshBakeMode.AddIfNoSurfaceExists)
{
var existingUnbakedSurfaces = existingSurfaces.Where(x => x.navMeshData == null);
surfacesToBake = surfacesToBake.Concat(existingUnbakedSurfaces);
}
// Bake
foreach (var surface in surfacesToBake.Distinct())
surface.BuildNavMesh();
}
}
// Add links between rooms
if (AddNavMeshLinksBetweenRooms)
{
foreach (var connection in dungeon.Connections)
foreach (var linkInfo in NavMeshAgentTypes)
AddNavMeshLink(connection, linkInfo);
}
if (OnProgress != null)
OnProgress(new NavMeshGenerationProgress() { Description = "Done", Percentage = 1.0f });
}
private void BakeFullDungeon(Dungeon dungeon)
{
if (AutoGenerateFullRebakeSurfaces)
{
foreach (var surface in fullBakeSurfaces)
if (surface != null)
surface.RemoveData();
fullBakeSurfaces.Clear();
int settingsCount = NavMesh.GetSettingsCount();
for (int i = 0; i < settingsCount; i++)
{
var settings = NavMesh.GetSettingsByIndex(i);
// Find a surface if it already exists
var surface = dungeon.gameObject.GetComponents()
.Where(s => s.agentTypeID == settings.agentTypeID)
.FirstOrDefault();
if (surface == null)
{
surface = dungeon.gameObject.AddComponent();
surface.agentTypeID = settings.agentTypeID;
surface.collectObjects = CollectObjects.Children;
surface.layerMask = LayerMask;
}
fullBakeSurfaces.Add(surface);
surface.BuildNavMesh();
}
// Disable all other surfaces to avoid overlapping navmeshes
foreach (var surface in dungeon.gameObject.GetComponentsInChildren())
if (!fullBakeSurfaces.Contains(surface))
surface.enabled = false;
}
else
{
foreach (var surface in FullRebakeTargets)
surface.BuildNavMesh();
}
if (OnProgress != null)
OnProgress(new NavMeshGenerationProgress() { Description = "Done", Percentage = 1.0f });
}
private NavMeshSurface[] AddMissingSurfaces(Tile tile, NavMeshSurface[] existingSurfaces)
{
addedSurfaces.Clear();
int settingsCount = NavMesh.GetSettingsCount();
for (int i = 0; i < settingsCount; i++)
{
var settings = NavMesh.GetSettingsByIndex(i);
// We already have a surface for this agent type
if (existingSurfaces.Where(x => x.agentTypeID == settings.agentTypeID).Any())
continue;
var surface = tile.gameObject.AddComponent();
surface.agentTypeID = settings.agentTypeID;
surface.collectObjects = CollectObjects.Children;
surface.layerMask = LayerMask;
addedSurfaces.Add(surface);
}
return addedSurfaces.ToArray();
}
private void AddNavMeshLink(DoorwayConnection connection, NavMeshAgentLinkInfo agentLinkInfo)
{
var doorway = connection.A.gameObject;
var agentSettings = NavMesh.GetSettingsByID(agentLinkInfo.AgentTypeID);
// We need to account for the agent's radius when setting the link's width
float linkWidth = Mathf.Max(connection.A.Socket.Size.x - (agentSettings.agentRadius * 2), 0.01f);
// Add NavMeshLink to one of the doorways
var link = doorway.AddComponent();
link.agentTypeID = agentLinkInfo.AgentTypeID;
link.bidirectional = true;
link.area = agentLinkInfo.AreaTypeID;
link.width = linkWidth;
if (UseAutomaticLinkDistance)
{
link.startPoint = doorway.transform.InverseTransformPoint(GetClosestPointOnNavMesh(doorway.transform.position, doorway.transform.forward)) + new Vector3(0f, 0f, AutomaticLinkDistanceOffset);
link.endPoint = doorway.transform.InverseTransformPoint(GetClosestPointOnNavMesh(doorway.transform.position, -doorway.transform.forward)) - new Vector3(0f, 0f, AutomaticLinkDistanceOffset);
}
else
{
link.startPoint = new Vector3(0, 0, -NavMeshLinkDistanceFromDoorway);
link.endPoint = new Vector3(0, 0, NavMeshLinkDistanceFromDoorway);
}
if (agentLinkInfo.DisableLinkWhenDoorIsClosed)
{
// If there is a door in this doorway, hookup event listeners to enable/disable the link when the door is opened/closed respectively
GameObject doorObj = (connection.A.UsedDoorPrefabInstance != null) ? connection.A.UsedDoorPrefabInstance : (connection.B.UsedDoorPrefabInstance != null) ? connection.B.UsedDoorPrefabInstance : null;
if (doorObj != null)
{
var door = doorObj.GetComponent();
link.enabled = door.IsOpen;
if (door != null)
door.OnDoorStateChanged += (d, o) => link.enabled = o;
}
}
}
///
/// Finds the closest point on the navigation mesh. Unlike NavMesh.FindClosestEdge and
/// NavMesh.Raycast, this method works reliably when given a point that is not on the navmesh
///
/// The position we want to find the nearest point to
/// An optional direction. If specified, this will ignore points in the opposite direction
///
private Vector3 GetClosestPointOnNavMesh(Vector3 point, Vector3? distanceDirection = null)
{
float CalculateDistance(Vector3 p0, Vector3 p1)
{
if (distanceDirection == null)
return (p0 - p1).magnitude;
else
{
float distance = Vector3.Dot(p1 - p0, distanceDirection.Value);
if (distance <= 0f)
return float.PositiveInfinity;
else
return (p0 - p1).magnitude;
}
}
var triangulation = NavMesh.CalculateTriangulation();
Vector3 closestPoint = point;
float closestDistance = float.PositiveInfinity;
for (int i = 2; i < triangulation.indices.Length; i += 3)
{
Vector3 v0 = triangulation.vertices[triangulation.indices[i - 2]];
Vector3 v1 = triangulation.vertices[triangulation.indices[i - 1]];
Vector3 v2 = triangulation.vertices[triangulation.indices[i]];
Vector3 p0 = GetClosestPointOnEdge(point, v0, v1);
Vector3 p1 = GetClosestPointOnEdge(point, v1, v2);
Vector3 p2 = GetClosestPointOnEdge(point, v2, v0);
float p0Dist = CalculateDistance(point, p0);
float p1Dist = CalculateDistance(point, p1);
float p2Dist = CalculateDistance(point, p2);
if (p0Dist < closestDistance)
{
closestDistance = p0Dist;
closestPoint = p0;
}
if (p1Dist < closestDistance)
{
closestDistance = p1Dist;
closestPoint = p1;
}
if (p2Dist < closestDistance)
{
closestDistance = p2Dist;
closestPoint = p2;
}
}
return closestPoint;
}
private Vector3 GetClosestPointOnEdge(Vector3 referencePoint, Vector3 edgePointA, Vector3 edgePointB)
{
Vector3 direction = edgePointB - edgePointA;
float lineLength = direction.magnitude;
direction.Normalize();
float projectDistance = Mathf.Clamp(Vector3.Dot(referencePoint - edgePointA, direction), 0f, lineLength);
return edgePointA + direction * projectDistance;
}
}
}
#endif