Olaya dayalı bir sistemde iç içe giriş


11

Olaylara ve delegelere sahip bir olay tabanlı girdi işleme sistemi kullanıyorum. Bir örnek:

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

Ancak, 'iç içe' girdiyle nasıl başa çıkacağımı merak etmeye başladım. Örneğin Half-Life 2'de (ya da gerçekten herhangi bir Kaynak oyunda) ile öğeleri alabilirsiniz E. Bir öğeyi seçtiğinizde, ateş edemezsiniz Left Mouse, bunun yerine nesneyi atarsınız. Hala atlayabilirsin Space.

(İç içe girişin belirli bir tuşa bastığınız yer olduğunu ve yapabileceğiniz eylemlerin değiştiğini söylüyorum. Menü değil.)

Üç vaka:

  • Önceki ile aynı eylemi yapabilmek (zıplamak gibi)
  • Aynı eylemi yapamamak (ateş etmek gibi)
  • Tamamen farklı bir işlem yapmak (NetHack'teki gibi, açık kapı tuşuna basmanın hareket etmediğiniz anlamına gelir, ancak kapıyı açmak için bir yön seçin)

Orijinal fikrim sadece girdi alındıktan sonra değiştirmekti:

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

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

Bu, büyük miktarda bağlantı, tekrarlayan kod ve diğer kötü tasarım işaretlerinden muzdariptir.

Diğer seçenek sanırım bir tür giriş durumu sistemi korumaktır - belki de bu çok sayıda kayıt / kayıt sicilini daha temiz bir yere (giriş sisteminde bir tür yığınla) yeniden düzenlemek için kayıt fonksiyonundaki başka bir delege veya belki de dizilerin dizileri tutmak ve ne yapmamak.

Eminim buradaki biri bu problemle karşılaştı. Nasıl çözdün?

tl; dr Bir olay sistemindeki başka bir belirli girdiden sonra alınan belirli girdilerle nasıl başa çıkabilirim?

Yanıtlar:


7

iki seçenek: eğer "iç içe giriş" vakaları en fazla üç, dört ise, sadece bayrakları kullanırdım. "Bir nesneyi mi tutuyorsun? Ateş edemezsin." Başka herhangi bir şey onu yeniliyor.

Aksi takdirde, olay işleyicileri için anahtar başına girdi yığını tutabilirsiniz.

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
}

Ya da bu etki için bir şey :)

Düzenleme : birisi yönetilemez if-else yapıları bahsetti. bir girdi olayı işleme rutini için tam veriye mi gideceğiz? Kesinlikle yapabilirsin, ama neden?

Her neyse, bunun için:

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

Düzenle 2

Kylotan, her oyunun sahip olması gereken temel bir özellik olan anahtar eşlemeden bahsetti (erişilebilirliği de düşünün). Tuş eşlemesini eklemek farklı bir hikaye.

Tuşa basma kombinasyonuna veya diziye bağlı olarak davranışın değiştirilmesi sınırlayıcıdır. O kısmı gözden kaçırdım.

Davranış, girişle değil oyun mantığıyla ilgilidir. Bu oldukça açık, düşünmeye geliyor.

Bu nedenle, aşağıdaki çözümü öneriyorum:

// //>

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

// //<

Bu, benden daha akıllı insanlar tarafından birçok yönden iyileştirilebilir, ancak bunun da iyi bir başlangıç ​​noktası olduğuna inanıyorum.


Basit oyunlar için iyi çalışıyor - şimdi bunun için bir anahtar eşleştiriciyi nasıl kodlayacaksınız? :) Bu tek başına girdiniz için veri odaklı gitmek için iyi bir neden olabilir.
Kylotan

@Kylotan, bu gerçekten iyi bir gözlem, cevabımı düzenleyeceğim.
Raine

Bu harika bir cevap. İşte ödülün: P
Komünist Ördek

Komünist Ördek - teşekkürler chap, umarım bu yardımcı olur.
Paine

11

Daha önce de belirttiğin gibi bir devlet sistemi kullandık.

Önceden eşlenmiş anahtarların geçişine izin verip vermeyecek bir bayrağa sahip belirli bir durum için tüm anahtarları içeren bir harita oluştururduk. Durumları değiştirdiğimizde, yeni harita açılacak veya önceki bir harita açılacaktır.

Giriş durumlarının hızlı basit bir örneği Varsayılan, Menü İçi ve Sihirli Mod olabilir. Varsayılan, koştuğunuz ve oyunu oynadığınız yerdir. Menü içi, başlat menüsünde olduğunuzda veya bir mağaza menüsü, duraklatma menüsü, bir seçenekler ekranı açtığınızda olacaktır. Menüde geçiş yok bayrağı bulunur çünkü bir menüde gezinirken karakterinizin hareket etmesini istemezsiniz. Öte yandan, öğenin taşınmasıyla ilgili örneğinize benzer şekilde, Magic-Mode, eylem / öğe kullanım tuşlarını, büyü yapmak için yeniden eşleştirecek (bunu ses ve parçacık efektlerine de bağlayacağız, ancak bu biraz ötesinde) senin sorun).

Haritaların itilmesine ve patlamasına neden olan şey size kalmış ve aynı zamanda dürüstçe söyleyeceğim, harita yığınının temiz tutulduğundan emin olmak için belirli 'açık' olaylarımız olduğunu, seviye yüklemesinin en belirgin zaman olduğunu (Cutscenes de zamanlar).

Bu yardımcı olur umarım.

TL; DR - Bastırabileceğiniz ve / veya patlatabileceğiniz durumları ve giriş haritasını kullanın. Haritanın önceki girdiyi tamamen kaldırıp kaldırmayacağını söylemek için bir bayrak ekleyin.


5
BU. Sayfalar ve iç içe ifadeler şeytan ise iç içe geçmiş sayfalar.
michael.bartnett

+1. Giriş hakkında düşündüğümde, her zaman akrobat Okuyucu'm var - Seçim Aracı, El Aracı, Seçim Çerçevesi Yakınlaştırma. IMO, bir yığın kullanmak zaman zaman aşırıya kaçabilir. GEF bunu AbstractTool aracılığıyla gizler . JHotDraw, Araç uygulamalarının hoş bir hiyerarşi görünümüne sahiptir .
Stefan Hanke

2

Bu, mirasın sorununuzu çözebileceği bir duruma benziyor. Varsayılan davranışı uygulayan bir grup yöntemle temel sınıfınız olabilir. Daha sonra bu sınıfı genişletebilir ve bazı yöntemleri geçersiz kılabilirsiniz. Anahtarlama modu sadece geçerli uygulamayı değiştirme meselesidir.

İşte bazı sahte kodlar

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

Bu James'in önerdiğine benzer.


0

Herhangi bir dilde tam kodu yazmıyorum. Sana fikir veriyorum.

1) Önemli eylemlerinizi etkinliklerinizle eşleyin.

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

2) Etkinliklerinizi aşağıda verildiği gibi atayın / değiştirin

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

Atlama eyleminizin, atlama ve ateş gibi diğer olaylarla ayrı kalmasına izin verin.

Yönetilemez koda yol açacağı için burada ..else .. koşullu denetimlerinden kaçının.


Bu bana hiç yardımcı olmuyor. Yüksek düzeyde bağlantı sorunu var ve tekrarlayan kod var gibi görünüyor.
Komünist Ördek

Sadece daha iyi bir anlayış elde etmek için - Bu konuda "tekrarlayan" ne olduğunu ve "yüksek düzeyde bağlantı" nerede gördüğünüzü açıklayabilir misiniz.
inRazor

Kod örneğimi görürseniz, işlevin kendisindeki tüm eylemleri kaldırmam gerekir. Hepsini fonksiyonun kendisine kodluyorum - ya iki fonksiyon aynı kayıt / kayıt kaydını paylaşmak istiyorsa? Ya da kodu çoğaltmak zorunda kalacak VEYA onları çift etmek zorunda kalacak. Ayrıca tüm istenmeyen eylemleri kaldırmak için büyük miktarda kod olurdu. Son olarak, onları değiştirmek için tüm orijinal eylemleri 'hatırlayan' bir yere sahip olmalıyım.
Komünist Ördek

Kodunuzdaki gerçek işlevlerden ikisini (gizli pelerin işlevi gibi) eksiksiz kayıt / kayıt silme ifadeleriyle verebilir misiniz?
inRazor

Aslında şu anda bu eylemler için kod yok. Ancak, secret cloakateş, koşma, yürüyüş ve silah değiştirme gibi şeylerin kayıtsız kalması ve kilidini açması gerekir.
Komünist Ördek

0

Kaydı silmek yerine, sadece bir durum belirtin ve ardından yeniden kaydolun.

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.

Tabii ki, bu basit fikri uzanan Hareket için ayrı devletler var ve böyle ve daha sonra yerine "Peki dikte, burada işler hepsi çıkabileceği olacaktır olamaz Gizli Cloak Modunda iken yapın burada her şeyi ben var olabilir "Gizli Pelerin Modunda".

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.