Fizyka piłki: Wygładzanie ostatniego odbicia, gdy piłka zatrzymuje się

12

W mojej małej skaczącej piłce spotkałem się z innym problemem.

Moja piłka odbija się dobrze, z wyjątkiem ostatnich chwil, kiedy ma się zatrzymać. Ruch piłki jest płynny dla głównej części, ale pod koniec piłka szarpnie się na chwilę, gdy osiada na dole ekranu.

Rozumiem, dlaczego tak się dzieje, ale nie wydaje mi się, żeby to łagodziło.

Byłbym wdzięczny za wszelkie porady, które można zaoferować.

Mój kod aktualizacji to:

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

Może powinienem również podkreślić, że Gravity, Resistance Grassi Concretesą typu Vector2.

Ste
źródło
Aby to potwierdzić: twoje „tarcie”, gdy piłka uderza w powierzchnię, ma wartość <1, co w zasadzie jest poprawnym współczynnikiem restytucji ?
Jorge Leitao,
@ JCLeitão - poprawnie.
Ste
Nie przysięgaj, że będziesz przestrzegać głosów podczas przyznawania nagrody i poprawnej odpowiedzi. Idź po cokolwiek, co ci pomogło.
aaaaaaaaaaaa
To zły sposób na radzenie sobie z nagrodą, w gruncie rzeczy mówisz, że nie możesz oceniać siebie, więc pozwalasz głosować wyborom ... W każdym razie, to, czego doświadczasz, jest powszechnym drżeniem kolizji. Można to rozwiązać, ustawiając maksymalną wartość przenikania, minimalną prędkość lub dowolną inną „granicę”, która raz osiągnięta spowoduje, że rutyna zatrzyma ruch i położy obiekt na spoczynek. Możesz także dodać stan spoczynku do swoich obiektów, aby uniknąć niepotrzebnych kontroli.
Darkwings
@Darkwings - Myślę, że społeczność w tym scenariuszu wie lepiej ode mnie, co jest najlepszą odpowiedzią. Właśnie dlatego głosy poparcia wpłyną na moją decyzję. Oczywiście, gdybym wypróbował rozwiązanie z największą popularnością i nie pomogło mi to, nie udzieliłbym takiej odpowiedzi.
Ste

Odpowiedzi:

19

Oto kroki niezbędne do ulepszenia pętli symulacji fizyki.

1. Timepep

Główny problem, jaki widzę w twoim kodzie, polega na tym, że nie uwzględnia on kroku kroku fizyki. Powinno być oczywiste, że coś jest nie tak, Position += Velocity;ponieważ jednostki się nie zgadzają. Albo Velocityfaktycznie nie jest prędkością, albo czegoś brakuje.

Nawet jeśli twoje wartości prędkości i grawitacji są skalowane w taki sposób, że każda klatka dzieje się w jednostce czasu 1(co oznacza, że np. Velocity Faktycznie oznacza odległość przebytą w ciągu jednej sekundy), czas musi pojawić się gdzieś w kodzie, niejawnie (poprzez ustalenie zmiennych tak, aby ich nazwy odzwierciedlają to, co naprawdę przechowują) lub jawnie (poprzez wprowadzenie pomiaru czasu). Uważam, że najłatwiej jest zadeklarować jednostkę czasu:

float TimeStep = 1.0;

I używaj tej wartości wszędzie tam, gdzie jest to potrzebne:

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

Zauważ, że każdy porządny kompilator uprości mnożenie 1.0, dzięki czemu część nie spowolni.

Teraz Position += Velocity * TimeStepwciąż nie jest całkiem dokładne (zobacz to pytanie, aby zrozumieć, dlaczego), ale prawdopodobnie tak się stanie.

Ponadto należy wziąć pod uwagę czas:

Velocity *= Physics.Air.Resistance;

Trochę trudniej jest to naprawić; jednym z możliwych sposobów jest:

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

2. Podwójne aktualizacje

Teraz sprawdź, co robisz podczas odbijania (pokazano tylko odpowiedni kod):

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

Możesz zobaczyć, że TimeStepjest używane dwukrotnie podczas odrzuceń. Zasadniczo daje to kuli dwa razy więcej czasu na aktualizację. Oto co powinno się stać:

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

Sprawdź teraz tę część kodu:

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

Dodajesz grawitację przez cały czas trwania ramki. Ale co jeśli piłka faktycznie odbije się podczas tej klatki? Wtedy prędkość zostanie odwrócona, ale dodana grawitacja sprawi, że piłka przyspieszy od ziemi! Tak więc nadmiar grawitacji będą musiały być usuwane, kiedy odbijania , a następnie ponownie dodaje się w odpowiednim kierunku.

Może się zdarzyć, że nawet ponowne dodanie grawitacji we właściwym kierunku spowoduje zbyt duże przyspieszenie prędkości. Aby tego uniknąć, możesz albo pominąć dodawanie grawitacji (w końcu to nie jest tak dużo i trwa tylko rama), albo prędkość klamry do zera.

4. Naprawiono kod

A oto w pełni zaktualizowany kod:

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. Dalsze uzupełnienia

Aby uzyskać jeszcze lepszą stabilność symulacji, możesz zdecydować się na przeprowadzenie symulacji fizyki z większą częstotliwością. Jest to trywialne z uwagi na powyższe zmiany TimeStep, ponieważ wystarczy podzielić ramkę na tyle części, ile chcesz. Na przykład:

public void Update()
{
    float TimeStep = 1.0;
    Update(TimeStep / 4);
    Update(TimeStep / 4);
    Update(TimeStep / 4);
    Update(TimeStep / 4);
}
sam hocevar
źródło
„gdzieś w kodzie musi pojawić się czas”. Reklamujesz, że pomnożenie przez 1 w całym miejscu to nie tylko dobry pomysł, to obowiązkowe? Oczywiście regulowany timepep jest przyjemną funkcją, ale z pewnością nie jest obowiązkowy.
aaaaaaaaaaaa
@eBusiness: mój argument dotyczy bardziej spójności i wykrywania błędów niż regulowanych kroków czasowych. Nie mówię, że mnożenie przez 1 jest konieczne, mówię, że velocity += gravityjest złe i velocity += gravity * timestepma sens. W końcu może dać ten sam wynik, ale bez komentarza: „Wiem, co tu robię” nadal oznacza błąd w kodzie, niechlujny programista, brak wiedzy o fizyce lub po prostu prototypowy kod, który musi usprawniać się.
sam hocevar
Mówisz, że to źle , kiedy to, co rzekomo masz na myśli, to to, że to zła praktyka. To jest twoja subiektywna opinia w tej sprawie i dobrze, że ją wyrażasz, ale JEST ona subiektywna, ponieważ kod w tym względzie robi dokładnie to, co powinien. Proszę tylko o to, żebyście rozróżnili subiektywnie od celu w swoim poście.
aaaaaaaaaaaa
2
@eBusiness: szczerze mówiąc, jest to błędne według jakiegokolwiek rozsądnego standardu. Kod wcale „nie działa tak, jak powinien”, ponieważ 1) dodanie prędkości i grawitacji w rzeczywistości nic nie znaczy; oraz 2) jeśli daje rozsądny wynik, to dlatego, że przechowywana wartość gravityjest w rzeczywistości… nie grawitacją. Ale mogę to wyjaśnić w poście.
sam hocevar
Przeciwnie, nazywanie tego niewłaściwym jest niewłaściwe według jakiegokolwiek rozsądnego standardu. Masz rację, że grawitacja nie jest przechowywana w zmiennej o nazwie grawitacja, zamiast tego istnieje liczba, i to wszystko, co kiedykolwiek będzie, nie ma ona żadnego związku z fizyką poza tym, jak ją wyobrażamy, mnożąc ją przez inny numer tego nie zmienia. Pozornie to zmienia twoją zdolność i / lub gotowość do mentalnego połączenia kodu z fizyką. Nawiasem mówiąc, dość interesująca obserwacja psychologiczna.
aaaaaaaaaaaa
6

Dodaj zaznaczenie, aby zatrzymać odbicie, używając minimalnej prędkości pionowej. A kiedy dostaniesz minimalne odbicie, ustaw piłkę w ziemi.

MIN_BOUNCE = <0.01 e.g>;

if( Velocity.Y < MIN_BOUNCE ){
    Velocity.Y = 0;
    Position.Y = <ground position Y>;
}
Zhen
źródło
3
Podoba mi się to rozwiązanie, ale nie ograniczałbym odbicia do osi Y. Obliczę normalną zderzacza w punkcie zderzenia i sprawdzę, czy wielkość prędkości zderzenia jest większa niż próg odskoku. Nawet jeśli świat PO pozwala tylko na odbicia Y, inni użytkownicy mogą znaleźć bardziej ogólne rozwiązanie pomocne. (Jeśli jestem niejasny, pomyśl o odbiciu się dwóch sfer w losowym punkcie)
brandon
@brandon, świetnie, powinno działać lepiej z normalnym.
Zhen
1
@Zhen, jeśli użyjesz normalnej powierzchni, masz szansę, że kulka przylgnie do powierzchni, która ma normalną, która nie jest równoległa do grawitacji. Jeśli to możliwe, spróbuję uwzględnić grawitację w obliczeniach.
Nic Foster
Żadne z tych rozwiązań nie powinno ustawiać żadnych prędkości na 0. Ograniczasz odbicie tylko w normie wektora w zależności od progu odskoku
brandon
1

Myślę więc, że problemem jest to, że twoja piłka zbliża się do limitu. Matematycznie piłka nigdy nie zatrzymuje się na powierzchni, zbliża się do powierzchni.

Twoja gra nie wykorzystuje jednak ciągłego czasu. Jest to mapa, która wykorzystuje przybliżenie do równania różniczkowego. I to przybliżenie nie jest poprawne w tej ograniczającej sytuacji (możesz, ale musiałbyś podjąć mniejsze kroki, które, jak zakładam, są niewykonalne.

Fizycznie rzecz biorąc, dzieje się tak, że gdy piłka jest bardzo blisko powierzchni, przylega do niej, jeśli całkowita siła jest poniżej określonego progu.

@Zhen odpowiedź byłaby w porządku, jeśli twój system jest jednorodny, co nie jest. Ma pewną grawitację na osi y.

Powiedziałbym więc, że rozwiązaniem nie byłoby, aby prędkość była poniżej określonego progu, ale całkowita siła przyłożona do piłki po aktualizacji powinna być poniżej określonego progu.

Siła ta jest udziałem siły wywieranej przez ścianę na piłkę + grawitację.

Warunek powinien być podobny do tego

if (newVelocity + Physics.Gravity.Force <próg)

zauważ, że newVelocity.y jest wartością dodatnią, jeśli odbicie jest na ścianie dna, a grawitacja jest wartością ujemną.

Zauważ również, że newVelocity i Physics.Gravity.Force nie mają takich samych wymiarów, jak napisałeś

Velocity += Physics.Gravity.Force;

co oznacza, że ​​podobnie jak ty zakładam, że delta_time = 1 i ballMass = 1.

Mam nadzieję że to pomoże

Jorge Leitao
źródło
1

Masz aktualizację pozycji w ramach kontroli kolizji, jest ona zbędna i błędna. I dodaje piłce energii, potencjalnie pomagając jej w ciągłym ruchu. Oprócz tego, że grawitacja nie jest stosowana w niektórych ramkach, daje to dziwny ruch. Usunąć to.

Teraz możesz zobaczyć inny problem, polegający na tym, że piłka „utknęła” poza wyznaczonym obszarem, nieustannie odbijając się w przód iw tył.

Prostym sposobem rozwiązania tego problemu jest sprawdzenie, czy piłka porusza się we właściwym kierunku przed jej zmianą.

Dlatego powinieneś zrobić:

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

W:

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

I podobnie dla kierunku Y.

Aby piłka dobrze się zatrzymała, w pewnym momencie musisz zatrzymać grawitację. Twoja obecna implementacja zapewnia, że ​​kula zawsze będzie się wynurzać, ponieważ grawitacja nie hamuje jej, dopóki jest pod ziemią. Powinieneś zmienić na zawsze działającą grawitację. Prowadzi to jednak do tego, że kula powoli osiada po ziemi. Szybkim rozwiązaniem tego jest, po zastosowaniu grawitacji, jeśli piłka znajduje się poniżej poziomu powierzchni i porusza się w dół, zatrzymaj ją:

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

Te zmiany łącznie powinny zapewnić przyzwoitą symulację. Pamiętaj jednak, że wciąż jest to bardzo prosta symulacja.

aaaaaaaaaaaa
źródło
0

Zastosuj metodę mutatora dla wszystkich zmian prędkości, a następnie w ramach tej metody możesz sprawdzić zaktualizowaną prędkość, aby ustalić, czy porusza się wystarczająco wolno, aby ją zatrzymać. Większość znanych mi systemów fizyki nazywa to „restytucją”.

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;

W powyższej metodzie ograniczamy odbijanie, gdy jest ono wzdłuż tej samej osi co grawitacja.

Inną kwestią do rozważenia byłoby wykrycie, kiedy piłka zderzy się z ziemią, a jeśli porusza się dość wolno w momencie zderzenia, ustaw prędkość wzdłuż osi grawitacji na zero.

Nic Foster
źródło
Nie będę głosować, ponieważ jest to poprawne, ale pytanie dotyczy progów odskoku, a nie progów prędkości. Są one prawie zawsze osobne w moim doświadczeniu, ponieważ efekt drżenia podczas odbijania jest zasadniczo oddzielny od efektu dalszego obliczania prędkości, gdy wizualnie jest w spoczynku.
brandon
Są jednym w tym samym. Silniki fizyki, takie jak Havok lub PhysX, i JigLibX bazują na restytucji prędkości liniowej (i prędkości kątowej). Ta metoda powinna działać w przypadku każdego ruchu piłki, w tym odbijania. W rzeczywistości podczas ostatniego projektu (LEGO Universe) zastosowałem niemal identyczną metodę, aby zatrzymać odbijanie monet po zwolnieniu. W takim przypadku nie używaliśmy fizyki dynamicznej, więc musieliśmy to zrobić ręcznie, zamiast pozwolić Havokowi zająć się nami.
Nic Foster
@NicFoster: Jestem zdezorientowany, ponieważ moim zdaniem obiekt może poruszać się bardzo szybko w poziomie i prawie wcale w pionie, w którym to przypadku twoja metoda nie zadziała. Myślę, że OP chciałby, aby odległość pionowa była ustawiona na zero, pomimo dużej długości prędkości.
George Duckett,
@GeorgeDuckett: Ach, dziękuję, źle zrozumiałem oryginalne pytanie. OP nie chce, aby piłka przestała się poruszać, po prostu zatrzymaj ruch pionowy. Zaktualizowałem odpowiedź do konta tylko dla prędkości odbicia.
Nic Foster
0

Kolejna rzecz: mnożymy przez stałą tarcia. Zmień to - obniż stałą tarcia, ale dodaj stałą absorpcję energii na odbiciu. Spowoduje to znacznie szybsze tłumienie ostatnich odbić.

Loren Pechtel
źródło