We will be talking about primitive mathematical models for the path of objects on ballistic trajectories:
- How do we calculate the position of a ballistic trajectory at an arbitrary time?
- Given a known target position, how do we determine the launch angle to hit a target?
There will be ramblings, sample code, and a simple Unity WebGL demo.
Definition of ballistic
What exactly is something that’s moving ballistically?
ballistic
ADJECTIVE
Denoting or relating to the flight of projectiles after power has been cut off, moving under their own momentum and the external forces of gravity and air resistance
Collins Dictionary
Basically, it’s an object that’s passively flying through the air, and there aren’t any dynamic or active forces acting on it. By “active forces,” we’re referring to something powered, reactive, responsive, or with intent and agency. And the term “reactive” includes any type of collision response.
We might affiliate the word ballistic with violent movements and tools of war, such as artillery shells, ICBMs, and bullet paths.
Note that some of these things may initially be in active flight and then convert to passive flight – for example, missiles that launch upwards with powered flight and then descend ballistically.
But that’s not necessarily the case. An object in ballistic flight can be anything that passively flies through the air.
When analyzing a ballistic object in flight, we’ll consider constant acceleration forces such as gravity. Sometimes we’ll also consider additional things such as air drag and wind. But for this post, we’ll stick to a 2D scene that doesn’t consider wind or drag.
If we did consider wind, arguably, the wind could not change mid-flight, or that would be a dynamic force.
We’re also going to use the terms projectile and particle interchangeably. Basically, that will be the position of your “bullet.” What exactly do I mean by position? Is the position the origin of the model’s mesh in the scene? Or perhaps it’s the simulated object’s center of mass? Technically, it should be the object’s center of mass. Still, often you don’t need to be that theoretically correct for simple game stuff if things look correct. Although, keep in mind that physics engines (including Unity’s – via scripting) will allow you to author the center of mass on rigid bodies.
Simulation vs. Calculation
There are typically two ways we can figure out a projectile’s path – or where a projectile will be at a specific time. Either calculated from a simulation over many iterated steps or as an analytical (closed-form solution) calculation.
Simulation Integration
In a math and simulation context, the term “integration” pretty much means: to calculate something that’s been progressing up to a certain point. For a computer game or simulation, the typical strategy is to calculate a simulation that progresses in small increments of time. Usually no larger than a thirtieth of a second, but that time window can often be smaller. That is a form of integration, and we can do something similar with the intent of figuring out where a projectile will be (in the future) once it’s launched. But, the further in time we’re interested in, the more integration steps we have to perform.
If we handwave the process, it looks something like the rough (really rough) pseudocode below.
For as long as the game is running:
For a small portion of a second:
For (all) objects in the scene:
Determine object (rigidbody) accelerations.
Apply accelerations to calculate object velocities.
Apply velocities to object positions.
Detect and resolve object collisions.
This is being handwaved because we’re not going to cover doing this – but here’s a reference for doing such a thing in a way that’s tightly integrated into Unity. When doing this to predict trajectories, it can get computationally expensive: for each individual frame, it simulates many frames into the future.
The video’s comments section suggests it may also be expensive for other reasons, but for the reasons above, it already sounds costly.
But to that method’s credit, it can actually handle interactions with the scene – where what we’re going to cover is kinematic and cannot.
Classical Newtonian Physics Formula
The formula we’re interested in can be found on Wikipedia in the Equations of motion article.
\begin{align} v = at + v_0\\ r = r_0 + v_0t + \dfrac{1}{2}at^2\\ r = r_0 + \dfrac{1}{2}(v + v_0)t\\ v^2 = v^2_0 + 2a(r - r_0)\\ r = r_0 + vt - \dfrac{1}{2}at^2 \end{align}
\begin{align*} \text{where:}\\ r0 \text{ is the particle's initial position}\\ r \text{ is the particle's final position}\\ v0 \text{ is the particle's initial velocity}\\ v \text{ is the particle's final velocity}\\ a \text{ is the particle's acceleration}\\ t \text{ is the time interval} \end{align*}
I know, math, right? 😱😱😱 But we don’t need to get lost in all this; what we’re directly interested in is solving the variable r. There are a few different equations we can choose from to calculate r: formulas 2, 3, and 5. In this article, we’ll focus on formula 2.
r = r_0 + v_0t + \dfrac{1}{2}at^2\\
This formula can find where the particle would be at any time without having to calculate and accumulate incremental steps. But, it can’t collide with the environment – and assumes a completely ballistic trajectory. This means no powered flight in the middle, no collisions, etc.
Below is a C# Unity snippet of the code we’ll use to calculate the particle’s position. It’s mainly taking in descriptive names and then renaming them to make the formula more similar to what was on Wikipedia.
public static class TrajMath { ... /// <summary> /// Given a certain time (t), as well as some other variables, calculate /// where a ballistically launched particle will be. /// </summary> /// <param name="t">The time (in seconds) to calculate for.</param> /// <param name="shootVec"> /// The launch vector of the particle. Where X is left/right, and Y is vertical. /// This is a normalized vector and should have a magnitude of 1. /// </param> /// <param name="shooterY">The starting height of the particle.</param> /// <param name="power">The launch power (initial velocity) of the particle.</param> /// <param name="gravity">Vertical gravity.</param> public static Vector2 PredictLaunchAtTime(float t, Vector2 shootVec, float shooterY, float power, float gravity) { // https://en.wikipedia.org/wiki/Equations_of_motion float x = shootVec.x * power * t; float r0 = shooterY; float a = gravity; float vt = shootVec.y * power; float r = r0 + vt * t + 0.5f * a * t * t; return new Vector2(x, r); } }
The sample code only calculates the calculus for the height and just moves it linearly for the horizontal (x) value. If we wanted to apply the calculus to more than just the height, we’d need to apply the formula to calculate r separately for every component (i.e., x/y/z). But I’m only dealing with a 2D demo, and the horizontal acceleration is 0, so it ends up as x = r0 + v0t.
The horizontal acceleration is 0 because the default Unity gravity vector is 0 for the x and z component. And we maintain the assumption that it’s not modified.
Preview For Launching
From here, we can calculate a bunch of these points and create a predicted trajectory.
const int PrevSamplesPerSecond = 20; // Number of samples per second to show the trajectory preview for const int SecondsOfPreview = 10; // The number of seconds to show the trajectory preview for Vector3 [] samplesPoints; // Cached trajectory points public void RebuildTrajectory() { int totalSamples = PrevSamplesPerSecond * SecondsOfPreview; for (int i = 0; i < totalSamples; ++i) { float t = (float)i / (float)PrevSamplesPerSecond; Vector2 cur = TrajMath.PredictLaunchAtTime( t, new Vector2( this.shooter.transform.forward.z, this.shooter.transform.forward.y), this.shooter.transform.position.y, this.power, -Physics.gravity.y); this.samplesPoints[i] = new Vector3(0.0f, cur.y, cur.x); } // Push points into something that will render them as a line strip this.trajMesh.SetVertices(this.samplesPoints); }
Calculating Trajectory Angles
What if you have a game with gameplay based on trajectories, and your AI needs to solve for correct launch angles?
Perhaps you have an artillery game or another type of game where projectiles are shot in an arc. For example, an FPS where a bot has a grenade launcher or mortar. While players can guestimate how high to shoot, how would the AI figure out those angles? What we would need is a formula that would allow us to get a shooting angle based on the target’s (2D) position and the launch velocity of our weapon.
For that, we have the formula:
\theta = atan(v^2 \pm \dfrac{\sqrt{v^4+g(-gx^2+2yv^2)}}{-gx})\\ \ \\\text{Where } v \text{ is the magnitude of the launch velocity.}
Note the ± symbol. That means this function returns two answers. Actually, because of the square root, there are three possible answers:
- If the result inside the square root is negative, we don’t have a solvable answer. This indicates that velocity (v, or what we’re calling launch power) didn’t have enough power to launch the projectile to reach the target.
- If we calculate the ± using minus, this returns the angle where we can shoot directly at the target.
- If we calculate the ± using plus, this returns the angle where we can shoot higher than the target, and the particle will hit the target while falling down.
public static class TrajMath { /// <summary> /// Given a target offset and a launch strength, find the angles to fire /// </summary> /// <param name="power">The launch power of the particle.</param> /// <param name="gravity">Vertical gravity.</param> /// <param name="dist">Horizontal offset of the target from the shooter.</param> /// <param name="above">Vertical offset of the target from the shooter.</param> /// <param name="usePos">Whether to return the positive or negative value.</param> /// <returns> /// The angle, in degrees, to shoot a particle from the shooter to hit the target. /// /// An error value of NaN is returned if the power is insufficient. /// </returns> public static float GetShootingAngle(float power, float gravity, float dist, float above, bool usePos) { float v = power; float v2 = v * v; float v4 = v2 * v2; float g = gravity; float x = dist; float y = above; float insideSqrt = v4 - g * (g * x * x + 2 * y * v2); // If the insideSqrt is less than 0, we're going to end up attempting // to get the sqrt of a negative number. This happens if the launch // power isn't enough to get us to our target. In which case there is // no solution. if (insideSqrt < 0.0f) return float.NaN; if (usePos == true) { float tanthP = (v2 + Mathf.Sqrt(insideSqrt)) / (g * x); return Mathf.Rad2Deg * Mathf.Atan(tanthP); } else { float tanthN = (v2 - Mathf.Sqrt(insideSqrt)) / (g * x); return Mathf.Rad2Deg * Mathf.Atan(tanthN); } } ... }
Demo
The demo can be accessed from GitHub.
Controls are listed below, but here are a few initial notes:
- There are no camera controls for this demo.
- Nothing happens if you hit the target. It’s simply a visual reference.
Controls
- Target
- Distance: Modify the horizontal distance of the target.
- Elevation: Modify the height of the target.
- Angle: Modify the launch angle. Disabled if an Automatch is enabled.
- Match: Press to calculate an angle to hit the target. Press multiple times to toggle between the plus and minus solutions. Disabled if an Automatch is enabled.
- Automatch[-]: Toggles automatically calculating the angle each frame for the minus solution. If enabled, reselect to disable.
- Automatch[+]: Toggles automatically calculating the angle each frame for the plus solution. If enabled, reselect to disable.
- Power: Adjust the launch power.
- Shoot: Shoot a box.
The boxes shot by the cannon will use Unity’s rigid bodies and physics system. When they’re spawned, they’re set to use the launch velocity and angle. In theory, the Newtonian formula should ideally be more accurate, but it should be similar enough to the trajectory preview that it’s practically the same.
Plus, the Unity physics simulation IS the representation of the game world, so it doesn’t matter what’s theoretically more accurate since the physics engine is the game’s source of truth.
Unity WebGL demo compiled with Unity 2018.4.32f1
Tested with Chrome 109.0.5414.120