Autor Wątek: Stała szybkość gry i nielimitowana liczba FPS  (Przeczytany 1264 razy)

Offline dsonyy

  • Użytkownik

# Sierpień 12, 2017, 00:48:44
Witam.
Jestem po lekturze tego (deWiTTERS Game Loop) artykułu o pętlach głównych stosowanych w grach. Rozpocząłem implementację ostatniej przedstawionej pętli korzystając z dobrodziejstw Allegro 5 i napotkałem na problem.

Skrócone założenia działania pętli:
  • szybkość gry (logiki) ma być stała (np. 25/s),
  • renderowanie klatek ma następować tak szybko jak to możliwe (brak maksymalnej liczby FPS),
  • funkcja rysująca jest wywoływana częściej niż logika, więc posiada możliwość sztucznego wytwarzania 'pośrednich' klatek pomiędzy dwoma wywołaniami logiki (dokładniej opisano to w artykule).

Nie mam wielkiego doświadczenia z Allegro 5 i jedyne co przychodzi mi do głowy to korzystanie ze standardowego ALLEGRO_EVENT_QUEUE, timera i switcha na eventy (link to allegrowej wiki). Jednak w tym przypadku wywołania funkcji rysującej siłą rzeczy będą regulowane przez timery. Prosiłbym o rady dotyczące tego w jaki sposób zaimplementować omawianą pętlę.

Z góry wielkie dzięki.
« Ostatnia zmiana: Sierpień 12, 2017, 01:08:36 wysłana przez dsonyy »

Offline Mr. Spam

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

Offline albireo

  • Użytkownik

# Sierpień 12, 2017, 15:08:54
Wywal timery i zrób coś w tym stylu:
ALLEGRO_EVENT ev;
while(1)
{
  while(al_get_next_event(event_queue, &ev))
  {
    // obsługa zdarzeń
    ...
  }
  // rysowanie
  ...
}
No i trzeba jeszcze sprawdzić ile czasu minęło między kolejnymi odrysowaniami (np korzystając z funkcji al_get_time) aby wywołać odpowiednio przeliczanie logiki i interpolacje miedzy kolejnymi jej krokami.
« Ostatnia zmiana: Sierpień 12, 2017, 15:12:58 wysłana przez albireo »

Offline MDW

  • Użytkownik
    • www.encore-games.com

  • +1
# Sierpień 12, 2017, 21:08:58
Ja może czegoś nie rozumiem ale po co robić taki zabieg? Jeżeli logika nie zmieni pozycji obiektów, niczego nie zmieni na scenie to po co taką scenę powtórnie renderować? Zazwyczaj ogranicza się renderowanie. No ale może jest jakiś powód żeby robić to na odwrót.

Offline dsonyy

  • Użytkownik

  • +1
# Sierpień 13, 2017, 00:13:48
Jeżeli logika nie zmieni pozycji obiektów, niczego nie zmieni na scenie to po co taką scenę powtórnie renderować?
Wyjaśnię to na przykładzie. Mamy prostokąt. Funkcja render() wyświetla figurę na ekranie. Funkcja logic() za każdym razem przesuwa ów prostokąt o 10px w prawo.
Przykładowe działanie pętli, którą opisujesz, wygląda tak:
  • logic() - Przesunięcie obiektu na pozycję (100, 100),
  • render() - Wyrenderowanie prostokąta na pozycji (100, 100),
  • render() - Ponowne wyrenderowanie prostokąta na pozycji (100, 100),
  • render() - i znowu (100, 100),
  • render() - ... ,
  • logic() - O! Przesunięcie na pozycję (110, 100),
Faktycznie, funkcja renderująca jest wywoływana wiele razy, ale w praktyce nic to nie daje. Natomiast w moim przypadku funkcja render() przyjmuje dodatkowy argument zwany interpolacją. Co to ta interpolacja? Funckja logic() w mojej pętli jest zawsze wywoływana 25 razy na sekundę - czyli raz na 40 milisekund. Interpolacja to liczba milisekund, które minęły od ostatniego wywołania logic() podzielona przez częstotliwość wywołań logiki (40 milisekund). Przykład:

'i' to interpolacja obliczana funkcją inter()
  • logic() - Przesunięcie obiektu na pozycję (100, 100),
  • inter() - obliczenie interpolacji (od wywołania logic() minęło 0ms; interpolacja = 0.0),
  • render(i) - Wyrenderowanie obiektu na pozycji (100, 100),
  • inter() - obliczenie interpolacji (od wywołania logic() minęło 20ms; interpolacja = (20 / 40) = 0.5)
  • render(i) - Wyrenderowanie obiektu na pozycji (105, 100)(!) - mimo iż fizycznie znajduje się na (100,100),
  • inter() - obliczenie interpolacji (od wywołania logic() minęło 30ms; interpolacja = (30 / 40) = 0.75),
  • render(i) - Wyrenderowanie obiektu na pozycji (107, 100),
  • logic() - Faktyczne przesunięcie obiektu na pozycję (110, 100),
  • inter() - obliczenie interpolacji (od wywołania logic() minęło 0ms; interpolacja = 0.0),
  • render(i) - Wyrenderowanie obiektu na pozycji (110, 100),
Jak widać, korzystanie z tego zabiegu zaczyna mieć powoli sens.

W funkcji render() 'widoczne' współrzędne obiektu są obliczane w ten sposób:
view_position = position + (speed * interpolation);Czyli:
view_position = 100 + (10 * (20 / 40)); // view_position równe 105
view_position = 100 + (10 * (30 / 40)); // view_position równe 107
// interpolacja to (liczba milisekund od ostatniego wywołania logic()) / (częstotliwość logiki (40ms))

Mam nadzieję, że moje wypociny będą dla kogoś zrozumiałe ;) Implementacja takiej pętli wiąże się oczywiście z rozbudową funkcji render() o dodatkowe mechanizmy sprawdzające, czy ciało jest w ruchu i obliczające 'widoczne' pozycje obiektów. Efektem jest to, że logika generuje stale 25 'surowych' klatek na sekundę, natomiast render(i) rysuje dodatkowo klatki 'pośrednie' nadając grze większą płynność.
 
« Ostatnia zmiana: Sierpień 13, 2017, 00:41:36 wysłana przez dsonyy »

Offline MDW

  • Użytkownik
    • www.encore-games.com

# Sierpień 13, 2017, 00:39:08
Bardzo rozbudowane wytłumaczenie. Wystarczyłoby żebyś je skończył w momencie gdy pierwszy raz użyłeś wyrazu "interpolacja". Jeżeli funkcja przyjmuje jakiś zmienny parametr to jak najbardziej ma to sens. :)
Swoją drogą to dałeś mi tym opisem trochę do myślenia. Całkiem zgrabnie to sobie wykombinowałeś.
Powodzenia!

Offline dsonyy

  • Użytkownik

# Sierpień 13, 2017, 01:07:05
Cała zasada działania pętli wraz z jej nie-Allegrową implementacją opisana jest w artykule z pierwszego posta. Przedstawia on też całkiem inne podejścia do tematu.

A co do rozbudowanego opisu, może faktycznie trochę przesadziłem ;) Jednak zasada działania mocno utkwiła mi przez to w pamięci.

Offline Karol

  • Użytkownik

  • +1
# Sierpień 13, 2017, 19:02:43
Niby fajne, ale trochę może być kłopotliwe. Przesuwanie czy obracanie obiektów - ok, ale jak logika będzie robić więcej (a na pewno będzie)? Zaczynasz rozkładać kod logiki i duplikować to w renderze. A jak interpolacja nie będzie dokładna/kompletna, to jak logika skoryguje obliczenia to będzie wszystko skakać.

Prosty przykład, do tego co podałeś załóżmy, że masz obiekt na 102 pikselu, który będzie kolidował z prostokątem. Czy render ma też obliczać kolizje przy interpolacji? Czy narysujesz go 8px wgłąb, aby potem przeskoczył jak logika skoryguje?

Tak na prawdę to się niczym nie różni od obliczania logiki z deltą tylko nie potrzebnie tą samą funkcjonalność wrzucasz w logikę i render.

@MDM rysowanie kilku takich samych klatek nawet jak się nic nie zmienia ma swój sens, choćby w tym, że gra nie wisi czekając na sygnał vsync, albo sztuczne opóźnienie, żeby wyprodukować daną ilość klatek na sekundę, a tym sensem jest: odczyt danych wejściowych. W niektórych grach/konfiguracjach sprzętowych włączenie vsync powoduje ogromny input lag, gdy myszka płynie po ekranie jak mucha w smole i reaguje z opóźnieniem, tak samo klawisze, a z dziwnych przyczyn z X fps robi się X/2 fps (za X wstaw odświeżanie monitora).

Może lepiej poczytaj to https://gafferongames.com/post/fix_your_timestep/ (to dla OP link)

« Ostatnia zmiana: Sierpień 14, 2017, 11:56:35 wysłana przez Karol »

Offline RootKiller

  • Użytkownik

# Sierpień 14, 2017, 11:10:15
@Karol mylisz interpolacje z ekstrapolacją - tutaj key frame końowy nie jest ekstrapolowaną wartością tylko aktualną (prawdopodobnie po rozwiązaniu kolizji) wyplutą z poprzedniej klatki logiki.

Offline Karol

  • Użytkownik

# Sierpień 14, 2017, 11:54:18
@Karol mylisz interpolacje z ekstrapolacją - tutaj key frame końowy nie jest ekstrapolowaną wartością tylko aktualną (prawdopodobnie po rozwiązaniu kolizji) wyplutą z poprzedniej klatki logiki.
Que? A kto tu w ogóle o ekstrapolacji mówi?

Logika ma za zadanie przesuwać wszystkie obiekty, sprawdzać kolizje i inne zdarzenia, prawda? A render ma narysować aktualny stan obiektów.

Jeżeli teraz render ma także interpolować pozycję obiektów to co się stanie w przypadku, który opisałem? Jeżeli podczas przesuwania obiektu przez render wejdzie on w kolizję z innym obiektem? OP pisał, że dopiero logika przesuwa faktycznie obiekt, więc mamy tutaj dwa wyjścia:

1. z punktu widzenia logiki i kolizji obiekty stoją w miejscu, a gracz widzi przesuwający się obiekt (bo render interpoluje sobie jego pozycję), wchodzący jeden w drugi, co prawda tylko przez kilka klatek, ale to wystarczy żeby coś zaczęło skakać po ekranie, bo jeżeli w końcu zadziała logika to wykryje kolizję i np. nie ustawi prostokątu w docelowym 110px tylko w 102px gdzie nastąpiła kolizja (może to Ty pomyliłeś to z ekstrapolacją? :P)

2. render będzie też obliczał logikę, żeby wyłapać tą kolizję i odpowiednio zareagować, tylko wtedy nie dość, że rozmywa się odpowiedzialność tych metod, to duplikuje się niepotrzebnie kod i wprowadza dodatkową warstwę zamieszania, to już lepiej obliczać logikę z deltą.

Możliwa jest także sytuacja, że ta interpolacja przez render nie będzie w 100% taka sama jak logika. Gdyby np. miała być interpolowana tylko pozycja, to co z obracającymi się obiektami? To by w ogóle komicznie wyglądało, niech ten nieszczęsny prostokąt ma się jeszcze obracać (i to robi logika, ale nie jest interpolowane przez render), załóżmy obrót co 1s, przy 25 obliczeniach logiki na sekundę to daje obrót o 14,4 stopnia co krok logiki. A klatek mamy np. 60 i co widzimy na ekranie: prostokąt udający wskazówkę zegara, który co kilka klatek przeskakuje 14,4 stopnia zamiast obracać się płynnie.

Offline albireo

  • Użytkownik

# Sierpień 17, 2017, 08:28:12
1. z punktu widzenia logiki i kolizji obiekty stoją w miejscu, a gracz widzi przesuwający się obiekt (bo render interpoluje sobie jego pozycję), wchodzący jeden w drugi, co prawda tylko przez kilka klatek, ale to wystarczy żeby coś zaczęło skakać po ekranie, bo jeżeli w końcu zadziała logika to wykryje kolizję i np. nie ustawi prostokątu w docelowym 110px tylko w 102px gdzie nastąpiła kolizja (może to Ty pomyliłeś to z ekstrapolacją? :P)
Ale to co opisujesz, to właśnie jest ekstrapolacja, przy interpolacji nie ma tego problemu, bo docelowa pozycje rzeczonego prostokąta (z uwzględnieniem kolizji) będzie obliczona wcześniej przez logikę (innymi słowy, z punktu widzenia renderera, ostatni stan wyliczony przez logikę, jest w przyszłości).

Offline Karol

  • Użytkownik

# Sierpień 17, 2017, 10:11:24
Czyli render sobie interpoluje między dwoma snapshotami stanu logiki, tak? Nie ma takiej wzmianki w poście OP.

Tak czy siak takie rozwiązanie też ma swoje minusy i też jest tak na prawdę ekstrapolacją (skoro już się upieracie :P), bo co jeżeli user wykona jakąś akcję pomiędzy krokami logiki?

- Ta wyliczona w przyszłości powinna ulec zmianie, ale render o tym nie będzie wiedział (więc jeszcze fajniej - dochodzi input lag) i będzie rysował po staremu. Znowu mamy sytuację, że coś przeskoczy na ekranie po kilku klatkach (bo logika z inputem usera skoryguje try-hard-polację rendera)
- Albo input usera wpłynie dopiero na kolejny krok, więc mamy jeszcze większy input lag (potencjalnie 80ms w rzeczonym przykładzie, to już będzie czuć)
« Ostatnia zmiana: Sierpień 17, 2017, 11:39:13 wysłana przez Karol »

Offline albireo

  • Użytkownik

# Sierpień 17, 2017, 18:26:56
W pierwszym przypadku nadal mamy ekstrapolację, ale robioną w logice, ale lag nie będzie większy niż jeśli ekstrapolowałby renderer, więc przeskoki mogą występować (ale tylko jeśli będzie jakiś user input), ale za to nie będzie trzeba duplikować funkcjonalności logiki w rendererze.
Drugi przypadek to już interpolacja, przeskoków żadnych nie będzie, za to będzie większy lag (coś za coś).

Offline Karol

  • Użytkownik

# Sierpień 17, 2017, 20:29:30
Fajnie, tylko odbiegasz od przykładów, które podał OP.

Dwa, że to nie zmienia faktu, że takie rozwiązanie wprowadza więcej problemów niż ich rozwiązuje (do czego cały czas dążę, a nie czy to jest bardziej inter czy bardziej ekstra).