Techniki zarządzania danymi wejściowymi w dużych grach

16

Czy istnieje standardowa technika zarządzania danymi wejściowymi w dużych grach. Obecnie w moim projekcie cała obsługa danych wejściowych odbywa się w pętli gry, podobnie jak:

while(SDL_PollEvent(&event)){
            switch(event.type){
                case SDL_QUIT:
                    exit = 1;
                    break;
                case SDL_KEYDOWN:
                    switch(event.key.keysym.sym){
                        case SDLK_c:
                            //do stuff
                            break;
                    }
                    break;
                case SDL_MOUSEBUTTONDOWN:
                    switch(event.button.button){
                        case SDL_BUTTON_MIDDLE:
                                //do stuff
                                break;
                            }
                    }
                    break;
            }

(Używam SDL, ale spodziewam się, że główna praktyka stosuje również biblioteki i frameworki). W przypadku dużego projektu nie wydaje się to najlepszym rozwiązaniem. Mogę mieć kilka obiektów, które chcą wiedzieć, co nacisnął użytkownik, więc byłoby bardziej sensowne, gdyby te obiekty obsługiwały dane wejściowe. Jednak nie wszystkie obsługują dane wejściowe, ponieważ po otrzymaniu zdarzenia zostanie ono wypchnięte z bufora zdarzeń, więc inny obiekt nie otrzyma danych wejściowych. Jaką metodę najczęściej stosuje się, aby temu przeciwdziałać?

w4etwetewtwet
źródło
Za pomocą menedżera zdarzeń możesz uruchomić zdarzenie wejściowe i pozwolić, aby wszystkie inne części gry się do nich zarejestrowały.
danijar
@danijar, co dokładnie masz na myśli przez menedżera wydarzeń, czy możliwe jest dostarczenie szkieletowego pseudo-kodu, aby pokazać, o czym mówisz?
w4etwetewtwet
1
Napisałem odpowiedź na opracowanie na temat menedżerów zdarzeń, które są dla mnie sposobem na obsługę danych wejściowych.
danijar,

Odpowiedzi:

12

Zapytany przez osobę rozpoczynającą wątek, opracowuję menedżerów wydarzeń. Myślę, że to dobry sposób na radzenie sobie z danymi wejściowymi w grze.

Menedżer zdarzeń to klasa globalna, która pozwala zarówno rejestrować funkcje oddzwaniania do klawiszy, jak i odpalać te oddzwaniania. Menedżer zdarzeń przechowuje zarejestrowane funkcje na prywatnej liście pogrupowanej według ich klucza. Za każdym razem, gdy klucz jest uruchamiany, wszystkie zarejestrowane połączenia zwrotne są wykonywane.

Oddzwonienia mogą być std::functionobiektami, które mogą pomieścić lambdas. Klucze mogą być ciągami znaków. Ponieważ menedżer ma charakter globalny, komponenty aplikacji mogą rejestrować się na klucze uruchamiane z innych komponentów.

// in character controller
// at initialization time
Events->Register("Jump", [=]{
    // perform the movement
});

// in input controller
// inside the game loop
// note that I took the code structure from the question
case SDL_KEYDOWN:
    switch(event.key.keysym.sym) {
    case SDLK_c:
        Events->Fire("Jump");
        break;
    }
    break;

Można nawet rozszerzyć tego menedżera zdarzeń, aby umożliwić przekazywanie wartości jako dodatkowych argumentów. Świetnie nadają się do tego szablony C ++. Można użyć takiego systemu, aby, powiedzmy, dla "WindowResize"zdarzenia przekazać nowy rozmiar okna, tak aby komponenty nasłuchujące nie musiały go pobierać samodzielnie. Może to znacznie zmniejszyć zależności kodu.

Events->Register<int>("LevelUp", [=](int NewLevel){ ... });

W swojej grze zaimplementowałem taki menedżer zdarzeń. Jeśli jesteś zainteresowany, opublikuję link do kodu tutaj.

Za pomocą menedżera zdarzeń możesz łatwo transmitować informacje wejściowe w aplikacji. Co więcej, pozwala to na miły sposób pozwalający użytkownikowi dostosować powiązania klawiszy. Komponenty słuchają zdarzeń semantycznych zamiast kluczy bezpośrednio ( "PlayerJump"zamiast "KeyPressedSpace"). Następnie możesz mieć komponent odwzorowania danych wejściowych, który nasłuchuje "KeyPressedSpace"i wyzwala dowolne działanie użytkownika związane z tym kluczem.

danijar
źródło
4
Świetna odpowiedź, dzięki. Chociaż chciałbym zobaczyć kod, nie chcę go kopiować, więc nie będę prosił cię o opublikowanie go, dopóki nie zaimplementuję własnego, ponieważ w ten sposób dowiem się więcej.
w4etwetewtwet
Pomyślałem o czymś, mogę przekazać dowolną funkcję składową takiego, czy nie będzie funkcja rejestr wziąć AClass :: func, ograniczając go do jednego klas funkcji składowych
w4etwetewtwet
To wielka zaleta wyrażeń lambda w C ++, możesz określić klauzulę przechwytywania, [=]a odniesienia do wszystkich zmiennych lokalnych dostępnych z lambda zostaną skopiowane. Więc nie musisz przekazywać tego wskaźnika ani czegoś takiego. Jednak pamiętać, że nie można przechowywać lambdy z klauzulą przechwytywania w starych wskaźników funkcji C . Jednak C ++ std::functiondziała dobrze.
danijar,
funkcja std :: jest bardzo wolna
TheStatehz
22

Podziel to na kilka warstw.

Na najniższej warstwie masz nieprzetworzone zdarzenia wejściowe z systemu operacyjnego. Wprowadzanie za pomocą klawiatury SDL, wprowadzanie za pomocą myszy, wprowadzanie za pomocą joysticka itp. Możesz mieć kilka platform (SDL jest najmniej powszechnym mianownikiem pozbawionym kilku formularzy wprowadzania, na przykład, o które możesz później dbać).

Możesz je streścić za pomocą niestandardowego typu zdarzenia na bardzo niskim poziomie, takiego jak „przycisk klawiatury w dół” lub podobny. Gdy warstwa platformy (pętla gry SDL) otrzymuje dane wejściowe, powinna utworzyć zdarzenia niskiego poziomu, a następnie przesłać je do menedżera danych wejściowych. Może to robić za pomocą prostych wywołań metod, funkcji zwrotnych, skomplikowanego systemu zdarzeń, co tylko zechcesz.

System wprowadzania ma teraz za zadanie przekształcanie danych wejściowych niskiego poziomu na zdarzenia logiczne wysokiego poziomu. Logika gry wcale nie dba o to, by nacisnąć SPACJĘ. Dba o to, aby JUMP został wciśnięty. Zadaniem menedżera wprowadzania danych jest zbieranie tych zdarzeń wejściowych niskiego poziomu i generowanie zdarzeń wejściowych wysokiego poziomu. Odpowiada za to, że spacja i przycisk pada „A” są odwzorowane na logiczne polecenie Skok. Zajmuje się kontrolkami gamepad vs mysz i tak dalej. Emituje zdarzenia logiczne wysokiego poziomu, które są możliwie jak najbardziej abstrakcyjne z poziomu kontrolek niskiego poziomu (są tu pewne ograniczenia, ale w zwykłym przypadku można całkowicie oderwać rzeczy).

Następnie kontroler postaci odbiera te zdarzenia i przetwarza te wejściowe zdarzenia wysokiego poziomu, aby faktycznie zareagować. Warstwa platformy wysłała zdarzenie „Klawisz spacji w dół”. System wejściowy odebrał to, przegląda tabele mapowania / logikę, a następnie wysyła zdarzenie „Pressed jump”. Logika gry / kontroler postaci odbiera to zdarzenie, sprawdza, czy gracz może faktycznie skoczyć, a następnie emituje zdarzenie „Gracz skoczył” (lub po prostu bezpośrednio powoduje skok), którego reszta logiki używa do robienia czegokolwiek .

Wszystko zależne od logiki gry trafia do kontrolera gracza. Wszystko zależne od systemu operacyjnego przechodzi w warstwę platformy. Cała reszta przechodzi do warstwy zarządzania danymi wejściowymi.

Oto kilka amatorskich dzieł ASCII, aby to opisać:

-----------------------------------------------------------------------
Platform Abstraction | Collect and forward OS input events
-----------------------------------------------------------------------
                          | |
                          | |
                         \   /
                          \_/
-----------------------------------------------------------------------
    Input Manager    | Translate OS input events into logical events
-----------------------------------------------------------------------
                          | |
                          | |
                         \   /
                          \_/
-----------------------------------------------------------------------
Character Controller | React to logical events and affect game play
-----------------------------------------------------------------------
                          | |
                          | |
                         \   /
                          \_/
-----------------------------------------------------------------------
      Game Logic     | React to player actions and provides feedback
-----------------------------------------------------------------------
Sean Middleditch
źródło
Fajna grafika ASCII, ale nie jest to konieczne, przepraszam. Proponuję zamiast tego użyć listy numerowanej. W każdym razie dobra odpowiedź!
danijar,
1
@danijar: Eh, eksperymentowałem, nigdy wcześniej nie próbowałem wyciągnąć odpowiedzi. Więcej pracy niż było warte, ale o wiele mniej pracy niż zajmowanie się programem do malowania. :)
Sean Middleditch
W porządku, zrozumiałe :-)
danijar,
8
Osobiście wolę sztukę ASCII bardziej niż nudną listę z numerami.
Jesse Emond,
@JesseEmond Hej, tu dla sztuki?
danijar