Search code examples
mathgotriggersmousegame-physics

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


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]


Solution

  • 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.