Input nidificato in un sistema guidato da eventi


11

Sto usando un sistema di gestione degli input basato su eventi con eventi e delegati. Un esempio:

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

Tuttavia, ho iniziato a chiedermi come gestire l'input "nidificato". Ad esempio in Half-Life 2 (o in qualsiasi gioco Source, davvero), puoi raccogliere oggetti con E. Quando hai raccolto un oggetto, non puoi sparare con Left Mouse, ma invece lanci l'oggetto. Puoi ancora saltare con Space.

(Sto dicendo che l'input nidificato è dove si preme un determinato tasto e le azioni che si possono fare cambiano. Non un menu.)

I tre casi sono:

  • Essere in grado di fare la stessa azione di prima (come saltare)
  • Non essere in grado di fare la stessa azione (come sparare)
  • Compiere un'azione completamente diversa (come in NetHack, in cui premendo il tasto apri porta non ti muovi, ma seleziona una direzione in cui aprire la porta)

La mia idea originale era di cambiarlo solo dopo aver ricevuto l'input:

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

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

Ciò soffre di grandi quantità di accoppiamento, codice ripetitivo e altri segni di cattiva progettazione.

Immagino che l'altra opzione sia quella di mantenere un qualche tipo di sistema di input state - forse un altro delegato sulla funzione register per refactoring quei numerosi registri / cancellazioni in un posto più pulito (con una sorta di stack sul sistema di input) o forse array di ciò mantenere e cosa non fare.

Sono sicuro che qualcuno qui deve aver riscontrato questo problema. Come l'hai risolto?

tl; dr Come posso gestire input specifici ricevuti dopo un altro input specifico in un sistema di eventi?

Risposte:


7

due opzioni: se i casi di "input nidificato" sono al massimo tre, quattro, utilizzerei solo i flag. "Tieni un oggetto? Non puoi sparare." Qualcos'altro lo sta ingegnando troppo.

Altrimenti, è possibile mantenere una pila di gestori di eventi per chiave di input.

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
}

O qualcosa in tal senso :)

Modifica : qualcuno ha menzionato costrutti if-else ingestibili. andremo completamente guidati dai dati per una routine di gestione degli eventi di input? Potresti sicuramente, ma perché?

Comunque, diamine:

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

Modifica 2

Kylotan ha menzionato la mappatura dei tasti, che è una caratteristica di base che ogni gioco dovrebbe avere (pensa anche all'accessibilità). Includere il keymapping è una storia diversa.

La modifica del comportamento in base a una combinazione o sequenza di tasti premuti è limitante. Ho trascurato quella parte.

Il comportamento è legato alla logica del gioco e non all'input. Il che è abbastanza ovvio, arrivando a pensarci.

Pertanto, sto proponendo la seguente soluzione:

// //>

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]();    
    }
}

// //<

Questo potrebbe essere migliorato in molti modi da persone più intelligenti di me, ma credo che sia anche un buon punto di partenza.


Funziona bene con giochi semplici - ora come hai intenzione di codificare un key-mapper per questo? :) Questo da solo può essere una buona ragione sufficiente per passare ai dati per i tuoi input.
Kylotan,

@Kylotan, questa è davvero una buona osservazione, ho intenzione di modificare la mia risposta.
Raine,

Questa è un'ottima risposta Ecco, abbi la generosità: P
L'anatra comunista

@L'anatra comunista - grazie amico, spero che questo ti aiuti.
Raine,

11

Abbiamo usato un sistema statale, come hai detto prima.

Creeremmo una mappa che contenga tutte le chiavi per uno stato specifico con un flag che consentirebbe il passaggio di chiavi precedentemente mappate o meno. Quando cambiassimo gli stati, la nuova mappa verrebbe spinta o una precedente mappa verrebbe espulsa.

Un semplice esempio rapido di stati di input sarebbe Default, In-Menu e Magic-Mode. L'impostazione predefinita è dove stai correndo e giocando. Nel menu sarebbe quando sei nel menu di avvio o quando hai aperto un menu negozio, il menu pausa, una schermata delle opzioni. In-Menu conterrebbe la bandiera no pass through perché mentre navighi in un menu non vuoi che il tuo personaggio si muova. Dall'altro lato, proprio come il tuo esempio con il trasporto dell'oggetto, la modalità magica rimappava semplicemente l'azione / l'oggetto utilizzava i tasti invece di lanciare incantesimi (lo legeremmo anche agli effetti sonori e particellari, ma è un po 'oltre la tua domanda).

Ciò che causa il push e lo scoppio delle mappe dipende da te, e onestamente dirò anche che abbiamo avuto alcuni eventi "chiari" per assicurarci che lo stack delle mappe fosse tenuto pulito, il caricamento dei livelli è il momento più ovvio (Cutscenes volte).

Spero che questo ti aiuti.

TL; DR - Usa gli stati e una mappa di input che puoi spingere e / o pop. Includi un flag per dire se la mappa rimuove completamente l'input precedente o meno.


5
QUESTO. Pagine e pagine di nidificati se le dichiarazioni sono il diavolo.
michael.bartnett,

+1. Quando penso all'input, ho sempre in mente Acrobat Reader: Seleziona strumento, Strumento mano, Zoom selezione. IMO, l'utilizzo di uno stack potrebbe essere eccessivo a volte. GEF lo nasconde tramite AbstractTool . JHotDraw ha una bella vista gerarchica delle loro implementazioni di strumenti .
Stefan Hanke,

2

Questo sembra un caso in cui l'ereditarietà potrebbe risolvere il tuo problema. Potresti avere una classe base con un sacco di metodi che implementano il comportamento predefinito. È quindi possibile estendere questa classe e sovrascrivere alcuni metodi. Il cambio di modalità è quindi solo una questione di passaggio all'attuale implementazione.

Ecco alcuni pseudo-codici

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. */ }

Questo è simile a quello che James ha proposto.


0

Non sto scrivendo il codice esatto in nessuna lingua particolare. Ti sto dando l'idea.

1) Mappa le tue azioni chiave ai tuoi eventi.

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

2) Assegna / modifica i tuoi eventi come indicato di seguito

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() {
 ....
}

Lascia che la tua azione di salto rimanga disaccoppiata con altri eventi come il salto e il fuoco.

Evita se..else .. controlli condizionali qui in quanto porterebbe a codice ingestibile.


Questo non sembra aiutarmi affatto. Ha il problema di alti livelli di accoppiamento e sembra avere un codice ripetitivo.
Il comunista Duck il

Giusto per capire meglio - Puoi spiegare cosa è "ripetitivo" in questo e dove vedi "alti livelli di accoppiamento".
inRazor

Se vedi il mio esempio di codice, devo rimuovere tutte le azioni nella funzione stessa. Sto codificando tutto per la funzione stessa - cosa succede se due funzioni vogliono condividere lo stesso registro / annullamento della registrazione? Dovrò duplicare il codice O dovrò accoppiarli. Inoltre ci sarebbe una grande quantità di codice per rimuovere tutte le azioni indesiderate. Infine, dovrei avere un posto che 'ricorda' tutte le azioni originali per sostituirle.
Il comunista Duck il

Puoi darmi due delle funzioni effettive dal tuo codice (come la funzione del mantello segreto) con le istruzioni complete di registro / cancellazione.
inRazor

Al momento non ho un codice per queste azioni. Tuttavia, secret cloakciò richiederebbe che cose come il fuoco, lo sprint, la camminata e il cambio dell'arma non fossero registrate, e lo sganciamento per essere registrato.
Il comunista Duck il

0

Invece di annullare la registrazione, basta creare uno stato e quindi ri-registrarsi.

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.

Naturalmente, estendere questa semplice idea sarebbe che potresti avere stati separati per lo spostamento, e così, e quindi invece di dettare "Bene, ecco tutte le cose che non posso fare mentre sono in modalità Mantello segreto, ecco tutte le cose che posso fare in modalità Mantello segreto ".

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.