Drawing ballistic trajectory in Unity

Whenever you want to throw an object in your game, you might want to add an expected trajectory for that object. But do you know how to draw a ballistic trajectory in Unity?

Well, I had to do a little bit of research to find out how. Luckily, you won’t have to do it. πŸ˜‰

Let’s dive into drawing ballistic trajectory!

Materials πŸ“š

To even start, we have to understand how projectiles move. You can read about that on the Wikipedia in the article Projectile Motion.

Long story short is that ballistic trajectory has a shape similar to parabola. Of course, there is more to it than just that.

When drawing the trajectory of the projectile, we have to consider many variables – velocity, drag, friction, angle of the throw, and more.

When I was looking for some guidance, I often found solutions for drawing some simple trajectories. These wouldn’t work for me, as I only have a start position and initial velocity. And I wanted to stay with that.

I finally came across this article: “Make a realistic bullets in Unity with C#“. This article had a missing piece, which I was looking for… 🀩

Prep work πŸ› 

In my last article, “How to Pick Up Items in Unity” I made a little demo, which we can also use here!

But we will need to extend SimpleGrabSystem class with an event to spread information about the current position of the player.

using UnityEngine;
using UnityEngine.Events;

// Source: https://www.patrykgalach.com/2020/03/16/pick-up-items-in-unity/

/// <summary>
/// Simple example of Grabbing system.
/// </summary>
public class SimpleGrabSystem : MonoBehaviour
{
    ...

    [Header("Throw")]
    // Velocity which which object will be thrown.
    [SerializeField]
    private Vector3 throwVelocity = new Vector3(0, 0, 5);

    /// <summary>
    /// Event class which will be displayed in the inspector.
    /// </summary>
    [System.Serializable]
    public class LocationChanged : UnityEvent<Vector3, Vector3> { }

    [Space]

    // Event for location change. Used to update ballistic trajectory.
    public LocationChanged OnLocationChanged;

    /// <summary>
    /// Method called every frame.
    /// </summary>
    private void Update()
    {
        ...

        // Broadcast location change
        OnLocationChanged?.Invoke(slot.position, slot.rotation * throwVelocity);
    }

    ...

    /// <summary>
    /// Method for dropping item.
    /// </summary>
    /// <param name="item">Item.</param>
    private void DropItem(PickableItem item)
    {
        ...

        // Add velocity to throw the item
        item.Rb.velocity = slot.rotation * throwVelocity;
    }
}

With that done, now we can see the event in the Inspector.

Added throw velocity and location event.

Ballistic Trajectory πŸ”«

Now, it’s time to create our BallisticTrajectoryRenderer script!

We will need a few things to start. First are variables that we will use to draw our trajectory.

using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// Ballistic trajectory renderer.
/// </summary>
[RequireComponent(typeof(LineRenderer))]
public class BallisticTrajectoryRenderer : MonoBehaviour
{
    // Reference to the line renderer
    private LineRenderer line;

    // Initial trajectory position
    [SerializeField]
    private Vector3 startPosition;

    // Initial trajectory velocity
    [SerializeField]
    private Vector3 startVelocity;

    // Step distance for the trajectory
    [SerializeField]
    private float trajectoryVertDist = 0.25f;

    // Max length of the trajectory
    [SerializeField]
    private float maxCurveLength = 5;

    [Header("Debug")]
    // Flag for always drawing trajectory
    [SerializeField]
    private bool _debugAlwaysDrawTrajectory = false;

    
}

Next, we need methods in our script to initialize and draw a ballistic trajectory.

using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// Ballistic trajectory renderer.
/// </summary>
[RequireComponent(typeof(LineRenderer))]
public class BallisticTrajectoryRenderer : MonoBehaviour
{
    ...

    /// <summary>
    /// Method called on initialization.
    /// </summary>
    private void Awake()
    {
        // Get line renderer reference
        line = GetComponent<LineRenderer>();
    }

    /// <summary>
    /// Method called on every frame.
    /// </summary>
    private void Update()
    {
        // Draw trajectory while pressing button
        if (Input.GetButton("Fire2") || _debugAlwaysDrawTrajectory)
        {
            // Draw trajectory
            DrawTrajectory();
        }

        // Clear trajectory after releasing button
        if (Input.GetButtonUp("Fire2") && !_debugAlwaysDrawTrajectory)
        {
            // Clear trajectory
            ClearTrajectory();
        }
    }

    /// <summary>
    /// Sets ballistic values for trajectory.
    /// </summary>
    /// <param name="startPosition">Start position.</param>
    /// <param name="startVelocity">Start velocity.</param>
    public void SetBallisticValues(Vector3 startPosition, Vector3 startVelocity)
    {
        this.startPosition = startPosition;
        this.startVelocity = startVelocity;
    }

    /// <summary>
    /// Draws the trajectory with line renderer.
    /// </summary>
    private void DrawTrajectory()
    {
        
    }

    /// <summary>
    /// Clears the trajectory.
    /// </summary>
    private void ClearTrajectory()
    {
        
    }
}

Before we draw our trajectory, let me explain how it’s going to work.

Our script has an initial position and velocity. We will use these values to basically make a step by step calculation of the trajectory. We also defined variable (trajectoryVertDist) which stores how big these steps will be.

So we start at startPosition with startVelocity and we loop as long as we won’t hit something or our trajectory is too long. With each loop step, we modify our position and velocity to take gravity into consideration. A new position is also added to the list of points to draw trajectory later.

When our trajectory hits, something loop is finished, and the last point is added to the list. If our trajectory didn’t hit anything, we are still fine. This condition is added to prevent our loop from running indefinitely.

Now, we just need to assign trajectory points to the line renderer to display it.

So now, you can take a look at the code.

using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// Ballistic trajectory renderer.
/// </summary>
[RequireComponent(typeof(LineRenderer))]
public class BallisticTrajectoryRenderer : MonoBehaviour
{
    ...

    /// <summary>
    /// Draws the trajectory with line renderer.
    /// </summary>
    private void DrawTrajectory()
    {
        // Create a list of trajectory points
        var curvePoints = new List<Vector3>();
        curvePoints.Add(startPosition);

        // Initial values for trajectory
        var currentPosition = startPosition;
        var currentVelocity = startVelocity;

        // Init physics variables
        RaycastHit hit;
        Ray ray = new Ray(currentPosition, currentVelocity.normalized);

        // Loop until hit something or distance is too great
        while (!Physics.Raycast(ray, out hit, trajectoryVertDist) && Vector3.Distance(startPosition, currentPosition) < maxCurveLength)
        {
            // Time to travel distance of trajectoryVertDist
            var t = trajectoryVertDist / currentVelocity.magnitude;

            // Update position and velocity
            currentVelocity = currentVelocity + t * Physics.gravity;
            currentPosition = currentPosition + t * currentVelocity;

            // Add point to the trajectory
            curvePoints.Add(currentPosition);

            // Create new ray
            ray = new Ray(currentPosition, currentVelocity.normalized);
        }

        // If something was hit, add last point there
        if (hit.transform)
        {
            curvePoints.Add(hit.point);
        }

        // Display line with all points
        line.positionCount = curvePoints.Count;
        line.SetPositions(curvePoints.ToArray());
    }

    /// <summary>
    /// Clears the trajectory.
    /// </summary>
    private void ClearTrajectory()
    {
        // Hide line
        line.positionCount = 0;
    }
}

Configure a new system βš™οΈ

The only thing left to do is to configure new components in the scene.

First, we need to add a new child to the player.

New child added to the player

Then, we need to add our component to it.

BallisticTrajectoryRenderer component
BallisticTrajectoryRenderer component

And we can’t forget about assigning our function from BallisticTrajectoryRenderer to the event in SimpleGrabSystem.

Assign SetBallisticValues method to the event
Assign SetBallisticValues method to the event

Result πŸ†

Okay, let’s finally see the result of our work!

Preview of the trajectory in the scene

So let’s throw something and see if this is even close to the actual result.

Let’s throw something

Well, it’s closer than I expected it to be! πŸ˜„

So what do you think about it? Let me know in the comment section below!

And share it with your friends! I would really appreciate that! πŸ₯°

You can check the whole project at my public repository. πŸ”—

And if you are interested in getting emails when I release a new post, sign up for the newsletter!

Hope to see you next time! πŸ€“

0 0 vote
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments