/// --------------------------------------------- /// Ultimate Character Controller /// Copyright (c) Opsive. All Rights Reserved. /// https://www.opsive.com /// --------------------------------------------- namespace Opsive.UltimateCharacterController.UI { using Opsive.Shared.Events; using Opsive.Shared.Game; using Opsive.Shared.Utility; using Opsive.UltimateCharacterController.Character; using Opsive.UltimateCharacterController.Utility; using UnityEngine; using UnityEngine.UI; /// /// The DamageIndicatorMonitor will show a directional arrow of the direction that the character was damaged from. /// public class DamageIndicatorMonitor : CharacterMonitor { /// /// Indicates the direction that the character took damage. /// private struct DamageIndicator { [Tooltip("The GameObject that did the damage.")] private Transform m_Attacker; [Tooltip("The position that the character was hit.")] private Vector3 m_Position; [Tooltip("The angle of the indicator.")] private float m_Angle; [Tooltip("The time that the indicator was shown.")] private float m_DisplayTime; [Tooltip("A reference to the indicator's GameObject.")] private GameObject m_GameObject; [Tooltip("A reference to the indicator's rect transform.")] private RectTransform m_RectTransform; [Tooltip("A reference to the indicator's image.")] private Image m_Image; public Transform Attacker { get { return m_Attacker; } } public Vector3 Position { get { return m_Position; } } public float Angle { get { return m_Angle; } set { m_Angle = value; } } public float DisplayTime { get { return m_DisplayTime; } set { m_DisplayTime = value; } } public GameObject GameObject { get { return m_GameObject; } } public RectTransform RectTransform { get { return m_RectTransform; } } public Image Image { get { return m_Image; } } /// /// Initializes the pooled HitIndicator values. /// /// The GameObject that did the damage. /// The angle of the indicator. /// A reference to the GameObject of the indicator. public void Initialize(Transform attacker, Vector3 position, float angle, GameObject gameObject) { m_Attacker = attacker; m_Position = position; m_GameObject = gameObject; m_RectTransform = gameObject.GetComponent(); m_Image = gameObject.GetComponent(); m_DisplayTime = Time.time; m_GameObject.SetActive(true); } } [Tooltip("Should the indicator be shown even if there isn't a force associated with the damage event?")] [SerializeField] protected bool m_AlwaysShowIndicator; [Tooltip("Should the indicator follow the position changes of the attacker?")] [SerializeField] protected bool m_FollowAttacker = true; [Tooltip("Prevents a new hit indicator from appearing if the angle is less than this threshold compared to an already displayed indicator.")] [SerializeField] protected float m_IndicatorAngleThreshold = 20; [Tooltip("The offset of the indicator from the center of the screen.")] [SerializeField] protected float m_IndicatorOffset = 50; [Tooltip("The amount of time that the indicator should be fully visible for.")] [SerializeField] protected float m_IndicatorVisiblityTime = 2; [Tooltip("The amount of time it takes for the indicator to fade.")] [SerializeField] protected float m_IndicatorFadeTime = 1; public bool AlwaysShowIndicator { get { return m_AlwaysShowIndicator; } set { m_AlwaysShowIndicator = value; } } public bool FollowAttacker { get { return m_FollowAttacker; } set { m_FollowAttacker = value; } } public float IndicatorAngleTreshold { get { return m_IndicatorAngleThreshold; } set { m_IndicatorAngleThreshold = value; } } public float IndicatorOffset { get { return m_IndicatorOffset; } set { m_IndicatorOffset = value; } } public float IndicatorVisiblityTime { get { return m_IndicatorVisiblityTime; } set { m_IndicatorVisiblityTime = value; } } public float IndicatorFadeTime { get { return m_IndicatorFadeTime; } set { m_IndicatorFadeTime = value; } } private GameObject m_GameObject; private Transform m_CameraTransform; private Transform m_CharacterTransform; private UltimateCharacterLocomotion m_CharacterLocomotion; private int m_ActiveDamageIndicatorCount; private DamageIndicator[] m_ActiveDamageIndicators; private GameObject[] m_StoredIndicators; private int m_DamageIndicatorIndex; /// /// Initialize the default values. /// protected override void Awake() { base.Awake(); m_GameObject = gameObject; var images = GetComponentsInChildren(true); m_StoredIndicators = new GameObject[images.Length]; m_ActiveDamageIndicators = new DamageIndicator[m_StoredIndicators.Length]; for (int i = 0; i < m_StoredIndicators.Length; ++i) { m_StoredIndicators[i] = images[i].gameObject; m_StoredIndicators[i].SetActive(false); } m_GameObject.SetActive(false); } /// /// Attaches the monitor to the specified character. /// /// The character to attach the monitor to. protected override void OnAttachCharacter(GameObject character) { if (m_Character != null) { EventHandler.UnregisterEvent(m_Character, "OnHealthDamage", OnDamage); EventHandler.UnregisterEvent(m_Character, "OnRespawn", OnRespawn); m_CameraTransform = null; } base.OnAttachCharacter(character); if (m_Character == null) { return; } // A camera must exist. var camera = UnityEngineUtility.FindCamera(m_Character); if (camera != null) { m_CameraTransform = camera.transform; } if (m_CameraTransform == null) { Debug.LogError("Error: The Damage Indicator Monitor must have a camera attached to the character."); return; } m_CharacterTransform = m_Character.transform; m_CharacterLocomotion = m_Character.GetCachedComponent(); EventHandler.RegisterEvent(m_Character, "OnHealthDamage", OnDamage); EventHandler.RegisterEvent(m_Character, "OnRespawn", OnRespawn); } /// /// The object has taken damage. /// /// The amount of damage taken. /// The position of the damage. /// The amount of force applied to the object while taking the damage. /// The GameObject that did the damage. /// The Collider that was hit. private void OnDamage(float amount, Vector3 position, Vector3 force, GameObject attacker, Collider hitCollider) { // Don't show a hit indicator if the force is 0 or there is no attacker. This prevents damage such as fall damage from showing the damage indicator. if ((!m_AlwaysShowIndicator && force.sqrMagnitude == 0) || attacker == null || m_ActiveDamageIndicatorCount == m_ActiveDamageIndicators.Length) { return; } var direction = Vector3.ProjectOnPlane(m_CharacterTransform.position - ((m_FollowAttacker && m_Character != attacker) ? attacker.transform.position : position), m_CharacterLocomotion.Up); // The hit indicator is shown on a 2D canvas so the y direction should be ignored. direction.y = 0; direction.Normalize(); // Determine the angle of the damage position to determine if a new damage indicator should be shown. var angle = Vector3.Angle(direction, m_CameraTransform.forward) * Mathf.Sign(Vector3.Dot(direction, m_CameraTransform.right)); // Do not show a new damage indicator if the angle is less than a threshold compared to the already displayed indicators. DamageIndicator damageIndicator; for (int i = 0; i < m_ActiveDamageIndicatorCount; ++i) { damageIndicator = m_ActiveDamageIndicators[i]; if (Mathf.Abs(angle - damageIndicator.Angle) < m_IndicatorAngleThreshold) { damageIndicator.DisplayTime = Time.time; m_ActiveDamageIndicators[i] = damageIndicator; return; } } // Add the indicator to the active hit indicators list and enable the component. damageIndicator = GenericObjectPool.Get(); damageIndicator.Initialize(attacker.transform, position, angle, m_StoredIndicators[m_DamageIndicatorIndex]); m_ActiveDamageIndicators[m_ActiveDamageIndicatorCount] = damageIndicator; m_ActiveDamageIndicatorCount++; m_DamageIndicatorIndex = (m_DamageIndicatorIndex + 1) % m_StoredIndicators.Length; // Allow the indicators to move/fade. m_GameObject.SetActive(true); } /// /// One or more hit indicators are shown. /// private void Update() { for (int i = m_ActiveDamageIndicatorCount - 1; i > -1; --i) { // The alpha value is determined by the amount of time the damage indicator has been visible. The indicator should be visible for a time of m_IndicatorVisiblityTime // with no fading. After m_IndicatorVisiblityTime the indicator should fade for visibilityTime. var alpha = (m_IndicatorFadeTime - (Time.time - (m_ActiveDamageIndicators[i].DisplayTime + m_IndicatorVisiblityTime))) / m_IndicatorFadeTime; if (alpha <= 0) { m_ActiveDamageIndicators[i].GameObject.SetActive(false); GenericObjectPool.Return(m_ActiveDamageIndicators[i]); m_ActiveDamageIndicatorCount--; // Sort the array so the complete indicators are at the end. for (int j = i; j < m_ActiveDamageIndicatorCount; ++j) { m_ActiveDamageIndicators[j] = m_ActiveDamageIndicators[j + 1]; } continue; } var color = m_ActiveDamageIndicators[i].Image.color; color.a = alpha; m_ActiveDamageIndicators[i].Image.color = color; var direction = Vector3.ProjectOnPlane(m_CharacterTransform.position - ((m_FollowAttacker && m_CharacterTransform != m_ActiveDamageIndicators[i].Attacker) ? m_ActiveDamageIndicators[i].Attacker.position : m_ActiveDamageIndicators[i].Position), m_CharacterLocomotion.Up); // The hit indicator is shown on a 2D canvas so the y direction should be ignored. direction.y = 0; direction.Normalize(); var angle = Vector3.Angle(direction, m_CameraTransform.forward) * Mathf.Sign(Vector3.Dot(direction, m_CameraTransform.right)); m_ActiveDamageIndicators[i].Angle = angle; // Face the image in the direction of the angle. var rotation = m_ActiveDamageIndicators[i].RectTransform.localEulerAngles; rotation.z = -m_ActiveDamageIndicators[i].Angle; m_ActiveDamageIndicators[i].RectTransform.localEulerAngles = rotation; // Position the indicator relative to the direction. var position = m_ActiveDamageIndicators[i].RectTransform.localPosition; position.x = -Mathf.Sin(m_ActiveDamageIndicators[i].Angle * Mathf.Deg2Rad) * m_IndicatorOffset; position.y = -Mathf.Cos(m_ActiveDamageIndicators[i].Angle * Mathf.Deg2Rad) * m_IndicatorOffset; m_ActiveDamageIndicators[i].RectTransform.localPosition = position; } // The component can be disabled when the damage indicators have disappeared. if (m_ActiveDamageIndicatorCount == 0) { m_GameObject.SetActive(false); } } /// /// The character has respawned. /// private void OnRespawn() { // No indicators should be shown when the character respawns. for (int i = m_ActiveDamageIndicatorCount - 1; i > -1; --i) { m_ActiveDamageIndicators[i].GameObject.SetActive(false); GenericObjectPool.Return(m_ActiveDamageIndicators[i]); } m_ActiveDamageIndicatorCount = 0; m_GameObject.SetActive(false); } /// /// Can the UI be shown? /// /// True if the UI can be shown. protected override bool CanShowUI() { return base.CanShowUI() && m_ActiveDamageIndicatorCount > 0; } } }