PROGRAMAÇÃO DE PROGRAMAÇÃO DE GAMES DE FÍSICA BOX2D - Orientando um objeto tipo torre usando torques

StackOverflow https://stackoverflow.com/questions/2617476

  •  26-09-2019
  •  | 
  •  

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"?

Foi útil?

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.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top