Física das Bolas: Suavizando os saltos finais quando a bola parar

12

Eu vim contra outra questão no meu pequeno jogo de bola quicando.

Minha bola está girando bem, exceto nos últimos momentos em que está prestes a descansar. O movimento da bola é suave para a parte principal, mas, no final, a bola estremece por um tempo enquanto se acomoda na parte inferior da tela.

Eu posso entender por que isso está acontecendo, mas não consigo suavizá-lo.

Ficaria grato por qualquer conselho que possa ser oferecido.

Meu código de atualização é:

public void Update()
    {
        // Apply gravity if we're not already on the ground
        if(Position.Y < GraphicsViewport.Height - Texture.Height)
        {
            Velocity += Physics.Gravity.Force;
        }            
        Velocity *= Physics.Air.Resistance;
        Position += Velocity;

        if (Position.X < 0 || Position.X > GraphicsViewport.Width - Texture.Width)
        {
            // We've hit a vertical (side) boundary
            // Apply friction
            Velocity *= Physics.Surfaces.Concrete;

            // Invert velocity
            Velocity.X = -Velocity.X;
            Position.X = Position.X + Velocity.X;
        }

        if (Position.Y < 0 || Position.Y > GraphicsViewport.Height - Texture.Height)
        {
            // We've hit a horizontal boundary
            // Apply friction
            Velocity *= Physics.Surfaces.Grass;

            // Invert Velocity
            Velocity.Y = -Velocity.Y;
            Position.Y = Position.Y + Velocity.Y;
        }
    }

Talvez eu deva também salientar isso Gravity, Resistance Grasse Concretesão do tipo que todos são Vector2.

Ste
fonte
Só para confirmar: seu "atrito" quando a bola bate em uma superfície é um valor <1, ​​que é basicamente o coeficiente de restituição correto?
Jorge Leitao
@ JCLeitão - Correto.
Ste
Por favor, não jure cumprir os votos ao conceder recompensa e resposta correta. Escolha o que o ajudou.
Aaaaaaaaaaaa
Essa é uma maneira ruim de lidar com uma recompensa, basicamente você está dizendo que não pode se julgar para deixar os votos positivos decidirem ... Enfim, o que você está enfrentando é um tremor de colisão comum. Isso pode ser resolvido definindo-se uma quantidade máxima de interpenetração, uma velocidade mínima ou qualquer outra forma de 'limite' que uma vez atingido faça com que sua rotina pare o movimento e coloque o objeto em repouso. Você também pode adicionar um status de repouso aos seus objetos para evitar verificações inúteis.
Darkwings
@ Darkwings - Eu acho que a comunidade nesse cenário sabe melhor do que eu qual é a melhor resposta. É por isso que os votos positivos influenciarão minha decisão. Obviamente, se eu tentasse a solução com mais votos positivos e isso não me ajudasse, não concederia essa resposta.
#

Respostas:

19

Aqui estão as etapas necessárias para melhorar seu loop de simulação física.

1. Timestep

O principal problema que vejo no seu código é que ele não leva em consideração o tempo da etapa da física. Deveria ser óbvio que há algo errado, Position += Velocity;porque as unidades não coincidem. Ou Velocitynão é realmente uma velocidade ou algo está faltando.

Mesmo se seus valores de velocidade e gravidade são escalados de modo que cada quadro acontece em uma unidade de tempo 1(o que significa que , por exemplo. Velocity Na verdade significa a distância percorrida em um segundo), o tempo deve aparecer em algum lugar no seu código, quer implicitamente (fixando as variáveis de modo a que seus nomes refletem o que eles realmente armazenam) ou explicitamente (introduzindo um timestep). Acredito que a coisa mais fácil a fazer é declarar a unidade de tempo:

float TimeStep = 1.0;

E use esse valor em qualquer lugar que for necessário:

Velocity += Physics.Gravity.Force * TimeStep;
Position += Velocity * TimeStep;
...

Observe que qualquer compilador decente simplificará as multiplicações 1.0, para que essa parte não torne as coisas mais lentas.

Agora Position += Velocity * TimeStepainda não é bem exato (veja esta pergunta para entender o porquê), mas provavelmente o fará por enquanto.

Além disso, isso precisa levar tempo em consideração:

Velocity *= Physics.Air.Resistance;

É um pouco mais difícil de corrigir; uma maneira possível é:

Velocity -= Vector2(Math.Pow(Physics.Air.Resistance.X, TimeStep),
                    Math.Pow(Physics.Air.Resistance.Y, TimeStep))
          * Velocity;

2. Atualizações duplas

Agora verifique o que você faz ao pular (apenas o código relevante é mostrado):

Position += Velocity * TimeStep;
if (Position.Y < 0)
{
    Velocity.Y = -Velocity.Y * Physics.Surfaces.Grass;
    Position.Y = Position.Y + Velocity.Y * TimeStep;
}

Você pode ver que TimeStepé usado duas vezes durante o salto. Isso basicamente dá à bola o dobro do tempo para se atualizar. Isto é o que deveria acontecer:

Position += Velocity * TimeStep;
if (Position.Y < 0)
{
    /* First, stop at Y = 0 and count how much time is left */
    float RemainingTime = -Position.Y / Velocity.Y;
    Position.Y = 0;

    /* Then, start from Y = 0 and only use how much time was left */
    Velocity.Y = -Velocity.Y * Physics.Surfaces.Grass;
    Position.Y = Velocity.Y * RemainingTime;
}

3. Gravidade

Verifique esta parte do código agora:

if(Position.Y < GraphicsViewport.Height - Texture.Height)
{
    Velocity += Physics.Gravity.Force * TimeStep;
}            

Você adiciona gravidade por toda a duração do quadro. Mas e se a bola realmente saltar durante esse quadro? Então a velocidade será invertida, mas a gravidade adicionada fará com que a bola acelere para longe do chão! Portanto, o excesso de gravidade terá que ser removido ao saltar e , em seguida, adicionado novamente na direção correta.

Pode acontecer que mesmo adicionar novamente a gravidade na direção correta faça com que a velocidade acelere demais. Para evitar isso, você pode pular a adição de gravidade (afinal, não é muito e dura apenas um quadro) ou a velocidade da pinça para zero.

4. código fixo

E aqui está o código totalmente atualizado:

public void Update()
{
    float TimeStep = 1.0;
    Update(TimeStep);
}

public void Update(float TimeStep)
{
    float RemainingTime;

    // Apply gravity if we're not already on the ground
    if(Position.Y < GraphicsViewport.Height - Texture.Height)
    {
        Velocity += Physics.Gravity.Force * TimeStep;
    }
    Velocity -= Vector2(Math.Pow(Physics.Air.Resistance.X, RemainingTime),
                        Math.Pow(Physics.Air.Resistance.Y, RemainingTime))
              * Velocity;
    Position += Velocity * TimeStep;

    if (Position.X < 0 || Position.X > GraphicsViewport.Width - Texture.Width)
    {
        // We've hit a vertical (side) boundary
        if (Position.X < 0)
        {
            RemainingTime = -Position.X / Velocity.X;
            Position.X = 0;
        }
        else
        {
            RemainingTime = (Position.X - (GraphicsViewport.Width - Texture.Width)) / Velocity.X;
            Position.X = GraphicsViewport.Width - Texture.Width;
        }

        // Apply friction
        Velocity -= Vector2(Math.Pow(Physics.Surfaces.Concrete.X, RemainingTime),
                            Math.Pow(Physics.Surfaces.Concrete.Y, RemainingTime))
                  * Velocity;

        // Invert velocity
        Velocity.X = -Velocity.X;
        Position.X = Position.X + Velocity.X * RemainingTime;
    }

    if (Position.Y < 0 || Position.Y > GraphicsViewport.Height - Texture.Height)
    {
        // We've hit a horizontal boundary
        if (Position.Y < 0)
        {
            RemainingTime = -Position.Y / Velocity.Y;
            Position.Y = 0;
        }
        else
        {
            RemainingTime = (Position.Y - (GraphicsViewport.Height - Texture.Height)) / Velocity.Y;
            Position.Y = GraphicsViewport.Height - Texture.Height;
        }

        // Remove excess gravity
        Velocity.Y -= RemainingTime * Physics.Gravity.Force;

        // Apply friction
        Velocity -= Vector2(Math.Pow(Physics.Surfaces.Grass.X, RemainingTime),
                            Math.Pow(Physics.Surfaces.Grass.Y, RemainingTime))
                  * Velocity;

        // Invert velocity
        Velocity.Y = -Velocity.Y;

        // Re-add excess gravity
        float OldVelocityY = Velocity.Y;
        Velocity.Y += RemainingTime * Physics.Gravity.Force;
        // If velocity changed sign again, clamp it to zero
        if (Velocity.Y * OldVelocityY <= 0)
            Velocity.Y = 0;

        Position.Y = Position.Y + Velocity.Y * RemainingTime;
    }
}

5. Adições adicionais

Para uma estabilidade de simulação ainda melhor, você pode decidir executar sua simulação de física em uma frequência mais alta. Isso é trivial pelas alterações acima TimeStep, porque você só precisa dividir seu quadro em quantos blocos desejar. Por exemplo:

public void Update()
{
    float TimeStep = 1.0;
    Update(TimeStep / 4);
    Update(TimeStep / 4);
    Update(TimeStep / 4);
    Update(TimeStep / 4);
}
sam hocevar
fonte
"o tempo deve aparecer em algum lugar do seu código." Você está anunciando que multiplicar por 1 em todo o lugar não é apenas uma boa ideia, é obrigatório? Certamente, um timestep ajustável é um recurso interessante, mas certamente não é obrigatório.
Aaaaaaaaaaaa
@eBusiness: meu argumento é muito mais sobre consistência e detecção de erros do que sobre timesteps ajustáveis. Não estou dizendo que multiplicar por 1 é necessário, estou dizendo que velocity += gravityestá errado e só velocity += gravity * timestepfaz sentido. Pode dar o mesmo resultado no final, mas sem um comentário dizendo "Eu sei o que estou fazendo aqui", ainda significa um erro de codificação, um programador desleixado, uma falta de conhecimento sobre física ou apenas um código de protótipo que precisa ser melhorado.
sam hocevar
Você diz que está errado , quando o que você pretende dizer é que é uma má prática. É sua opinião subjetiva sobre o assunto, e é bom que você o expresse, mas é subjetivo, pois o código nesse sentido faz exatamente o que deve ser. Tudo o que peço é que você faça clara a diferença entre o subjetivo e o objetivo em seu post.
Aaaaaaaaaaaa
2
@ eBusiness: honestamente, está errado em qualquer padrão são. O código não "faz como deveria", porque 1) adicionar velocidade e gravidade não significa nada; e 2) se der um resultado razoável, é porque o valor armazenado gravityé na verdade ... não a gravidade. Mas posso deixar isso mais claro no post.
sam hocevar
Pelo contrário, chamá-lo errado é errado por qualquer padrão são. Você está certo de que a gravidade não é armazenada na variável denominada gravidade, em vez disso, existe um número e isso é tudo o que sempre haverá; ela não tem nenhuma relação com a física além da relação que imaginamos ter, multiplicando-a por outro número não muda isso. O que aparentemente muda é a sua capacidade e / ou vontade de fazer a conexão mental entre o código e a física. A propósito, uma observação psicológica bastante interessante.
Aaaaaaaaaaaa
6

Adicione uma verificação para interromper o salto, usando uma velocidade vertical mínima. E quando você conseguir o salto mínimo, coloque a bola no chão.

MIN_BOUNCE = <0.01 e.g>;

if( Velocity.Y < MIN_BOUNCE ){
    Velocity.Y = 0;
    Position.Y = <ground position Y>;
}
Zhen
fonte
3
Eu gosto dessa solução, mas não limitaria o salto ao eixo Y. Eu calcularia o normal do colisor no ponto de colisão e verificaria se a magnitude da velocidade de colisão é maior que o limite de rejeição. Mesmo que o mundo do OP permita saltos em Y, outros usuários podem achar útil uma solução mais geral. (Se eu estou sendo claro, pensar em saltar duas esferas juntos em um ponto aleatório)
Brandon
@brandon, ótimo, deve funcionar melhor com o normal.
Zhen
1
@Zhen, se você usar o normal da superfície, terá a chance de fazer com que a bola grude em uma superfície que tenha um normal que não seja paralelo ao da gravidade. Eu tentaria fatorar a gravidade no cálculo, se possível.
Nic Foster
Nenhuma dessas soluções deve definir quaisquer velocidades a 0. Você só limitar o reflexo do outro lado da normal do vetor, dependendo do limiar de salto
Brandon
1

Então, acho que o problema de por que isso está acontecendo é que sua bola está se aproximando de um limite. Matematicamente, a bola nunca para na superfície, ela se aproxima da superfície.

No entanto, seu jogo não está usando um tempo contínuo. É um mapa, que está usando uma aproximação à equação diferencial. E essa aproximação não é válida nessa situação limitadora (você pode, mas teria que tomar etapas menores e menores, o que eu suponho que não é viável.

Fisicamente falando, o que acontece é que, quando a bola está muito próxima da superfície, ela gruda nela se a força total estiver abaixo de um determinado limite.

A resposta @Zhen seria boa se o seu sistema for homogêneo, o que não é. Tem alguma gravidade no eixo y.

Então, eu diria que a solução não seria que a velocidade estivesse abaixo de um determinado limite, mas a força total aplicada na bola após a atualização deveria estar abaixo de um determinado limite.

Essa força é a contribuição da força exercida pela parede na bola + a gravidade.

A condição deve ser algo como

if (newVelocity + Physics.Gravity.Force <limite)

observe que newVelocity.y é uma quantidade positiva se o salto estiver na parede inferior e a gravidade for uma quantidade negativa.

Observe também que newVelocity e Physics.Gravity.Force não têm as mesmas dimensões, como você escreveu em

Velocity += Physics.Gravity.Force;

o que significa que, como você, estou assumindo que delta_time = 1 e ballMass = 1.

Espero que isto ajude

Jorge Leitao
fonte
1

Você tem uma atualização de posição dentro de sua verificação de colisão, é redundante e incorreta. E acrescenta energia à bola, potencialmente ajudando-a a se mover perpetuamente. Juntamente com a gravidade não sendo aplicada em alguns quadros, isso gera um movimento estranho. Remova.

Agora você pode ver uma questão diferente: a bola fica "presa" fora da área designada, saltando perpetuamente para frente e para trás.

Uma maneira simples de resolver esse problema é verificar se a bola se move na direção correta antes de alterá-la.

Assim você deve fazer:

if (Position.X < 0 || Position.X > GraphicsViewport.Width - Texture.Width)

Para dentro:

if ((Position.X < 0 && Velocity.X < 0) || (Position.X > GraphicsViewport.Width - Texture.Width && Velocity.X > 0))

E semelhante para a direção Y.

Para que a bola pare bem, você precisa parar a gravidade em algum momento. Sua implementação atual garante que a bola sempre ressurja, pois a gravidade não a freia enquanto estiver no subsolo. Você deve mudar para sempre aplicar a gravidade. No entanto, isso leva a bola a afundar lentamente no chão depois de se estabelecer. Uma solução rápida para isso é, após aplicar a gravidade, se a bola estiver abaixo do nível da superfície e se mover para baixo, pare-a:

Velocity += Physics.Gravity.Force;
if(Position.Y > GraphicsViewport.Height - Texture.Height && Velocity.Y > 0)
{
    Velocity.Y = 0;
}

Essas alterações no total devem fornecer uma simulação decente. Mas observe que ainda é uma simulação muito simples.

aaaaaaaaaaaa
fonte
0

Tenha um método mutador para toda e qualquer alteração de velocidade; nesse método, você pode verificar a velocidade atualizada para determinar se está se movendo devagar o suficiente para colocá-la em repouso. A maioria dos sistemas de física que conheço chama isso de 'restituição'.

public Vector3 Velocity
{
    public get { return velocity; }
    public set
    {
        velocity = value;

        // We get the direction that gravity pulls in
        Vector3 GravityDirection = gravity;
        GravityDirection.Normalize();

        Vector3 VelocityDirection = velocity;
        VelocityDirection.Normalize();

        if ((velocity * GravityDirection).SquaredLength() < 0.25f)
        {
            velocity.Y = 0.0f;
        }            
    }
}
private Vector3 velocity;

No método acima, limitamos a rejeição sempre que ela está no mesmo eixo da gravidade.

Outra coisa a considerar seria detectar sempre que uma bola colidisse com o solo e, se estiver se movendo bastante devagar no momento da colisão, ajuste a velocidade ao longo do eixo da gravidade para zero.

Nic Foster
fonte
Não vou diminuir o voto porque isso é válido, mas a pergunta é sobre limites de rejeição, não limites de velocidade. Estes são quase sempre separados na minha experiência, porque o efeito do tremor durante o salto é geralmente separado do efeito de continuar a calcular a velocidade quando está visualmente em repouso.
brandon
Eles são um no mesmo. Os mecanismos de física, como Havok ou PhysX, e JigLibX baseiam a restituição na velocidade linear (e velocidade angular). Este método deve funcionar para todo e qualquer movimento da bola, inclusive quicando. De fato, o último projeto que eu participei (LEGO Universe) usou um método quase idêntico a esse para interromper o movimento de moedas depois que elas diminuíram a velocidade. Nesse caso, não estávamos usando física dinâmica, por isso tivemos que fazê-lo manualmente, em vez de deixar Havok cuidar disso por nós.
Nic Foster
@ NicFoster: Estou confuso, pois na minha opinião um objeto pode estar se movendo muito rápido na horizontal e quase na vertical, caso em que seu método não seria acionado. Eu acho que o OP gostaria que a distância vertical fosse zerada, apesar do comprimento da velocidade ser alto.
George Duckett
@ GeorgeDuckett: Ah, obrigado, eu entendi errado a pergunta original. O OP não quer que a bola pare de se mover, apenas pare o movimento vertical. Atualizei a resposta para explicar apenas a velocidade de salto.
Nic Foster
0

Outra coisa: você está se multiplicando por uma constante de atrito. Mude isso - diminua a constante de atrito, mas adicione uma absorção de energia fixa em um salto. Isso irá amortecer os últimos saltos muito mais rapidamente.

Loren Pechtel
fonte