/// --------------------------------------------- /// Ultimate Character Controller /// Copyright (c) Opsive. All Rights Reserved. /// https://www.opsive.com /// --------------------------------------------- namespace Opsive.UltimateCharacterController.SurfaceSystem { using Opsive.Shared.Game; using Opsive.UltimateCharacterController.Game; using Opsive.UltimateCharacterController.Utility; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; /// /// The DecalManager is responsible for managing the spawned decals. The decals can be capped at a limit to prevent too many from being /// spawned. These decals can then slowly be faded (weathered) for a smooth transition rather than the decal just popping out of existance. /// public class DecalManager : MonoBehaviour { private static DecalManager s_Instance; private static DecalManager Instance { get { if (!s_Initialized) { s_Instance = new GameObject("Decal Manager").AddComponent(); s_Initialized = true; } return s_Instance; } } private static bool s_Initialized; [Tooltip("The maximum number of decals.")] [SerializeField] protected int m_DecalLimit = 100; [Tooltip("The number of decals which should slowly fade after the decal limit has been reached.")] [SerializeField] protected int m_WeatheredDecalLimit = 20; [Tooltip("The speed that the decals should fadeout after they have been removed from the weathered array.")] [SerializeField] protected int m_RemoveFadeoutSpeed = 10; public int DecalLimit { get { return m_DecalLimit; } set { m_DecalLimit = value; } } public int WeatheredDecalLimit { get { return m_WeatheredDecalLimit; } set { m_WeatheredDecalLimit = value; } } public int RemoveFadeoutSpeed { get { return m_RemoveFadeoutSpeed; } set { m_RemoveFadeoutSpeed = value; } } private List m_Decals = new List(); private List m_WeatheredDecals = new List(); private List m_DecalsToFade = new List(); private Dictionary m_DecalRendererMap = new Dictionary(); private Dictionary m_DecalMeshMap = new Dictionary(); /// /// The object has been enabled. /// private void OnEnable() { // The object may have been enabled outside of the scene unloading. if (s_Instance == null) { s_Instance = this; s_Initialized = true; SceneManager.sceneUnloaded -= SceneUnloaded; } } /// /// Instantiates a new decal. /// /// The original prefab to spawn an instance of. /// The RaycastHit which caused the decal to spawn. /// The scale of the decal to spawn. /// How close to the edge the decal is allowed to spawn. public static void Spawn(GameObject original, RaycastHit hit, float scale, float allowedEdgeOverlap) { Instance.SpawnInternal(original, hit, scale, allowedEdgeOverlap); } /// /// Internal method which instantiates a new decal. /// /// The original prefab to spawn an instance of. /// The RaycastHit which caused the decal to spawn. /// The scale of the decal to spawn. /// How close to the edge the decal is allowed to spawn. private void SpawnInternal(GameObject original, RaycastHit hit, float scale, float allowedEdgeOverlap) { SpawnDecal(original, hit, Quaternion.LookRotation(hit.normal) * Quaternion.AngleAxis(Random.Range(0, 360), Vector3.forward), scale, allowedEdgeOverlap); } /// /// Instantiates a new footprint. /// /// The original prefab to spawn an instance of. /// The RaycastHit which caused the footprint to spawn. /// The scale of the decal to spawn. /// How close to the edge the footprint is allowed to spawn. /// The direction that the footprint decal should face. /// Should the footprint decal be flipped? public static void SpawnFootprint(GameObject original, RaycastHit hit, float scale, float allowedEdgeOverlap, Vector3 footprintDirection, bool flipFootprint) { Instance.SpawnFootprintInternal(original, hit, scale, allowedEdgeOverlap, footprintDirection, flipFootprint); } /// /// Internal method which instantiates a new footprint. /// /// The original prefab to spawn an instance of. /// The RaycastHit which caused the footprint to spawn. /// The scale of the decal to spawn. /// How close to the edge the footprint is allowed to spawn. /// The direction that the footprint decal should face. /// Should the footprint decal be flipped? private void SpawnFootprintInternal(GameObject original, RaycastHit hit, float scale, float allowedEdgeOverlap, Vector3 footprintDirection, bool flipFootprint) { var decal = SpawnDecal(original, hit, Quaternion.LookRotation(hit.normal, footprintDirection), scale, allowedEdgeOverlap); // Changing the local x axis will flip the footprint. if (decal != null && flipFootprint) { var localScale = decal.transform.localScale; localScale.x *= -1; decal.transform.localScale = localScale; } } /// /// Instantiates a new decal. /// /// The original prefab to spawn an instance of. /// The RaycastHit which caused the footprint to spawn. /// The rotation of the decal which should be spawned. /// The scale of the decal to spawn. /// How close to the edge the footprint is allowed to spawn. /// The spawned decal. Can be null. private GameObject SpawnDecal(GameObject original, RaycastHit hit, Quaternion rotation, float scale, float allowedEdgeOverlap) { // Prevent z fighting by slightly raising the decal off of the surface. var decal = ObjectPool.Instantiate(original, hit.point + (hit.normal * 0.001f), rotation); // Only set the decal parent to the hit transform on uniform objects to prevent stretching. if (MathUtility.IsUniform(hit.transform.localScale)) { decal.transform.parent = hit.transform; } if (scale != 1) { var vectorScale = Vector3.one; vectorScale.x = vectorScale.y = scale; decal.transform.localScale = Vector3.Scale(decal.transform.localScale, vectorScale); } // Destroy the object if it cannot be cached. The object won't be able to be cached if it doesn't have all of the required components. if (!CacheMeshAndRenderer(decal)) { ObjectPool.Destroy(decal); return null; } // Do a test on the decal's quad to ensure all four corners are flush against a surface. This will prevent the decal from sticking out on an edge. if (allowedEdgeOverlap < 0.5f) { if (!DoQuadTest(decal, allowedEdgeOverlap)) { ObjectPool.Destroy(decal); return null; } } // The decal can be added. Add(decal); return decal; } /// /// Stores the decal's mesh and renderer. /// /// The decal to store the mesh and renderer of. /// True if the mesh and renderer were able to be cached. private bool CacheMeshAndRenderer(GameObject decal) { Renderer renderer; if (!m_DecalRendererMap.TryGetValue(decal, out renderer)) { var meshFilter = decal.GetComponent(); if (meshFilter == null) { return false; } if (meshFilter.mesh == null) { return false; } renderer = decal.GetComponent(); if (renderer == null) { return false; } if (renderer.material == null) { return false; } // Cache the decal renderer and mesh. m_DecalRendererMap.Add(decal, renderer); m_DecalMeshMap.Add(decal, meshFilter.mesh); } // The decal should start opaque. var color = renderer.material.color; color.a = 1; renderer.material.color = color; return true; } /// /// Check all four corners of the decal for surface contact. /// /// The decal to check the corners of. /// How close to the edge the decal is allowed to spawn. /// True if all four corners are flush against a surface. private bool DoQuadTest(GameObject decal, float allowedEdgeOverlap) { Mesh mesh; if (!m_DecalMeshMap.TryGetValue(decal, out mesh)) { return false; } RaycastHit hit; for (int i = 0; i < 4; i++) { // The decal isn't hitting anything if the raycast returns false. if (!Physics.Raycast(decal.transform.TransformPoint(mesh.vertices[i] * (1 - (allowedEdgeOverlap * 2))) + (decal.transform.forward * 0.1f), -decal.transform.forward, out hit, 0.2f, ~((1 << LayerManager.TransparentFX) | (1 << LayerManager.IgnoreRaycast) | (1 << LayerManager.VisualEffect) | (1 << LayerManager.Water)), QueryTriggerInteraction.Ignore)) { return false; } } return true; } /// /// Adds the decal to the active decal stack. /// /// The decal to add. private void Add(GameObject decal) { m_Decals.Add(decal); // If the total decal count is greater than the specified limit then the oldest decal should start to be weathered. if (m_Decals.Count >= m_DecalLimit) { var oldestDecalRenderer = m_DecalRendererMap[m_Decals[0]]; m_WeatheredDecals.Add(oldestDecalRenderer); m_Decals.RemoveAt(0); WeatherDecals(); } } /// /// Slowly fade out the oldest decal in the weathered list. /// private void WeatherDecals() { // As each decal is added to the weathered list it should slowly fade out. for (int i = 0; i < m_WeatheredDecals.Count; ++i) { if (m_WeatheredDecals[i] == null) { m_WeatheredDecals.RemoveAt(i); continue; } var color = m_WeatheredDecals[i].material.color; color.a = Mathf.Clamp01(color.a - (1 / (float)m_WeatheredDecalLimit)); m_WeatheredDecals[i].material.color = color; } // Remove the oldest weathered decal if the limit is reached. This decal will be added to the fade list. if (m_WeatheredDecals.Count >= m_WeatheredDecalLimit) { m_DecalsToFade.Add(m_WeatheredDecals[0]); m_WeatheredDecals.RemoveAt(0); enabled = true; } } /// /// Fade out the decals in the decals to fade list. /// private void Update() { for (int i = m_DecalsToFade.Count - 1; i >= 0; --i) { if (m_DecalsToFade[i] == null) { m_DecalsToFade.RemoveAt(i); continue; } var color = m_DecalsToFade[i].material.color; color.a = Mathf.Lerp(color.a, 0, Time.deltaTime * m_RemoveFadeoutSpeed); // The decal can be removed from the list when it is completely faded out. if (color.a == 0) { ObjectPool.Destroy(m_DecalsToFade[i].gameObject); m_DecalsToFade.RemoveAt(i); } else { m_DecalsToFade[i].material.color = color; } } // The component can be disabled when there are no decals within the list. if (m_DecalsToFade.Count == 0) { enabled = false; } } /// /// Reset the initialized variable when the scene is no longer loaded. /// /// The scene that was unloaded. private void SceneUnloaded(Scene scene) { s_Initialized = false; s_Instance = null; SceneManager.sceneUnloaded -= SceneUnloaded; } /// /// The object has been disabled. /// private void OnDisable() { SceneManager.sceneUnloaded += SceneUnloaded; } #if UNITY_2019_3_OR_NEWER /// /// Reset the static variables for domain reloading. /// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void DomainReset() { s_Initialized = false; s_Instance = null; } #endif } }