483 lines
22 KiB
C#
483 lines
22 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.Serialization;
|
|
// ReSharper disable ArrangeAccessorOwnerBody
|
|
// ReSharper disable UnusedMember.Global
|
|
// ReSharper disable ForCanBeConvertedToForeach
|
|
|
|
namespace DestroyIt
|
|
{
|
|
/// <summary>Put this script on an object you want to be destructible.</summary>
|
|
[DisallowMultipleComponent]
|
|
public class Destructible : MonoBehaviour
|
|
{
|
|
public float TotalHitPoints
|
|
{
|
|
get { return _totalHitPoints; }
|
|
set
|
|
{
|
|
_totalHitPoints = value;
|
|
damageLevels.CalculateDamageLevels(_totalHitPoints);
|
|
}
|
|
}
|
|
|
|
public float CurrentHitPoints
|
|
{
|
|
get { return _currentHitPoints; }
|
|
set { _currentHitPoints = value; }
|
|
}
|
|
|
|
[SerializeField]
|
|
[FormerlySerializedAs("totalHitPoints")]
|
|
[HideInInspector]
|
|
private float _totalHitPoints = 50f;
|
|
|
|
[SerializeField]
|
|
[FormerlySerializedAs("currentHitPoints")]
|
|
[HideInInspector]
|
|
private float _currentHitPoints = 50f;
|
|
|
|
[HideInInspector] public List<DamageLevel> damageLevels;
|
|
[HideInInspector] public GameObject destroyedPrefab;
|
|
[HideInInspector] public GameObject destroyedPrefabParent;
|
|
[HideInInspector] public ParticleSystem fallbackParticle;
|
|
[HideInInspector] public int fallbackParticleMatOption;
|
|
[HideInInspector] public List<DamageEffect> damageEffects;
|
|
[HideInInspector] public float velocityReduction = .5f;
|
|
[HideInInspector] public bool limitDamage = false;
|
|
[HideInInspector] public float minDamage = 10f; // The minimum amount of damage the object can receive per hit.
|
|
[HideInInspector] public float maxDamage = 100f; // The maximum amount of damage the object can receive per hit.
|
|
[HideInInspector] public float minDamageTime = 0f; // The minimum amount of time (in seconds) that must pass before the object can be damaged again.
|
|
[HideInInspector] public float ignoreCollisionsUnder = 2f;
|
|
[HideInInspector] public List<GameObject> unparentOnDestroy;
|
|
[HideInInspector] public bool disableKinematicOnUparentedChildren = true;
|
|
[HideInInspector] public List<MaterialMapping> replaceMaterials;
|
|
[HideInInspector] public List<MaterialMapping> replaceParticleMats;
|
|
[HideInInspector] public bool canBeDestroyed = true;
|
|
[HideInInspector] public bool canBeRepaired = true;
|
|
[HideInInspector] public List<string> debrisToReParentByName;
|
|
[HideInInspector] public bool debrisToReParentIsKinematic;
|
|
[HideInInspector] public List<string> childrenToReParentByName;
|
|
[HideInInspector] public bool isDebrisChipAway;
|
|
[HideInInspector] public float chipAwayDebrisMass = 1f;
|
|
[HideInInspector] public float chipAwayDebrisDrag;
|
|
[HideInInspector] public float chipAwayDebrisAngularDrag = 0.05f;
|
|
[HideInInspector] public bool autoPoolDestroyedPrefab = true;
|
|
[HideInInspector] public bool useFallbackParticle = true;
|
|
[HideInInspector] public Vector3 centerPointOverride;
|
|
[HideInInspector] public Vector3 fallbackParticleScale = Vector3.one;
|
|
[HideInInspector] public Transform fallbackParticleParent;
|
|
[HideInInspector] public bool sinkWhenDestroyed;
|
|
[HideInInspector] public bool shouldDeactivate; // If true, this script will deactivate after a set period of time (configurable on DestructionManager).
|
|
[HideInInspector] public bool isTerrainTree; // Is this Destructible object a stand-in for a terrain tree?
|
|
[HideInInspector] public AudioClip destroyedSound;
|
|
[HideInInspector] public AudioClip damagedSound;
|
|
[HideInInspector] public AudioClip repairedSound;
|
|
|
|
// Private variables
|
|
private const float InvulnerableTimer = 0.5f; // How long (in seconds) the destructible object is invulnerable after instantiation.
|
|
private DamageLevel _currentDamageLevel;
|
|
private bool _isInitialized;
|
|
private float _deactivateTimer;
|
|
private bool _firstFixedUpdate = true;
|
|
private Rigidbody _rigidBody; // store a reference to this destructible object's rigidbody, so we don't have to use GetComponent() at runtime.
|
|
private bool _isInvulnerable; // Determines whether the destructible object starts with a short period of invulnerability. Prevents destructible debris being immediately destroyed by the same forces that destroyed the original object.
|
|
|
|
// Properties
|
|
public bool UseProgressiveDamage { get; set; } = true; // Used to determine if the shader on the destructible object is
|
|
public bool CheckForClingingDebris { get; set; } = true; // This is an added optimization used when we are auto-pooling destroyed prefabs. It allows us to avoid a GetComponentsInChildren() check for ClingPoints destruction time.
|
|
public Rigidbody[] PooledRigidbodies { get; set; } // This is an added optimization used when we are auto-pooling destroyed prefabs. It allows us to avoid multiple GetComponentsInChildren() checks for Rigidbodies at destruction time.
|
|
public GameObject[] PooledRigidbodyGos { get; set; } // This is an added optimization used when we are auto-pooling destroyed prefabs. It allows us to avoid multiple GetComponentsInChildren() checks for the GameObjects on Rigidibodies at destruction time.
|
|
public float VelocityReduction => Mathf.Abs(velocityReduction - 1f) /* invert the velocity reduction value (so it makes sense in the UI) */;
|
|
public Quaternion RotationFixedUpdate { get; private set; }
|
|
public Vector3 PositionFixedUpdate { get; private set; }
|
|
public Vector3 VelocityFixedUpdate { get; private set; }
|
|
public Vector3 AngularVelocityFixedUpdate { get; private set; }
|
|
public float LastRepairedAmount { get; private set; }
|
|
public float LastDamagedAmount { get; private set; }
|
|
public float LastDamagedTime { get; private set; }
|
|
public bool IsDestroyed => !_isInvulnerable && canBeDestroyed && CurrentHitPoints <= 0;
|
|
public Vector3 MeshCenterPoint { get; private set; }
|
|
|
|
// Events
|
|
public event Action DamagedEvent;
|
|
public event Action DestroyedEvent;
|
|
public event Action RepairedEvent;
|
|
|
|
public void Start()
|
|
{
|
|
CheckForClingingDebris = true;
|
|
|
|
if (damageLevels == null || damageLevels.Count == 0)
|
|
damageLevels = DestructibleHelper.DefaultDamageLevels();
|
|
damageLevels.CalculateDamageLevels(TotalHitPoints);
|
|
|
|
// Store a reference to this object's rigidbody, for better performance.
|
|
_rigidBody = GetComponent<Rigidbody>();
|
|
|
|
// Only calculate the mesh center point if the destructible object uses a fallback particle.
|
|
if (useFallbackParticle)
|
|
{
|
|
MeshRenderer[] meshRenderers = gameObject.GetComponentsInChildren<MeshRenderer>();
|
|
MeshCenterPoint = gameObject.GetMeshCenterPoint(meshRenderers);
|
|
if (gameObject.IsAnyMeshPartOfStaticBatch(meshRenderers) && centerPointOverride == Vector3.zero)
|
|
Debug.Log($"[{gameObject.name}] is a Destructible object with one or more static meshes, but no position override for the fallback particle effect. Particle effect may not spawn where you expect.");
|
|
}
|
|
|
|
PlayDamageEffects();
|
|
_isInvulnerable = true;
|
|
Invoke(nameof(RemoveInvulnerability), InvulnerableTimer);
|
|
|
|
if (gameObject.HasTagInParent(Tag.TerrainTree))
|
|
isTerrainTree = true;
|
|
|
|
// If AutoPool is turned on, add the destroyed prefab to the ObjectPool.
|
|
if (autoPoolDestroyedPrefab)
|
|
ObjectPool.Instance.AddDestructibleObjectToPool(this);
|
|
|
|
_isInitialized = true;
|
|
}
|
|
|
|
public void RemoveInvulnerability()
|
|
{
|
|
_isInvulnerable = false;
|
|
}
|
|
|
|
public void FixedUpdate()
|
|
{
|
|
if (!_isInitialized) return;
|
|
DestructionManager destructionManager = DestructionManager.Instance;
|
|
if (destructionManager == null) return;
|
|
|
|
// Use the fixed update position/rotation for placement of the destroyed prefab.
|
|
PositionFixedUpdate = transform.position;
|
|
RotationFixedUpdate = transform.rotation;
|
|
if (_rigidBody != null)
|
|
{
|
|
VelocityFixedUpdate = _rigidBody.velocity;
|
|
AngularVelocityFixedUpdate = _rigidBody.angularVelocity;
|
|
}
|
|
|
|
SetDamageLevel();
|
|
PlayDamageEffects();
|
|
|
|
// Check if this script should be auto-deactivated, as configured on the DestructionManager
|
|
if (destructionManager.autoDeactivateDestructibles && !isTerrainTree && shouldDeactivate)
|
|
UpdateDeactivation(destructionManager.deactivateAfter);
|
|
else if (destructionManager.autoDeactivateDestructibleTerrainObjects && isTerrainTree && shouldDeactivate)
|
|
UpdateDeactivation(destructionManager.deactivateAfter);
|
|
|
|
if (IsDestroyed)
|
|
destructionManager.ProcessDestruction(this, destroyedPrefab, new ExplosiveDamage());
|
|
|
|
// If this is the first fixed update frame and autoDeativateDestructibles is true, start this component deactivated.
|
|
if (_firstFixedUpdate)
|
|
this.SetActiveOrInactive(destructionManager);
|
|
|
|
_firstFixedUpdate = false;
|
|
}
|
|
|
|
private void UpdateDeactivation(float deactivateAfter)
|
|
{
|
|
if (_deactivateTimer > deactivateAfter)
|
|
{
|
|
_deactivateTimer = 0f;
|
|
shouldDeactivate = false;
|
|
enabled = false;
|
|
}
|
|
else
|
|
_deactivateTimer += Time.fixedDeltaTime;
|
|
}
|
|
|
|
/// <summary>Applies a generic amount of damage, with no specific impact or explosive force.</summary>
|
|
public void ApplyDamage(float amount)
|
|
{
|
|
if (IsDestroyed || _isInvulnerable || !DestructionManager.Instance.allowDamage) return; // don't try to apply damage to an already-destroyed or invulnerable object, or if damaging object is not allowed.
|
|
|
|
// Adjust the damage based on Min/Max/Time thresholds.
|
|
if (limitDamage)
|
|
{
|
|
if (LastDamagedTime > 0f && minDamageTime > 0f && Time.time < LastDamagedTime + minDamageTime)
|
|
return;
|
|
|
|
if (maxDamage >= 0 && amount > maxDamage)
|
|
amount = maxDamage;
|
|
|
|
if (minDamage >= 0 && minDamage <= maxDamage && amount < minDamage)
|
|
amount = minDamage;
|
|
|
|
if (amount <= 0) return;
|
|
}
|
|
|
|
LastDamagedAmount = amount;
|
|
LastDamagedTime = Time.time;
|
|
FireDamagedEvent();
|
|
|
|
// Check for any audio clip we may need to play
|
|
if (damagedSound != null)
|
|
AudioSource.PlayClipAtPoint(damagedSound, transform.position);
|
|
|
|
CurrentHitPoints -= amount;
|
|
if (CurrentHitPoints > 0) return;
|
|
if (CurrentHitPoints < 0) CurrentHitPoints = 0;
|
|
|
|
PlayDamageEffects();
|
|
|
|
if (IsDestroyed)
|
|
DestructionManager.Instance.ProcessDestruction(this, destroyedPrefab, new DirectDamage{DamageAmount = amount});
|
|
}
|
|
|
|
public void ApplyDamage(Damage damage)
|
|
{
|
|
if (IsDestroyed || _isInvulnerable || !DestructionManager.Instance.allowDamage) return; // don't try to apply damage to an already-destroyed or invulnerable object, or if damaging object is not allowed.
|
|
|
|
// Adjust the damage based on Min/Max/Time thresholds.
|
|
if (limitDamage)
|
|
{
|
|
if (LastDamagedTime > 0f && minDamageTime > 0f && Time.time < LastDamagedTime + minDamageTime)
|
|
return;
|
|
|
|
if (maxDamage >= 0 && damage.DamageAmount > maxDamage)
|
|
damage.DamageAmount = maxDamage;
|
|
|
|
if (minDamage >= 0 && minDamage <= maxDamage && damage.DamageAmount < minDamage)
|
|
damage.DamageAmount = minDamage;
|
|
|
|
if (damage.DamageAmount <= 0) return;
|
|
}
|
|
|
|
LastDamagedAmount = damage.DamageAmount;
|
|
LastDamagedTime = Time.time;
|
|
FireDamagedEvent();
|
|
|
|
// Check for any audio clip we may need to play
|
|
if (damagedSound != null)
|
|
AudioSource.PlayClipAtPoint(damagedSound, transform.position);
|
|
|
|
CurrentHitPoints -= damage.DamageAmount;
|
|
if (CurrentHitPoints > 0) return;
|
|
if (CurrentHitPoints < 0) CurrentHitPoints = 0;
|
|
|
|
PlayDamageEffects();
|
|
|
|
if (IsDestroyed)
|
|
DestructionManager.Instance.ProcessDestruction(this, destroyedPrefab, damage);
|
|
}
|
|
|
|
public void RepairDamage(float amount)
|
|
{
|
|
if (IsDestroyed || !canBeRepaired) return; // object cannot be repaired if it is either already destroyed or not repairable.
|
|
|
|
LastRepairedAmount = amount;
|
|
|
|
CurrentHitPoints += amount;
|
|
if (CurrentHitPoints > TotalHitPoints) // object cannot be over-repaired beyond its total hit points.
|
|
CurrentHitPoints = TotalHitPoints;
|
|
|
|
PlayDamageEffects();
|
|
FireRepairedEvent();
|
|
|
|
// Check for any audio clip we may need to play
|
|
if (repairedSound != null)
|
|
AudioSource.PlayClipAtPoint(repairedSound, transform.position);
|
|
}
|
|
|
|
public void Destroy()
|
|
{
|
|
if (IsDestroyed || _isInvulnerable) return; // don't try to destroy an already-destroyed or invulnerable object.
|
|
|
|
LastDamagedAmount = CurrentHitPoints;
|
|
LastDamagedTime = Time.time;
|
|
FireDamagedEvent();
|
|
|
|
CurrentHitPoints = 0;
|
|
PlayDamageEffects();
|
|
|
|
DestructionManager.Instance.ProcessDestruction(this, destroyedPrefab, CurrentHitPoints);
|
|
}
|
|
|
|
/// <summary>Advances the damage state, applies damage-level materials as needed, and plays particle effects.</summary>
|
|
private void SetDamageLevel()
|
|
{
|
|
DamageLevel damageLevel = damageLevels?.GetDamageLevel(CurrentHitPoints);
|
|
if (damageLevel == null) return;
|
|
if (_currentDamageLevel != null && damageLevel == _currentDamageLevel) return;
|
|
|
|
_currentDamageLevel = damageLevel;
|
|
Renderer[] renderers = GetComponentsInChildren<Renderer>();
|
|
|
|
foreach (Renderer rend in renderers)
|
|
{
|
|
Destructible parentDestructible = rend.GetComponentInParent<Destructible>(); // child Destructible objects should not be affected by damage on their parents.
|
|
if (parentDestructible != this) continue;
|
|
bool isAcceptableRenderer = rend is MeshRenderer || rend is SkinnedMeshRenderer;
|
|
if (isAcceptableRenderer && !rend.gameObject.HasTag(Tag.ClingingDebris) && rend.gameObject.layer != DestructionManager.Instance.debrisLayer)
|
|
{
|
|
for (int j = 0; j < rend.sharedMaterials.Length; j++)
|
|
DestructionManager.Instance.SetProgressiveDamageTexture(rend, rend.sharedMaterials[j], _currentDamageLevel);
|
|
}
|
|
}
|
|
|
|
PlayDamageEffects();
|
|
}
|
|
|
|
/// <summary>Gets the material to use for the fallback particle effect when this Destructible object is destroyed.</summary>
|
|
public Material GetDestroyedParticleEffectMaterial()
|
|
{
|
|
Renderer[] renderers = GetComponentsInChildren<Renderer>();
|
|
|
|
foreach (Renderer rend in renderers)
|
|
{
|
|
Destructible parentDestructible = rend.GetComponentInParent<Destructible>(); // only get the material for the parent object to use for a particle effect
|
|
if (parentDestructible != this) continue;
|
|
bool isAcceptableRenderer = rend is MeshRenderer || rend is SkinnedMeshRenderer;
|
|
if (isAcceptableRenderer)
|
|
return rend.sharedMaterial;
|
|
}
|
|
|
|
return null; // could not find an acceptable material to use for particle effects
|
|
}
|
|
|
|
private void PlayDamageEffects()
|
|
{
|
|
// Check if we should play a particle effect for this damage level
|
|
if (damageEffects == null || damageEffects.Count == 0) return;
|
|
|
|
int currentDamageLevelIndex = 0;
|
|
if (_currentDamageLevel != null)
|
|
currentDamageLevelIndex = damageLevels.IndexOf(_currentDamageLevel); // FindIndex(a => a == currentDamageLevel);
|
|
|
|
foreach (DamageEffect effect in damageEffects)
|
|
{
|
|
if (effect == null || effect.Prefab == null) continue;
|
|
|
|
// Get rotation
|
|
Quaternion rotation = transform.rotation;
|
|
if (effect.Rotation != Vector3.zero)
|
|
rotation = transform.rotation * Quaternion.Euler(effect.Rotation);
|
|
|
|
// Is this effect only played if the destructible object has a certain tag?
|
|
if (effect.HasTagDependency && !gameObject.HasTag(effect.TagDependency))
|
|
continue;
|
|
|
|
if (_currentDamageLevel != null && effect.TriggeredAt < damageLevels.Count)
|
|
{
|
|
// TURN ON pre-destruction damage effects
|
|
if (currentDamageLevelIndex >= effect.TriggeredAt && !effect.HasStarted)
|
|
{
|
|
if (effect.GameObject != null)
|
|
{
|
|
for (int i = 0; i < effect.ParticleSystems.Length; i++)
|
|
{
|
|
ParticleSystem.EmissionModule emission = effect.ParticleSystems[i].emission;
|
|
emission.enabled = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// set parent to this destructible object and play
|
|
effect.GameObject = ObjectPool.Instance.Spawn(effect.Prefab, effect.Offset, rotation, transform);
|
|
|
|
if (effect.GameObject != null)
|
|
{
|
|
effect.ParticleSystems = effect.GameObject.GetComponentsInChildren<ParticleSystem>();
|
|
|
|
if (effect.Scale != Vector3.one)
|
|
{
|
|
foreach (ParticleSystem ps in effect.ParticleSystems)
|
|
{
|
|
var main = ps.main;
|
|
main.scalingMode = ParticleSystemScalingMode.Hierarchy;
|
|
}
|
|
|
|
effect.GameObject.transform.localScale = effect.Scale;
|
|
}
|
|
}
|
|
}
|
|
|
|
effect.HasStarted = true;
|
|
}
|
|
|
|
// TURN OFF pre-destruction damage effects
|
|
if (currentDamageLevelIndex < effect.TriggeredAt && effect.HasStarted)
|
|
{
|
|
if (effect.GameObject != null)
|
|
{
|
|
for (int i = 0; i < effect.ParticleSystems.Length; i++)
|
|
{
|
|
ParticleSystem.EmissionModule emission = effect.ParticleSystems[i].emission;
|
|
emission.enabled = false;
|
|
}
|
|
}
|
|
|
|
effect.HasStarted = false;
|
|
}
|
|
}
|
|
|
|
// Destroyed effects
|
|
if (effect.TriggeredAt == damageLevels.Count && IsDestroyed && !effect.HasStarted)
|
|
{
|
|
effect.GameObject = canBeDestroyed ?
|
|
ObjectPool.Instance.Spawn(effect.Prefab, transform.TransformPoint(effect.Offset), rotation) :
|
|
ObjectPool.Instance.Spawn(effect.Prefab, effect.Offset, rotation, transform);
|
|
|
|
if (effect.GameObject != null)
|
|
{
|
|
effect.ParticleSystems = effect.GameObject.GetComponentsInChildren<ParticleSystem>();
|
|
|
|
if (effect.Scale != Vector3.one)
|
|
{
|
|
foreach (ParticleSystem ps in effect.ParticleSystems)
|
|
{
|
|
var main = ps.main;
|
|
main.scalingMode = ParticleSystemScalingMode.Hierarchy;
|
|
}
|
|
|
|
effect.GameObject.transform.localScale = effect.Scale;
|
|
}
|
|
}
|
|
|
|
effect.HasStarted = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// NOTE: OnCollisionEnter will only fire if a rigidbody is attached to this object!
|
|
public void OnCollisionEnter(Collision collision)
|
|
{
|
|
if (DestructionManager.Instance == null) return;
|
|
if (!isActiveAndEnabled) return;
|
|
|
|
this.ProcessDestructibleCollision(collision, GetComponent<Rigidbody>());
|
|
|
|
if (collision.contacts.Length <= 0) return;
|
|
|
|
Destructible destructibleObj = collision.contacts[0].otherCollider.gameObject.GetComponentInParent<Destructible>();
|
|
if (destructibleObj != null && collision.contacts[0].otherCollider.attachedRigidbody == null)
|
|
destructibleObj.ProcessDestructibleCollision(collision, GetComponent<Rigidbody>());
|
|
}
|
|
|
|
// NOTE: OnDrawGizmos will only fire if Gizmos are turned on in the Unity Editor!
|
|
public void OnDrawGizmos()
|
|
{
|
|
damageEffects.DrawGizmos(transform);
|
|
centerPointOverride.DrawGizmos(transform);
|
|
}
|
|
|
|
public void FireDestroyedEvent()
|
|
{
|
|
DestroyedEvent?.Invoke(); // If there is at least one listener, trigger the event.
|
|
}
|
|
|
|
public void FireRepairedEvent()
|
|
{
|
|
RepairedEvent?.Invoke(); // If there is at least one listener, trigger the event.
|
|
}
|
|
|
|
public void FireDamagedEvent()
|
|
{
|
|
DamagedEvent?.Invoke(); // If there is at least one listener, trigger the event.
|
|
}
|
|
}
|
|
} |