Bileşen tabanlı bir varlık sisteminde mesaj işleme nasıl düzgün bir şekilde gerçekleştirilir?


30

Aşağıdaki özelliklere sahip bir varlık sistemi varyantı uyguluyorum:

  • Bileşenleri birbirine bağlayan bir kimlikten biraz daha küçük olan bir varlık sınıfı

  • "Bileşen mantığı" olmayan bir grup bileşen sınıfı , sadece veri

  • Bir grup sistem sınıfı ("alt sistemler", "yöneticiler"). Bunlar tüm varlık mantık işlemlerini yapar. Çoğu temel durumda, sistemler sadece ilgilendikleri varlıkların listesini yineler ve her biri için bir eylemde bulunur

  • Tüm oyun sistemleri tarafından paylaşılan bir MessageChannel sınıfı nesnesi . Her sistem dinlemek için belirli mesaj türlerine abone olabilir ve aynı zamanda diğer sistemlere mesaj yayınlamak için kanalı kullanabilir.

Sistem mesajı işlemenin ilk değişkeni şuydu:

  1. Her oyun sisteminde art arda bir güncelleme çalıştırın
  2. Bir sistem bir bileşene bir şey yaparsa ve bu işlem diğer sistemlere ilgi gösteriyorsa, sistem uygun bir mesaj gönderir (örneğin, bir sistem

    messageChannel.Broadcast(new EntityMovedMessage(entity, oldPosition, newPosition))

    bir varlık ne zaman taşınırsa)

  3. Belirli bir mesaja abone olan her sistem, onun adı verilen mesaj işleme yöntemini alır.

  4. Bir sistem bir olayla ilgileniyorsa ve olay işleme mantığı başka bir mesajın yayınlanmasını gerektiriyorsa, mesaj hemen yayınlanır ve başka bir mesaj işleme yöntemi zinciri çağrılır

Bu değişken çarpışma algılama sistemini optimize etmeye başlayana kadar iyiydi (varlık sayısı arttıkça yavaşlıyordu). İlk başta basit bir kaba kuvvet algoritması kullanarak her varlık çiftini yinelemeliydi. Daha sonra, belirli bir hücrenin alanı içindeki varlıkları depolayan bir hücre ızgarasına sahip bir "uzamsal indeksi" ekledim, böylece sadece komşu hücrelerdeki varlıklar üzerinde kontroller yapmayı sağladım.

Bir işletme her hareket ettiğinde, çarpışma sistemi işletmenin yeni pozisyonda bir şeyle çarpışıp çarpışmadığını kontrol eder. Öyleyse, bir çarpışma algılanır. Ve eğer her iki çarpışan varlık "fiziksel nesneler" ise (her ikisi de RigidBody bileşenine sahipler ve aynı alanı işgal etmeyecek şekilde birbirlerini uzağa itmek istiyorlarsa), özel bir katı cisim ayrıştırma sistemi, hareket sistemini varlıklardan bazılarına taşımasını ister. onları ayıracak belirli pozisyonlar. Bu da hareket sisteminin değişen varlık pozisyonları hakkında bildirimde bulunan mesajlar göndermesine neden olur. Çarpışma algılama sistemi tepki vermek içindir, çünkü uzaysal endeksini güncellemesi gerekir.

Bazı durumlarda, hücre içeriği (C # 'daki genel bir Varlık nesneleri Listesi) yinelenirken değiştirildiğinden ve yineleyici tarafından atılmasının bir istisna olmasına neden olduğu için bir soruna neden olur.

Peki ... çarpışma kontrolü yapılırken çarpışma sisteminin durmasını nasıl önleyebilirim?

Tabii ki hücre içeriğinin doğru bir şekilde yinelenmesini sağlayan bazı "akıllı" / "zor" bir mantık ekleyebilirim, ancak sorunun çarpışma sisteminde değil (diğer sistemlerde de benzer problemler yaşadım) olduğunu düşünüyorum. mesajlar sistemden sisteme giderken ele alınır. İhtiyacım olan, belirli bir olay işleme yönteminin herhangi bir kesinti olmadan işini yapmasını sağlamak için bir yol.

Ne denedim:

  • Gelen mesaj kuyrukları . Bazı sistemler bir mesajı her yayınladığında, mesaj, ilgilenen sistemlerin mesaj sıralarına eklenir. Bu mesajlar her kareye bir sistem güncellemesi çağrıldığında işlenir. Sorun : eğer sistem A sistemin B kuyruğuna bir mesaj eklerse, eğer sistem B'nin sistem A'dan daha sonra güncellenmesi gerekiyorsa iyi çalışır (aynı oyun çerçevesinde); aksi takdirde mesajın bir sonraki oyun karesini işlemesine neden olur (bazı sistemler için istenmez)
  • Giden mesaj kuyrukları . Bir sistem bir olayı ele alırken yayınladığı tüm mesajlar giden mesaj sırasına eklenir. İletilerin bir sistem güncellemesinin işlenmesini beklemesi gerekmez: ilk ileti işleyicisinin çalışması bittikten sonra "derhal" ele alınırlar. Mesajların kullanılması diğer mesajların yayınlanmasına neden olursa, onlar da giden bir kuyruğa eklenir, böylece tüm mesajlar aynı çerçevede ele alınır. Sorun: eğer varlık ömür boyu sistemi (bir sistemle varlık ömür boyu yönetimini uyguladım) bir varlık yaratırsa, bazı A ve B sistemlerini bu konuda bilgilendirir. Sistem A mesajı iletirken, sonuçta yaratılan varlığın tahrip olmasına neden olan bir mesaj zincirine neden olur (örneğin, bir mermi varlığının tam olarak bir engelle çarpıştığı ve merminin kendi kendini yok etmesine neden olduğu yerde yaratıldığı). Mesaj zinciri çözülürken, B sistemi varlık oluşturma mesajını alamaz. Böylece, eğer B sistemi varlık imha mesajıyla da ilgileniyorsa, onu alır ve sadece "zincir" çözüldükten sonra, başlangıç ​​varlık oluşturma mesajını alır. Bu, imha mesajının göz ardı edilmesine, yaratma mesajının "kabul edilmesine" neden olur,

EDIT - SORULAR VE CEVAPLAR YANITLAR:

  • Çarpışma sistemi üzerindeyken hücrenin içeriğini kim değiştirir?

Çarpışma sistemi bazı varlıklar ve komşuları üzerinde çarpışma kontrolleri yaparken, bir çarpışma algılanabilir ve varlık sistemi hemen başka sistemler tarafından tepki verilecek bir mesaj gönderir. Mesaja verilen tepki, diğer mesajların oluşturulmasına ve hemen ele alınmasına neden olabilir. Bu nedenle diğer bazı sistemler çarpışma sisteminin daha sonra hemen işlemesi gerektiğine dair bir mesaj oluşturabilir (örneğin, bir varlık taşındı, böylece çarpışma sistemi mekansal endeksini güncellemesi gerekiyordu), önceki çarpışma kontrolleri henüz bitmedi.

  • Global bir giden mesaj sırası ile çalışamaz mısın?

Son zamanlarda tek bir küresel kuyruk denedim. Yeni sorunlara neden olur. Sorun: Bir tank varlığını bir duvar varlığına taşırım (tank klavyeyle kontrol edilir). Sonra tankın yönünü değiştirmeye karar verdim. Tankı ve duvarı her çerçeveye ayırmak için, CollidingRigidBodySeparationSystem, tankı duvardan mümkün olan en küçük miktarda uzaklaştırır. Ayırma yönü, tankın hareket yönünün tersi olmalıdır (oyun çizimi başladığında, tank hiç bir zaman duvara hareket etmemiş gibi görünmelidir). Ancak, yön YENİ yönün tersi olur, böylece tankı başlangıçta olduğundan farklı bir duvar tarafına hareket ettirir. Neden sorun ortaya çıkıyor: Mesajların şu anda nasıl ele alındığı (basitleştirilmiş kod):

public void Update(int deltaTime)
{   
    m_messageQueue.Enqueue(new TimePassedMessage(deltaTime));
    while (m_messageQueue.Count > 0)
    {
        Message message = m_messageQueue.Dequeue();
        this.Broadcast(message);
    }
}

private void Broadcast(Message message)
{       
    if (m_messageListenersByMessageType.ContainsKey(message.GetType()))
    {
        // NOTE: all IMessageListener objects here are systems.
        List<IMessageListener> messageListeners = m_messageListenersByMessageType[message.GetType()];
        foreach (IMessageListener listener in messageListeners)
        {
            listener.ReceiveMessage(message);
        }
    }
}

Kod böyle akıyor (ilk oyun karesi olmadığını varsayalım):

  1. Sistemler TimePassedMessage'ı işlemeye başlar
  2. InputHandingSystem tuş basmalarını varlık hareketine dönüştürür (bu durumda, sol ok MoveWest hareketine dönüşür). Varlık işlemi ActionExecutor bileşeninde saklanır
  3. ActionExecutionSystem , varlık eylemine tepki olarak, mesaj sırasının sonuna bir MovementDirectionChangeRequestedMessage ekler
  4. MovementSystem , Velocity bileşen verisine dayanarak varlık pozisyonunu taşır ve sıranın sonuna PositionChangedMessage mesajı ekler. Hareket önceki karenin hareket yönü / hızı kullanılarak yapılır (kuzeye diyelim)
  5. Sistemler TimePassedMessage işlemeyi durduruyor
  6. Sistemler MovementDirectionChangeRequestedMessage işlemeye başlar
  7. MovementSystem varlık hızını / hareket yönünü istendiği gibi değiştirir
  8. Sistemler işlemeyi durdurur MovementDirectionChangeRequestedMessage
  9. Sistemler PositionChangedMessage'ı işlemeye başlar
  10. CollisionDetectionSystem bir varlığın taşınması nedeniyle başka bir varlığa çarptığını tespit etti (tank bir duvarın içine girdi). Sıraya bir CollisionOccuredMessage ekler
  11. Sistemler PositionChangedMessage işlemini durdurur
  12. Sistemler CollisionOccuredMessage'ı işlemeye başlar
  13. ÇarpışanRigidBodySeparationSystem, tankı ve duvarı ayırarak çarpışmaya tepki verir. Duvar statik olduğundan, sadece tank hareket ettirilir. Tankların hareket yönü, tankın nereden geldiğinin bir göstergesi olarak kullanılır. Ters yönde dengelenmiş

HATA: Tank bu çerçeveyi hareket ettirdiğinde, önceki çerçeveden hareket yönü kullanılarak hareket etti, ancak ayrıldığı zaman, THIS çerçevesinden hareket yönü, zaten farklı olsa bile kullanıldı. Böyle çalışması gerekmiyor!

Bu hatayı önlemek için eski hareket yönünün bir yere kaydedilmesi gerekir. Sadece bu hatayı düzeltmek için bazı bileşenlere ekleyebilirim, ancak bu durum mesajların temelde yanlış bir şekilde kullanıldığını göstermiyor mu? Ayırma sistemi neden kullandığı hareket yönünü önemsiyor? Bu sorunu zarif bir şekilde nasıl çözebilirim?

  • Gördüğünüz sorunların bir kısmını hangi tarafa attığını belirlemek için, Aspect’le neler yaptıklarını görmek için gamadu.com/artemis sayfasını okumak isteyebilirsiniz.

Aslında, bir süredir Artemis ile tanıştım. Kaynak kodunu araştırdı, forumları vb. Okudum. Ama "Yönler" in yalnızca birkaç yerde bahsedildiğini gördüm ve anladığım kadarıyla temelde "Sistemler" anlamına geliyorlar. Ancak Artemis'in bazı sorunlarımı nasıl yönlendirdiğini anlayamıyorum. Mesaj bile kullanmıyor.

  • Ayrıca bakınız: "Varlık iletişimi: Mesaj kuyruğu - Yayın / Abone vs Signal / Slots"

Varlık sistemleriyle ilgili tüm gamedev.stackexchange sorularını zaten okudum. Bu, karşılaştığım sorunları tartışmıyor gibi görünüyor. Bir şey mi kaçırıyorum?

  • İki vakayı farklı ele alın, ızgarayı güncellemek çarpışma sisteminin bir parçası olduğu için hareket mesajlarına dayanması gerekmez

Neyi kastettiğinden emin değilim. Eski CollisionDetectionSystem uygulamaları bir güncelleme üzerindeki çarpışmaları kontrol eder (bir TimePassedMessage kullanıldığında), ancak performans nedeniyle çekleri en aza indirmem gerekiyordu. Bu yüzden, bir varlık hareket ettiğinde çarpışma kontrolüne geçtim (oyunumdaki çoğu varlık statiktir).


Bana açık olmayan bir şey var. Çarpışma sistemi üzerindeyken hücrenin içeriğini kim değiştirir?
Paul Manta

Global bir giden mesaj kuyruğu ile çalışamaz mısın? Bu nedenle, oradaki tüm mesajlar bir sistem yapıldıktan sonra her defasında gönderilir, buna sistemin kendi kendini imha etmesi de dahildir.
Roy T.

Bu kıvrımlı tasarımı korumak istiyorsanız, @RoyT izlemelisiniz. 'nin tavsiyesi, sıralama probleminizi çözmenin tek yolu (karmaşık, zamana dayalı mesajlaşma olmadan). Gördüğünüz sorunların bir kısmını hangi tarafa attığını belirlemek için, Aspect’le neler yaptıklarını görmek için gamadu.com/artemis sayfasını okumak isteyebilirsiniz .
Patrick Hughes,


2
Axum'un CTP'yi indirerek ve bazı kodları derleyerek nasıl yaptığını öğrenmek isteyebilir ve ardından ILSpy kullanarak sonucu C # ile tersine çevirebilirsiniz. İleti iletmek, aktör model dillerinin önemli bir özelliğidir ve Microsoft'un ne yaptıklarını bildiğinden eminim - bu nedenle 'en iyi' uygulamaya sahip olduklarını görebilirsiniz.
Jonathan Dickinson,

Yanıtlar:


12

Muhtemelen Tanrı / Blob nesnesinin anti-paternini duymuşsunuzdur. Senin problemin bir Tanrı / Kabarcık döngüsü. Mesaj geçiş sisteminizle uğraşmak en iyi ihtimalle bir Band-Aid çözümü sağlayacaktır ve en kötüsü tam bir zaman kaybı olacaktır. Aslında, probleminizde oyun geliştirmeyle hiçbir ilgisi yoktur. Kendimi bir koleksiyonda birkaç kez yineleyerek değiştirmeye çalışırken yakaladım ve çözüm her zaman aynı: alt böl, alt böl, alt böl.

Sorunuzun ifadesini anladığım gibi, çarpışma sisteminizi güncelleme yönteminiz şu anda geniş çapta aşağıdaki gibi görünüyor.

for each possible collision
    check for collision
    handle collision
    modify collision world to reflect change // exception happens here

Bunun gibi açıkça yazılmış, sadece bir tane olması gerektiğinde, döngünüzün üç sorumluluğu olduğunu görebilirsiniz. Sorununuzu çözmek için, mevcut döngünüzü üç farklı algoritmik geçişi temsil eden üç ayrı döngüye bölün .

for each possible collision
    check for collision, record it if a collision occurs

for each found collision
    handle collision, record the collision response (delete object, ignore, etc.)

for each collision response
    modify collision world according to response

Orijinal döngünüzü üç alt gruba bölerek, artık üzerinde yineleme yaptığınız koleksiyonu değiştirmeye çalışamazsınız. Ayrıca, orijinal döngünüzden daha fazla iş yapmadığınızı ve aslında aynı işlemleri birçok kez sırasıyla yaparak bazı önbellek kazançları elde edebileceğinizi unutmayın.

Ayrıca, paralelliğinizi kodunuza ekleyebileceğiniz başka bir fayda vardır . Kombine döngü yaklaşımınız doğal olarak seridir (temelde eşzamanlı değişiklik istisnasının size anlattığı şey budur!), Çünkü her döngü yineleme potansiyel olarak çarpışma dünyanıza hem okur hem de yazar. Bununla birlikte, yukarıda sunduğum üç alt döngü, hepsi ya okundu ya da yazdı, ancak ikisini birden değil. En azından ilk geçişte, olası tüm çarpışmaları kontrol etmek utanç verici bir şekilde paralel hale geldi ve kodunuzu nasıl yazdığınıza bağlı olarak ikinci ve üçüncü geçişler de olabilir.


Buna tamamen katılıyorum. Oyunumda buna çok benzer bir yaklaşım kullanıyorum ve bunun uzun vadede işe yarayacağına inanıyorum. Bu çarpışma sisteminin (veya yöneticisinin) nasıl çalışması gerektiğidir (aslında bir mesajlaşma sistemine sahip olmamanın mümkün olduğuna inanıyorum).
Emiliano

11

Bileşen tabanlı bir varlık sisteminde mesaj işleme nasıl düzgün bir şekilde gerçekleştirilir?

İki tür mesaj istediğinizi söyleyebilirim: Eşzamanlı ve Eşzamansız. Eşzamanlı mesajlar, eşzamansız aynı yığın çerçevesinde değil (aynı oyun karesinde ele alınabilir) iken hemen ele alınır. Genellikle "mesaj sınıfı başına" bazında verilen karar, örneğin "tüm EnemyDied mesajları asenkrondir".

Bazı olaylar, bu yollardan biriyle çok daha kolaydır. Örneğin, benim deneyimlerime göre bir ObjectGetsDeletedNow - olayı çok daha az seksi ve geri aramaların uygulanması ObjectWillBeDeletedAtEndOfFrame'den daha zor. Sonra tekrar, herhangi bir "veto" benzeri mesaj işleyicisi (yürütüldüklerinde bazı eylemleri iptal edebilen veya değiştirebilen kod, bir Shield efekti gibi, DamageEvent'i değiştirir ), zaman uyumsuz ortamlarda kolay olmayacak senkron aramalar

Zaman uyumsuz bazı durumlarda daha etkili olabilir (örneğin, nesne daha sonra silindiğinde bazı olay işleyicilerini atlayabilirsiniz). Bazen senkronizasyon daha verimlidir, özellikle bir olayın parametresini hesaplamak pahalıya mal olur ve önceden hesaplanmış değerler yerine belirli parametreleri almak için geri arama işlevlerini geçmek yerine (zaten bu özel parametreye hiç kimsenin ilgisini çekmemesi durumunda) geri arama işlevlerini geçmek istersiniz.

Sadece senkronize mesaj sistemlerinde başka bir genel problemden bahsettiniz: Senkronize mesaj sistemlerinde edindiğim tecrübeye göre, genel olarak en fazla hata ve keder durumlarından biri, bu listeler üzerinde yinelenirken liste değişikliğidir.

Bir düşünün: Senkronize bir yapıya sahiptir (bir eylemin tüm etkilerini hemen uygulayın) ve mesaj sistemi (alıcının göndericiden ayrılması, böylece gönderenin kime eylemlere kimin tepki verdiğini bilmemesi gereğidir; bu tür döngüler tespit edin. Demek istediğim: Bu tür kendi kendini değiştiren yinelemeleri çok fazla kullanmaya hazır olun. "Tasarım" olarak tür. ;-)

Çarpışma kontrolü yapılırken çarpışma sisteminin durmasını nasıl önleyebilirim?

Çarpışma algılamasıyla ilgili özel probleminiz için, çarpışma olaylarını senkronize etmemeniz yeterli olabilir, bu nedenle çarpışma yöneticisi tamamlanıp ardından bir toplu iş olarak (veya çerçevenin bazı noktalarında) toplu iş olarak yürütülene kadar sıraya alınırlar. Bu sizin çözümünüz "gelen sıra" dır.

Sorun: eğer bir sistem A sistemin B kuyruğuna bir mesaj eklerse, eğer sistem B'nin sistem A'dan daha sonra güncellenmesi gerekiyorsa iyi çalışır (aynı oyun çerçevesinde); aksi takdirde mesajın bir sonraki oyun karesini işlemesine neden olur (bazı sistemler için istenmez)

Kolay:

while (! queue.empty ()) {queue.pop (). handle (); }

Hiçbir mesaj kalana kadar kuyruğu tekrar tekrar çalıştırın. (Şimdi "sonsuz döngü" diye bağırırsanız, bir sonraki kareye ertelenmesi durumunda büyük olasılıkla bu sorunu "ileti spam" olarak alacağınızı unutmayın. Sonsuz döngüleri algılamak için () aklı başında bir yineleme sayısı için () iddiasında bulunabilirsiniz. Eğer böyle hissediyorum;))


Asenkron mesajların ne zaman işlendiğini tam olarak "ne zaman" hakkında konuşmadığımı unutmayın. Kanımca çarpışma tespit modülünün bittikten sonra mesajlarını temizlemesine izin vermek bence mükemmel. Bunu ayrıca "senkronize mesajlar, döngünün sonuna kadar gecikmeli" veya "şık bir şekilde yinelemenin" yinelemeyi değiştirilebilecek şekilde değiştirilebileceği şekilde uygulayabilmesi "olarak da düşünebilirsiniz
Imi

5

Aslında ECS'nin veri odaklı tasarım doğasını kullanmaya çalışıyorsanız, bunu yapmanın en DOD yolu hakkında düşünmek isteyebilirsiniz.

BitSquid bloguna bir göz atın , olaylarla ilgili özellikle parçasını. ECS ile iyi uyum sağlayan bir sistem sunulmuştur. Tüm olayları, mesaj başına yazılan temiz bir kuyruğa sıraya sokun, ECS'deki sistemler aynı şekilde bileşen başına. Daha sonra güncellenen sistemler, belirli bir ileti türünün bunları işlemesi için sıra üzerinde verimli bir şekilde yinelenebilir. Ya da sadece onları görmezden gel. Hangisi.

Örneğin, CollisionSystem çarpışma olaylarıyla dolu bir tampon üretecektir. Çarpışmadan sonra çalışan herhangi bir sistem daha sonra listede değişiklik yapabilir ve gerektiğinde bunları işleyebilir.

ECS tasarımının veri odaklı paralel niteliğini, mesaj kaydı ve benzeri tüm karmaşıklık olmadan sürdürür. Yalnızca, belirli bir olayı önemseyen sistemler, bu tür için sıra üzerinde yinelenir ve ileti kuyruğu üzerinde düz bir tek geçiş yineleme yapmak, alabileceğiniz kadar etkilidir.

Bileşenleri her bir sistemde düzenli bir şekilde düzenli tutarsanız (örneğin, tüm bileşenleri varlık kimliğine göre sipariş edin veya bunun gibi bir şey varsa), mesajların onları yinelemek ve ilgili bileşenleri aramak için en verimli sırada üretilmesi avantajını bile elde edersiniz. işlem sistemi Yani, varlıklar 1, 2 ve & 3'e sahipseniz, mesajlar bu sırada üretilir ve mesaj işlenirken gerçekleştirilen bileşen aramaları kesinlikle artan (en hızlı olan) adres sırasına göre artar.


1
+1, ancak bu yaklaşımın dezavantajı olmadığına inanamıyorum. Bu bizi sistemler arasındaki karşılıklı bağımlılıkları zorlamaya zorlamaz mı? Veya belki de bu bağımlılıkların bir şekilde veya başka bir şekilde kodlanmış olması kastedilmiştir?
Patryk Czachurski

2
@Dedaelus: Eğer oyun mantığı doğru mantığı yapmak için fizik güncellemelerine ihtiyaç duyuyorsa , bu bağımlılığa nasıl sahip olmayacaksınız ? Bir pubsub modelinde bile, yalnızca başka bir sistem tarafından oluşturulan mesaj türüne açıkça abone olmanız gerekir. Bağımlılıklardan kaçınmak zordur ve çoğunlukla sadece doğru katmanları bulmaktır. Örneğin grafikler ve fizik bağımsız, ancak enterpolasyonlu fizik simülasyonu güncellemelerinin grafiklere vb.
Yansımasını

Bu kabul edilen cevap olmalı. Bunu yapmanın basit bir yolu, yeni bir bileşen türü oluşturmaktır; örneğin, bir çarpışma gerçekleştikten sonra işleri yapmakla ilgilenen her sistem tarafından işlenecek olan Çarpışma Çözülebilir. Bu, Drake'in önerisine uygun olacaktı, ancak her alt bölme döngüsü için bir sistem var.
user8363 26.0315
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.