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ć?

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