Entrada anidada en un sistema controlado por eventos

11

Estoy usando un sistema de manejo de entrada basado en eventos con eventos y delegados. Un ejemplo:

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

Sin embargo, comencé a preguntarme cómo manejar la entrada 'anidada'. Por ejemplo, en Half-Life 2 (o en cualquier juego Source, realmente), puedes recoger objetos con E. Cuando recoges un objeto, no puedes disparar con él Left Mouse, sino que lo lanzas. Todavía puedes saltar con Space.

(Estoy diciendo que la entrada anidada es donde presionas una tecla determinada, y las acciones que puedes hacer cambian. No es un menú).

Los tres casos son:

  • Ser capaz de hacer la misma acción que antes (como saltar)
  • No poder hacer la misma acción (como disparar)
  • Realizar una acción completamente diferente (como en NetHack, donde presionar la tecla de puerta abierta significa que no se mueve, sino que selecciona una dirección para abrir la puerta)

Mi idea original era simplemente cambiarlo después de recibir la entrada:

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

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

Esto sufre de grandes cantidades de acoplamiento, código repetitivo y otros signos de mal diseño.

Supongo que la otra opción es mantener algún tipo de sistema de estado de entrada, tal vez otro delegado en la función de registro para refactorizar esos numerosos registros / desregistro en un lugar más limpio (con algún tipo de pila en el sistema de entrada) o tal vez matrices de lo que mantener y qué no hacer.

Estoy seguro de que alguien aquí debe haber encontrado este problema. ¿Cómo lo has resuelto?

tl; dr ¿Cómo puedo manejar la entrada específica recibida después de otra entrada específica en un sistema de eventos?

El pato comunista
fuente

Respuestas:

7

dos opciones: si los casos de "entrada anidada" son como máximo tres, cuatro, simplemente usaría indicadores. "¿Sosteniendo un objeto? No se puede disparar". Cualquier otra cosa es sobregeniarlo.

De lo contrario, puede mantener una pila de controladores de eventos por clave de entrada.

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 algo por el estilo :)

Editar : alguien mencionó construcciones if-else inmanejables. ¿Vamos a manejar todos los datos para una rutina de manejo de eventos de entrada? Seguramente podrías, pero ¿por qué?

De todos modos, por el gusto de hacerlo:

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

Editar 2

Kylotan mencionó el mapeo de teclas, que es una característica básica que todo juego debería tener (piense también en la accesibilidad). Incluir mapas de teclas es una historia diferente.

Cambiar el comportamiento dependiendo de una combinación de teclas o secuencia es limitante. Pasé por alto esa parte.

El comportamiento está relacionado con la lógica del juego y no con la entrada. Lo cual es bastante obvio, ahora que lo pienso.

Por lo tanto, propongo la siguiente solución:

// //>

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

// //<

Esto podría ser mejorado de muchas maneras por personas más inteligentes que yo, pero creo que también es un buen punto de partida.

Raine
fuente
Funciona bien para juegos simples: ahora, ¿cómo va a codificar un mapeador de teclas para esto? :) Eso por sí solo puede ser una razón lo suficientemente buena como para utilizar los datos para su entrada.
Kylotan
@Kylotan, esa es una buena observación, voy a editar mi respuesta.
Raine
Esta es una respuesta genial. Aquí, tenga la recompensa: P
El pato comunista
@El pato comunista: gracias amigo, espero que esto ayude.
Raine
11

Usamos un sistema de estado, como mencionaste antes.

Crearíamos un mapa que contendría todas las claves para un estado específico con una bandera que permitiría el paso de claves previamente asignadas o no. Cuando cambiamos de estado, se empujaría el nuevo mapa o se quitaría un mapa anterior.

Un ejemplo simple y rápido de estados de entrada sería Predeterminado, En menú y Modo mágico. El valor predeterminado es donde estás corriendo y jugando el juego. En el menú sería cuando esté en el menú de inicio, o cuando haya abierto un menú de la tienda, el menú de pausa, una pantalla de opciones. En el menú contendría la bandera de no pasar porque cuando navegas por un menú no quieres que tu personaje se mueva. Por otro lado, al igual que su ejemplo con el transporte del elemento, el modo mágico simplemente reasignaría las teclas de uso de acción / elemento para lanzar hechizos (también lo vincularíamos con los efectos de sonido y partículas, pero eso está un poco más allá tu pregunta).

Lo que hace que los mapas sean empujados y desplegados depende de usted, y también honestamente diré que tuvimos ciertos eventos 'claros' para asegurarnos de que la pila de mapas se mantuviera limpia, siendo la carga de nivel el momento más obvio (las escenas de corte también en veces).

Espero que esto ayude.

TL; DR: usa estados y un mapa de entrada que puedes empujar y / o reventar. Incluya una bandera para decir si el mapa elimina o no completamente la entrada anterior o no.

James
fuente
55
ESTA. Páginas y páginas de declaraciones anidadas si son el diablo.
michael.bartnett
+1. Cuando pienso en la entrada, siempre tengo en mente Acrobat Reader: seleccione Herramienta, Herramienta de mano, Zoom de marquesina. En mi opinión, usar una pila puede ser excesivo a veces. GEF oculta esto a través de AbstractTool . JHotDraw tiene una buena vista de jerarquía de sus implementaciones de herramientas .
Stefan Hanke
2

Esto parece un caso en el que la herencia podría resolver su problema. Podría tener una clase base con un montón de métodos que implementen el comportamiento predeterminado. Entonces podría extender esta clase y anular algunos métodos. El modo de cambio es solo una cuestión de cambiar la implementación actual.

Aquí hay un pseudocódigo

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

Esto es similar a lo que James propuso.

subb
fuente
0

No estoy escribiendo el código exacto en ningún idioma en particular. Te estoy dando la idea.

1) Asigna tus acciones clave a tus eventos.

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

2) Asigna / modifica tus eventos como se indica a continuación

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

Deje que su acción de salto permanezca desacoplada con otros eventos como saltar y disparar.

Evite si ... más ... verificaciones condicionales aquí ya que conduciría a un código inmanejable.

inRazor
fuente
Esto no parece ayudarme en absoluto. Tiene el problema de los altos niveles de acoplamiento y parece tener código repetitivo.
El pato comunista
Solo para obtener una mejor comprensión: ¿Puede explicar qué es "repetitivo" en esto y dónde ve "altos niveles de acoplamiento"?
inRazor
Si ve mi ejemplo de código, tengo que eliminar todas las acciones en la función misma. Estoy codificando todo a la función en sí, ¿qué pasa si dos funciones quieren compartir el mismo registro / anulación de registro? Tendré que duplicar el código O tener que acoplarlos. También habría una gran cantidad de código para eliminar todas las acciones no deseadas. Por último, tendría que tener un lugar que 'recuerde' todas las acciones originales para reemplazarlas.
El pato comunista
¿Me puede dar dos de las funciones reales de su código (como la función de capa secreta) con las declaraciones completas de registro / desregistro.
inRazor
En realidad no tengo código para esas acciones en este momento. Sin embargo, secret cloakesto requeriría que cosas como fuego, sprint, caminar y cambiar de arma no estén registradas y que no se registren.
El pato comunista
0

En lugar de cancelar el registro, simplemente haga un estado y luego vuelva a registrarse.

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.

Por supuesto, ampliar esta simple idea sería que podría tener estados separados para moverse, y así, y luego, en lugar de dictar "Bueno, aquí están todas las cosas que no puedo hacer mientras estoy en el Modo de capa secreta, aquí están todas las cosas que puedo hacer en el modo de capa secreta ".

DeadMG
fuente