Скорость поворота на вращение игрока, совершающего 360 раз, когда он попадает на 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
    }
}

МышьРад - радиан для поворота, чтобы смотреть в лицо мыши, и я просто добавляю скорость поворота [в данном случае, 2].

То, что происходит, когда мышь достигает левой стороны и пересекает центральную ось y, радианный угол идет от -pi к pi или наоборот. Это заставляет игрока делать полный 360.

Каков правильный способ исправить это? Я попытался сделать угол абсолютным значением, и он только сделал это в pi и 0 [в левой и правой частях квадрата по оси y].

Я приложил gif проблемы, чтобы лучше визуализировать. gif

Основное обобщение:

Игрок медленно вращается, чтобы следовать за мышью, но когда угол достигает pi, он изменяет полярность, которая заставляет игрока делать 360 [отсчитывает все обратные к противоположному полю полярности].

Изменение: dt - время дельта, только для правильных изменений в движении,

p.rotateRad начинается с 0 и является float64.

Github repo временно: здесь

Вам нужна эта библиотека для ее создания! [иди возьми это]

Ответ 1

Запомните заранее: я загрузил ваш пример repo и применил свои изменения на нем, и он работал безупречно. Здесь его запись:

fixed cursor following

(для справки, GIF записан с byzanz)


mouseRad и простым решением было бы не сравнивать углы (mouseRad и измененный p.rotateRad), а скорее вычислять и "нормализовать" разницу, чтобы она -Pi..Pi в диапазоне -Pi..Pi. И тогда вы можете решить, каким образом повернуть на основе знака разницы (отрицательной или положительной).

"Нормализация" угла может быть достигнута путем добавления/вычитания 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() может быть оптимизирован для "больших" углов, используя плавающие операции с аналоговым целым делением и остатком, но если вы придерживаетесь нормализованных или "малых" углов, эта версия работает быстрее.

Ответ 2

Предисловие: этот ответ предполагает некоторое знание линейной алгебры, тригонометрии и поворотов/преобразований.

Ваша проблема связана с использованием углов поворота. Из-за прерывистого характера обратных тригонометрических функций довольно сложно (если не прямо невозможно) устранить "скачки" в значении функций для относительно близких входных данных. В частности, когда x < 0, atan2(+0, x) = +pi (где +0 - положительное число, очень близкое к нулю), но atan2(-0, x) = -pi. Именно поэтому вы ощущаете разницу в 2 * pi которая вызывает вашу проблему.

Из-за этого часто лучше работать непосредственно с векторами, матрицами вращения и/или кватернионами. Они используют углы в качестве аргументов для тригонометрических функций, которые являются непрерывными и исключают любые разрывы. В нашем случае сферическая линейная интерполяция (slerp) должна делать трюк.

Поскольку ваш код измеряет угол, образованный относительным положением мыши, к абсолютному вращению объекта, наша цель сводится к вращению объекта таким образом, что локальная ось (1, 0) (= (cos rotateRad, sin rotateRad) в мировое пространство) указывает на мышь. Фактически, мы должны повернуть объект таким образом, чтобы (cos p.rotateRad, sin p.rotateRad) равно (win.MousePosition().Y - p.pos.Y, win.MousePosition().X - p.pos.X).normalized.

Как здесь происходит игра? Учитывая вышеприведенное утверждение, нам просто нужно геометрически вырезать из (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 - угол между единичными векторами p0 и p1, или cos A = dot(p0, p1).

В нашем случае p0 == current и p1 == target. Остается только вычисление параметра t, которое также можно рассматривать как долю угла до проскальзывания. Поскольку мы знаем, что мы будем вращаться под углом 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

Чтобы избежать вычисления A с помощью acos, мы можем использовать формулу составного угла для 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) = aX * bY - aY * bX.

Теперь возникает проблема фактического нахождения вращения от result. Обратите внимание, что result = (cos newRotation, sin newRotation). Есть две возможности:

  1. Непосредственно вычислите rotateRad помощью p.rotateRad = atan2(result.Y, result.X) или
  2. Если у вас есть доступ к матрице 2D-преобразования, просто замените матрицу вращения матрицей

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