Đầu vào lồng nhau trong một hệ thống hướng sự kiện


11

Tôi đang sử dụng một hệ thống xử lý đầu vào dựa trên sự kiện với các sự kiện và đại biểu. Một ví dụ:

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

Tuy nhiên, tôi bắt đầu tự hỏi về cách xử lý đầu vào 'lồng nhau'. Ví dụ, trong Half-Life 2 (hoặc bất kỳ trò chơi Nguồn nào, thực sự), bạn có thể nhặt các vật phẩm với E. Khi bạn nhặt được một vật phẩm, bạn không thể bắn Left Mouse, nhưng thay vào đó bạn ném vật đó. Bạn vẫn có thể nhảy với Space.

(Tôi đang nói đầu vào lồng nhau là nơi bạn nhấn một phím nhất định và các hành động bạn có thể thay đổi. Không phải là một menu.)

Ba trường hợp là:

  • Có thể thực hiện hành động tương tự như trước đây (như nhảy)
  • Không thể thực hiện hành động tương tự (như bắn)
  • Thực hiện một hành động hoàn toàn khác (như trong NetHack, khi nhấn phím mở có nghĩa là bạn không di chuyển, nhưng chọn một hướng để mở cửa)

Ý tưởng ban đầu của tôi là chỉ thay đổi nó sau khi nhận được đầu vào:

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

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

Điều này phải chịu một lượng lớn khớp nối, mã lặp đi lặp lại và các dấu hiệu thiết kế xấu khác.

Tôi đoán tùy chọn khác là duy trì một số loại hệ thống trạng thái đầu vào - có thể là một đại biểu khác trong chức năng đăng ký để cấu trúc lại số lượng đăng ký / deregister đó vào một nơi sạch hơn (với một loại ngăn xếp trên hệ thống đầu vào) hoặc có thể là mảng của những gì để giữ và những gì không.

Tôi chắc rằng ai đó ở đây đã gặp phải vấn đề này. Làm thế nào bạn đã giải quyết nó?

tl; dr Làm cách nào tôi có thể xử lý đầu vào cụ thể nhận được sau một đầu vào cụ thể khác trong hệ thống sự kiện?

Câu trả lời:


7

hai tùy chọn: nếu trường hợp "đầu vào lồng nhau" nhiều nhất là ba, bốn, tôi chỉ sử dụng cờ. "Giữ một vật? Không thể bắn." Bất cứ điều gì khác là áp đảo nó.

Mặt khác, bạn có thể giữ một ngăn xếp theo từng khóa của trình xử lý sự kiện.

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
}

Hoặc một cái gì đó cho hiệu ứng này :)

Biên tập : ai đó đã đề cập đến các cấu trúc if-other không thể quản lý được. chúng ta sẽ đi theo hướng dữ liệu đầy đủ cho một thói quen xử lý sự kiện đầu vào? Bạn chắc chắn có thể, nhưng tại sao?

Dù sao, vì cái quái gì đó:

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

Chỉnh sửa 2

Kylotan đã đề cập đến ánh xạ khóa, đây là một tính năng cơ bản mà mọi trò chơi nên có (cũng nghĩ về khả năng truy cập). Bao gồm keymapping là một câu chuyện khác nhau.

Thay đổi hành vi tùy thuộc vào tổ hợp phím hoặc trình tự nhấn là hạn chế. Tôi bỏ qua phần đó.

Hành vi có liên quan đến logic trò chơi và không nhập liệu. Đó là khá rõ ràng, đến để nghĩ về nó.

Do đó, tôi đang đề xuất giải pháp sau:

// //>

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

// //<

Điều này có thể được cải thiện theo nhiều cách bởi những người thông minh hơn tôi, nhưng tôi tin rằng đó cũng là một điểm khởi đầu tốt.


Hoạt động tốt cho các trò chơi đơn giản - bây giờ bạn sẽ mã hóa trình ánh xạ khóa cho việc này như thế nào? :) Điều đó một mình có thể là một lý do đủ tốt để đi theo hướng dữ liệu cho đầu vào của bạn.
Kylotan

@Kylotan, đó thực sự là một quan sát tốt, tôi sẽ chỉnh sửa câu trả lời của mình.
Raine

Đây là một câu trả lời tuyệt vời. Ở đây, có tiền thưởng: P
Vịt Cộng sản

@ Vịt Cộng sản - cảm ơn chap, hy vọng điều này sẽ giúp.
Raine

11

Chúng tôi đã sử dụng một hệ thống nhà nước, như bạn đã đề cập trước đó.

Chúng tôi sẽ tạo một bản đồ có chứa tất cả các khóa cho một trạng thái cụ thể với một cờ có cho phép đi qua các khóa được ánh xạ trước đó hay không. Khi chúng tôi thay đổi trạng thái, bản đồ mới sẽ được đẩy lên hoặc bản đồ trước đó sẽ bị bật ra.

Ví dụ đơn giản nhanh về các trạng thái đầu vào sẽ là Mặc định, Trong Menu và Chế độ ma thuật. Mặc định là nơi bạn đang chạy xung quanh và chơi trò chơi. Trong Menu sẽ là khi bạn ở menu bắt đầu hoặc khi bạn đã mở menu cửa hàng, menu tạm dừng, màn hình tùy chọn. Trong Menu sẽ chứa cờ không đi qua vì khi bạn điều hướng một menu bạn không muốn nhân vật của mình di chuyển xung quanh. Mặt khác, giống như ví dụ của bạn với việc mang vật phẩm, Chế độ ma thuật sẽ chỉ đơn giản là ánh xạ lại các phím sử dụng hành động / vật phẩm để thay thế phép thuật (chúng ta cũng sẽ buộc nó vào hiệu ứng âm thanh và hạt nhưng điều đó hơi vượt xa câu hỏi của bạn).

Điều gì khiến các bản đồ bị đẩy và bật là tùy thuộc vào bạn và tôi cũng sẽ thành thật nói rằng chúng tôi đã có một số sự kiện 'rõ ràng' để đảm bảo ngăn xếp bản đồ được giữ sạch sẽ, tải cấp là thời gian rõ ràng nhất (Cutscenes cũng tại lần).

Hi vọng điêu nay co ich.

TL; DR - Sử dụng các trạng thái và bản đồ đầu vào mà bạn có thể đẩy và / hoặc bật. Bao gồm một cờ để cho biết liệu bản đồ có loại bỏ hoàn toàn đầu vào trước đó hay không.


5
ĐIỀU NÀY. Các trang và các trang của lồng nhau nếu các tuyên bố là ma quỷ.
michael.bartnett

+1. Khi tôi nghĩ về đầu vào, tôi luôn có sẵn Acrobat Reader - Chọn Công cụ, Công cụ cầm tay, Marquee Zoom. IMO, sử dụng ngăn xếp có thể quá mức cần thiết. GEF che giấu điều này thông qua AbstractTool . JHotDraw có một cái nhìn phân cấp tốt đẹp về việc triển khai Công cụ của họ .
Stefan Hanke

2

Điều này trông giống như một trường hợp mà thừa kế có thể giải quyết vấn đề của bạn. Bạn có thể có một lớp cơ sở với một loạt các phương thức thực hiện hành vi mặc định. Sau đó, bạn có thể mở rộng lớp này và ghi đè một số phương thức. Chuyển đổi chế độ sau đó chỉ là vấn đề chuyển đổi việc thực hiện hiện tại.

Đây là một số mã giả

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

Điều này tương tự với những gì James đề xuất.


0

Tôi không viết mã chính xác bằng bất kỳ ngôn ngữ cụ thể nào. Tôi đang cho bạn ý tưởng.

1) Ánh xạ các hành động chính của bạn đến các sự kiện của bạn.

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

2) Chỉ định / Sửa đổi các sự kiện của bạn như được đưa ra dưới đây

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

Hãy để hành động nhảy của bạn vẫn tách rời với các sự kiện khác như nhảy và bắn.

Tránh if..else .. kiểm tra có điều kiện ở đây vì nó sẽ dẫn đến mã không thể quản lý được.


Điều này dường như không hỗ trợ tôi cả. Nó có vấn đề về mức độ khớp nối cao và dường như có mã lặp đi lặp lại.
Vịt Cộng sản

Chỉ để hiểu rõ hơn - Bạn có thể giải thích "lặp đi lặp lại" trong vấn đề này là gì và bạn thấy "mức độ khớp nối cao" ở đâu.
inRazor

Nếu bạn thấy ví dụ mã của tôi, tôi phải loại bỏ tất cả các hành động trong chính hàm đó. Tôi đang mã hóa tất cả cho chính hàm đó - điều gì xảy ra nếu hai hàm muốn chia sẻ cùng một thanh ghi / hủy đăng ký? Tôi sẽ phải sao chép mã HOẶC phải ghép chúng lại. Ngoài ra, sẽ có một lượng lớn mã để loại bỏ tất cả các hành động không mong muốn. Cuối cùng, tôi sẽ phải có một số nơi 'nhớ' tất cả các hành động ban đầu để thay thế chúng.
Vịt Cộng sản

Bạn có thể cho tôi hai trong số các hàm thực tế từ mã của bạn (như hàm áo choàng bí mật) với các câu lệnh đăng ký / deregister hoàn chỉnh.
inRazor

Tôi thực sự không có mã cho những hành động đó vào lúc này. Tuy nhiên, secret cloaksẽ yêu cầu những thứ như lửa, chạy nước rút, đi bộ và thay đổi vũ khí để không được đăng ký và không được đăng ký.
Vịt Cộng sản

0

Thay vì không đăng ký, chỉ cần tạo một trạng thái và sau đó đăng ký lại.

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.

Tất nhiên, mở rộng ý tưởng đơn giản này là bạn có thể có các trạng thái riêng biệt để di chuyển, và như vậy, thay vì ra lệnh "Chà, đây là tất cả những điều tôi không thể làm trong khi ở Chế độ áo choàng bí mật, đây là tất cả những điều tôi có thể làm trong Chế độ áo choàng bí mật. ".

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.