using System.Collections.Generic; using UnityEngine; // Sci-Fi Ship Controller. Copyright (c) 2018-2023 SCSM Pty Ltd. All rights reserved. namespace SciFiShipController { /// /// This module enables you to manage a prefab as it breaks into fragments. /// TODO: consider using simple capule or box colliders on fragments /// TODO: deal with proximityTrigger (should be per fragment or whole object) /// TODO: destructFragment.isObjectVisible isn't being set /// TODO: set explosion direction based on hit normal. /// [AddComponentMenu("Sci-Fi Ship Controller/Object Components/Destruct Module")] [HelpURL("http://scsmmedia.com/ssc-documentation")] public class DestructModule : MonoBehaviour { #region Enumerations public enum DisableRigidbodyMode { Destroy = 0, SetAsKinematic = 1, DontDisable = 2 } public enum DespawnCondition { Time = 0, DontDespawn = 1 //DistanceFromOrigin } #endregion #region Public Variables /// /// Should the explosion occur immediately the scene is started or the module is instantiated? /// This should be disabled if used with a pooling system. /// public bool isExplodeOnStart = false; /// /// Whether pooling is used when spawning destruct objects of this type. /// Currently we don't support changing this at runtime. /// public bool usePooling = true; /// /// The starting size of the pool. /// public int minPoolSize = 5; /// /// The maximum allowed size of the pool. /// public int maxPoolSize = 100; /// /// Add rigidbodies to the fragments in the prefab /// public bool isAddRigidBodiesEnabled = false; /// /// Add mesh colliders to the fragments in the prefab /// public bool isAddMeshCollidersEnabled = false; /// /// The default effective range of the blast /// public float explosionRadius = 5f; /// /// The default power of the blast /// public float explosionPower = 100f; /// /// When the speed in any direction of the fragment falls below this value, the fragment is considered to have stopped moving /// public float unmovingVelocity = 0.1f; /// /// This is the total mass of all the fragments /// public float mass = 1f; /// /// This may be more accurate when there the is a lot of variation between the size of each fragment. /// This method is slower during the initial configuration phase and may affect performance of non-pooled modules. /// public bool isCalcMassByBounds = false; /// /// The amount of drag the fragments have. A solid block of metal would be 0.001, while a feather would be 10. /// public float drag = 0.01f; /// /// The amount of angular drag the fragments have /// public float angularDrag = 0.05f; /// /// Fragments are effected by gravity /// public bool useGravity = true; /// /// The rigidbody interpolation /// public RigidbodyInterpolation interpolation = RigidbodyInterpolation.None; /// /// The rigidbody collision detection mode /// public CollisionDetectionMode collisionDetection = CollisionDetectionMode.Discrete; /// /// If a fragment has been unmoving for more than the maximum time, set the object as static /// and "disable" the rigidbody according to the "Disable Rigidbody Mode". /// public float maxTimeUnmoving = 1f; /// /// Start in Static mode rather than Dynamic. /// This is NOT compatible with disableRigidbodyMode == DisableRigidbodyMode.Destroy /// public bool isStartStatic = false; /// /// After this time (in seconds), the destruct object is automatically despawned or removed from the scene. /// public float despawnTime = 5f; // How the rigidbodies are disabled when the object is considered to be Static. // "Destroy" removes the rigidbody component and adds it back in as needed - best performance for unmoving objects // "Set As Kinematic" sets the rigidbody to kinematic - half/half performance // "Don't Disable" uses Unity default rigidbody behaviour where rigidbodies go into sleep mode until something collides with them // - best for objects being moved // "Destroy" is best for objects far away from each other, and "Don't Disable" is best for objects close to each other public DisableRigidbodyMode disableRigidbodyMode = DisableRigidbodyMode.Destroy; // The conditions under which this object despawns public DespawnCondition despawnCondition = DespawnCondition.DontDespawn; /// /// Wait until the fragment is not being rendered by the camera before being despawned /// NOTE: This has not been implemented yet /// public bool waitUntilNotRenderedToDespawn = false; /// /// Wait until the fragment is set to static before being despawned /// public bool waitUntilStaticToDespawn = true; /// /// [INTERNAL ONLY] /// [HideInInspector] public bool allowRepaint = false; #endregion #region Public Properties /// /// Has the module been initially configured? /// public bool IsInitialised { get; private set; } /// /// Is the destruct module currently in use? /// public bool IsActivated { get; private set; } /// /// [READONLY] Is the destruct module currently ready for use, or has it been paused? /// public bool IsDestructEnabled { get { return isDestructEnabled; } } public float EstimatedDespawnTime { get { return IsActivated && isDestructEnabled && despawnCondition == DespawnCondition.Time ? despawnTimer - despawnTime : float.PositiveInfinity; } } #endregion #region Private and Internal Variables [System.NonSerialized] private List destructFragmentList = null; [System.NonSerialized] private MeshRenderer[] meshRenderers = null; private int numFragments = 0; private int numActiveFragments = 0; private float sqrUnmovingVelocity; private bool isDynamic = false; //private SphereCollider proximityTrigger; private float despawnTimer = 0f; /// /// Is the destruct module currently running (ready of use), or has it been paused? /// private bool isDestructEnabled = true; /// /// [INTERNAL ONLY] /// Used to determine uniqueness /// [System.NonSerialized] internal uint itemSequenceNumber; /// /// [INTERNAL ONLY] /// Use with pooling which gets set in Activate(..) /// [System.NonSerialized] internal int destructPoolListIndex; #endregion #region Internal Static Variables internal static uint nextSequenceNumber = 1; #endregion #region Initialise Methods // Start is called before the first frame update void Start() { InitialiseDestruct(); if (isExplodeOnStart) { InstantiateDestructParameters dstParms = new InstantiateDestructParameters { position = transform.position, rotation = transform.rotation, explosionPowerFactor = 1f, explosionRadiusFactor = 1f }; ActivateModule(-1); Explode(dstParms); } } /// /// The module must always be initialised AND activated before it can be used. /// It should only be initialised once, while it can be activated and deactivated /// multiple times. /// internal bool InitialiseDestruct() { if (IsInitialised) { return true; } // Assume non-pooling. This gets set when Activate(..) is called. destructPoolListIndex = -1; // Calculate square of unmovingVelocity so that at runtime we can use the // vector3.sqrMagnitude function instead of the slower vector3.magnitude to compare velocities sqrUnmovingVelocity = unmovingVelocity * unmovingVelocity; meshRenderers = GetComponentsInChildren(); numFragments = meshRenderers == null ? 0 : meshRenderers.Length; // When first initialsed, all fragments are active. numActiveFragments = numFragments; if (numFragments > 0) { destructFragmentList = new List(numFragments); if (destructFragmentList != null) { int numFragmentsAdded = 0; GameObject fragmentGO = null; #region Calculate Total Bounds float totalVolume = 0f; // This will calculate the combined volume of all fragments as if they were laid out side by side. // It uses bounds to calculate volume rather than the actual volume of the fragment. if (isCalcMassByBounds) { for (int fIdx = 0; fIdx < numFragments; fIdx++) { MeshRenderer meshRenderer = meshRenderers[fIdx]; Bounds _fragmentBounds = meshRenderer.bounds; totalVolume += _fragmentBounds.size.x * _fragmentBounds.size.y * _fragmentBounds.size.z; } } #endregion #region Populate the list of destructFragments. for (int fIdx = 0; fIdx < numFragments; fIdx++) { MeshRenderer meshRenderer = meshRenderers[fIdx]; DestructFragment destructFragment = new DestructFragment(); if (destructFragment != null) { fragmentGO = meshRenderer.gameObject; destructFragment.originalLocalPosition = fragmentGO.transform.localPosition; destructFragment.originalLocalRotation = fragmentGO.transform.localRotation; destructFragment.mRen = meshRenderer; destructFragment.isObjectVisible = meshRenderer.isVisible; // Calculate fragment mass if (isCalcMassByBounds && totalVolume > 0f) { Bounds _fragmentBounds = meshRenderer.bounds; destructFragment.mass = (_fragmentBounds.size.x * _fragmentBounds.size.y * _fragmentBounds.size.z) / totalVolume; //Debug.Log("[DEBUG] fragment mass: " + destructFragment.mass); } else { destructFragment.mass = mass / numFragments; } // Add a mesh collider if required if (isAddMeshCollidersEnabled) { MeshCollider meshCollider = meshRenderer.GetComponent(); if (meshCollider == null) { meshCollider = fragmentGO.AddComponent(); } if (meshCollider != null) { // Required for non-kinematic mesh colliders meshCollider.convex = true; } } Rigidbody rBody = meshRenderer.GetComponent(); // Add a rigid body if required if (isAddRigidBodiesEnabled && rBody == null) { rBody = fragmentGO.AddComponent(); } if (rBody != null) { destructFragment.rBody = rBody; } if (disableRigidbodyMode != DisableRigidbodyMode.Destroy) { SetUpRigidbody(destructFragment, useGravity, drag, angularDrag, interpolation, collisionDetection); } if (isStartStatic) { // If starting static, set the object as static SetStatic(destructFragment); } else { // Else set the object as dynamic SetDynamic(destructFragment); } destructFragmentList.Add(destructFragment); numFragmentsAdded++; } } #endregion IsInitialised = numFragmentsAdded == numFragments; } #if UNITY_EDITOR else { Debug.LogWarning("ERROR: DestructMoudle.Initialise() - could not create a new list of fragments. PLEASE REPORT"); } #endif } return IsInitialised; } /// /// INCOMPLETE - should turn off renderers etc /// internal void DeactivateModule() { IsActivated = false; } /// /// Turn the destruct module on and get it ready for use /// internal uint ActivateModule(int poolIndex) { if (InitialiseDestruct()) { IncrementSequenceNumber(); // Reset despawn timer despawnTimer = 0f; if (usePooling) { for (int fIdx = 0; fIdx < numFragments; fIdx++) { DestructFragment destructFragment = destructFragmentList[fIdx]; if (destructFragment != null) { destructFragment.timeUnmoving = 0f; if (destructFragment.isDespawned) { Transform transform = destructFragment.mRen == null ? null : transform = destructFragment.mRen.transform; // Reset the rigidbody Rigidbody rBody = destructFragment.rBody; if (rBody != null && transform != null) { // Reset position and rotation // At this point it may be Kinematic and should be only moved from // FixedUpdate with rBody.MovePosition(..) and rBody.MoveRotation(..). However, // as we're about to turn off Kinematic so we should be ok... maybe. transform.localPosition = destructFragment.originalLocalPosition; transform.localRotation = destructFragment.originalLocalRotation; rBody.velocity = Vector3.zero; rBody.angularVelocity = Vector3.zero; } if (transform != null) { transform.gameObject.SetActive(true); } destructFragment.isDespawned = false; } } } numActiveFragments = numFragments; destructPoolListIndex = poolIndex; } else { destructPoolListIndex = -1; } SetDynamicAll(); isDestructEnabled = true; IsActivated = true; return itemSequenceNumber; } else { return 0; } } #endregion #region Update Methods // Update is called once per frame void Update() { if (!isDestructEnabled || !IsActivated) { return; } #region Disabling dynamic // Determine what to do when a fragment stops moving if (disableRigidbodyMode != DisableRigidbodyMode.DontDisable) { for (int fIdx = 0; fIdx < numFragments; fIdx++) { DestructFragment destructFragment = destructFragmentList[fIdx]; if (destructFragment != null && destructFragment.isDynamic && !destructFragment.isDespawned && destructFragment.rBody != null) { // Check if the object is currently "unmoving" if (destructFragment.rBody.velocity.sqrMagnitude < sqrUnmovingVelocity) { // If it isn't moving increment the unmoving timer destructFragment.timeUnmoving += Time.deltaTime; if (destructFragment.timeUnmoving > maxTimeUnmoving) { // If it has been unmoving for more than the maximum time set the object as static SetStatic(destructFragment); // Reset the timer destructFragment.timeUnmoving = 0f; } } else { // If it is moving reset the unmoving timer destructFragment.timeUnmoving = 0f; } } } } #endregion #region Despawn based on elapsed time if (despawnCondition == DespawnCondition.Time) { // Increment the despawn timer despawnTimer += Time.deltaTime; for (int fIdx = 0; fIdx < numFragments; fIdx++) { DestructFragment destructFragment = destructFragmentList[fIdx]; // If needed wait until it isn't rendered to despawn if (destructFragment != null && !destructFragment.isDespawned && (!waitUntilNotRenderedToDespawn || !destructFragment.isObjectVisible)) { // If needed to wait until it is static to despawn if (!waitUntilStaticToDespawn || !destructFragment.isDynamic || (disableRigidbodyMode == DisableRigidbodyMode.DontDisable && destructFragment.rBody.IsSleeping())) { // Only despawn if a given time has elapsed if (despawnTimer > despawnTime) { // Despawn the fragment Despawn(destructFragment, fIdx); } } } } } #endregion //// Despawn based on distance //else if (despawnCondition == DespawnCondition.DistanceFromOrigin) //{ // // If needed wait until it isn't rendered to despawn // if (!waitUntilNotRenderedToDespawn || !objectVisible) // { // // If needed wait until it is static to despawn // if (!waitUntilStaticToDespawn || !isDynamic || (disableRigidbodyMode == DisableRigidbodyMode.DontDisable && rBody.IsSleeping())) // { // // Only despawn if it is more than a given distance from its starting point // if (Vector3.Distance(origin, transform.position) > despawnDistance) // { // // Despawn the object // Despawn(); // } // } // } //} } #endregion #region Private and Internal Methods /// /// Makes this DestructModule unique from all others that have gone before them. /// This is called every time beam is Activated. /// internal void IncrementSequenceNumber() { itemSequenceNumber = nextSequenceNumber++; // if sequence number needs to be wrapped, do so to a high-ish number that is unlikely to be in use if (nextSequenceNumber > uint.MaxValue - 100) { nextSequenceNumber = 100000; } } /// /// CURRENTLY NOT IMPLEMENTED /// /// private void OnTriggerEnter(Collider other) { // If the proximity trigger collider is triggered when the object is static... if (disableRigidbodyMode != DisableRigidbodyMode.DontDisable && !isDynamic) { // Check whether the other collider has a rigidbody attached and is moving. Rigidbody otherRBody = other.attachedRigidbody; if (otherRBody != null && otherRBody.velocity.sqrMagnitude > sqrUnmovingVelocity) { SetDynamicAll(); } } } // Use OnBecameVisible and OnBecameInvisible events to track object visibility // INCOMPLETE - currently only works if there is a mesh renderer attached to the // parent gameobject. private void OnBecameVisible() { //Debug.Log("[DEBUG] Become visible T:" + Time.time); //objectVisible = true; } private void OnBecameInvisible() { //objectVisible = false; //Debug.Log("[DEBUG] Become invisible T:" + Time.time); } private void SetDynamicAll() { for (int fIdx = 0; fIdx < numFragments; fIdx++) { DestructFragment destructFragment = destructFragmentList[fIdx]; if (destructFragment != null) { SetDynamic(destructFragment); } } } private void SetDynamic(DestructFragment destructFragment) { // Disable the static flags, enable the rigidbody and disable the proximity trigger //if (proximityTrigger != null) { proximityTrigger.enabled = false; } if (disableRigidbodyMode == DisableRigidbodyMode.Destroy) { SetUpRigidbody(destructFragment, useGravity, drag, angularDrag, interpolation, collisionDetection); } else if (disableRigidbodyMode == DisableRigidbodyMode.SetAsKinematic) { if (destructFragment.rBody != null) { destructFragment.rBody.isKinematic = false; destructFragment.rBody.collisionDetectionMode = collisionDetection; } } // Set the state of isDynamic destructFragment.isDynamic = true; } private void SetStaticAll() { for (int fIdx = 0; fIdx < numFragments; fIdx++) { DestructFragment destructFragment = destructFragmentList[fIdx]; if (destructFragment != null) { SetStatic(destructFragment); } } } private void SetStatic(DestructFragment destructFragment) { // Enable the static flags (except for batching), disable the rigidbody and enable the proximity trigger // NOTE: batching and proximity trigger are not implemented yet. //if (proximityTrigger != null) { proximityTrigger.enabled = true; } if (disableRigidbodyMode == DisableRigidbodyMode.Destroy) { // Destroy may not happen in the current frame. If SetDynamic is called immediately after SetStatic, // like on activation when isStartStatic is true, SetDynamic may add the rigidbody only to have it // immediately destroyed. if (destructFragment.rBody != null) { Destroy(destructFragment.rBody); destructFragment.rBody = null; } } else if (disableRigidbodyMode == DisableRigidbodyMode.SetAsKinematic) { if (destructFragment.rBody != null) { destructFragment.rBody.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative; destructFragment.rBody.isKinematic = true; } } // Set the state of isDynamic destructFragment.isDynamic = false; } // Set up a rigidbody for the fragment private void SetUpRigidbody(DestructFragment destructFragment, bool rUseGravity, float rDrag, float rAngularDrag, RigidbodyInterpolation rInterpolation, CollisionDetectionMode rCollisionDetection) { if (destructFragment != null && destructFragment.mRen != null) { destructFragment.rBody = destructFragment.mRen.GetComponent(); if (destructFragment.rBody == null) { destructFragment.rBody = destructFragment.mRen.gameObject.AddComponent(); } destructFragment.rBody.mass = destructFragment.mass; destructFragment.rBody.drag = rDrag; destructFragment.rBody.angularDrag = rAngularDrag; destructFragment.rBody.interpolation = rInterpolation; destructFragment.rBody.collisionDetectionMode = rCollisionDetection; destructFragment.rBody.useGravity = rUseGravity; } } /// /// Set up a spherecollider and return that spherecollider /// CURENTLY NOT FULLY IMPLEMENTED - set for the whole object not per fragment /// /// /// private SphereCollider SetUpProximityTrigger(float proximity) { // Set up a "proximity trigger" collider: a sphere collider of a defined radius with "isTrigger" enabled SphereCollider trigger = gameObject.AddComponent(); trigger.isTrigger = true; // Radius needs to be scaled down by the scale of the object to be a correct proximity measurement Vector3 objectScale = transform.lossyScale; trigger.radius = proximity / Mathf.Max(objectScale.x, objectScale.y, objectScale.z); return trigger; } /// /// If no more fragements to despawn, this then calls Despawn(). /// /// /// private void Despawn(DestructFragment destructFragment, int fragmentIndex) { if (usePooling) { // Deactivate the fragment destructFragment.isDespawned = true; destructFragment.mRen.gameObject.SetActive(false); } else { Destroy(destructFragment.mRen.gameObject); destructFragmentList.RemoveAt(fragmentIndex); numFragments--; } numActiveFragments--; if (numActiveFragments < 1) { Despawn(); } } // Despawn the object private void Despawn() { if (usePooling && destructPoolListIndex >= 0) { IsActivated = false; // Return it to the pool gameObject.SetActive(false); } else { Destroy(gameObject); } } /// /// UNTESTED /// /// private void EnableOrDisableDestruct(bool isEnabled) { if (IsActivated) { for (int fIdx = 0; fIdx < numFragments; fIdx++) { DestructFragment destructFragment = destructFragmentList[fIdx]; // Fragments that are already despawned and those without a rigidbody do not need to be paused if (destructFragment != null && !destructFragment.isDespawned && destructFragment.rBody != null) { // If enabling and this fragement was paused, unpause it now if (isEnabled && destructFragment.isPaused && destructFragment.isDynamic) { destructFragment.rBody.isKinematic = false; destructFragment.rBody.collisionDetectionMode = collisionDetection; } else if (!isEnabled) { destructFragment.rBody.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative; destructFragment.rBody.isKinematic = true; } destructFragment.rBody.detectCollisions = isEnabled; destructFragment.isPaused = !isEnabled; } } } isDestructEnabled = isEnabled; } #endregion #region Public API Methods /// /// Disable or pause the Destruct module /// public void DisableDestruct() { EnableOrDisableDestruct(false); } /// /// Enable or unpause the Destruct module /// public void EnableDestruct() { EnableOrDisableDestruct(true); } /// /// Explode or begin the destruction process /// /// public void Explode(InstantiateDestructParameters dstParms) { if (IsActivated) { for (int fIdx = 0; fIdx < numFragments; fIdx++) { DestructFragment destructFragment = destructFragmentList[fIdx]; if (destructFragment != null) { if (destructFragment.rBody != null) { destructFragment.rBody.AddExplosionForce(explosionPower * dstParms.explosionPowerFactor, dstParms.position, explosionRadius * dstParms.explosionRadiusFactor); } } } } } #endregion } }