Это - проблема, которую я поразил при попытке реализовать игру с помощью механизма LÖVE, который покрывает box2d сценариями Lua.
Цель проста: подобный башенке объект (замеченный от вершины, на 2D среде) должен ориентировать себя так, это указывает на цель.
Башенка находится на x, y координаты, и цель находится на tx, ty. Мы можем полагать, что x, y фиксируются, но tx, ty имеют тенденцию варьироваться с одного момента к другому (т.е. они были бы курсором мыши).
Башенка имеет ротор, который может применить вращательную силу (крутящий момент) на любой данный момент, по часовой стрелке или против часовой стрелки. Величина той силы имеет верхний предел, названный maxTorque.
Башенка также имеет определенную вращательную инерцию, которая действует углового перемещения тем же путем массовые действия для линейного перемещения. Нет никакого трения никакого вида, таким образом, башенка будет продолжать вращаться, если это будет иметь угловую скорость.
Башенка имеет небольшую функцию AI, которая переоценивает ее ориентацию, чтобы проверить, что она указывает на правильное направление и активирует вращающее устройство. Это происходит каждый dt (~60 раз в секунду). Это похоже на это прямо сейчас:
function Turret:update(dt)
local x,y = self:getPositon()
local tx,ty = self:getTarget()
local maxTorque = self:getMaxTorque() -- max force of the turret rotor
local inertia = self:getInertia() -- the rotational inertia
local w = self:getAngularVelocity() -- current angular velocity of the turret
local angle = self:getAngle() -- the angle the turret is facing currently
-- the angle of the like that links the turret center with the target
local targetAngle = math.atan2(oy-y,ox-x)
local differenceAngle = _normalizeAngle(targetAngle - angle)
if(differenceAngle <= math.pi) then -- counter-clockwise is the shortest path
self:applyTorque(maxTorque)
else -- clockwise is the shortest path
self:applyTorque(-maxTorque)
end
end
... это перестало работать. Позвольте мне объяснить с двумя иллюстративными ситуациями:
Я думаю, что моя башенка должна начать применять крутящие моменты в "противоположном направлении кратчайшего пути", прежде чем это достигнет целевого угла (как автомобиль, тормозящий прежде, чем остановиться).
Интуитивно, я думаю, что башенка должна "начать применять крутящие моменты на противоположное направление кратчайшего пути, когда это о на полпути к целевому уровню". Моя интуиция говорит мне, что имеет некоторое отношение к угловой скорости. И затем существует то, что цель мобильна - я не знаю, должен ли я принять это во внимание так или иначе или просто проигнорировать ее.
Как я вычисляю, когда башенка должна "начать тормозить"?
Хорошо, я считаю, что нашел решение.
Это основано на идее Беты, но с некоторыми необходимыми изменениями. Вот оно:
local twoPi = 2.0 * math.pi -- small optimisation
-- returns -1, 1 or 0 depending on whether x>0, x<0 or x=0
function _sign(x)
return x>0 and 1 or x<0 and -1 or 0
end
-- transforms any angle so it is on the 0-2Pi range
local _normalizeAngle = function(angle)
angle = angle % twoPi
return (angle < 0 and (angle + twoPi) or angle)
end
function Turret:update(dt)
local tx, ty = self:getTargetPosition()
local x, y = self:getPosition()
local angle = self:getAngle()
local maxTorque = self:getMaxTorque()
local inertia = self:getInertia()
local w = self:getAngularVelocity()
local targetAngle = math.atan2(ty-y,tx-x)
-- distance I have to cover
local differenceAngle = _normalizeAngle(targetAngle - angle)
-- distance it will take me to stop
local brakingAngle = _normalizeAngle(_sign(w)*2.0*w*w*inertia/maxTorque)
local torque = maxTorque
-- two of these 3 conditions must be true
local a,b,c = differenceAngle > math.pi, brakingAngle > differenceAngle, w > 0
if( (a and b) or (a and c) or (b and c) ) then
torque = -torque
end
self:applyTorque(torque)
end
Идея проста: мне нужно вычислить, сколько «пространства» (угла) требуется турели, чтобы полностью остановиться. Это зависит от того, насколько быстро движется турель и какой крутящий момент она может приложить к себе. Вкратце, это то, что я вычисляю с помощью brakingAngle
.
Моя формула для вычисления этого угла немного отличается от формулы Беты. Мой друг помог мне с физикой, и, ну, похоже, они работают. Добавление знака w было моей идеей.
Мне пришлось реализовать функцию «нормализации», которая возвращает любой угол в зону 0–2Pi.
Изначально это было запутанное if-else-if-else. Поскольку условия очень часто повторяются, я использовал некоторую булеву логику , чтобы упростить алгоритм. Обратной стороной является то, что даже если он работает нормально и несложно, непонятно, почему он работает.
Как только код станет более чистым, я размещу здесь ссылку на демо.
Большое спасибо.
РЕДАКТИРОВАТЬ: Рабочий образец LÖVE теперь доступен здесь . Важная информация находится внутри агентов / AI.lua (файл .love можно открыть с помощью распаковщика zip)
Думайте задом наперед. Турель должна "начать торможение", когда у нее будет достаточно места для замедления от текущей угловой скорости до мертвой остановки, то есть столько же места, сколько ей нужно для ускорения от мертвой остановки до текущей угловой скорости, то есть
|differenceAngle| = w^2*Inertia/2*MaxTorque.
У вас также могут возникнуть проблемы с небольшими колебаниями вокруг цели, если ваше время шага слишком велико; это потребует немного больше изящества, вам придется тормозить немного раньше и более мягко. Не беспокойтесь об этом, пока не увидите.
Этого должно быть достаточно на данный момент, но есть еще одна загвоздка, которая может поставить вас в тупик в дальнейшем: выбор пути. Иногда идти длинным путем быстрее, если вы уже идете в ту сторону. В этом случае вам нужно решить, какой путь займет меньше времени, что несложно, но, опять же, переходите этот мост, когда до него дойдете.
EDIT:
Мое уравнение было неправильным, оно должно быть Inertia/2*maxTorque, а не 2*maxTorque/Inertia (вот что я получаю за попытки заниматься алгеброй на клавиатуре). Я исправил это.
Попробуйте так:
local torque = maxTorque;
if(differenceAngle > math.pi) then -- clockwise is the shortest path
torque = -torque;
end
if(differenceAngle < w*w*Inertia/(2*MaxTorque)) then -- brake
torque = -torque;
end
self:applyTorque(torque)
Вы можете найти уравнение для зависимости угловой скорости от углового расстояния для ротора при приложении ускоряющего момента и найти такое же уравнение для случая, когда приложен тормозной момент.
Затем измените уравнение разрыва таким образом, чтобы оно пересекало ось углового расстояния под требуемым углом. С помощью этих двух уравнений вы можете рассчитать угловое расстояние, на котором они пересекаются, что даст вам точку излома.
Могу быть совершенно неправильным, не делал ничего подобного в течение долгого времени. Наверное, более простое решение. Я предполагаю, что ускорение не линейное.
Упрощенный вариант этой проблемы довольно просто решить. Предположим, что у двигателя бесконечный крутящий момент, то есть он может мгновенно изменять скорость. Это, очевидно, не является физически точным, но значительно упрощает решение проблемы и, в конце концов, не является проблемой.
Сосредоточьтесь на целевой угловой скорости, а не на заданном угле.
current_angle = "the turrets current angle";
target_angle = "the angle the turret should be pointing";
dt = "the timestep used for Box2D, usually 1/60";
max_omega = "the maximum speed a turret can rotate";
theta_delta = target_angle - current_angle;
normalized_delta = normalize theta_delta between -pi and pi;
delta_omega = normalized_deta / dt;
normalized_delta_omega = min( delta_omega, max_omega );
turret.SetAngularVelocity( normalized_delta_omega );
Причина, по которой это работает, заключается в том, что турель автоматически пытается двигаться медленнее, когда достигает заданного угла.
Бесконечный крутящий момент маскируется тем фактом, что турель не пытается мгновенно сократить расстояние. Вместо этого он пытается сократить расстояние за один шаг. Кроме того, поскольку диапазон от -pi до pi довольно мал, возможно, безумное ускорение никогда не проявляется. Благодаря максимальной угловой скорости вращения башни выглядят реалистично.
Я никогда не составлял реальное уравнение для решения с крутящим моментом вместо угловой скорости, но я полагаю, что оно будет очень похоже на уравнения ПИД.
Это похоже на проблему, которую можно решить с помощью ПИД-регулятора . Я использую их в своей работе, чтобы управлять мощностью нагревателя и задавать температуру.
Для компонента «P» вы применяете крутящий момент, который пропорционален разнице между углом револьверной головки и целевым углом, т.е.
P = P0 * differenceAngle
Если он все еще слишком сильно колеблется ( бит), затем добавьте компонент «I»,
integAngle = integAngle + differenceAngle * dt
I = I0 * integAngle
Если это слишком много, добавьте член «D»
derivAngle = (prevDifferenceAngle - differenceAngle) / dt
prevDifferenceAngle = differenceAngle
D = D0 * derivAngle
P0
, I0
и D0
являются константами которые вы можете настроить, чтобы получить желаемое поведение (например, скорость реакции турелей и т. д.)
Как подсказка, обычно P0
> I0
> D0
Используйте эти термины для определения приложенного крутящего момента, например
magnitudeAngMomentum = P + I + D
РЕДАКТИРОВАТЬ:
Вот приложение, написанное с использованием Обработки , которое использует PID. На самом деле он отлично работает без I или D. Посмотрите, как он работает здесь
// Demonstration of the use of PID algorithm to
// simulate a turret finding a target. The mouse pointer is the target
float dt = 1e-2;
float turretAngle = 0.0;
float turretMass = 1;
// Tune these to get different turret behaviour
float P0 = 5.0;
float I0 = 0.0;
float D0 = 0.0;
float maxAngMomentum = 1.0;
void setup() {
size(500, 500);
frameRate(1/dt);
}
void draw() {
background(0);
translate(width/2, height/2);
float angVel, angMomentum, P, I, D, diffAngle, derivDiffAngle;
float prevDiffAngle = 0.0;
float integDiffAngle = 0.0;
// Find the target
float targetX = mouseX;
float targetY = mouseY;
float targetAngle = atan2(targetY - 250, targetX - 250);
diffAngle = targetAngle - turretAngle;
integDiffAngle = integDiffAngle + diffAngle * dt;
derivDiffAngle = (prevDiffAngle - diffAngle) / dt;
P = P0 * diffAngle;
I = I0 * integDiffAngle;
D = D0 * derivDiffAngle;
angMomentum = P + I + D;
// This is the 'maxTorque' equivelant
angMomentum = constrain(angMomentum, -maxAngMomentum, maxAngMomentum);
// Ang. Momentum = mass * ang. velocity
// ang. velocity = ang. momentum / mass
angVel = angMomentum / turretMass;
turretAngle = turretAngle + angVel * dt;
// Draw the 'turret'
rotate(turretAngle);
triangle(-20, 10, -20, -10, 20, 0);
prevDiffAngle = diffAngle;
}