using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using UnityEngine; public class ProjectileLauncher : MonoBehaviour { [SerializeField] List<GameObject> potential_Projectiles; // List of potential projectiles that can be spawned to launch [Header("Locations")] [SerializeField] Transform origin_Point; // Point where the player doesn't see the object return [SerializeField] Transform ready_Point; // Point where the object prepares to fire [SerializeField] Transform destination_Point; // Point where the object is aimed towards [Header("Values")] [SerializeField] float speedToReady = 20.0f; // Speed of the projectile approaching the ready point [SerializeField] float speedUpMultiplier = 3.0f; // Speeds up the object along the path [SerializeField][Tooltip("In Milliseconds. Low numbers means almost instantaneous launch.")] int launch_Delay = 5; // Delay before object is launched [Header("Gizmo")] [SerializeField] bool disableLineTest = false; [SerializeField] LineRenderer lineComp; [SerializeField] float step; // General GameObject Components and References GameObject gameObjectInstance; // Instance of the game object bool wasPaused = false; // Indicator/trigger for when the launches are paused to properly resume the async calls in the function Rigidbody rb; // Objects rigidbody component // Threading Components CancellationTokenSource cancellationToken; // Cancellation token reference that is used when the call is made Task task; // Task reference to make sure only one sequence of the launch is occurring per script reference // Position components to be gathered for launching the object and drawing the path Vector3 direction; // Primary direction component Vector3 groundPosition; // Primary position based on x and z components in the world Vector3 targetPosition; // Primary target position based on the x and y components in the world float height; void Start() { cancellationToken = new CancellationTokenSource(); // Gather certain parameters each frame, specifically the target position and direction direction = destination_Point.localPosition - ready_Point.localPosition; groundPosition = new Vector3(direction.x, 0.0f, direction.z); targetPosition = new Vector3(groundPosition.magnitude, direction.y, 0.0f); height = targetPosition.y + targetPosition.magnitude / 2.0f; height = Mathf.Max(destination_Point.localPosition.y, height); } async void Update() { // Calculates necessary parameters to pass to the drawing and projectile motion functions CalculatePathWithHeight(targetPosition, height, out float initVel, out float angle, out float timeParam); if (!disableLineTest) DrawPath(groundPosition.normalized, initVel, timeParam, angle); if (MainMenu.IsPaused && !wasPaused) { PauseLaunches(); wasPaused = true; } else if (!MainMenu.IsPaused && wasPaused) { RestartLaunches(); wasPaused = false; } else if (!MainMenu.IsPaused && task == null && !wasPaused) { try { // Debug.LogWarning("Task is beginning"); task = LaunchObjectAsync(cancellationToken.Token, groundPosition.normalized, timeParam, angle, initVel); await task; } catch (TaskCanceledException ex) { throw new Exception("The launch at object -" + gameObject.name + "- has been canceled.", ex); } catch (Exception ex) when (ex is MissingReferenceException) { #if UNITY_EDITOR // Catches the Missing reference exceptions that are thrown when stopping play mode or when reloading the level #endif } } } /// <summary> /// Sends the cancellation token to the tasks available to halt the processes /// </summary> void PauseLaunches() { // Debug.LogWarning("Sending signal to cancellation token!"); cancellationToken.Cancel(); cancellationToken.Dispose(); } /// <summary> /// Begins the launch sequences again /// </summary> void RestartLaunches() => cancellationToken = new CancellationTokenSource(); /// <summary> /// Asynchronous function that calls through the functions of readying the projectile and then launching it towards the designated point /// </summary> /// <param name="token"></param> /// <returns>Attempts to return task, but will never do so as it infinitely calls for the objects cycling back to its start and firing points</returns> /// <exception cref="TaskCanceledException"></exception> async Task LaunchObjectAsync(CancellationToken token, Vector3 direction, float timeParam, float angle, float initVel) { while (!token.IsCancellationRequested) // When token sends cancel request loop will break { // Creates game object instance from list of objects available and collects rigidbody from instance if (gameObjectInstance == null) { gameObjectInstance = Instantiate(potential_Projectiles[UnityEngine.Random.Range(0, potential_Projectiles.Count)], origin_Point.position, Quaternion.identity, this.transform); rb = gameObjectInstance.GetComponent<Rigidbody>(); } // Retrieves the target direction from the ready point to the destination point as the instance moves to the ready point await ReachedPoint(token, direction); // Debug.LogWarning("TaskPointVar has been acquired --> " + taskPointVar); await Task.Delay(launch_Delay); // Delays launch for player to have time to react // Retrieves signal for when the object fired has reached a particular point and time has passed after the fact bool signal = await FireObject(token, direction, timeParam, angle, initVel); if (signal) // Destroys the object instance and nullifies the object and rigidbody before a new one is created { // Debug.LogWarning("FireObject has been successfully completed, destroying current iteration now."); DestroyImmediate(gameObjectInstance); gameObjectInstance = null; rb = null; } else { // TODO ---> Implement object freezing Debug.LogError("Tasks were not completed, cancellation was called, destroying renegade elements."); DestroyImmediate(gameObjectInstance); gameObjectInstance = null; rb = null; } } if (token.IsCancellationRequested) throw new TaskCanceledException(); } /// <summary> /// Awaits until the objects current position has reached the target position and then returns true /// </summary> /// <param name="token"></param> /// <param name="location"></param> /// <returns>Signal that the point has been reached</returns> /// <exception cref="TaskCanceledException"></exception> async Task<bool> ReachedPoint(CancellationToken token, Vector3 direction) { gameObjectInstance.transform.rotation = Quaternion.LookRotation(direction); // While the game object's position does not equal the target position this thread will continually move the object // towards the target position until it reaches the correct position while (gameObjectInstance.transform.localPosition != ready_Point.localPosition) { gameObjectInstance.transform.localPosition = Vector3.MoveTowards(gameObjectInstance.transform.localPosition, ready_Point.localPosition, speedToReady * Time.deltaTime); // Debug.Log("Awaiting object to get to Target Position -> " + targetPosition); if (token.IsCancellationRequested) throw new TaskCanceledException(); await Task.Yield(); } return true; } /// <summary> /// Launches the designated object in the target direction until it is lower than the y component of the game object transform reference in the scene /// then the function will return true /// </summary> /// <param name="token"></param> /// <param name="location"></param> /// <param name="direction"></param> /// <returns>Returns true when the projectile reaches below the position of the designated y component of the destination point</returns> /// <exception cref="TaskCanceledException"></exception> async Task<bool> FireObject(CancellationToken token, Vector3 direction, float timeParam, float angle, float initVel) { rb.useGravity = true; // Switches gravity on after bringing the object to the ready point try { await ProjectileMotion(token, direction, initVel, angle, timeParam); // Object is fired } catch (Exception ex) when (ex is TaskCanceledException) { Debug.LogWarning("Cancellation Request made while calculating projectile motion... stopping calculations"); return false; } await Task.Delay(1000); // Delay until the object is out of view of the player screen if (token.IsCancellationRequested) throw new TaskCanceledException(); // Debug.LogWarning("Object has reached its target destination"); return true; } /// <summary> /// Main function that shoots the projectile along the calculated path /// </summary> /// <param name="token"></param> /// <param name="direction"></param> /// <param name="initVel"></param> /// <param name="angle"></param> /// <param name="tTime"></param> /// <returns>Call that the projectile has carried out along its full path, otherwise false if the task was cancelled for any reason</returns> /// <exception cref="TaskCanceledException"></exception> async Task<bool> ProjectileMotion(CancellationToken token, Vector3 direction, float initVel, float angle, float tTime) { float t = 0.0f; try { while (t < tTime) { // Horizontal Displacement float dis_X = initVel * t * Mathf.Cos(angle); // Vertical Displacement float dis_Y = initVel * t * Mathf.Sin(angle) - 0.5f * -Physics.gravity.y * Mathf.Pow(t, 2); // Position of the object is incremented each while loop after displacement calculations gameObjectInstance.transform.localPosition = ready_Point.localPosition + direction * dis_X + Vector3.up * dis_Y; t += Time.deltaTime * speedUpMultiplier; await Task.Yield(); } } catch (Exception ex) when (ex is TaskCanceledException) { return false; } return true; } /// <summary> /// Calculates parameters for DrawPath and ProjectileMotion Functions such as the initVel, angle, and timeParams /// </summary> /// <param name="targetPosition"></param> /// <param name="height"></param> /// <param name="initVel"></param> /// <param name="angle"></param> /// <param name="time"></param> void CalculatePathWithHeight(Vector3 targetPosition, float height, out float initVel, out float angle, out float time) { float xt = targetPosition.x; float yt = targetPosition.y; float g = -Physics.gravity.y; float b = Mathf.Sqrt(2 * g * height); float a = (-0.5f * g); float c = -yt; float tPlus = QuadraticEquation(a, b, c, 1); float tMinus = QuadraticEquation(a, b, c, -1); time = tPlus > tMinus ? tPlus : tMinus; angle = Mathf.Atan(b * time / xt); initVel = b / Mathf.Sin(angle); } /// <summary> /// Basic Quadratic Equation function /// </summary> /// <param name="a"></param> /// <param name="b"></param> /// <param name="c"></param> /// <param name="sign"></param> /// <returns>Solution to equation parameters</returns> float QuadraticEquation(float a, float b, float c, float sign) => (-b + sign* Mathf.Sqrt(b* b - 4 * a* c)) / (2 * a); /// <summary> /// Draws the path through the line component attached to this component based on the direction parameter passed /// </summary> /// <param name="direction"></param> /// <param name="initVel"></param> /// <param name="time"></param> /// <param name="angle"></param> void DrawPath(Vector3 direction, float initVel, float time, float angle) { step = Mathf.Max(0.01f, step); // Clamp between 0.01f for position calculations of the line and the step parameter gathered from the Unity Editor // Creates steps in the line lineComp.positionCount = (int)(time / step) + 2; int count = 0; // Draws the beginning to second to last steps of the line for (float i = 0; i < time; i += step) { float x = initVel * i * Mathf.Cos(angle); float y = initVel * i * Mathf.Sin(angle) - 0.5f * - Physics.gravity.y * Mathf.Pow(i, 2); lineComp.SetPosition(count, ready_Point.localPosition + direction * x + Vector3.up * y); count++; } // Draws the last step of the line float xFinal = initVel * time * Mathf.Cos(angle); float yFinal = initVel * time * Mathf.Sin(angle) - 0.5f * -Physics.gravity.y * Mathf.Pow(time, 2); lineComp.SetPosition(count, ready_Point.localPosition + direction * xFinal + Vector3.up * yFinal); } }