July 2nd, 2013 by Steff Kelsey
This post discusses targeting projectiles with Unity3D. The main applications of targeting projectiles using physics are sports simulations (basketball, golf, etc) and anything where you want to launch something into a world with gravity and have it hit the desired target.
Unity3D comes packaged with a really nice physics engine. A lot of the heavy lifting is done for you just by opening up the application. But to have control of objects in the Unity3D world, it helps to have a knowledge of physics. In this case, simple equations of motion are all that is really needed (nothing crazy). The Kinematic Equations of Linear Motion (for Rigid Bodies) are:
You may have noted that the equations are setup to describe motion on the y-axis. This is no accident and the reasons will be made clear in a little while. The first equation is the most familiar to anyone with some high school physics. It describes the displacement between two points in a trajectory at any time, t, by the initial velocity and any acceleration due to forces. The second equation describes the final velocity at any time, t, by the initial velocity, acceleration due to forces, and the displacement. To keep things simple, we are only going to worry about the force of gravity, neglecting air friction and wind forces. If we needed more realism in our simulation, we could add the acceleration due to those forces, but that is material for another blog post (in other words, if enough people ask about it, you will see it in the possible near future).
The 3 equations that we will be dealing with:
g is the acceleration due to gravity
t is the time
v is the velocity
y is the position
Making the Math Work for Us
One of the constant stumbling blocks between knowing the physics and coding the simulations is understanding how to apply the equations. Don’t worry about memorizing anything you can find on the internet (like the kinematic equations of motion), but learning how to break a problem into steps that can be solved by the equations is the real skill to focus on. First, we are going to analyze our problem in the form of a Unity scene. To demonstrate the problem, I pulled together some assets from the Google 3D Warehouse (you rock, SketchUp! is a great place to find free models to use in demos like this or when you’re proving out a scene and don’t want to waste time creating placeholder art while you’re waiting for final assets from the art guys) and created a little basketball scene. We have a ball, a court floor, and a nice stand holding up a backboard and rim. I know, no net! But I didn’t want to get into interactive cloth in this demo and just focus on the ball going into the hoop. Our problem is this: we want to be able to launch the ball from any position on the court and have it go through the hoop.
The first step we can take to simplify the problem is to remove a dimension. We currently have the Vector3 position of the ball and the Vector3 position of the target (an empty GameObject places in the center of the hoop), but if we move the ball to be directly to the left of the hoop, we can simplify the equations of motion to describe motion along the y-axis and motion along the x-axis. Don’t worry, we can pick that z-axis back up later. Fig 1 shows the possible trajectory that we need to describe.
As you can see, we have reduced the problem down to 2 dimensions. But how to apply the equations of motion? The four equations can accurately describe the trajectory in Figure 1, but isolating the variables so we can solve for something as a function of time will be a good amount of work using the given initial and final points. We would probably have to use calculus (gasp!). Can it be done? Yes. But if you don’t want to bust out the big guns (and don’t want root around your office looking for a calculus calculator or differential equations book), then we can analyze the motion and apply some constraints to simplify things even further. I like the power of calculus and diffy-q (as we nerds like to call differential equations), but we don’t actually need either to solve this. Save those tools for the zombie attack.
To simplify, I like to analyze the motion along each axis and find places where I can assume that an unknown is equal to 0. Let’s look at our knowns and unknowns first.
We know our locations of the ball and the target at all times because Unity gives them to us. We know the start time of our flight because we can just call it 0 (we have that power). We know gravity because we can get and set it right in Unity using Physics. Gravity (default is (0, -9.81, 0) where the units are m/s2). What we don’t know are the initial and final velocities for the x or the y-direction. Looking at your figure, can we find a point in time on the trajectory where we can assume that either v(t)xor v(t)yis 0?
Looking at motion in the x-direction first, the ball will be given an initial velocity toward the basket, it has no forces acting on it for it’s entire flight (because we have no drag in our world), and it will finish with the same velocity at the end. So vxi=vxf! Does that help us… not really.
Looking at the motion described in the y-direction, we will have an initial velocity upwards and then gravity will be acting on the ball through the entire flight. The ball will travel upwards, switch direction as gravity pulls it down and then will finish at the target location. And what happens when there is a change in the direction of velocity in a trajectory? The y-velocity at the very top of the path is zero! Now, we can break up the problem into two trajectories and then solve for each to find our initial velocities. Fig 2 shows how to divide it up.
Now, we can solve for some stuff! First off, we need to find the initial velocity in the y-direction. knowing only positions and gravity:
We still need to find the horizontal component of the initial velocity. Before looking at the equations in terms of x, we can solve for one more unknown: the total flight time, t. Armed with the initial velocity in the y-direction, we can find the flight time for each step that we broke out above and then add them up. First, the time it takes the ball to travel from the start point to the top of the arc.
Next, the time it takes the ball to travel from the top of the arc back down to the target.
Adding it up:
What is great about the total flight time from point 0 to point 2, is that it is the total flight time no matter which component of motion you’re analyzing! We can use it to find the initial velocity in the x-direction. (Remember, there are no accelerations due to force along the x-axis).
Now, we have the initial velocity in both the horizontal and vertical direction solved in terms of our knowns, it is time to go into Unity and apply this all using C#. The exercise we did above is process I do before I start coding most things. Sketch things out on paper and understand the math before you go into Unity. Once you’re into Unity, you can use the same process.
Steps to Implement in Unity
find the initial position and the target position
reduce the motion to 2 dimensions
set the maximum height of the ball
find the initial velocity in the y-direction
find the initial velocity along the horizontal
find the initial velocity in the x and z directions
Below is our findInitialVelocities function. The parameters of the function are the start position, final position, maxHeightOffset, and rangeOffset. The two offset parameters are specific to the problem of basketball targeting. We’re not just trying to hit a target; we’re trying to make sure the ball goes through a hoop from above. The max height offset is the amount to add to the target y-position to make sure the ball clears the hoop before falling into it (for short range shots). We can’t just set an initial launch angle because it won’t work for every spot on the court (close range needs a very high launch angle and then we move toward 45 degrees the farther we get from the target). The range offset is to make each shot trend toward the back of the rim. The highest percentage shooters all aim for just hitting the back of the rim. Aiming for that spot decreases the risk of hitting the front of the rim. This is good practice even in a simulation, since there will be rounding of floating point values. We want to guarantee that the ball always goes through the hoop.
* Finds the initial velocity of a projectile given the initial positions and some offsets
* @param Vector3 startPosition - the starting position of the projectile
* @param Vector3 finalPosition - the position that we want to hit
* @param float maxHeightOffset (default=0.6f) - the amount we want to add to the height for short range shots. We need enough clearance so the
* ball will be able to get over the rim before dropping into the target position
* @param float rangeOffset (default=0.11f) - the amount to add to the range to increase the chances that the ball will go through the rim
* @return Vector3 - the initial velocity of the ball to make it hit the target under the current gravity force.
private Vector3 findInitialVelocity(Vector3 startPosition, Vector3 finalPosition, float maxHeightOffset = 0.6f, float rangeOffset = 0.11f)
// get our return value ready. Default to (0f, 0f, 0f)
Vector3 newVel = new Vector3();
// Find the direction vector without the y-component
Vector3 direction = new Vector3(finalPosition.x, 0f, finalPosition.z) - new Vector3(startPosition.x, 0f, startPosition.z);
// Find the distance between the two points (without the y-component)
float range = direction.magnitude;
// Add a little bit to the range so that the ball is aiming at hitting the back of the rim.
// Back of the rim shots have a better chance of going in.
// This accounts for any rounding errors that might make a shot miss (when we don't want it to).
range += rangeOffset;
// Find unit direction of motion without the y component
Vector3 unitDirection = direction.normalized;
// Find the max height
// Start at a reasonable height above the hoop, so short range shots will have enough clearance to go in the basket
// without hitting the front of the rim on the way up or down.
float maxYPos = targetPosition.y + maxHeightOffset;
// check if the range is far enough away where the shot may have flattened out enough to hit the front of the rim
// if it has, switch the height to match a 45 degree launch angle
if (range / 2f > maxYPos)
maxYPos = range / 2f;
// find the initial velocity in y direction
newVel.y = Mathf.Sqrt(-2.0f * Physics.gravity.y * (maxYPos - startPosition.y));
// find the total time by adding up the parts of the trajectory
// time to reach the max
float timeToMax = Mathf.Sqrt(-2.0f * (maxYPos - startPosition.y) / Physics.gravity.y);
// time to return to y-target
float timeToTargetY = Mathf.Sqrt(-2.0f * (maxYPos - finalPosition.y) / Physics.gravity.y);
// add them up to find the total flight time
float totalFlightTime = timeToMax + timeToTargetY;
// find the magnitude of the initial velocity in the xz direction
float horizontalVelocityMagnitude = range / totalFlightTime;
// use the unit direction to find the x and z components of initial velocity
newVel.x = horizontalVelocityMagnitude * unitDirection.x;
newVel.z = horizontalVelocityMagnitude * unitDirection.z;
From the top, we find the direction vector from the start position to the final position excluding the y-component. We exclude the y-component because this is how we simplify the problem from 3 dimensions to 2. We find the range next (the equivalent of x2-x0or total change in x in our work above). The rangeOffset is added at this point to make sure the ball is aimed more toward the back of the rim than the exact center of the hoop. The next step is very important for getting our final velocities back into 3 dimensional space: we find the unit direction along the xz plane.
Next we have our max height calculations. We use the maxHeightOffset parameter and add that to the target height to get our close range arcs. As we back off from the hoop, we set the max height to trend toward a 45 degree launch angle. Why 45 degrees? Research has revealed that the most consistent basketball shooters at any level all shoot with the same launch angle around the mid 40s (http://tinyurl.com/2djrtt9).
Now that the knowns have been defined, we use our equations from above to solve for our unknowns. First, we solve for the initial velocity in the y-direction. Note that we use the Unity3d gravity found in Physics.gravity.y. Next, we find the total flight time by finding the time to the max y-position from the start and the time from the max y position to the end. The final unknown to solve for is the magnitude initial velocity along the horizontal and we find it with the range and the total flight time.
The last step is to convert the horizontal velocity to x and z components so the ball will have the correct trajectory for a 3d simulation. We use the unit direction along the xz plane that we had tucked away. Multiply each component of the unit vector by the magnitude and we have all of the initial velocities.
After doing all that work, it’s fun to play around a little bit. Move the ball around and make sure it goes through the hoop no matter how far away you get. Play with the magnitude of the gravity force. If you increase the magnitude, the ball ends up traveling faster to the target. It is important to get a feel for how things change your simulation. A better understanding of the physics will give you much more control in all your projects with this type of engine driving the motion.
In this demo, we learned how to simplify a problem into a couple of steps that can be solved using the equations of motion without using math more advanced than basic algebra. We learned that Unity is a powerful engine that can simulate motion in a 3d space and how to control motion in that simulation by applying a little knowledge of physics. From this starting place, we can describe more complex motion. We can add air resistance, wind forces, gravity from other rigid bodies, and even magnetic forces. We could take it to the ground and add friction forces to calculate the distance a golf ball will travel in a putt. Or a pool ball across a felt table top. And the projectile will always travel to our target point!