Fisica della palla: livellare i rimbalzi finali man mano che la palla si ferma


12

Mi sono imbattuto in un altro problema nel mio piccolo gioco con la palla che rimbalza.

La mia palla rimbalza bene, tranne per gli ultimi momenti in cui sta per riposarsi. Il movimento della palla è regolare per la parte principale ma, verso la fine, la palla si scuote per un po 'mentre si deposita sul fondo dello schermo.

Posso capire perché questo sta accadendo, ma non riesco a smussarlo.

Le sarei grato per qualsiasi consiglio che possa essere offerto.

Il mio codice di aggiornamento è:

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

Forse dovrei anche sottolineare questo Gravity, Resistance Grasse Concretesono tutti del tipo Vector2.


Solo per confermare questo: il tuo "attrito" quando la palla colpisce una superficie ha un valore <1, che è sostanzialmente il coefficiente di restituzione corretto?
Jorge Leitao,

@ JCLeitão - Corretto.
Ste

Per favore, non giurare di rispettare i voti quando premi la taglia e la risposta corretta. Scegli quello che ti ha aiutato.
aaaaaaaaaaaa

Questo è un brutto modo di gestire una taglia, in pratica stai dicendo che non puoi giudicare te stesso, quindi lascia che i voti decidano ... Comunque, quello che stai vivendo è un comune jitter di collisione. Ciò può essere risolto impostando una quantità di compenetrazione massima, una velocità minima o qualsiasi altra forma di "limite" che una volta raggiunto provocherà l'interruzione del movimento da parte della routine e farà riposare l'oggetto. Puoi anche aggiungere uno stato di riposo ai tuoi oggetti per evitare controlli inutili.
Darkwings,

@Darkwings - Penso che la comunità in questo scenario sappia meglio di me quale sia la risposta migliore. Ecco perché i voti influenzeranno la mia decisione. Ovviamente, se avessi provato la soluzione con il maggior numero di voti e non mi avesse aiutato, non avrei assegnato una risposta a quella risposta.
Ste

Risposte:


19

Ecco i passaggi necessari per migliorare il tuo ciclo di simulazione fisica.

1. Timestep

Il problema principale che posso vedere con il tuo codice è che non tiene conto del tempo di passaggio della fisica. Dovrebbe essere ovvio che c'è qualcosa che non va Position += Velocity;perché le unità non corrispondono. O in Velocityrealtà non è una velocità o manca qualcosa.

Anche se i valori di velocità e gravità sono ridimensionati in modo tale che ciascun fotogramma si verifichi in un'unità di tempo 1(il che significa che, ad esempio, Velocity significa effettivamente la distanza percorsa in un secondo), il tempo deve apparire in qualche punto del codice, sia implicitamente (fissando le variabili in modo che i loro nomi riflettono ciò che realmente memorizzano) o esplicitamente (introducendo un timestep). Credo che la cosa più semplice da fare sia dichiarare l'unità di tempo:

float TimeStep = 1.0;

E usa quel valore ovunque sia necessario:

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

Nota che qualsiasi compilatore decente semplificherà le moltiplicazioni di 1.0, in modo che quella parte non rallenti le cose.

Ora non Position += Velocity * TimeStepè ancora del tutto esatto (vedi questa domanda per capire perché) ma probabilmente lo farà per ora.

Inoltre, questo deve prendere in considerazione il tempo:

Velocity *= Physics.Air.Resistance;

È un po 'più complicato da risolvere; un modo possibile è:

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

2. Doppi aggiornamenti

Ora controlla cosa fai quando rimbalzi (mostrato solo il codice pertinente):

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

Puoi vedere che TimeStepviene usato due volte durante il rimbalzo. Questo in pratica dà alla palla il doppio del tempo per aggiornarsi. Questo è ciò che dovrebbe accadere invece:

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. Gravità

Controlla questa parte del codice ora:

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

Aggiungete la gravità per l'intera durata del fotogramma. Ma cosa succede se la palla rimbalza effettivamente durante quel frame? Quindi la velocità verrà invertita, ma la gravità aggiunta farà accelerare la palla da terra! Quindi la gravità in eccesso dovrà essere rimossa durante il rimbalzo , quindi aggiunta nuovamente nella direzione corretta.

Può accadere che anche l'aggiunta di gravità nella direzione corretta provochi un'accelerazione eccessiva della velocità. Per evitarlo, puoi saltare l'aggiunta di gravità (dopotutto, non è molto e dura solo un frame) o bloccare la velocità a zero.

4. Codice fisso

Ed ecco il codice completamente aggiornato:

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. Ulteriori aggiunte

Per una stabilità della simulazione ancora migliore, puoi decidere di eseguire la simulazione fisica a una frequenza più elevata. Ciò è reso banale dalle modifiche di cui sopra che coinvolgono TimeStep, perché devi solo dividere la cornice in tutti i blocchi che desideri. Per esempio:

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

"il tempo deve apparire da qualche parte nel tuo codice." Stai pubblicizzando che moltiplicare per 1 in tutto il luogo non è solo una buona idea, è obbligatorio? Sicuramente un timestep regolabile è una bella caratteristica, ma sicuramente non è obbligatorio.
aaaaaaaaaaaa,

@eBusiness: il mio argomento è molto più sulla coerenza e sul rilevamento degli errori che sui timestep regolabili. Non sto dicendo che è necessario moltiplicare per 1, sto dicendo che velocity += gravityè sbagliato e velocity += gravity * timestepha senso. Potrebbe dare lo stesso risultato alla fine, ma senza un commento che dice "So cosa sto facendo qui" significa ancora un errore di codifica, un programmatore sciatto, una mancanza di conoscenza della fisica o solo un codice prototipo che deve essere migliorato.
sam hocevar,

Dici che è sbagliato , quando ciò che presumibilmente intendi dire è che è una cattiva pratica. È la tua opinione soggettiva sulla questione, ed è bello che tu la esprima, ma È soggettiva poiché il codice in questo senso fa esattamente come dovrebbe. Tutto ciò che chiedo è che tu faccia la differenza tra il soggettivo e l'obiettivo chiaro nel tuo post.
aaaaaaaaaaaa

2
@eBusiness: onestamente, è sbagliato secondo qualsiasi standard sano di mente. Il codice non "fa come dovrebbe" affatto, perché 1) l'aggiunta di velocità e gravità in realtà non significa nulla; e 2) se dà un risultato ragionevole è perché il valore memorizzato gravityè in realtà ... non gravità. Ma posso renderlo più chiaro nel post.
sam hocevar,

Al contrario, chiamarlo sbagliato è sbagliato secondo qualsiasi standard sano di mente. Hai ragione sul fatto che la gravità non è memorizzata nella variabile chiamata gravità, invece c'è un numero, e questo è tutto ciò che ci sarà mai, non ha alcuna relazione con la fisica al di là di quella che immaginiamo abbia, moltiplicandola per un altro numero non lo modifica. Ciò che apparentemente cambia è la tua capacità e / o volontà di stabilire una connessione mentale tra il codice e la fisica. A proposito un'osservazione psicologica piuttosto interessante.
aaaaaaaaaaaa

6

Aggiungi un segno di spunta per fermare il rimbalzo, usando una velocità verticale minima. E quando ottieni il minimo rimbalzo, metti la palla a terra.

MIN_BOUNCE = <0.01 e.g>;

if( Velocity.Y < MIN_BOUNCE ){
    Velocity.Y = 0;
    Position.Y = <ground position Y>;
}

3
Mi piace questa soluzione, ma non limiterei il rimbalzo sull'asse Y. Vorrei calcolare la normale del collider nel punto di collisione e controllare se l'entità della velocità di collisione è maggiore della soglia di rimbalzo. Anche se il mondo del PO consente solo Y rimbalzi, altri utenti potrebbero trovare utile una soluzione più generale. (Se non sono chiaro, pensa a far rimbalzare due sfere insieme in un punto casuale)
Brandon

@brandon, fantastico, dovrebbe funzionare meglio con il normale.
Zhen,

1
@Zhen, se usi la normale della superficie hai la possibilità che la palla finisca per attaccarsi a una superficie che ha una normale che non è parallela a quella della gravità. Vorrei provare a considerare la gravità nel calcolo, se possibile.
Nic Foster

Nessuna di queste soluzioni dovrebbe impostare alcuna velocità su 0. Limiti la riflessione sulla normale del vettore solo in base alla soglia di rimbalzo
brandon

1

Quindi, penso che il problema del perché questo accada sia che la tua palla si sta avvicinando a un limite. Matematicamente, la palla non si ferma mai in superficie, si avvicina alla superficie.

Tuttavia, il tuo gioco non utilizza un tempo continuo. È una mappa che utilizza un'approssimazione dell'equazione differenziale. E quell'approssimazione non è valida in questa situazione limitante (puoi, ma dovresti fare passi più piccoli e più piccoli, che presumo non siano fattibili.

Fisicamente parlando, ciò che accade è che quando la palla è molto vicina alla superficie, si attacca ad essa se la forza totale è al di sotto di una determinata soglia.

La risposta @Zhen andrebbe bene se il tuo sistema è omogeneo, il che non lo è. Ha una certa gravità sull'asse y.

Quindi, direi che la soluzione non sarebbe che la velocità dovrebbe essere inferiore a una determinata soglia, ma la forza totale applicata sulla palla dopo l'aggiornamento dovrebbe essere inferiore a una determinata soglia.

Quella forza è il contributo della forza esercitata dal muro sulla palla + la gravità.

La condizione dovrebbe quindi essere qualcosa di simile

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

notare che newVelocity.y è una quantità positiva se il rimbalzo è sulla parete del fondo e la gravità è una quantità negativa.

Nota anche che newVelocity e Physics.Gravity.Force non hanno le stesse dimensioni, come hai scritto in

Velocity += Physics.Gravity.Force;

nel senso che, come te, presumo che delta_time = 1 e ballMass = 1.

Spero che sia di aiuto


1

È presente un aggiornamento della posizione all'interno del controllo delle collisioni, è ridondante e errato. E aggiunge energia alla palla aiutandola potenzialmente a muoversi perpetuamente. Insieme alla gravità che non viene applicata in alcuni fotogrammi, questo dà il tuo strano movimento. Rimuoverlo.

Ora potresti vedere un problema diverso, che la palla viene "bloccata" fuori dall'area designata, rimbalzando continuamente avanti e indietro.

Un modo semplice per risolvere questo problema è verificare che la palla si muova nella direzione corretta prima di cambiarla.

Quindi dovresti fare:

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

In:

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

E simile per la direzione Y.

Affinché la palla si fermi bene, è necessario arrestare la gravità ad un certo punto. La tua attuale implementazione assicura che la palla riemerga sempre poiché la gravità non la frena finché è sotterranea. Dovresti passare ad applicare sempre la gravità. Questo, tuttavia, porta la palla che affonda lentamente nel terreno dopo essersi depositata. Una soluzione rapida per questo è, dopo aver applicato la gravità, se la palla è sotto il livello della superficie e si muove verso il basso, fermarla:

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

Questi cambiamenti in totale dovrebbero darti una simulazione decente. Ma nota che è ancora una simulazione molto semplice.


0

Avere un metodo mutatore per qualsiasi e tutte le variazioni di velocità, quindi all'interno di quel metodo è possibile controllare la velocità aggiornata per determinare se si sta muovendo abbastanza lentamente da metterla a riposo. La maggior parte dei sistemi fisici che conosco chiamano questa "restituzione".

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;

Nel metodo precedente limitiamo il rimbalzo ogni volta che si trova lungo lo stesso asse della gravità.

Qualcos'altro da considerare sarebbe rilevare ogni volta che una palla si è scontrata con il terreno e se si muove abbastanza lentamente al momento della collisione, imposta la velocità lungo l'asse di gravità su zero.


Non voglio sottovalutare perché questo è valido, ma la domanda è porre soglie di rimbalzo, non soglie di velocità. Questi sono quasi sempre separati nella mia esperienza perché l'effetto del jittering durante il rimbalzo è generalmente separato dall'effetto di continuare a calcolare la velocità una volta che è visivamente a riposo.
brandon,

Sono una cosa sola. I motori fisici, come Havok o PhysX, e la restituzione di base di JigLibX sulla velocità lineare (e sulla velocità angolare). Questo metodo dovrebbe funzionare per qualsiasi movimento della palla, incluso il rimbalzo. In effetti, l'ultimo progetto in cui mi trovavo (LEGO Universe) ha usato un metodo quasi identico a questo per fermare il rimbalzo delle monete dopo che avevano rallentato. In quel caso non stavamo usando la fisica dinamica, quindi dovevamo farlo manualmente piuttosto che lasciare che Havok se ne occupasse per noi.
Nic Foster

@NicFoster: sono confuso, secondo me un oggetto potrebbe muoversi molto velocemente in orizzontale e quasi in senso verticale, nel qual caso il tuo metodo non si innescherebbe. Penso che l'OP vorrebbe che la distanza verticale fosse impostata su zero nonostante la lunghezza della velocità fosse alta.
George Duckett,

@GeorgeDuckett: Ah grazie, ho frainteso la domanda originale. L'OP non vuole che la palla si fermi, basta fermare il movimento verticale. Ho aggiornato la risposta per tenere conto solo della velocità di rimbalzo.
Nic Foster

0

Un'altra cosa: stai moltiplicando per una costante di attrito. Cambialo - abbassa la costante di attrito ma aggiungi un assorbimento di energia fisso su un rimbalzo. Questo smorza gli ultimi rimbalzi molto più velocemente.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.