//======= Copyright (c) Stereolabs Corporation, All rights reserved. ===============
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
#if UNITY_EDITOR
using UnityEditor;
#endif
///
/// Takes a mesh supplied by ZEDManager after a scan and converts it into a
/// NavMesh at runtime that can be used for AI pathfinding.
/// If this script is present, the process will happen automatically when a scan is completed.
/// See the ZED spatial mapping tutorial for more info: https://docs.stereolabs.com/mixed-reality/unity/spatial-mapping-unity/
///
//[RequireComponent(typeof(ZEDManager))]
public class NavMeshSurface: MonoBehaviour
{
#if UNITY_5_6_OR_NEWER
///
/// Reference to this GameObject's ZEDSpatialMappingManager component.
///
[SerializeField]
public ZEDManager zedManagerSpatialMapping = null;
///
/// The ID of the agent type the NavMesh will be built for.
/// See available agent types (or create a new one) in Unity's Navigation window.
///
[SerializeField]
public int agentTypeID = 0;
///
/// List of all NavMeshBuildSource objects the script creates to make the final mesh.
/// One is created for each 'chunk' of the scan.
///
private List sources = new List();
///
/// The final NavMesh.
///
private NavMeshData navMesh;
///
/// Outside bounds of the mesh. Computed after all the chunks have been collected.
///
private Bounds bounds;
///
/// Material used to display the final NavMesh in the editor.
/// Normally Mat_ZED_Transparent_NavMesh.
///
private Material materialTransparent;
#endif
///
/// Arguments passed by NavMeshSurface's OnNavMeshReady event, so a class that places NPCs
/// (like EnemyManager) can place said NPC properly.
///
public class PositionEventArgs : System.EventArgs
{
///
/// The world space position of the center of the new NavMesh.
///
public Vector3 position;
///
/// Whether the NavMesh created is valid for placement (and drawing) purposes.
///
public bool valid = false;
///
/// The ID of the NavMesh's agent type.
///
public int agentTypeID = 0;
}
///
/// Event that gets called when the NavMesh is finished being created.
///
public static event System.EventHandler OnNavMeshReady;
///
/// Whether to display the NavMesh in the editor.
///
[HideInInspector]
[SerializeField]
public bool hideFlag = true;
///
/// Whether the NavMesh construction is finished or not.
///
[HideInInspector]
[SerializeField]
public bool isOver = false;
///
/// The GameObject that represents the NavMesh visually.
///
private GameObject navMeshObject;
///
/// World space position of the final NavMesh.
///
private Vector3 navMeshPosition;
///
/// Public accessor for the world space position of the final NavMesh.
///
public Vector3 NavMeshPosition { get { return navMeshObject != null ? navMeshPosition : Vector3.zero; } }
// Use this for initialization
void Start()
{
if(!zedManagerSpatialMapping)
{
zedManagerSpatialMapping = FindObjectOfType();
}
#if UNITY_5_6_OR_NEWER
navMesh = new NavMeshData();
NavMesh.AddNavMeshData(navMesh);
//Initialize a position for the bounds.
bounds = new Bounds(transform.position, new Vector3(30, 30, 30));
materialTransparent = Resources.Load("Materials/Mat_ZED_Transparent_NavMesh") as Material; //The material applied to the display object.
#endif
if (zedManagerSpatialMapping) {
zedManagerSpatialMapping.GetSpatialMapping.OnMeshReady += MeshIsOver;
zedManagerSpatialMapping.GetSpatialMapping.OnMeshStarted += NewNavMesh;
}
}
///
/// Clears the existing display object, if one exists.
///
void NewNavMesh()
{
if(navMeshObject != null)
{
Destroy(navMeshObject);
}
}
void OnDisable()
{
//Unsubscribe from events, as they can otherwise still fire when this component is disabled.
if (zedManagerSpatialMapping) {
zedManagerSpatialMapping.GetSpatialMapping.OnMeshReady -= MeshIsOver;
zedManagerSpatialMapping.GetSpatialMapping.OnMeshStarted -= NewNavMesh;
}
}
///
/// Called when a new NavMesh has finished being processed.
/// Updates several values and calls the OnMeshReady event with the proper arguments.
///
void MeshIsOver()
{
#if UNITY_5_6_OR_NEWER
UpdateNavMesh();
//Nav mesh has been built. Clear the sources as we don't need them
sources.Clear();
//Draw the nav mesh is possible (triangulation from navmesh has return an area)
bool isDrawn = Draw();
#endif
if (isDrawn)
{
PositionEventArgs args = new PositionEventArgs();
args.position = NavMeshPosition;
args.valid = isDrawn;
args.agentTypeID = agentTypeID;
System.EventHandler handler = OnNavMeshReady;
if (handler != null)
{
handler(this, args);
}
}else
{
Debug.LogWarning(ZEDLogMessage.Error2Str(ZEDLogMessage.ERROR.NAVMESH_NOT_GENERATED));
}
isOver = true;
}
///
/// Creates a mesh with the same shape as the NavMesh for the display object.
///
/// Whether the mesh is valid.
bool Draw()
{
NavMeshTriangulation triangulation = NavMesh.CalculateTriangulation();
if (triangulation.areas.Length == 0)
return false;
//Create a child object of this one that displays the NavMesh.
if (navMeshObject == null)
{
navMeshObject = new GameObject("NavMesh");
}
navMeshObject.transform.parent = transform;
MeshFilter meshFilter = navMeshObject.GetComponent();
if (!meshFilter)
{
meshFilter = navMeshObject.AddComponent();
}
meshFilter.mesh.Clear();
meshFilter.mesh.vertices = triangulation.vertices;
meshFilter.mesh.triangles = triangulation.indices;
meshFilter.mesh.RecalculateNormals();
MeshRenderer r = navMeshObject.GetComponent();
if (!r)
{
r = navMeshObject.AddComponent();
}
r.sharedMaterial = materialTransparent;
navMeshPosition = r.bounds.center;
navMeshObject.SetActive(false); //Hidden by default.
return true;
}
///
/// Collect all the submeshes, or 'chunks', converts them into NavMeshBuildSource objects,
/// then fills the sources list with them.
///
void CollectSources()
{
#if UNITY_5_6_OR_NEWER
sources.Clear();
foreach (var o in zedManagerSpatialMapping.MappingChunkList)
{
MeshFilter m = o.o.GetComponent();
if (m != null)
{
NavMeshBuildSource s = new NavMeshBuildSource();
s.shape = NavMeshBuildSourceShape.Mesh;
s.sourceObject = m.mesh;
s.transform = o.o.transform.localToWorldMatrix;
s.area = agentTypeID;
sources.Add(s);
}
}
#endif
}
///
/// Calculates the NavMesh's bounds and inflates them slightly.
/// Called after all NavmeshBuildSource objects have been created.
///
/// Sources.
void CalculateBounds(List sources)
{
//Use the unscaled matrix for the NavMeshSurface.
if (sources.Count != 0) {
bounds.center = transform.position;
//For each source, grows the bounds.
foreach (var src in sources) {
Mesh m = src.sourceObject as Mesh;
bounds.Encapsulate (m.bounds);
}
}
//Inflate the bounds a bit to avoid clipping co-planar sources.
bounds.Expand(0.1f);
}
///
/// Collects the meshes and constructs a single NavMesh from them.
///
void UpdateNavMesh()
{
isOver = false;
//First collect all the sources (submeshes).
CollectSources();
#if UNITY_5_6_OR_NEWER
if (sources.Count != 0)
{
//Adjust bounds.
CalculateBounds(sources);
//Update the NavMesh with sources and bounds.
var defaultBuildSettings = NavMesh.GetSettingsByID(agentTypeID);
NavMeshBuilder.UpdateNavMeshData(navMesh, defaultBuildSettings, sources, bounds);
}
#endif
}
#if UNITY_5_6_OR_NEWER
///
/// Hide or display the NavMesh using the display object (navMeshObject).
///
public void SwitchStateDisplayNavMesh()
{
hideFlag = !hideFlag;
if (navMeshObject != null)
{
navMeshObject.SetActive(hideFlag);
}
}
#endif
private void OnApplicationQuit()
{
Destroy(navMeshObject);
}
}
#if UNITY_5_6_OR_NEWER
#if UNITY_EDITOR
///
/// Custom editor for NavMeshSurface to extend how it's drawn in the Inspector.
/// It adds a custom drop-down for agent types, and the Display button that toggles the NavMesh's visibility.
///
[CustomEditor(typeof(NavMeshSurface))]
class ZEDNavMeshEditor : Editor
{
//Represent the relevant properties as SerializedProperties.
//This lets us manipulate and also save (serialize) the data in the scene.
///
/// Bound to zedManager, manager of ZED Camera
///
SerializedProperty managerID;
///
/// Bound to agentTypeID, the agent type of the NavMesh.
///
SerializedProperty agentID;
///
/// Bound to hideFlag, whether the NavMesh is visible or not.
///
SerializedProperty hideFlag;
///
/// Bound to isOver, whether the NavMesh has been calculated or not.
///
SerializedProperty isOver;
private void OnEnable()
{
//Bind the serialized properties to the relevant properties in NavMeshSurface.
managerID = serializedObject.FindProperty("zedManagerSpatialMapping");
agentID = serializedObject.FindProperty("agentTypeID");
hideFlag = serializedObject.FindProperty("hideFlag");
isOver = serializedObject.FindProperty("isOver");
}
///
/// Creates a custom drop-down for the Agent Type enum, which includes a button to
/// open the Navigation window for creating/looking up agent types.
///
/// Text of the label beside the drop-down. Usually "Agent Type".
/// SerializedProperty that the drop-down reads/writes.
public static void AgentTypePopup(string labelName, SerializedProperty agentTypeID)
{
var index = -1;
var count = NavMesh.GetSettingsCount();
//var agentTypeNames = new string[count + 2];
GUIContent[] agentTypeNames = new GUIContent[count + 2];
for (var i = 0; i < count; i++)
{
var id = NavMesh.GetSettingsByIndex(i).agentTypeID;
var name = NavMesh.GetSettingsNameFromID(id);
agentTypeNames[i] = new GUIContent(name);
if (id == agentTypeID.intValue)
index = i;
}
agentTypeNames[count] = new GUIContent("");
agentTypeNames[count + 1] = new GUIContent("Open Agent Settings...");
bool validAgentType = index != -1;
if (!validAgentType)
{
EditorGUILayout.HelpBox("Agent Type invalid.", MessageType.Warning);
}
var rect = EditorGUILayout.GetControlRect(true, EditorGUIUtility.singleLineHeight);
EditorGUI.BeginProperty(rect, GUIContent.none, agentTypeID);
EditorGUI.BeginChangeCheck();
GUIContent text = new GUIContent(labelName, "The ID of the agent type the NavMesh will be built for.");
index = EditorGUI.Popup(rect, text, index, agentTypeNames);
if (EditorGUI.EndChangeCheck())
{
if (index >= 0 && index < count)
{
var id = NavMesh.GetSettingsByIndex(index).agentTypeID;
agentTypeID.intValue = id;
}
else if (index == count + 1)
{
UnityEditor.AI.NavMeshEditorHelpers.OpenAgentSettings(-1);
}
}
EditorGUI.EndProperty();
}
public override void OnInspectorGUI()
{
NavMeshSurface obj = (NavMeshSurface)target;
serializedObject.Update();
GUIContent zedManagerLink = new GUIContent("ZED Rig", "ZEDManager (Rig) to link to NavMesh generation");
obj.zedManagerSpatialMapping = (ZEDManager)EditorGUILayout.ObjectField(zedManagerLink, obj.zedManagerSpatialMapping , typeof(ZEDManager), true);
AgentTypePopup("Agent Type", agentID);
GUI.enabled = isOver.boolValue; //Only let the user click the button if the NavMesh is finished.
GUIContent text = new GUIContent(hideFlag.boolValue ? "Hide" : "Display", "Toggle the visibility of the NavMesh.");
if (GUILayout.Button(text))
{
obj.SwitchStateDisplayNavMesh(); //Switch the setting to whatever it wasn't before.
}
serializedObject.ApplyModifiedProperties(); //Applies everything we just changed.
}
}
#endif
#endif