/// --------------------------------------------- /// Ultimate Character Controller /// Copyright (c) Opsive. All Rights Reserved. /// https://www.opsive.com /// --------------------------------------------- namespace Opsive.UltimateCharacterController.Objects { using Opsive.Shared.Events; using Opsive.Shared.Game; using Opsive.UltimateCharacterController.Character; using Opsive.UltimateCharacterController.Events; using Opsive.UltimateCharacterController.Game; #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER using Opsive.UltimateCharacterController.Networking; using Opsive.UltimateCharacterController.Networking.Game; using Opsive.UltimateCharacterController.Networking.Objects; #endif using Opsive.UltimateCharacterController.Objects.ItemAssist; using Opsive.UltimateCharacterController.StateSystem; using Opsive.UltimateCharacterController.SurfaceSystem; using Opsive.UltimateCharacterController.Traits; using Opsive.UltimateCharacterController.Utility; using UnityEngine; /// /// The Destructible class is an abstract class which acts as the base class for any object that destroys itself and applies a damange. /// Primary uses include projectiles and grenades. /// [RequireComponent(typeof(Rigidbody))] public abstract class Destructible : TrajectoryObject { [Tooltip("The layers that the object can stick to.")] [SerializeField] protected LayerMask m_StickyLayers = ~((1 << LayerManager.IgnoreRaycast) | (1 << LayerManager.Water) | (1 << LayerManager.UI) | (1 << LayerManager.VisualEffect) | (1 << LayerManager.Overlay) | (1 << LayerManager.Character) | (1 << LayerManager.SubCharacter)); [Tooltip("Should the projectile be destroyed when it collides with another object?")] [SerializeField] protected bool m_DestroyOnCollision = true; [Tooltip("The amount of time after a collision that the object should be destroyed.")] [SerializeField] protected float m_DestructionDelay; [Tooltip("The objects which should spawn when the object is destroyed.")] [SerializeField] protected ObjectSpawnInfo[] m_SpawnedObjectsOnDestruction; [Tooltip("Unity event invoked when the destructable hits another object.")] [SerializeField] protected UnityFloatVector3Vector3GameObjectEvent m_OnImpactEvent; public LayerMask StickyLayers { get { return m_StickyLayers; } set { m_StickyLayers = value; } } public bool DestroyOnCollision { get { return m_DestroyOnCollision; } set { m_DestroyOnCollision = value; } } public float DestructionDelay { get { return m_DestructionDelay; } set { m_DestructionDelay = value; } } public ObjectSpawnInfo[] SpawnedObjectsOnDestruction { get { return m_SpawnedObjectsOnDestruction; } set { m_SpawnedObjectsOnDestruction = value; } } public UnityFloatVector3Vector3GameObjectEvent OnImpactEvent { get { return m_OnImpactEvent; } set { m_OnImpactEvent = value; } } protected float m_DamageAmount; protected float m_ImpactForce; protected int m_ImpactForceFrames; protected string m_ImpactStateName; protected float m_ImpactStateDisableTimer; private TrailRenderer m_TrailRenderer; private ParticleSystem m_ParticleSystem; private bool m_Destroyed; private UltimateCharacterLocomotion m_StickyCharacterLocomotion; #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER private INetworkInfo m_NetworkInfo; private IDestructibleMonitor m_DestructibleMonitor; #endif /// /// Initialize the defualt values. /// protected override void Awake() { base.Awake(); m_TrailRenderer = GetComponent(); if (m_TrailRenderer != null) { m_TrailRenderer.enabled = false; } m_ParticleSystem = GetComponent(); if (m_ParticleSystem != null) { m_ParticleSystem.Stop(); } // The Rigidbody is only used to notify Unity that the object isn't static. The Rigidbody doesn't control any movement. var rigidbody = GetComponent(); rigidbody.mass = m_Mass; rigidbody.isKinematic = true; rigidbody.constraints = RigidbodyConstraints.FreezeAll; #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER m_NetworkInfo = GetComponent(); m_DestructibleMonitor = GetComponent(); #endif if (m_DestroyOnCollision && m_CollisionMode != CollisionMode.Collide) { Debug.LogWarning($"Warning: The Destructible {name} will be destroyed on collision but does not have a Collision Mode set to Collide."); m_CollisionMode = CollisionMode.Collide; } } /// /// Initializes the object. This will be called from an object creating the projectile (such as a weapon). /// /// The velocity to apply. /// The torque to apply. /// The amount of damage to apply to the hit object. /// The amount of force to apply to the hit object. /// The number of frames to add the force to. /// The layers that the projectile can impact with. /// The name of the state to activate upon impact. /// The number of seconds until the impact state is disabled. /// A reference to the Surface Impact triggered when the object hits an object. /// The object that instantiated the trajectory object. public virtual void Initialize(Vector3 velocity, Vector3 torque, float damageAmount, float impactForce, int impactForceFrames, LayerMask impactLayers, string impactStateName, float impactStateDisableTimer, SurfaceImpact surfaceImpact, GameObject originator) { InitializeDestructibleProperties(damageAmount, impactForce, impactForceFrames, impactLayers, impactStateName, impactStateDisableTimer, surfaceImpact); base.Initialize(velocity, torque, originator); } /// /// Initializes the destructible properties. /// /// The amount of damage to apply to the hit object. /// The amount of force to apply to the hit object. /// The number of frames to add the force to. /// The layers that the projectile can impact with. /// The name of the state to activate upon impact. /// The number of seconds until the impact state is disabled. /// A reference to the Surface Impact triggered when the object hits an object. public void InitializeDestructibleProperties(float damageAmount, float impactForce, int impactForceFrames, LayerMask impactLayers, string impactStateName, float impactStateDisableTimer, SurfaceImpact surfaceImpact) { m_Destroyed = false; m_DamageAmount = damageAmount; m_ImpactForce = impactForce; m_ImpactForceFrames = impactForceFrames; m_ImpactLayers = impactLayers; m_ImpactStateName = impactStateName; m_ImpactStateDisableTimer = impactStateDisableTimer; // The SurfaceImpact may be set directly on the destructible prefab. if (m_SurfaceImpact == null) { m_SurfaceImpact = surfaceImpact; } if (m_TrailRenderer != null) { m_TrailRenderer.Clear(); m_TrailRenderer.enabled = true; } if (m_ParticleSystem != null) { m_ParticleSystem.Play(); } if (m_Collider != null) { m_Collider.enabled = false; } // The object may be reused and was previously stuck to a character. if (m_StickyCharacterLocomotion != null) { m_StickyCharacterLocomotion.RemoveIgnoredCollider(m_Collider); m_StickyCharacterLocomotion = null; } } /// /// The object has collided with another object. /// /// The RaycastHit of the object. Can be null. protected override void OnCollision(RaycastHit? hit) { base.OnCollision(hit); var forceDestruct = false; if (m_CollisionMode == CollisionMode.Collide) { // When there is a collision the object should move to the position that was hit so if it's not destroyed then it looks like it // is penetrating the hit object. if (hit != null && hit.HasValue && m_Collider != null) { var closestPoint = m_Collider.ClosestPoint(hit.Value.point); m_Transform.position += (hit.Value.point - closestPoint); // Only set the parent to the hit transform on uniform objects to prevent stretching. if (MathUtility.IsUniform(hit.Value.transform.localScale)) { // The parent layer must be within the sticky layer mask. if (MathUtility.InLayerMask(hit.Value.transform.gameObject.layer, m_StickyLayers)) { m_Transform.parent = hit.Value.transform; // If the destructible sticks to a character then the object should be added as a sub collider so collisions will be ignored. m_StickyCharacterLocomotion = hit.Value.transform.gameObject.GetCachedComponent(); if (m_StickyCharacterLocomotion != null) { m_StickyCharacterLocomotion.AddIgnoredCollider(m_Collider); } } else { forceDestruct = true; } } } } if (m_TrailRenderer != null) { m_TrailRenderer.enabled = false; } if (m_ParticleSystem != null) { Scheduler.ScheduleFixed(Time.fixedDeltaTime - 0.01f, StopParticleSystem); } // The object may not have been initialized before it collides. if (m_GameObject == null) { InitializeComponentReferences(); } if (hit != null && hit.HasValue) { var hitValue = hit.Value; var hitGameObject = hitValue.collider.gameObject; // The shield can absorb some (or none) of the damage from the destructible. var damageAmount = m_DamageAmount; #if ULTIMATE_CHARACTER_CONTROLLER_MELEE ShieldCollider shieldCollider; if ((shieldCollider = hitGameObject.GetCachedComponent()) != null) { damageAmount = shieldCollider.Shield.Damage(this, damageAmount); } #endif // Allow a custom event to be received. EventHandler.ExecuteEvent(hitGameObject, "OnObjectImpact", damageAmount, hitValue.point, m_Velocity.normalized * m_ImpactForce, m_Originator, this, hitValue.collider); if (m_OnImpactEvent != null) { m_OnImpactEvent.Invoke(damageAmount, hitValue.point, m_Velocity.normalized * m_ImpactForce, m_Originator); } // If the shield didn't absorb all of the damage then it should be applied to the character. if (damageAmount > 0) { // If the Health component exists it will apply a force to the rigidbody in addition to deducting the health. Otherwise just apply the force to the rigidbody. Health hitHealth; if ((hitHealth = hitGameObject.GetCachedParentComponent()) != null) { hitHealth.Damage(damageAmount, hitValue.point, -hitValue.normal, m_ImpactForce, m_ImpactForceFrames, 0, m_Originator, this, hitValue.collider); } else if (m_ImpactForce > 0) { var collisionRigidbody = hitGameObject.GetCachedParentComponent(); if (collisionRigidbody != null && !collisionRigidbody.isKinematic) { collisionRigidbody.AddForceAtPosition(-hitValue.normal * m_ImpactForce * MathUtility.RigidbodyForceMultiplier, hitValue.point); } else { var forceObject = hitGameObject.GetCachedParentComponent(); if (forceObject != null) { forceObject.AddForce(m_Transform.forward * m_ImpactForce); } } } } // An optional state can be activated on the hit object. if (!string.IsNullOrEmpty(m_ImpactStateName)) { StateManager.SetState(hitGameObject, m_ImpactStateName, true); // If the timer isn't -1 then the state should be disabled after a specified amount of time. If it is -1 then the state // will have to be disabled manually. if (m_ImpactStateDisableTimer != -1) { StateManager.DeactivateStateTimer(hitGameObject, m_ImpactStateName, m_ImpactStateDisableTimer); } } } // The object can destroy itself after a small delay. if (m_DestroyOnCollision || forceDestruct) { Scheduler.ScheduleFixed(m_DestructionDelay, Destruct, hit); } } /// /// Destroys the object. /// /// The RaycastHit of the object. Can be null. protected void Destruct(RaycastHit? hit) { if (m_Destroyed) { return; } #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER // The object can only explode on the server. if (m_NetworkInfo != null && !m_NetworkInfo.IsServer()) { return; } #endif // The RaycastHit will be null if the destruction happens with no collision. var hitPosition = (hit != null && hit.HasValue) ? hit.Value.point : m_Transform.position; var hitNormal = (hit != null && hit.HasValue) ? hit.Value.normal : m_Transform.up; #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (m_NetworkInfo != null && m_NetworkInfo.IsServer()) { m_DestructibleMonitor.Destruct(hitPosition, hitNormal); } #endif Destruct(hitPosition, hitNormal); } /// /// Destroys the object. /// /// The position of the destruction. /// The normal direction of the destruction public void Destruct(Vector3 hitPosition, Vector3 hitNormal) { for (int i = 0; i < m_SpawnedObjectsOnDestruction.Length; ++i) { if (m_SpawnedObjectsOnDestruction[i] == null) { continue; } var spawnedObject = m_SpawnedObjectsOnDestruction[i].Instantiate(hitPosition, hitNormal, m_NormalizedGravity); if (spawnedObject == null) { continue; } var explosion = spawnedObject.GetCachedComponent(); if (explosion != null) { explosion.Explode(m_DamageAmount, m_ImpactForce, m_ImpactForceFrames, m_Originator); } } // The component and collider no longer need to be enabled after the object has been destroyed. if (m_Collider != null) { m_Collider.enabled = false; } m_Destroyed = true; // The destructible should be destroyed. #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER if (NetworkObjectPool.IsNetworkActive()) { // The object may have already been destroyed over the network. if (!m_GameObject.activeSelf) { return; } NetworkObjectPool.Destroy(m_GameObject); return; } #endif ObjectPool.Destroy(m_GameObject); } /// /// Stops the particle system. /// private void StopParticleSystem() { m_ParticleSystem.Stop(); } /// /// The component has been disabled. /// protected override void OnDisable() { base.OnDisable(); if (m_DestroyOnCollision && m_StickyCharacterLocomotion != null) { m_StickyCharacterLocomotion.RemoveIgnoredCollider(m_Collider); m_StickyCharacterLocomotion = null; } } } }