1
\$\begingroup\$

I'm brushing up for a pending game jam and just for kicks I thought I'd let the gravity in my 2D platformer push at an arbitrary angle. Normally gravity points downward (3 PI / 2), but if I wanted it to point to the right (0) then the character would rotate accordingly and be able to walk along walls as though they were floor. In that case, pressing left would cause the player to move downward on the screen, as that would be left relative to the current angle of gravity.

The problem comes when I try to implement jumping or falling. As I'm now realizing, I lack the mental language to even properly describe my problem, but I clearly don't intuitively grasp how to handle rotated vectors.

For example: I'd like to know when the player is falling. Normally I'd just check if their velocity.y > 0, but that only makes sense if gravity is pointing downward. If it's pointing to the right, then a velocity.y > 0 implies that they're moving to the left, relative to gravity.

How do I go about understanding and working with 2D vectors that have an arbitrary global rotation applied to them?

\$\endgroup\$
  • \$\begingroup\$ Well, normally when working with directions, you have a Vector showing this direction. So, e.g., if your gravity is pointing down you have the vector (0,-1). Your velocity is a vector too. To determine whether you are falling (the velocity is roughly in the direction of your gravity, you could just normalize your velocity vector, take it's dot-product withe the gravity vector and check whether the returned value is lager than 0. \$\endgroup\$ – Anonymous Anonymous Jun 23 '18 at 17:30
2
\$\begingroup\$

If I understand correctly, you are making gravity a vector, and that is messing the rest of your code base.

So, first of all, as Anonymous Anonymous points out you will have to make velocity a vector. In fact, any other thing will probably have to be a vector too.

Note: I will not define what a vector is. Also, everything I say here is for working in 2D. However, there are similar and sometimes equivalent concepts for 3D, but that is beyond the scope of the answer.

Please use a library that does vector and matrix operations. I will explain enough to implement one yourself, just so you understand what is going on, but I will not cover optimizations.


Vector addition and substraction

If we consider a vector a movement (from one place to another). The addition of the vectors is the result of doing both movements:

va + vb = {x: va.x + vb.x, y: va.y + vb.y}

As you can see the order does not matter in vector addition.

Now, if a vector is a movement that takes you a place, and another vector is another movement that takes you to another place... the movement needed to go from one place to the other is is their difference, we get it by vector substraction:

va - vb = {x: va.x - vb.x, y: va.y - vb.y}

Here, the order matters. va - vb is the vector that goes from the place that vb takes you to the place that va takes you.

You might be doing this vector substraction to place objects respect to the screen. You have world coordinates, a camera that has a position, and you need to figure out where on the screen the objects in the world are. That is, you need the difference from the position of the camera to the world object. To get that, you take the world object and subtract the position of the camera.

The difference is also useful to measure the distance between two places, to do that we need the norm of the vector.


Norm and unit vectors

You will need to be able to measure the norm of a vector and to normalize it (make a unit vector out of it).

Assuming you are using euclidean geometry (a.k.a. normal, regular, everyday geometry), you compute the norm of the vector by the Pythagorean theorem: the square root of the squares of the components.

norm(v) = sqrt(v.x * v.x + v.y * v.y)

You get a unit vector by dividing the components of the vector by the norm of the vector. The norm of the resulting unit vector is 1 (within rounding error).

unit(v) = {x: v.x/norm(v), y: v.y/norm(v)}

The unit vector will be useful to specify directions, you will be able to scale it (multiply its components by a scalar) to get a vector of the appropiate norm you need.

scale(v, l) = {x: l * v.x, y: l * v.y}

Note: That is scalar product, and I will write it like this: v * l. We can also say that the unit vector is unit(v) = v * 1/norm(v).


Orthogonal vectors

You will need to be able to rotate vectors. For example, if you need the jump to always be against gravity, you can take the gravity vector, normalize it, rotate it half turn, and sacale it to the magnitud you need.

jump_velocity = rotateHalfTurn(unit(gravity)) * initial_velocity

The quarter turn and half turn rotations are easy. For a half turn - counterclockwise - rotation, you flip the components (X becoems Y and Y becomes X) and change the sign of the new X. And of course, you can reverse the process to rotate the other way. For half turn the short-cut is to change the sings of both components.

rotateQuarterTurn(v) = {x: -v.y, y: v.x}
rotateHalfTurn(v) = {x: -v.x, y: -v.y}
rotateThreeQuartersTurn(v) = {x: v.y, y: -v.x}

Note: This are equivalent as interpreting the vector as a complex number and multiplying by the imaginary unit multiple times. No, that you need that fact, but it might help you link the concept.

This way, you will be able to create your velocity vectors for the jump.


Other rotations

So, you want to know if the character is falling. For whatever reason. But your coordinates are not aligned with the gravity vector.

If the gravity vector is just rotated a multiple of quarter turns, you can rotate the velocity of your character accordingly, and then check the component you need.

That is, if you need to check that the Y component of the velocity is greater than 0 when gravity is down... but gravity has been rotated a quarter turn, you rotate the velocity of the character a quarter turn too and then check the Y component of the resulting vector.

This will be more problematic if the rotation is not a multiple of quarter turns. To solve those, we can do some trigonometrics.

To rotate by an arbitrary angle... just get the formula:

rotate(v, theta) = {
                       x: v.x * cos(theta) - v.y * sin(theta),
                       y: v.x * sin(theta) + v.y * cos(theta)
                   }

We can begin understand this as defined an inclined line by an angle, and then walking a certain length along that line, and then figuring out where we end. In that case we have a length over the hypothenuse, one adjacent angle, and of course the opposite angle to the hypothenuse is a right angle... then we solve with trigonometrics.

However, we do not only have movement along the line, but also across (perpendicular). We have to solve a second triangle for that, then simplify and you get these formulas.

You will also find these emerge if you consider the vector a complex number, and we describe rotations by euler to the power the imaginary unit multiplied by the angle... and then when we separate the components we get trigonometric functions by the euler's formula. Again, not that you need to know that, but it could be useful to link some concepts.

Notice that both X and Y components affect both of the resulting components. Unlike the scalar product where X only affects X.


Matrix transformations

Note: please use a library that provides vector and matrix operations.

We can put the transformation described by a rotation by an arbitrary angle in a matrix. And then we multiply by this matrix to transform the vectors.

                   |¯ cos(theta)   -sin(theta) ¯|
rotate(v, theta) = |                            | * v
                   |_ sin(theta)    cos(theta) _|

The product works like this:

|¯ A  B ¯|   |¯ v.x ¯|   |¯ v.x * A + v.y * B ¯|
|        | * |       | = |                     |
|_ C  D _|   |_ v.y _|   |_ v.x * C + v.y * D _|

As you can imagine, we can multiply by multiple matrices to apply multiple transformations. In fact, we can compose transformations by multiplying the matrices.

This is also useful to apply the same transformation to a large number of vectors. In particular if we can take advantage of SIMD to do matrix multiplication, or if we can get the GPU to do them for us. That is beyond the scope of this answer.

Futhermore, if we compute the multiplicative inverse of the matrix, we will have a matrix that reverses the transformation. Alternatively, you can create an method that does the inverse of the matrix product.

You will find that this matrices are also useful to describe scaling, shear and other transformations aside from rotation.


You will also find augmented transformation matrices (those are matrices with an extra column) with translation information.

I like to resolve them as a 3 by 3 matrix with the last row being 0, although that can make the inverse a bit complicated. Usually you do not have to implement this (you use a library), just in case, this is a solution:

If we have a matrix:

    |¯ A B C ¯|
M = |  D E F  |
    |_ G H I _|

Then:

transform = function (m, v) {
    var w = v.x * m.g + v.y * m.h + m.i;
    var x = (v.x * m.a + v.y * m.b + m.c) / w;
    var y = (v.x * m.d + v.y * m.e + m.f) / w;
    return new Vector(x, y);
};

transformInverse = function (m, v) {
    var w = (v.x * (m.d * m.h - m.e * m.g) + v.y * (m.b * m.g - m.a * m.h) + m.a * m.e - m.b * m.d);
    var y = (v.x * (m.f * m.g - m.d * m.i) + v.y * (m.a * m.i - m.c * m.g) + m.c * m.d - m.a * m.f) / w;
    var x = (v.x * (m.e * m.i - m.f * m.h) + v.y * (m.c * m.h - m.b * m.i) + m.b * m.f - m.c * m.e) / w;
    return new Vector(x, y);
}

Note: Yes, that code is for 2D vectors. You can simply it if m.g == 0, m.h == 0 and m.i == 0.


Coordinate systems

You can understand the default location, orientation and scale of your world as a coordinate system.

We can do the same for the view port... for example, the upper left corner is usually the origin, and the axis are aligned with the screen, with the vertical one being positive downwards, and une unit is a pixel.

However, even if the axis of the world are aligned with the view port, the origin usually isn't. This way, you can implement a camera that follows the playwer along the world.

Then, to transform a position from the world coordinates to the view coordinates, we can use a matrix transformation (that will automatically support rotation, flipping axis, scaling zooming, and similar effects). And if the player click on the screen, and we want to know what on the world is there, we can do the inverse transformaton to get wolrd coodinates from screen coordinates.

Futhermore, we can have auxiliary coordinates systems. For example, if we have an elevator (a moving platform), instead of having it push the objects on top of it by collision and physics engine, we can hack it by defining the position of those objects respect to the position of the elevator.

Under the same idea, it is possible to construct esqueleton animation if we place bones respect to each other. For instance, the weapon is placed respect to the hand, the hand is placed respect to the arm, and so on.

Notes:

  • In the above paragraphs, read "respect to" as "using a matrix transformation relative to".
  • What I describe here is not appropiate for every game, it may not be a good idea for yours, however it is a tool you are better of being aware of.
  • When I said that you rotate the velocity the same way that you rotated the gravity, what I should have said is that you transform the velocity to a coordinate system that is rotated according to the gravity vector. However, I had not explained what that means yet.

Convergence

If we want a measure of how similar is the direction of two vectors. We can use the dot product:

 dot(va, vb) = va.x * vb.x + va.y * vb.y

Note: that is a dot product, from now on I will write it as va · vb, and we will computed as described above.

The dot product is also equivalent to:

 va · vb = norm(va) * norm(vb) * cos(angle(va, vb))

As you know, if both vectors go in the same direction, the angle between them will be 0. And the cosine of 0 is 1 (which is the maximun value for cosine). On the other hand, if the vectors go in opposite directions, the angle between them will be a half turn. And the cosine of half turn is -1 (which is the minimun value for consie).

Thus, dot product will give you a measure of how convergent the vectors are (how similar is their direction).

Addendum: for a quick and dirty check of convergence, we can check if the vectors are in the same quadrant. As you may already know, we can get the angle of a vector (respect the horizonal axis) with atan. And we can use atan2 for an angle that allows us to check the quadrant (just by checking how many quarter of turn fit in the angle).

We can go an step futher and solve angle(va, vb) out of the equation above:

 va · vb = norm(va) * norm(vb) * cos(angle(va, vb))

 =>

 (va · vb)/(norm(va) * norm(vb)) = cos(angle(va, vb))

 =>

 arccos((va · vb)/(norm(va) * norm(vb))) = angle(va, vb)

And of course, we already have a way to compute the dot product.


Curl

Some times we need an idea of the rotation of angles. For example, if we have a series of angles that describe a polygon, and we want to know if the polygon is concave or convex. We can do this with a projection of the cross product of the vectors.

The cross product exists in 3D. What we will do is assume Z = 0. The resulting product is a kind of wedge product (which is the name I will use here), and can be computed as follows:

 wedge_product(va, vb) = va.x * vb.y - va.y * vb.x;

You will find that if the vectors converge, the wedge product is 0. The value will be positive if the rotation from va to vb is counterclockwise, and negative otherwise.

There is a link between this and the sine of the angle between the vectors, however that is clearer in 3D and I will not go there.


Projection, rejection and reflection

While these names imply light (for example, we have a light source and we want to compute where the shadow is projected over a given surface), they are an useful tool in solving collisions (I consider collisions beyond the scope of this answer).

To do a projection of a vector over another, we need a vector that goes in the same direction as the other vector. In fact, what we want is to rotate the first vector to a coordinate system that is rotated according to the second vector. And then take the component that goes in the same direction of the second vector.

Yes, that is the thing you do for the velocity and gravity. You are projecting it.

Now, using matrices to do a projection is overkill. We have a simpler solution:

project = function (a, b) {
    var factor = (a.x * b.x + a.y * b.y) / (b.x * b.x + b.y * b.y);
    return new Vector(b.x * factor, b.y * factor);
}

Addendum: What you get from the projection is a vector. That vector is the component of of your projected vector that is in the direction of the vector over which you projected... expressed in your regular coordinate system. If you want to know how long that component was, you take the norm of the projection. However, note that you lose directionality when you take the norm (by squaring the components their sign is lost), so a measure of convergence (read "convergence" above) can help you know if the vector goes in the same direction or against.


If the projection is the component that goes along the second vector. The rejection is the other component. Or, the vector that goes from the projection to the original vector.

reject = function (a, b) {
    var factor = (a.x * b.x + a.y * b.y) / (b.x * b.x + b.y * b.y);
    return new Vector(a.x - b.x * factor, a.y - b.y * factor);
}

Finally, a reflection is what you use to bounce along an inclined surface. As you know, the angle to the normal must be preserved (that is, we flip along the normal):

reflect = function (v, normal) {
    var val = 2 * ((v.x * normal.x) + (v.y * normal.y));
    return new Vector(v.x - (normal.x * val), v.y - (normal.y * val));
}

Your questions

In that case, pressing left would cause the player to move downward on the screen, as that would be left relative to the current angle of gravity.

This makes me think that you either: A) want to rotate the world instead of rotating gravity (which I have mentioned how to do, read "coordinate systems" and "matrix transformations"). Or B) you will define the effects of the input commands in terms of the gravity, in which case you will have to transform them accordingly (which I have mentioned too, read "other rotations" and "matrix transformations").

As I'm now realizing, I lack the mental language to even properly describe my problem, but I clearly don't intuitively grasp how to handle rotated vectors.

Hopefully I have fixed that, or at least gave you keywords to search.

Normally I'd just check if their velocity.y > 0, but that only makes sense if gravity is pointing downward. If it's pointing to the right, then a velocity.y > 0 implies that they're moving to the left, relative to gravity.

You can use projections for the check. The concept of "relative to gravity" is the coordinate system rotated according to gravity. If projections fall short of what you want to do, use matrix transformations (I think you will end there anyway, given the other things you mention).

How do I go about understanding and working with 2D vectors that have an arbitrary global rotation applied to them?

You are working on a different coordinate system, one that is rotated. And that you can describe with a matrix transformation.


I will plug other two other answers by me here:

\$\endgroup\$
  • \$\begingroup\$ Holy cow. I think Projection was both the term and operation I was looking for, but I'll definitely be saving this as my goto for all-things-vector for a while. I hope you didn't type the entirety of it just now, and thanks for also tailoring it to my question. \$\endgroup\$ – Hammer Bro. Jun 23 '18 at 22:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.