Wątek przeniesiony 2021-10-04 15:51 z Algorytmy i struktury danych przez Shalom.

Silnik gry - obsługa myszy

1

Na 100% rozwiązanie najbardziej uniwersalne ale jednocześnie najtrudniejsze w implementacji to takie, w którym masz osobne wątki dla:

  • cyklicznego renderowania sceny;
  • odczytu stanów pracując na deltach w odniesieniu do zegara czasu rzeczywistego;
  • zarządzania logiką gry w odniesieniu do zegara czasu rzeczywistego;
Crow napisał(a):

@furious programming:
Czyli w przypadku klawiatury obsługiwać najświeższy (ostatni) stan klawisza, a przy myszy sumować wszystkie eventy ruchu? Też właśnie rozważałem taki pomysł, tak jak pisałem wcześniej, że -3 + 5 + 4 da w sumie przesunięcie o 7 jednostek w prawo, czyli mysz dotrze finalnie tam, gdzie gracz chciał, żeby dotarła, choć na ekranie nie zobaczy pełnej drogi, a jedynie wynik.

Tak i to nie ma znaczenia czy to robisz w modelu z renderowaniem obok czy naprzemiennym z odczytywaniem stanu. Te różnie logiki wpływają jedynie na zależności czasowe samej akcji i konieczność dobrania odpowiednich metod aktualizacji sceny.

Tylko czy aby na pewno jest to pożądany efekt?

Tak.

Bo ruchy myszą zwykle wykonujemy w oparciu o "feedbak" wzrokowy, czyli ruszamy ręką, obserwujemy wynik na ekranie i korygujemy. Gdy natomiast tego "feedbaku" nie ma, to i ruch staje się trochę chaotyczny, opóźniony. Może więc lepiej wykonywać tylko ostatni ruch, żeby gracz mimo wszystko odczuwał jak najmniejsze opóźnienie (tzn. by ruch na ekranie był jak najbardziej zsynchronizowany z ruchem ręki). Rozważam różne opcje, bo nie wiem jak to się robi w prawdziwym gamedevie.

Tak się właśnie robi. Niezależnie od tego na ilu wątkach to wszystko będzie działać. Jedynie musisz mieć świadomość tego co robisz, w jakim modelu pracujesz i co wówczas dzieje się z czasem.

I pamiętaj o tym, że gry mają jeszcze dźwięk...

2
Crow napisał(a):

@furious programming:
Czyli w przypadku klawiatury obsługiwać najświeższy (ostatni) stan klawisza, a przy myszy sumować wszystkie eventy ruchu? Też właśnie rozważałem taki pomysł, tak jak pisałem wcześniej, że -3 + 5 + 4 da w sumie przesunięcie o 6 jednostek w prawo, czyli mysz dotrze finalnie tam, gdzie gracz chciał, żeby dotarła, choć na ekranie nie zobaczy pełnej drogi, a jedynie wynik.

Tzn. u Ciebie trzeba kombinować, dlatego że korzystasz z komunikatów, które jakoś trzeba przechowywać. U mnie olewam komunikaty, bo stan klawiszy i przycisków kontrolera aktualizuję hurtem. W SDL raz pobieram sobie wskaźnik bufora wszystkich klawiszy (funkcja SDL_GetKeyboardState z argumentem nil) i z tego bufora czytam stany klawiszy. Stan przycisków kontrolera podobnie, tyle że nie mam wskaźnika na bufor, więc używam funkcji SDL_JoystickGetButton, SDL_JoystickGetAxis i SDL_JoystickGetHat. Nie interesują mnie stany poprzednie, które nie zostały w poprzedniej klatce obsłużone — przy 60fps nie ma potrzeby używania kolejki komunikatów i historii inputu.

Taki sposób jest prosty w implementacji i w niczym nie przeszkadza. W przypadku bardzo zaawansowanych i zasobożernych produkcji, raczej by się nie sprawdził. Choć też trudno powiedzieć, nie mając fizycznego programu do sprawdzenia. A jak klatkaż spada do 15fps, to na nic responsywność, skoro i tak na gracz jej nie odczuje przez brak natychmiastowych efektów na ekranie.

Tylko czy aby na pewno jest to pożądany efekt?

A bo ja wiem — musisz to zaimplementować, żeby wiedzieć czy to ma prawo działać czy nie. W teorii ma.

Bo ruchy myszą zwykle wykonujemy w oparciu o "feedbak" wzrokowy, czyli ruszamy ręką, obserwujemy wynik na ekranie i korygujemy. Gdy natomiast tego "feedbaku" nie ma, to i ruch staje się trochę chaotyczny, opóźniony.

Zwróć tylko uwagę na to, że nieważne ile wątków użyjesz i jak będziesz aktualizował logikę, bo jeśli procesor nie będzie wyrabiał to ani nie zaktualizujesz logiki na czas, ani nie wyrenderujesz odpowiedniej liczby klatek. Tak więc nieistotne co zrobisz, czy zsumujesz ruchy czy nie, gracz i tak nie będzie w stanie zaobserwować tej różnicy.

Pamiętaj tylko, że to nie jest normalne działanie silnika, a sytuacja, w której pecet nie wyrabia. W normalnym przypadku takie problemy nie będą wystepować. Wątpię, aby Twój silnik nie był w stanie działać płynnie na współczesnych komputerach.

Może więc lepiej wykonywać tylko ostatni ruch, żeby gracz mimo wszystko odczuwał jak najmniejsze opóźnienie (tzn. by ruch na ekranie był jak najbardziej zsynchronizowany z ruchem ręki). Nie wiem, czy to dobry pomysł, rozważam różne opcje, bo nie wiem jak to się robi w prawdziwym gamedevie.

Zakładając, że gracz ruszył kursorem w lewo i prawo w ramach jednej klatki, a silnik ma oba te ruchy uwzględnić podczas renderowania kolejnej klatki, to masz kilka opcji:

  • wykonać jedną aktualizację i wziąć pod uwagę:
    • pierwszy ruch, resztę olać,
    • ostatni ruch, a poprzednie olać,
    • uśrednić ruch na podstawie wszystkich komunikatów,
  • wykonać kilka aktualizacji na podstawie wszystkich komunikatów.

W przypadku, gdy nie było żadnego laga, a po prostu gracz ma padaczkę, możesz zrobić cokolwiek — i tak nie zauważy różnicy, bo klatkaż jest wysoki, gra działa płynnie, a ludzkie oko nie jest w stanie wyłapać tak krótkotrwałych ruchów.

Jednak jeśli pojawił się duży lag i delta jest zbyt duża, to możesz wykonać kilka aktualizacji logiki, tak aby zapobiec błędom fizyki (ww. przechodzenie przez ściany), nadal robiąc cokolwiek — gracz znów nie zauważy różnicy, skoro obraz na ekranie był zamrożony i żabkował (nie będzie w stanie wywnioskować, że silnik go oszukał).

To które rozwiązanie wybrać, zależy jedynie od specyfiki projektu. Znów — aby mieć pewność, sprawdź jak te sposoby działają w Twoim projekcie. Napisz sobie kod w taki sposób, aby co chwilę symulował laga, a Ty staraj się grać i w ten sposób wywnioskuj który sposób sprawdza się najlepiej.


katakrowa napisał(a):

I pamiętaj o tym, że gry mają jeszcze dźwięk...

Dźwięk jest buforowany — odtwarzany niezależnie od tego co się dzieje w pętli gry.

0

Tak sobie myślałem o tym rozbiciu renderowania i logiki na osobne wątki (skoro i tak używam wielowątkowości w swoim silniku to w sumie czemu nie spróbować, choćby w celach edukacyjnych?), ale pewnej rzeczy nie mogę rozgryźć, a nigdzie o tym nie piszą (dzięki @katakrowa za linki). Załóżmy, że logika i renderowanie działają niezależnie w osobnych wątkach. Oba wątki muszą pracować z określoną częstotliwością (może być taka sama, może być różna dla każdego z nich, np. 60/120 jak robi się w bijatykach czy grach wyścigowych). W przypadku renderowania mam V-SYNC, który zadba o odpowiednie interwały i "przesypianie" wymaganych odcinków czasu. Ale co zrobić z wątkiem odświeżającym logikę? To znaczy wiem, że można użyć metody, w której wątek śmiga ile fabryka dała i po prostu w każdym ticku odpytuje ile jednostek czasu (np. nanosekund) upłynęło od ostatniego zapytania, a jak doliczy się do pożądanego limitu (po zsumowaniu wyników), przechodzi do kolejnej iteracji (kolejnego odświeżenia logiki) i tak w kółko. Sam stosowałem takie rozwiązanie jeszcze zanim przesiadłem się na API z V-SYNC'kiem, działa to całkiem dobrze (bardzo precyzyjnie z marginesem błędu może 1 klatki na sekundę), ale ma jedną, za to ogromną wadę - zżera duże ilości mocy CPU (praktycznie 100% rdzenia). Wiem, że można kombinować z usypianiem wątku (@furious programming tak robi), ale to jest bardziej takie obejście problemu, "hackowanie" przy użyciu sztuczek, a mnie zastanawia, jak to się robi w rzeczywistym gamedevie. Na Windowsie musi przecież istnieć jakiś sposób bardzo precyzyjnego wstrzymywania wątków, bez zgadywania, doliczania korekt (np. -1 sekunda), szacowania itd. Nic nie mogę wygooglować na ten temat.

1
Crow napisał(a):

Tak sobie myślałem o tym rozbiciu renderowania i logiki na osobne wątki (skoro i tak używam wielowątkowości w swoim silniku to w sumie czemu nie spróbować, choćby w celach edukacyjnych?), ale pewnej rzeczy nie mogę rozgryźć, a nigdzie o tym nie piszą (dzięki @katakrowa za linki). Załóżmy, że logika i renderowanie działają niezależnie w osobnych wątkach. Oba wątki muszą pracować z określoną
częstotliwością (może być taka sama, może być różna dla każdego z nich, np. 60/120 jak robi się w bijatykach czy grach wyścigowych).

Renderowanie pracuje tak jak może. Jedynie odczytywanie stanów interfejsu sterowania i sama obsługa logiki dla ułatwienia algorytmów i obliczeń może być wywoływana z określoną i znaną częstotliwością.

W przypadku renderowania mam V-SYNC, który zadba o odpowiednie interwały i "przesypianie" wymaganych odcinków czasu. Ale co zrobić z wątkiem odświeżającym logikę? To znaczy wiem, że można użyć metody, w której wątek śmiga ile fabryka dała i po prostu w każdym ticku odpytuje ile jednostek czasu (np. nanosekund) upłynęło od ostatniego zapytania, a jak doliczy się do pożądanego limitu (po zsumowaniu wyników), przechodzi do kolejnej iteracji (kolejnego odświeżenia logiki) i tak w kółko. Sam stosowałem takie rozwiązanie jeszcze zanim przesiadłem się na API z V-SYNC'kiem, działa to całkiem dobrze (bardzo precyzyjnie z marginesem błędu może 1 klatki na sekundę), ale ma jedną, za to ogromną wadę - zżera duże ilości mocy CPU (praktycznie 100% rdzenia). Wiem, że można kombinować z usypianiem wątku (@furious programming tak robi), ale to jest bardziej takie obejście problemu, "hackowanie" przy użyciu sztuczek, a mnie zastanawia, jak to się robi w rzeczywistym gamedevie. Na Windowsie musi przecież istnieć jakiś sposób bardzo precyzyjnego wstrzymywania wątków, bez zgadywania, doliczania korekt (np. -1 sekunda), szacowania itd. Nic nie mogę wygooglować na ten temat.

Przecież możesz zrobić rozsądne ograniczenie ilości renderowanych klatek/sec.
Algorytm wątku renderującego powinien wyglądać miej więcej tak

:start
  :czekaj_az_scena_gotowa
     if ( scena_gotowa == false ) goto :czekaj_az_scena_gotowa;

  :renderuj
    kopia_sceny = scena_globalna;
    obraz = renderujemy( kopia_sceny );
    kopiuj_obraz_do_karty_graficznej ( obraz ) // kopiowanie z bufora ;

  :powrot_plamki ( V-SYNC )
    if ( plamka_doleciala == false ) goto :powrot_plamki;

  REM tu można rzeczywiście dodać jakieś opóźnienie jeśli wyjcie że robi np. 1000 klatek / sek. tylko po co ?

  goto :start

Wątek odpowiadając za logikę gry:

:start
   odczytaj_stany_interfejsow();  
   scena_globalna = poprzestawiaj_obiekty_na_scenie(scena_gloalna);

   czekaj_do_okreslonego_interwalu_czasowego();
   goto :start

Nie wiem w czym to piszesz ale np. w JavaScript taki "wątek" logiki mógłby być zrealizowany na zwykłym setInterval()... W czasie gdy renderowanie leci na SetTimeout( func(), 1 );

0
Crow napisał(a):

W przypadku renderowania mam V-SYNC, który zadba o odpowiednie interwały i "przesypianie" wymaganych odcinków czasu.

Dla mnie to akurat był i nadal jest mindfuck. Może i to ja tego nie rozumiem, ale V-Sync jest synchronizacją wyświetlania obrazu na ekranie, a nie jego renderowaniem — a tym bardziej nie synchronizacją akrualizacji logiki. Skoro tak, to w domyśle mogę sobie renderować 200 klatek na sekundę, a GPU powinno słać klatki jedynie 60 razy na sekundę, zgodnie z odświeżaniem monitora. Reszta powinna być pominięta, coby uniknąć rozrywania ekranu.

Tymczasem włączam sobie V-Sync w SDL i logika mi pracuje ze sztywną prędkością odświeżania ekranu. Co do cholery? Dlaczego wyświetlanie wyrenderowanych klatek na ekranie wpływa na proces ich renderowania i logikę? Dla mnie ten mechanizm w SDL wygląda na skopany.

Wiem, że można kombinować z usypianiem wątku (@furious programming tak robi), ale to jest bardziej takie obejście problemu, "hackowanie" przy użyciu sztuczek, a mnie zastanawia, jak to się robi w rzeczywistym gamedevie.

Nie żartuj — to nie są żadne sztuczki czy hakowanie, tak to ma działać. Jeśli silnik ma pracować z częstotliwością np. 60fps, to powinien aktualizować logikę i renderować klatki w równych odstępach czasu, a pozostałą moc obliczeniową oddawać dla innych wątków i procesów. Usypianie wątku pomiędzy obsugą klatek to jedyne prawidłowe rozwiązanie.

Pod Windows trzeba to robić w dwóch krokach, dlatego że na tej platformie nie ma mechanizmu usypiania działającego z precyzją większą niż milisekunda. W dodatku istniejący mechanizm może wstrzymywać pracę wątku z marginesem błędu wynoszącym jedną milisekundę. Dlatego najpierw wykonuje się zamrożenie bazujące na Sleep z jedną milisekundą mniej niż potrzeba, a resztę zjada się busy waitingiem (rozkaz pause w pętli). To daje bardzo dużą precyzję działania, jednocześnie oddając wolną moc obliczeniową systemowi. W Fairtrisie robię to tak:

procedure TClock.UpdateFrameAlign();
var
  SleepTime: Single;
begin
  // oblicz ile milisekund należy poczekać
  // od wyniku odejmij "1", aby uodpornić kod na niedokładność shedulera
  SleepTime := 1000 / FFrameRateLimit * (1 - (FFrameTicksEnd - FFrameTicksBegin) / FTicksPerFrame) - 1;

  // odejmij milisekundę, jeśli po zaokrągleniu zapas jest mniejszy niż milisekunda
  SleepTime -= Ord(Round(SleepTime) > SleepTime);

  // jeśli wynik jest mniejszy niż "0", ustaw czas na "0"
  SleepTime := Max(SleepTime, 0);

  // wykonaj zamrożenie wątku
  SDL_Delay(Round(SleepTime));

  // busy waiting
  //
  // w kółko odczytuj stan sprzętowego zegara za pomocą "QueryPerformanceCounter" i czekaj dotąd, aż
  // liczba cykli będzie większa lub równa tej dla rozpoczęcia aktualizacji kolejnej klatki
  while GetCounterValue() < FFrameTicksNext do
  asm
    pause
  end;
end;

W systemach uniksowych jest znacznie łatwiej, bo jest funkcja nanosleep (we Free Pascalu FPNanoSleep), działająca z dokładnością do mikrosekundy. Busy waiting nie jest konieczny, bo wystarczy wyliczyć czas zamrożenia od razu z wysoką precyzją. Tak więc Uniksy mają tutaj przewagę. Wyżej masz kod dla Windows, a jeśli szukasz dla systemów uniksowych, to znajdziesz odpowiednik w źródłach Deep Platformera.

Na Windowsie musi przecież istnieć jakiś sposób bardzo precyzyjnego wstrzymywania wątków, bez zgadywania, doliczania korekt (np. -1 sekunda), szacowania itd. Nic nie mogę wygooglować na ten temat.

IMO nic takiego nie ma. Jest Sleep, który pozwala wstrzymać pracę wątku na określoną liczbę milisekund (rozbieżność może wynosić milisekundę lub więcej, jeśli CPU nie wyrabia). Przy czym aby mieć dostęp do precyzji niższej niż 10ms, należy skorzystać z funkcji timeGetDevCaps, pobrać sobie minimalną rozdzielczość i ją ustawić za pomocą funkcji timeBeginPeriod (na koniec pracy przywrócić oryginalną za pomocą funkcji timeEndPeriod).

Jedynym mechanizmem zapewniającym bardzo wysoką precyzję jest właśnie QueryPerformanceCounter — dokładność do iluś tam nanosekud. Tyle że nie da się go wykorzystać bezpośrednio, bo Sleep działa z dokładnością do milisekund. Dlatego trzeba pobrać rozdzielczość zegara sprzętowego za pomocą funkcji QueryPerformanceFrequency i obliczyć ile cykli przypada na jedną klatkę. Podczas aktualizacji nowej klatki, zapamiętuje się stan zegara sprzętowego przed aktualizacją, wykonuje się update logiki i renderuje się klatkę. Po wszystkim pobiera się stan zegara znowu, oblicza ile cykli trwało generowanie nowej klatki, odejmuje wynik od liczby cykli przypadających na klatkę i dostaje się w wyniku liczbę cykli do odczekania. Tutaj przelicza się tę liczbę cykli na liczbę milisekund, odejmuje margines kompensujący niedokładność Sleep, zamraża wątek Sleepem, SDL_Delayem czy inną funkcją, a resztę cykli zjada busy waitingiem. IMO inaczej pod Windows się nie da.

Wszystko powyższe znajdziesz w źródłach Fairtrisa (dla Windows) oraz Deep Platformera (dla Uniksów). Jednak pamiętaj, że to co podałem wyżej jest bardzo precyzyjne i oddaje nadmiar mocy innym procesom, ale kosztem braku wsparcia V-Synca.

0

Dodam jeszcze, że na świecie jest jeszcze sporo leśnych dziadów myślących, że po to jest CPU, aby zużywać jego całą moc obliczeniową. To jest bullshit, DOS-owska mentalność — pecety od wielu lat robią setki rzeczy jednocześnie, oprócz obsługi procesu gry. Gracz sobie gra, słucha muzyki, nagrywa ekran OBS-em, do tego obsługuje Discorda i inne bajery. Im więcej mocy zeżre proces gry, tym mniej jej zostanie dla innych procesów. Poza tym dziś normą jest granie na laptopach i smartfonach, więc im więcej mocy zeżre gra, tym krócej wytrzyma bateria.

Jeśli Twoja gra nie będzie wymagała ogromnych zapasów mocy obliczeniowej, to powinna działać w stałym klatkażu, oddając nadmiar mocy innym procesom. Stały klatkaż może być narzucony przez V-Sync lub ustalony przez gracza w ustawieniach. Pamiętaj tylko, aby aktualizację logiki oprzeć o deltę, nawet jeśli klatkaż ma być stały.

0
furious programming napisał(a):

Dla mnie to akurat był i nadal jest mindfuck. Może i to ja tego nie rozumiem, ale V-Sync jest synchronizacją wyświetlania obrazu na ekranie, a nie jego renderowaniem — a tym bardziej nie synchronizacją akrualizacji logiki. Skoro tak, to w domyśle mogę sobie renderować 200 klatek na sekundę, a GPU powinno słać klatki jedynie 60 razy na sekundę, zgodnie z odświeżaniem monitora. Reszta powinna być pominięta, coby uniknąć rozrywania ekranu.

Tymczasem włączam sobie V-Sync w SDL i logika mi pracuje ze sztywną prędkością odświeżania ekranu. Co do cholery? Dlaczego wyświetlanie wyrenderowanych klatek na ekranie wpływa na proces ich renderowania i logikę? Dla mnie ten mechanizm w SDL wygląda na skopany.

Bazując na informacjach, które czytałem odnośnie Direct'a 2D (tego API używam), V-SYNC działa tam po prostu tak, że po zakończeniu renderowania danej klatki, blokuje wątek CPU obsługujący render. Następnie, we właściwym monecie, zostaje on wybudzony przez odpowiedni Notify Event wysłany przez GPU (co jest oczywiście powiązane z częstotliwością odświeżania monitora). Nie wiem czy w SDL jest podobnie, ale pewnie tak. Jak rozbijesz render i logikę na dwa wątki, to tego unikniesz, ale też będziesz musiał samodzielnie uporać się z zrządzaniem zużyciem CPU...

Nie żartuj — to nie są żadne sztuczki czy hakowanie, tak to ma działać. Jeśli silnik ma pracować z częstotliwością np. 60fps, to powinien aktualizować logikę i renderować klatki w równych odstępach czasu, a pozostałą moc obliczeniową oddawać dla innych wątków i procesów. Usypianie wątku pomiędzy obsugą klatek to jedyne prawidłowe rozwiązanie.

Pod Windows trzeba to robić w dwóch krokach, dlatego że na tej platformie nie ma mechanizmu usypiania działającego z precyzją większą niż milisekunda. W dodatku istniejący mechanizm może wstrzymywać pracę wątku z marginesem błędu wynoszącym jedną milisekundę. Dlatego najpierw wykonuje się zamrożenie bazujące na Sleep z jedną milisekundą mniej niż potrzeba, a resztę zjada się busy waitingiem (rozkaz pause w pętli). To daje bardzo dużą precyzję działania, jednocześnie oddając wolną moc obliczeniową systemowi.
W systemach uniksowych jest znacznie łatwiej, bo jest funkcja nanosleep (we Free Pascalu FPNanoSleep), działająca z dokładnością do mikrosekundy. Busy waiting nie jest konieczny, bo wystarczy wyliczyć czas zamrożenia od razu z wysoką precyzją. Tak więc Uniksy mają tutaj przewagę. Wyżej masz kod dla Windows, a jeśli szukasz dla systemów uniksowych, to znajdziesz odpowiednik w źródłach Deep Platformera.

Na Windowsie musi przecież istnieć jakiś sposób bardzo precyzyjnego wstrzymywania wątków, bez zgadywania, doliczania korekt (np. -1 sekunda), szacowania itd. Nic nie mogę wygooglować na ten temat.

IMO nic takiego nie ma. Jest Sleep, który pozwala wstrzymać pracę wątku na określoną liczbę milisekund (rozbieżność może wynosić milisekundę lub więcej, jeśli CPU nie wyrabia). Przy czym aby mieć dostęp do precyzji niższej niż 10ms, należy skorzystać z funkcji timeGetDevCaps, pobrać sobie minimalną rozdzielczość i ją ustawić za pomocą funkcji timeBeginPeriod (na koniec pracy przywrócić oryginalną za pomocą funkcji timeEndPeriod).

Jedynym mechanizmem zapewniającym bardzo wysoką precyzję jest właśnie QueryPerformanceCounter — dokładność do iluś tam nanosekud. Tyle że nie da się go wykorzystać bezpośrednio, bo Sleep działa z dokładnością do milisekund. Dlatego trzeba pobrać rozdzielczość zegara sprzętowego za pomocą funkcji QueryPerformanceFrequency i obliczyć ile cykli przypada na jedną klatkę. Podczas aktualizacji nowej klatki, zapamiętuje się stan zegara sprzętowego przed aktualizacją, wykonuje się update logiki i renderuje się klatkę. Po wszystkim pobiera się stan zegara znowu, oblicza ile cykli trwało generowanie nowej klatki, odejmuje wynik od liczby cykli przypadających na klatkę i dostaje się w wyniku liczbę cykli do odczekania. Tutaj przelicza się tę liczbę cykli na liczbę milisekund, odejmuje margines kompensujący niedokładność Sleep, zamraża wątek Sleepem, SDL_Delayem czy inną funkcją, a resztę cykli zjada busy waitingiem. IMO inaczej pod Windows się nie da.

Wszystko powyższe znajdziesz w źródłach Fairtrisa (dla Windows) oraz Deep Platformera (dla Uniksów). Jednak pamiętaj, że to co podałem wyżej jest bardzo precyzyjne i oddaje nadmiar mocy innym procesom, ale kosztem braku wsparcia V-Synca.

Nie wykluczone, że w twojej implementacji to działa, ale jednak większość źródeł odradza takie podejście, nazywając je naiwnym i na dłuższą metę problematycznym. Dlatego właśnie szukam "profesjonalnego" podejścia, tzn. takiego, którego używa się w "prawdziwym" gamedevie. Niestety, zaskakująco trudno dokopać się do jakichś konkreteów na ten temat.

furious programming napisał(a):

Dodam jeszcze, że na świecie jest jeszcze sporo leśnych dziadów myślących, że po to jest CPU, aby zużywać jego całą moc obliczeniową. To jest bullshit, DOS-owska mentalność — pecety od wielu lat robią setki rzeczy jednocześnie, oprócz obsługi procesu gry. Gracz sobie gra, słucha muzyki, nagrywa ekran OBS-em, do tego obsługuje Discorda i inne bajery. Im więcej mocy zeżre proces gry, tym mniej jej zostanie dla innych procesów. Poza tym dziś normą jest granie na laptopach i smartfonach, więc im więcej mocy zeżre gra, tym krócej wytrzyma bateria.

Jeśli Twoja gra nie będzie wymagała ogromnych zapasów mocy obliczeniowej, to powinna działać w stałym klatkażu, oddając nadmiar mocy innym procesom. Stały klatkaż może być narzucony przez V-Sync lub ustalony przez gracza w ustawieniach. Pamiętaj tylko, aby aktualizację logiki oprzeć o deltę, nawet jeśli klatkaż ma być stały.

No właśnie z tą deltą też, jak czytam, jest sporo problemów. Nawet w prostej grze, z prostą fizyką (np. w stylu Mario) wystarczy nagły frame drop, żeby odległość skoków się zmieniała, albo postać przenikała przez ściany. Źródła zalecają, żeby w "takich" zastosowaniach (czyli w zasadzie jakich? Sam nie wiem jak odróżnić kiedy delta powinna a kiedy nie powinna być stosowana...) stawiać na podejście deterministyczne, ze stałą częstotliwością odświeżania logiki i ewentualnym pomijaniem odświeżeń, gdy nadmiarowy czas zrówna się z czasem klatki. Tutaj chyba najlepszy artykuł na ten temat:

https://gafferongames.com/post/fix_your_timestep/

Powyższe nie rozwiązuje jednak problemu zarządzania mocą obliczeniową CPU, bo tam też wątek śmiga ile fabryka dała...

0

@Crow:
Da się? Da się! Masz loop w pętli na osobnym wątku i nie obciąża CPU wcale:

Bez sleep() w funkcji TMyWorkerThread.Execute; jest cały jeden rdzeń ociążony ze sleep prawie wcale..
Zatem odsyłam szanownego kolegę do szukania i czytania bowiem ewidentnie się DA!
Delphi kompiluje do kodu natywnego tak jak C++ zatem musi być analogiczna możliwość.

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
  TForm1 = class(TForm)
    btnStop: TButton;
    btnStart: TButton;
    procedure btnStopClick(Sender: TObject);
    procedure btnStartClick(Sender: TObject);
  private
  public
  end;

  TMyWorkerThread = class(TThread)
  public
    procedure Execute; override;
  end;
var
  Form1: TForm1;
  x:integer;

implementation
{$R *.dfm}

procedure TMyWorkerThread.Execute;
var
  i : integer ;
begin
  x := 0 ;
  i := 0 ;
  while x = 0 do
  begin
    if ( i mod 256 = 0 ) then
    begin
      write ( 'x' );
    end;
    sleep(1);
    i := i + 1 ;
  end;
end;

procedure TForm1.btnStartClick(Sender: TObject);
begin
  TMyWorkerThread.Create(false);
end;

procedure TForm1.btnStopClick(Sender: TObject);
  begin
    x := 1;
  end;
end.

ZE SLEEP():

screenshot-20211005184646.png

BEZ SLEEP():

screenshot-20211005191101.png

0
katakrowa napisał(a):

jeszcze kilka godzin temu przekonywałeś mnie, że robienie gry z ustawionym na sztywno 60 klatek na sekundę to super pomysł.

Gdzie? Kiedy? Przecież od początku pisałem, że używam V-SYNC'a i delty.

a teraz nagle Ci precyzja Sleepa przeszkadza.

Jeżeli twoim zdaniem oparcie odświeżania logiki gry na Sleep() jest dobrym pomysłem, to zupełnie nie ogarniasz...

Nie martw się do 200 klatek na sekundę w zupełności Ci wystarczy.

Ale ja nie chcę 200 klatek, tylko możliwości własnoręcznego ustawienia limitu odświeżania, z opcją precyzyjnego odmierzania interwałów między iteracjami i bez niepotrzebnego busy waitingu, zjadającego moc obliczeniową CPU. Czego nie rozumiesz?

1 użytkowników online, w tym zalogowanych: 0, gości: 0, botów: 1

Robot: Applebot