Eingabeverwaltungstechniken in großen Spielen

16

Gibt es eine Standardtechnik zum Verwalten von Eingaben in großen Spielen? Derzeit wird in meinem Projekt die gesamte Eingabebearbeitung in der Spieleschleife ausgeführt:

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;
            }

(Ich verwende SDL, aber ich gehe davon aus, dass die Hauptpraxis auch für Bibliotheken und Frameworks gilt.) Für ein großes Projekt scheint dies nicht die beste Lösung zu sein. Ich kann mehrere Objekte haben, die alle wissen möchten, was der Benutzer gedrückt hat, daher wäre es sinnvoller, wenn diese Objekte Eingaben verarbeiten. Sie können jedoch nicht alle Eingaben verarbeiten, da sie nach dem Empfang eines Ereignisses aus dem Ereignispuffer entfernt werden, sodass ein anderes Objekt diese Eingaben nicht erhält. Mit welcher Methode wird dem am häufigsten entgegengewirkt?

w4etwetewtwet
quelle
Mit einem Event-Manager können Sie ein Ereignis in der Eingabe auslösen und alle anderen Teile Ihres Spiels registrieren lassen.
Danijar
@danijar, was genau meinst du mit einem Event-Manager? Ist es möglich, dass du einen Pseudo-Code bereitstellst, um zu zeigen, von was für einer Sache du sprichst?
w4etwetewtwet
1
Ich habe eine Antwort geschrieben, um die Event-Manager zu erläutern, die für mich der Weg zur Eingabe sind.
Danijar

Antworten:

12

Da ich vom Threadstarter gefragt werde, gehe ich auf Eventmanager ein. Ich denke, das ist eine gute Möglichkeit, mit Eingaben in einem Spiel umzugehen.

Ein Event Manager ist eine globale Klasse, die es ermöglicht, Rückruffunktionen auf Tasten zu registrieren und diese Rückrufe auszulösen. Der Ereignismanager speichert registrierte Funktionen in einer privaten Liste, die nach ihrem Schlüssel gruppiert ist. Jedes Mal, wenn ein Schlüssel abgefeuert wird, werden alle registrierten Rückrufe ausgeführt.

Die Rückrufe können std::functionObjekte sein, die Lambdas enthalten können. Die Schlüssel könnten Strings sein. Da der Manager global ist, können sich Komponenten Ihrer Anwendung für Schlüssel registrieren, die von anderen Komponenten abgefeuert wurden.

// 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;

Sie können diesen Ereignismanager sogar erweitern, um die Übergabe von Werten als zusätzliche Argumente zuzulassen. Dafür sind C ++ - Templates hervorragend geeignet. Sie könnten ein solches System verwenden, um beispielsweise für ein "WindowResize"Ereignis die neue Fenstergröße zu übergeben, damit die Empfangskomponenten es nicht selbst abrufen müssen. Dies kann die Codeabhängigkeiten erheblich reduzieren.

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

Ich habe einen solchen Event Manager für mein Spiel implementiert. Bei Interesse poste ich hier den Link zum Code.

Mithilfe eines Ereignismanagers können Sie auf einfache Weise Eingabeinformationen in Ihrer Anwendung übertragen. Darüber hinaus können Sie auf diese Weise die Tastenbelegung anpassen. Komponenten lauschen semantischen Ereignissen anstelle von Schlüsseln direkt ( "PlayerJump"anstelle von "KeyPressedSpace"). Dann können Sie eine Eingabe-Mapping-Komponente haben, "KeyPressedSpace"die auf jede Aktion wartet und diese auslöst, die der Benutzer an diesen Schlüssel gebunden hat.

danijar
quelle
4
Tolle Antwort, danke. Obwohl ich den Code sehr gerne sehen würde, möchte ich ihn nicht kopieren. Deshalb werde ich Sie nicht bitten, ihn zu veröffentlichen, bis ich meinen eigenen implementiert habe, da ich auf diese Weise mehr erfahren werde.
w4etwetewtwet
Ich habe mir gerade etwas überlegt, kann ich eine Member-Funktion wie diese übergeben oder muss die Register-Funktion nicht AClass :: func verwenden, um sie auf Member-Funktionen einer Klasse zu beschränken
w4etwetewtwet
Das ist das Tolle an Lambda-Ausdrücken in C ++. Sie können eine Capture-Klausel angeben [=]und Referenzen auf alle lokalen Variablen, auf die vom Lambda zugegriffen wird, werden kopiert. Sie müssen diesen Zeiger oder ähnliches also nicht übergeben. Beachten Sie jedoch , dass Sie Lambdas mit Capture-Klausel nicht in alten C-Funktionszeigern speichern können . Das C ++ std::functionfunktioniert jedoch einwandfrei.
Danijar
STD :: Funktion ist sehr langsam
TheStatehz
22

Teilen Sie dies in mehrere Ebenen.

Auf der untersten Ebene haben Sie unformatierte Eingabeereignisse vom Betriebssystem. SDL-Tastatureingabe, Mauseingabe, Joystick-Eingabe usw. Möglicherweise gibt es mehrere Plattformen (SDL ist ein Nenner mit den wenigsten Gemeinsamkeiten, dem beispielsweise mehrere Eingabeformulare fehlen, die Sie später interessieren könnten).

Sie können diese mit einem sehr einfachen benutzerdefinierten Ereignistyp wie "Tastatur gedrückt" oder ähnlichem abstrahieren. Wenn Ihre Plattformebene (SDL-Spieleschleife) Eingaben empfängt, sollte sie diese Ereignisse auf niedriger Ebene erstellen und dann an einen Eingabemanager weiterleiten. Dies kann mit einfachen Methodenaufrufen, Rückruffunktionen, einem komplizierten Ereignissystem geschehen, ganz wie Sie möchten.

Das Eingabesystem hat nun die Aufgabe, Eingaben auf niedriger Ebene in logische Ereignisse auf hoher Ebene zu übersetzen. Die Spielelogik kümmert sich überhaupt nicht darum, dass SPACE gedrückt wurde. Es ist wichtig, dass JUMP gedrückt wurde. Die Aufgabe des Eingabe-Managers besteht darin, diese Eingabeereignisse auf niedriger Ebene zu erfassen und Eingabeereignisse auf hoher Ebene zu generieren. Es ist dafür verantwortlich zu wissen, dass sowohl die Leertaste als auch die A-Schaltfläche des Gamepads dem logischen Befehl Jump zugeordnet sind. Es befasst sich mit Gamepad vs Maus-Look-Steuerelementen und so weiter. Es gibt logische Ereignisse auf hoher Ebene aus, die von den Steuerelementen auf niedriger Ebene so abstrakt wie möglich sind (es gibt hier einige Einschränkungen, aber Sie können die Dinge im allgemeinen Fall vollständig abstrahieren).

Ihr Charakter-Controller empfängt dann diese Ereignisse und verarbeitet diese übergeordneten Eingabeereignisse, um tatsächlich zu reagieren. Die Plattformebene hat das Ereignis "Leertaste drücken" gesendet. Das Eingabesystem hat dies empfangen, überprüft seine Zuordnungstabellen / -logik und sendet dann das Ereignis "Gepresster Sprung". Der Spiellogik- / Charakter-Controller empfängt dieses Ereignis, prüft, ob der Spieler tatsächlich springen darf, und gibt dann das Ereignis "Spieler gesprungen" aus (oder veranlasst direkt einen Sprung), mit dem die übrige Spiellogik alles Mögliche ausführt .

Alles, was von der Spielelogik abhängt, geht in den Player-Controller. Alles, was vom Betriebssystem abhängt, liegt in der Plattformebene. Der Rest geht in die Eingabeverwaltungsebene.

Hier sind einige amateurhafte ASCII-Grafiken, um dies zu beschreiben:

-----------------------------------------------------------------------
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
quelle
Coole ASCII-Grafik, aber nicht unbedingt notwendig, tut mir leid. Ich schlage vor, stattdessen eine nummerierte Liste zu verwenden. Trotzdem eine gute Antwort!
Danijar
1
@danijar: Ähm, ich habe experimentiert und noch nie versucht, eine Antwort zu finden. Mehr Arbeit als es wert war, aber viel weniger Arbeit als die Arbeit mit einem Malprogramm. :)
Sean Middleditch
Okay, verständlich :-)
Danijar
8
Persönlich bevorzuge ich die ASCII-Kunst weit mehr als eine langweilige nummerierte Liste.
Jesse Emond
@ JesseEmond Hey, hier für Kunst?
Danijar