Zagnieżdżone dane wejściowe w systemie sterowanym zdarzeniami

11

Korzystam z systemu obsługi danych wejściowych opartego na zdarzeniach ze zdarzeniami i uczestnikami. Przykład:

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

Zacząłem jednak zastanawiać się, jak radzić sobie z „zagnieżdżonymi” danymi wejściowymi. Na przykład w Half-Life 2 (lub w dowolnej grze źródłowej) możesz zbierać przedmioty za pomocą E. Po podniesieniu przedmiotu nie możesz strzelać Left Mouse, ale zamiast tego rzucasz przedmiotem. Nadal możesz skakać Space.

(Mówię, że wejście zagnieżdżone to miejsce, w którym naciskasz określony klawisz, a działania, które możesz wykonać, zmieniają się. Nie menu.)

Trzy przypadki to:

  • Będąc w stanie wykonać tę samą akcję, co wcześniej (jak skakanie)
  • Niemożność wykonania tej samej czynności (jak strzelanie)
  • Wykonanie zupełnie innej czynności (jak w NetHack, gdzie naciśnięcie klawisza otwartych drzwi oznacza, że ​​się nie ruszasz, ale wybierasz kierunek otwierania drzwi)

Mój pierwotny pomysł polegał na zmianie tego po otrzymaniu danych wejściowych:

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

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

To cierpi z powodu dużej ilości sprzężenia, powtarzalnego kodu i innych złych znaków projektowych.

Wydaje mi się, że drugą opcją jest utrzymanie pewnego rodzaju systemu stanu wejściowego - być może innego uczestnika funkcji rejestru, aby przefiltrować te liczne rejestry / wyrejestrować w czystsze miejsce (z jakimś stosem w systemie wejściowym) lub może tablice tego, co zatrzymać, a czego nie.

Jestem pewien, że ktoś tutaj napotkał ten problem. Jak to rozwiązałeś?

tl; dr Jak mogę poradzić sobie z konkretnymi danymi wejściowymi otrzymanymi po kolejnym określonym danych wejściowych w systemie zdarzeń?

Kaczka komunistyczna
źródło

Odpowiedzi:

7

dwie opcje: jeśli przypadki „wejścia zagnieżdżonego” wynoszą co najwyżej trzy, cztery, po prostu użyłbym flag. „Trzymanie przedmiotu? Nie można strzelać”. Wszystko inne go przerabia.

W przeciwnym razie można przechowywać stos obsługi zdarzeń dla klucza wejściowego.

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
}

Lub coś w tym celu :)

Edycja : ktoś wspomniał o niemożliwych do zarządzania konstrukcjach if-else. czy zamierzamy przejść w pełni sterowane danymi na procedurę obsługi zdarzeń wejściowych? Z pewnością mógłbyś, ale dlaczego?

W każdym razie, do cholery:

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

Edytuj 2

Kylotan wspomniał o mapowaniu klawiszy, które jest podstawową funkcją każdej gry (pomyśl także o dostępności). Włączanie mapowania klawiszy to inna historia.

Zmiana zachowania w zależności od kombinacji klawiszy lub sekwencji jest ograniczona. Przeoczyłem tę część.

Zachowanie jest związane z logiką gry, a nie wejściem. Co jest dość oczywiste, gdy o tym pomyślę.

Dlatego proponuję następujące rozwiązanie:

// //>

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

// //<

Ludzie mądrzejsi ode mnie mogą to poprawić na wiele sposobów, ale uważam, że to także dobry punkt wyjścia.

Raine
źródło
Działa dobrze w przypadku prostych gier - jak zamierzasz w tym celu zakodować klucz-maper? :) Już sam ten fakt może być wystarczającym powodem, aby wprowadzić dane w oparciu o dane.
Kylotan
@ Kylotan, to naprawdę dobra obserwacja, zamierzam zredagować swoją odpowiedź.
Raine
To świetna odpowiedź. Oto nagroda: P
Kaczka komunistyczna
@The Communist Duck - dzięki, mam nadzieję, że to pomoże.
Raine
11

Użyliśmy systemu państwowego, jak wspomniałeś wcześniej.

Stworzylibyśmy mapę, która zawierałaby wszystkie klucze dla określonego stanu z flagą, która pozwalałaby na przejście wcześniej mapowanych kluczy lub nie. Kiedy zmieniliśmy stany, nowa mapa byłaby popychana lub poprzednia mapa byłaby usuwana.

Szybki prosty przykład stanów wejściowych to Domyślny, W menu i Tryb magiczny. Domyślnie biegasz i grasz. In-Menu będzie wtedy, gdy będziesz w menu Start lub gdy otworzysz menu sklepu, menu pauzy, ekran opcji. In-Menu zawiera flagę zakazu przejścia, ponieważ podczas poruszania się po menu nie chcesz, aby twoja postać się poruszała. Z drugiej strony, podobnie jak twój przykład z noszeniem przedmiotu, Tryb Magii po prostu zamapowałby klawisze akcji / przedmiotu, aby zamiast tego rzucić zaklęcia (powiązalibyśmy to również z efektami dźwiękowymi i cząsteczkowymi, ale to nieco więcej Twoje pytanie).

To, co powoduje, że mapy są wypychane i wyskakujące, zależy od Ciebie, a także szczerze powiem, że mieliśmy pewne „jasne” zdarzenia, aby upewnić się, że stos map został utrzymany w czystości, a ładowanie poziomów jest najbardziej oczywistym czasem (przerywniki również w czasy).

Mam nadzieję że to pomoże.

TL; DR - Używaj stanów i mapy wejściowej, którą możesz popychać i / lub pop. Dołącz flagę informującą, czy mapa całkowicie usuwa poprzednie dane wejściowe, czy nie.

James
źródło
5
TO. Strony i strony zagnieżdżone, jeśli instrukcje są diabłem.
michael.bartnett
+1. Kiedy myślę o wprowadzaniu danych, zawsze mam na myśli Acrobat Reader - wybierz narzędzie, narzędzie ręczne, powiększenie markizy. IMO, używanie stosu może być czasami przesadzone. GEF ukrywa to za pośrednictwem AbstractTool . JHotDraw ma ładny widok hierarchiczny swoich implementacji narzędzi .
Stefan Hanke
2

Wygląda to na przypadek, w którym dziedziczenie mogłoby rozwiązać twój problem. Możesz mieć klasę podstawową z wieloma metodami, które implementują domyślne zachowanie. Następnie możesz rozszerzyć tę klasę i zastąpić niektóre metody. Tryb przełączania jest wtedy tylko kwestią przełączenia bieżącej implementacji.

Oto pseudo-kod

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

Jest to podobne do tego, co zaproponował James.

podpunkt
źródło
0

Nie piszę dokładnego kodu w żadnym konkretnym języku. Daję ci pomysł.

1) Zamapuj swoje kluczowe działania na wydarzenia.

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

2) Przypisz / zmień swoje wydarzenia, jak podano poniżej

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

Niech twoja akcja skoku pozostanie oddzielona od innych wydarzeń, takich jak skok i strzelanie.

Unikaj sprawdzania warunkowego if..else .., ponieważ prowadziłoby to do niemożliwego do zarządzania kodu.

inRazor
źródło
To wcale mi nie pomaga. Ma problem z wysokim poziomem sprzężenia i wydaje się, że ma powtarzalny kod.
Kaczka komunistyczna
Żeby lepiej zrozumieć - czy możesz wyjaśnić, co w tym jest „powtarzalne” i gdzie widzisz „wysoki poziom sprzężenia”.
inRazor
Jeśli widzisz mój przykład kodu, muszę usunąć wszystkie akcje w samej funkcji. Wszystko to zapisuję na stałe w samej funkcji - co jeśli dwie funkcje chcą współdzielić ten sam rejestr / wyrejestrować się? Będę musiał albo zduplikować kod LUB połączyć je. Byłaby również duża ilość kodu do usunięcia wszystkich niechcianych działań. Na koniec musiałbym mieć miejsce, które „pamięta” wszystkie oryginalne działania, aby je zastąpić.
Kaczka komunistyczna
Czy możesz podać mi dwie rzeczywiste funkcje ze swojego kodu (takie jak funkcja tajnego maskowania) z kompletnymi instrukcjami rejestru / wyrejestrowania.
inRazor
W tej chwili nie mam kodu dla tych akcji. Jednak secret cloakwymagałoby rzeczy, takich jak pożar, sprint, chodzić, i zmienić broń zostanie wyrejestrowana i demaskować być zarejestrowany.
Kaczka komunistyczna
0

Zamiast wyrejestrować się, po prostu wprowadź stan, a następnie ponownie się zarejestruj.

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.

Oczywiście, rozszerzenie tego prostego pomysłu byłoby takie, że możesz mieć oddzielne stany ruchu, i takie, a następnie zamiast dyktować „Cóż, oto wszystkie rzeczy, których nie mogę zrobić w trybie Sekretnego Płaszcza, oto wszystkie rzeczy, które mogę zrobić w trybie Secret Cloak. ”.

DeadMG
źródło