Posso pular de A para B?

10

Estou fazendo uma IA rudimentar para o meu side-scroller e preciso saber se uma unidade de IA pode alcançar o ponto B a partir do ponto A simplesmente dando um pulo.

A trajetória de vôo dos meus personagens é um pouco incomum, pois eles podem aplicar força no ar (como no Jazz Jackrabbit 2, por exemplo), tão diferente da trajetória clássica de um projétil que é sobre ...

caminho que um projétil lançado ou lançado seguirá (...) sem propulsão.

... Suponho que meu problema seja mais sobre um projétil com propulsão (por exemplo, foguete).

Para ilustrar isso, é assim que a curva de vôo se parece com o meu personagem se eu pular e pressionar continuamente o "botão esquerdo" (parece diferente no lado esquerdo, é aqui que eu estava fazendo algumas manobras no ar): insira a descrição da imagem aqui

A força aplicada durante o vôo é sempre paralela ao eixo X, então é F = (-f, 0) se eu segurar "esquerda" e é F = (f, 0) se eu segurar "direita".

Ele pode se mover muito como um saltador de esqui:

insira a descrição da imagem aqui

Por isso, difere muito da trajetória clássica que é simplesmente uma parábola (fonte: wikipedia ):

insira a descrição da imagem aqui

Para tornar mais difícil, estou simulando uma resistência simples ao ar para que meus personagens possam acelerar apenas até algum valor de velocidade máxima.

Isso é feito aplicando uma pequena força na direção oposta da viagem :

b2Vec2 vel = body->GetLinearVelocity();
float speed = vel.Normalize(); //normalizes vector and returns length
body->ApplyForce( AIR_RESISTANCE_MULT * speed * speed * -vel, body->GetWorldCenter() );

O AIR_RESISTANCE_MULT é uma constante que, no meu caso, é igual a 0,1.

Vamos supor que meu personagem seja um ponto infinitamente pequeno.

E NÃO estou levando em consideração os obstáculos, então minha pergunta é assim ...

Como determinar (pelo menos adivinhar com confiabilidade), dada a velocidade inicial V, um impulso J = (0, -j) que aplico ao personagem ao pular, gravidade G = (0, g) , força F = (+ -f , 0) é aplicado continuamente durante o vôo e AIR_RESISTANCE_MULT se realmente decidirmos levar em consideração a resistência do ar (isso é opcional) , se um ponto está abaixo da curva traçada pelo caminho que meu personagem seguirá?

Não tenho literalmente idéia de por onde começar com os cálculos e, de fato, não estou necessariamente interessado em uma resposta exata; um hack / aproximação que funcione bem seria ótimo, pois a IA nunca precisa funcionar perfeitamente.

edit: Eu decidi resolver isso usando simulação, como Jason sugere, mas como lidar com esse caso? insira a descrição da imagem aqui

Devo desenhar um segmento de C a D e verificar se o ponto desejado está abaixo desse segmento?

Ou devo procurar binário os timestados entre C e D para procurar o ponto que está suficientemente próximo na distância horizontal do ponto desejado e só então verificar a diferença vertical? (parece um pouco exagerado para mim)

Patryk Czachurski
fonte
Acho que encontrei uma solução para o caso em que não levamos em consideração a resistência do ar: gamedev.stackexchange.com/questions/37916/…
Patryk Czachurski

Respostas:

4

Conforme você declara, a melhor opção é aproximar, neste caso usando um esquema numérico. Divida o tempo em grandes intervalos de tempo (digamos 100-300ms) e use a aproximação parabólica para cada intervalo de tempo. As forças são as mesmas, exceto a resistência do ar. O caminho parabólico é basicamente para aceleração constante, mas com a resistência do ar, a aceleração muda porque a força depende da velocidade. Uma aproximação razoável é tratar a resistência do ar como constante a cada passo do tempo. Mas usar uma aproximação quadrática (isto é, parabólica) ao integrar permite lidar com timestados muito maiores. Então você apenas calcula até que uma parábola cruze o ponto desejado na direção horizontal e, em seguida, compare as alturas.

EDIT: Um pouco mais de detalhes sobre a comparação. Você sabe que ao longo do tempo (que pode ser grande parte dos quadros de jogo), o jogador cruza o alvo <targetx,targety>. Seu caminho é descrito pela posição em <ax*t^2 + bx*t + cx, ay*t^2 + by*t + cy>que:

ax = 1/2 * accel.x
bx = velocity.x
cx = position.x

té o tempo no timestep ( 0 <= t <= dt) e da mesma forma para y. Então, quando t=0o personagem está na posição anterior e quando t=dt, ele está na próxima posição. Observe que esta é basicamente a atualização de Euler dtsubstituída por, tpara que possamos calcular em qualquer lugar ao longo da trajetória. Agora sabemos que a posição x é uma função quadrática, para que possamos resolver ax*t^2 + bx*t + cx = targetx e obter (até) duas vezes durante a etapa em que o personagem está diretamente acima ou abaixo do alvo. Em seguida, descartamos quaisquer soluções que não estejam no intervalo [0,dt], pois esses não estão no timestep atual. (Para maior robustez, adicione uma pequena constante às extremidades do intervalo para não ter problemas de arredondamento). Agora, não poderíamos ter soluções (após a filtragem), caso em que não atingimos o alvo neste passo de tempo. Caso contrário, avaliamos ay*t^2 + by*t + cyas soluções e comparamos isso com targety. Observe que você pode estar acima do alvo em um ponto da sua trajetória e abaixo dele mais tarde (ou vice-versa). Você precisará interpretar essas situações de acordo com o que deseja fazer.

Considerar vários passos de tempo é muito mais fácil do que encontrar uma solução analítica para o problema original e muito mais flexível, pois você pode alterar o modelo de movimento e isso ainda funcionará aproximadamente.

Pontos de bônus por usar etapas variáveis, por exemplo, 100ms no primeiro segundo (dez pontos), 200ms nos próximos dois (dez mais pontos), 400ms em 4 segundos, etc. De fato, conforme seu personagem se aproxima da velocidade terminal, a variação em a resistência diminui e, de qualquer maneira, você não precisa de intervalos maiores. Dessa forma, você pode lidar com saltos muito longos sem muito processamento, pois a complexidade por T segundos é O (log T) em vez de O (T).

Você também pode simular o que acontece quando o personagem para de aumentar no meio do salto ou começa a aumentar de outra maneira. Com o truque acima, a complexidade é O ((log T) ^ 2), o que não é muito ruim.


fonte
+1, ótima resposta! Como eu não poderia considerar a simulação real. Você poderia, por favor, elaborar uma "aproximação parabólica" (eu não entendo direito)? Você quer dizer apenas o método de integrar velocidades, como, por exemplo, RK4 e Euler? Em caso afirmativo, você poderia explicá-lo ou, pelo menos, criar um link para algumas informações sobre como executá-lo?
Patryk Czachurski
1
Normalmente você faz x'= x + v*dt. Em vez disso, use x' = x + v*dt + 1/2*a*dt*dt. Quando dtpequeno, dt^2pequeno, geralmente é deixado de fora na integração tradicional do Euler nos jogos. Aqui dtnão é pequeno, então você precisa do termo de aceleração. Como dté elevado à segunda potência, essa é uma integração quadrática, e o caminho é uma parábola, portanto, uma aproximação parabólica. O RK4 calcula essencialmente derivadas mais altas e, portanto, pode fornecer aproximações cúbicas, quárticas, quinticas etc. O RK4 é um exagero para isso, provavelmente, pois a estabilidade não é importante.
e suponho que a velocidade em si deva ser integrada como no Euler tradicional? v' = v + a*dt
Patryk Czachurski
1
Sim. Você não tem idiota, está assumindo que é zero.
Por favor, dê uma olhada na edição.
Patryk Czachurski
4

Yay! Eu fiz isso!

Estou usando uma simulação simples que leva a primeira posição para pousar atrás do eixo vertical do ponto de destino - a partir daí, pego a posição simulada anterior e faço um segmento. Agora, verifico se o ponto de destino está abaixo desse segmento. Se for - podemos pular lá.

insira a descrição da imagem aqui

É um personagem controlado pelo jogador no gif. O rosa é o caminho previsto, os segmentos amarelos são as posições subseqüentes previstas e o segmento final fica branco se o ponto de destino estiver abaixo dele, vermelho caso contrário. Curva vermelha é a rota de vôo real. Há algumas pequenas imprecisões devido a interpolação Física do Estado ligado.

Os cálculos foram surpreendentemente fáceis, no entanto, fazer meu ambiente funcionar da mesma maneira que esses cálculos puros ... foi uma dor enorme no traseiro. Pelo menos eu resolvi alguns erros graves por aí, então foi um exercício útil, afinal.

Aqui está o código completo em Lua usado para resolver o problema original (o código pressupõe que você tenha sua própria rotina "debug_draw" e sua própria classe de vetor com métodos básicos como "length_sq" (length ao quadrado), "normalize" ou operadores +, * :

function simple_integration(p, dt)
    local new_p = {}

    new_p.acc = p.acc
    new_p.vel = p.vel + p.acc * dt 
    new_p.pos = p.pos + new_p.vel * dt
    -- uncomment this if you want to use quadratic integration
    -- but with small timesteps even this is an overkill since Box2D itself uses traditional Euler
    -- and I found that for calculations to be accurate I either way must keep the timesteps very low at the beginning of the jump
     --+ p.acc * dt * dt * 0.5

    return new_p
end

function point_below_segment(a, b, p)
    -- make sure a is to the left
    if a.x > b.x then a,b = b,a end

    return ((b.x - a.x)*(p.y - a.y) - (b.y - a.y)*(p.x - a.x)) < 0
end

-- returns true or false
function can_point_be_reached_by_jump
(
gravity, -- vector (meters per seconds^2)
movement_force, -- vector (meters per seconds^2)
air_resistance_mult, -- scalar
queried_point, -- vector (meters)
starting_position, -- vector (meters)
starting_velocity, -- vector (meters per seconds)
jump_impulse, -- vector (meters per seconds)
mass -- scalar (kilogrammes)
)

    local my_point = {
        pos = starting_position,
        vel = starting_velocity + jump_impulse/mass
    }

    local direction_left = movement_force.x < 0
    local step = 1/60

    while true do           
        -- calculate resultant force
        my_point.acc = 
        -- air resistance (multiplier * squared length of the velocity * opposite normalized velocity)
        (vec2(my_point.vel):normalize() * -1 * air_resistance_mult * my_point.vel:length_sq()) / mass
        -- remaining forces
        + gravity + movement_force/mass

        -- I discard any timestep optimizations at the moment as they are very context specific
        local new_p = simple_integration(my_point, step)

        debug_draw(my_point.pos, new_p.pos, 255, 0, 255, 255)
        debug_draw(new_p.pos, new_p.pos+vec2(0, -1), 255, 255, 0, 255)

        if (direction_left and new_p.pos.x < queried_point.x) or (not direction_left and new_p.pos.x > queried_point.x) then
            if point_below_segment(new_p.pos, my_point.pos, queried_point) then
                debug_draw(new_p.pos, my_point.pos, 255, 0, 0, 255)
                return true
            else
                debug_draw(new_p.pos, my_point.pos, 255, 255, 255, 255)
                return false
            end
        else 
            my_point = new_p
        end
    end

    return false
end

Accept vai até Jason por me colocar na direção certa! Obrigado!

Patryk Czachurski
fonte
2

Você pode "apenas calcular" a resposta, mas tenho certeza de que a achará insuficiente assim que a tiver, devido à natureza altamente interativa da sua física de "queda livre".

Considere usar uma abordagem diferente: Pesquisando. Aqui está como é feito o Super Mario AI: http://aigamedev.com/open/interview/mario-ai/

A busca de possíveis caminhos para ir de A a B permite interatividade ilimitada no ar enquanto ainda é computacionalmente eficiente.

Jonas Bötel
fonte
1
Isso é prático apenas para certos mundos. Em particular, Mario limita o tamanho do gráfico de pesquisa por ser linear, ter um número limitado de velocidades e ter uma excelente heurística. Dependendo do jogo, isso pode não ser verdade. Também computacionalmente eficiente é relativo, pois essa IA provavelmente funcionaria para mais de um personagem / inimigo, enquanto em Mario há apenas um para controlar.