Verschachtelte Eingabe in einem ereignisgesteuerten System

11

Ich verwende ein ereignisbasiertes Eingabeverarbeitungssystem mit Ereignissen und Delegierten. Ein Beispiel:

InputHander.AddEvent(Keys.LeftArrow, player.MoveLeft); //Very simplified code

Ich begann mich jedoch zu fragen, wie ich mit 'verschachtelten' Eingaben umgehen sollte. Zum Beispiel können Sie in Half-Life 2 (oder wirklich in jedem Quellenspiel) Gegenstände mit abholen E. Wenn Sie einen Gegenstand aufgehoben haben, können Sie nicht mit ihm schießen Left Mouse, sondern werfen den Gegenstand. Sie können immer noch mit springen Space.

(Ich sage, bei verschachtelten Eingaben drücken Sie eine bestimmte Taste, und die Aktionen, die Sie ausführen können, ändern sich. Kein Menü.)

Die drei Fälle sind:

  • In der Lage sein, die gleiche Aktion wie zuvor auszuführen (wie Springen)
  • Nicht in der Lage sein, die gleiche Aktion auszuführen (wie das Schießen)
  • Führen Sie eine völlig andere Aktion aus (wie in NetHack, wo das Drücken der Taste zum Öffnen der Tür bedeutet, dass Sie sich nicht bewegen, sondern eine Richtung zum Öffnen der Tür auswählen).

Meine ursprüngliche Idee war es, sie einfach zu ändern, nachdem die Eingabe eingegangen war:

Register input 'A' to function 'Activate Secret Cloak Mode'

In 'Secret Cloak Mode' function:
Unregister input 'Fire'
Unregister input 'Sprint'
...
Register input 'Uncloak'
...

Dies leidet unter großen Mengen an Kopplung, sich wiederholendem Code und anderen schlechten Entwurfszeichen.

Ich denke, die andere Option besteht darin, eine Art Eingabestatussystem beizubehalten - vielleicht einen anderen Delegierten in der Registerfunktion, um diese zahlreichen Register / Abmeldungen an einen saubereren Ort (mit einer Art Stapel auf dem Eingabesystem) oder möglicherweise Arrays von was umzugestalten zu behalten und was nicht.

Ich bin sicher, dass jemand hier auf dieses Problem gestoßen sein muss. Wie hast du es gelöst?

tl; dr Wie kann ich mit bestimmten Eingaben umgehen, die nach einer anderen bestimmten Eingabe in einem Ereignissystem empfangen wurden?

Die kommunistische Ente
quelle

Antworten:

7

Zwei Optionen: Wenn "verschachtelte Eingaben" höchstens drei, vier sind, würde ich nur Flags verwenden. "Ein Objekt halten? Kann nicht schießen." Alles andere überarbeitet es.

Andernfalls können Sie einen Stapel von Ereignishandlern pro Eingabeschlüssel behalten.

Actions.Empty = () => { return; };
if(IsPressed(Keys.E)) {
    keyEventHandlers[Keys.E].Push(Actions.Empty);
    keyEventHandlers[Keys.LeftMouseButton].Push(Actions.Empty);
    keyEventHandlers[Keys.Space].Push(Actions.Empty);
} else if (IsReleased(Keys.E)) {
    keyEventHandlers[Keys.E].Pop();
    keyEventHandlers[Keys.LeftMouseButton].Pop();
    keyEventHandlers[Keys.Space].Pop();        
}

while(GetNextKeyInBuffer(out key)) {
   keyEventHandlers[key].Invoke(); // we invoke only last event handler
}

Oder etwas in diesem Sinne :)

Bearbeiten : Jemand erwähnte nicht verwaltbare if-else-Konstrukte. Werden wir für eine Routine zur Behandlung von Eingabeereignissen vollständig datengesteuert sein? Sie könnten sicherlich, aber warum?

Wie auch immer, zum Teufel:

void BuildOnKeyPressedEventHandlerTable() {
    onKeyPressedHandlers[Key.E] = () => { 
        keyEventHandlers[Keys.E].Push(Actions.Empty);
        keyEventHandlers[Keys.LeftMouseButton].Push(Actions.Empty);
        keyEventHandlers[Keys.Space].Push(Actions.Empty);
    };
}

void BuildOnKeyReleasedEventHandlerTable() {
    onKeyReleasedHandlers[Key.E] = () => { 
        keyEventHandlers[Keys.E].Pop();
        keyEventHandlers[Keys.LeftMouseButton].Pop();
        keyEventHandlers[Keys.Space].Pop();              
    };
}

/* get released keys */

foreach(var releasedKey in releasedKeys)
    onKeyReleasedHandlers[releasedKey].Invoke();

/* get pressed keys */
foreach(var pressedKey in pressedKeys) 
    onKeyPressedHandlers[pressedKey].Invoke();

keyEventHandlers[key].Invoke(); // we invoke only last event handler

Bearbeiten 2

Kylotan erwähnte das Key Mapping, eine grundlegende Funktion, die jedes Spiel haben sollte (denken Sie auch an die Barrierefreiheit). Das Einschließen von Keymapping ist eine andere Geschichte.

Das Ändern des Verhaltens in Abhängigkeit von einer Tastendruckkombination oder -sequenz ist einschränkend. Ich habe diesen Teil übersehen.

Das Verhalten hängt mit der Spielelogik zusammen und nicht mit der Eingabe. Was ziemlich offensichtlich ist, wenn man daran denkt.

Daher schlage ich folgende Lösung vor:

// //>

void Init() {
    // from config file / UI
    // -something events should be set automatically
    // quake 1 ftw.
    // name      family         key      keystate
    "+forward" "movement"   Keys.UpArrow Pressed
    "-forward"              Keys.UpArrow Released
    "+shoot"   "action"     Keys.LMB     Pressed
    "-shoot"                Keys.LMB     Released
    "jump"     "movement"   Keys.Space   Pressed
    "+lstrafe" "movement"   Keys.A       Pressed
    "-lstrafe"              Keys.A       Released
    "cast"     "action"     Keys.RMB     Pressed
    "picknose" "action"     Keys.X       Pressed
    "lockpick" "action"     Keys.G       Pressed
    "+crouch"  "movement"   Keys.LShift  Pressed
    "-crouch"               Keys.LShift  Released
    "chat"     "user"       Keys.T       Pressed      
}  

void ProcessInput() {
    var pk = GetPressedKeys();
    var rk = GetReleasedKeys();

    var actions = TranslateToActions(pk, rk);
    PerformActions(actions);
}                

void TranslateToActions(pk, rk) {
    // use what I posted above to switch actions depending 
    // on which keys have been pressed
    // it's all about pushing and popping the right action 
    // depending on the "context" (it becomes a contextual action then)
}

actionHandlers["movement"] = (action, actionFamily) => {
    if(player.isCasting)
        InterruptCast();    
};

actionHandlers["cast"] = (action, actionFamily) => {
    if(player.isSilenced) {
        Message("Cannot do that when silenced.");
    }
};

actionHandlers["picknose"] = (action, actionFamily) => {
    if(!player.canPickNose) {
        Message("Your avatar does not agree.");
    }
};

actionHandlers["chat"] = (action, actionFamily) => {
    if(player.isSilenced) {
        Message("Cannot chat when silenced!");
    }
};

actionHandlers["jump"] = (action, actionFamily) => {
    if(player.canJump && !player.isJumping)
        player.PerformJump();

    if(player.isJumping) {
        if(player.CanDoubleJump())
            player.PerformDoubleJump();
    }

    player.canPickNose = false; // it's dangerous while jumping
};

void PerformActions(IList<ActionEntry> actions) {
    foreach(var action in actions) {
        // we pass both action name and family
        // if we find no action handler, we look for an "action family" handler
        // otherwise call an empty delegate
        actionHandlers[action.Name, action.Family]();    
    }
}

// //<

Dies könnte in vielerlei Hinsicht von Leuten verbessert werden, die klüger sind als ich, aber ich glaube, es ist auch ein guter Ausgangspunkt.

Raine
quelle
Funktioniert gut für einfache Spiele - wie werden Sie nun einen Key-Mapper dafür codieren? :) Das allein kann ein guter Grund sein, für Ihre Eingabe datengesteuert zu werden.
Kylotan
@Kylotan, das ist in der Tat eine gute Beobachtung, ich werde meine Antwort bearbeiten.
Raine
Dies ist eine großartige Antwort. Hier haben Sie das Kopfgeld: P
Die kommunistische Ente
@ The Communist Duck - danke Junge, hoffe das hilft.
Raine
11

Wir haben ein Staatssystem verwendet, wie Sie bereits erwähnt haben.

Wir würden eine Karte erstellen, die alle Schlüssel für einen bestimmten Status mit einem Flag enthält, das den Durchgang von zuvor zugeordneten Schlüsseln ermöglicht oder nicht. Wenn wir den Status ändern, wird die neue Karte weitergeschoben oder eine vorherige Karte wird entfernt.

Ein einfaches Beispiel für Eingabestatus wäre Standard, In-Menu und Magic-Mode. Standardmäßig laufen Sie herum und spielen das Spiel. In-Menu ist, wenn Sie sich im Startmenü befinden oder wenn Sie ein Shop-Menü, das Pausenmenü und einen Optionsbildschirm geöffnet haben. In-Menu würde das Flag no pass through enthalten, da Sie beim Navigieren in einem Menü nicht möchten, dass sich Ihr Charakter bewegt. Auf der anderen Seite würde der Magic-Modus, ähnlich wie in Ihrem Beispiel beim Tragen des Gegenstands, einfach die Aktions- / Gegenstands-Verwendungstasten neu zuordnen, um stattdessen Zauber zu wirken (wir würden das auch mit Klang- und Partikeleffekten verknüpfen, aber das ist etwas weiter Ihre Frage).

Was dazu führt, dass die Karten verschoben und geknallt werden, liegt bei Ihnen, und ich werde auch ehrlich sagen, dass wir bestimmte "klare" Ereignisse hatten, um sicherzustellen, dass der Kartenstapel sauber gehalten wurde, wobei das Laden auf gleicher Ebene die naheliegendste Zeit war (auch Zwischensequenzen) mal).

Hoffe das hilft.

TL; DR - Verwenden Sie Status und eine Eingabekarte, die Sie drücken und / oder einfügen können. Fügen Sie ein Flag hinzu, um anzugeben, ob die Karte vorherige Eingaben vollständig entfernt oder nicht.

James
quelle
5
DIES. Seiten und Seiten von verschachtelten if-Anweisungen sind der Teufel.
michael.bartnett
+1. Wenn ich an Eingaben denke, denke ich immer an Acrobat Reader - Auswahlwerkzeug, Handwerkzeug, Auswahlzoom. IMO, die Verwendung eines Stapels kann manchmal übertrieben sein. GEF versteckt dies über AbstractTool . JHotDraw hat eine schöne Hierarchieansicht der Tool-Implementierungen .
Stefan Hanke
2

Dies scheint ein Fall zu sein, in dem die Vererbung Ihr Problem lösen könnte. Sie könnten eine Basisklasse mit einer Reihe von Methoden haben, die das Standardverhalten implementieren. Sie können diese Klasse dann erweitern und einige Methoden überschreiben. Das Umschalten des Modus ist dann nur eine Frage des Umschaltens der aktuellen Implementierung.

Hier ist ein Pseudocode

class DefaultMode
    function handle(key) {/* call the right method based on the given key. */}
    function run() { ... }
    function pickup() { ... }
    function fire() { ... }


class CarryingMode extends DefaultMode
      function pickup() {} //empty method, so no pickup action in this mode
      function fire() { /*throw object and switch to DefaultMode. */ }

Dies ähnelt dem, was James vorgeschlagen hat.

subb
quelle
0

Ich schreibe nicht den genauen Code in einer bestimmten Sprache. Ich gebe dir die Idee.

1) Ordnen Sie Ihre Schlüsselaktionen Ihren Ereignissen zu.

(Keys.LeftMouseButton, left_click_event), (Keys.E, e_key_event), (Keys.Space, space_key_event)

2) Weisen Sie Ihre Ereignisse wie unten angegeben zu / ändern Sie sie

def left_click_event = fire();
def e_key_event = pick_item();
def space_key_event = jump();

pick_item() {
 .....
 left_click_action = throw_object();
}

throw_object() {
 ....
 left_click_action = fire();
}

fire() {
 ....
}

jump() {
 ....
}

Lassen Sie Ihre Sprungaktion mit anderen Ereignissen wie Sprung und Feuer entkoppelt bleiben.

Vermeiden Sie, wenn ... sonst ... bedingte Überprüfungen durchgeführt werden, da dies zu nicht verwaltbarem Code führen würde.

inRazor
quelle
Das scheint mir überhaupt nicht zu helfen. Es hat das Problem eines hohen Kopplungsniveaus und scheint sich wiederholenden Code zu haben.
Die kommunistische Ente
Nur um ein besseres Verständnis zu erhalten - Können Sie erklären, was sich darin "wiederholt" und wo Sie "hohe Kopplungsgrade" sehen?
InRazor
Wenn Sie mein Codebeispiel sehen, muss ich alle Aktionen in der Funktion selbst entfernen. Ich codiere alles fest in die Funktion selbst - was ist, wenn zwei Funktionen dasselbe Register teilen / die Registrierung aufheben möchten? Ich muss entweder Code duplizieren oder sie koppeln. Außerdem würde es eine große Menge an Code geben, um alle unerwünschten Aktionen zu entfernen. Zuletzt müsste ich einen Ort haben, der sich an alle ursprünglichen Aktionen "erinnert", um sie zu ersetzen.
Die kommunistische Ente
Können Sie mir zwei der tatsächlichen Funktionen aus Ihrem Code (wie die geheime Umhangfunktion) mit den vollständigen Register- / Deregister-Anweisungen geben?
InRazor
Ich habe derzeit keinen Code für diese Aktionen. Dies secret cloakwürde jedoch erfordern, dass Dinge wie Feuer, Sprint, Gehen und Waffenwechsel nicht registriert und der Umhang nicht registriert werden.
Die kommunistische Ente
0

Anstatt die Registrierung aufzuheben, erstellen Sie einfach einen Status und registrieren Sie sich erneut.

In 'Secret Cloak Mode' function:
Grab the state of all bound keys- save somewhere
Unbind all keys
Re-register the keys/etc that we want to do.

In `Unsecret Cloak Mode`:
Unbind all keys
Rebind the state that we saved earlier.

Eine Erweiterung dieser einfachen Idee wäre natürlich, dass Sie möglicherweise separate Zustände für das Bewegen und dergleichen haben und dann, anstatt zu diktieren: "Nun, hier sind alle Dinge, die ich im geheimen Umhangmodus nicht tun kann , hier sind alle Dinge, die ich kann." im geheimen Umhangmodus. "

DeadMG
quelle