1
\$\begingroup\$

I was reading this answer, and while it works for that use-case, mine is slightly different (simpler?), but I can't figure out how to do it.

What I'm looking for is a way for the player to retain their speed. That is, if they are running into a wall at 5 meters per second, then when they collide with the wall, regardless of the angle they hit the wall, they will slide along that wall at 5 meters per second.

I've illustrated an example of what I mean in the following picture:

enter image description here

The green arrows are the player velocities, the orange arrow is what I want the resulting velocity to look like, and the white lines are the rectangles the player is colliding with.

Is there a way to do this? If so, how? Also, does it require the dot product? My intuition is that it shouldn't, because we want to maintain the player's speed, but I'm not sure.

\$\endgroup\$
1
  • \$\begingroup\$ You can follow that answer and do something extra : 1. Normalize the result vector. 2.Multiply it by magnitude of the original vector. \$\endgroup\$
    – Mangata
    Apr 15, 2022 at 12:45

1 Answer 1

2
\$\begingroup\$

Using the Dot Product

To begin, we reject the vector from the normal. I mean to subtract from the vector its projection over the normal. The vector projection looks like this:

Vector project(Vector a, Vector b)
{
    return b * (dotProduct(a, b)) / (dotProduct(b, b));
}

However, if we know we are projecting on a vector of already 1, then dotProduct(b, b) is 1. So we are left with:

Vector projectOnUnitVector(Vector a, Vector b)
{
    return b * (dotProduct(a, b));
}

So we are going to do this:

Vector undesiredMotion = normal * (dotProduct(input, normal))

Then we subtract, which leads us to the accepted answer of the linked question:

Vector undesiredMotion = normal * (dotProduct(input, normal));
Vector desiredMotion = input - undesiredMotion;

And then we can normalize the result and scale it to the length we want, like this:

Vector undesiredMotion = normal * (dotProduct(input, normal));
Vector desiredMotion = input - undesiredMotion;
Vector output = normalize(desiredMotion) * length(input);

Using the Cross Product

I want to reiterate that I describe above is the general solution. If you are working in 2D, you have some advantages. First of all: You know all your vectors are coplanar.

So, we know that the result will be the normal, rotated a quarter turn (either clockwise or counterclockwise, we don't know yet) and scaled up to the length of the input.

And the second advantage is that doing the quarter turn rotation is trivial:

Vector2D rotateQuarterTurnCounterClockwise(Vector2D v)
{
    return Vector2D(-v.y, v.x);
}

Or:

Vector2D rotateQuarterTurnClockwise(Vector2D v)
{
    return Vector2D(v.y, -v.x);
}

But, what way do you want to turn it? Well, you want the result to turn from the input to the same side as the normal turns from the input. In other words, you the output have the same sign curl as the normal. I mean, you want the wedge product to have the sign. I mean, the length of the cross product. The cross product has different sign if the vectors are turning to one side or the other.

I mean:

sign(length(cross(input, normal))) == sign(length(cross(input, output)))

The cross product here would expand the vectors with 0 on the third axis. I remind you that we are working in 2D. So this is an external product (the result is outside the space of the inputs).

Now, as you know, if we multiply one tentative output by -1, we get the other one. To reiterate, we either want the direction of the normal rotated a quarter turn clockwise or counterclockwise.

So if we do this:

Vector2D direction = rotateQuarterTurnCounterClockwise(normal);
direction = direction * sign(length(cross(input, direction)));

We are always getting a direction that turns to the same side no matter what (if the direction was turning the other side, the product flips it). And then we do this:

Vector2D direction = rotateQuarterTurnCounterClockwise(normal);
direction = direction * sign(length(cross(input, direction)));
direction = direction * sign(length(cross(input, normal)));

So we get a direction that turns from the input to the same side as the normal turns from the input.

Then you can scale it:

Vector2D direction = rotateQuarterTurnCounterClockwise(normal);
direction = direction * sign(length(cross(input, direction)));
direction = direction * sign(length(cross(input, normal)));
Vector2D output = direction * length(input);

Actually, it will be like this when we are done with it, I explain what is going on below:

Vector2D direction = rotateQuarterTurnCounterClockwise(normal);
direction = direction * sign(wedge(input, direction));
direction = direction * sign(wedge(input, normal));
Vector2D output = direction * length(input);

I want to reiterate that the approach I'm describing is for 2D only.

Although, you could take arbitrary input and normal vectors in 3D, find a plane that contains both, project, do the approach in this answer, and un-project the result… That extra work would decrease precision, decrease performance, and be error prone. Thus I would advice against it.


Alright, let us unpack this:

sign(length(cross(a, b)))

The cross product is like this:

Vector3D crossProduct(Vector3D a, Vector3D b)
{
    return Vector3D(
        a.y * b.z - a.z * b.y,
        a.z * b.x - a.x * b.z,
        a.x * b.y - a.y * b.x
    );
}

We are, however, defining one that works on 2D vectors. And this happens:

Vector3D cross(Vector2D a, Vector2D b)
{
    return Vector3D(
        a.y * 0.0 - 0.0 * b.y,
        0.0 * b.x - a.x * 0.0,
        a.x * b.y - a.y * b.x
    );
}

In other words:

Vector3D cross(Vector2D a, Vector2D b)
{
    return Vector3D(
        0.0,
        0.0,
        a.x * b.y - a.y * b.x
    );
}

Only the z axis of the result has value (after all, it has to be perpendicular to the inputs, and the inputs are in the XY plane). Consequently, the length is the value of the z axis. Let me define a function for that:

float wedge(Vector2D a, Vector2D b)
{
    return a.x * b.y - a.y * b.x;
}

Therefore, instead of writing this:

sign(length(cross(a, b)))

We write this:

sign(wedge(a, b))

And nobody has to complain that you can't make a cross product of 2D vectors.


For contrast, this is dot product:

float dotProduct(Vector2D a, Vector2D b)
{
    return a.x * b.x + a.y * b.y;
}

Wait a minute… This:

float wedge(Vector2D a, Vector2D b)
{
    return a.x * b.y - a.y * b.x;
}

Is this:

float wedge(Vector2D a, Vector2D b)
{
    return dotProduct(a, Vector2D(b.y, -b.x))
}

A dot product with a vector rotated a quarter turn clockwise!


Using Trigonometry

I suppose we can go about it yet another way: trigonometry. The idea is that we want to rotate the input vector. And we need to find out by what angle.

First we need to find the angle between the input and the normal… And that could be done with the dot product or the cross product, but since we want to do something else… Good old atan2:

float inputAngle = atan2(input.y, input.x);
float normalAngle = atan2(normal.y, normal.x);

To be clear: this is also a 2D only solution. Project and un-project shenanigans apply.

To find out if it is a positive or negative turn:

float inputAngle = atan2(input.y, input.x);
float normalAngle = atan2(normal.y, normal.x);
float turnSign = sign(normalAngle - inputAngle);

And the output angle will be:

float inputAngle = atan2(input.y, input.x);
float normalAngle = atan2(normal.y, normal.x);
float turnSign = sign(normalAngle - inputAngle);
float outputAngle = normalAngle + quarterTurn * turnSign;

Here quarterTurn is the angle that represents a quarter turn. That is 90º, or TAU/4 radians, I mean… PI/2.

And thus we need a turn of:

float inputAngle = atan2(input.y, input.x);
float normalAngle = atan2(normal.y, normal.x);
float turnSign = sign(normalAngle - inputAngle);
float outputAngle = normalAngle + quarterTurn * turnSign;
float turn = outputAngle - inputAngle;

We can rotate the input vector with a rotation matrix:

Vector output = RotationMatrix(turn) * input

Which is something like this:

         +-                        -+   +-         -+
         |  cos(turn)   -sin(turn)  |   |  input.x  |
output = |                          | * |           |
         |  sin(turn)   cos(turn)   |   |  input.y  |
         +-                        -+   +-         -+

Which is the same as:

         +-                                           -+
         |  input.x * cos(turn) - input.y * sin(turn)  |
output = |                                             |
         |  input.x * sin(turn) + input.y * cos(turn)  |
         +-                                           -+

In other words:

float c = cos(turn);
float s = sin(turn);
Vector2D output = Vector2D(input.x * c - input.y * s, input.x * s + input.y * c);

Wait, wait, I can rewrite that as two dot products:

float c = cos(turn);
float s = sin(turn);
Vector2D xRot = Vector2D(c, -s);
Vector2D yRot = Vector2D(s, c);
Vector2D output = Vector2D(dotProduct(input, xRot), dotProduct(input, yRot));

Muahahahaha!

Embrace the dot product.

\$\endgroup\$
9
  • 1
    \$\begingroup\$ @RyanPeschel The way you are defining the OBB (Oriented Bounding Box) is harder than it needs to be. Notice that this answer does not have any trigonometry: gamedev.stackexchange.com/a/136075/10408 - That is because it does not define the OBB as a rectangle rotated by an arbitrary angle. In general you should not be computing the collision normal, but picking among pre-computed normals. The OBB has four possible normals, and they are all a quarter turn of each other. You can start a normal, project the center of the circle on it and measure, then use that to pick which normal you want. \$\endgroup\$
    – Theraot
    Apr 17, 2022 at 19:40
  • 1
    \$\begingroup\$ @RyanPeschel Well, the corners are an special case, aren't they? I forget what is the common solution. Edit: search for OBB-Circle collision, you will find plenty on the topic. \$\endgroup\$
    – Theraot
    Apr 17, 2022 at 19:45
  • 1
    \$\begingroup\$ @RyanPeschel And you are rending it, right? So you have computed the positions of the corners to be able to render it. So you have the direction of the segments that connect those corners. And the normal is that direction rotated a quarter turn. Yes, you would be using sine and cosine, but not when something collides, but when it rotates. \$\endgroup\$
    – Theraot
    Apr 17, 2022 at 20:38
  • 1
    \$\begingroup\$ @RyanPeschel Since you say "hundreds of normals", I think you want the position of center of the circle minus the position of the corner, normalized. The issue with the corners is on deciding what you want to do with them. Ok, you have the center of the circle, you subtract the center of the OBB, and project it on one of the normals. Compare the length of the projection with the size of the OBB on the direction of that normal, if it is less, you want one of the normals to the sides. You can subtract the projection from the center of the circle, and take the length for the other check. \$\endgroup\$
    – Theraot
    Apr 17, 2022 at 21:02
  • 1
    \$\begingroup\$ @RyanPeschel Oh, yes, that would work. It is just one step removed from storing the rotation matrix. In fact another common approach is to store a transformation matrix (translation, rotation and scale) and then handle it like a unit AABB-Circle collision. And then transform the normal back. \$\endgroup\$
    – Theraot
    Apr 17, 2022 at 21:59

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .