Скорость поворота на вращение игрока, совершающего 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 проблемы, чтобы лучше визуализировать.
Основное обобщение:
Игрок медленно вращается, чтобы следовать за мышью, но когда угол достигает pi, он изменяет полярность, которая заставляет игрока делать 360 [отсчитывает все обратные к противоположному полю полярности].
Изменение: dt - время дельта, только для правильных изменений в движении,
p.rotateRad начинается с 0 и является float64.
Github repo временно: здесь
Вам нужна эта библиотека для ее создания! [иди возьми это]
Ответы
Ответ 1
Запомните заранее: я загрузил ваш пример repo и применил свои изменения на нем, и он работал безупречно. Здесь его запись:
(для справки, 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)
. Есть две возможности:
- Непосредственно вычислите
rotateRad
помощью p.rotateRad = atan2(result.Y, result.X)
или -
Если у вас есть доступ к матрице 2D-преобразования, просто замените матрицу вращения матрицей
|result.X -result.Y|
|result.Y result.X|