2010-04-11 11 views
8

Este es un problema que golpeo al intentar implementar un juego usando el motor LÖVE, que cubre box2d con secuencias de comandos Lua.programación de juegos de física box2d - orientación de un objeto parecido a una torreta usando torques

El objetivo es simple: un objeto tipo torre (visto desde la parte superior, en un entorno 2D) necesita orientarse para apuntar a un objetivo.

La torre está en las coordenadas x, y, y el objetivo está en tx, ty. Podemos considerar que x, y son fijas, pero tx, ty tienden a variar de un instante a otro (es decir, serían el cursor del mouse).

La torreta tiene un rotor que puede aplicar una fuerza de rotación (torque) en cualquier momento dado, en sentido horario o antihorario. La magnitud de esa fuerza tiene un límite superior llamado maxTorque.

La torreta también tiene cierta inercia de rotación, que actúa para el movimiento angular de la misma manera que la masa actúa para el movimiento lineal. No hay fricción de ningún tipo, por lo que la torreta seguirá girando si tiene una velocidad angular.

La torreta tiene una pequeña función AI que reevalúa su orientación para verificar que apunta en la dirección correcta y activa el rotador. Esto sucede cada dt (~ 60 veces por segundo). Se ve así ahora:

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 

... falla. Permítanme explicarlo con dos situaciones ilustrativas:

  • La torreta "oscila" alrededor del ángulo de destino.
  • Si el objetivo está "justo detrás de la torreta, solo un poco en el sentido de las agujas del reloj", la torreta comenzará a aplicar torques en sentido horario y seguirá aplicándolos hasta el instante en que supere el ángulo deseado. En ese momento comenzará a aplicar pares en la dirección opuesta. Pero habrá ganado una velocidad angular significativa, por lo que seguirá funcionando en el sentido de las agujas del reloj durante algún tiempo ... hasta que el objetivo quede "justo detrás, pero un poco en sentido contrario a las agujas del reloj". Y comenzará de nuevo. Entonces la torreta oscilará o incluso irá en círculos redondos.

Creo que mi torreta debería comenzar a aplicar pares en la "dirección opuesta a la ruta más corta" antes de que alcance el ángulo objetivo (como un frenado de automóvil antes de detenerse).

Intuitivamente, creo que la torreta debería "comenzar a aplicar pares en la dirección opuesta de la ruta más corta cuando está a mitad de camino del objetivo objetivo". Mi intuición me dice que tiene algo que ver con la velocidad angular. Y luego está el hecho de que el objetivo es móvil: no sé si debería tomar eso en cuenta de alguna manera o simplemente ignorarlo.

¿Cómo calculo cuándo la torreta debe "comenzar a frenar"?

Respuesta

1

Ok, creo que tengo la solución.

Esto se basa en la idea de Beta, pero con algunos ajustes necesarios. Aquí va:

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 

El concepto detrás de esto es simple: Tengo que calcular la cantidad de "espacio" (ángulo) necesita la torreta con el fin de detener por completo. Eso depende de qué tan rápido se mueva la torreta y cuánto torque pueda aplicar a sí mismo. En pocas palabras, eso es lo que calculo con brakingAngle.

Mi fórmula para calcular este ángulo es ligeramente diferente de la de Beta. Un amigo mío me ayudó con la física, y bueno, parece que están funcionando. Agregar el signo de w fue mi idea.

Tuve que implementar una función de "normalización", que devuelve cualquier ángulo a la zona 0-2Pi.

Inicialmente esto era un if-else-if-else enredado. Dado que las condiciones son muy repetitivas, utilicé algunos boolean logic para simplificar el algoritmo. El inconveniente es que, incluso si funciona bien y no es complicado, no sucede por qué funciona.

Una vez que el código sea un poco más depurado, publicaré un enlace a una demostración aquí.

Muchas gracias.

EDITAR: La muestra LÖVE de trabajo ya está disponible here. Lo importante es dentro de actores/AI.lua (el archivo .love se puede abrir con un descompresor zip)

0

Puede encontrar una ecuación de velocidad angular vs distancia angular para el rotor cuando se aplica un par de aceleración, y encuentra la misma ecuación para cuando se aplica el par de frenado.

A continuación, modifique la ecuación de ruptura de modo que integre el eje de distancia angular en el ángulo requerido. Con estas dos ecuaciones puede calcular la distancia angular a la que se cruzan, lo que le daría el punto de ruptura.

Podría ser totalmente erróneo, sin embargo, no se ha hecho así durante mucho tiempo. Probablemente una solución más simple. Supongo que la aceleración no es lineal.

1

Parece un problema que se puede resolver con PID controller. Los uso en mi trabajo para controlar la salida de un calentador y establecer una temperatura.

Para el componente 'P', se aplica un par de torsión que es proporcional a la diferencia entre el ángulo de la torreta y el ángulo objetivo es decir

P = P0 * differenceAngle

Si esto todavía oscila demasiado (lo hará un poco) a continuación, añadir un componente 'i',

integAngle = integAngle + differenceAngle * dt 
I = I0 * integAngle

Si esto sobrepasa demasiado, entonces agregar una 'D' plazo

derivAngle = (prevDifferenceAngle - differenceAngle)/dt 
prevDifferenceAngle = differenceAngle 
D = D0 * derivAngle

P0, I0 y D0 son constantes que puede ajustar para obtener el comportamiento que desea (es decir, la rapidez con las torretas responden etc.)

Así como una punta, normalmente P0>I0>D0

usan estos términos para determinar la cantidad de par se aplica es decir

magnitudeAngMomentum = P + I + D

EDIT:

Aquí hay una aplicación escrita usando Processing que usa PID. En realidad funciona bien sin I o D. ver su funcionamiento here


// 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; 
} 
+0

El problema con este enfoque es que está diseñado para cosas como sistemas de calefacción, donde lo que controlas es energía, que es la primera derivada de temperatura; egarcia controla el torque, que es el segundo. P rebasará violentamente porque su objetivo es a = 0, no w = 0, I no ayuda con la oscilación, D podría funcionar pero hará que el proceso LENTO. – Beta

+0

Tiene razón, el ejemplo que doy no trata directamente con el par de torsión. Sin embargo, 'maxAngleMomentum' es proporcional a' maxTorque' cuando se considera la fricción en el 'mecanismo' de rotación de la torreta, se pueden considerar intercambiables cuando se usan unidades arbitrarias. – Brendan

+0

La implementación se ve bien. La idea de "conservar el momento angular" es interesante. Sin embargo, esto no es lo que estaba pidiendo, quería aplicar pares, mientras que al final está configurando el ángulo usted mismo. Pero +1 por hacer una demostración y elegancia de código. – kikito

2

Piense hacia atrás. La torreta debe "comenzar a frenar" cuando tiene espacio suficiente para desacelerar desde su velocidad angular actual a una parada muerta, que es la misma que la habitación que necesitaría para acelerar de una parada a su velocidad angular actual, que es

|differenceAngle| = w^2*Inertia/2*MaxTorque. 

También puede tener algunos problemas con pequeñas oscilaciones alrededor del objetivo si su tiempo de paso es demasiado grande; eso requerirá un poco más de delicadeza, tendrás que frenar un poco más rápido y con más cuidado. No te preocupes hasta que lo veas.

Eso debería ser lo suficientemente bueno por ahora, pero hay otro truco que puede hacerte tropear más tarde: decidir qué camino tomar. A veces, recorrer el largo camino es más rápido, si ya vas por ese camino. En ese caso, debes decidir qué camino toma menos tiempo, lo que no es difícil, pero una vez más, cruza ese puente cuando llegues a él.

EDIT:
Mi ecuación estaba mal, debe ser Inercia/2 * maxTorque, no maxTorque 2 */Inercia (eso es lo que me pasa por tratar de hacer el álgebra en el teclado). Lo he arreglado

Prueba esto:

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) 
+0

Hola Beta, gracias por responder. Debería haber mencionado que mis matemáticas no son muy fuertes. ¿Qué se supone que debo hacer con esa ecuación? Algo parece estar implícito para ti, pero simplemente no puedo verlo. – kikito

+0

mm He pensado más.De acuerdo con mis humildes intentos, angle = MaxTorque * t * t/inercia, durante un período de tiempo dado t. Cómo se pasa de esa ecuación a la que se muestra en su ejemplo se me escapa. – kikito

+0

Casi lo tienes: aceleración alpha = maxTorque/inercia, w = alpha * t, pero para calcular el ángulo debes usar la velocidad promedio durante el período de tiempo, entonces angle = (maxTorque * t/Inertia) * t/2 . Ahora tome w = alpha * t y lo cuadre: w * w = alpha * alpha * t * t, y úselo para deshacerse de t * t: angle = alpha * t * t/2 = alpha * (w * w/alpha * alpha)/2 = w * w/2 * alpha = w * w * Inercia/2 * maxTorque. – Beta

0

Una versión simplificada de este problema es bastante simple de resolver. Supongamos que el motor tiene un par infinito, es decir, puede cambiar la velocidad de forma instantánea. Obviamente, esto no es físicamente preciso, pero hace que el problema sea mucho más simple de resolver y, al final, no es un problema.

Enfoque en una velocidad angular objetivo no en un ángulo objetivo.

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); 

La razón por la que esto funciona es que la torreta intenta automáticamente moverse más despacio cuando alcanza su ángulo objetivo.

El par infinito está enmascarado por el hecho de que la torreta no intenta cerrar la distancia de forma instantánea. En cambio, trata de cerrar la distancia en un paso de tiempo. Además, dado que el rango de -pi a pi es bastante pequeño, las aceleraciones posiblemente insanas nunca se muestran. La velocidad angular máxima mantiene la rotación realista de la torreta.

Nunca he resuelto la ecuación real para resolver con torque en lugar de velocidad angular, pero imagino que se parecerá mucho a las ecuaciones PID.

+0

Me gusta su Idea y la tendré en cuenta para el futuro. Gracias por compartirlo. Si está interesado, logré encontrar la ecuación y programar un ejemplo de trabajo. – kikito

Cuestiones relacionadas