• Najnowsze pytania
  • Bez odpowiedzi
  • Zadaj pytanie
  • Kategorie
  • Tagi
  • Zdobyte punkty
  • Ekipa ninja
  • IRC
  • FAQ
  • Regulamin
  • Książki warte uwagi

Pętla stałokrokowa - tworzenie gier.

Object Storage Arubacloud
+1 głos
826 wizyt
pytanie zadane 26 maja 2016 w C i C++ przez Kyoya Początkujący (260 p.)
Witam. Moje pytanie, a raczej prośba jest krótka i rzeczowa. Mam problem ze zrozumieniem mechanizmu działania gier, a mianowicie nie jestem w stanie znaleźć żadnego szczegółowego wyjaśnienia dotyczącego zagadnienia pętli stałokrokowej.Przeczytałem już masę artykułów, zarówno w języku polskim i angielskim. Szczerze mówiąc, żaden z nich nie był w stanie mi pomóc. Nie zależy mi na gotowym kodzie, tylko na możliwości zrozumienia w pełni całego zamysłu konceptualnego, który jest związany z tym zagadnieniem. Jeśli ktoś z Was, jest bardziej obeznany w tym temacie, proszę o wyjaśnienie mi tego pojęcia, albo podzielenie się jakimś artykułem, który Wam pomógł - być może jeszcze na niego nie trafiłem. Pozdrawiam.

2 odpowiedzi

+2 głosów
odpowiedź 26 maja 2016 przez draghan VIP (106,230 p.)
wybrane 26 maja 2016 przez Kyoya
 
Najlepsza

Czytałeś ten artykuł? Kiedy chciałem poznać to zagadnienie, okazał się bardzo pomocny. Plus lektura uzupełniająca.

komentarz 26 maja 2016 przez Kyoya Początkujący (260 p.)
Czytałem obydwa zaproponowane przez Ciebie źródła i niestety zbytnio mi nie pomogły. Żebym nie wyszedł na osobę leniwą, która chce wszystko podane na tacy, przeczytam  ten artykuł jeszcze raz. :)
komentarz 26 maja 2016 przez draghan VIP (106,230 p.)
W podesłanym pdfie wyjaśnione jest wszystko naprawdę dokładnie. Ponadto Patrycjerz wyjaśnił w swoim przykładowym kawałku kodu to zagadnienie chyba najprościej jak można. Nic lepszego Ci nie podeślę.

Pytaj o to, czego nie możesz załapać, ewentualnie zostaw to zagadnienie na parę dni i wróć do niego ze świeżym podejściem.
komentarz 26 maja 2016 przez Kyoya Początkujący (260 p.)
Co do pdf'a - to mam takie zastrzeżenia, że autor wprowadza w nim nowe zmienne, kompletnie nie wyjaśniając ich zastosowania. Mój problem jest taki, że przeczytałem już bardzo dużo artykułów na ten temat i rozumiem w jaki sposób to wszystko powinno działać, aczkolwiek patrząc na wszystkie dostępne implementacje - mam wrażenie, że żadna z nich nie zgadza się z tym jak to powinno wyglądać w teorii. Otóż dowiedziałem się czegoś takiego, że (cytat z pdf'a): "jeżeli między klatkami minęły 62 milisekundy, to UpdateGame() wykona się dwa razy przed renderingiem". Patrząc na kod, jakoś tego nie widzę. Po prostu nie mogę tego pojąć. :)
komentarz 26 maja 2016 przez draghan VIP (106,230 p.)

Może spróbuję wyjaśnić to własnymi słowami. Być może trafię do Ciebie lepiej, niż pozostałe źródła, chociaż... wątpię. Myślę że po prostu potrzeba Ci trochę dystansu, żeby to załapać.

 // powiedzmy, że Time reprezentuje klasę, która przedstawia czas, mierzony w ms

/*  Zmienna, informująca czy już minął określony czas. To ona "pilnuje", czy można już
    wykonać pewne działanie, czy nie.
*/
Time elapsed_time = 0;

/* Zmienna, mówiąca o tym, co ile ms dane działanie (np. update gry) ma się wykonać.
   Jeśli co 33 ms, to będzie ok. 30 razy na sekundę.
*/
const Time update_step = 33;

while(game.on()) // główna pętla gry
{
    /*  Sprawdź, "która godzina" jest przed kolejnym obiegiem pętli. Zostanie to 
        wykorzystane do obliczenia, ile czasu pochłonął dany obieg pętli i w ten
        sposób zorientujemy się, czy już minęło odpowiednio dużo czasu, żeby
        móc uruchomić metodę game::update().
    */
    Time on_begin = Time::get_now(); 

    /*  Jeśli czas który minął od poprzedniego wywołania update jest większy niż
        zakładany, to zerujemy elapsed_time i wywołujemy update.
   */
    if(elapsed_time > update_step)
    {
        elapsed_time = 0; // będziemy liczyć znów od zera, bo wywołamy update
        game.update();
    }

/*  W elapsed_time akumulowany jest czas, który upływa w każdym obiegu pętli.
    W celu wyznaczenia tego odcinka czasu, pytamy która jest godzina na końcu obiegu
    i po prostu odejmujemy czas końcowy od początkowego.       
*/
   Time on_end = Time::get_now();
   elapsed_time += on_end - on_begin;
}

 

komentarz 26 maja 2016 przez Kyoya Początkujący (260 p.)
A jednak przemówiłeś do mnie o wiele lepiej niż źródła dostępne w internecie. To chyba kwestia doboru słów i rozległego tłumaczenia. :) Rozumiem już wszystko poza tym jednym szczegółem - dlaczego w tym pdf'ie była mowa o wykonywaniu się Update'u gry 2 razy, w przypadku kiedy "elapsed_time" będzie wynosił dwukrotnie więcej niż "upadete_step"? Jak widać, niektóre artykuły mogą bardziej namieszać niż pomóc. :)
komentarz 26 maja 2016 przez draghan VIP (106,230 p.)

Tam masz nieco inną, "bogatszą" wersję pętli. Pętlę z "nadganianiem" zaległych zadań. Zauważ, że tam jest zapis w rodzaju:

while(game.on())
{
    while(elapsed_time > update_step) // nie if!
    {
        /* Nie zeruję, tylko pomniejszam - i jeśli po odjęciu, 
           czas który minął od poprzedniej iteracji będzie wciąż większy 
           niż update step, znaczy że update powinien się wykonać 
           ponownie - a co za tym idzie, ta pętelka wykona się raz jeszcze,
           znów odejmując i znów sprawdzając czy może update w tym czasie
           powinien wykonać się jeszcze raz?
         */
        elapsed_time -= update_step; 
        game.update();
    }
}

...pominąłem pobieranie czasu i całą nieistotną resztę...

komentarz 26 maja 2016 przez Kyoya Początkujący (260 p.)
Wiszę Ci porządne piwo. Nawet nie jestem w stanie wyrazić jak bardzo mi pomogłeś. :)

A teraz tak odbiegając z deka od tematu. Jaka pętla w grach jest najbezpieczniejszą lub najlepszą opcją? Czy może to wszystko zależy od programisty i przypadku?

Jeszcze raz dziękuję za pomoc. :)
komentarz 26 maja 2016 przez maciek061 Gaduła (4,490 p.)
@Kyoya nie jestem ekspertem, ale to pewnie zależy od rodzaju gry
komentarz 26 maja 2016 przez draghan VIP (106,230 p.)

Wiszę Ci porządne piwo. Nawet nie jestem w stanie wyrazić jak bardzo mi pomogłeś. :)
Jeszcze raz dziękuję za pomoc. :)

Na zdrowie. :)

A teraz tak odbiegając z deka od tematu. Jaka pętla w grach jest najbezpieczniejszą lub najlepszą opcją? Czy może to wszystko zależy od programisty i przypadku?

Nie ma jednoznacznej odpowiedzi. Wszystko zależy od konkretnych wymagań.

Powiem Ci, jak to wygląda według mnie - zaś czy to jest dobre podejście, czy nie... nie wiem.
Dobrze jest, jeśli gra jest obsługiwana przez pętlę stałokrokową. Ale to już od samej logiki aplikacji zależy, czy dany fragment MUSI wykonać się zadaną liczbę razy w zadanym czasie (wersja "nadganiająca"), czy wystarczy kiedy wykona się "w czasie nie mniejszym niż".
Pierwszą opcję widziałbym dla krytycznych obliczeń, które pilnują poprawności przebiegu aplikacji, jak na przykład wyliczanie fizyki. Drugą opcję zastosowałbym dla renderowania klatek, bo nadganianie z renderowaniem w sytuacji gdzie to sam rendering opóźnia aplikację, spowoduje że gra będzie stała na samym renderowaniu w nieskończoność.

komentarz 26 maja 2016 przez Kyoya Początkujący (260 p.)
Można w jakiś sposób rozdzielić fizykę gry od renderingu? Tak żeby nadganianie następowało tylko dla fizyki?
komentarz 26 maja 2016 przez draghan VIP (106,230 p.)
Przykład z nieskończonym renderowaniem jest oczywiście przejaskrawiony.

Rozdzielić fizykę od części renderującej nie tylko można, ale należy. Nie wyobrażam sobie dobrego kodu, w którym byłyby wymieszane obliczenia fizyki z kodem odpowiedzialnym za rysowanie.
komentarz 26 maja 2016 przez Kyoya Początkujący (260 p.)
Czy takim zabezpieczeniem mogłoby być na przykład umieszczenie samej fizyki w pętli stałokrokowej, a renderowania poza nią?
komentarz 26 maja 2016 przez draghan VIP (106,230 p.)

Możesz wywoływać update fizyki w wewnętrznym while'u, a całą resztę poza nim, oczywiście w ramach pętli głównej. Ale nic nie stoi na przeszkodzie, żeby użyć kilku wariantów w jednej pętli:

const Time physics_step = 33;
const Time rendering_step = 33; // można oczywiście dać inny interwał dla obu zadań

Time physics_elapsed_time = 0;
Time rendering_elapsed_time = 0;

while(game.on())
{
    Time on_begin = Time::get_now(); 

    while(physics_elapsed_time > physics_step)
    {
        physics_elapsed_time -= physics_step;
        game.update_physics();
    }

    if(rendering_elapsed_time > rendering_step)
    {
        rendering_elapsed_time = 0;
        game.draw();
    }

   Time delta = Time::get_now() - on_begin;
   physics_elapsed_time += delta;
   rendering_elapsed_time += delta;
}

 

komentarz 26 maja 2016 przez Kyoya Początkujący (260 p.)
Genialne. Dziękuję ślicznie. :)
komentarz 31 października 2023 przez Morvan Nowicjusz (100 p.)

@draghan, Niestety linki juz wygasły. Masz albo ktos inny ma alternatywę?

komentarz 31 października 2023 przez Morvan Nowicjusz (100 p.)
0 głosów
odpowiedź 26 maja 2016 przez Patrycjerz Mędrzec (192,320 p.)
Głównym zadaniem pętli gry jest aktualizacja aplikacji, czyli wszystkiego, co się w grze dzieje na okrągło (zdarzenia, przemieszczanie obiektów, ich rysowanie itp.).

Pętle gry mają różnoraką postać, zależną od konkretnych potrzeb.

Pętla stałokrokowa umożliwia kontrolowanie gry wg stałego kroku czasowego - taka cecha może być przydatna, gdy tworzymy w naszej grze np. symulacje fizyczne i potrzebna jest nam precyzja obliczeń (znając dokładny czas, wg którego zmienia się stan obiektów, możemy uniknąć niemiłych sytuacji i się na nie przygotować).
komentarz 26 maja 2016 przez Kyoya Początkujący (260 p.)
To wszystko już zdążyłem wyczytać z internetu. Mój problem bardziej polega na tym, że nie wiem jak się do tego wszystkiego zabrać. Jestem w stanie stworzyć podstawową pętlę gry, z uwzględnieniem obsługi zdarzeń itd, ale nie wiem jak w to wszystko zaimplementować pętlę stałokrokową.
komentarz 26 maja 2016 przez Patrycjerz Mędrzec (192,320 p.)
edycja 26 maja 2016 przez Patrycjerz

Należy pamiętać, że stan obiektów jest zależny od czasu - to jest słowo klucz, zapominane przez początkujących twórców gier.

Przy pętli stałokrokowej wywołujesz funkcje aktualizujące grę tylko w pewnych odstępach czasu, nigdy wcześniej:

float krok = 0.0f;
while(gra.czyOtwarta())
{
	float czas_1 = zmierzCzas();
	if(krok > 0.1f) // Czyli 10 FPS
	{
		gra.aktualizuj();
		krok = 0.0f;
		continue;
	}
	float czas_2 = zmierzCzas();
	krok += czas_2 - czas_1;
}

PS: Możesz także użyć funkcję zatrzymującą dany proces na pewien czas (na Windowsie to Sleep) - będzie to nawet lepszy pomysł, niż ciągłe mierzenie czasu.

komentarz 26 maja 2016 przez Kyoya Początkujący (260 p.)
No i właśnie w tym momencie jestem trochę zdezorientowany. :)

1. Zmienna "krok" ma w tym przypadku zajmować się pomiarem czasu trwania obiegu pętli?
2. Dlaczego blok warunkowy ma się wykonywać kiedy kro jest większy od 0.1f?
3. Dlaczego po bloku warunkowym występuje obliczanie delty czasu?

Widziałem już tak wiele implementacji pętli stałokrokowej, a nadal nie mogę tego tematu ugryźć.

@Edit: Używanie sleep'ów zbytecznie obciążyłoby aplikację, a tego chyba nie chcemy, prawda? :)
komentarz 26 maja 2016 przez Patrycjerz Mędrzec (192,320 p.)

1. Tak.

2. Ponieważ jest to główne założenie pętli stałokrokowej. Aktualizujesz grę tylko po upłynięciu pewnego czasu, nigdy wcześniej.

Rozpatrzmy dwa przypadki:

  • Komputer potrafi aktualizować stan gry szybciej, niż jest to mu ograniczone - wtedy masz pewność, że twoja symulacja nie ulegnie awarii, gdyż kontrolujesz czas zmian.
  • Komputer nie potrafi aktualizować stan gry szybciej, niż jest to mu ograniczone - gra będzie wtedy działać wolniej niż na szybszym sprzęcie.

Jeśli takie cechy ci nie odpowiadają, to pomyśl o innym typie pętli - jest ich wiele.

3. Do obliczenia kroku czasowego zastosowałem wyimaginowaną funkcję, która zwraca ilość sekund od pewnej daty... przyjmijmy, że od 1 stycznia 1970 roku (analogicznie do funkcji time).

Wtedy muszę obliczyć różnicę czasów, która będzie równa czasowi trwania jednego obiegu pętli.

Co do funkcji Sleep, to jest ona przeciwieństwem "obciążenia aplikacji", gdyż przerywa ona cały wątek - w skrócie: przyspiesza ona komputer, a nie zwalnia.

komentarz 26 maja 2016 przez draghan VIP (106,230 p.)

PS: Możesz także użyć funkcję zatrzymującą dany proces na pewien czas (na Windowsie to Sleep) - będzie to nawet lepszy pomysł, niż ciągłe mierzenie czasu.

Zatrzymanie procesu w grze (a już w ogóle poprzez Sleep) nie jest dobrym pomysłem. Głównym powodem jest konieczność zbierania wejścia na bieżąco - nie może tu występować martwa przerwa, bo gracz miałby możliwość interakcji ze światem tylko podczas wstrzelenia się z przyciśnięciem klawisza akurat kiedy aplikacja nie śpi... czyli generalnie słabo.
Ale tak pominąwszy kwestię zbierania wejścia - co w przypadku, kiedy AI chcemy aktualizować 7 razy na sekundę, a update obiektów i rysowanie klatki 30 razy na sekundę? Odmierzanie czasu i taka postać pętli, jaką podałeś, to rozwiązanie zupełnie dobre.

komentarz 26 maja 2016 przez Kyoya Początkujący (260 p.)
Jeżeli dobrze rozumiem, to przy pierwszym obiegu pętli, zmienna czas_1, będzie równa 0, prawda? Załóżmy, że obieg pętli przypisze do zmiennej czas_2 wartość 0.1f. Krok wyniesie wtedy 0.1f - 0.f, czyli 0.1f. Warunek krok > 0.1f nie zostanie spełniony. Co stanie się w takim przypadku? Według tego co przeanalizowałem krok po kroku, jeżeli założymy, że wykonanie się instrukcji zawartej w pętli trwałoby 0.1f, to czy nie miałoby to takiego skutku, że instrukcja w bloku warunkowym nie wykonałaby się nigdy?
komentarz 26 maja 2016 przez Patrycjerz Mędrzec (192,320 p.)

@draghan

Tak, masz rację, ale zauważ, że obsługą zdarzeń można obarczyć inny wątek.

Podany przez ciebie przykład aktualizacji AI i obiektów jest trochę nietrafiony, gdyż jest wg mnie bezsensowny - taką fanaberie należałoby obsłużyć poprzez zmodyfikowaną pętlę stałokrokową - gdy porzucimy ten pomysł, to Sleep wydaje się całkiem niezłym rozwiązaniem.

Popatrz na metodę setFramerateLimit z SFML, która właśnie używa przerwanie wątku, ograniczając tym samym zużycie procesora (bez tego oscyluje ono w granicach 15-20%, z tym praktycznie spada do zera):

void Window::display()
{
    // Display the backbuffer on screen
    if (setActive())
        m_context->display();

    // Limit the framerate if needed
    if (m_frameTimeLimit != Time::Zero)
    {
        sleep(m_frameTimeLimit - m_clock.getElapsedTime());
        m_clock.restart();
    }
}

Ogólnie temat pętli gier jest bardzo obszerny i trudno opisać wszystko w jednej odpowiedzi. Pętlę można stworzyć na wiele sposób i tak naprawdę od programisty zależy, w jaki sposób ją zaimplementuje, albo czy nie wymyśli w ogóle innego typu, idealnego do jego potrzeb.

@Kyoya

Czy ty w ogóle czytasz, co napisałem? Zmienna czas_1 będzie miała wartość sekund od 1 stycznia 1970 roku, czas_2 zaś troszkę więcej - odejmując te dwa czasy uzyskasz czas obiegu pętli.

komentarz 26 maja 2016 przez draghan VIP (106,230 p.)

Tak, masz rację, ale zauważ, że obsługą zdarzeń można obarczyć inny wątek.

Wątek można wstrzymać i będzie to dobre rozwiązanie. Wstrzymanie procesu już nie.

Podany przez ciebie przykład aktualizacji AI i obiektów jest trochę nietrafiony, gdyż jest wg mnie bezsensowny

Dlaczego uważasz go za bezsensowny? AI w żadnym wypadku nie powinno wykonywać obliczeń co klatkę, a przecież pętla stałokrokowa może bez problemu obsługiwać kilka różnych 'zdarzeń', wymagających obsługi z różnym interwałem.

Podobne pytania

0 głosów
2 odpowiedzi 401 wizyt
pytanie zadane 10 sierpnia 2017 w C i C++ przez WireNess Stary wyjadacz (11,240 p.)
+4 głosów
3 odpowiedzi 1,785 wizyt
pytanie zadane 30 maja 2016 w C i C++ przez niezalogowany
0 głosów
8 odpowiedzi 20,199 wizyt

92,555 zapytań

141,403 odpowiedzi

319,553 komentarzy

61,939 pasjonatów

Motyw:

Akcja Pajacyk

Pajacyk od wielu lat dożywia dzieci. Pomóż klikając w zielony brzuszek na stronie. Dziękujemy! ♡

Oto polecana książka warta uwagi.
Pełną listę książek znajdziesz tutaj.

Akademia Sekuraka

Kolejna edycja największej imprezy hakerskiej w Polsce, czyli Mega Sekurak Hacking Party odbędzie się już 20 maja 2024r. Z tej okazji mamy dla Was kod: pasjamshp - jeżeli wpiszecie go w koszyku, to wówczas otrzymacie 40% zniżki na bilet w wersji standard!

Więcej informacji na temat imprezy znajdziecie tutaj. Dziękujemy ekipie Sekuraka za taką fajną zniżkę dla wszystkich Pasjonatów!

Akademia Sekuraka

Niedawno wystartował dodruk tej świetnej, rozchwytywanej książki (około 940 stron). Mamy dla Was kod: pasja (wpiszcie go w koszyku), dzięki któremu otrzymujemy 10% zniżki - dziękujemy zaprzyjaźnionej ekipie Sekuraka za taki bonus dla Pasjonatów! Książka to pierwszy tom z serii o ITsec, który łagodnie wprowadzi w świat bezpieczeństwa IT każdą osobę - warto, polecamy!

...