Вложенный ввод в управляемой событиями системе


11

Я использую на основе событий ввода системы обработки с событиями и делегатами. Пример:

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

Тем не менее, я начал задумываться о том, что делать с «вложенным» вводом. Например, в Half-Life 2 (или в любой игре Source) вы можете выбирать предметы E. Когда вы подняли предмет, вы не можете стрелять Left Mouse, но вместо этого вы бросаете предмет. Вы все еще можете прыгать с Space.

(Я говорю, что вложенный ввод - это то, где вы нажимаете определенную клавишу, и действия, которые вы можете сделать, меняются. Не меню.)

Три случая:

  • Возможность выполнять те же действия, что и раньше (например, прыжки)
  • Не в состоянии сделать то же самое действие (как стрельба)
  • Выполнение различных действий полностью (например, в NetHack, где прижимной открытых средства ключевых двери не двигается, но выбрать направление, чтобы открыть дверь в)

Моя первоначальная идея состояла в том, чтобы просто изменить его после того, как вход был получен:

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

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

Это страдает от большого количества муфты, повторяющегося кода и других плохих признаков дизайна.

Я предполагаю, что другой вариант заключается в поддержании некоторой системы состояний ввода - возможно, другого делегата в функции регистра для рефакторинга этих многочисленных регистров / отмены регистрации в более чистое место (с некоторым видом стека в системе ввода) или, возможно, массивов чего-либо держать и что не делать.

Я уверен, что кто-то здесь, должно быть, столкнулся с этой проблемой. Как вы решили это?

tl; dr Как я могу иметь дело с конкретными входными данными, полученными после другого конкретного входного сигнала в системе событий?

Ответы:


7

два варианта: если случаи «вложенного ввода» не более трех, четырех, я бы просто использовал флаги. «Держать предмет? Не могу стрелять». Все остальное переоценивает это.

В противном случае вы можете сохранить стек обработчиков событий для каждого ключа ввода.

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
}

Или что-то на этот счет :)

Редактировать : кто-то упомянул неуправляемые конструкции if-else. мы собираемся пойти полностью управляемыми данными для подпрограммы обработки входных событий? Вы, конечно, могли, но почему?

Во всяком случае, черт возьми это:

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

Редактировать 2

Килотан упомянул раскладку клавиш, которая является основной функцией каждой игры (подумайте и о доступности). Включение раскладки клавиш - это отдельная история.

Изменение поведения в зависимости от комбинации нажатия клавиш или последовательности является ограничением. Я пропустил эту часть.

Поведение связано с игровой логикой, а не с вводом. Что довольно очевидно, если подумать об этом.

Поэтому я предлагаю следующее решение:

// //>

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

// //<

Это может быть улучшено многими людьми, умнее меня, но я считаю, что это также хорошая отправная точка.


Отлично работает для простых игр - теперь , как вы собираетесь кодировать ключ-картограф для этого? :) Это само по себе может быть достаточно веской причиной, чтобы использовать данные для вашего ввода.
Kylotan

@ Килотан, это действительно хорошее замечание, я собираюсь отредактировать свой ответ.
Рейн

Это отличный ответ. Здесь есть щедрость: P
Коммунистическая Утка

@ Коммунистическая утка - спасибо, парень, надеюсь, это поможет.
Рейн

11

Мы использовали государственную систему, как вы упоминали ранее.

Мы создали бы карту, которая будет содержать все ключи для определенного состояния с флагом, который позволит проходить ранее отображенные ключи или нет. Когда мы изменили состояние, новая карта будет нажата или предыдущая карта будет удалена.

Простой простой пример состояний ввода: Default, In-Menu и Magic-Mode. По умолчанию вы бегаете и играете в игру. In-Menu будет, когда вы находитесь в меню «Пуск», или когда вы открыли меню магазина, меню паузы, экран опций. In-Menu будет содержать флаг отсутствия прохождения, поскольку при перемещении по меню вы не хотите, чтобы ваш персонаж перемещался. С другой стороны, так же, как в вашем примере с переносом предмета, магический режим просто переназначает ключи использования действия / предмета вместо того, чтобы разыгрывать заклинания (мы также привязываем это к эффектам звука и частиц, но это немного за пределами ваш вопрос).

То, что заставляет карты выдвигаться и извлекаться, зависит от вас, и я также честно скажу, что у нас были определенные «чистые» события, чтобы убедиться, что стек карт был чистым, загрузка уровней является наиболее очевидным временем (ролики также в раз).

Надеюсь это поможет.

TL; DR - Используйте состояния и входную карту, которую вы можете нажать и / или вытолкнуть. Включите флаг, чтобы сказать, удаляет ли карта полностью предыдущий ввод или нет.


5
ЭТО. Страницы и страницы вложенных операторов if являются дьяволом.
Майкл Бартнетт

+1. Когда я думаю о вводе, я всегда имею в виду Acrobat Reader - Выбор инструмента, Ручной инструмент, Увеличение масштаба. IMO, использование стека может быть излишним время от времени. ГЭФ скрывает это через AbstractTool . JHotDraw имеет хороший иерархический вид их реализаций инструментов .
Стефан Ханке

2

Это похоже на случай, когда наследование может решить вашу проблему. Вы можете иметь базовый класс с кучей методов, которые реализуют поведение по умолчанию. Затем вы можете расширить этот класс и переопределить некоторые методы. Переключение режима - это просто вопрос переключения текущей реализации.

Вот немного псевдокода

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

Это похоже на то, что предложил Джеймс.


0

Я не пишу точный код на каком-либо конкретном языке. Я даю вам идею.

1) Сопоставьте свои ключевые действия с вашими событиями.

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

2) Назначьте / измените ваши события, как указано ниже

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

Пусть ваше действие прыжка остается отделенным от других событий, таких как прыжок и огонь.

Избегайте условных проверок if..else .., поскольку это приведет к неуправляемому коду.


Это, кажется, не помогает мне вообще. Он имеет проблему высокого уровня связи и, кажется, имеет повторяющийся код.
Коммунистическая утка

Просто для лучшего понимания - Можете ли вы объяснить, что является «повторяющимся» в этом и где вы видите «высокий уровень связи».
inRazor

Если вы видите мой пример кода, я должен удалить все действия в самой функции. Я жестко запрограммировал все это на саму функцию - что если две функции захотят использовать один и тот же регистр / незарегистрированный? Я должен либо дублировать код, либо соединить их. Также будет большой объем кода для удаления всех нежелательных действий. Наконец, у меня должно быть какое-то место, которое «запоминает» все оригинальные действия, чтобы заменить их.
Коммунистическая утка

Можете ли вы дать мне две действительные функции из вашего кода (например, секретную функцию маскировки) с полными операторами регистрации / отмены регистрации.
inRazor

На самом деле у меня нет кода для этих действий в настоящее время. Тем не менее, secret cloakпотребовалось бы такие вещи, как огня, спринтом, ходить, и менять оружие , чтобы быть незарегистрированными, и разоблачают быть зарегистрированы.
Коммунистическая Дак

0

Вместо того, чтобы отменить регистрацию, просто сделайте состояние, а затем перерегистрируйте.

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.

Конечно, расширение этой простой идеи может заключаться в том, что у вас могут быть отдельные состояния для перемещения и тому подобное, а затем вместо того, чтобы диктовать: «Ну, вот все, что я не могу сделать, пока в режиме секретного плаща, вот все, что я могу сделать в режиме секретного плаща.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.