Entrée imbriquée dans un système événementiel

11

J'utilise un système de gestion des entrées basé sur les événements avec des événements et des délégués. Un exemple:

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

Cependant, j'ai commencé à me demander comment gérer les entrées «imbriquées». Par exemple, dans Half-Life 2 (ou n'importe quel jeu Source, vraiment), vous pouvez ramasser des objets avec E. Lorsque vous avez ramassé un objet, vous ne pouvez pas tirer avec Left Mouse, mais au lieu de cela, vous lancez l'objet. Vous pouvez toujours sauter avec Space.

(Je dis que l'entrée imbriquée est l'endroit où vous appuyez sur une certaine touche et les actions que vous pouvez effectuer. Pas un menu.)

Les trois cas sont:

  • Être capable de faire la même action qu'auparavant (comme sauter)
  • Ne pas pouvoir faire la même action (comme tirer)
  • Faire une action complètement différente (comme dans NetHack, où appuyer sur la touche de porte ouverte signifie que vous ne bougez pas, mais sélectionnez une direction pour ouvrir la porte)

Mon idée originale était de le changer juste après la réception de l'entrée:

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

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

Cela souffre de grandes quantités de couplage, de code répétitif et d'autres signes de mauvaise conception.

Je suppose que l'autre option est de maintenir une sorte de système d'état d'entrée - peut-être un autre délégué sur la fonction de registre pour refactoriser ces nombreux registre / désenregistrement dans un endroit plus propre (avec une sorte de pile sur le système d'entrée) ou peut-être des tableaux de ce à garder et à ne pas faire.

Je suis sûr que quelqu'un ici a dû rencontrer ce problème. Comment l'avez-vous résolu?

tl; dr Comment puis-je gérer une entrée spécifique reçue après une autre entrée spécifique dans un système d'événements?

Le canard communiste
la source

Réponses:

7

deux options: si les cas "d'entrée imbriquée" sont au plus trois, quatre, j'utiliserais simplement des indicateurs. "Tenir un objet? Vous ne pouvez pas tirer." Tout le reste le sur-conçoit.

Sinon, vous pouvez conserver une pile de gestionnaires d'événements par clé d'entrée.

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 quelque chose à cet effet :)

Edit : quelqu'un a mentionné des constructions if-else ingérables. allons-nous utiliser des données complètes pour une routine de gestion des événements d'entrée? Vous pourriez sûrement le faire, mais pourquoi?

Quoi qu'il en soit, pour le plaisir:

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

Modifier 2

Kylotan a mentionné la cartographie clé, qui est une fonctionnalité de base que chaque jeu devrait avoir (pensez également à l'accessibilité). Inclure le mappage de touches est une autre histoire.

Le changement de comportement en fonction d'une combinaison ou d'une séquence de touches est limitatif. J'ai négligé cette partie.

Le comportement est lié à la logique du jeu et non à la saisie. Ce qui est assez évident, à y penser.

Par conséquent, je propose la solution suivante:

// //>

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

// //<

Cela pourrait être amélioré à bien des égards par des gens plus intelligents que moi, mais je pense que c'est aussi un bon point de départ.

Raine
la source
Fonctionne bien pour les jeux simples - comment allez-vous coder un mappeur de touches pour cela? :) Cela seul peut être une raison suffisante pour utiliser les données pour votre entrée.
Kylotan
@Kylotan, c'est une bonne observation, je vais modifier ma réponse.
Raine
C'est une excellente réponse. Ici, ayez la générosité: P
The Communist Duck
@Le canard communiste - merci, j'espère que cela vous aidera.
Raine
11

Nous avons utilisé un système d'État, comme vous l'avez mentionné précédemment.

Nous créerions une carte qui contiendrait toutes les clés pour un état spécifique avec un indicateur qui permettrait ou non le passage des clés précédemment mappées. Lorsque nous changions d'état, la nouvelle carte était activée ou une carte précédente était supprimée.

Un exemple simple et rapide des états d'entrée serait Default, In-Menu et Magic-Mode. La valeur par défaut est l'endroit où vous courez et jouez au jeu. In-Menu serait lorsque vous êtes dans le menu de démarrage, ou lorsque vous avez ouvert un menu de magasin, le menu de pause, un écran d'options. Dans le menu contiendrait le drapeau d'interdiction, car lorsque vous naviguez dans un menu, vous ne voulez pas que votre personnage se déplace. De l'autre côté, un peu comme votre exemple avec le transport de l'objet, le mode magique remapperait simplement les touches d'action / objet à la place pour lancer des sorts (nous lierions également cela aux effets sonores et aux particules, mais c'est un peu au-delà ta question).

Ce qui fait que les cartes sont poussées et sautées dépend de vous, et je dirai honnêtement que nous avons eu certains événements `` clairs '' pour nous assurer que la pile de cartes a été maintenue propre, le chargement de niveau étant le moment le plus évident (les cinématiques aussi sur fois).

J'espère que cela t'aides.

TL; DR - Utilisez des états et une carte d'entrée que vous pouvez pousser et / ou faire apparaître. Incluez un indicateur pour indiquer si la carte supprime ou non complètement l'entrée précédente.

James
la source
5
CETTE. Les pages et les pages des instructions imbriquées sont le diable.
michael.bartnett
+1. Quand je pense à la saisie, j'ai toujours Acrobat Reader en tête - Outil de sélection, outil à main, zoom de sélection. OMI, l'utilisation d'une pile peut être parfois excessive. Le FEM masque cela via AbstractTool . JHotDraw a une belle vue hiérarchique de leurs implémentations d'outils .
Stefan Hanke
2

Cela ressemble à un cas où l'héritage pourrait résoudre votre problème. Vous pourriez avoir une classe de base avec un tas de méthodes qui implémentent le comportement par défaut. Vous pouvez ensuite étendre cette classe et remplacer certaines méthodes. Le mode de commutation n'est alors qu'une question de commutation de l'implémentation actuelle.

Voici un pseudo-code

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

C'est similaire à ce que James a proposé.

subb
la source
0

Je n'écris pas le code exact dans une langue particulière. Je te donne l'idée.

1) Mappez vos actions clés à vos événements.

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

2) Attribuez / modifiez vos événements comme indiqué ci-dessous

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

Laissez votre action de saut rester découplée avec d'autres événements comme le saut et le feu.

Évitez if..else .. les vérifications conditionnelles ici car cela conduirait à un code ingérable.

inRazor
la source
Cela ne semble pas du tout m'aider. Il a le problème des niveaux élevés de couplage et semble avoir un code répétitif.
The Communist Duck
Juste pour avoir une meilleure compréhension - Pouvez-vous expliquer ce qui est "répétitif" dans ce domaine et où voyez-vous "des niveaux élevés de couplage".
inRazor
Si vous voyez mon exemple de code, je dois supprimer toutes les actions de la fonction elle-même. Je code tout cela en fonction de la fonction elle-même - que se passe-t-il si deux fonctions veulent partager le même registre / se désinscrire? Je devrai soit dupliquer le code OU les coupler. Il y aurait également une grande quantité de code pour supprimer toutes les actions indésirables. Enfin, je devrais avoir un endroit qui «se souvienne» de toutes les actions initiales pour les remplacer.
The Communist Duck
Pouvez-vous me donner deux des fonctions réelles de votre code (comme la fonction de cape secrète) avec les instructions d'enregistrement / de désenregistrement complètes.
inRazor
Je n'ai actuellement pas de code pour ces actions. Cependant, secret cloakcela nécessiterait que des éléments tels que le feu, le sprint, la marche et le changement d'arme ne soient pas enregistrés et que la cape soit enregistrée.
The Communist Duck
0

Au lieu de vous désinscrire, créez simplement un état puis réenregistrez-vous.

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.

Bien sûr, l' extension de cette idée simple serait que vous pourriez avoir des états séparés pour déplacer, et ce, puis au lieu de dicter « Eh bien, voici toutes les choses que je ne peux pas faire tout en mode secret Cape, voici tout ce que je peut faire en mode Secret Cloak. ".

DeadMG
la source