/// --------------------------------------------- /// Ultimate Character Controller /// Copyright (c) Opsive. All Rights Reserved. /// https://www.opsive.com /// --------------------------------------------- namespace Opsive.UltimateCharacterController.Objects { using Opsive.Shared.Game; using Opsive.UltimateCharacterController.Audio; using Opsive.UltimateCharacterController.Game; using Opsive.UltimateCharacterController.Events; using Opsive.UltimateCharacterController.Objects.ItemAssist; using Opsive.UltimateCharacterController.Traits; using Opsive.UltimateCharacterController.Utility; using System.Collections.Generic; using UnityEngine; using EventHandler = Opsive.Shared.Events.EventHandler; /// /// Creates an explosion which applies a force and damage to any object that is within the specified radius. /// public class Explosion : MonoBehaviour { [Tooltip("Should the object explode when the object is enabled?")] [SerializeField] protected bool m_ExplodeOnEnable; [Tooltip("Determines how far out the explosion affects other objects.")] [SerializeField] protected float m_Radius = 5; [Tooltip("The maximum amount of damage the explosion applies to objects with the Health component.")] [SerializeField] protected float m_DamageAmount = 10; [Tooltip("The maximum amount of force the explosion applies to nearby Rigidbody/IForceObject objects.")] [SerializeField] protected float m_ImpactForce = 2; [Tooltip("The number of frames to add the impact force to.")] [SerializeField] protected int m_ImpactForceFrames = 1; [Tooltip("The layers that the explosion can affect.")] [SerializeField] protected LayerMask m_ImpactLayers = ~(1 << LayerManager.IgnoreRaycast | 1 << LayerManager.Water | 1 << LayerManager.SubCharacter | 1 << LayerManager.Overlay | 1 << LayerManager.VisualEffect); [Tooltip("Does the explosion require line of sight in order to damage the hit object?")] [SerializeField] protected bool m_LineOfSight; [Tooltip("The duration of the explosion.")] [SerializeField] protected float m_Lifespan = 3; [Tooltip("The maximum number of objects that the explosions can detect.")] [SerializeField] protected int m_MaxCollisionCount = 100; [Tooltip("A set of AudioClips that can be played when the explosion occurs.")] [SerializeField] protected AudioClipSet m_ExplosionAudioClipSet = new AudioClipSet(); [Tooltip("Unity event invoked when the explosion hits another object.")] [SerializeField] protected UnityFloatVector3Vector3GameObjectEvent m_OnImpactEvent; public bool ExplodeOnEnable { get { return m_ExplodeOnEnable; } set { m_ExplodeOnEnable = value; } } public float Radius { get { return m_Radius; } set { m_Radius = value; } } public float DamageAmount { get { return m_DamageAmount; } set { m_DamageAmount = value; } } public float ImpactForce { get { return m_ImpactForce; } set { m_ImpactForce = value; } } public int ImpactForceFrames { get { return m_ImpactForceFrames; } set { m_ImpactForceFrames = value; } } public LayerMask ImpactLayers { get { return m_ImpactLayers; } set { m_ImpactLayers = value; } } public bool LineOfSight { get { return m_LineOfSight; } set { m_LineOfSight = value; } } public float Lifespan { get { return m_Lifespan; } set { m_Lifespan = value; } } public AudioClipSet ExplosionAudioClipSet { get { return m_ExplosionAudioClipSet; } set { m_ExplosionAudioClipSet = value; } } public UnityFloatVector3Vector3GameObjectEvent OnImpactEvent { get { return m_OnImpactEvent; } set { m_OnImpactEvent = value; } } private GameObject m_GameObject; private Transform m_Transform; private HashSet m_ObjectExplosions = new HashSet(); private Collider[] m_CollidersHit; private RaycastHit m_RaycastHit; /// /// Initialize the default values. /// private void Awake() { m_GameObject = gameObject; m_Transform = transform; m_CollidersHit = new Collider[m_MaxCollisionCount]; AudioManager.Register(m_GameObject); } /// /// Explode if requested when the component is enabled. /// private void OnEnable() { if (m_ExplodeOnEnable) { Explode(m_DamageAmount, m_ImpactForce, m_ImpactForceFrames, null); } } /// /// Do the explosion. /// public void Explode() { Explode(m_DamageAmount, m_ImpactForce, m_ImpactForceFrames, null); } /// /// Do the explosion. /// /// The amount of damage to apply to the hit objects. public void Explode(GameObject originator) { Explode(m_DamageAmount, m_ImpactForce, m_ImpactForceFrames, originator); } /// /// Do the explosion. /// /// The amount of damage to apply to the hit objects. /// The amount of force to apply to the hit object. /// The number of frames to add the force to. /// The originator of the object. public void Explode(float damageAmount, float impactForce, int impactForceFrames, GameObject originator) { Health health = null; Rigidbody colliderRigidbody = null; IForceObject forceObject = null; var hitCount = Physics.OverlapSphereNonAlloc(m_Transform.position, m_Radius, m_CollidersHit, m_ImpactLayers, QueryTriggerInteraction.Ignore); #if UNITY_EDITOR if (hitCount == m_MaxCollisionCount) { Debug.LogWarning("Warning: The maximum number of colliders have been hit by " + m_GameObject.name + ". Consider increasing the Max Collision Count value."); } #endif for (int i = 0; i < hitCount; ++i) { // A GameObject can contain multiple colliders. Prevent the explosion from occurring on the same GameObject multiple times. if (m_ObjectExplosions.Contains(m_CollidersHit[i].gameObject)) { continue; } m_ObjectExplosions.Add(m_CollidersHit[i].gameObject); // The base character GameObject should only be checked once. if ((forceObject = m_CollidersHit[i].gameObject.GetCachedParentComponent()) != null) { if (m_ObjectExplosions.Contains(forceObject)) { continue; } m_ObjectExplosions.Add(forceObject); } // OverlapSphere can return objects that are in a different room. Perform a cast to ensure the object is within the explosion range. if (m_LineOfSight) { // Add a slight vertical offset to prevent a floor collider from getting in the way of the cast. var position = m_Transform.TransformPoint(0, 0.1f, 0); var direction = m_CollidersHit[i].transform.position - position; if (Physics.Raycast(position - direction.normalized * 0.1f, direction, out m_RaycastHit, direction.magnitude, m_ImpactLayers, QueryTriggerInteraction.Ignore) && !(m_RaycastHit.transform.IsChildOf(m_CollidersHit[i].transform) #if FIRST_PERSON_CONTROLLER // The cast should not hit any colliders who are a child of the camera. || m_RaycastHit.transform.gameObject.GetCachedParentComponent() != null #endif )) { // If the collider is part of a character then ensure the head can't be hit. var parentAnimator = m_CollidersHit[i].transform.gameObject.GetCachedParentComponent(); if (parentAnimator != null && parentAnimator.isHuman) { var head = parentAnimator.GetBoneTransform(HumanBodyBones.Head); direction = head.position - position; if (Physics.Raycast(position, direction, out m_RaycastHit, direction.magnitude, m_ImpactLayers, QueryTriggerInteraction.Ignore) && !m_RaycastHit.transform.IsChildOf(m_CollidersHit[i].transform) && !m_CollidersHit[i].transform.IsChildOf(m_RaycastHit.transform) && m_RaycastHit.transform.IsChildOf(m_Transform) #if FIRST_PERSON_CONTROLLER // The cast should not hit any colliders who are a child of the camera. && m_RaycastHit.transform.gameObject.GetCachedParentComponent() == null #endif ) { continue; } } else { continue; } } } // The shield can absorb some (or none) of the damage from the explosion. var hitDamageAmount = damageAmount; #if ULTIMATE_CHARACTER_CONTROLLER_MELEE ShieldCollider shieldCollider; if ((shieldCollider = m_CollidersHit[i].transform.gameObject.GetCachedComponent()) != null) { hitDamageAmount = shieldCollider.Shield.Damage(this, hitDamageAmount); } #endif // ClosestPoint only works with a subset of collider types. Vector3 closestPoint; if (m_CollidersHit[i] is BoxCollider || m_CollidersHit[i] is SphereCollider || m_CollidersHit[i] is CapsuleCollider || (m_CollidersHit[i] is MeshCollider && (m_CollidersHit[i] as MeshCollider).convex)) { closestPoint = m_CollidersHit[i].ClosestPoint(m_Transform.position); } else { closestPoint = m_CollidersHit[i].ClosestPointOnBounds(m_Transform.position); } var hitDirection = closestPoint - m_Transform.position; // Allow a custom event to be received. EventHandler.ExecuteEvent(m_CollidersHit[i].transform.gameObject, "OnObjectImpact", hitDamageAmount, closestPoint, hitDirection * m_ImpactForce, originator, this, m_CollidersHit[i]); if (m_OnImpactEvent != null) { m_OnImpactEvent.Invoke(hitDamageAmount, closestPoint, hitDirection * m_ImpactForce, originator); } // If the shield didn't absorb all of the damage then it should be applied to the character. if (hitDamageAmount > 0) { // If the Health component exists it will apply an explosive force to the character/character in addition to deducting the health. // Otherwise just apply the force to the character/rigidbody. if ((health = m_CollidersHit[i].gameObject.GetCachedParentComponent()) != null) { // The further out the collider is, the less it is damaged. var damageModifier = Mathf.Max(1 - (hitDirection.magnitude / m_Radius), 0.01f); health.Damage(hitDamageAmount * damageModifier, m_Transform.position, hitDirection.normalized, impactForce * damageModifier, impactForceFrames, m_Radius, originator, this, null); } else if (forceObject != null) { var damageModifier = Mathf.Max(1 - (hitDirection.magnitude / m_Radius), 0.01f); forceObject.AddForce(hitDirection.normalized * impactForce * damageModifier); } else if ((colliderRigidbody = m_CollidersHit[i].gameObject.GetCachedComponent()) != null) { colliderRigidbody.AddExplosionForce(impactForce * MathUtility.RigidbodyForceMultiplier, m_Transform.position, m_Radius); } } } m_ObjectExplosions.Clear(); // An audio clip can play when the object explodes. m_ExplosionAudioClipSet.PlayAudioClip(m_GameObject); Scheduler.Schedule(m_Lifespan, Destroy); } /// /// Place the object back in the ObjectPool. /// private void Destroy() { ObjectPool.Destroy(gameObject); } } }