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.
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.
Then, we need to add our component to it.
And we can’t forget about assigning our function from BallisticTrajectoryRenderer to the event in SimpleGrabSystem.
Result ?
Okay, let’s finally see the result of our work!
So let’s throw something and see if this is even close to the actual result.
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! ?
This was very useful for me in implementing War Thunder-like ballistic computer. Thanks!
The best trajectory tutorial ever. Super easy to follow and implement into my own project. Thank you 🙂
Any idea how this could be used with a gizmo? Would it work with Handles.DrawBezier?