Event loop, kolejka, odczyt klawiszy

Event loop, kolejka, odczyt klawiszy
CR
  • Rejestracja:ponad 16 lat
  • Ostatnio:11 miesięcy
0

Zainspirowany tym co poradził mi @alagner (bardzo dziękuję!) zacząłem główkować nad przeprojektowaniem GameLoop'a w prostym silniku, który piszę. Inputy z klawiatury rzeczywiście nie powinny być synchronizowane co klatkę obrazu (przez polling), tylko przetwarzane na zasadzie kolejkowanych eventów. Wyszło mi coś takiego (to póki co prototyp):

Kopiuj
///////////////// Objaśnienia typów:

struct Input
{
	unsigned char Index = 0 
	unsigned char State = 0;
}

InputCleanUp, InputQueue //std::queue<Input>

struct ControlState
{
	bool Pressed = false;
	bool Released = false;
	bool Held = false;
	bool DoubleClicked = false;
};

Control //ControlState[256]

//////////////// Kod właściwy:

void MainLoop()
{
	while (!InputCleanUp.empty())
	{
		switch (InputCleanUp.front().State)
		{
		case 0: //Released
			Control[InputCleanUp.front().Index].Released = false;
			break;
		case 1: //Pressed
			Control[InputCleanUp.front().Index].Pressed = false;
			break;
		case 2: //Double Clicked
			Control[InputCleanUp.front().Index].Pressed = false;
			Control[InputCleanUp.front().Index].DoubleClicked = false;
			break;
		}
		InputCleanUp.pop();
	}

	while (!InputQueue.empty())
	{
		switch (InputQueue.front().State)
		{
		case 0: //Released
			Control[InputQueue.front().Index].Released = true;
			Control[InputQueue.front().Index].Held = false;
			break;
		case 1: //Pressed
			Control[InputQueue.front().Index].Pressed = true;
			Control[InputQueue.front().Index].Held = true;
			break;
		case 2: //Double Clicked
			Control[InputQueue.front().Index].Pressed = true;
			Control[InputQueue.front().Index].DoubleClicked = true;
			Control[InputQueue.front().Index].Held = true;
			break;
		}
		InputCleanUp.push({ InputQueue.front().Index, InputQueue.front().State });
		InputQueue.pop();
	}

	//Update Game Logic
	//Render Frame
}

W WinProc obsługuję komunikaty i przy każdym WM_KEYDOWN, WM_KEYUP i odpowiednich BUTTONDBLCLK (dwuklik myszy) wrzucam do kolejki kod klawisza (z przedziału 0-255) i status (równy 0, 1 lub 2):

Kopiuj
void Controls::AddInput(unsigned char uchIndex, unsigned char uchState)
	{
		InputQueue.push({ uchIndex, uchState });
	}

A czemu dwie kolejki? Flagi Pressed i Released mogą być zapalone tylko na czas jednej klatki (działają jak jednorazowe eventy) i po jej zakończeniu muszą zostać zgaszone, czym zajmuje się właśnie druga kolejka InputCleanUp, resetująca odpowiednie flagi.

Problem w tym, że główny MessageLoop z WinApi (odpowiedziany za obsługę windowsowych komunikatów) i mój MainLoop to dwa różne wątki, a powyższy prototyp nie jest thread-safe i potrzebuję porady jak to rozwiązać. Wiem, że mógłbym walnąć jakiegoś mutexa, ale boję się, że wtedy wątki będą się wzajemnie spowalniać (bo jeden będzie czekał na drugi) i przez to silnik będzie gubił klatki. A może zupełnie biorę się za to od złej strony i powinienem to zaprojektować inaczej?

Byłbym bardzo wdzięczny za wszelkie sugestie.

edytowany 1x, ostatnio: Crow
_13th_Dragon
Czemu zwyczajnie nie zrobisz przegląd komunikatów przed obróbką w głównym MessageLoop?
CR
W jakim sensie przegląd?
_13th_Dragon
To znaczy że przynajmniej część obróbki puścić w MessageLoop, z tym że musisz mieć jakiegoś lock'a bo dodanie i usuwanie z kolejki z różnych wątków bez lock'a ...
CR
No właśnie nie wydaję mi się to dobrym pomysłem, bo zmiana statusu klawisza końcowego (tego, na bazie którego dokonywany jest update logiki gry), musi następować zawsze w tym samym momencie, tj. na początku klatki. Natomiast komunikaty jak WM_KEYDOWN czy WM_KEYUP mogą się pojawić w każdym momencie klatki (po wciśnięciu bądź odpuszczeniu klawisza). A innej obróbki niż przestawianie flag klawisza końcowego Control tu nie ma. Co do lock'a to się zgadzam, dlatego stworzyłem ten wątek.
_13th_Dragon
  • Rejestracja:ponad 19 lat
  • Ostatnio:2 miesiące
2

Weź przed switchami pobierz zawartości
InputCleanUp.front().State oraz InputCleanUp.front().Index do zmiennych lokalnych.


Wykonuję programy na zamówienie, pisać na Priv.
Asm/C/C++/Pascal/Delphi/Java/C#/PHP/JS oraz inne języki.
CR
W prawdziwym kodzie tak robię, tutaj celowo wrzuciłem najbardziej minimalistyczną wersję, żeby nie zaciemniać problemu dodatkowymi linijkami kodu i skupić się tylko na tym, co faktycznie jest istotne.
_13th_Dragon
@kq czy można prosić o bana dla hater'a @13thTroll_4thProgrammer wraz z adresem IP, podejrzewam że @Crow poleci również
AL
  • Rejestracja:prawie 11 lat
  • Ostatnio:prawie 3 lata
  • Postów:1493
0

Nie do końca łapię koncepcję dwóch kolejek, imho mógłbyś w wątku klawiatury zrobić stosowny preprocessing i z głowy: masz znać stan klawiszy na czas kliku timera, a co się dzieje pomiędzy niezbyt powinno pętlę zdarzeń obchodzić imho.
Nie wiem też na ile to wykonalne, ale rozsądne zdaje się być odpytanie o stan inputu w danym ticku zegara, tzn. „zatrzaskujesz” stan klawiatury/myszki na zboczu i ew. porównujesz z poprzednim. Koniec, jak zrobisz to odpowiednio szybko - gracz nie zauważy. Wtedy temat problem wątków znika i ogólnie tematu nie ma, pytanie tylko czy możesz to robić jednowątkowo (oraz czy ma to sens, bo może syscall do czytania klawiatury trwa na tyle długo, że popsuje to fpsy).

edytowany 2x, ostatnio: alagner
CR
  • Rejestracja:ponad 16 lat
  • Ostatnio:11 miesięcy
0

@alagner:
Dlaczego dwie kolejki? Żeby w kodzie samej gry nie trzeba było wprowadzać dodatkowych flag. Bo wyobraź sobie, że chcesz zrobić coś takiego:

Kopiuj
if (Control[Key::VK_RETURN].Pressed) OpenInventoryWindow(); //otwiera okno z przedmiotami

Gdybym odczytywał stan klawisza tylko z danego "ticku" (lub jak ja to wolę określać: z danej klatki), to ta funkcja, mimo wciśnięcia klawisza tylko raz, zostałaby wywołana od kilku do kilku tysięcy razy (zależnie od ilości klatek na sekundę i interwałów między nimi). A to dlatego, że człowiek nie jest fizycznie w stanie nacisnąć klawisza tylko raz i nawet przy frame locku na 60/30 FPS jedno naciśnięcie zostałoby rozciągnięte na wiele kolejnych klatek, a co za tym idzie, w każdej takiej klatce okno zostałoby otwarte na nowo. Żeby temu zapobiec, należałoby już w logice gry dodać sobie flagę w stylu IsInventoryWindowOpened i sprawdzać ją przed każdym wywołaniem funkcji OpenInventoryWindow. No i takich flag trzeba by tworzyć i testować mnóstwo, dziesiątki, może setki (praktycznie dla każdej akcji przypisanej do jakiegoś inputu). Żeby tego uniknąć, wzorując się na systemie zaproponowanym przez David'a Barr'a, znanego jako Javidx9, zastosowałem rozwiązanie, w którym flagi Pressed, DoubleClicked i Released mają zagwarantowaną jednorazowość (czyli 1 naciśnięcie = 1 klatka = 1 wywołanie funkcji), a potem zostają zresetowane.

Jedna kolejka przetwarza zatem input i zapala odpowiednie flagi (dając szansę by na nie zareagować w logice gry), druga zwalnia flagi na początku kolejnej klatki, by nie dopuścić do kolejnych wywołań na ich podstawie. A gdyby potrzebny był faktyczny stan klawisza w czasie rzeczywistym (wciśnięty lub wyciśnięty), to od tego właśnie jest flaga Held.

edytowany 2x, ostatnio: Crow
FA
  • Rejestracja:ponad 3 lata
  • Ostatnio:ponad 3 lata
  • Postów:2
0

Możesz też poluzować zależności na wątkach.

  1. Te dwie kolejki - od klawiszy - dać na aktualizację lockami.
  2. Update Game Logic - cokolwiek to oznacza robić tak jak jest.
  3. Rendering frama dać w osobnym wątku.

Wtedy dane do renderingu wrzucasz w ostatniej chwili.
Jeśli danych zależnych od renderingu jest dużo to robisz płytką kopię obiektu i podmieniasz części (te które się zmieniły), aby nie blokować renderingu.

Tak zbudowana aplikacja powinna być banalnie prosta do testowania.

AL
  • Rejestracja:prawie 11 lat
  • Ostatnio:prawie 3 lata
  • Postów:1493
0

@Crow:

Gdybym odczytywał stan klawisza tylko z danego "ticku" (lub jak ja wolę określać: z danej klatki), to ta funkcja zostałaby wywołana od kilku do kilku tysięcy razy (zależnie od FPS).

Po to robisz event loopa, żeby to nie było zależne od fps (chyba)?

Żeby tego uniknąć, wzorując się na systemie zaproponowanym przez David'a Barr'a, znanego jako Javidx9, zastosowałem rozwiązanie, w którym flagi Pressed, DoubleClicked i Released mają zagwarantowaną jednorazowość (czyli 1 naciśnięcie = 1 klatka = 1 wywołanie funkcji), a potem zostają zresetowane.

No ale czekaj, masz stan obecny inputu (zakładam, że jesteś w ticku timera) oraz stan poprzedni (z zeszłego ticku). Czytasz czy klawisz jest "zwolniony" (powiedzmy false) czy "wciśnięty" (true).
No to wiesz, że:

Kopiuj
Pressed=!prev&&current;
Released=prev&&!current;
Held=current; //aczkolwiek jak by zrobił prev&&current, czy tam zliczasz ile cykli current==true;

i kombinacje alpejskie z dwiema kolejkami odpadają.

EDIT: przy czym ja bym jednak polecał sprawdzać czy isInventoryOpen zamiast odnosić się do globalnego stanu klawiatury.
Pomyśl o tym tak: będziesz chciał... nie wiem, powiedzmy wzorem UT, ładować dodatkową rakietę kiedy fire jest przytrzymany przez 2 sek., ale odpalić wszystkie po 10 "no matter what" - to co, lepiej zrobić "heldFor5secs" i "heldFor10Secs", jako dwa globalne eventy czy może lepiej zliczać te cykle dla broni? Łatwiej będzie tryby przełączać lokalnie.

edytowany 1x, ostatnio: alagner
_13th_Dragon
  • Rejestracja:ponad 19 lat
  • Ostatnio:2 miesiące
0
Crow napisał(a):

@alagner:

Kopiuj
if (Control[Key::VK_RETURN].Pressed) OpenInventoryWindow(); //otwiera okno z przedmiotami

Gdybym odczytywał stan klawisza tylko z danego "ticku" (lub jak ja to wolę określać: z danej klatki), to ta funkcja, mimo wciśnięcia klawisza tylko raz, zostałaby wywołana od kilku do kilku tysięcy razy.

A jeżeli już to okienko Inventory się pojawiło to jaka ma być reakcja na ten RETURN?


Wykonuję programy na zamówienie, pisać na Priv.
Asm/C/C++/Pascal/Delphi/Java/C#/PHP/JS oraz inne języki.
CR
Chociażby zamknięcie okna (w grach ekwipunek zazwyczaj otwiera się i zamyka tym samym klawiszem).
_13th_Dragon
Ok, czyli rozumiem że po odwołaniu się do Control[Key::VK_RETURN].Pressed które zwróci true i odpali inventory kolejne odwołanie do Control[Key::VK_RETURN].Pressed w tej samej klatce musi zwrócić false bo inaczej zamknie to inventory?
CR
  • Rejestracja:ponad 16 lat
  • Ostatnio:11 miesięcy
0

@alagner:

alagner napisał(a):

No ale czekaj, masz stan obecny inputu (zakładam, że jesteś w ticku timera) oraz stan poprzedni (z zeszłego ticku). Czytasz czy klawisz jest "zwolniony" (powiedzmy false) czy "wciśnięty" (true).
No to wiesz, że:

Kopiuj
Pressed=!prev&&current;
Released=prev&&!current;
Held=current; //aczkolwiek jak by zrobił prev&&current, czy tam zliczasz ile cykli current==true;

i kombinacje alpejskie z dwiema kolejkami odpadają.

No ale przecież wtedy musiałbym wrócić do pollingu (sprawdzać każdy klawisz po kolei), co do którego ustaliliśmy, że nie jest najlepszym rozwiązaniem, bo wymaga odpytywania całej klawiatury na ślepo przy każdej klatce (porównując stany klawisza z obecnej i poprzedniej klatki).

EDIT: przy czym ja bym jednak polecał sprawdzać czy isInventoryOpen zamiast odnosić się do globalnego stanu klawiatury.

Globalnego? Z tego co wiem, windowsowe komunikaty są indywidualizowane dla każdego wątku, więc to stan klawiatury tylko dla mojego silnika...?

Pomyśl o tym tak: będziesz chciał... nie wiem, powiedzmy wzorem UT, ładować dodatkową rakietę kiedy fire jest przytrzymany przez 2 sek., ale odpalić wszystkie po 10 "no matter what" - to co, lepiej zrobić "heldFor5secs" i "heldFor10Secs", jako dwa globalne eventy czy może lepiej zliczać te cykle dla broni? Łatwiej będzie tryby przełączać lokalnie.

No ale czy właśnie moja metoda nie sprzyja takiemu rozwiązaniu bardziej?

  1. Odbiera flagę Pressed i uruchamia timer zliczający czas przytrzymania klawisza.
  2. Sprawdza stan timera przy każdym ticku (klatce).
  3. Sprawdza flagę Released, jeżeli jest zapalona, zatrzymuje timer i podejmuje odpowiednie kroki zależnie od jego stanu (np. wystrzeliwuje 3 rakiety) i resetuje timer.
  4. Jeżeli nie wykryto Released, dokłada rakietę gdy stan timera wynosi 2s, 4s, 6s, 8s albo 10s.
  5. Gdy stan timera = 10s, a po drodze nie wykryto Released wypuszcza wszystkie rakiety, deaktywuje timer i resetuje go.

A bez mojego rozwiązania wyglądałoby to tak:

  1. Obiera flagę Pressed i sprawdza czy timer już działa (np. odczytując dodatkową flagę IsRocketTimerAlreadyActive). Jeżeli pomocnicza flaga nie jest aktywna, uruchamia timer i ją zapala, jeżeli jest, nie robi nic.
  2. Sprawdza stan timera przy każdym ticku (klatce).
  3. Sprawdza flagę Released, jeżeli jest zapalona, sprawdza, czy była też zapalona w ostatniej klatce (np. odczytując dodatkową flagę WasRocketAlreadyReleased). Jeżeli pomocnicza flaga była zapalona w ostatniej klatce, nie robi nic, jeżeli nie była, zatrzymuje timer i podejmuje odpowiednie kroki zależnie od jego stanu (np. wystrzeliwuje 3 rakiety), następnie zapala pomocniczą flagę i resetuje timer.
  4. Jeżeli flaga Released nie była zapalona w ostatniej klatce i nie jest zapalona teraz, dokłada rakietę gdy stan timera wynosi 2s, 4s, 6s, 8s albo 10s.
  5. Gdy stan timera = 10s, a po drodze nie wykryto Released wypuszcza wszystkie rakiety, deaktywuje timer i resetuje go.

Czy ta druga metoda nie jest, zupełnie niepotrzebnie, bardziej skomplikowana (wymaga dodatkowego sprawdzania flag i stosowania flag pomocniczych)?

edytowany 7x, ostatnio: Crow
CR
  • Rejestracja:ponad 16 lat
  • Ostatnio:11 miesięcy
0

@faffik:

Myślałem o tym, ale jak szkielet takiego rozwiązania miałby wyglądać? Coś tam ogarniam w zakresie wielowątkowości, ale nie aż tak dużo i nie jestem pewien jak powinien działać taki system. Do głowy przychodzi mi coś w tym stylu:.

Kopiuj
std::mutex M;
std::condition_variable CV;
std::atomic<bool> ThreadAlive;

void InputLoop()
	{
		std::unique_lock<std::mutex> UL(M);

		while (ThreadAlive)
		{
			while (!InputQueue.empty())
			{
				//Obróbka klawisza
				InputQueue.pop();
			}

			CV.wait(UL);
		}	
	}
};

void MainLoop()
{
	CV.notify_one(); //Wybudza wątek obrabiający klawisze, wątek opróżnia kolejkę i znowu zasypia, aż do następnej klatki.
	//Update Game Logic
	//Render Frame - ewentualnie to też do osobnego wątku, ale po kolei, najpierw muszę uporać się z inputami :).
}

To właściwy trop? Byłbym bardzo wdzięczny za jakieś wskazówki, choćby w formie pseudo kodu.

A może wybudzanie powinno następować na po

edytowany 10x, ostatnio: Crow
AL
  • Rejestracja:prawie 11 lat
  • Ostatnio:prawie 3 lata
  • Postów:1493
0

@Crow: googlasz i masz:
https://stackoverflow.com/a/16075550/4885321
Analogiczna implementacja jest w C++ Concurrency in action Williamsa.

edytowany 1x, ostatnio: alagner
FA
  • Rejestracja:ponad 3 lata
  • Ostatnio:ponad 3 lata
  • Postów:2
0

@Crow
Bardziej chodziło mi tutaj o niezależne działanie.
Model synchronizacji może być jeden (jedna baza kodowa - zalecane rozwiązanie - mniej testowania i z reguły łatwiejsze w obsłudze i implementacji).

Nie wiem jak wygląda Twój pozostały kod.
Jeśli dobrze się domyślam to powinieneś mieć coś takiego jak (dzieląc to na niezależne moduły)
Struktura:

  1. Odczyt klawiszy (te dwie kolejki)
  2. Reakcja logiki
  3. Rendering sceny

Działanie:

  1. Odczyt klawiszy
  2. Odczytane klawisze trafiają do logiki - tam następuje przetwarzanie
  3. Logika generuje dane / obiekty które mają być wyrenderowane
  4. Klasa renderująca pobiera te dane i renderuje klatkę

Miałem na myśli, aby odczyt klawiszy żył własnym życiem i wysyłał dane do logiki.
Logika żyje własnym życiem i generuje dane do renderingu
Rendering żyje własnym życiem - pobiera dane (do renderingu) takie jakie ma lub czeka niewielki czas, jeśli akurat trafiła na moment aktualizacji danych. Jeśli dane nie są aktualizowane (nie są gotowe lub się nie zmieniły) to rendering renderuje normalnie - olewając co się dzieje w pozostałych częściach.

Wtedy między odczytem klawiszy a logiką masz jednego locka do update danych (jeden mutex - tylko na czas aktualizacji),
Pomiędzy logiką a danymi do renderingu masz jednego locka (ten sam schemat)
Rendering odwołuje się do tego samego locka co aktualizacja danych przez logikę - lub innego to zależy co tam masz.

Jeśli aktualizujesz niewielkie ilości danych lub robisz płytką kopię / podmieniasz / swapujesz, problem z blokowaniem się wątków (wydajnością) nie powinien wystąpić (po prostu nie ma co ich zablokować, operacje są bardzo szybkie)

W najprostszym wypadku potrzebujesz tylko jednego mutexa na każde połączenie.
Logika może mieć waita, natomiast rendering raczej nie powinien mieć waita.

CR
Ale uważasz, że przepisywanie klawiszy z kolejki do tablicy powinno działać w osobnym wątku, czy wystarczy w MainLoop, na początku każdej klatki, z użyciem locka?
CR
  • Rejestracja:ponad 16 lat
  • Ostatnio:11 miesięcy
0

@faffik

No właśnie zastanawiam się, co będzie najkorzystniejsze (póki co skupiam się na rozwiązaniu kwestii klawiszy). Bo mógłbym to (chyba?) zrobić tak, że odświeżanie klawiszy (przepisywanie ich z kolejki do tablicy) mogłoby być obsługiwane przez osobny wątek i wymieniać się dostępem z wątkiem głównym, tj., tym odpowiedzialnym za windowsowy MessageLoop:

Kopiuj
//Pseudo kod:

//Funkcja używana przez główny wątek:
void AddInputToQueue()
{
	InputMutex.lock();
	//dodaje klawisz do kolejki
	InputMutex.unlock();
}

//Funkcja używana przez wątek do obróbki klawiszy:
void HandleTheQueue()
{
	InputMutex.lock();
	//przepisuje klawisze do tablicy, opróżnia kolejkę
	InputMutex.unlock();
}

Wtedy MainLoop (nie biorący bezpośredniego udziału w obsłudze klawiszy) mógłby zachowywać się na dwa sposoby:

Kopiuj
void MainLoop()
{
	InputMutex.lock();
	//Update Game Logic
	InputMutex.unlock();
	//Render Frame
}

Czyli wtedy odświeżanie klawiszy byłoby wstrzymywane na czas odświeżania logiki gry. Minus byłby taki, że wtedy czasem to logika gry musiałaby czekać na uwolnienie mutexa, żeby móc go przejąć (co mogłoby powodować jakieś tam opóźnienia czy nawet obniżać FPS. Tylko czy to w ogóle mogłoby być znaczące? W końcu ile może trwać przepisane 5-10 elementów, po 2 bajty każdy, z kolejki do tablicy?).

Alternatywnie MainLoop mógłby to robić tak:

Kopiuj
void MainLoop()
{
	//CanUpdate = std::atomic<bool>

	CanUpdate = false;
	//Update Game Logic
	CanUpdate = true;
	//Render Frame
}

Wtedy po prostu MainLoop zapalałby flagę, która blokowałaby przepisywanie kolejki do tablicy (ale nowe inputy nadal mogłyby być dodawane do kolejki). Tutaj minus byłby z kolei taki, że czasem nie wszystkie inputy dałyby radę złapać się na jedną klatkę. Np. zakolejkowane było 5, ale do tablicy udało się wpisać tylko 3, więc pozostałe 2 będą przepisane dopiero później (przy kolejnej klatce).

No i pytanie, która opcja jest lepsza? A może w ogóle źle kombinuję?

A jeżeli opcja 1 jest ok (ta z twardym blokowaniem mutexem), to może w ogóle nie warto tworzyć osobnego wątku dla odświeżania klawiszy (przepisywania ich z kolejki do tablicy), tylko zostawić to w MainLoopie (tak jak zaproponowałem w pierwszym poście) ale z dołożonym mutexem (co spowalniałoby kolejkowanie nowych klawiszy na czas blokady mutexem, ale to chyba akurat niewielki problem)? Bo to chyba wyjdzie na to samo?

edytowany 6x, ostatnio: Crow
AL
  • Rejestracja:prawie 11 lat
  • Ostatnio:prawie 3 lata
  • Postów:1493
1

@Crow pozwolę sobie jeszcze na chwilę odejść od tematu wątków, wrócić do released/pressed i Twojego wcześniejszego posta: jak chcesz o tym konkretnie pogadać to załóż nowy temat, bo tu się może bajzel zrobić:

ja generalnie skłaniałbym się do opcji nr 2, bo enkapsualacja lepsza a większa dostępność do software'owych timerów to jest wartość, nie kompilkacja. IsRocketTimerAlreadyActive jest polem wewnętrznym wyrzutni/gracza. Ad.3 - nie czytasz dodatkowych flag tylko wykorzystujesz maszynę stanów (przykładowa implementacja tu: https://gameprogrammingpatterns.com/state.html), możesz nawet zrobić hierarchiczną.
Rozrysuj sobie graf przejść-wyjść poszczególnych elementów gry, to Ci się to uprości. Fakt, można przegiąć i czasem flaga będzie lepsza, ale dla bardziej skomplikowanych sekwencji ten wzorzec Ci się opłaci.
Odnoszę wrażenie, że bardzo starasz się pracować na zmiennych globalnych/"paraglobalnych". A jedyne co jest globalne (w idealnym przypadku) to stan inputu i może ekranu.

edytowany 1x, ostatnio: alagner
Zobacz pozostały 1 komentarz
AL
@Crow "A bez mojego rozwiązania wyglądałoby to tak:"
CR
Ale przecież to różnica na zasadzie jedna kolejka vs dwie kolejki, to aż tak znaczące?
AL
@Crow: imho to jest różnica w całej architekturze jak się temu szerzej przyglądnąć.
CR
Jak rozumiem, rozmawiamy o 2 modelach, w jednym flagi są gaszone na końcu kolejnej klatki (czym zajmuje się osobna kolejka), w drugim modelu jest tylko jedna kolejka, a klawisze są binarne (albo wciśnięty albo wyciśnięty). Między nimi jest jakaś kolosalna różnica? Obecność tej drugiej kolejki wpływa jakoś na wydajność? Bo działanie samej pętli MainLoop się chyba jakoś nie zmienia? Dopytuję, bo serio nie rozumiem.
AL
A możesz wrzucić więcej kodu? Trochę trudno o tym rozmawiać bez tego. Wydajność jest zawsze relatywna do czegoś, niemniej: na ile rozumiem Twój model dwóch kolejek, do każdej z nich ładuje wątek czytania klawiatury. No to masz dwa mutexy. Do tego: w Twoim modelu masz ileś flag odpowiadających za stan gry. Ja proponuję trzymać je lokalnie na ile to możliwe.
CR
  • Rejestracja:ponad 16 lat
  • Ostatnio:11 miesięcy
0

@alagner:
No ja bym to widział tak jak pisałem w pierwszym poście i poście powyżej:

Kopiuj
//Pseudo kod:

void MainLoop()
{
    InputMutex.lock();
    //Kolejka czyszcząca flagi klawiszy
    //Kolejka przepisująca klawisze do tablicy Control[256] (zawierającej structa z flagami Pressed, Released, Double Clicked i Held)...
    //...i dodająca elementy do kolejki czyszczącej
    //Odświeżenie logiki gry (na bazie tablicy).
    InputMutex.unlock();
    //Renderowanie klatki obrazu
}

Jak rozumiem, model preferowany przez ciebie wyglądałby identycznie, tylko zamiast dwóch kolejek byłaby jedna, a zamiast 4 flag byłyby 2 - Up i Down, które byłyby odświeżane tylko w momencie kliknięcia bądź odpuszczenia klawisza. Zgadza się czy ja coś źle rozumiem?

edytowany 6x, ostatnio: Crow
_13th_Dragon
  • Rejestracja:ponad 19 lat
  • Ostatnio:2 miesiące
1

Zastanów się nad:

Kopiuj
#include <algorithm>
#include <iostream>
#include <vector>
#include <bitset>
using namespace std;

class KeySlower;

enum KeyState { Pressed,Realized,Held,BitSize };

class CallList
{
	private:
	vector<KeySlower*> list;
	CallList() {}
	public:
	static CallList *inst()
	{
		static CallList cs;
		return &cs;
	}
	void add(KeySlower *ks) { list.push_back(ks); }
	void del(KeySlower *ks) 
	{
		vector<KeySlower*>::iterator end=remove(list.begin(),list.end(),ks);
		list.resize(end-list.begin());
	}
	void keyinvoke(int key,KeyState st)const;
	void nextframe()const;
};

class KeySlower
{
	private:
	int key;
	bitset<KeyState::BitSize> state,nextstate;
	public:
	KeySlower(int key):key(key) { CallList::inst()->add(this); }
	~KeySlower() { CallList::inst()->del(this); }
	void keyinvoke(int key,KeyState st)
	{
		if(key!=this.key) return;
		nextstate.set(st);
		// pozostała logika
	}
	void nextframe() { state=nextstate; }
	bool presed()const { return state.test(KeyState::Pressed); }
	bool realized()const { return state.test(KeyState::Realized); }
	bool held()const { return state.test(KeyState::Held); }
};

void CallList::keyinvoke(int key,KeyState st)const { for(KeySlower *ks:list) ks->keyinvoke(key,st); }
void CallList::nextframe()const { for(KeySlower *ks:list) ks->nextframe(); }


int main()
{
	KeySlower ret(13); // lapiemy enter
	KeySlower esc(27); // lapiemy escape
	
	CallList::inst()->keyinvoke(60,KeyState::Pressed); // Wywolania pętli WinMain
	CallList::inst()->keyinvoke(13,KeyState::Pressed); // Wywolania pętli WinMain
	CallList::inst()->keyinvoke(27,KeyState::Pressed); // Wywolania pętli WinMain
	cout<<"ret.presed() == "<<ret.presed()<<endl;
	cout<<"esc.presed() == "<<esc.presed()<<endl;
	CallList::inst()->nextframe(); // Wywolanie przy starcie klatki
	cout<<"ret.presed() == "<<ret.presed()<<endl;
	cout<<"esc.presed() == "<<esc.presed()<<endl;
	
	return 0;
}

Wtedy obrabiasz wyłącznie "zarejestrowane klawisze", ba żadnych locków i/lub dodatkowych wątków.


Wykonuję programy na zamówienie, pisać na Priv.
Asm/C/C++/Pascal/Delphi/Java/C#/PHP/JS oraz inne języki.
edytowany 1x, ostatnio: _13th_Dragon
Zobacz pozostałe 6 komentarzy
CR
Nie co klatka, tylko przez dwie kolejne klatki po zmianie statusu klawisza. W pierwszej klatce klawisz zostaje przepisany z kolejki to tablicy. W drugiej kolejka "czyści" flagi ustawione w poprzedniej klatce. I tyle, potem wątek zasypia i czeka na wybudzenie (kolejne inputy). Przy twoim rozwiązaniu polling - choćby nawet jednego klawisza - jest dużo bardziej obciążający, bo nawet bez jakichkolwiek akcji na klawiaturze, klawisz musi być cały czas testowany i aktualizowany. A niech się zbierze więcej klawiszy do testowania, to już w ogóle będzie kupa.
_13th_Dragon
Nie! Nawet komentarzy nie przeczytałeś!
CR
Jak nie? Przecież sam napisałeś Co klatka przypisuje się jeden int do drugiego int'a tyle razy ile "zamówiono" klawiszy. A u mnie nic nie dzieje się "co klatka", a jedynie w razie potrzeby (po wykryciu nowego klawisza w kolejce), przez 2 kolejne klatki. Natomiast w braku akcji na klawiaturze, nie dzieje się zupełnie nic (kolejki są puste, wątek śpi).
_13th_Dragon
Tak w void CallList::nextframe()const { for(KeySlower *ks:list) ks->nextframe(); } aby zrobić bezsensowną rzecz do bezsensowności której nie dajesz się przekonać, bo jak za trudne pytanie już nie odpowiadasz w komentarzach. Dobra widzę masz wciąż zakuty łeb, pogadamy kiedy skończysz i nie będzie działać jak sobie myślałeś że będzie :D
CR
Koniec, z chamem, gburem i prostakiem rozmawiać nie będę. Nie dopisuj się więcej w moich wątkach, bo zarówno ty, jak i twój żałosny kod będziecie ignorowani. Żegnam.
CR
  • Rejestracja:ponad 16 lat
  • Ostatnio:11 miesięcy
0

Żeby nie zostawiać wątku bez rozwiązania (może ktoś kiedyś na niego trafił), ostatecznie zdecydowałem się na coś takiego:

Kopiuj
void MainLoop()
{
	Controls.Update(); //<-- Synchronizuje kolejkę klawiszy z tablicą
	//Update Game Logic
	Controls.CleanUp(); //<-- Czyści flagi
	//Draw Frame
}

Sama kolejka wygląda tak:

Kopiuj
struct Input
{
	unsigned char Index;
	unsigned char State;
	void Set(const unsigned char& uiIndex, const unsigned char& uchState)
	{
		Index = uiIndex;
		State = uchState;
	}
};

class InputQueue
{
private:
	Input DataA[256], DataB[256];
	Input* Gather = DataA;
	Input* Process = DataB;
	std::mutex Mutex;
public:
	unsigned char& GatherSize()
	{
		return Gather[0].Index; //Wielkość kolejki (ilość elementów) zapisywana jest w pierwszym indexie, w zmiennej... Index.
	}
	unsigned char& ProcessSize()
	{
		return Process[0].Index; //Wielkość kolejki (ilość elementów) zapisywana jest w pierwszym indexie, w zmiennej... Index.
	}
	void Swap()
	{
		std::lock_guard<std::mutex> Guard(Mutex);
		std::swap(Gather, Process);
		Gather[0].Index = 0;
	}
	void AddInput(const unsigned int& uiIndex, unsigned char uchState)
	{
		std::lock_guard<std::mutex> Guard(Mutex);
		Gather[++Gather[0].Index].Set(uiIndex, uchState);
	}
	void AddSpecialInput(const unsigned int uiIndex, const unsigned char uchState);
	{
		Process[++Process[0].Index].Set(uiIndex, uchState);
	}
	Input* begin()
	{
		return &Process[1];
	}
	Input* end();
	{
		return Process + (Process[0].Index + 1);
	}
};


void Controls::Update()
{
	if (Queue.GatherSize())
	{
		Queue.Swap();
		for (Input& I : Queue)
		{
			switch (I.State)
			{
			case 0: //Released
				Control[I.Index].R = true;
				Control[I.Index].H = false;
				break;
			case 1: //Pressed
				Control[I.Index].P = true;
				Control[I.Index].H = true;
				break;
			case 2: //Double Clicked
				Control[I.Index].P = true;
				Control[I.Index].S = true;
				Control[I.Index].H = true;
				break;
			}
				if (I.Index == 0x2C) Control[I.Index].S = false;
		}
	}

	//Print Screen

	if ((GetKeyState(0x2C) & 0x80) && !Control[0x2C].S)
	{
		Control[0x2C].P = true;
		Control[0x2C].H = true;
		Control[0x2C].S = true;
		Queue.AddSpecialInput(0x2C, 1);
	}

	//Toggle Keys

	if (GetKeyState(0x14) & 0x1) //CapsLock Toggle
	{
		if (!Control[0x14].S) Control[0x14].S = true;
	}
	else
	{
		if (Control[0x14].S) Control[0x14].S = false;
	}

	if (GetKeyState(0x90) & 0x1) //NumLock Toggle
	{
		if (!Control[0x90].S) Control[0x90].S = true;
	}
	else
	{
		if (Control[0x90].S) Control[0x90].S = false;
	}

	if (GetKeyState(0x91) & 0x1) //ScrollLock Toggle
	{
		if (!Control[0x91].S) Control[0x91].S = true;
	}
	else
	{
		if (Control[0x91].S) Control[0x91].S = false;
	}
}

void Controls::CleanUp()
{
	if (Queue.ProcessSize())
	{
		for (Input& I : Queue)
		{
			switch (I.State)
			{
			case 0: //Released
				Control[I.Index].R = false;
				break;
			case 1: //Pressed
				Control[I.Index].P = false;
				break;
			case 2: //Double Clicked
				Control[I.Index].P = false;
				Control[I.Index].S = false;
				break;
			}
		}
	}
}

Kolejka składa się z dwóch statycznych tablic, do których dostęp można uzyskać poprzez wskaźniki. Jedna tablica służy tylko do zbierania nowych inputów (dodawanych do listy przez windowsowe komunikaty jak WM_KEYDOWN czy WM_KEYUP), a druga do ich faktycznej obróbki (konwertowania inputów na właściwe flagi). Sztuczka polega na tym, że na początku każdej klatki wskaźniki są zamieniane (funkcja swap()) i jest to jedyny, krótki moment, który wymaga zamknięcia mutexem, w pozostałej części tablice działają thread-safe. Nie ma też już przepisywania danych z jednej tablicy do drugiej (jak wcześniej), bo teraz jedna wystarczy do ustawienia stosownych flag na początku klatki, a potem ich zresetowania na końcu tej samej klatki. Dodatkowo kolejki nie są czyszczone (zerowane), a jedynie nadpisuje się ich wcześniejsza zawartość (aktualizowana jest zmienna przechowująca ilość elementów, ukryta w pierwszym indexie), więc powinny być raczej szybkie.

Print Screen jest "dziwnym" klawiszem, bo nie wysyła komunikatu WM_KEYDOWN i trzeba go testować ręcznie, w każdej klatce z osobna, a potem porównywać stan z ostatniej i obecnej klatki. Wykorzystuję do tego flagę S (specjalną), która normalnie służy do przechowywania dodatkowych informacji o specjalnych klawiszach (podwójne kliknięcie w przypadku przycisków myszy, albo też stan kontrolki w przypadku klawiszy typu LOCK - Scroll, Num i Caps). Zwykłe klawisze jej nie używają (nie jest w ogóle widoczna), więc mogłem się nią posłużyć.

Gdyby ktoś miał jakieś uwagi, chętnie wysłucham. Dziękuję też wszystkim, którzy udzielili mi konstruktywnej pomocy i wskazówek.

edytowany 5x, ostatnio: Crow
_13th_Dragon
  • Rejestracja:ponad 19 lat
  • Ostatnio:2 miesiące
0
Kopiuj
    if (GetKeyState(0x90) & 0x1) //NumLock Toggle
    {
        if (!Control[0x90].S) Control[0x90].S = true;
    }
    else
    {
        if (Control[0x90].S) Control[0x90].S = false;
    }

Doprawdy?
Czy zdajesz sobie sprawy że to jest równoważne:
Control[0x90].S = (GetKeyState(0x90) & 0x1);

Czyli 8 długo działających zagmatwanych wierszy zamiast jednego szybkiego i czytelnego?
To samo z resztą tego kodu wyżej.


Wykonuję programy na zamówienie, pisać na Priv.
Asm/C/C++/Pascal/Delphi/Java/C#/PHP/JS oraz inne języki.
edytowany 1x, ostatnio: _13th_Dragon
Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)