GameLoop - obsługa sterowania (inputs)

0

W swoim silniku (napisanym w C++) do obsługi sterowania (przyciski klawiatury, kontrolera, myszy) używam windowsowych komunikatów przechwytywanych przez WndProc pokroju WM_KEYDOWN czy WM_KEYUP. Odpowiednie kody przycisków (wraz z informacją o tym, czy przycisk jest wciśnięty czy nie), są zapisywane do kolejki. Kolejka natomiast jest synchronizowana z tablicą przycisków na początku każdej klatki. Dzięki temu z poziomu logiki gry można się odwoływać do tej tablicy, np. żeby sprawdzić sobie stan odpowiedniej flagi danego klawisza Controls[Key::SPACE].Pressed (tak działa to obecnie).

Zastanawia mnie jednak, czy tak właśnie powinna wyglądać kwestia synchronizacji i oczyszczania kolejki. Załóżmy np., że mam grę typu FPS. Gracz nacisnął guzik odpowiedzialny za przeładowanie, więc rozpoczęła się animacja przeładowania broni. Zanim jednak animacja dobiegła końca, gracz nacisnął kolejny klawisz, odpowiedzialny za strzał. Oczywiście ta akcja nie mogła nastąpić w czasie przeładowania, więc input musiał w danym momencie (w danej klatce) zostać zignorowany. Tylko co z nim teraz zrobić? Zablokować przetwarzanie kolejki aż pojawi się możliwość obsługi tego klawisza (to raczej odpada, bo gracz powinien nadal móc np. się poruszać w czasie przeładowania)? Przerzucić ten klawisz na koniec kolejki i przetwarzać dalej pozostałe inputy, a potem do niego wrócić i sprawdzić, czy już można? A jeżeli miałbym przerzucić, to ile takich opóźnionych klawiszy powinno zostać zapisanych do wykonania "na potem"? Bo co będzie, jak np. w czasie tego przeładowania gracz naciśnie 10 innych klawiszy, odpowiedzialnych za 10 innych, chwilowo zablokowanych akcji (np. skok, kucnięcie, otwarcie ekwipunku itd.)? Czy wszystkie powinny zostać wykonane z opóźnieniem, zgodnie z kolejnością naciskania? A może powinien tu obowiązywać jakiś limit (np. do 3 ostatnio wykonanych akcji, które trafiają do bufora, a te starsze przepadają)? Nie pytam o konkretny kod (stąd temat w tym dziale), tylko raczej o koncepcję, ogólny zarys jak to powinno działać, żeby było responsywnie i wygodnie.

Swoje pytanie kieruję głównie do osób, które mają jakieś praktyczne doświadczenie w projektowaniu tego typu algorytmów.

1

Załóżmy np., że mam grę typu FPS. Gracz nacisnął guzik odpowiedzialny za przeładowanie, więc rozpoczęła się animacja przeładowania broni. Zanim jednak animacja dobiegła końca, gracz nacisnął kolejny klawisz, odpowiedzialny za strzał. Oczywiście ta akcja nie mogła nastąpić w czasie przeładowania, więc input musiał w danym momencie (w danej klatce) zostać zignorowany.

ja bym ignorował, w wielu grach tak jest, zwłaszcza w tym przykładzie przeładowanie a strzelanie. Ale jak najbardziej tak jak piszesz z chodzeniem i przeładowaniem, nie blokowałbym samej kolejki.
Reszta zależy już od samej gry, ale czemu by tych klawiszy po prostu nie obsługiwać?
Gracz naciska jednocześnie skok, kucnięcie, otwarcie ekwipunku - cudem udaje mu się zsynchronizować, żeby przeszło w jednej klatce, ale mamy taki przypadek.
No to:

  • zaczynamy skakać
  • zaczynamy kucać, co może w jakiś sposób przerywać skok, lub kucanie się nie chce wykonać, ponieważ postać już jest oznaczona jako 'nieuziemiona'. Z perspektywy inputu wołamy jednak o skok, i nie obchodzi nas co dalej
  • otwieramy ekwipunek (co może pauzować czas, ale nie wpływa zasadniczo na to, co się stało, czyli skok/kucanie)

Ogólnie raczej starałbym się nie mieszać samej interakcji z obsługą inputu, a przynajmniej nie w ten sposób - Twój input daje info czy klawisz jest wciśnięty, i ok - jego zadanie na tym może być skończone, a dalsza odpowiedzialność zostać przejęta przez coś innego.

0

Dostarczasz event naciśniętego klawisza do obiektu gry, niech to będzie broń. Jak strzela, a ma być reload, możesz to możesz to zignorować albo zakolejkować w ramach samej broni. Jak w międzyczasie gracz zmieni giwerę to resetujesz kolejkę guna (czy tam ustawiasz stan początkowy, jeden pies). Input generuje event który potem może być różnorako obsłużony i tyle.

0

No właśnie sam nie wiem. Bo zauważcie, że wiele gier, zwłaszcza takich, które wymagają od gracza precyzji (np. platformówek) wydaje się w pewnym stopniu "zapamiętywać" inputy, których nie udało się wykonać od razu. Np. wykonujecie skok, postać leci (więc teoretycznie jest zablokowana i nie odbiera żadnych nowych inputów, chyba, że można sterować lotem, ale przyjmijmy, że nie). Chcecie zaraz po wylądowaniu wykonać jakąś nową akcję (np. strzał, bo zbliża się do was przeciwnik albo korektę pozycji postaci, bo jest za blisko przepaści). W wielu grach da się zauważyć, że występuje w takich sytuacjach pewien margines błędu, tzn. nie trzeba się wstrzelić z nowym inputem idealnie wtedy, gdy postać wyląduje (a więc odzyska aktywność), tylko można nacisnąć guzik troszkę wcześniej (w ostatniej fazie lotu) i postać co prawda nie zareaguje od razu, ale zrobi to zaraz po wylądowaniu. To oznacza, że jakieś kolejkowanie niewykorzystanych inputów tam musi być.

Ja myślałem nad czymś takim (powiedzcie co myślicie):

  1. Dla każdej konkretnej gry rejestrować konkretne inputy (np. Key::Control albo Key::Space). To co niezarejestrowane, nie będzie w ogóle kolejkowane.
  2. Zrezygnować z synchronizacji kolejki do tablicy i odpytywania tablicy na zasadzie if (Controls[Key::CONTROL_LEFT].Released) DoSomething(), tylko zbudować obsługę bezpośrednio samej kolejki i obrabiać ją "ręcznie", z poziomu gry, nie silnika, np. tak:
//Na początku klatki, jeszcze przed odświeżeniem logiki gry
while (!Queue.empty()) //To nie standardowe std::queue, tylko własna klasa z możliwością iteracji i paroma innymi funkcjami.
{
	//Odczytuje najstarszy input z kolejki 
	switch (Input)
	{
	case Key::MOUSE_LEFT: //Strzał z broni
		if (!RealoadingWeapon())
		{
			Gun.Shoot();
			Queue.pop(); //Input obsłużony, wylatuje z kolejki
		} 
		else //Nie można obsłużyć inputu i trzeba zdecydować co dalej
		{
			Queue.pop() //Po prostu input zostanie pominięty, albo:
			Queue.save() //Input wylatuje z tej kolejki, ale zostanie dopisany do następnej, analizowanej na początku kolejnej klatki
			//dzięki temu jest szansa, że input zostanie obsłużony później.
		{
		break;
	case Key::SPACE:
		//cośtam
		break;
	default: //Domyślnie wywala input z kolejki i nic nie robi
		Queue.pop();
		break;
	}
}

Czyli kolejka nie byłaby zarządzana automatycznie, tylko trzeba by (w zależności od gry) decydować, co się ma dziać z danym inputem (wylatuje albo nie).
Myślałem też o wprowadzeniu np. limitu klatek, że jak np. input został przeniesiony do nowej kolejki ileś tam razy (np. przez 60 kolejnych klatek) i dalej nie został obsłużony, to z automatu wylatuje, bo jest już zbyt "stary".

0

Paczaj :P
https://github.com/alagner/exult/blob/master/tqueue.cc#L145
Tutaj architektura niekoniecznie jest najlepsza, bo tqueue siedzi w singletonie Game_window i może być modyfikowana podczas obsługi eventów (inwalidując/dokładając inne), przez co ciężko ten kod rozpatrywać lokalnie. Niemniej: dla inspiracji możesz zerknąć, ja się sporo nauczyłem na tym kodzie.

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