一旦玩家的旋转速度达到 pi,它就会进行 360 度旋转

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

使用 Golang 制作游戏,因为它似乎非常适合游戏。我让玩家始终面对鼠标,但想要一个转身率来让某些角色转得比其他角色慢。以下是它如何计算转弯圆:

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
    }
}

mouseRad 是面向鼠标的转弯弧度,我只是添加转弯速率 [在本例中为 2]。

发生的事情是当鼠标到达左侧并穿过中心 y 轴时,弧度角从 -pi 变为 pi,反之亦然。这会导致播放器进行完整的 360 度旋转。

解决这个问题的正确方法是什么?我试过使角度成为绝对值,但它只使它出现在 pi 和 0 [左 正方形中心 y 轴的右侧]。

我附上了问题的 gif 图像以提供更好的可视化效果。

基本总结:

播放器缓慢旋转以跟随鼠标,但当角度达到 pi 时,它会改变极性,导致播放器进行 360 度旋转 [将所有后退计数到相反的极性角度]。

编辑: dt 是增量时间,显然只是为了适当的帧解耦运动变化

p.rotateRad 从 0 开始,是一个 float64。

Github 临时回购:here

您需要 this library 来构建它! [去拿它]

前言:此答案假定您具有线性代数、三角学和 rotations/transformations.

的一些知识

您的问题源于旋转角度的使用。由于反三角函数的不连续性,很难(如果不是完全不可能)消除相对接近输入的函数值中的 "jumps"。具体来说,当 x < 0atan2(+0, x) = +pi(其中 +0 是一个非常接近于零的正数),但 atan2(-0, x) = -pi。这正是您遇到导致问题的 2 * pi 差异的原因。

因此,直接使用向量、旋转矩阵 and/or 四元数通常会更好。他们使用角度作为三角函数的参数,三角函数是连续的并且消除了任何不连续性。在我们的例子中,spherical linear interpolation (slerp) 应该可以解决问题。

由于您的代码测量鼠标的相对位置与对象的绝对旋转形成的角度,我们的目标归结为旋转对象,使得 local(1, 0) (= (cos rotateRad, sin rotateRad) in world space) 指向鼠标。实际上,我们必须旋转对象,使 (cos p.rotateRad, sin p.rotateRad) 等于 (win.MousePosition().Y - p.pos.Y, win.MousePosition().X - p.pos.X).normalized.

slerp 在这里如何发挥作用?考虑到上面的语句,我们只需要通过一个适当的参数将slerp geometrically(cos p.rotateRad, sin p.rotateRad)(用current表示)到(win.MousePosition().Y - p.pos.Y, win.MousePosition().X - p.pos.X).normalized(用target表示)由转速决定。

现在我们已经奠定了基础,我们可以继续实际计算新的旋转。根据slerp公式,

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

其中A是单位向量p0p1之间的夹角,或者cos A = dot(p0, p1).

在我们的例子中,p0 == currentp1 == target。唯一剩下的就是参数 t 的计算,它也可以被认为是 slerp through 的角度分数。因为我们知道我们将在每个时间步旋转一个角度 p.turnSpeed * dt,所以 t = p.turnSpeed * dt / A。代入t的值后,我们的slerp公式就变成了

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

为了避免必须使用 acos 计算 A,我们可以使用 sin 的复合角公式进一步简化。注意slerp操作的结果存放在result.

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

我们现在拥有计算 result 所需的一切。如前所述,cos A = dot(p0, p1)。同样,sin A = abs(cross(p0, p1)),其中 cross(a, b) = a.X * b.Y - a.Y * b.X.

现在是从 result 实际找到旋转的问题。请注意 result = (cos newRotation, sin newRotation)。有两种可能:

  1. 通过p.rotateRad = atan2(result.Y, result.X)直接计算rotateRad,或者
  2. 如果可以访问二维旋转矩阵,只需将旋转矩阵替换为矩阵

    |result.X -result.Y|
    |result.Y  result.X|
    

事先注意:我下载了您的示例存储库并应用了我的更改,它运行完美。这是它的录音:

(供参考,用byzanz录制的GIF)


一个简单易行的解决方案是不比较角度mouseRad和改变后的p.rotateRad),而是计算 "normalize" 差 ,所以它在 -Pi..Pi 的范围内。然后你可以根据差值的符号决定转向

"Normalizing"一个角度可以通过加/减2*Pi直到落在-Pi..Pi范围内。添加/减去 2*Pi 不会改变角度,因为 2*Pi 正好是一个完整的圆。

这是一个简单的标准化函数:

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

然后像这样在 handleTurn() 中使用它:

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
    }
}

您可以在此工作 Go Playground 演示中使用它。

请注意,如果您将角度归一化(在 -Pi..Pi 范围内),normalize() 函数中的循环将最多进行 1 次迭代,因此速度会非常快。显然,您不想存储像 100*Pi + 0.1 这样的角度,因为它与 0.1 相同。 normalize() 将使用这两个输入角度产生正确的结果,而前者的循环将有 50 次迭代,后者的情况下将有 0 次迭代。

另请注意,normalize() 可以通过使用类似于整数除法和余数的浮动运算针对 "big" 角度进行优化,但如果您坚持使用标准化或 "small" 角度,则此版本实际上更快。