イベント駆動型システムの入れ子になった入力


11

イベントとデリゲートを使用したイベントベースの入力処理システムを使用しています。例:

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

しかし、「ネストされた」入力を処理する方法について疑問に思い始めました。たとえば、Half-Life 2(または実際には任意のソースゲーム)では、でアイテムをピックアップできますE。アイテムを拾ったら、で発砲することはできませんLeft Mouseが、代わりにオブジェクトを投げます。まだジャンプできSpaceます。

(入れ子になった入力とは、特定のキーを押す場所であり、実行できるアクションが変わるものです。メニューではありません。)

3つのケースは次のとおりです。

  • 以前と同じアクションを実行できる(ジャンプなど)
  • 同じアクションを実行できない(発砲など)
  • 別のアクションを完全に実行する(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

2つのオプション:「ネストされた入力」のケースが最大3つ、4つである場合、フラグを使用します。「物を持っている?発砲できない」他のものはそれを過剰設計しています。

それ以外の場合は、イベントハンドラの入力キーごとのスタックを保持できます。

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、それは確かに良い観察です。私は私の回答を編集します。
Raine

これは素晴らしい答えです。ここに賞金があります:P
共産主義者のアヒル

@共産ダック-ありがとう、これが役に立てば幸いです。
Raine

11

前述のように、私たちは状態システムを使用しました。

以前にマップされたキーのパススルーを許可するかどうかを示すフラグを持つ特定の状態のすべてのキーを含むマップを作成します。状態を変更すると、新しいマップがプッシュされるか、以前のマップがポップオフされます。

入力状態の簡単な例は、デフォルト、メニュー内、およびマジックモードです。デフォルトは、走り回ってゲームをプレイする場所です。メニュー内は、スタートメニューにいるとき、またはショップメニュー、一時停止メニュー、オプション画面を開いているときです。In-Menuには、パススルーフラグが含まれます。これは、メニューをナビゲートするときに、キャラクターを動かしたくないためです。反対に、アイテムを運ぶ例と同様に、Magic-Modeはアクション/アイテムの使用キーを再マッピングするだけで、代わりに呪文を唱えます(サウンドとパーティクルエフェクトに結び付けますが、それは少し超えています。あなたの質問)。

マップがプッシュされたりポップされたりする原因はあなた次第であり、正直に言うと、マップスタックがクリーンに保たれることを確認するための特定の「クリア」イベントがあり、レベルのロードが最も明白な時間でした(カットシーンも回)。

お役に立てれば。

TL; DR-プッシュおよび/またはポップできる状態と入力マップを使用します。マップが以前の入力を完全に削除するかどうかを示すフラグを含めます。


5
この。ネストされたifステートメントのページとページは悪魔です。
michael.bartnett

+1。入力について考えるとき、私は常にAcrobat Readerを念頭に置いています-選択ツール、ハンドツール、マーキーズーム。IMO、スタックを使用することは時々やり過ぎかもしれません。GEFは、これをAbstractToolを介して非表示にします。JHotDrawには、ツールの実装の階層ビューが表示されます。
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

私のコード例が表示されたら、関数自体のすべてのアクションを削除する必要があります。すべてを関数自体にハードコーディングしています-2つの関数が同じレジスタを登録/登録解除したい場合はどうなりますか?コードを複製するか、それらを結合する必要があります。また、すべての不要なアクションを削除するための大量のコードがあります。最後に、私はそれらを置き換えるためのすべての元のアクションを「記憶」するいくつかの場所が必要でした。
共産主義者アヒル

完全な登録/登録解除ステートメントを使用して、コードから実際の関数(シークレットクローク関数など)を2つ教えてください。
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.

もちろん、この単純なアイデアを拡張すると、移動などの状態が別々になり、「まあ、ここではシークレットクロークモードで実行できないすべての操作ができるようになります」シークレットクロークモードで行います。」

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.