Gamasutra: The Art & Business of Making Gamesspacer
View All     RSS
September 17, 2014
arrowPress Releases
September 17, 2014
PR Newswire
View All





If you enjoy reading this site, you might also want to check out these UBM Tech sites:


 
2D Physics: Analytically Targeted Rigidbody Projectiles
by Alex Rose on 07/11/14 07:09:00 pm   Featured Blogs

The following blog post, unless otherwise noted, was written by a member of Gamasutra’s community.
The thoughts and opinions expressed are those of the writer and not Gamasutra or its parent company.

 

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

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:

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:

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:

 

SOHCAHTOA

Tackling 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:

s = x (the horizontal distance)

u = vcos(theta) (The x component of velocity)

a = 0

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

So, as we know:

s = ut + at^2/2

x = vcos(theta)t

And therefore:

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.

s = y  (vertical displacement)

u = vsin(theta)

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

t = x/vcos(theta))

Alright, so let’s get our equation:

s = ut + at^2/2

y = xsin(theta)/cos(theta) - gx^2/(v^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:

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

1/cos(x) = sec(x)

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

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

y = xtan(theta) - (1+tan^2(theta))times(gx^2/(v^2))

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

tau = tan(theta)

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

gx^2tau^2/2v^2 - xtau + (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 (gx^2/2v^2).

This gives us:

tau^2-2v^2tau/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:

-bpm(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:

-v^2/gxpmsqrt{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:

theta = tan^{-1}((v^2pmsqrt{v^4-2v^2yg-g^2x^2})/gx)

Rude Bear Two Solutions

Nice 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

//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 dis = Mathf.Pow(v, 4) - 2 * v * v * g * y - g * g * x * x;

//If the discriminant 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(dis>0){

float plusminus = Mathf.Sqrt(dis);
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 here like an angle for an "attempted" shot.
}

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 (click for video).

Have fun!

Originally posted at my dev blog.

@Vorpal_Games

 


Related Jobs

Blizzard Entertainment
Blizzard Entertainment — Irvine, California, United States
[09.17.14]

SENIOR SOFTWARE ENGINEER, GRAPHICS
Blizzard Entertainment
Blizzard Entertainment — Irvine, California, United States
[09.17.14]

ASSOCIATE SOFTWARE ENGINEER, TOOLS
Trion Worlds
Trion Worlds — Redwood City, California, United States
[09.17.14]

Senior Graphics Engineer
Telltale Games
Telltale Games — San Rafael, California, United States
[09.17.14]

Tools Engineer (Qt)






Comments


Bojan Lovrovic
profile image
Things get slightly more difficult if you use air resistance force (drag) that is proportional to velocity (note that this is only a simplification of an actual physics model in which drag is proportional to squared velocity). You end up having this equation:

m*a = m*g - c*v

where:
m - mass of the body (scalar)
a - current acceleration (vector)
g - acceleration due to gravity (vector)
c - some constant that affects how powerfull the drag will be (scalar)

This is a simple nonhomogeneous linear second-order differential equation and here is the solution:

p(t) = c1 + c2*exp(-c*t/m) + t*g*m/c

Where p(t) is a position (vector) as a function of time.
The only thing that is left to do is calculate c1 and c2 which are also both vectors.

c2 = -m*v(0)/c
c1 = p(0) - c2

Where p(0) is initial position vector and v(0) is initial velocity vector.

Here is the code example:

//gravity
float gx = 0;
float gy = -9.81;
float gz = 0;

//mass
float m = 1;

//drag coefficient
float c = 1;

//initial velocity
float v0x = 25;
float v0y = 0;
float v0z = 0;

//initial position
float p0x = 0;
float p0y = 3;
float p0z = 0;

float c2x = -m*v0x/c;
float c2y = -m*v0y/c;
float c2z = -m*v0z/c;
float c1x = p0x - c2x;
float c1y = p0y - c2y;
float c1z = p0z - c2z;

// in update method you should use something like this
myObject.updatePosition(
c1x + c2x*math.exp(-c*gameTime/m) + gameTime*gx*m/c,
c1y + c2y*math.exp(-c*gameTime/m) + gameTime*gy*m/c,
c1z + c2z*math.exp(-c*gameTime/m) + gameTime*gz*m/c);


Hope this turns out to be useful to someone :-)

Alex Rose
profile image
Hey, nice one.

You can actually reduce this a bit more, since drag is based on density, so it's proportional to mass, so you can yank that out of your constant and reduced the first equation to:

m*a = m(g-c*v)
1/(g-cv)dv = dt

So, integrating both sides you get:
-log(g-cv)/c = t + k

Where k is the initial time.

Or rather:

log(1/(g-cv)) = t + k

So

1/(g-cv) = e^(t+k)

And

e^-(t+k) = g-cv

So

v = (g-e^-(t+k))/c

Which you can use as your velocity at any point and then get the x from it, or you could get x in terms of v by doing:

vdv/dx = (g-cv)
v/(g-cv)dv = dx
v/(g-cv) = (v - g/c)/(g-cv) + (g/c)/(g-cv) = -1/c + g/(cg-vc^2)
x = -v/c - g*ln(cg-vc^2)/c^2

But I mean.. there really isn't any point to doing that analytically, because you're doing it frame by frame. Especially as that would only give linear air resistance.

If you use the euler cromer method you can just iterate that, it's much less resource intensive, and although it won't give a perfect solution like an analytical one - you probably don't want one, because that means it can't collide or interact with anything because it's on some set perfect course.

So if you just set your conditions and then every step do:

a = (-v.x*v.x,g-c*v.y*v.y,-v.z*v.z)*c/m;
v = v+a*Time.fixedDeltaTime;
x = x+v*Time.fixedDeltaTime;

And now we have motion based on v^2 drag. (This is all assuming a frame of reference in which g is positive like you have, but I mean.. you would normally take down as negative). Difference between this and Euler is that it uses the new velocity. Could use verlet integration if you want it more accurate.

Point is, it's really trivial to do this without expensive exponential calls, and most game engines have this functionality built in, in Unity your rigidbody will just do all this stuff automatically.

The problem that's being tackled here is not the idea of making something move with air resistance, but to predict in advance exactly where it is going to be in a hundred steps, and it's much more efficient to do that in a single call than to simulate hundreds or thousands of steps to find out where it'll end up and find it through trial and error.

In 3D it's should be fine to convert this, you just need to change the x axis to the direction in the xz plane you're interested in, and you'll end up using x/cos(phi) where phi is the angle to that point, and in the final vector using (xcos(phi)cos(theta),ysin(theta),zsin(phi)cos(theta))*basevelocity to get it to fire at the correct angle.

Cheers.

ultra brite
profile image
here is a little something I came up with years ago that still serves me well:

inline vec3 ballisticLaunchSpeed(const vec3& origin, const vec3& destination, const vec3& gravity, float duration)
{
return (destination-origin)/duration-(gravity*duration)/2;
}

useful to have an object jump from A to B in a fixed time, if you don't need a specific angle or velocity.

Alex Rose
profile image
Nice one.

Yeah, I just quickly sketched it up in 2D so my working may have been wrong (especially since I did it in ms paint and not a notepad), but in 2d that should be:

return Vector2(x/t, (y+gt^2)/t);

Where t is the fixed time, x is the horizontal displacement, y is the vertical displacement and g is the strength of gravity.

Dimensions check out.

David Erosa
profile image
Great article! A few weeks ago I had the same problem (solving the angle for a ball to reach a target) but I had neither the time nor the patience to find out how to do it properly. Now I'll study your article, as I get lost from the "Solving the Quadratic" part.

Thanks for the writing!

Alex Rose
profile image
I'll write it inline so it might help to write it by hand if you can't read maths on one line. It can get cluttered, it's an acquired skill to be able to read big formulae off one line.

First there's the trig identities. The first two are true by definition, the third is true but the proof isn't really worth seeing, so you may as well just accept that it's true:

sin/cos = tan
1/cos = sec
sec^2 = 1+tan^2(x)

Also in case you didn't know:

cosx*cosx = cos^2x
secx*secx = sec^2x

So, we had:

y = xsin(Θ)/cos(Θ) - gx^2/(2v^2cos^2(Θ))

So that reduces to:

y=xtan(Θ) - gx^2*sec^2(Θ)/(2v^2)

Which becomes:

y=xtan(Θ)-gx^2(1+tan^2(Θ))/(2v^2)

Which is where that line comes from.

Now, this has the form of a quadratic formula, as in:

aT^2+bT+c=0

We just need to move things around a bit.

It's best to keep the squared term positive, so we'll move everything to the left side of the equals, and we get:

gx^2(1+tan^2(Θ))/v^2-xtan(Θ)+y = 0

And now let's expand those brackets to move the constant over.

gx^2tan^2(Θ)/v^2-xtan(Θ)+y+gx^2/v^2 = 0

Now, I would normally keep it like this but I get that this looks a bit confusing, so we call T = tan(Θ). That way we get rid of the trig functions and we're left with:

gx^2T^2/(2v^2) - xT + y+gx^2/(2v^2) = 0

Now, let's divide by gx^2, so multiply the bottom by gx^2 everywhere.

T^2/(2v^2) - xT/gx^2 + y/gx^2 + 1/(2v^2) = 0

Now let's multiply by 2v^2 everywhere.

T^2 - 2xv^2T/gx^2 + 2yv^2/gx^2 + 1 = 0

Now let's make that constant into one term. You know that gx^2/gx^2 = 1 so we'll replace that 1 for gx^2/gx^2 and make it one big fraction:

T^2 - 2v^2T/gx + (2yv^2 + gx^2)/gx^2 = 0

And the general solution to the quadratic equation is:

-b+/-sqrt(b^2-4ac)/2a

Where in the equation:

aT^2+bT+c=0

So we've made a = 1, b = 2v^2/gx and c=(2yv^2 + gx^2)/gx^2

So now we put it in the equation

T = (2v^2/gx +/- sqrt((2v^2/gx)^2-4(2yv^2 + gx^2)/gx^2))/2

Now, notice everything inside is multiplied by 4 because 2*2 is 4 in the first bit and there's a 4 afterwards, then outside the root it's all divided by 2, but 2^2 = 4, so you can divide everything inside by 4 and get:

T = v^2/gx +/- sqrt((v^2/gx)^2-(2yv^2 + gx^2)/gx^2)

And now you can expand the first square, so:

T = v^2/gx +/- sqrt(v^4/(g^2x^2)-(2yv^2 + gx^2)/gx^2)

Now notice inside, the left bit is over g^2x^2 and the right bit is over gx^2, so if you multiply everything on the right by g and divide it by g, everything will be over g^2x^2, so you'll get:

T = v^2/gx +/- sqrt(v^4/(g^2x^2)-(2gyv^2 + g^2x^2)/g^2x^2)


And now EVERYTHING is divided by gx, because inside the root that will be g^2x^2, so you can yank that out to make it:

T = (v^2 +/- sqrt(v^4-(2gyv^2 + g^2x^2)))/gx

so

tan(Θ) = (v^2 +/- sqrt(v^4-2gyv^2 - g^2x^2))/gx

and therefore:

Θ = tan^-1(v^2 +/- sqrt(v^4-2gyv^2 - g^2x^2))/gx

And there you go.

David Erosa
profile image
Thanks for the additional steps. I'll write everything by hand and follow it again. I'm sure your explanations will be enough.

Math fluency is something I lost years ago and never got back. I wish I could dedicate it the time it deserves...

Jeff Postma
profile image
This was awesome, thanks for taking the time to put this together and break down the math. Physics is awesome.


none
 
Comment: