using System.Collections.Generic; using UnityEngine; // Sci-Fi Ship Controller. Copyright (c) 2018-2023 SCSM Pty Ltd. All rights reserved. namespace SciFiShipController { /// /// This module can be used to trigger a DestructModule when the health of the object reaches 0. /// For a simplified solution, see SSCTargetable. /// IMPORTANT: If you want a ship, a ship’s damage regions, or a surface turret to take damage, /// use the features included in the Ship Control Module or the Surface Turret Module. This /// module is to be used with regular gameobjects that don’t include those components. /// Examples could include buildings or destructible props. /// [AddComponentMenu("Sci-Fi Ship Controller/Object Components/Destructive Object Module")] [DisallowMultipleComponent] [RequireComponent(typeof(SciFiShipController.DamageReceiver))] [HelpURL("http://scsmmedia.com/ssc-documentation")] public class DestructibleObjectModule : MonoBehaviour { #region Public Variables public bool initialiseOnStart = false; /// /// How much health the object has initially /// public float startingHealth = 100f; /// /// Whether this object uses shielding. Up until a point, shielding protects the object from damage /// public bool useShielding = false; /// /// Damage below this value will not affect the shield or the object's health while the shield is still active /// (i.e. until the shield has absorbed damage more than or equal to the shieldingAmount value from damage events above the /// damage threshold). Only relevant if useShielding is enabled. /// public float shieldingDamageThreshold = 10f; /// /// How much damage the shield can absorb before it ceases to protect the object from damage. Only relevant if /// useShielding is enabled. /// public float shieldingAmount = 100f; /// /// When useShielding is true, this is the rate per second that a shield will recharge (default = 0) /// [Range(0f, 100f)] public float shieldingRechargeRate = 0f; /// /// When useShielding is true, and shieldingRechargeRate is greater than 0, this is the delay, in seconds, /// between when damage occurs to a shield and it begins to recharge. /// [Range(0f, 300f)] public float shieldingRechargeDelay = 10f; /// /// The sound or particle FX used when object is destroyed /// If you modify this, call ReinitialiseEffects(). /// public EffectsModule destructionEffectsObject; /// /// The offset in the forward direction, from the objects gameobject, that the destruction effect is instantiated. /// public Vector3 destructionEffectsOffset = Vector3.zero; /// /// This is used when you want pre-build fragments of the object to explode out from the object position when it is destroyed. /// If you modify this, call ReinitialiseDestructObjects(). /// public DestructModule destructObject = null; /// /// The offset in the forward direction, from the objects gameobject, that the destruct module is instantiated. /// public Vector3 destructObjectOffset = Vector3.zero; /// /// The relative size of the blip on the radar mini-map /// Must be between 1 and 5 inclusive. /// [Range(1, 5)] public byte radarBlipSize = 1; #endregion #region Public Properties /// /// The current health value of this object. /// public float Health { get { return health; } set { // Update the health value health = value < 0f ? 0f : value; if (isInitialised && health == 0f) { DestructObject(); } } } /// /// [READONLY] /// Normalised (0.0 – 1.0) value of the health of the object. /// public float HealthNormalised { get { float _healthN = startingHealth == 0f ? 0f : health / startingHealth; if (_healthN > 1f) { return 1f; } else if (_healthN < 0f) { return 0f; } else { return _healthN; } } } /// /// [READONLY] Has the module been initialised? /// public bool IsInitialised { get { return isInitialised; } } /// /// [READONLY] The number used by the SSCRadar system to identify this object at a point in time. /// This should not be stored across frames and is updated as required by the system. /// public int RadarId { get { return radarItemIndex; } } /// /// The current health value of this object's shield. /// When a shield is destroyed, its value is set to -0.01. /// public float ShieldHealth { get { return shieldHealth; } set { // Update the health value shieldHealth = value; } } /// /// [READONLY] /// Normalised (0.0 – 1.0) value of the shield for this object. If useShielding is false, it will /// always return 0. /// public float ShieldNormalised { get { float _shieldN = !useShielding || shieldingAmount == 0f ? 0f : shieldHealth / shieldingAmount; if (_shieldN > 1f) { return 1f; } else if (_shieldN < 0f) { return 0f; } else { return _shieldN; } } } #endregion #region Private or Internal variables private bool isInitialised = false; private DamageReceiver damageReceiver = null; private float health; private float shieldHealth; /// /// Whether damage type multipliers are used when calculating damage from projectiles. /// [SerializeField] private bool useDamageMultipliers = false; /// /// The array of damage multipliers for this object. /// [SerializeField] private float[] damageMultipliersArray; /// /// [INTERNAL USE ONLY] Instead, call EnableRadar() or DisableRadar(). /// [SerializeField] private bool isRadarEnabled = false; /// /// The faction or alliance the object belongs to. This can be used to identify if a object is friend or foe. Neutral = 0. /// [SerializeField] private int factionId = 0; /// /// Although normally representing a squadron of ships, this can be used on a gameobjects to group it with other things in your scene /// [SerializeField] private int squadronId = -1; /// /// This identifies the destructionEffectsObject instance that may have been instantiated. /// [System.NonSerialized] internal SSCEffectItemKey destructionEffectItemKey; /// /// The ID number for this damage region's destruct prefab (as assigned by the SSCManager in the scene). /// This is the index in the SSCManager destructTemplateList. /// [INTERNAL USE ONLY] /// [System.NonSerialized] internal int destructObjectPrefabID; /// /// Flag for whether the destruct object has been activated. /// [INTERNAL USE ONLY] /// [System.NonSerialized] internal bool isDestructObjectActivated; /// /// This identifies the destruct object instance that may have been instantiated. /// [System.NonSerialized] internal SSCDestructItemKey destructItemKey; /// /// [INTERNAL USE ONLY] /// Flag for when the object destroy "event" has been actioned after health reaches 0 /// [System.NonSerialized] internal bool isDestroyed = false; [System.NonSerialized] private SSCManager sscManager = null; // Radar variables [System.NonSerialized] private SSCRadar sscRadar = null; [System.NonSerialized] internal int radarItemIndex = -1; //[System.NonSerialized] internal SSCRadarPacket sscRadarPacket; /// /// [INTERNAL USE ONLY] /// [System.NonSerialized] internal float shieldRechargeDelayTimer; /// /// The ID number for this object's destruction effects object prefab (as assigned by the Ship Controller Manager in the scene). /// This is the index in the SSCManager effectsObjectTemplatesList. /// [INTERNAL USE ONLY] /// [System.NonSerialized] public int effectsObjectPrefabID; /// /// Flag for whether the destruction effects object has been instantiated. /// [INTERNAL USE ONLY] /// [System.NonSerialized] public bool isDestructionEffectsObjectInstantiated; #endregion #region Initialise methods // Start is called before the first frame update void Start() { if (initialiseOnStart) { Initialise(); } } #endregion #region Update Methods private void Update() { if (isInitialised) { CheckShieldRecharge(); } } #endregion #region Private Methods /// /// Add or remove this gameobject from radar. /// /// private void EnableOrDisableRadar(bool isEnabled) { if (isEnabled) { if (isInitialised && radarItemIndex == -1) { // Create as a RadarItemType.GameObject if (sscRadar == null) { sscRadar = SSCRadar.GetOrCreateRadar(); } if (sscRadar != null) { radarItemIndex = sscRadar.EnableRadar(gameObject, transform.position, factionId, squadronId, 0, radarBlipSize); } isRadarEnabled = radarItemIndex >= 0; } } else { if (isInitialised && isRadarEnabled && radarItemIndex >= 0) { sscRadar.DisableRadar(radarItemIndex); } isRadarEnabled = false; } } /// /// Recharge the shield if required /// private void CheckShieldRecharge() { // Is shield recharging enabled? if (health > 0 && useShielding && shieldingRechargeRate > 0) { // Can the shield be recharged? if (shieldingRechargeDelay > shieldRechargeDelayTimer) { shieldRechargeDelayTimer += Time.deltaTime; } else { shieldHealth += shieldingRechargeRate * Time.deltaTime; if (shieldHealth > shieldingAmount) { shieldHealth = shieldingAmount; } } } } #endregion #region Public API Methods /// /// Apply damage to the object. If Use Damage Multipliers is enabled, you can optionally pass in the DamageTye. /// /// /// public void ApplyDamage (float damageAmount, ProjectileModule.DamageType damageType = ProjectileModule.DamageType.Default) { float actualDamage = damageAmount; bool objectDamaged = true; // Modify damage dealt based on relevant damage multipliers if (useDamageMultipliers) { actualDamage *= GetDamageMultiplier(damageType); } // Determine whether shielding is active for this object if (useShielding && shieldHealth > 0f) { // Set the shielding to active objectDamaged = false; // Only do damage to the shield if the damage amount is above the shielding threshold if (actualDamage >= shieldingDamageThreshold) { shieldHealth -= actualDamage; shieldRechargeDelayTimer = 0f; // If this damage destroys the shielding entirely... if (shieldHealth <= 0f) { // Get the residual damage value actualDamage = -shieldHealth; // Set the shielding to inactive objectDamaged = true; shieldHealth = -0.01f; } } } if (objectDamaged) { // Reduce health of object itself Health -= actualDamage; } } /// /// This routine is called by our damage receiver when a projectile or beam hits our object /// /// public void ApplyDamage (CallbackOnObjectHitParameters callbackOnObjectHitParameters) { if (callbackOnObjectHitParameters.hitInfo.transform != null) { ProjectileModule projectile = callbackOnObjectHitParameters.projectilePrefab; if (projectile != null) { ApplyDamage(projectile.damageAmount, projectile.damageType); } // Should have been a beam weapon that fired at the object else { BeamModule beam = callbackOnObjectHitParameters.beamPrefab; if (beam != null) { ApplyDamage(callbackOnObjectHitParameters.damageAmount, beam.damageType); } else { // if we don't need to know what hit the object, simply reduce the health ApplyDamage(callbackOnObjectHitParameters.damageAmount); } } } } /// /// Add health to the object. If isAffectShield is true, and the health reaches the maximum /// configured, excess health will be applied to the shield. /// To incur damage use the ApplyDamage(..). /// /// /// public void AddHealth (float healthAmount, bool isAffectShield) { if (healthAmount > 0f) { if (health < 0f) { health = healthAmount; } else { health += healthAmount; } if (health > startingHealth) { if (isAffectShield && useShielding) { float newShieldHealth = 0f; // When shielding is -ve (e.g. -0.01 when it has been used up) set the shielding amount rather than adding it if (shieldHealth < 0f) { newShieldHealth = health - startingHealth; } else { newShieldHealth = shieldHealth + health - startingHealth; } // Cap shielding to maximum permitted. if (newShieldHealth > shieldingAmount) { newShieldHealth = shieldingAmount; } shieldHealth = newShieldHealth; } // Cap health to maximum permitted health = startingHealth; } } } /// /// Disable radar for this object. If you want to change the visibility to other radar consumers, /// consider calling SetRadarVisibility(..) rather than disabling the radar and (later) /// calling EnableRadar() again. This will be automatically called by DestructObject(). /// public void DisableRadar() { EnableOrDisableRadar(false); } // Enable radar for this object. It will be visible on radar to others in the scene. public void EnableRadar() { EnableOrDisableRadar(true); } /// /// Returns the damage multiplier for damageType. /// /// /// public float GetDamageMultiplier (ProjectileModule.DamageType damageType) { switch ((int)damageType) { // Hardcoded int values for performance // Default = 0 case 0: return 1f; // Type A = 100 case 100: return damageMultipliersArray[0]; // Type B = 105 case 105: return damageMultipliersArray[1]; // Type C = 110 case 110: return damageMultipliersArray[2]; // Type D = 115 case 115: return damageMultipliersArray[3]; // Type E = 120 case 120: return damageMultipliersArray[4]; // Type F = 125 case 125: return damageMultipliersArray[5]; // Default case default: return 1f; } } /// /// Reinitialises variables required for Destruct Module. /// Call after modifying the destructObject. /// public void ReinitialiseDestructObjects() { if (sscManager == null) { sscManager = SSCManager.GetOrCreateManager(); } if (sscManager != null) { // Initialise destruct modules sscManager.UpdateDestructObjects(this); } #if UNITY_EDITOR else { Debug.LogWarning("DestructibleObjectModule.ReinitialiseDestructObjects Warning: could not find SSCManager to update effects."); } #endif } /// /// Reinitialises variables required for effects of the Destructible Object Module. /// Call after modifying any effect data for this module. /// public void ReinitialiseEffects() { if (sscManager == null) { sscManager = SSCManager.GetOrCreateManager(); } if (sscManager != null) { // Initialise effects objects sscManager.UpdateEffects(this); } #if UNITY_EDITOR else { Debug.LogWarning("DestructibleObjectModule.ReinitialiseEffects Warning: could not find SSCManager to update effects."); } #endif } /// /// Reset the health of the object back to initial values /// public void ResetHealth() { // Reset health value for the object health = startingHealth; // Reset shield health value for the object shieldHealth = shieldingAmount; shieldRechargeDelayTimer = 0f; isDestructObjectActivated = false; isDestroyed = false; } /// /// Sets the damage multiplier for damageType to damageMultiplier. /// /// /// public void SetDamageMultiplier (ProjectileModule.DamageType damageType, float damageMultiplier) { switch ((int)damageType) { // Hardcoded int values for performance // Type A = 100 case 100: damageMultipliersArray[0] = damageMultiplier; break; // Type B = 105 case 105: damageMultipliersArray[1] = damageMultiplier; break; // Type C = 110 case 110: damageMultipliersArray[2] = damageMultiplier; break; // Type D = 115 case 115: damageMultipliersArray[3] = damageMultiplier; break; // Type E = 120 case 120: damageMultipliersArray[4] = damageMultiplier; break; // Type F = 125 case 125: damageMultipliersArray[5] = damageMultiplier; break; } } /// /// If radar is enabled for this object, set its visibility to radar. /// /// public void SetRadarVisibility (bool isVisible) { if (isInitialised && isRadarEnabled && radarItemIndex >= 0) { sscRadar.SetVisibility(radarItemIndex, isVisible); } } /// /// Set the Squadron Id for the object. If radar is enabled, this will also update the radar. /// /// public void SetSquadronId (int newSquadronId) { if (squadronId != newSquadronId) { squadronId = newSquadronId; // Do we need to update the radar item? if (isInitialised && isRadarEnabled && radarItemIndex >= 0) { sscRadar.SetSquardronId(radarItemIndex, squadronId); } } } /// /// Set the Faction Id for the object. If radar is enabled, this will also update the radar. /// /// public void SetFactionId (int newFactionId) { if (factionId != newFactionId) { factionId = newFactionId; // Do we need to update the radar item? if (isInitialised && isRadarEnabled && radarItemIndex >= 0) { sscRadar.SetFactionId(radarItemIndex, factionId); } } } /// /// Verify that the damage multiplier array is correctly sized /// public void VerifyMultiplierArray() { // Check that the damage multipliers array exists if (damageMultipliersArray == null) { damageMultipliersArray = new float[] { 1f, 1f, 1f, 1f, 1f, 1f }; } else { // Check that the damage multipliers array is of the correct length int damageMultipliersArrayLength = damageMultipliersArray.Length; if (damageMultipliersArrayLength != 6) { // If it is not the correct length, resize it // Convert the array into a list List tempDamageMultipliersList = new List(); tempDamageMultipliersList.AddRange(damageMultipliersArray); if (damageMultipliersArrayLength > 6) { // If we have too many items in the array, remove some for (int i = damageMultipliersArrayLength; i > 6; i--) { tempDamageMultipliersList.RemoveAt(i - 1); } } else { // If we don't have enough items in the array, add some for (int i = damageMultipliersArrayLength; i < 6; i++) { tempDamageMultipliersList.Add(1f); } } // Convert the list back into an array damageMultipliersArray = tempDamageMultipliersList.ToArray(); } } } #endregion #region Public Virtual and Protected API Methods /// /// Initialises the DestructibleObjectModule. If you wish to override this in a child (inherited) /// class you almost always will want to call the base method first. /// public override void Initialise() /// { /// base.Initialise(); /// // Do stuff here /// } /// public virtual void Initialise() { if (!isInitialised) { damageReceiver = GetComponent(); if (damageReceiver == null) { damageReceiver = gameObject.AddComponent(); } if (damageReceiver == null) { #if UNITY_EDITOR Debug.LogWarning("ERROR: DestructibleOjectModule.Initialise() - could not find or add DamageReceiver component to " + name); #endif } else { VerifyMultiplierArray(); ResetHealth(); // Get a reference to the Ship Controller Manager instance sscManager = SSCManager.GetOrCreateManager(); // Not sure if we need these or just the ReinitialiseEffects() effectsObjectPrefabID = -1; isDestructionEffectsObjectInstantiated = false; destructionEffectItemKey = new SSCEffectItemKey(-1, -1, 0); // Not sure if we need these or just the ReinitialiseDestructObjects() destructObjectPrefabID = -1; isDestructObjectActivated = false; destructItemKey = new SSCDestructItemKey(-1, -1, 0); // Initialise effects objects ReinitialiseEffects(); // Initialise destruct modules ReinitialiseDestructObjects(); // Get notified when the object is hit by a projectile or beam damageReceiver.callbackOnHit = ApplyDamage; isInitialised = true; } if (isInitialised) { EnableOrDisableRadar(isRadarEnabled); } } } /// /// Destroys the object. /// If you wish to override this in a child (inherited) class you almost /// always will want to call the base after doing your actions. /// public override void DestructObject() /// { /// // Do stuff here /// base.DestructObject(); /// } /// public virtual void DestructObject() { if (isInitialised) { if (isRadarEnabled) { EnableOrDisableRadar(false); } #region Instantiate the destruction effects prefab if (destructionEffectsObject != null) { InstantiateEffectsObjectParameters ieParms = new InstantiateEffectsObjectParameters { effectsObjectPrefabID = effectsObjectPrefabID, position = transform.position + (transform.rotation * destructionEffectsOffset), rotation = transform.rotation }; sscManager.InstantiateEffectsObject(ref ieParms); } #endregion #region Instantiate the destruct module prefab if (!isDestructObjectActivated && destructObjectPrefabID >= 0 && destructObject != null) { // Turn off all colliders. As we are going to destroy the gameobject, // we can simply deactivate it. gameObject.SetActive(false); // Instantiate the region destruct prefab InstantiateDestructParameters dstParms = new InstantiateDestructParameters { destructPrefabID = destructObjectPrefabID, position = transform.position + (transform.rotation * destructObjectOffset), rotation = transform.rotation, explosionPowerFactor = 1f, explosionRadiusFactor = 1f }; // Keep track of the DestructModule instance that was instantiated for this object // In our case, we don't really need this as we're going to destroy this script. However, // it may be useful if we want to do something else in the future. if (sscManager.InstantiateDestruct(ref dstParms) != null) { destructItemKey = new SSCDestructItemKey(destructObjectPrefabID, dstParms.destructPoolListIndex, dstParms.destructSequenceNumber); } isDestructObjectActivated = true; } #endregion // Destroy the original object Destroy(gameObject); } } /// /// Destroys the object after a delay period in seconds /// /// public virtual void DestructObjectDelayed (float delayDuration) { if (delayDuration > 0f) { Invoke("DestructObject", delayDuration); } else { DestructObject(); } } #endregion } }