Turn rate on a player's rotation doing a 360 once it hits pi
Asked Answered
E

2

15

Making a game using Golang since it seems to work quite well for games. I made the player face the mouse always, but wanted a turn rate to make certain characters turn slower than others. Here is how it calculates the turn circle:

func (p *player) handleTurn(win pixelgl.Window, dt float64) {
    mouseRad := math.Atan2(p.pos.Y-win.MousePosition().Y, win.MousePosition().X-p.pos.X) // the angle the player needs to turn to face the mouse
    if mouseRad > p.rotateRad-(p.turnSpeed*dt) {
        p.rotateRad += p.turnSpeed * dt
    } else if mouseRad < p.rotateRad+(p.turnSpeed*dt) {
        p.rotateRad -= p.turnSpeed * dt
    }
}

The mouseRad being the radians for the turn to face the mouse, and I'm just adding the turn rate [in this case, 2].

What's happening is when the mouse reaches the left side and crosses the center y axis, the radian angle goes from -pi to pi or vice-versa. This causes the player to do a full 360.

What is a proper way to fix this? I've tried making the angle an absolute value and it only made it occur at pi and 0 [left and right side of the square at the center y axis].

I have attached a gif of the problem to give better visualization.gif

Basic summarization:

Player slowly rotates to follow mouse, but when the angle reaches pi, it changes polarity which causes the player to do a 360 [counts all the back to the opposite polarity angle].

Edit: dt is delta time, just for proper frame-decoupled changes in movement obviously

p.rotateRad starts at 0 and is a float64.

Github repo temporarily: here

You need this library to build it! [go get it]

Edmonton answered 8/4, 2018 at 5:45 Comment(4)
Any way you can push this to a repo so I can log some things on order to debug and test?Factoring
@Factoring Yeah gimme a minute. It uses Go obviously, and the library github.com/faiface/pixel -- just go get thatEdmonton
@Factoring added to the main postEdmonton
Thanks, I know someone already answered it but I’m going to download this and check it out anyway. I haven’t used any visual libraries with go before and want to mess around with it.Factoring
D
7

Note beforehand: I downloaded your example repo and applied my change on it, and it worked flawlessly. Here's a recording of it:

fixed cursor following

(for reference, GIF recorded with byzanz)


An easy and simple solution would be to not compare the angles (mouseRad and the changed p.rotateRad), but rather calculate and "normalize" the difference so it's in the range of -Pi..Pi. And then you can decide which way to turn based on the sign of the difference (negative or positive).

"Normalizing" an angle can be achieved by adding / subtracting 2*Pi until it falls in the -Pi..Pi range. Adding / subtracting 2*Pi won't change the angle, as 2*Pi is exactly a full circle.

This is a simple normalizer function:

func normalize(x float64) float64 {
    for ; x < -math.Pi; x += 2 * math.Pi {
    }
    for ; x > math.Pi; x -= 2 * math.Pi {
    }
    return x
}

And use it in your handleTurn() like this:

func (p *player) handleTurn(win pixelglWindow, dt float64) {
    // the angle the player needs to turn to face the mouse:
    mouseRad := math.Atan2(p.pos.Y-win.MousePosition().Y,
        win.MousePosition().X-p.pos.X)

    if normalize(mouseRad-p.rotateRad-(p.turnSpeed*dt)) > 0 {
        p.rotateRad += p.turnSpeed * dt
    } else if normalize(mouseRad-p.rotateRad+(p.turnSpeed*dt)) < 0 {
        p.rotateRad -= p.turnSpeed * dt
    }
}

You can play with it in this working Go Playground demo.

Note that if you store your angles normalized (being in the range -Pi..Pi), the loops in the normalize() function will have at most 1 iteration, so that's gonna be really fast. Obviously you don't want to store angles like 100*Pi + 0.1 as that is identical to 0.1. normalize() would produce correct result with both of these input angles, while the loops in case of the former would have 50 iterations, in the case of the latter would have 0 iterations.

Also note that normalize() could be optimized for "big" angles by using floating operations analogue to integer division and remainder, but if you stick to normalized or "small" angles, this version is actually faster.

Dallis answered 11/4, 2018 at 11:41 Comment(2)
Thanks for this, since I am only going to be using a "small" angle for now this answer is perfect. Great explanation too, it works perfect!Edmonton
@Edmonton Don't misunderstand, my solution works on all angles (not just for "small" ones), and you probably wouldn't even notice the performance difference. I'm just saying for "big" angles the normalization could be more efficient. For details, see this or this.Dallis
S
2

Preface: this answer assumes some knowledge of linear algebra, trigonometry, and rotations/transformations.

Your problem stems from the usage of rotation angles. Due to the discontinuous nature of the inverse trigonometric functions, it is quite difficult (if not outright impossible) to eliminate "jumps" in the value of the functions for relatively close inputs. Specifically, when x < 0, atan2(+0, x) = +pi (where +0 is a positive number very close to zero), but atan2(-0, x) = -pi. This is exactly why you experience the difference of 2 * pi which causes your problem.

Because of this, it is often better to work directly with vectors, rotation matrices and/or quaternions. They use angles as arguments to trigonometric functions, which are continuous and eliminate any discontinuities. In our case, spherical linear interpolation (slerp) should do the trick.

Since your code measures the angle formed by the relative position of the mouse to the absolute rotation of the object, our goal boils down to rotating the object such that the local axis (1, 0) (= (cos rotateRad, sin rotateRad) in world space) points towards the mouse. In effect, we have to rotate the object such that (cos p.rotateRad, sin p.rotateRad) equals (win.MousePosition().Y - p.pos.Y, win.MousePosition().X - p.pos.X).normalized.

How does slerp come into play here? Considering the above statement, we simply have to slerp geometrically from (cos p.rotateRad, sin p.rotateRad) (represented by current) to (win.MousePosition().Y - p.pos.Y, win.MousePosition().X - p.pos.X).normalized (represented by target) by an appropriate parameter which will be determined by the rotation speed.

Now that we have laid out the groundwork, we can move on to actually calculating the new rotation. According to the slerp formula,

slerp(p0, p1; t) = p0 * sin(A * (1-t)) / sin A + p1 * sin (A * t) / sin A

Where A is the angle between unit vectors p0 and p1, or cos A = dot(p0, p1).

In our case, p0 == current and p1 == target. The only thing that remains is the calculation of the parameter t, which can also be considered as the fraction of the angle to slerp through. Since we know that we are going to rotate by an angle p.turnSpeed * dt at every time step, t = p.turnSpeed * dt / A. After substituting the value of t, our slerp formula becomes

p0 * sin(A - p.turnSpeed * dt) / sin A + p1 * sin (p.turnSpeed * dt) / sin A

To avoid having to calculate A using acos, we can use the compound angle formula for sin to simplify this further. Note that the result of the slerp operation is stored in result.

result = p0 * (cos(p.turnSpeed * dt) - sin(p.turnSpeed * dt) * cos A / sin A) + p1 * sin(p.turnSpeed * dt) / sin A

We now have everything we need to calculate result. As noted before, cos A = dot(p0, p1). Similarly, sin A = abs(cross(p0, p1)), where cross(a, b) = a.X * b.Y - a.Y * b.X.

Now comes the problem of actually finding the rotation from result. Note that result = (cos newRotation, sin newRotation). There are two possibilities:

  1. Directly calculate rotateRad by p.rotateRad = atan2(result.Y, result.X), or
  2. If you have access to the 2D rotation matrix, simply replace the rotation matrix with the matrix

    |result.X -result.Y|
    |result.Y  result.X|
    
Sukiyaki answered 11/4, 2018 at 5:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.