Física de la pelota: suaviza los rebotes finales cuando la pelota se detiene

12

Me he encontrado con otro problema en mi pequeño juego de pelota que rebota.

Mi pelota está rebotando bien, excepto en los últimos momentos cuando está a punto de descansar. El movimiento de la pelota es suave para la parte principal pero, hacia el final, la pelota se sacude un poco mientras se asienta en la parte inferior de la pantalla.

Puedo entender por qué sucede esto, pero parece que no puedo suavizarlo.

Le agradecería cualquier consejo que pueda ofrecer.

Mi código de actualización es:

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

Quizás también debería señalar eso Gravity, Resistance Grassy Concreteson todos del tipo Vector2.

Ste
fuente
Solo para confirmar esto: su "fricción" cuando la pelota golpea una superficie es un valor <1, ​​¿cuál es básicamente el coeficiente de restitución correcto?
Jorge Leitao
@ JCLeitão - Correcto.
Ste
No jure respetar los votos cuando otorgue recompensas y responda correctamente. Ve por lo que sea que te haya ayudado.
aaaaaaaaaaaa
Esa es una mala manera de manejar una recompensa, básicamente estás diciendo que no puedes juzgarte a ti mismo, así que dejas que los votos positivos decidan ... De todos modos, lo que estás experimentando es una inquietud de colisión común. Eso se puede resolver estableciendo una cantidad máxima de interpenetración, una velocidad mínima o cualquier otra forma de "límite" que una vez alcanzado hará que su rutina detenga el movimiento y descanse el objeto. También es posible que desee agregar un estado de reposo a sus objetos para evitar controles inútiles.
Darkwings
@Darkwings: creo que la comunidad en este escenario sabe mejor que yo cuál es la mejor respuesta. Es por eso que los votos positivos influirán en mi decisión. Obviamente, si probé la solución con la mayoría de los votos positivos y no me ayudó, entonces no otorgaría esa respuesta.
Ste

Respuestas:

19

Aquí los pasos necesarios para mejorar su ciclo de simulación física.

1. Timestep

El principal problema que puedo ver con su código es que no tiene en cuenta el tiempo del paso de física. Debería ser obvio que hay algo mal Position += Velocity;porque las unidades no coinciden. O en Velocityrealidad no es una velocidad, o falta algo.

Incluso si sus valores de velocidad y gravedad están escalados de manera que cada cuadro ocurra en una unidad de tiempo 1(lo que significa que, por ejemplo, en Velocity realidad significa la distancia recorrida en un segundo), el tiempo debe aparecer en algún lugar de su código, ya sea implícitamente (arreglando las variables de manera que sus nombres reflejan lo que realmente almacenan) o explícitamente (al introducir un paso de tiempo). Creo que lo más fácil es declarar la unidad de tiempo:

float TimeStep = 1.0;

Y use ese valor donde sea necesario:

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

Tenga en cuenta que cualquier compilador decente simplificará las multiplicaciones 1.0, por lo que esa parte no hará las cosas más lentas.

Ahora Position += Velocity * TimeSteptodavía no es del todo exacto (vea esta pregunta para entender por qué), pero probablemente lo hará por ahora.

Además, esto debe tener en cuenta el tiempo:

Velocity *= Physics.Air.Resistance;

Es un poco más complicado de arreglar; Una forma posible es:

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

2. actualizaciones dobles

Ahora verifique lo que hace cuando rebota (solo se muestra el código relevante):

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

Puede ver que TimeStepse usa dos veces durante el rebote. Básicamente, esto le da a la pelota el doble de tiempo para actualizarse. Esto es lo que debería suceder en su lugar:

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. Gravedad

Verifique esta parte del código ahora:

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

Agrega gravedad durante toda la duración del cuadro. Pero, ¿qué pasa si la pelota realmente rebota durante ese cuadro? ¡Entonces la velocidad se invertirá, pero la gravedad que se agregó hará que la bola acelere lejos del suelo! Por lo tanto , se deberá eliminar el exceso de gravedad al rebotar y luego volver a agregarlo en la dirección correcta.

Puede suceder que incluso volver a agregar la gravedad en la dirección correcta provoque que la velocidad se acelere demasiado. Para evitar esto, puede omitir la adición de gravedad (después de todo, no es tanto y solo dura un fotograma) o fijar la velocidad a cero.

4. Código fijo

Y aquí está el código completamente actualizado:

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. Adiciones adicionales

Para mejorar aún más la estabilidad de la simulación, puede decidir ejecutar su simulación física a una frecuencia más alta. Esto se hace trivial por los cambios anteriores que involucran TimeStep, porque solo necesita dividir su marco en tantos trozos como desee. Por ejemplo:

public void Update()
{
    float TimeStep = 1.0;
    Update(TimeStep / 4);
    Update(TimeStep / 4);
    Update(TimeStep / 4);
    Update(TimeStep / 4);
}
sam hocevar
fuente
"el tiempo debe aparecer en algún lugar de su código". ¿Está anunciando que multiplicar por 1 en todo el lugar no es solo una buena idea, es obligatorio? Claro que un paso de tiempo ajustable es una buena característica, pero ciertamente no es obligatorio.
aaaaaaaaaaaa
@eBusiness: mi argumento es mucho más sobre la coherencia y la detección de errores que sobre los pasos de tiempo ajustables. No digo que sea necesario multiplicar por 1, digo que velocity += gravityestá mal y solo velocity += gravity * timesteptiene sentido. Puede dar el mismo resultado al final, pero sin un comentario que diga "Sé lo que estoy haciendo aquí" todavía significa un error de codificación, un programador descuidado, una falta de conocimiento sobre física o simplemente un código prototipo que necesita Ser mejorado.
sam hocevar
Dices que está mal , cuando lo que supuestamente quieres decir es que es una mala práctica. Es su opinión subjetiva sobre el asunto, y está bien que lo exprese, pero ES subjetivo ya que el código a este respecto hace exactamente lo que debe. Todo lo que pido es que hagas una diferencia clara entre lo subjetivo y lo objetivo en tu publicación.
aaaaaaaaaaaa
2
@eBusiness: honestamente, está mal según cualquier estándar sensato. El código no "hace lo que debe" en absoluto, porque 1) agregar velocidad y gravedad en realidad no significa nada; y 2) si da un resultado razonable es porque el valor almacenado gravityes en realidad ... no la gravedad. Pero puedo aclarar eso en la publicación.
Sam Hocevar
Por el contrario, llamarlo incorrecto es incorrecto según cualquier estándar sensato. Tienes razón en que la gravedad no está almacenada en la variable llamada gravedad, en cambio hay un número, y eso es todo lo que siempre habrá, no tiene ninguna relación con la física más allá de la relación que imaginamos, multiplicándola por otro número no cambia eso. Lo que aparentemente cambia es su capacidad y / o disposición para establecer la conexión mental entre el código y la física. Por cierto una observación psicológica bastante interesante.
aaaaaaaaaaaa
6

Agregue una marca para detener el rebote, utilizando una velocidad vertical mínima. Y cuando consigas el mínimo rebote, coloca la pelota en el suelo.

MIN_BOUNCE = <0.01 e.g>;

if( Velocity.Y < MIN_BOUNCE ){
    Velocity.Y = 0;
    Position.Y = <ground position Y>;
}
Zhen
fuente
3
Me gusta esta solución, pero no limitaría el rebote al eje Y. Calcularía la normalidad del colisionador en el punto de colisión y comprobaría si la magnitud de la velocidad de colisión es mayor que el umbral de rebote. Incluso si el mundo del OP solo permite rebotes en Y, otros usuarios pueden encontrar útil una solución más general. (Si no estoy claro, piense en hacer rebotar dos esferas juntas en un punto aleatorio)
brandon
@brandon, genial, debería funcionar mejor con normal.
Zhen
1
@Zhen, si usa la normalidad de la superficie, tiene la posibilidad de que la bola termine pegada a una superficie que tiene una normalidad que no es paralela a la de la gravedad. Intentaría incluir la gravedad en el cálculo si fuera posible.
Nic Foster,
Ninguna de estas soluciones debe establecer velocidades a 0. Solo limita la reflexión a través de la normalidad del vector dependiendo del umbral de rebote
Brandon
1

Entonces, creo que el problema de por qué sucede esto es que su bola se está acercando a un límite. Matemáticamente, la pelota nunca se detiene en la superficie, se acerca a la superficie.

Sin embargo, tu juego no usa un tiempo continuo. Es un mapa, que utiliza una aproximación a la ecuación diferencial. Y esa aproximación no es válida en esta situación limitante (puede hacerlo, pero tendría que tomar pasos de tiempo más pequeños y más pequeños, lo que supongo que no es factible).

Físicamente hablando, lo que sucede es que cuando la pelota está muy cerca de la superficie, se adhiere a ella si la fuerza total está por debajo de un umbral dado.

La respuesta de @Zhen estaría bien si su sistema es homogéneo, lo cual no lo es. Tiene algo de gravedad en el eje y.

Entonces, diría que la solución no sería que la velocidad debería estar por debajo de un umbral dado, sino que la fuerza total aplicada sobre la pelota después de la actualización debería estar por debajo de un umbral dado.

Esa fuerza es la contribución de la fuerza ejercida por la pared sobre la pelota + la gravedad.

La condición debería ser algo así como

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

observe que newVelocity.y es una cantidad positiva si el rebote está en la pared inferior, y la gravedad es una cantidad negativa.

Observe también que newVelocity and Physics.Gravity.Force no tienen las mismas dimensiones, como ha escrito en

Velocity += Physics.Gravity.Force;

lo que significa que, como usted, supongo que delta_time = 1 y ballMass = 1.

Espero que esto ayude

Jorge Leitao
fuente
1

Tiene una actualización de posición dentro de su verificación de colisión, es redundante e incorrecta. Y agrega energía a la pelota, lo que puede ayudarla a moverse perpetuamente. Junto con la gravedad que no se aplica en algunos cuadros, esto le da un movimiento extraño. Quitarlo

Ahora puede ver un problema diferente, que la pelota se "atasca" fuera del área designada, rebotando constantemente hacia adelante y hacia atrás.

Una manera simple de resolver este problema es verificar que la pelota se mueva en la dirección correcta antes de cambiarla.

Por lo tanto, debe hacer:

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

Dentro:

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

Y similar para la dirección Y.

Para que la pelota se detenga bien, debes detener la gravedad en algún momento. Su implementación actual asegura que la bola siempre resurgirá ya que la gravedad no la frena siempre que esté bajo tierra. Debes cambiar a aplicar siempre la gravedad. Sin embargo, esto lleva a que la pelota se hunda lentamente en el suelo después de asentarse. Una solución rápida para esto es, después de aplicar la gravedad, si la pelota está por debajo del nivel de la superficie y se mueve hacia abajo, deténgala:

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

Estos cambios en total deberían darle una simulación decente. Pero tenga en cuenta que sigue siendo una simulación muy simple.

aaaaaaaaaaaa
fuente
0

Tenga un método de mutación para todos y cada uno de los cambios de velocidad, luego dentro de ese método puede verificar la velocidad actualizada para determinar si se está moviendo lo suficientemente lento como para ponerla en reposo. La mayoría de los sistemas de física que conozco llaman a esto 'restitución'.

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;

En el método anterior, limitamos el rebote siempre que esté en el mismo eje que la gravedad.

Algo más a considerar sería detectar cada vez que una pelota ha chocado con el suelo, y si se mueve bastante lento en el momento de la colisión, ajuste la velocidad a lo largo del eje de gravedad a cero.

Nic Foster
fuente
No votaré en contra porque esto es válido, pero la pregunta es acerca de los umbrales de rebote, no los umbrales de velocidad. En mi experiencia, estos casi siempre están separados porque el efecto de la vibración durante el rebote generalmente está separado del efecto de continuar calculando la velocidad una vez que está visualmente en reposo.
Brandon
Son uno en lo mismo. Motores de física, como Havok o PhysX, y la restitución de base JigLibX en velocidad lineal (y velocidad angular). Este método debería funcionar para cualquier movimiento de la pelota, incluido el rebote. De hecho, el último proyecto en el que estuve (LEGO Universe) usó un método casi idéntico a este para detener el rebote de las monedas una vez que se habían ralentizado. En ese caso no estábamos usando física dinámica, así que tuvimos que hacerlo manualmente en lugar de dejar que Havok se encargue de nosotros.
Nic Foster,
@NicFoster: Estoy confundido, en mi opinión, un objeto podría moverse muy rápido horizontalmente y casi nada verticalmente, en cuyo caso su método no se dispararía. Creo que el OP querría que la distancia vertical se establezca en cero a pesar de que la longitud de la velocidad sea alta.
George Duckett
@GeorgeDuckett: Ah, gracias, no entendí la pregunta original. El OP no quiere que la pelota deje de moverse, simplemente detiene el movimiento vertical. He actualizado la respuesta para tener en cuenta solo la velocidad de rebote.
Nic Foster,
0

Otra cosa: estás multiplicando por una constante de fricción. Cambie eso: reduzca la constante de fricción pero agregue una absorción de energía fija en un rebote. Esto amortiguará esos últimos rebotes mucho más rápido.

Loren Pechtel
fuente