事件驱动系统中的嵌套输入


11

我正在使用具有事件和委托的基于事件的输入处理系统。一个例子:

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

但是,我开始怀疑如何处理“嵌套”输入。例如,在《半条命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如何处理事件系统中另一个特定输入之后收到的特定输入?

Answers:


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

Kylotan提到了按键映射,这是每个游戏都应该具备的基本功能(也要考虑可访问性)。包括键映射是另一回事。

根据按键组合或顺序更改行为是有限的。我忽略了那部分。

行为与游戏逻辑有关,而非输入。考虑一下,这是显而易见的。

因此,我提出以下解决方案:

// //>

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

@Kylotan,确实是一个很好的观察,我将编辑我的答案。
Raine

这是一个很好的答案。在这里,有赏金:P
共产党鸭子

@共产主义鸭子-谢谢你,希望这会有所帮助。
Raine

11

如前所述,我们使用了状态系统。

我们将创建一个映射,其中将包含一个特定状态的所有键,并带有一个标志,该标志将允许通过或不允许通过先前映射的键。当我们更改状态时,将启动新地图,或者弹出先前的地图。

输入状态的快速简单示例是“默认”,“菜单内”和“魔术模式”。默认状态是您到处奔跑并玩游戏的地方。进入菜单时指的是当您处于开始菜单时,或者当您打开商店菜单,暂停菜单,选项屏幕时。菜单中将包含禁止通过标志,因为在导航菜单时,您不希望角色四处移动。另一方面,就像您携带物品的示例一样,Magic-Mode会简单地重新映射动作/项目使用键来代替投射法术(我们还将其与声音和粒子效果绑定在一起,但这超出了范围你的问题)。

导致地图被推送和弹出的原因完全取决于您,我也要坦白地说,我们有某些“清除”事件来确保地图堆栈保持干净,关卡加载是最明显的时间(Cutscenes以及次)。

希望这可以帮助。

TL; DR-使用状态和可以推送和/或弹出的输入映射。包含一个标志,以说明地图是否完全删除了先前的输入。


5
这个。嵌套if语句的页面和页面都是恶魔。
michael.bartnett

+1。在考虑输入时,我始终会想到Acrobat Reader-选择工具,手动工具,选框缩放。IMO,有时使用堆栈可能会过大。GEF通过AbstractTool将其隐藏。JHotDraw具有其Tool实现的漂亮的层次结构视图。
Stefan Hanke 2014年

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 2011年

如果您看到我的代码示例,则必须删除函数本身中的所有动作。我将所有内容都硬编码为函数本身-如果两个函数要共享相同的寄存器/注销,该怎么办?我要么必须重复代码,要么必须将它们耦合。同样,将有大量代码删除所有不需要的操作。最后,我必须在某个位置“记住”所有原始动作以替换它们。
共产党鸭子

您能否通过完整的register / deregister语句从代码中给我两个实际功能(例如秘密斗篷功能)。
inRazor 2011年

目前,我实际上没有这些操作的代码。但是,这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.

当然,扩展这个简单的想法是,你可能有单独的状态进行移动,并且这样的,然后,而不是发号施令“嗯,这里的所有的事情,我不能在秘密隐身状态做,同时,这里的所有的事情,我可以在“秘密披风”模式下进行。”。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.