Homebound / Scripts / ProjectileLauncher.cs
ProjectileLauncher.cs
Raw
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);
    }
}