Autor Wątek: Pętla gry w SDL  (Przeczytany 2484 razy)

Offline lolcio

  • Użytkownik

# Maj 20, 2012, 19:25:29
Witam,
Chciałbym zapytać co sądzicie o mojej pętli gry:
    register float dt = 0.0f;
    register float lastUpdateTime = (float)( SDL_GetTicks() / 1000 );
    register float accumulator = 0.0f;
    register const float TIME_STEP = Property::getSetting("TIME_STEP");

    while ( !pIsDone ) {

        // Obliczanie czasu aktualizacji
        dt = (float)( SDL_GetTicks() - lastUpdateTime ) / 1000 ;
        lastUpdateTime += dt;
        accumulator += dt;

        // Obsluga zdarzen
        processEvent();

        // Aktualizacja
        while ( accumulator > TIME_STEP )
        {
            pGame->update( TIME_STEP );
            accumulator -= TIME_STEP;
        }

        // Rysowanie
        {
            SDL_FillRect(pScreen ,NULL ,0 );
            pGame->draw();
            SDL_Flip(pScreen);
        }

        pIsDone = pGame->isDone();
    }

Chodzi mi tutaj głównie o aktualizacje gry. Wydaje mi się że źle obliczam czas.

Offline Mr. Spam

  • Miłośnik przetworów mięsnych

Offline Xirdus

  • Redaktor

# Maj 20, 2012, 22:31:39
    register float dt = 0.0f;
    register float lastUpdateTime = (float)( SDL_GetTicks() / 1000 );
    register float accumulator = 0.0f;
    register const float TIME_STEP = Property::getSetting("TIME_STEP");
Dlaczego "register"?


Kod wygląda OK. Jedyne co bym zmienił to w "accumulator > TIME_STEP" dał ">=", usunął SDL_FillRect() (chyba że koniecznie potrzebujesz koloru tła), SDL_Flip() wrzucił do pGame->draw(), a pIsDone w warunku pętli zamienił na pGame->isDone().

Offline koirat

  • Użytkownik

# Maj 21, 2012, 00:20:53
Można by pokusić się o przechowywanie czasu gry jako ticki,  zmienna typu int, i dopiero konwertować na zmiennoprzecinkowe sekundy w ostatnim momencie. Wtedy nie będziesz miał strat związanych z zaokrągleniami i dokładnością. Oraz trochę szybsze, ale bez znaczenia biorąc pod uwagę całość aplikacji.

Co do pGame->draw(); to tą funkcję przerobił bym na funkcję z parametrem będącym czasem od ostatniego wywołania pGame->update( TIME_STEP );  czyli  pGame->draw(dt);. Dzięki temu będziesz mógł przeprowadzić integrację ruchu w celu osiągnięcia płynniejszej animacji.
Przykładowo Unity posiada aż 3 rodzaje updatów FixedUpdate , Update oraz LateUpdate.

btw. co robi processevents ? I czy na pewno go potrzebujesz.

« Ostatnia zmiana: Maj 21, 2012, 00:24:08 wysłana przez koirat »

Offline lolcio

  • Użytkownik

# Maj 21, 2012, 09:05:37
Xirdus:
Register jest związane z optymalizacją tego fragmentu aplikacji. Zmienna z tym kwalfikatorem nie jest przechowywana w pamięci komputera tylko w rejestrze procesora, dzięki czemu mamy do niej szybszy dostęp ( Czasamii kompilator sam może włączyc taką optymalizacje np. w zmiennych będących licznikiem pętli ).
Ekran muszę wypełniać jednolitym kolorem żeby usunąć poprzednie rysowanie.
Zmienna pIsDone jest polem klasy, i zgodnie z przyjęta przezemnie konwencją pola nazywam zaczynając od literki p. :)

koirat:
Faktycznie czas gry można później zaokrąglać. Tak też zrobie.
A co do draw z prameterem...nidgy tak nie robiłem, jak powinna wyglądać taka funkcja rysująca? Myślałem że tylko update wymaga przekazania jako parametr czasu.
ProcessEvent obsługuje zdarzenia:
void App::processEvent() {
    while ( SDL_PollEvent( &pEvent ) )
    {
        if ( pEvent.type == SDL_QUIT )
        {
            pIsDone = true;
        }
        else if (pEvent.type == SDL_KEYDOWN && pEvent.key.keysym.sym == SDLK_ESCAPE ) {
            gInfo<string>("ESC pressed");
            pGame->pressedEsc();
        }
//#endif DEBUG
        else if (pEvent.type == SDL_KEYDOWN && pEvent.key.keysym.sym == SDLK_RETURN ) {
            pGame->pressedReturn();
        }
        else if (pEvent.type == SDL_KEYDOWN && pEvent.key.keysym.sym == SDLK_LEFT ) {
            pGame->pressedLeft();
        }
        else if (pEvent.type == SDL_KEYDOWN && pEvent.key.keysym.sym == SDLK_RIGHT ) {
            pGame->pressedRight();
        }
        else if (pEvent.type == SDL_KEYDOWN && pEvent.key.keysym.sym == SDLK_UP ) {
            pGame->pressedUp();
        }
        else if (pEvent.type == SDL_KEYDOWN && pEvent.key.keysym.sym == SDLK_DOWN ) {
            pGame->pressedDown();
        }
        else if (pEvent.type == SDL_KEYDOWN && pEvent.key.keysym.sym == SDLK_SPACE ) {
            pGame->pressedSpace();
        }
        else if ( pEvent.type == SDL_KEYDOWN &&
                  ( pEvent.key.keysym.sym >= 97 && pEvent.key.keysym.sym >= 122 )
                )
        {
            ///@TODO obsluga klawiatury do wpisania highscore
        }
        ///@TODO Reszta zdarzen do dopisania
    }
}

Offline asmen

  • Użytkownik

  • +2
# Maj 21, 2012, 09:14:24
Haha. Myślę, że Xirdus wie po co jest 'register' pytał tylko po co ty je tam pakujesz ;p

Offline Kos

  • Użytkownik
    • kos.gd

  • +4
# Maj 21, 2012, 10:49:28
Słówko 'register' było potrzebne może 10, 20 lat temu. Teraz kompilatory potrafią zrobić konkretną analizę przepływu danych i wierz mi, potrafią sobie same poprzydzielać wartości do rejestrów ;-).

Offline koirat

  • Użytkownik

# Maj 21, 2012, 19:17:27
A co do draw z prameterem...nidgy tak nie robiłem, jak powinna wyglądać taka funkcja rysująca? Myślałem że tylko update wymaga przekazania jako parametr czasu.

Chodzi o to iż twoja funkcja Update powinna ustanawiać pewien przyszły stan obiektu. Pozycja Rotacja a również animacja będzie o jeden Update spóźniona, jednak posiadając wcześniejszy stan obiektu i przyszły stan obiektu możesz interpolować pozycje, orientacje i animacje obiektów pomiędzy wywoływaniami Update. Jeśli FPS wyświetlania będzie większy niż FPS updatów otrzymasz bardziej płynny efekt zmiany stanów. Możesz fizykę wywoływać 30 razy na sekundę a stan obiektu będzie się płynnie zmieniał przykładowo 120 razy.

Offline lolcio

  • Użytkownik

# Maj 22, 2012, 23:58:29
Dopisać register nic nie kosztuje :P A zawsze może się zdarzyc że kompilator źle zoptymalizuje kod... Czasami spotykam się z sytuacją że na jednym kompilatorze kod działa bez problemu, na innym się wywala, i kilkanaście godzin stracone na szukanie rozwiązania. ( Mowa o róznych werjsach gcc pod linuksa ).

A co sądzisz koirat o takiej wersji pętli:
register float dt = 0.0f;
    register float lastUpdateTime = (float)( SDL_GetTicks() / 1000 );
    register float accumulator = 0.0f;
    register const float TIME_STEP = Property::getSetting("TIME_STEP");

   
    while ( !pIsDone ) {

        // Obliczanie czasu aktualizacji
        dt = (float)(( SDL_GetTicks() - lastUpdateTime )) / 1000 ;
        lastUpdateTime += dt;
        accumulator += dt;

        // Obsluga zdarzen
        processEvent();

//
        while ( accumulator > TIME_STEP )
        {
  SDL_FillRect(pScreen ,NULL ,0 );     
  pGame->draw();
          SDL_Flip(pScreen);
          accumulator -= TIME_STEP;
        }

        //
        {
  pGame->update( TIME_STEP );         
        }

        pIsDone = pGame->isDone();

#ifdef DEBUG
        pframeRating++; //licznik wyswietlonych klatek w danej jednostce czasu
#endif

      SDL_Delay( 1 );

    }//END WHILE

Offline koirat

  • Użytkownik

# Maj 23, 2012, 00:24:59
Ta pętla jest zła.
Po pierwsze lastUpdateTime trzymasz w zmiennej zmiennoprzecinkowej jako sekundy, a później używasz tej zmiennej odejmując ją od SDL_GetTicks() co jest błędem i ta pętla nie może ci dobrze działać.
Po co robić pętlę dla draw ?

Zmąciłem przykład na szybko ale radzę ci coś o tym poczytać, choćby:
http://gafferongames.com/game-physics/fix-your-timestep/

while(true)
{
        while ( accumulator > TIME_STEP )
        {     
  pGame->update(TIME_STEP);
          accumulator -= TIME_STEP;

          //Nad tym się można zastanowić gdyż czasem może dojść do sytuacji w której nie będziesz wyrabiał i twój    accumlator będzie rósł i rósł.
            if(accumlator >=TIME_STEP) 
                   accumlator=0;
        }

  pGame->draw(dt);

        if(pGame->isDone)
           break;
}

Offline ShadowDancer

  • Redaktor

# Maj 23, 2012, 00:28:16
Co do register, to wątpię, aby kompilator brał sobie do serca to co ty mu powiesz, podobnie jak z inline.

Offline Anton Chigurh

  • Użytkownik

# Maj 23, 2012, 10:51:10
while(true)
{
        while ( accumulator > TIME_STEP )
        {     
  pGame->update(TIME_STEP);
          accumulator -= TIME_STEP;

          //Nad tym się można zastanowić gdyż czasem może dojść do sytuacji w której nie będziesz wyrabiał i twój    accumlator będzie rósł i rósł.
            if(accumlator >=TIME_STEP) 
                   accumlator=0;
        }
}

Tak na prawdę proponujesz kanoniczne rozwiązanie z akumulatorem + hack, więc nie ma sensu udawać, że naprawdę chodzi o "while ( accumulator > TIME_STEP )"). To, co proponujesz można imo jaśniej napisać tak :
while(true)
{
        // Tu gdzieś inkrementacja akumulatora...

        if(accumulator < TIMESTEP)
                continue;

        accumulator = 0;
        pGame->update(TIME_STEP);
        pGame->draw(TIME_STEP); // Czy tu powinien być potrzebny "dt" ???
}
Pomysł wygląda rozsądnie (ja sie nigdy świadomie nie przygotowywałem na "gubienie klatek").

Offline Kos

  • Użytkownik
    • kos.gd

# Maj 23, 2012, 11:06:27
Bardziej złożone podejście bierze pod uwagę, że logika może być odpalana w innym tempie niż grafika i podczas rysowania interpoluje pozycje obiektów między 2 klatkami logiki.

Offline koirat

  • Użytkownik

# Maj 23, 2012, 11:35:36
@Anton
drugi "while" zostawiłem gdyż jak napisałem element z zerowaniem akumulatora jest kwestią do rozważenia.
Tak w draw powinien być dt, gdyż tak jak napisał @kos a ja kilka postów powyżej chodzi o interpolacje. Prawdę powiedziawszy to nawet nie koniecznie dt powinno się tam przekazywać tylko to co zostało w akumulatorze po odjęciu TIME_STEP, albo jeszcze dodatkowo podzielić to przez TIME_STEP.

W twoim przypadku logika i rendering są wywoływane zawsze w tym samym momencie. czyli jak TIME_STEP będziemy mieli 20/sek to FPS też będzie 20. Ogólnie twoja pętla jest dość minimalistyczna.

Offline Anton Chigurh

  • Użytkownik

# Maj 23, 2012, 12:58:12
Prawdę powiedziawszy to nawet nie koniecznie dt powinno się tam przekazywać tylko to co zostało w akumulatorze po odjęciu TIME_STEP
O, to już mnie tak nie dziwi.

W twoim przypadku logika i rendering są wywoływane zawsze w tym samym momencie. czyli jak TIME_STEP będziemy mieli 20/sek to FPS też będzie 20. Ogólnie twoja pętla jest dość minimalistyczna.
Chodziło mi tylko o bardziej przejrzyste przedstawienie tego, co ująłeś "hakerskim" ifem.
Pełna implementacja oczywiście wymaga ujęcia zarówno sytuacji, gdy logika chodzi n-krotnie wolniej od renderingu i odwrotnie (chyba, że dla konkretnej gry proporcja jest zawsze "w jedną stronę").

Może się czepiam i nieco plączę, ale nie jestem w tej dziedzinie ekspertem a dodatkowo może to czytać wiele osób, które nigdy takiej pętli [dobrze] nie napisały, więc chętnie brnę w uściślenia.

Offline hashedone

  • Użytkownik

# Maj 23, 2012, 13:16:17
To, co proponujesz można imo jaśniej napisać tak :
while(true)
{
        // Tu gdzieś inkrementacja akumulatora...

        if(accumulator < TIMESTEP)
                continue;

        accumulator = 0;
        pGame->update(TIME_STEP);
        pGame->draw(TIME_STEP); // Czy tu powinien być potrzebny "dt" ???
}
Pomysł wygląda rozsądnie (ja sie nigdy świadomie nie przygotowywałem na "gubienie klatek").
To jest złe i powoduje wybuch fizyki na wolnych komputerach. Poczytaj to co przytoczył koirat;)