2D Physics: Analytically Targeted Rigidbody Projectiles


I frequently get asked when I exhibit Super Rude Bear Resurrection how I managed to get Unity’s physics to actually feel good for a platformer, and I’ve been asked for help on that note. I’m concentrating on development so I don’t really have time at the moment to go back to square one and walk through building everything from a character controller up, but I’ve decided henceforth to document everything physicsy as I continue development (which there should be a lot of, especially with all the planned boss battles).

I’m going to do a pretty quick tip this time, but I’ll walk you through it carefully rather than just dumping you with a formula.

So, here’s today’s conundrum:

Rude Bear Projectiles

I’m just planning the iceworld levels of SRBR, and one of the first obstacles I wanted to add were snowman who lob snowballs at you.

However, I wanted the snowballs to:

  • Travel in an arc (obviously)
  • Always be projected at the same velocity (or a variable velocity that I can regulate)
  • Be solved analytically immediately instead of computationally
  • Always hit you provided there’s a clear path and you stay still

So, if we’re travelling in an arc we’re obviously just doing normal projection dynamics. We’re going to take the typical physicist “ignore air resistance” attitude in this case even though that’s not true for most things in SRBR, because it would make for an insanely complicated problem, and I want something pretty.

If we’re defining the velocity then the game will have to solve for the angle on its own. Now, because we’re not using air resistance, we have linear acceleration, so we can use SUVAT equations, which you may or may not be familiar with, but they’re a very useful thing to be intimately familiar with.

I’m going to briefly explain SUVAT so feel free to glaze over this part if you’re already familiar with it.

SUVAT Overview

If you aren’t aware, SUVAT stands for the 5 variables of linear acceleration: Displacement (from the Latin spatium), Initial velocity, Final velocity, Acceleration and Time, and there’s a set of 5 equations that go with this. Each one of them uses 4 of the 5 variables. So if you know any 3 variables and want to know a fourth, you can pick an equation and plug in the numbers to get the solution.

e.g. for

$latex v=u+at$

If you want to know the velocity of an object that starts moving at 3 tiles per second, after 5 seconds, given that it accelerates at 1.5 tile per second squared, you get:

$latex v = 3 + 5 \times 1.5$

Which is 10.5.

So, we only ever need 3 variables if we only want to find 1. Usually. However, we want to find the angle, so this is going to be slightly more complicated.

Now, the only equation we’re concerned with here is:

$latex s = ut + at^2/2$

Because we know our displacement, acceleration and initial velocity, and thanks to how spacetime works, time in the x direction and time in the y direction are the same. (This is not actually true thanks to special relativity but thankfully we’re not working at relativistic speeds).

Now, displacement s and velocity u & v are both vector values, so they have a direction as well as a magnitude (which is the difference between displacement and distance).

What this means is.. we can break this into two equations. A horizontal one and a vertical one.

This is done with trigonometry. You should know trig. If you don’t, go look up SOHCAHTOA and Pythagoras’s Thereom right now and make sure you know them. You should be able to see that if you have your launch velocity along some line, if you break it down into components you get this:

SOHCAHTOATackling the Problem

Let’s consider the problem horizontally:

We don’t have air resistance, and it’s flying through the air so there’s no friction. There is nothing slowing the snowball down in the x direction, so there is no horizontal acceleration.

So let’s list our relevant variables:

$latex s = x$ (the horizontal distance)

$latex u = vcos(\theta)$ (The x component of velocity)

$latex a = 0 $

$latex t = t $ (We’re just calling t t, because time is the same everywhere in our game).

So, as we know:

$latex s = ut + at^2/2 $

$latex x = vcos(\theta)t $

And therefore:

$latex t = x/(vcos(\theta))$

Okay, so now we know what the time is at any point. Let’s look in the important direction now: vertical.

$latex s = y$  (vertical displacement)

$latex u = vsin(\theta)$

$latex a=-g$ (gravitational acceleration, negative because it points down)

$latex t = x/vcos(\theta))$

Alright, so let’s get our equation:

$latex s = ut + at^2/2$

$latex y = xsin(\theta)/cos(\theta) – gx^2/(2v^2cos^2(\theta))$

Okay, so now we have a conundrum. We’re trying to solve for theta, but we have a whole bunch of trigonometric functions and we need to get it on its own.

Solving the Quadratic

This is the point where you need to know trigonometric identities. It really helps to know these, there’s 3 I would particularly recommend knowing that tend to allow you to solve pretty much any equation analytically, but in this case we’re going to use the following:

$latex sin(x)/cos(x) = tan(x)$

$latex 1/cos(x) = sec(x)$

$latex sec^2(x) = 1+tan^2(x)$

So, we can get everything in terms of tan(x), which is exactly what we need.

$latex y = xtan(\theta) – (1+tan^2(\theta))\times(gx^2/(2v^2))$

And this.. is a quadratic equation. Which means if we put it equal to 0 we can get a solution. Let’s define:

$latex \tau = \tan(\theta)$

To make things easier on the eyes. And let’s move everything to one side of the equals.

$latex gx^2\tau^2/2v^2 – x\tau + (y + gx^2/2v^2) = 0$

Now, I like to get rid of the coefficient of the τ term because that’s the type of person I am, so I’m dividing everything by $latex (gx^2/2v^2)$.

This gives us:

$latex \tau^2-2v^2\tau/gx + (2v^2y+gx^2)/gx^2 = 0$

If you don’t know where that gx^2 on the top came from, it’s because gx^2/gx^2 = 1, I just wanted to reduce it to one term by having a consistent denominator.

Okay so now we know the generic solution to the quadratic equation is:

$latex -b\pm(\sqrt{b^2-4ac})/2a$

(Where a is the coefficient of the τ² term, b is the coefficient of the τ term and c is the constant). So a is 1 for us. You can thank me later.

So we know that τ is equal to:

$latex -v^2/gx\pm\sqrt{v^4/(g^2x^2)-2v^2yg/(g^2x^2)-g^2x^2/g^2x^2}$

Ultimate pro tip for this step: When you divide by 2a in the quadratic formula, you can absorb the 2 into the square root, and divide everything by 4 instead, so you get something really neat. Note I also divided and multipled the middle term by g so everything has the same denominator. That way everything plugs into each other really simply. And you might notice as well, that the denominator outside the root is gx, and the denominator inside the root is g²x². In other words, we can yank that out of the square root, and end up with this:

$latex \theta = tan^{-1}((v^2\pm\sqrt{v^4-2v^2yg-g^2x^2})/gx)$

Rude Bear Two SolutionsNice and elegant. Now, notice that this has two solutions. One will be on the “way up” and one on the way down. So if my snowman throws a snowball up at you, he could throw it such that it strikes you on its first path, or throw it super high so it goes above you and lands on you. I prefer the low route so I’m taking the minus value.

So! Code time.

Code

[cpp]//float v is predefined in the class
float x = target.transform.position.x – transform.position.x;
float y = target.transform.position.y – transform.position.y;
float g = rigidbody2D.gravityScale * 10f;

float det = Mathf.Pow(v, 4) – 2 * v * v * g * y – g * g * x * x;

//If the determinant is less than 0, that means the projectile can’t reach the target, as the square root of a negative number is imaginary.
//So we need to account for that.
if(det>0){

float plusminus = Mathf.Sqrt(det);
float dividend = v*v – plusminus;

//For once we actually don’t want atan2 – it’d mess with our results.
float theta = Mathf.Atan(dividend/(g*x));
//Instead we just flip the vector if the target is on the left
rigidbody2D.velocity = new Vector2((x > 0 ? 1 : -1) * v * Mathf.Cos(theta),
(x > 0 ? 1 : -1) * v * Mathf.Sin(theta));

}
else{
//Whatever you want to do here if the target can’t be reached.
//You could do something cool and mathsy here like an angle for an "attempted" shot.
}
[/cpp]

And there we go, we’ve now set our rigidbody2D to always hit a target* as long as there’s nothing in the way.

In the case that the target is moving at a fixed velocity, thanks to Galilean relativity you can simply add/subtract the target’s velocity to the value of v before the calculation of theta, and then revert to the original value of v for the assignment of rigidbody2D.velocity.

*NB, the larger your timestep, the larger the error, and the longer your projectile is in flight, the more the error will blow up (especially if using the Euler Method). There’s nothing simple you can do about that really, but it’s not been a problem in my experience for this case.

Here’s what it looks like in action.

Have fun!

@Alexrosegames