PROGRAMAÇÃO DE PROGRAMAÇÃO DE GAMES DE FÍSICA BOX2D - Orientando um objeto tipo torre usando torques
Pergunta
Este é um problema que eu atingi ao tentar implementar um jogo usando o AMOR motor, que cobre Box2d com script de lua.
O objetivo é simples: um objeto semelhante à torre (visto de cima, em um ambiente 2D) precisa se orientar para que aponte para um alvo.
A torre está nas coordenadas x, y, e o alvo está no tx, ty. Podemos considerar que x, y são fixos, mas o TX, tendem a variar de um instante para o outro (ou seja, eles seriam o cursor do mouse).
A torre possui um rotor que pode aplicar uma força rotacional (torque) em um determinado momento, no sentido horário ou no sentido anti-horário. A magnitude dessa força tem um limite superior chamado maxtorque.
A torre também possui certa inércia rotacional, que atua pelo movimento angular da mesma maneira que a massa atua para o movimento linear. Não há nenhum tipo de atrito, então a torre continuará girando se tiver uma velocidade angular.
A torre possui uma pequena função de IA que reavalia sua orientação para verificar se aponta para a direção certa e ativa o rotador. Isso acontece a cada DT (~ 60 vezes por segundo). Parece que isso agora:
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
... Falha. Deixe -me explicar com duas situações ilustrativas:
- A torre "oscila" em torno do Targetangle.
- Se o alvo estiver "logo atrás da torre, apenas um pouco de relógio", a torre começará a aplicar torques no sentido horário e continuará aplicando-os até o instante em que ultrapassará o ângulo de destino. Naquele momento, ele começará a aplicar torques na direção oposta. Mas terá uma velocidade angular significativa, por isso continuará no sentido horário por algum tempo ... até que o alvo fique "logo atrás, mas um pouco no sentido anti-horário". E isso começará de novo. Portanto, a torre oscila ou até mesmo vai em círculos redondos.
Eu acho que minha torre deve começar a aplicar torques na "direção oposta do caminho mais curto" antes de atingir o ângulo alvo (como uma frenagem de carro antes de parar).
Intuitivamente, acho que a torre deve "começar a aplicar torques na direção oposta do caminho mais curto quando está no meio do objetivo do objetivo". Minha intuição me diz que tem algo a ver com a velocidade angular. E há o fato de que o alvo é móvel - não sei se devo levar isso em consideração de alguma forma ou simplesmente ignorá -lo.
Como faço para calcular quando a torre deve "começar a frear"?
Solução 3
Ok, eu acredito que consegui a solução.
Isso se baseia na ideia da versão beta, mas com alguns ajustes necessários. Aqui vai:
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
O conceito por trás disso é simples: preciso calcular quanto "espaço" (ângulo) a torre precisa para parar completamente. Isso depende da rapidez com que a torre se move e de quanto torque pode se aplicar a si mesmo. Em poucas palavras, é isso que eu calculo com brakingAngle
.
Minha fórmula para calcular esse ângulo é ligeiramente diferente dos beta. Um amigo meu me ajudou com a física e, bem, eles parecem estar funcionando. Adicionando o sinal de W foi minha ideia.
Eu tive que implementar uma função "normalizando", que coloca qualquer ângulo de volta à zona de 0-2pi.
Inicialmente, este era um if-else-else emaranhado. Desde as condições onde muito repetitivas, usei alguns lógica booleana Para simplificar o algoritmo. A desvantagem é que, mesmo que funcione bem e não seja complicado, não transpira por que funciona.
Depois que o código estiver um pouco mais dependido, postarei um link para uma demonstração aqui.
Muito obrigado.
Editar: A amostra de trabalho de trabalho está agora disponível aqui. O material importante é dentro dos atores/ai.lua (o arquivo .Love pode ser aberto com um Zip Uncpressor)
Outras dicas
Pense para trás. A torre deve "começar a frear" quando tiver espaço suficiente para desacelerar de sua velocidade angular atual para uma parada morta, que é a mesma que a sala precisaria acelerar de uma parada morta para sua velocidade angular atual, que é
|differenceAngle| = w^2*Inertia/2*MaxTorque.
Você também pode ter alguns problemas com pequenas oscilações em torno do alvo se o tempo de passo for muito grande; Isso exigirá um pouco mais de delicadeza, você terá que frear um pouco mais cedo e mais suavemente. Não se preocupe com isso até ver.
Isso deve ser bom o suficiente por enquanto, mas há outra captura que pode viajá -lo mais tarde: decidir o caminho a seguir. Às vezes, percorrer o longo caminho é mais rápido, se você já está indo por esse caminho. Nesse caso, você deve decidir de que maneira leva menos tempo, o que não é difícil, mas, novamente, atravessa essa ponte quando chegar a ela.
EDITAR:
Minha equação estava errada, deve ser inércia/2*maxtorque, não 2*maxtorque/inércia (é isso que recebo por tentar fazer álgebra no teclado). Eu consertei isso.
Experimente isso:
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)
Isso parece ser um problema que pode ser resolvido com um Controlador PID. Eu os uso no meu trabalho para controlar uma saída de aquecedor para definir uma temperatura.
Para o componente 'P', você aplica um torque proporcional à diferença entre o ângulo da torre e o ângulo alvo, ou seja,
P = P0 * differenceAngle
Se isso ainda oscilar demais (será um pouco), adicione um componente 'i',
integAngle = integAngle + differenceAngle * dt
I = I0 * integAngle
Se isso ultrapassar demais, adicione um termo 'd'
derivAngle = (prevDifferenceAngle - differenceAngle) / dt
prevDifferenceAngle = differenceAngle
D = D0 * derivAngle
P0
, I0
e D0
são constantes que você pode sintonizar para obter o comportamento que deseja (ou seja, quão rápido as torres respondem etc.)
Apenas como uma dica, normalmente P0
> I0
> D0
Use estes termos para determinar quanto torque é aplicado, ou seja,
magnitudeAngMomentum = P + I + D
EDITAR:
Aqui está um aplicativo escrito usando Em processamento que usa PID. Na verdade funciona bem sem eu ou D. vejo isso funcionando aqui
// 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;
}
Você pode encontrar uma equação para velocidade angular versus distância angular para o rotor quando acelerar o torque é aplicado e encontrar a mesma equação para quando o torque de frenagem é aplicado.
Em seguida, modifique a equação de quebra de modo que ele intense o eixo de distância angular no ângulo necessário. Com essas duas equações, você pode calcular a distância angular na qual elas se cruzam, o que lhe daria o ponto de ruptura.
Pode estar totalmente errado, porém, não é feito assim por muito tempo. Provavelmente uma solução mais simples. Suponho que a aceleração não seja linear.
Uma versão simplificada desse problema é bastante simples de resolver. Suponha que o motor tenha um torque infinito, ou seja, ele pode mudar a velocidade instantaneamente. Obviamente, isso não é fisicamente preciso, mas torna o problema muito mais simples de resolver e, no final, não é um problema.
Concentre -se em uma velocidade angular alvo, não em um ângulo de destino.
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 );
A razão pela qual isso funciona é que a torre tenta automaticamente se mover mais devagar quando atinge seu ângulo de destino.
O torque infinito é mascarado pelo fato de a torre não tentar fechar a distância instantaneamente. Em vez disso, ele tenta fechar a distância em um timestep. Também como o alcance de -Pi a Pi é bem pequeno, as acelerações possivelmente insanas nunca se mostram. A velocidade angular máxima mantém as rotações da torre parecendo realistas.
Eu nunca elaborei a equação real para resolver com torque em vez de velocidade angular, mas imagino que ela se pareça muito com as equações do PID.