/// --------------------------------------------- /// Ultimate Character Controller /// Copyright (c) Opsive. All Rights Reserved. /// https://www.opsive.com /// --------------------------------------------- namespace Opsive.UltimateCharacterController.Traits { using Opsive.Shared.Events; using Opsive.Shared.Game; using Opsive.Shared.Utility; using Opsive.UltimateCharacterController.Audio; using Opsive.UltimateCharacterController.Events; using Opsive.UltimateCharacterController.Game; using Opsive.UltimateCharacterController.Objects; #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER using Opsive.UltimateCharacterController.Networking; using Opsive.UltimateCharacterController.Networking.Traits; #endif using Opsive.UltimateCharacterController.StateSystem; using Opsive.UltimateCharacterController.Utility; using System.Collections.Generic; using UnityEngine; /// /// Adds health and a shield to the object. /// [RequireComponent(typeof(AttributeManager))] public class Health : StateBehavior { [Tooltip("Is the object invincible?")] [SerializeField] protected bool m_Invincible; [Tooltip("The amount of time that the object is invincible after respawning.")] [SerializeField] protected float m_TimeInvincibleAfterSpawn; [Tooltip("The name of the health attribute.")] [SerializeField] protected string m_HealthAttributeName = "Health"; [Tooltip("The name of the shield attribute.")] [SerializeField] protected string m_ShieldAttributeName; [Tooltip("The list of Colldiers that should apply a multiplier when damaged.")] [SerializeField] protected Hitbox[] m_Hitboxes; [Tooltip("The maximum number of colliders that can be detected when determining if a hitbox was damaged.")] [SerializeField] protected int m_MaxHitboxCollisionCount = 10; [Tooltip("Any object that should spawn when the object dies.")] [SerializeField] protected GameObject[] m_SpawnedObjectsOnDeath; [Tooltip("Any object that should be destroyed when the object dies.")] [SerializeField] protected GameObject[] m_DestroyedObjectsOnDeath; [Tooltip("Should the object be deactivated on death?")] [SerializeField] protected bool m_DeactivateOnDeath; [Tooltip("If DeactivateOnDeath is enabled, specify a delay for the object to be deactivated.")] [SerializeField] protected float m_DeactivateOnDeathDelay; [Tooltip("The layer that the GameObject should switch to upon death.")] [SerializeField] protected LayerMask m_DeathLayer; [Tooltip("A set of AudioClips that can be played when the object takes damage.")] [HideInInspector] [SerializeField] protected AudioClipSet m_TakeDamageAudioClipSet = new AudioClipSet(); [Tooltip("A set of AudioClips that can be played when the object is healed.")] [HideInInspector] [SerializeField] protected AudioClipSet m_HealAudioClipSet = new AudioClipSet(); [Tooltip("A set of AudioClips that can be played when the object dies.")] [HideInInspector] [SerializeField] protected AudioClipSet m_DeathAudioClipSet = new AudioClipSet(); [Tooltip("Unity event invoked when taking damage.")] [SerializeField] protected UnityFloatVector3Vector3GameObjectEvent m_OnDamageEvent; [Tooltip("Unity event invoked when healing.")] [SerializeField] protected UnityFloatEvent m_OnHealEvent; [Tooltip("Unity event invoked when the object dies.")] [SerializeField] protected UnityVector3Vector3GameObjectEvent m_OnDeathEvent; public bool Invincible { get { return m_Invincible; } set { m_Invincible = value; } } public float TimeInvincibleAfterSpawn { get { return m_TimeInvincibleAfterSpawn; } set { m_TimeInvincibleAfterSpawn = value; } } public string HealthAttributeName { get { return m_HealthAttributeName; } set { m_HealthAttributeName = value; if (Application.isPlaying) { if (!string.IsNullOrEmpty(m_HealthAttributeName)) { m_HealthAttribute = m_AttributeManager.GetAttribute(m_HealthAttributeName); } else { m_HealthAttribute = null; } } } } public string ShieldAttributeName { get { return m_ShieldAttributeName; } set { m_ShieldAttributeName = value; if (Application.isPlaying) { if (!string.IsNullOrEmpty(m_ShieldAttributeName)) { m_ShieldAttribute = m_AttributeManager.GetAttribute(m_ShieldAttributeName); } else { m_ShieldAttribute = null; } } } } [NonSerialized] public Hitbox[] Hitboxes { get { return m_Hitboxes; } set { m_Hitboxes = value; } } public int MaxHitboxCollisionCount { get { return m_MaxHitboxCollisionCount; } set { m_MaxHitboxCollisionCount = value; } } public GameObject[] SpawnedObjectsOnDeath { get { return m_SpawnedObjectsOnDeath; } set { m_SpawnedObjectsOnDeath = value; } } public GameObject[] DestroyedObjectsOnDeath { get { return m_DestroyedObjectsOnDeath; } set { m_DestroyedObjectsOnDeath = value; } } public bool DeactivateOnDeath { get { return m_DeactivateOnDeath; } set { m_DeactivateOnDeath = value; } } public float DeactivateOnDeathDelay { get { return m_DeactivateOnDeathDelay; } set { m_DeactivateOnDeathDelay = value; } } public LayerMask DeathLayer { get { return m_DeathLayer; } set { m_DeathLayer = value; } } public AudioClipSet TakeDamageAudioClipSet { get { return m_TakeDamageAudioClipSet; } set { m_TakeDamageAudioClipSet = value; } } public AudioClipSet HealAudioClipSet { get { return m_HealAudioClipSet; } set { m_HealAudioClipSet = value; } } public AudioClipSet DeathAudioClipSet { get { return m_DeathAudioClipSet; } set { m_DeathAudioClipSet = value; } } public UnityFloatVector3Vector3GameObjectEvent OnDamageEvent { get { return m_OnDamageEvent; } set { m_OnDamageEvent = value; } } public UnityFloatEvent OnHealEvent { get { return m_OnHealEvent; } set { m_OnHealEvent = value; } } public UnityVector3Vector3GameObjectEvent OnDeathEvent { get { return m_OnDeathEvent; } set { m_OnDeathEvent = value; } } protected GameObject m_GameObject; protected Transform m_Transform; private IForceObject m_ForceObject; private Rigidbody m_Rigidbody; private AttributeManager m_AttributeManager; private Attribute m_HealthAttribute; private Attribute m_ShieldAttribute; #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER private INetworkInfo m_NetworkInfo; private INetworkHealthMonitor m_NetworkHealthMonitor; #endif private float m_SpawnTime; private int m_AliveLayer; private Dictionary m_ColliderHitboxMap; private RaycastHit[] m_RaycastHits; private UnityEngineUtility.RaycastHitComparer m_RaycastHitComparer; public float HealthValue { get { return (m_HealthAttribute != null ? m_HealthAttribute.Value : 0); } } public float ShieldValue { get { return (m_ShieldAttribute != null ? m_ShieldAttribute.Value : 0); } } public float Value { get { return HealthValue + ShieldValue; } } /// /// Initialize the default values. /// protected override void Awake() { base.Awake(); m_GameObject = gameObject; m_Transform = transform; m_ForceObject = m_GameObject.GetCachedComponent(); m_Rigidbody = m_GameObject.GetCachedComponent(); m_AttributeManager = GetComponent(); if (!string.IsNullOrEmpty(m_HealthAttributeName)) { m_HealthAttribute = m_AttributeManager.GetAttribute(m_HealthAttributeName); } if (!string.IsNullOrEmpty(m_ShieldAttributeName)) { m_ShieldAttribute = m_AttributeManager.GetAttribute(m_ShieldAttributeName); } m_AliveLayer = m_GameObject.layer; #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER m_NetworkInfo = m_GameObject.GetCachedComponent(); m_NetworkHealthMonitor = m_GameObject.GetCachedComponent(); if (m_NetworkInfo != null && m_NetworkHealthMonitor == null) { Debug.LogError("Error: The object " + m_GameObject.name + " must have a NetworkHealthMonitor component."); } #endif if (m_Hitboxes != null && m_Hitboxes.Length > 0) { m_ColliderHitboxMap = new Dictionary(); for (int i = 0; i < m_Hitboxes.Length; ++i) { m_ColliderHitboxMap.Add(m_Hitboxes[i].Collider, m_Hitboxes[i]); } m_RaycastHits = new RaycastHit[m_MaxHitboxCollisionCount]; m_RaycastHitComparer = new UnityEngineUtility.RaycastHitComparer(); } EventHandler.RegisterEvent(m_GameObject, "OnRespawn", OnRespawn); } /// /// The object has been damaged. /// /// The amount of damage taken. public void Damage(float amount) { Damage(amount, m_Transform.position, Vector3.zero, 1, 0, 0, null, null); } /// /// The object has been damaged. /// /// The amount of damage taken. /// The position of the damage. /// The direction that the object took damage from. /// The magnitude of the force that is applied to the object. public void Damage(float amount, Vector3 position, Vector3 direction, float forceMagnitude) { Damage(amount, position, direction, forceMagnitude, 1, 0, null, null); } /// /// The object has been damaged. /// /// The amount of damage taken. /// The position of the damage. /// The direction that the object took damage from. /// The magnitude of the force that is applied to the object. /// The radius of the explosive damage. If 0 then a non-exposive force will be used. public void Damage(float amount, Vector3 position, Vector3 direction, float forceMagnitude, float radius) { Damage(amount, position, direction, forceMagnitude, 1, radius, null, null); } /// /// The object has been damaged. /// /// The amount of damage taken. /// The position of the damage. /// The direction that the object took damage from. /// The magnitude of the force that is applied to the object. /// The GameObject that did the damage. public void Damage(float amount, Vector3 position, Vector3 direction, float forceMagnitude, GameObject attacker) { Damage(amount, position, direction, forceMagnitude, 1, 0, attacker, null); } /// /// The object has been damaged. /// /// The amount of damage taken. /// The position of the damage. /// The direction that the object took damage from. /// The magnitude of the force that is applied to the object. /// The number of frames to add the force to. /// The radius of the explosive damage. If 0 then a non-exposive force will be used. /// The GameObject that did the damage. public void Damage(float amount, Vector3 position, Vector3 direction, float forceMagnitude, int frames, float radius, GameObject attacker) { Damage(amount, position, direction, forceMagnitude, frames, radius, attacker, null); } /// /// The object has been damaged. /// /// The amount of damage taken. /// The position of the damage. /// The direction that the object took damage from. /// The magnitude of the force that is applied to the object. /// The number of frames to add the force to. /// The radius of the explosive damage. If 0 then a non-explosive force will be used. /// The GameObject that did the damage. /// The Collider that was hit. public void Damage(float amount, Vector3 position, Vector3 direction, float forceMagnitude, int frames, float radius, GameObject attacker, Collider hitCollider) { Damage(amount, position, direction, forceMagnitude, frames, radius, attacker, null, hitCollider); } /// /// The object has been damaged. /// /// The amount of damage taken. /// The position of the damage. /// The direction that the object took damage from. /// The magnitude of the force that is applied to the object. /// The number of frames to add the force to. /// The radius of the explosive damage. If 0 then a non-explosive force will be used. /// The GameObject that did the damage. /// The object that did the damage. /// The Collider that was hit. public void Damage(float amount, Vector3 position, Vector3 direction, float forceMagnitude, int frames, float radius, GameObject attacker, object attackerObject, Collider hitCollider) { #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (m_NetworkInfo != null && !m_NetworkInfo.IsLocalPlayer()) { return; } #endif // Don't take any damage if the object is invincible, already dead, or just spawned and is invincible for a small amount of time. if (m_Invincible || !IsAlive() || m_SpawnTime + m_TimeInvincibleAfterSpawn > Time.time || amount == 0) { return; } OnDamage(amount, position, direction, forceMagnitude, frames, radius, attacker, attackerObject, hitCollider); } /// /// The object has taken been damaged. /// /// The amount of damage taken. /// The position of the damage. /// The direction that the object took damage from. /// The magnitude of the force that is applied to the object. /// The number of frames to add the force to. /// The radius of the explosive damage. If 0 then a non-explosive force will be used. /// The GameObject that did the damage. /// The object that did the damage. /// The Collider that was hit. public virtual void OnDamage(float amount, Vector3 position, Vector3 direction, float forceMagnitude, int frames, float radius, GameObject attacker, object attackerObject, Collider hitCollider) { #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (m_NetworkInfo != null && m_NetworkInfo.IsLocalPlayer()) { m_NetworkHealthMonitor.OnDamage(amount, position, direction, forceMagnitude, frames, radius, attacker, hitCollider); } #endif // Add a multiplier if a particular collider was hit. Do not apply a multiplier if the damage is applied through a radius because multiple // collider are hit. if (radius == 0 && direction != Vector3.zero && hitCollider != null) { Hitbox hitbox; if (m_ColliderHitboxMap != null && m_ColliderHitboxMap.Count > 0) { if (m_ColliderHitboxMap.TryGetValue(hitCollider, out hitbox)) { amount *= hitbox.DamageMultiplier; } else { // The main collider may be overlapping child hitbox colliders. Perform one more raycast to ensure a hitbox collider shouldn't be hit. float distance = 0.2f; if (hitCollider is CapsuleCollider) { distance = (hitCollider as CapsuleCollider).radius; } else if (hitCollider is SphereCollider) { distance = (hitCollider as SphereCollider).radius; } // The hitbox collider may be underneath the base collider. Fire a raycast to detemine if there are any colliders underneath the hit collider // that should apply a multiplier. var hitCount = Physics.RaycastNonAlloc(position, direction, m_RaycastHits, distance, ~(1 << LayerManager.IgnoreRaycast | 1 << LayerManager.Overlay | 1 << LayerManager.VisualEffect), QueryTriggerInteraction.Ignore); for (int i = 0; i < hitCount; ++i) { var closestRaycastHit = QuickSelect.SmallestK(m_RaycastHits, hitCount, i, m_RaycastHitComparer); if (closestRaycastHit.collider == hitCollider) { continue; } // A new collider has been found - stop iterating if the hitbox map exists and use the hitbox multiplier. if (m_ColliderHitboxMap.TryGetValue(closestRaycastHit.collider, out hitbox)) { amount *= hitbox.DamageMultiplier; hitCollider = hitbox.Collider; break; } } } } } // Apply the damage to the shield first because the shield can regenrate. if (m_ShieldAttribute != null && m_ShieldAttribute.Value > m_ShieldAttribute.MinValue) { var shieldAmount = Mathf.Min(amount, m_ShieldAttribute.Value - m_ShieldAttribute.MinValue); amount -= shieldAmount; m_ShieldAttribute.Value -= shieldAmount; } // Decrement the health by remaining amount after the shield has taken damage. if (m_HealthAttribute != null && m_HealthAttribute.Value > m_HealthAttribute.MinValue) { m_HealthAttribute.Value -= Mathf.Min(amount, m_HealthAttribute.Value - m_HealthAttribute.MinValue); } var force = direction * forceMagnitude; if (forceMagnitude > 0) { // Apply a force to the object. if (m_ForceObject != null) { m_ForceObject.AddForce(force, frames); } else { // Apply a force to the rigidbody if the object isn't a character. if (m_Rigidbody != null && !m_Rigidbody.isKinematic) { if (radius == 0) { m_Rigidbody.AddForceAtPosition(force * MathUtility.RigidbodyForceMultiplier, position); } else { m_Rigidbody.AddExplosionForce(force.magnitude * MathUtility.RigidbodyForceMultiplier, position, radius); } } } } // Let other interested objects know that the object took damage. EventHandler.ExecuteEvent(m_GameObject, "OnHealthDamage", amount, position, force, attacker, hitCollider); if (m_OnDamageEvent != null) { m_OnDamageEvent.Invoke(amount, position, force, attacker); } // The object is dead when there is no more health or shield. if (!IsAlive()) { #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (m_NetworkInfo == null || m_NetworkInfo.IsLocalPlayer()) { #endif Die(position, force, attacker); #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER } #endif } else { // Play any take damage audio if the object did not die. If the object died then the death audio will play. m_TakeDamageAudioClipSet.PlayAudioClip(m_GameObject); } } /// /// Is the object currently alive? /// /// True if the object is currently alive. public bool IsAlive() { return (m_HealthAttribute != null && m_HealthAttribute.Value > m_HealthAttribute.MinValue) || (m_ShieldAttribute != null && m_ShieldAttribute.Value > m_ShieldAttribute.MinValue); } /// /// The object is no longer alive. /// /// The position of the damage. /// The amount of force applied to the object while taking the damage. /// The GameObject that killed the character. public virtual void Die(Vector3 position, Vector3 force, GameObject attacker) { #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (m_NetworkInfo != null && m_NetworkInfo.IsLocalPlayer()) { m_NetworkHealthMonitor.Die(position, force, attacker); } #endif // Spawn any objects on death, such as an explosion if the object is an explosive barrel. if (m_SpawnedObjectsOnDeath != null) { for (int i = 0; i < m_SpawnedObjectsOnDeath.Length; ++i) { var spawnedObject = ObjectPool.Instantiate(m_SpawnedObjectsOnDeath[i], transform.position, transform.rotation); Explosion explosion; if ((explosion = spawnedObject.GetCachedComponent()) != null) { explosion.Explode(gameObject); } var rigidbodies = spawnedObject.GetComponentsInChildren(); for (int j = 0; j < rigidbodies.Length; ++j) { rigidbodies[j].AddForceAtPosition(force, position); } } } // Destroy any objects on death. The objects will be placed back in the object pool if they were created within it otherwise the object will be destroyed. if (m_DestroyedObjectsOnDeath != null) { for (int i = 0; i < m_DestroyedObjectsOnDeath.Length; ++i) { if (ObjectPool.InstantiatedWithPool(m_DestroyedObjectsOnDeath[i])) { ObjectPool.Destroy(m_DestroyedObjectsOnDeath[i]); } else { Object.Destroy(m_DestroyedObjectsOnDeath[i]); } } } // Change the layer to a death layer. if (m_DeathLayer.value != 0) { m_AliveLayer = m_GameObject.layer; m_GameObject.layer = m_DeathLayer; } // Play any take death audio. Use PlayAtPosition because the audio won't play if the GameObject is inactive. m_DeathAudioClipSet.PlayAtPosition(m_Transform.position); // Deactivate the object if requested. if (m_DeactivateOnDeath) { Scheduler.Schedule(m_DeactivateOnDeathDelay, Deactivate); } // The attributes shouldn't regenerate. if (m_ShieldAttribute != null) { m_ShieldAttribute.CancelAutoUpdate(); } if (m_HealthAttribute != null) { m_HealthAttribute.CancelAutoUpdate(); } // Notify those interested. EventHandler.ExecuteEvent(m_GameObject, "OnDeath", position, force, attacker); if (m_OnDeathEvent != null) { m_OnDeathEvent.Invoke(position, force, attacker); } } /// /// Kills the object immediately. /// public void ImmediateDeath() { ImmediateDeath(m_Transform.position, Vector3.zero, 0); } /// /// Kills the object immediately. /// /// The position the character died. /// The direction that the object took damage from. /// The magnitude of the force that is applied to the object. public void ImmediateDeath(Vector3 position, Vector3 direction, float forceMagnitude) { var amount = 0f; if (m_HealthAttribute != null) { amount += m_HealthAttribute.Value; } if (m_ShieldAttribute != null) { amount += m_ShieldAttribute.Value; } // If ImmediateDeath is called then the object should die even if it is invincible. var invincible = m_Invincible; m_Invincible = false; Damage(amount, position, direction, forceMagnitude); m_Invincible = invincible; } /// /// Adds amount to health and then to the shield if there is still an amount remaining. Will not go over the maximum health or shield value. /// /// The amount of health or shield to add. /// True if the object was healed. public virtual bool Heal(float amount) { #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (m_NetworkInfo != null && m_NetworkInfo.IsLocalPlayer()) { m_NetworkHealthMonitor.Heal(amount); } #endif var healAmount = 0f; // Contribute the amount of the health first. if (m_HealthAttribute != null && m_HealthAttribute.Value < m_HealthAttribute.MaxValue) { var healthAmount = Mathf.Min(amount, m_HealthAttribute.MaxValue - m_HealthAttribute.Value); amount -= healthAmount; m_HealthAttribute.Value += healthAmount; healAmount += healthAmount; } // Add any remaining amount to the shield. if (m_ShieldAttribute != null && amount > 0 && m_ShieldAttribute.Value < m_ShieldAttribute.MaxValue) { var shieldAmount = Mathf.Min(amount, m_ShieldAttribute.MaxValue - m_ShieldAttribute.Value); m_ShieldAttribute.Value += shieldAmount; healAmount += shieldAmount; } // Don't play any effects if the object wasn't healed. if (healAmount == 0) { return false; } // Play any heal audio. m_HealAudioClipSet.PlayAudioClip(m_GameObject); EventHandler.ExecuteEvent(m_GameObject, "OnHealthHeal", healAmount); if (m_OnHealEvent != null) { m_OnHealEvent.Invoke(healAmount); } return true; } /// /// The object doesn't have any health or shield left and should be deactivated. /// private void Deactivate() { m_GameObject.SetActive(false); } /// /// The object has spawned again. Set the health and shield back to their starting values. /// protected virtual void OnRespawn() { if (m_HealthAttribute != null) { m_HealthAttribute.ResetValue(); } if (m_ShieldAttribute != null) { m_ShieldAttribute.ResetValue(); } // Change the layer back to the alive layer. if (m_DeathLayer.value != 0) { m_GameObject.layer = m_AliveLayer; } m_SpawnTime = Time.time; } /// /// The GameObject has been destroyed. /// private void OnDestroy() { EventHandler.UnregisterEvent(m_GameObject, "OnRespawn", OnRespawn); } } }