using UnityEngine; using System.Collections.Generic; // Sci-Fi Ship Controller. Copyright (c) 2018-2023 SCSM Pty Ltd. All rights reserved. namespace SciFiShipController { [HelpURL("http://scsmmedia.com/ssc-documentation")] [AddComponentMenu("Sci-Fi Ship Controller/Weapon Components/Projectile Module")] public class ProjectileModule : MonoBehaviour { #region Public Enumerations public enum DamageType { /// /// Damage from projectiles of this type will be unaffected by ship damage multipliers /// i.e. the amount of damage done to the ship will be identical to damageAmount. /// Default = 0, TypeA = 100, TypeB = 105, TypeC = 110, TypeD = 115, TypeE = 120, TypeF = 125, } #endregion #region Public Variables /// /// The starting speed of the projectile in metres per second. /// public float startSpeed = 100f; /// /// Whether the projectile is affected by gravity. /// public bool useGravity = false; /// /// The type of damage the projectile does. The amount of damage dealt to a ship upon collision is dependent /// on the ship's multiplier for this damage type (i.e. if a Type A projectile with a damage amount of 10 hits a ship /// with a Type A damage multiplier of 2, a total damage of 20 will be done to the ship). If the damage type is set to Default, /// the damage multipliers are ignored i.e. the damage amount is unchanged. /// public DamageType damageType = DamageType.Default; /// /// The amount of damage the projectile does on collision with a ship or object. NOTE: Non-ship objects need /// a DamageReceiver component. /// public float damageAmount = 10f; /// /// Whether Entity Component System and Job System is used when spawning projectiles of this type /// public bool useECS = false; /// /// Whether pooling is used when spawning projectiles of this type. /// Currently we don't support changing this at runtime. /// public bool usePooling = true; /// /// The starting size of the pool. Only relevant when usePooling is enabled. /// public int minPoolSize = 100; /// /// The maximum allowed size of the pool. Only relevant when usePooling is enabled. /// public int maxPoolSize = 1000; /// /// The projectile will be automatically despawned after this amount of time (in seconds) has elapsed. /// public float despawnTime = 1f; /// /// The ID number for this projectile prefab (as assigned by the Ship Controller Manager in the scene). /// This is the index in the SSCManager projectileTemplatesList. /// [INTERNAL USE ONLY] /// public int projectilePrefabID; /// /// The sound or particle FX used when a collision occurs. /// If you modify this, call shipControlModule.ReinitialiseShipProjectilesAndEffects() and/or /// surfaceTurretModule.ReinitialiseTurretProjectilesAndEffects() for each ship/surface turret /// this projectile is used on. /// public EffectsModule effectsObject = null; /// /// The ID number for this projectile's destruction effects object prefab (as assigned by the Ship Controller Manager in the scene). /// This is the index in the SSCManager effectsObjectTemplatesList. Not defined = -1. /// [INTERNAL USE ONLY] /// public int effectsObjectPrefabID = -1; /// /// The sound or particle FX used when a collision occurs with a shield (instead of the effectsObject). /// If you modify this, call shipControlModule.ReinitialiseShipProjectilesAndEffects() and/or /// surfaceTurretModule.ReinitialiseTurretProjectilesAndEffects() for each ship/surface turret /// this projectile is used on. /// public EffectsModule shieldEffectsObject = null; /// /// The ID number for this projectile's effects object prefab (as assigned by the Ship Controller Manager in the scene) /// for when a shielded ship is hit. /// This is the index in the SSCManager effectsObjectTemplatesList. Not defined = -1. /// [INTERNAL USE ONLY] /// public int shieldEffectsObjectPrefabID = -1; /// /// The sound and/or particle FX used when a projectile is fired from a weapon. /// If you modify this, call shipControlModule.ReinitialiseShipProjectilesAndEffects() and/or /// surfaceTurretModule.ReinitialiseTurretProjectilesAndEffects() for each ship/surface turret /// this projectile is used on. /// public EffectsModule muzzleEffectsObject = null; /// /// The distance in local space that the muzzle Effects Object should be instantiated /// from the weapon firing point. Typically only the z-axis would be used when the projectile /// is instantiated in front or forwards from the actual weapon. /// public Vector3 muzzleEffectsOffset = Vector3.zero; /// /// The ID number for this projectile's muzzle object prefab (as assigned by the Ship Controller Manager in the scene). /// This is the index in the SSCManager effectsObjectTemplatesList. Not defined = -1. /// [INTERNAL USE ONLY] /// public int muzzleEffectsObjectPrefabID = -1; /// /// The Id of the ship that fired the projectile /// public int sourceShipId = -1; /// /// The Squadron which the ship belonged to when it fired the projectile /// public int sourceSquadronId = -1; /// /// Is this projectile guided to a target with kinematics? Position and velocity will be determined by aiming at a target. It will /// not consider the forces required to move the projectile. /// NOTE: All projectiles in SSC use kinematics but by default they are fire and forget rather than guided towards a target. /// If you modify this, call shipControlModule.ReinitialiseShipProjectilesAndEffects() and/or /// surfaceTurretModule.ReinitialiseTurretProjectilesAndEffects() for each ship/surface turret /// this projectile is used on. /// Currently does not support DOTS/ECS. /// public bool isKinematicGuideToTarget = false; /// /// The max turning speed in degrees per second for a guided projectile. /// Only relevant when isKinematicGuideToTarget is enabled. /// [Range(10f, 360f)] public float guidedMaxTurnSpeed = 90f; /// /// The layer mask used for collision testing for this projectile. /// Default is everything. /// public LayerMask collisionLayerMask = ~0; /// /// The estimated range (in metres) of this projectile assuming it travels at a constant velocity. /// public float estimatedRange { get { return startSpeed * despawnTime; } } /// /// Current velocity of the projectile. /// Should only be updated when using the sscManager.callbackProjectileMoveTo delegate. /// public Vector3 Velocity { get { return velocity; } set { if (isKinematicGuideToTarget) { velocity = value; } } } /// /// The world space position of the projectile in the current frame /// public Vector3 ThisFramePosition { get { return thisFramePosition; } internal set { thisFramePosition = value; } } /// /// The world space position of the projectile in the last frame /// public Vector3 LastFramePosition { get { return lastFramePosition; } internal set { lastFramePosition = value; } } /// /// If a ship is being targeted, will return its name. If it is being targeted by is NULL, will assume destroyed. /// public string TargetShipName { get { return isTargetShip ? (targetShip == null ? "Destroyed" : targetShip.name) : "-"; } } /// /// If a ship damage region is being targeted, will return its name. If it is being targeted but is NULL, will assume destroyed. /// public string TargetShipDamageRegionName { get { return isTargetShipDamageRegion ? (targetShip == null ? "Destroyed" : targetShip.IsInitialised && targetguidHash != 0 ? targetShip.shipInstance.GetDamageRegion(targetguidHash).name : "-") : "-"; } } /// /// If a gameobject is being targeted, will return its name /// public string TargetGameObjectName { get { return targetGameObject != null ? (string.IsNullOrEmpty(targetGameObject.name) ? "no name" : targetGameObject.name) : "-"; } } #endregion #region Protected variables // These variables can be modified by classes that inherit from ProjectileModule protected Vector3 velocity = Vector3.zero; protected float speed = 0f; protected Vector3 lastFramePosition = Vector3.zero; protected Vector3 thisFramePosition = Vector3.zero; protected RaycastHit hitInfo; protected bool isInitialised = false; protected bool isProjectileEnabled = true; protected float despawnTimer = 0f; #endregion #region Private variables /// /// From Ship, the magnitude of the acceleration (in metres per second squared) due to gravity. /// private float gravitationalAcceleration; /// /// From Ship, the direction in world space in which gravity acts upon the ship. /// private Vector3 gravityDirection; /// /// The current ship (if any) being targeted when isKinematicGuideToTarget is true. /// private ShipControlModule targetShip = null; /// /// The guidHash of the target. Currently only set for ship damage regions. /// private int targetguidHash = 0; /// /// The current ship damage region (if any) being targeted when isKinematicGuideToTarget is true. /// private DamageRegion targetShipDamageRegion = null; /// /// The current GameObject (if any) being targeted when isKinematicGuideToTarget is true. /// private GameObject targetGameObject = null; // Is a ship being targeted? private bool isTargetShip = false; /// /// Is a ship's damage region being targeted? /// private bool isTargetShipDamageRegion = false; /// /// Private reference to the SSCManager in the scene. Currently only populated /// when isKinematicGuideToTarget is true. Used to get the CallbackProjectileMoveTo /// method from SSCManager. /// private SSCManager sscManager = null; // Is there a user-defined CallbackProjectileMoveTo configured for this projectile? private bool isCallbackOnMoveTo = false; // Augmented Proportional Navigation (APN) variables // current and previous frame's Line of Sight (normalised) private Vector3 lastFrameLOSN = Vector3.zero; private Vector3 thisFrameLOSN = Vector3.zero; private static readonly float NavConst = 3f; #endregion #region Enable/Disable Event Methods void OnDisable() { isInitialised = false; } #endregion #region Update Methods // FixedUpdate is called once per physics update (typically about 50 times per second) private void FixedUpdate () { if (isInitialised && isProjectileEnabled) { // Remember last frame position as last frame position - we are about to update this frame position lastFramePosition = thisFramePosition; CalcPositionAndVelocity(); // Check to see if the projectile has collided with anything during this frame // If nothing hit but using pooling, decrement the timer. if (!CheckCollision() && usePooling) { // To avoid a pooled projectile being despawned at the incorrect time, // do it here rather than calling Invoke("DestroyProjectile", despawnTime) // when it is initialised. despawnTimer += Time.deltaTime; if (despawnTimer >= despawnTime) { DestroyProjectile(); } } // Update the position of the object if (!isCallbackOnMoveTo) { transform.position = thisFramePosition; } } } #endregion #region Public Obsolete Methods [System.Obsolete("This method will be removed in a future version. Please use InitialiseProjectile (InstantiateProjectileParameters ipParms).")] public void InitialiseProjectile(Vector3 weaponVelocity, int projectilePrefabID, float gravity, Vector3 gravityDirection, int shipId, int squadronId) { InitialiseProjectile(new InstantiateProjectileParameters { weaponVelocity = weaponVelocity, projectilePrefabID = projectilePrefabID, gravity = gravity, gravityDirection = gravityDirection, shipId = shipId, squadronId = squadronId, // targets are not supported with this older method targetShip = null, targetGameObject = null, targetguidHash = 0 } ); } #endregion #region Virtual and Protected Methods /// /// Initialises the projectile. 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 InitialiseProjectile(InstantiateProjectileParameters ipParms) /// { /// base.InitialiseProjectile(ipParms); /// // Do stuff here /// } /// public virtual void InitialiseProjectile(InstantiateProjectileParameters ipParms) { // Initialise the velocity based on the forwards direction // The forwards direction should have been set correctly prior to enabling the object speed = startSpeed; velocity = transform.forward * speed; // Initialise last/this frame positions // Shift the position forward by the weapon velocity, so that projectiles don't ever end up behind the ship lastFramePosition = transform.position + (velocity * Time.fixedDeltaTime); thisFramePosition = lastFramePosition; transform.position = thisFramePosition; // Add the weapon velocity to the projectile velocity // This needs to be done after the above so that the projectiles aren't spawned to the sides of the ship velocity += ipParms.weaponVelocity; // Store the index to the ProjectileTemplate in the SSCManager projectileTemplatesList // This is used with Projectile FX when we know the ProjectileModule but not the parent ProjectileTemplate. this.projectilePrefabID = ipParms.projectilePrefabID; // Store the index to the EffectsObjectTemplate in sscManager.effectsObjectTemplatesList this.effectsObjectPrefabID = ipParms.effectsObjectPrefabID; this.shieldEffectsObjectPrefabID = ipParms.shieldEffectsObjectPrefabID; this.gravitationalAcceleration = ipParms.gravity; this.gravityDirection = ipParms.gravityDirection; this.sourceShipId = ipParms.shipId; this.sourceSquadronId = ipParms.squadronId; if (!useECS && isKinematicGuideToTarget) { targetShip = ipParms.targetShip; targetGameObject = ipParms.targetGameObject; targetguidHash = ipParms.targetguidHash; if (ipParms.targetShip != null) { targetShipDamageRegion = null; if (targetguidHash != 0 && targetShip.IsInitialised && targetShip.shipInstance != null) { targetShipDamageRegion = targetShip.shipInstance.GetDamageRegion(targetguidHash); } isTargetShipDamageRegion = targetShipDamageRegion != null; isTargetShip = !isTargetShipDamageRegion; // If not a damage region or one was not found, reset the targetguidHash. if (isTargetShip) { targetguidHash = 0; } } else { isTargetShip = false; isTargetShipDamageRegion = false; targetShipDamageRegion = null; } // reset this frame Line of Sight (normalised) thisFrameLOSN.x = 0f; thisFrameLOSN.y = 0f; thisFrameLOSN.z = 0f; // If in a pool, this may have already been called if (!usePooling || sscManager == null) { sscManager = SSCManager.GetOrCreateManager(); } isCallbackOnMoveTo = sscManager != null && sscManager.callbackProjectileMoveTo != null; } else { ClearTarget(); } // After a given amount of time, automatically destroy this projectile if (usePooling) { despawnTimer = 0f; } else { Invoke("DestroyProjectile", despawnTime); } isInitialised = true; } /// /// Calculate new velocity and thisFramePosition /// protected virtual void CalcPositionAndVelocity() { if (useGravity) { // F = ma // a = dv/dt // F = mdv/dt // dv = Fdt/m velocity += (gravitationalAcceleration * Time.deltaTime * gravityDirection); } if (isKinematicGuideToTarget) { if (isCallbackOnMoveTo) { sscManager.callbackProjectileMoveTo(this); } else { GuideToTarget(); } } // Move the projectile according to its current velocity thisFramePosition = lastFramePosition + (velocity * Time.deltaTime); } /// /// Check to see if the projectile has collided with anything during this frame /// /// protected virtual bool CheckCollision() { // TODO: Look into whether this can be done using RaycastCommand with JobSystem // LayerMask defaults to everything (inverse of nothing ~0). Don't detect trigger colliders if (Physics.Linecast(lastFramePosition, thisFramePosition, out hitInfo, collisionLayerMask, QueryTriggerInteraction.Ignore)) { bool isShieldHit = false; ShipControlModule shipControlModule; // Do we need to check for ship shield hits? if (shieldEffectsObjectPrefabID >= 0 && CheckShipHit(hitInfo, damageAmount, damageType, sourceShipId, sourceSquadronId, projectilePrefabID, out shipControlModule)) { isShieldHit = shipControlModule.shipInstance.HasActiveShield(hitInfo.point); } // No shield effects so perform a regular CheckShipHit else if (shieldEffectsObjectPrefabID < 0 && CheckShipHit(hitInfo, damageAmount, damageType, sourceShipId, sourceSquadronId, projectilePrefabID)) { // No need to do anything else here } else { // If it hit an object with a DamageReceiver script attached, take appropriate action like call a custom method CheckObjectHit(hitInfo, damageAmount, damageType, sourceShipId, sourceSquadronId, projectilePrefabID); } if (isShieldHit && shieldEffectsObjectPrefabID >= 0) { if (sscManager == null) { sscManager = SSCManager.GetOrCreateManager(); } if (sscManager != null) { InstantiateEffectsObjectParameters ieParms = new InstantiateEffectsObjectParameters { effectsObjectPrefabID = shieldEffectsObjectPrefabID, position = hitInfo.point + (hitInfo.normal * 0.0005f), rotation = Quaternion.LookRotation(-hitInfo.normal), }; // For projectiles we don't need to get the effectsObject key from ieParms. sscManager.InstantiateEffectsObject(ref ieParms); } } else if (!isShieldHit && effectsObjectPrefabID >= 0 && effectsObject != null) { if (sscManager == null) { sscManager = SSCManager.GetOrCreateManager(); } //SSCManager sscManager = SSCManager.GetOrCreateManager(); if (sscManager != null) { InstantiateEffectsObjectParameters ieParms = new InstantiateEffectsObjectParameters { effectsObjectPrefabID = effectsObjectPrefabID, position = hitInfo.point, rotation = transform.rotation }; // For projectiles we don't need to get the effectsObject key from ieParms. sscManager.InstantiateEffectsObject(ref ieParms); } } DestroyProjectile(); return true; } else { return false; } } /// /// Removes the projectile. How this is done depends on what system is being used (i.e. pooling etc.). /// When overriding, write your own logic then call base.DestroyProjectile(). /// protected virtual void DestroyProjectile() { if (usePooling) { if (isKinematicGuideToTarget) { ClearTarget(); } // Deactivate the projectile gameObject.SetActive(false); } else { // Destroy the projectile Destroy(gameObject); } } /// /// It MUST update the velocity and tranform.forward. Does not update the transform.position. /// FUTURE - be able to track other rigid bodies, not just Ships. /// FUTURE - be able to be guided towards a Location (which may not have a gameObject) /// To write your own version set sscManager.callbackProjectileMoveTo(..). /// protected void GuideToTarget() { if (isKinematicGuideToTarget && (isTargetShip || isTargetShipDamageRegion || targetGameObject != null)) { // Did the target become null without us knowing? if ((isTargetShip || isTargetShipDamageRegion) && targetShip == null) { ClearTarget(); } // If the target ship has been destroyed, stop targetting this ship else if ((isTargetShip || isTargetShipDamageRegion) && targetShip.shipInstance.Destroyed()) { ClearTarget(); } else { // Simple (rubbish) chase-style guidance //transform.LookAt(targetShip.transform); //velocity = transform.forward * velocity.magnitude; // Augmented Proportional Navigation Vector3 targetPosition = isTargetShip ? targetShip.shipInstance.TransformPosition : (isTargetShipDamageRegion ? targetShip.shipInstance.GetDamageRegionWSPosition(targetShipDamageRegion) : targetGameObject.transform.position); lastFrameLOSN = thisFrameLOSN; thisFrameLOSN = (targetPosition - transform.position).normalized; if (lastFrameLOSN.x != 0f || lastFrameLOSN.y != 0f || lastFrameLOSN.z != 0f) { Vector3 deltaLOS = thisFrameLOSN - lastFrameLOSN; // The rate at which the LOS angle is changing // When angleRateLOS is zero the missile is on a collision course with the target. float angleRateLOS = deltaLOS.magnitude; // Closing velocity is -deltaLOS. // Proportional Navigation v = NavConst * closing_velocity * angleRateLOS float apnBias = gravitationalAcceleration * Time.deltaTime * (NavConst * 0.5f); // NOTE: Final velocity should have the same magnitude (length) of the original velocity. Vector3 apn_acceleration = (thisFrameLOSN * (angleRateLOS * NavConst) + (deltaLOS * apnBias)).normalized; // Avoid forward = zero vector. if (apn_acceleration.x == 0f && apn_acceleration.y == 0f && apn_acceleration.z == 0f) { } else { // The initial rotation is defined by the transform.forwards direction // The target rotation is defined by the apn_acceleration direction // In this frame we are only allowed to turn by (guidedMaxTurnSpeed * Time.deltaTime) degrees transform.forward = Quaternion.RotateTowards(Quaternion.LookRotation(transform.forward), Quaternion.LookRotation(apn_acceleration), guidedMaxTurnSpeed * Time.deltaTime) * Vector3.forward; } velocity = transform.forward * velocity.magnitude; // Commanded Acceleration = NavConst * Vc * angleRateLOS + ( NavConst * Nt ) / 2 } } } } #endregion #region Private Methods /// /// [INTERNAL ONLY] /// Stop targeting anything. /// internal void ClearTarget() { targetShip = null; targetGameObject = null; targetShipDamageRegion = null; targetguidHash = 0; isTargetShip = false; isTargetShipDamageRegion = false; } /// /// [INTERNAL ONLY] - subject to change without notice. /// Currently does not support DOTS/ECS /// /// internal void SetTargetShip(ShipControlModule shipControlModule) { if (!useECS && isKinematicGuideToTarget) { targetShip = shipControlModule; isTargetShip = targetShip != null; if (isTargetShip) { targetGameObject = shipControlModule.gameObject; } else { targetGameObject = null; } } } /// /// [INTERNAL ONLY] - subject to change without notice. /// Currently does not support DOTS/ECS. /// If a ship is being targeted, call SetTargetShip(..) instead. /// /// internal void SetTarget(GameObject targetGameObj) { if (!useECS && isKinematicGuideToTarget) { targetGameObject = targetGameObj; // clear target ship targetShip = null; isTargetShip = false; } } #endregion #region Public Static Methods /// /// Determine if a ship has been hit. Apply damage as required. /// Can be called by both an instance of ProjectileModule and ProjectileSystem. /// /// /// /// /// /// /// /// public static bool CheckShipHit (RaycastHit raycastHitInfo, float projectileDamageAmount, DamageType projectileDamageType, int sourceShipId, int sourceShipSquadronId, int projectilePrefabID) { bool isHit = false; Rigidbody hitRigidbody = raycastHitInfo.rigidbody; if (hitRigidbody != null) { // Check if there is a ship control module attached to the hit object ShipControlModule shipControlModule; //if (shipControlModule != null) if (hitRigidbody.TryGetComponent(out shipControlModule)) { // Apply damage to the ship shipControlModule.shipInstance.ApplyNormalDamage(projectileDamageAmount, projectileDamageType, raycastHitInfo.point); // If required, call the custom method if (shipControlModule.callbackOnHit != null) { // Get a reference to the SSCManager SSCManager sscManager = SSCManager.GetOrCreateManager(); // Create a struct with the necessary parameters CallbackOnShipHitParameters callbackOnShipHitParameters = new CallbackOnShipHitParameters { hitInfo = raycastHitInfo, projectilePrefab = sscManager.GetProjectilePrefab(projectilePrefabID), beamPrefab = null, damageAmount = projectileDamageAmount, sourceSquadronId = sourceShipSquadronId, sourceShipId = sourceShipId }; // Call the custom callback shipControlModule.callbackOnHit(callbackOnShipHitParameters); } isHit = true; } } return isHit; } /// /// Determine if a ship has been hit. Apply damage as required. /// /// /// /// /// public static bool CheckShipHit (RaycastHit raycastHitInfo, float projectileDamageAmount, DamageType projectileDamageType, int sourceShipId, int sourceShipSquadronId, int projectilePrefabID, out ShipControlModule shipControlModule) { bool isHit = false; Rigidbody hitRigidbody = raycastHitInfo.rigidbody; if (hitRigidbody != null) { // Check if there is a ship control module attached to the hit object if (hitRigidbody.TryGetComponent(out shipControlModule)) { // Apply damage to the ship shipControlModule.shipInstance.ApplyNormalDamage(projectileDamageAmount, projectileDamageType, raycastHitInfo.point); // If required, call the custom method if (shipControlModule.callbackOnHit != null) { // Get a reference to the SSCManager SSCManager sscManager = SSCManager.GetOrCreateManager(); // Create a struct with the necessary parameters CallbackOnShipHitParameters callbackOnShipHitParameters = new CallbackOnShipHitParameters { hitInfo = raycastHitInfo, projectilePrefab = sscManager.GetProjectilePrefab(projectilePrefabID), beamPrefab = null, damageAmount = projectileDamageAmount, sourceSquadronId = sourceShipSquadronId, sourceShipId = sourceShipId }; // Call the custom callback shipControlModule.callbackOnHit(callbackOnShipHitParameters); } isHit = true; } } else { shipControlModule = null; } return isHit; } /// /// Determine if an object with a DamageReceiver component attached has been hit by a Projectile. /// TODO - check performance /// /// /// /// /// /// /// public static bool CheckObjectHit (RaycastHit raycastHitInfo, float projectileDamageAmount, DamageType projectileDamageType, int sourceShipId, int sourceShipSquadronId, int projectilePrefabID) { bool isHit = false; if (raycastHitInfo.collider != null) { DamageReceiver damageReceiver = raycastHitInfo.collider.GetComponent(); if (damageReceiver != null && damageReceiver.callbackOnHit != null) { // Get a reference to the SSCManager SSCManager sscManager = SSCManager.GetOrCreateManager(); // Create a struct with the necessary parameters CallbackOnObjectHitParameters callbackOnObjectHitParameters = new CallbackOnObjectHitParameters { hitInfo = raycastHitInfo, projectilePrefab = sscManager.GetProjectilePrefab(projectilePrefabID), beamPrefab = null, damageAmount = projectileDamageAmount, sourceSquadronId = sourceShipSquadronId }; // Call the custom callback damageReceiver.callbackOnHit(callbackOnObjectHitParameters); isHit = true; } } return isHit; } #endregion #region Public API Methods /// /// If /// /// public ShipControlModule GetTargetShip() { if (isKinematicGuideToTarget) { return targetShip; } else { return null; } } /// /// Re-enable a projectile after it has been disabled with /// DisableProjectile(). See also SSCManager.ResumeProjectiles() /// public void EnableProjectile() { isProjectileEnabled = true; } /// /// Useful when you want to pause the action in a game. /// Should always be called BEFORE setting Time.timeScale to 0. /// See also SSCManager.PauseProjectiles(). /// public void DisableProjectile() { isProjectileEnabled = false; } #endregion } }