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

Silnik gry - obsługa myszy

0

Obsługa inputów w moim silniku wygląda tak, że są 2 wątki, jeden nieustannie zbiera wszystkie inputy i dodaje je do kolejki (korzystam z windowsowych RAW INPUTS, obsługując komunikat WM_INPUT), drugi zajmuje się generowaniem kolejnych klatek (logika gry, rysowanie itd.). Na początku każdej klatki następuje synchronizacja inputów, tj. zawartość kolejki zostaje przepisana do tablicy przechowującej informacje na temat stanu wszystkich klawiszy i przycisków, następnie kolejka jest zerowana. Działa to więc tak, że jeżeli w kolejce znajduje się np.:

1, "SPACJA" - wciśnięty
2. "SPACJA" - wyciśnięty
3. "SPACJA" - wciśnięty

to do tablicy trafia jedynie informacja, że w danej klatce SPACJA jest wciśnięta (czyli poszczególne przejścia są pomijane, liczy się tylko ostatni stan klawisza, jaki trafił do kolejki w chwili synchronizacji, czyli na początku klatki). Po zakończeniu klatki tablica inputów również jest zerowana, by być gotową na synchronizację kolejnych inputów w następnej klatce.

Teraz staram się dodać obsługę ruchów myszy i wydaje mi się, że takie podejście jak wyżej nie będzie dobre, bo może powodować opóźnienia albo brak odpowiedniej precyzji (co byłoby nieakceptowalne np. w FPSie). Doszedłem więc do wniosku, że muszę tu wprowadzić 2 zmiany:

  1. Synchronizacja nie powinna odbywać się automatycznie na początku każdej klatki, tylko ręcznie, po wywołaniu odpowiedniej funkcji np. SynchronizeMouseMovement(). Dzięki temu można by w jednej klatce obsłużyć więcej ruchów, zamiast przenosić je do następnej klatki (tak się dzieje w przypadku klawiszy, którym nie udało się im załapać na automatyczną synchronizację na początku danej klatki).
  2. Obsługiwane powinny być wszystkie ruchy myszy, które odbyły się między kolejnymi synchronizacjami, a nie tylko ten ostatni (najświeższy), jak dzieje się w przypadku klawiszy. Co za tym idzie, ruchy myszy powinny mieć własną kolejkę.

Dobrze kombinuje? A jeżeli tak, to co miałbym zrobić w sytuacji, gdyby w danej klatce synchronizacja nie została wywołana? Na przykładzie pseudo kodu:

if (OknoZamkniete = true)
{
   SynchronizeMouseMovement();
   //Jakaś tam obsługa ruchów myszy.
}

Gdy okno jest zamknięte, ruchy są synchronizowane i obsługiwane. Jeżeli natomiast jest otwarte, ruchy myszy nie są synchronizowane, a więc gromadzą się w kolejce (być może przez bardzo długi czas) i jasnym jest, że po zamknięciu okna nie mogę one wszystkie zostać obsłużone i część powinna zostać odrzucona. Jak zaprojektować taki mechanizm, żeby jednocześnie nie pominąć żadnych ruchów, które powinno zostać obsłużone?

Jeżeli ktoś ma jakieś doświadczenia w zakresie obsługi myszy na potrzeby gier (albo czegoś podobnego, co wymaga precyzji), proszę uprzejmie o jakieś wskazówki.

0

Z uwagi na brak odpowiedzi, uproszczę problem. Załóżmy, że pomiędzy dwoma następującymi po sobie klatkami, zarejestrowano 3 ruchy myszą:

1. Mouse.X = 3 //(3 jednostki w prawo).
2. Mouse.X = -5 //(5 jednostek w lewo).
3. Mouse.X = 4 //(4 jednostki w prawo).

W praktyce to mało prawdopodobne, bo nawet przy 30 klatkach na sekundę, gracz miałby na to wszystko jedynie 33 milisekundy, ale teoretycznie jest to wykonalne. Jak powinna wyglądać obsługa powyższych zdarzeń?

  1. Obsłużyć pierwszy ruch, a pozostałe przenieść do kolejnych klatek (czyli obsługiwać jeden ruch na klatkę?). Pozwoli to zachować pełną płynność ruchu, bo wszystkie akcje będą reprezentowane na ekranie, ale może(?) powodować opóźnienia.
  2. Obsłużyć wszystkie ruchy w jednej klatce, czyli de facto zsumować je i traktować jako jeden ruch? 3 + (-5) + 4 = 2, czyli tak jakby gracz przesunął mysz o 2 jednostki w prawo. Nie da to co prawda wrażenia ruchu (bo trudno przedstawić 3 pozycje myszy w jednej klatce), ale mysz (przynajmniej teoretycznie) powinna się znaleźć tam, gdzie gracz chciał, żeby finalnie była.
  3. Olać wszystkie ruchy za wyjątkiem ostatniego, czyli w tym przypadku wykonać przesunięcie o 4 jednostki w prawo?

Jak to się robi w gamedevie, żeby zachować wrażenie płynności i responsywności? Ktoś? Coś? Za wszelkie sugestie i pomysły będę bardzo wdzięczny.

0

@Crow - Jak to się robi w gamedevie
No zupełnie nie tak jak piszesz. Zarówno renderowanie poszczególnych klatek jak i odczyt stanu urządzeń t.j. klawiatura, mysz są realizowane w pewnych "cyklach" (osobnych).
W przypadku myszy będzie inaczej niż z klawiaturą. Żadnego klawisza nie można pominąć i muszą być buforowane.. ale też zależy które ponieważ inaczej będzie z klawiszami odpowiedzialnymi za chodzenie, a inaczej ze strzałem lub przyciskiem jakiejś funkcji.

Zatem mamy sygnały z myszy i klawiatury odpowiadające za:

  1. wywoływanie jakiś akcji ( strzał, zmiana broni, wywołanie, skok menu itp. )
  2. ruch w lewo prawo, do przodu, tyłu itp...

Te z punktu 1 nie mogą być pomijane i muszą być buforowane ( bądź obsługiwane natychmiast ),
Te z punktu 2 są odczytywane co "cykl odczytu" i następnie na podstawie ich stanu:

  • przyciski na podstawie stanu naciśnięcia ;
  • ruchy myszą na podstawie różnicy pozycji kursora względem ostatniego odświeżania / ostatniej klatki (przy czym to duże uproszczenie),
    i na tej podstawie powyższego odczytu stanów i ich różnic wylicza się odpowiednie przesunięcie, zmianę pozycji itp...

Żeby było trudniej to renderowanie obrazu zwykle odbywa się tak obok (jest niezależne) "ruchu" po scenie gry tak aby móc maksymalnie wykorzystać możliwości komputera ale jednocześnie by możliwe było granie na maszynie o słabej wydajności.
Sterowanie ruchem i pozycją postaci na scenie natomiast powinno być na tyle szybkie aby działać nawet na najwolniejszym sprzęcie oraz wywoływane ze stałym interwałem czasowym aby zawsze i wszędzie działało tak samo i zachowane były zależności z czasem realnym.

Innymi słowy osobo mamy:

  • model sceny z obiektami ich pozycjami i stanem oraz obsługą ruchu,
  • renderowanie, które jest jakby kolejnymi zdjęciami tej sceny wykonywanymi na tyle często na ile pozwala sprzęt nie wpływając na pozycję obiektów ani bohaterów na scenie.
0

Ale jak to powinno działać strukturalnie? Wracając do przykładu z pierwszego posta. Dajmy na to, że pomiędzy dwiema następującymi po sobie klatkami gracz wciska spację, odpuszcza ją, a potem znowu wciska. W praktyce coś takiego pewno nie stanie zbyt często (bo przy 60 czy nawet 30 klatkach na sekundę to prawie niewykonalne), ale załóżmy, że framerate jest bardzo niski, np. 10 FPS. Jak coś takiego powinno zostać obsłużone z poziomu silnika? Powinienem brać pod uwagę tylko ostatni stan (spacja wciśnięta) i renderować klatkę na tej podstawie, a może rozbić wszystkie 3 stany na 3 kolejne klatki? W jaki mechanizm to ubrać?

0

Dajmy na to, że pomiędzy dwiema następującymi po sobie klatkami gracz wciska spację,

I tu jest Twój pierwszy błąd w myśleniu. Nie ma czegoś takiego jak pomiędzy dwiema następującymi po sobie klatkami.
Klatki renderują się osobno, a ruch osobno jak zrobisz tak jak piszesz to gra będzie na wolnych komputerach chodziła wolniej a na szybszych szybciej.
Stan sceny nie może być zależny od czasu renderowania/rysowania klatki. Chyba, że gra jest bardzo prymitywna i zakładasz, że na nawet najwolniejszym sprzęcie masz czas na wyrenderowanie i obsługę sceny ustalając, że na sztywno będziesz miał 30 klatek / sekundę, a moc obliczeniowa komputera zawsze zapewni że zdążysz wszystko policzyć.

ale załóżmy, że framerate jest bardzo niski, np. 10 FPS - właśnie tak jak Ci napisałem.
Ruch na scenie odbywa się cały czas niezależnie od tego ile klatek na sekundę ogarniesz graficznie.
Zauważ co się dzieje w grach jak "przycinają"... Klatki pojawiają się co sekundę ale gra w tle trwa dalej. Jedynie "zdjęcia" ze sceny rzadziej się pokazują i tracona jest płynność animacji ale nie akcji gry. Zgadza się?

0

Klatki renderują się osobno, a ruch osobno

W jaki sposób? Chyba nie do końca rozumiem.

jak zrobisz tak jak piszesz to gra będzie na wolnych komputerach chodziła wolniej a na szybszych szybciej.

Ale dlaczego? Przecież 60 klatek na sekundę to 60 klatek na sekundę, niezależnie od mocy komputera czas płynie przecież tak samo. Ważne, żeby czas klatki był mniej więcej stały (a właściwie, żeby suma ich czasów była jak najbliższa 1 sekundy, nie przekraczając limitu czasu klatki, który niezbędny jest to zachowania oczekiwanej płynności. Np. dla 60 FPS, maksymalny czas klatki to 16,66 ms. Wystarczy się tego trzymać i płynność powinna zostać zachowana, oczywiście biorąc pod uwagę przy wszelkich przesunięciach DeltaTime, tj. czas poprzedniej klatki).

Stan sceny nie może być zależny od czasu renderowania/rysowania klatki

U mnie to wygląda tak, że inputy są zbierane w czasie rzeczywistym przez osobny wątek. Na początku każdej klatki wątek roboczy (odpowiedzialny za GameLoop i renderowanie klatek) synchronizuje inputy z kolejki obsługiwanej przez ten drugi wątek. Wedle schematu:

  1. Synchronizuje inputy z wątku zbierającego (tego, który obsługuje message loop i WM_INPUT).
  2. Odświeża logikę gry (także w oparciu o te inputy).
  3. Renderuje klatkę na bazie odświeżonej logiki gry.
  4. Czeka przez odpowiednią ilość czasu, żeby zachować płynny FPS, sparowany z odświeżaniem monitora (używam API Direct2D z włączonym V-SYNC'iem, więc za to odpowiada "automat").

Ruch na scenie odbywa się cały czas niezależnie od tego ile klatek na sekundę ogarniesz graficznie.

No i tak właśnie robię, ale przecież ruch nie może odbywać się w czasie rzeczywistym (bo GameLoop tak nie działa), tylko sekwencyjnie, "per frame", niezależnie od ilości klatek czy ich częstotliwości. Nadal jednak nie rozumiem jak to się ma do mojego problemu. Bo przecież renderując kolejną klatkę (a więc odświeżając wpierw logikę gry, bo to poprzedza każde renderowanie), muszę wziąć pod uwagę wszystkie zmiany, jakie zaszły w logice gry od czasu ostatniej klatki. No i co mam zrobić w sytuacji, gdy od ostatniej klatki zarejestrowano Spacja DOWN, Spacja UP, Spacja DOWN? W realiach tej klatki, którą za chwilę będę renderował, stan Spacji = DOWN, ale czy to oznacza, że poprzednie stany mam pominąć? Czy może rozbić je na trzy kolejne klatki i obsłużyć wszystkie?

0
Crow napisał(a):

Klatki renderują się osobno, a ruch osobno

W jaki sposób? Chyba nie do końca rozumiem.

Np. na osobnym wątku.

jak zrobisz tak jak piszesz to gra będzie na wolnych komputerach chodziła wolniej a na szybszych szybciej.

Ale dlaczego? Przecież 60 klatek na sekundę to 60 klatek na sekundę, niezależnie od mocy komputera czas płynie przecież tak samo. Ważne, żeby czas klatki był mniej więcej stały.

A co jak komputer nie wyrobi wyrenderować 60 klatek a jedynie 10?

Stan sceny nie może być zależny od czasu renderowania/rysowania klatki

U mnie to wygląda tak, że inputy są zbierane w czasie rzeczywistym przez osobny wątek. Na początku każdej klatki wątek roboczy (odpowiedzialny za GameLoop i renderowanie klatek) synchronizuje inputy z kolejki obsługiwanej przez ten drugi wątek. Wedle schematu:

Inputów się nie zbiera i buforuje tylko się je odpowiednio obsługuje. W jaki sposób napisałem Ci wyżej.

  1. Synchronizuje inputy.
  2. Odświeża logikę gry (także w oparciu o inputy).
  3. Renderuje klatkę na bazie odświeżonej logiki gry.
  4. Czeka przez odpowiednią ilość czasu, żeby zachować płynny FPS, sparowany z odświeżaniem minitora (używam API Direct2D z włączonym V-SYNC'iem, więc za to odpowiada "automat").

No i właśnie ta powyższa kolejność jest zła. jedynie wątek renderowania czeka na "powrót plamki" ale nie wątek odpowiadający za akcję gry. Wątek odpowiedzialny za akcję gry niech sprawdza stany klawiszy i różnice pozycji myszy 200 albo 2000 razy na sekundę tam nie będzie aż tylu obliczeń co podczas renderowania. Wówczas nie będziesz musiał nic buforować.

Ruch na scenie odbywa się cały czas niezależnie od tego ile klatek na sekundę ogarniesz graficznie.

No i tak właśnie robię,

No właśnie tak nie robisz.

ale przecież ruch nie może odbywać się w czasie rzeczywistym, tylko "per frame",

Właśnie nie a a 100% nie przez per frame renderowania. Jeśli już to "per frame" stanu akcji.

niezależnie od ich ilości czy częstotliwości. Nadal jednak nie rozumiem jak to się ma do mojego problemu. Bo przecież renderując kolejną klatkę (a więc odświeżając logikę gry), muszę wziąć pod uwagę wszystkie zmiany, jakie zaszły od czasu ostatniej klatki.

To wynika już samo z siebie jeśli zastosujesz model o którym Ci piszę. Scenę masz zawsze aktualną zrobienie zdjęcia aktualnej sceny zawsze będzie dobrym zdjęciem.

No i co mam zrobić w sytuacji, gdy od ostatniej klatki zarejestrowano Spacja DOWN, Spacja UP, Spacja DOWN?

nie mam pojęcia bo to jest złe podejście do sprawy. Musisz przerobić logikę swojego silnika.

W realiach tej klatki, którą za chwilę będę renderował, stan Spacji = DOWN, ale czy to oznacza, że poprzednie stany mam pominąć? Czy może rozbić je na trzy kolejne klatki?

j.w. ciągle tkwisz w błędnym podejściu do tematu.

0

niech sprawdza stany klawiszy i różnice pozycji myszy 200 albo 2000 razy na sekundę tam nie będzie aż tylu obliczeń co podczas renderowania.

Ale właśnie w tym tkwi sendo mojego pytania! ZAWSZE będą jakieś cykliczne sprawdzenia stanu, prawda? Nie ważne czy 10, 100 czy 10000000 razy na sekundę, zawsze będzie jakieś POPRZEDNIO i jakieś TERAZ, niezależne od modelu renderowania, rozbicia na wątki itd., prawda? No i właśnie o to pytam, co zrobić w sytuacji, gdy pomiędzy POPRZEDNIO i TERAZ mam więcej niż jeden stan (np. kilka wciśnięć i odpuszczeń tego samego klawisza albo kilka ruchów myszą)? Wiem, że w praktyce to mało wykonalne, bo zwykły człowiek czegoś takiego raczej nie dokona mając do dyspozycji milisekundy, ale przecież jakiś hipotetyczny model muszę przyjąć i mieć przygotowane jakieś zachowanie silnika w takiej sytuacji, prawda? Coś z tym "nadmiarem" informacji muszę zrobić, uśrednić, zachować, wyrzucić... O to mi chodzi od samego początku.

0
Crow napisał(a):

niech sprawdza stany klawiszy i różnice pozycji myszy 200 albo 2000 razy na sekundę tam nie będzie aż tylu obliczeń co podczas renderowania.

Ale właśnie w tym tkwi sendo mojego pytania! ZAWSZE będą jakieś cykliczne sprawdzenia stanu, prawda? Nie ważne czy 10, 100 czy 10000000 razy na sekundę, zawsze będzie jakieś POPRZEDNIO i jakieś TERAZ, niezależne od modelu renderowania, rozbicia na wątki itd., prawda? No i właśnie o to pytam, co zrobić w sytuacji, gdy pomiędzy POPRZEDNIO i TERAZ mam więcej niż jeden stan (np. kilka wciśnięć i odpuszczeń tego samego klawisza albo kilka ruchów myszą)? Wiem, że w praktyce to mało wykonalne, bo zwykły człowiek czegoś takiego raczej nie dokona mając do dyspozycji milisekundy, ale przecież jakiś hipotetyczny model muszę przyjąć i mieć przygotowane jakieś zachowanie silnika w takiej sytuacji, prawda? Coś z tym "nadmiarem" informacji muszę zrobić, uśrednić, zachować, wyrzucić... O to mi chodzi od samego początku.

Uparłeś się przy swoim i męczysz. Wygląda na to, że nie przeczytałeś tego co pisałem wcześniej.

  1. Przyciski akcji obsługujesz z buforów systemowych. Po odczycie stanów możesz wyczyścisz bufory ale to zależy co chcesz osiągnąć.

  2. Ruch myszą czy przyciski klawiatury odpowiedzialne za ruch obsługujesz na podstawie ich bieżącego stanu:

    • klawisze: w danym momencie jest naciśnięty albo nie i koniec!
    • mysz: w danym momencie wyliczasz różnicę pozycji względem ostatniego odczytu stanu i koniec!
      W doopie masz stany pośrednie, a duża częstotliwość weryfikacji stanów gwarantuje Ci ciągłość i płynność reakcji na działania użytkownika.
      Nawet jeśli w ciągu tej jednej milisekundy pomiędzy odczytami stanów użytkownik przesunął mysz o kilometr ale wrócił nią do pozycji ostatniego odczytu to olewasz to przesunięcie o kilometr i bierzesz jedynie stan z BIEŻĄCEGO MOMENTU. Nie analizujesz żadnych historii.

Dlatego odczytu stanów klawiatury i myszy nie możesz robić między renderoaniem klatek bo to może się okazać zbyt wolne!

Nawet pod DOS w prostych grach symulowano taką "wielozadaniowość" na przerwaniu 0x1c, którego częstotliwość wywoływania można było ustawić i tym samym w głównym wątku w kółko się coś renderowało a ruch po scenie był realizowany właśnie pod tym przerwaniem.

0

Uparłeś się przy swoim i męczysz. Wygląda na to, że nie przeczytałeś tego co pisałem wcześniej.

Absolutnie nie, po prostu staram się zrozumieć co masz na myśli. Być może masz rację, po prostu nie wszystko co mówisz, jest dla mnie jasne i pewne rzeczy wydają się kłócić z logiką, dlatego dopytuję, żeby nauczyć się jak najwięcej w tym zakresie.

Przyciski akcji obsługujesz z buforów systemowych. Po odczycie stanów możesz wyczyścisz bufory ale to zależy co chcesz osiągnąć.

Jak już pisałem, używam chyba najlepszego narzędzia, jakie Windows w tym momencie oferuje, czyli RAW INPUTS. Trochę przypomina to wcześniejszy system windowsowych komunikatów, ale jest bardziej surowy (mniej obrobiony przez OS), przez co nie ma np. wkurzającego rozbicia na WM_KEYDOWN i WM_SYSKEYDOWN, tylko wszystko podawane jest w jednym WM_INPUT, a do tego działa tam odczyt klawiszy, które nie są obsługiwane przy starych komunikatach, np. Print Screen. Fajne jest też to, że ruchy myszą nie mają tam akceleracji (lub jak nazywają to w MSDN - "balistyki kursora"), co o wiele lepiej sprawdza się w FPS'ach.
istotne jest jednak to, że działa to na zasadzie eventowej, a więc nie muszę robić tego przez polling (ciągłe odpytywanie o stan poszczególnych klawiszy), tylko za pośrednictwem własnej kolejki. Przechwytuję wszystkie inputy z WM_INPUT, wrzucam je do kolejki, a na początku każdej klatki kolejka jest synchronizowana do tablicy stanów (na której bazuje logika gry), a potem czyszczona.

Ruch myszą czy przyciski klawiatury odpowiedzialne za ruch obsługujesz na podstawie ich bieżącego stanu:

Czyli odrzucam stany pośrednie? No właśnie od początku o to właśnie pytałem.

mysz: w danym momencie wyliczasz różnicę pozycji względem ostatniego odczytu stanu i koniec!

No ja nie mam dostępu do absolutnego stanu myszy, tylko system komunikatów, które podają jej przesunięcia względem poprzedniego położenia (ja to dla uproszczenia nazywam szybkością myszy). Czyli np. wykonasz lekki ruch w prawo, dostaniesz komunikat Mouse.X = 1. Mocno (szybko) szarpniesz w lewo, dostaniesz Mouse.X = -7. Przydatne, bo wyznacza nie tylko kierunek, ale też "szybkość" ruchu.

Dlatego odczytu stanów klawiatury i myszy nie możesz robić między renderoaniem klatek bo to może się okazać zbyt wolne!

No ale tu czegoś nie rozumiem. Jeżeli logika gry będzie odświeżana w osobnym wątku z odblokowaną liczbą klatek (a więc ile fabryka dała mocy), to czy to nie da efektu, o którym pisałeś na początku, tj., że szybkość gry będzie zależała od szybkości komputera? Bo w twoim modelu logika jest odświeżana bez przerwy, najszybciej jak się da, tak? Wiec na szybkim komputerze może to być 6000 razy na sekundę, a na wolnym 38 razy na sekundę, więc bez zastosowania DeltaTime logika w obu przypadkach będzie się rozjeżdżać, prawda? Ale nawet przy użyciu DT, nie będzie to niepotrzebnie obciążające do CPU? Bo po co zżerać 100% mocy rdzenia (albo większej ich ilości), skoro gra może tego nawet nie potrzebować?

2

W Fairtrisie i innych projektach robię tak, że stan klawiszy i przycisków kontrolerów czytam przed obsługą logiki i renderowaniem, w ramach jednego wątku. Stan klawiszy i przycisków przepisuję do własnych list zawierających obiekty reprezentujące wszystkie klawisze/przyciski. Dzięki temu input odczytuję raz i wiem jak zmienił się stan każdego klawisza. Proste, wygodne w obsłudze, wszystkie ważne informacje są dostępne.

Wątki raczej nie będą potrzebne, tym bardziej, jeśli mowa o nieskomplikowanych zastosowaniach. Zresztą nieistotne jest to ilu wątków się użyje i w jaki sposób pobiera się informacje o stanie urządzeń wejścia. Istotne jest to, aby tuż przed obsługą logiki, mieć komplet informacji na temat tych urządzeń. Mieć aktualny stan wszystkich klawiszy/przycisków oraz stan z poprzedniej klatki, tak aby wiedzieć co się zmieniło.

0

@furious programming:

furious programming napisał(a):

W Fairtrisie i innych projektach robię tak, że stan klawiszy i przycisków kontrolerów czytam przed obsługą logiki i renderowaniem, w ramach jednego wątku. Stan klawiszy i przycisków przepisuję do własnych list zawierających obiekty reprezentujące wszystkie klawisze/przyciski. Dzięki temu input odczytuję raz i wiem jak zmienił się stan każdego klawisza. Proste, wygodne w obsłudze, wszystkie ważne informacje są dostępne.

Czyli dokładnie tak jak ja!

  1. Inputy (synchronizowane z wątku obsługującego windowsowe komunikaty do własnej tablicy, której potem używa logika gry).
  2. Logika gry - aktualizowana na bazie inputów i z użyciem DeltaTime.
  3. Renderowanie klatki obrazu na bazie odświeżonej logiki.
  4. "Przesypianie" reszty w oczekiwaniu na kolejną klatkę (tym zajmuje się dla mnie API).

Wątki raczej nie będą potrzebne, tym bardziej, jeśli mowa o nieskomplikowanych zastosowaniach. Zresztą nieistotne jest to ilu wątków się użyje i w jaki sposób pobiera się informacje o stanie urządzeń wejścia. Istotne jest to, aby tuż przed obsługą logiki, mieć komplet informacji na temat tych urządzeń. Mieć aktualny stan wszystkich klawiszy/przycisków oraz stan z poprzedniej klatki, tak aby wiedzieć co się zmieniło.

Dokładnie i tutaj mam właśnie problem, zwłaszcza z myszą. Bo mam wrażenie, że o ile w przypadku klawiszy wystarczy automatyczna synchronizacja na początku każdej klatki, tak w przypadku myszy może to być za mało responsywne. Dodatkowo przy klawiszach stany pośrednie domyślne sobie pomijam, czyli obchodzi mnie jedynie ostatni stan klawisza w chwili synchronizacji, ale już nie to, że był wcześniej wciśnięty i wyciśnięty np. 3 razy (choć przy rozsądnym klatkarzu i tak prawie niewykonalne, żeby tyle akcji udało się ogarnąć pomiędzy dwoma sąsiednimi klatkami, czyli w kilka milisekund). Czy przy myszy takie podejście też wystarczy? Mogę np. wywalić wcześniejsze ruchy i uwzględnić tylko ostatni? A może je po prostu sumować? Np. dostanę 4 komunikaty, każdy z nich Mouse.X = 1, więc po prostu przesunę o 4 w prawo?

1

Nie wiem czemu komunikaty inputu obsługujesz w osobnym wątku, skoro one są dostarczane w wątku głównym. W każdym razie w przypadku myszy możesz robić dokładnie to samo, co w przypadku klawiatury — synchronizować tylko świeży stan. Ważne jest tylko to, aby nie pogubić danych, których nie da się odczytać w dowolnym momencie — tutaj chodzi mi głównie o rolkę myszy, bo jej stanu nie da się odczytać inaczej niż obsługując stosowny komunikat. Ale jak najbardziej jest to wykonalne.

Podczas pisania kodu Fairtrisa, myślałem właśnie o obsłudze wyłącznie komunikatów (w SDL zdarzeń), ze względu na problemy z kontrolerami. Ale kiedy okazało się, że problemy są zarówno w przypadku zdarzeń, jak i innych dostępnych możliwości, wróciłem do starego, sprawdzonego spowobu. Tyle tylko, że moja gierka nie używa myszy, więc mogłem sobie na spokojnie odpuścić zdarzenia.

Summa summarum — implementacja zależy od wymagań, a można to zrobić na wiele sposobów.

Crow napisał(a):

Czy przy myszy takie podejście też wystarczy? Mogę np. wywalić wcześniejsze ruchy i uwzględnić tylko ostatni? A może je po prostu sumować? Np. dostanę 4 komunikaty, każdy z nich Mouse.X = 1, więc po prostu przesunę o 4 w prawo?

Jeśli potrzebujesz znać dystans pokonany przez kursor, to sobie go oblicz na podstawie stanu obecnego i tego z poprzedniej klatki. Jeśli w ramach jednej klatki dostałeś kilka komunikatów o kliknięciu, to możesz to zignorować lub obsłużyć — zależnie od wymagań (przy stałym klatkażu raczej ignorowałbym i brał tylko świeży stan).

Przy zmiennym/niskim klatkażu, obsługa wszystkich otrzymanych komunikatów powinna być realizowana. Kliknięcie może trwać krócej niż czas pomiędzy klatkami, więc nie powinno być zgubione. W takim przypadku gracz zauważy, że gra nie reaguje i będzie zirytowany. To samo z klawiaturą czy kontrolerem — jeśli gracz poprawnie wcisnął klawisz/przycisk, nawet jeśli bardzo szybko, to te akcje nie mogą być ignorowane. Nie jego wina, że gra nie wyrabia. ;)

0

@furious programming:

furious programming napisał(a):

Jeśli potrzebujesz znać dystans pokonany przez kursor, to sobie go oblicz na podstawie stanu obecnego i tego z poprzedniej klatki. Jeśli w ramach jednej klatki dostałeś kilka komunikatów o kliknięciu, to możesz to zignorować lub obsłużyć — zależnie od wymagań (przy stałym klatkażu raczej ignorowałbym i brał tylko świeży stan).

Ruch myszy przechwytuję na 2 sposoby. Używając Raw Inputs, co ma taką zaletę, że tam nie ma akceleracji i podawana jest nie obecna pozycja kursora, tylko przesunięcia myszy w danej osi. Jeżeli więc otrzymam 2 komunikaty, oznaczać to będzie, że pomiędzy poprzednią i obecną klatką, zarejestrowano 2 osobne ruchy myszą (a przynajmniej tyle wychwycił OS). Drugi rodzaj inputów ruchu myszy, to te pochodzące z WM_MOVE, które podają pozycję kursora (w relacji do współrzędnych ekranu), od razu z akceleracją (a więc pewnym "zaokrąglaniem" i wygładzaniem). Pierwszy typ sprawdza się lepiej np. w FPSach (gdzie nie ma kursora, a ważna jest precyzja), drugi np. w RTSach albo ogólnie interface'ach gier, obsługiwanych myszą.

Przy zmiennym/niskim klatkażu, obsługa wszystkich otrzymanych komunikatów powinna być realizowana. Kliknięcie może trwać krócej niż czas pomiędzy klatkami, więc nie powinno być zgubione. W takim przypadku gracz zauważy, że gra nie reaguje i będzie zirytowany. To samo z klawiaturą czy kontrolerem — jeśli gracz poprawnie wcisnął klawisz/przycisk, nawet jeśli bardzo szybko, to te akcje nie mogą być ignorowane. Nie jego wina, że gra nie wyrabia. ;)

Ale właśnie jak do tego podejść? Taka sytuacja (pomiędzy dwoma klatkami, np. wtedy, gdy ktoś ma 15 FPS):

1. SPACJA - DOWN
2. SPACJA - UP

3. SPACJA - DOWN
4. SPACJA - UP

5. SPACJA - DOWN

Jak to obsłużyć? Technicznie rzecz biorąc, gracz nacisnął klawisz 3 razy, więc mam to rozbić na 3 osobne klatki? I jak odróżnić sytuację, w której inputy powinny być obsłużone od tych, w których inputy mogą być pominięte? Bo w jednej klatce tego nie idzie obsłużyć. Przyjmijmy, że Spacja otwiera jakieś okno (a odpuszczenie je zamyka), więc nie da rady w jednej klatce zamknąć i otworzyć okna 3 razy, więc albo muszę to zrobić tylko raz, albo rozbić na trzy klatki, żeby gracz widział zmiany. I to jest właśnie problem, że nie wiem jak do tego podejść, zwłaszcza w przypadku myszy.

0
Crow napisał(a):

Uparłeś się przy swoim i męczysz. Wygląda na to, że nie przeczytałeś tego co pisałem wcześniej.

Absolutnie nie, po prostu staram się zrozumieć co masz na myśli. Być może masz rację, po prostu nie wszystko co mówisz, jest dla mnie jasne i pewne rzeczy wydają się kłócić z logiką, dlatego dopytuję, żeby nauczyć się jak najwięcej w tym zakresie.

Przyciski akcji obsługujesz z buforów systemowych. Po odczycie stanów możesz wyczyścisz bufory ale to zależy co chcesz osiągnąć.

Jak już pisałem, używam chyba najlepszego narzędzia, jakie Windows w tym momencie oferuje, czyli RAW INPUTS. Trochę przypomina to wcześniejszy system windowsowych komunikatów, ale jest bardziej surowy (mniej obrobiony przez OS), przez co nie ma np. wkurzającego rozbicia na WM_KEYDOWN i WM_SYSKEYDOWN, tylko wszystko podawane jest w jednym WM_INPUT, a do tego działa tam odczyt klawiszy, które nie są obsługiwane przy starych komunikatach, np. Print Screen. Fajne jest też to, że ruchy myszą nie mają tam akceleracji (lub jak nazywają to w MSDN - "balistyki kursora"), co o wiele lepiej sprawdza się w FPS'ach.

W przypadku sterowania nie ma znaczenia jakiego narzędzia używasz. Masz potrafić odczytać stan przycisku czy jest wciśnięty czy nie i to wszystko. Reszta Cię nie interesuje. Tak samo jak to czy przycisk "już był" naciśnięty.

istotne jest jednak to, że działa to na zasadzie eventowej, a więc nie muszę robić tego przez polling (ciągłe odpytywanie o stan poszczególnych klawiszy), tylko za pośrednictwem własnej kolejki. Przechwytuję wszystkie inputy z WM_INPUT, wrzucam je do kolejki, a na początku każdej klatki kolejka jest synchronizowana do tablicy stanów (na której bazuje logika gry), a potem czyszczona.

No i to wcale nie jest dobre. Bo masz Twoim zadaniem jest jedynie odczytać stan przycisku.

Ruch myszą czy przyciski klawiatury odpowiedzialne za ruch obsługujesz na podstawie ich bieżącego stanu:

Czyli odrzucam stany pośrednie? No właśnie od początku o to właśnie pytałem.

mysz: w danym momencie wyliczasz różnicę pozycji względem ostatniego odczytu stanu i koniec!

No ja nie mam dostępu do absolutnego stanu myszy, tylko system komunikatów, które podają jej przesunięcia względem poprzedniego położenia (ja to dla uproszczenia nazywam szybkością myszy). Czyli np. wykonasz lekki ruch w prawo, dostaniesz komunikat Mouse.X = 1. Mocno (szybko) szarpniesz w lewo, dostaniesz Mouse.X = -7. Przydatne, bo wyznacza nie tylko kierunek, ale też "szybkość" ruchu.

To użyj takiego, które da Ci taką możliwość albo samemu je zaimplementuj w oparciu o eventy, które już masz.

Dlatego odczytu stanów klawiatury i myszy nie możesz robić między renderoaniem klatek bo to może się okazać zbyt wolne!

No ale tu czegoś nie rozumiem. Jeżeli logika gry będzie odświeżana w osobnym wątku z odblokowaną liczbą klatek (a więc ile fabryka dała mocy), to czy to nie da efektu, o którym pisałeś na początku, tj., że szybkość gry będzie zależała od szybkości komputera?

Nie jedynie szybkość renderowania będzie na wolnych komputerach będziesz miał 10 klatek/sek. a na szybkich 200 klatek/sek. Czyli tak jak ma być.

Bo w twoim modelu logika jest odświeżana bez przerwy, najszybciej jak się da, tak? Wiec na szybkim komputerze może to być 6000 razy na sekundę, a na wolnym 38 razy na sekundę, więc bez zastosowania DeltaTime logika w obu przypadkach będzie się rozjeżdżać, prawda?

Nic się nie będzie rozjeżdżać bo logika gry jest odświeżana a każdym komputerze z taką samą częstotliwością. Natomiast renderowanie z różną.

Ale nawet przy użyciu DT, nie będzie to niepotrzebnie obciążające do CPU? Bo po co zżerać 100% mocy rdzenia (albo większej ich ilości), skoro gra może tego nawet nie potrzebować?
Zawsze możesz ograniczyć ilość klatek ale stoi to w sprzeczności z trendami bo z tego co "na mieście powiadają" to im więcej klatek tym lepiej, a i monitory 200Hz robią...

0

To co proponuje @furious programming czyli w jednym wątku też jest do zrobienia ale jest problematyczne z powodów o których piałem.

Najlepiej żeby logika miała swój wątek, a renderowanie swój. Są gry gdzie logika też bardzo obciąża grę i nie jest drobiazgiem dla CPU wtedy sprawa komplikuje się jeszcze bardziej.
Wyobraź sobie prostą rzecz. Załóżmy, że wciśnięcie strzałki do przodu rozpędza samochód. Chcemy zrobić tak aby nasze auto rozpędzało się 5 sek do 100km/k.
Jeśli masz odczytywanie stanu interfejsu cykliczne ( 100x / sek ) w tych samych odstępach czasowych to możesz w każdym cyklu zwiększyć prędkość o 0,02km/h i uzyskasz ten efekt na każdym komputerze taki sam. To samo odnosi się do odległości, którą przejdzie w danym czasie sterowana postać postać albo jak szybko będzie się obracać.
Jeśli odczyt stanów interfejsów będzie w czasie "losowy" wówczas masz problem bo wszystkie obliczenia zawsze będziesz musiał odnosić do jakiegoś zegara czasu rzeczywistego co zwyczajnie komplikuje obliczenia. Oczywiście jest to jakiś sposób na zrobienie sterowania ale dużo trudniejszy.

Natomiast odczytywanie stanu interfejsu pomiędzy klatkami niestety ma tę wadę, że jak grafika "przytnie" to do 5 klatek/sek. to stracimy także kontrolę nad sterowaniem.

Większość prostych gier z bardziej zaawansowaną grafiką jednak działa tak jak Ci napisałem - sterowanie na stałym interwale grafika obok. Wówczas nie ma znaczenia czy odpalisz ją na grafice 320x200 czy monitorze 8k bo czar renderowania Cię nie interesuje i możesz spokojnie zająć się oprogramowywaniem logiki gry.

2
katakrowa napisał(a):

To co proponuje @furious programming czyli w jednym wątku też jest do zrobienia ale jest problematyczne z powodów o których piałem.

W moim przypadku akurat praca w jednym wątku w niczym nie przeszkadza, a wręcz jest porządana. W przypadku innych gier, również jednowątkowość nie jest problemem, nawet jeśli framerate jest zmienny. Podział logiki i renderowania na wątki jest sprawą mocno skomplikowaną, dlatego jeśli nie trzeba to się tego nie robi.

@Crow: jeśli chcesz konkretów to sam podaj konkrety — napisz co robisz i jak bardzo zasobożerne to będzie. Bo jeśli robisz coś prostego, to jeden wątek i polling najpewniej wystarczą, a jeśli klon Cyberpunka, to wtedy do tematu trzeba będzie podejść inaczej.

Wyobraź sobie prostą rzecz. Załóżmy, że wciśnięcie strzałki do przodu rozpędza samochód. Chcemy zrobić tak aby nasze auto rozpędzało się 5 sek do 100km/k.

Ale to jest kwestia używania delty, a nie wątków czy kolejki komunikatów inputu.

Większość prostych gier z bardziej zaawansowaną grafiką jednak działa tak jak Ci napisałem - sterowanie na stałym interwale grafika obok. Wówczas nie ma znaczenia czy odpalisz ją na grafice 320x200 czy monitorze 8k bo czar renderowania Cię nie interesuje i możesz spokojnie zająć się oprogramowywaniem logiki gry.

Tym bardziej, jeśli renderuje się na tylnym buforze a potem siup — upscaling i gotowe. :D

2
Crow napisał(a):

Ale właśnie jak do tego podejść? Taka sytuacja (pomiędzy dwoma klatkami, np. wtedy, gdy ktoś ma 15 FPS):

Jak ktoś gra przy 15fps to responsywność akurat jest jego najmniejszym problemem. :D

Jak to obsłużyć? Technicznie rzecz biorąc, gracz nacisnął klawisz 3 razy, więc mam to rozbić na 3 osobne klatki? I jak odróżnić sytuację, w której inputy powinny być obsłużone od tych, w których inputy mogą być pominięte?

Jeśli dostałeś w ramach jednej klatki kilka komunikatów klawisza, który np. otwiera jakieś okienko, to wykonujesz akcję (czyli otwierasz okienko) i czyścisz kolejkę, tak aby nie obsługiwać reszty oraz aby stan inputu nie został w tyle (czyli aby stare komunikaty nie były obsługiwane w kolejnych klatkach).

W przypadku myszy, jeśli masz kilka komunikatów dostarczających przesunięcie kursora, to powinieneś takie przesunięcie zsumować. Natomiast w przypadku bardzo niskiego klatkażu, byłbym skłonny rozbijać deltę na mniejsze części i w ramach jednej klatki aktualizować logikę kilka razy. Jeśli tego nie zrobisz, to będzie można wykorzystać lagowanie aby przechodzić przez ściany itp., czyli tak jak to się dawno temu działo.

0

@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.
Tylko czy aby na pewno jest to pożądany efekt? 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). Nie wiem, czy to dobry pomysł, rozważam różne opcje, bo nie wiem jak to się robi w prawdziwym gamedevie.

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: 1