- Home /
Vector-based Pong-ball bounce calculations
Hello everyone.
I'm making a 2D game with pads and balls, sort of like Pong, in Unity 4.6.1. The pads are going to appear in various rotations, and therefore the physics for bouncing the balls off the pads, have to be vector-based.
EDIT: I've answered my own question (see below this post). Full code with many comments included.
My wishes:
Depending on where the ball hits the paddle, it should bounce off in an angle of between -90 and 90 degrees, where the middle of the pad would give 0 degrees, and the extremities on each side should give -90 or 90 respectively. (The numbers are just for reference.)
The natural wall-bounce of the ball, should be taken into account, so the resulting bounce vector, will be a product of the normalized velocity vector of the ball, and the calculated and normalized directional vector from the normal wall/pad-bounce (see "Point 1"). (NOTE: The speed of the ball after the bounce is handled later!)
On the image below, the red arrow is the velocity vector of the ball. The effect of "Point 1" is the blue arrow, and the end-result implementing the functionality of both "Point 1" and "Point 2", is the green arrow.
Additional information:
The pads can change in size. I always have the following variables available: current width in units, standard width in units, and current scale as a float.
The ball has a circle collider, which is not a trigger.
The pads have a manually fitted polygon-collider, matching the bend on it. This is a trigger.
Using the code below, I almost have it going, but not quite. It's not working consistently.
"Point 1" isn't being computed correctly, mostly because I'm having trouble finding a calculation that's precise. When I created my first Pong game, it was easy, because I could just see the difference in ball.y and pad.y and hold that against the width of the pad. I can't for the life of me figure out how to do that using vectors.
"Point 2" is giving me trouble, because I can't figure out how to calculate the normal vector for the pad. The calculations noted in this other answer and here say, that I need to get this normal correct to get the right bounce.
This code is in "OnTriggerEnter2D(Collider2D col)" on the Pad:
GameObject ball = col.gameObject;
BallScript ballScript = col.gameObject.GetComponent<BallScript>();
// We know the ball has hit the pad, so we raycast to see where it hits.
RaycastHit2D hit = Physics2D.Raycast(
new Vector2(ball.transform.position.x, ball.transform.position.y),
ball.rigidbody2D.velocity);
// Get the normal for the hit. I think this is also not going to work for my purposes,
// since my pad isn't square)
Vector2 n = hit.normal;
n.Normalize(); // this has to be normalized
// Get the velocity vector of the ball
Vector2 v = ball.rigidbody2D.velocity;
v.Normalize(); // I normalize v, because I want to add speed manually later
float dotOfvn = Vector2.Dot(v, n);
// Always reports -1f or 0.9998f, most likely because n is the same as v
// (or the exact opposite, I'm not sure)
Debug.Log("Dot: " + dotOfvn);
// Apply the function...
Vector2 R = -2 * dotOfvn * n + v;
// Without the following, the ball always returns in the same direction it came from.
// With the following, I almost have it all functioning, except when a ball comes
// flying as in image 2 (below).
R += (Vector2)((ballScript.transform.position - _transform.position) / currentPadScale).normalized;
R.Normalize();
ball.rigidbody2D.velocity = R * ballScript.speed;
Result of the current code is, that everything seems to almost work, but this image shows a situation that happens often, which proves it doesn't. Red arrow is the velocity vector of the ball, and the blue is where the pad sends it upon being hit.
As the code-comments state, I'm not sure which parts I'm doing correctly, but I'm sure it is solvable. I would very much appreciate a helping hand.
Sincerely, Ultroman.
I've solved part of my problem. Namely "Point 2". Here's the code:
GameObject ball = col.gameObject;
BallScript ballScript = col.gameObject.GetComponent<BallScript>();
Vector2 n = _transform.up;
Vector2 v = ball.rigidbody2D.velocity;
v.Normalize(); // I normalize v, because I want to add speed manually later
float dotOfvn = Vector2.Dot(v, n);
Vector2 R = -2 * dotOfvn * n + v;
R.Normalize();
ball.rigidbody2D.velocity = R * ballScript.speed;
This bounces the ball from the pad, as if the pad was a wall, and it works at any angle. So far so good. The missing piece I was looking for, was that _transform.up is essentially the normal vector for my pad. It might be _transform.right or an inversion of either, depending on how the sprite is positioned at import. $$anonymous$$ine is positioned like so:
Setting n = _transform.up and v to ball.rigidbody2D.velocity is redundant, but is in this example for clarity, so the function retains its variable-names, v and n.
Now, all I need to do, is solve "Point 1". I need to find the length in units, from where the ball hit the pad, to the middle of the pad. I can't just use the length between the position of the ball and the position of the pad. It will not be precise enough.
Answer by Ultroman · Jan 22, 2015 at 03:55 PM
SOLUTION:
I've kept some extra variables, to make it easier to see how they fit into the mathematical functions. Optimize it yourself :) Don't make local variables if you can help it!
This code is placed inside my script on the pad itself, in its OnTriggerEnter2D(Collider2D col). The ball has a rigidbody2d which is NOT a trigger, but the pad is a trigger.
// The ball colliding with my pad (I've checked its tag before getting here!)
GameObject ball = col.gameObject;
// The ball holds the ever-increasing speed of my ball, which is to
// be updated for each pad-bounce.
BallScript ballScript = col.gameObject.GetComponent<BallScript>();
// The formula for a standard wall-bounce is:
// -2*(V dot N)*N + V
// where V is the incoming velocity of the ball, and N is the normal-vector
// for the wall/pad i.e. upwards/outwards from the pad, when the long side
// of it is left-to-right, and top and bottom are up and down respectively.
// Because my pad-sprite is like this <======>, I use _transform.up for N.
// For other implementations using pad-sprites that are turned differently,
// _transform.right or an inversion of either might be used.
// First, I find my velocity:
Vector2 v = ball.rigidbody2D.velocity;
// I normalize v, because I want to make a change in speed later (below).
// Not normalizing can also introduce weird behavior in the functions, so to
// be safe, you can save your v.magnitude, which is the length/speed of the
// vector, to a variable, and apply it afterwards.
// More on that later.
v.Normalize();
// We need the dot-product of v and n for the function.
float dotOfvn = Vector2.Dot(v, (Vector2)_transform.up);
// I make a clean 0,0 vector2, to store my resulting bounce-vector in.
// I want to make it so the players can choose which type of bounce
// they want, and even mix them, so I've split up the two ("Point 1"
// and "Point 2"), so I can add their effects separately.
Vector2 R = new Vector2();
// I add the effect of the wall-bounce to my R.
// If you ONLY want a wall-bounce, this is the function you want, and in
// that case,you can skip the normalization of v above, so you don't need
// to manually apply v.magnitude afterwards. But if you want to continue,
// I advise you to follow the code as written.
R += -2 * dotOfvn * (Vector2)_transform.up + v;
// Now R represents the precise bounce-vector, that the ball would have
// if it had struck a flat wall.
// Now, on to the Pong-style bounce!
// We want to also be able to take into account the Pong/Arkanoid-style of
// bouncing from the pad, where the ball shoots in a different direction,
// depending on where it hits the pad. The further from the middle, the
// sharper the exit-angle.
// That means we have to be able to interpolate between the normal-vector
// for the pad (upwards), and the vector going along the pad (sideways).
// The function for getting an interpolated vector between two 2D vectors
// is: A*t + (1-t)*B
// where t is the unit-based "percentage" you want to be close to A
// compared to how close you want to be to B.
// That means I get a float of 0.01 for 1%, 1 for 100% and -1 for -100%.
// To be able to use this, we need to know how far from the middle the ball
// struck. We want to find the length from where the ball hit, to the middle
// of the pad, but not directly from the position of the ball, but from
// where it hit, and it has to be projected to a point on the pad's sideways
// normal-vector, to be precise. See the last image of this post for
// clarification.
// We know the ball has hit the pad, so we raycast to see where it hits.
// You have to make sure that your raycast can only hit the pad. Do some
// layering or something, so it doesn't hit anything else first.
RaycastHit2D hit = Physics2D.Raycast(new Vector2(ball.transform.position.x,
ball.transform.position.y), ball.rigidbody2D.velocity);
// We find the vector from the hit-point to the center of the pad.
// The pivot of my pad sprite is in the center of it, so its position
// is also its center. If your pivot is at the bottom of the pad, this
// should also work fine.
Vector2 vectorFromHitPointToPadCenter =
hit.point - (Vector2)_transform.position;
// Then we can use the Dot-procuct of that vector and our pad's sideways-
// vector, to project that vector onto the sideways-vector of the pad, which
// gives us the length from the center of the pad, to the red X (see the
// last image in this post). If the hitPoint is on the left side of the pad,
// this length will be negative, because I use _transform.right as the
// sideways-vector.
float length = Vector2.Dot(vectorFromHitPointToPadCenter, _transform.right);
// Now, a unit-percentage float, where 1.0f is 100% and -1.0f is -100%.
// This gives us a factor of how far from the pad-center the ball has hit.
// If it is negative, then it has hit the left side of the pad.
// E.g. if it is -1.0f, the ball has hit the far left extremity of the pad.
// You'll have to figure out your "halfCurrentPadWidth" variable yourself.
// It should be half the width of the pad in units.
var percentageOfLengthVSHalfPadLength = length / halfCurrentPadWidth;
// We apply A*t + (1-t)*B
// Again, I add this to my existing vector. If you ONLY want this standard
// Pong-bounce, just use this, and forget the wall-bounce math above, and
// change the += to just =.
R += (Vector2)_transform.right * percentageOfLengthVSHalfPadLength +
(1 - percentageOfLengthVSHalfPadLength) * (Vector2)_transform.up;
// Gotta normalize.
R.Normalize();
// Apply speed to the ball. If you just want the ball to continue at its
// present speed, you can replace ballScript.speed with
// ball.rigidbody2D.velocity.magnitude.
ball.rigidbody2D.velocity = R * ballScript.speed;
This is the image I keep referring to. Here is a quick summary of the solution. The red X is the point we want for our calculations. The blue line goes from the center of the pad to the hitPoint. After we find the red X, we can find the length between it and center of the pad, and compare/divide that with half the length of the pad, which gives us a unit-based "percentage"/factor of how far from the middle the ball struck. Then we can apply a "percentage"/factor-based interpolation between the pad's sideways-vector and its up-vector, to determine the bounce-vector for the ball. If the length we find is positive, the ball will bounce right, and if it's negative, the ball will bounce left.
When we combine this effect with the standard wall-bounce, then no matter where the ball hits, it can bounce both to the left and to the right, depending on how sharp its trajectory is, resulting in the behaviour I've tried to describe in the question.
UPDATE 2021:
There is a problem with this code, if the ball reaches a velocity that allows it to move so far through the pad in one physics update that the ball's center ends up underneath the center of the pad. This messes up the calculation on lines 72 and 73. You'll need to raise the number of physics iterations to support fast-moving balls, anyway, so look at those values in Project Settings => 2D Physics, and also make sure to set the collision detection on your ball to Continuous.
There is also the fact that I'm using the ball's current position (which has already moved into the trigger and possibly behind it, as noted above), instead of the position it had after the last FixedUpdate. Doing that will fix all of these problems.
Then there's also the ever-present problem that you can't know if the ball has hit several walls either in this FixedUpdate or the previous one, making trajectories very difficult to establish after-the-fact. This answer/tutorial does not account for these things, but I thought I'd note them since they've come to mind after reading it through years later. This should still serve as a good base for your own Pong physics, until Unity gives us full control of collision trajectory handling during the physics-step, instead of only between them.
Wow Ultroman... Your code solved my problem I was stucked in from last 2 days. Awesome work man... And thanks for sharing your code. It is really very very informative.
You're very welcome. Thanks for the kind words :D I was hoping my struggles would ease someone else's one day. I can see that it could do with a bit more clarification, on the fact that it's unit-based "percentages". Plus the image I keep referring to, isn't there. Will fix momentarily
EDIT: Fixed!
Hey $$anonymous$$an, you are awesome... would you please help me further?
In case of CircleCollider2D how can I achieve the same angle reflection based on the position it hit on that circle?
Sorry for troubling you... Also can you please tell me how to get the reflection if hit at the right/left side of the BoxCollider2D... as per your current solution it will only take the directions and addforce when it will hit the up/down side of the gameobject but if it hits right/left side of the gameobject then how to achieve that?
Actually I am currently putting the Circular and Box type of object and I want to have the angle reflection bounce/addforce to the ball...
I am expecting your kind favour... Sorry for my weak/bad english.
I haven't solved the one with the sides yet myself. I'm only using this for my pads and the outer walls of my level, and nothing ever hits their sides, as they don't have any. For exactly this reason, I have had to just use the normal physics for the rest of my objects. You'll have to figure that one out for yourself.
In the case of using the CircleCollider for the pad, I'd imagine it should work as it is.