Entrada aninhada em um sistema controlado por eventos

11

Estou usando um sistema de manipulação de entrada baseado em eventos com eventos e delegados. Um exemplo:

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

No entanto, comecei a pensar sobre como lidar com as entradas 'aninhadas'. Por exemplo, no Half-Life 2 (ou qualquer outro jogo de origem, na verdade), você pode pegar itens com E. Quando você pega um item, não pode atirar com ele Left Mouse, mas, em vez disso, joga o objeto. Você ainda pode pular com Space.

(Estou dizendo que a entrada aninhada é onde você pressiona uma determinada tecla e as ações que você pode executar mudam. Não é um menu.)

Os três casos são:

  • Ser capaz de executar a mesma ação de antes (como pular)
  • Não é possível executar a mesma ação (como disparar)
  • Executar uma ação completamente diferente (como no NetHack, onde pressionar a tecla da porta aberta significa que você não se move, mas seleciona uma direção para abrir a porta)

Minha idéia original era apenas alterá-lo após o recebimento da entrada:

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

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

Isso sofre com grandes quantidades de acoplamento, código repetitivo e outros sinais ruins de design.

Eu acho que a outra opção é manter algum tipo de sistema de estado de entrada - talvez outro delegado na função de registro refatore esses numerosos registros / cancelamentos de registro para um local mais limpo (com algum tipo de pilha no sistema de entrada) ou talvez matrizes do que manter e o que não fazer.

Tenho certeza de que alguém aqui deve ter encontrado esse problema. Como você resolveu isso?

tl; dr Como posso lidar com entradas específicas recebidas após outra entrada específica em um sistema de eventos?

O Pato Comunista
fonte

Respostas:

7

duas opções: se os casos de "entrada aninhada" tiverem no máximo três, quatro, eu apenas usaria sinalizadores. "Segurando um objeto? Não é possível disparar." Qualquer outra coisa está sobrecarregando.

Caso contrário, você pode manter uma pilha de manipuladores de eventos por chave 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
}

Ou algo nesse sentido :)

Edit : alguém mencionou construções if-else incontroláveis. vamos orientar todos os dados para uma rotina de manipulação de eventos de entrada? Você certamente poderia, mas por quê?

De qualquer forma, para o inferno:

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 mencionou o mapeamento de teclas, que é um recurso básico que todo jogo deve ter (pense também em acessibilidade). Incluir mapeamento de teclas é uma história diferente.

Alterar o comportamento dependendo de uma combinação ou sequência de pressionamento de tecla é limitador. Eu ignorei essa parte.

O comportamento está relacionado à lógica do jogo e não à entrada. O que é bastante óbvio, pensando nisso.

Portanto, estou propondo a seguinte solução:

// //>

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

// //<

Isso pode ser melhorado de várias maneiras por pessoas mais inteligentes que eu, mas acredito que também é um bom ponto de partida.

Raine
fonte
Funciona bem para jogos simples - agora como você codifica um mapeador de chaves para isso? :) Isso por si só pode ser uma razão boa o suficiente para ser direcionado a dados para sua entrada.
Kylotan
@Kylotan, é uma boa observação, vou editar minha resposta.
Raine
Esta é uma ótima resposta. Aqui, temos a recompensa: P
A Comunista Duck
@ O Pato Comunista - obrigado, espero que isso ajude.
Raine
11

Usamos um sistema de estados, como você mencionou antes.

Criaríamos um mapa que conteria todas as chaves para um estado específico com um sinalizador que permitiria a passagem de chaves mapeadas anteriormente ou não. Quando mudamos de estado, o novo mapa seria acionado ou um mapa anterior seria acionado.

Um exemplo simples e rápido de estados de entrada seria Padrão, No menu e Modo Mágico. O padrão é onde você está correndo e jogando o jogo. No menu seria quando você está no menu Iniciar ou quando abre um menu de loja, o menu de pausa, uma tela de opções. O menu no menu conteria o sinalizador de não passagem, pois ao navegar no menu, você não quer que seu personagem se mova. Por outro lado, muito parecido com o seu exemplo com o porte do item, o Modo Mágico simplesmente remapearia as teclas de ação / item para lançar feitiços (também amarrávamos isso aos efeitos sonoros e de partículas, mas isso é um pouco além sua pergunta).

O que faz com que os mapas sejam empurrados e estourados é com você, e também honestamente digo que tivemos alguns eventos 'claros' para garantir que a pilha de mapas fosse mantida limpa, sendo o carregamento nivelado o momento mais óbvio (Cutscenes, bem como vezes).

Espero que isto ajude.

TL; DR - Use estados e um mapa de entrada que você pode pressionar e / ou pop. Inclua um sinalizador para dizer se o mapa remove ou não completamente a entrada anterior.

James
fonte
5
ISTO. Páginas e páginas de instruções aninhadas se forem o diabo.
michael.bartnett
+1. Quando penso na entrada, sempre tenho em mente o Acrobat Reader - Selecione Ferramenta, Ferramenta Manual, Marquee Zoom. Na IMO, o uso de uma pilha pode ser um exagero às vezes. O GEF oculta isso por meio do AbstractTool . O JHotDraw possui uma boa visão hierárquica de suas implementações de ferramentas .
Stefan Hanke
2

Parece um caso em que a herança pode resolver seu problema. Você pode ter uma classe base com vários métodos que implementam o comportamento padrão. Você pode estender essa classe e substituir alguns métodos. O modo de comutação é apenas uma questão de alternar a implementação atual.

Aqui estão alguns pseudo-códigos

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

Isso é semelhante ao que James propôs.

subb
fonte
0

Não estou escrevendo o código exato em nenhum idioma específico. Estou te dando uma idéia.

1) Mapeie suas ações principais para seus eventos.

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

2) Atribua / modifique seus eventos como indicado abaixo

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

Deixe sua ação de salto permanecer dissociada com outros eventos como salto e tiro.

Evite as verificações condicionais if..else .. aqui, pois isso levaria a um código incontrolável.

inRazor
fonte
Isso não parece me ajudar em nada. Ele tem o problema de altos níveis de acoplamento e parece ter código repetitivo.
The Duck comunista
Apenas para entender melhor - Você pode explicar o que é "repetitivo" nisso e onde você vê "altos níveis de acoplamento".
InRazor
Se você vir meu exemplo de código, tenho que remover todas as ações da própria função. Estou codificando tudo para a própria função - e se duas funções quiserem compartilhar o mesmo registro / cancelar o registro? Terei que duplicar o código OU precisar acoplar. Além disso, haveria uma grande quantidade de código para remover todas as ações indesejadas. Por fim, eu teria que ter um lugar que "lembre" todas as ações originais para substituí-las.
The Duck comunista
Você pode me fornecer duas das funções reais do seu código (como a função de capa secreta) com as instruções completas de registro / cancelamento de registro.
InRazor
Na verdade, não tenho código para essas ações no momento. No entanto, secret cloakisso exigiria que coisas como fogo, sprint, caminhada e troca de armas não fossem registradas e que a capa fosse registrada.
The Duck comunista
0

Em vez de cancelar o registro, apenas faça um estado e depois registre-se novamente.

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.

É claro, estender essa idéia simples seria que você pode ter estados separados para se mudar, e tal, e depois em vez de ditar "Bem, aqui estão todas as coisas que não posso fazer enquanto estiver no modo Secret Cloak, aqui estão todas as coisas que posso faça no modo Secret Cloak. ".

DeadMG
fonte