Varlık iletişimi nasıl çalışır?


115

İki kullanıcı durumum var:

  1. Nasıl entity_Abir take-damagemesaj gönderirsiniz entity_B?
  2. HP'nin entity_Asorgusu nasıl olur entity_B?

İşte şimdiye kadar karşılaştığım şey:

  • Mesaj sırası
    1. entity_Abir take-damagemesaj oluşturur ve entity_Bmesaj kuyruğuna gönderir.
    2. entity_Abir query-hpmesaj oluşturur ve gönderir entity_B. entity_Bkarşılığında bir response-hpmesaj oluşturur ve ona gönderir entity_A.
  • Yayınla / Abone
    1. entity_Btake-damagemesajlara abone olur (muhtemelen bazı önleyici filtrelemeyle yalnızca ilgili mesaj iletilir). referans veren bir mesaj entity_Aüretir .take-damageentity_B
    2. entity_Aupdate-hpmesajlara abone olur (filtrelendi). Her kare mesaj entity_Byayınlar update-hp.
  • Sinyal / Yuvaları
    1. ???
    2. entity_Abir bağlantı update-hpiçin yuvası entity_B'in update-hpsinyali.

Daha iyi bir şey var mı? Bu iletişim planlarının bir oyun motorunun varlık sistemine nasıl bağlanacağı hakkında doğru bir fikrim var mı?

Yanıtlar:


67

İyi soru! İstediğiniz belirli sorulara ulaşmadan önce şunu söyleyeceğim: sadelik gücünü küçümseme. Tenpn haklı. Bu yaklaşımlarla yapmaya çalıştığınız tek şeyin, bir işlev çağrısını ertelemek veya arayan kişiyi arayandan ayırmak için zarif bir yol bulmak olduğunu unutmayın. Coroutinler'i, bu sorunlardan bazılarını hafifletmek için şaşırtıcı derecede sezgisel bir yol olarak önerebilirim, ancak bu biraz konu dışı. Bazen, sadece işlevi çağırmaktan ve A öğesinin doğrudan varlık B'ye bağlı olduğu gerçeğiyle yaşamaktan daha iyisin.

Bu, basit mesaj geçişi ile birlikte sinyal / slot modelini kullandım ve mutlu oldum. C ++ ve Lua'da çok sıkı bir program olan oldukça başarılı bir iPhone unvanını kullandım.

Sinyal / slot durumunda, eğer A varlığının B varlığına cevap vermek için bir şeyler yapmasını istersem (örneğin, bir şey öldüğü zaman bir kapının kilidini açabilirim) A varlığının doğrudan varlık B'nin ölüm olayına abone olmasını sağlayabilirim. Veya muhtemelen A işletmesi, bir grup kuruluşun her birine abone olur, ateşlenen her olay için bir sayaç artırır ve N öldükten sonra kapının kilidini açar. Ayrıca, "varlık grubu" ve "bunlardan N" genellikle seviye verilerinde tanımlanan tasarımcı olacaktır. (Bir kenara bırakıldığında, bu, coroutine'lerin gerçekten parlayabileceği bir alandır, örneğin, WaitForMultiple ("Dying", entA, entB, entC); door.Unlock ();)

Ancak bu, C ++ koduna sıkıca bağlanan reaksiyonlara ya da doğal olarak geçici oyun olaylarına gelince hantal hale gelebilir: hasar verme, silah yükleme, hata ayıklama, oyuncu odaklı konum tabanlı AI geri bildirimleri. Bu, mesaj iletmenin boşlukları doldurabileceği yerdir. "Bu alandaki bütün varlıklara 3 saniye içinde zarar vermelerini söyle" ya da "kimi vurduğumu, bu senaryo işlevini çalıştırmalarını söylediklerini anladım." Bunu yayınlama / abone olma veya sinyal / yuva kullanarak güzel bir şekilde yapmayı öğrenmek zor.

Bu kolayca üstesinden gelinebilir (tenpn örneğine karşı). Çok fazla hareketiniz varsa, bu aynı zamanda kabalıksız olabilir. Ancak dezavantajlarına rağmen, bu "mesajlar ve olaylar" yaklaşımı kodlanmış oyun kodu (örneğin Lua'da) ile çok iyi örtüşüyor. Betik kodu, C ++ kodunun bakımı olmadan kendi mesajlarını ve olaylarını tanımlayabilir ve bunlara tepki verebilir. Ayrıca kod kodu, C ++ kodunu tetikleyen, seviye değiştirme, ses çalma ve hatta bir silahın TakeDamage mesajının ne kadar hasar vereceğini belirleme gibi mesajları kolayca gönderebilir. Bana bir sürü zaman kazandırdı, çünkü sürekli olarak etrafta dolanmak zorunda değildim. Ve tüm kodumu tek bir yerde tutmama izin verdi, çünkü çoğu yoktu. Düzgün bir şekilde bağlandığında,

Ayrıca, # 2 numaralı dava konusundaki deneyimim, onu diğer yönde bir olay olarak ele alma konusunda daha iyi olmanız. İşletmenin sağlığının ne olduğunu sormak yerine, sağlık önemli bir değişiklik yaptığında bir etkinlik başlat / mesaj gönder.

Arayüzler açısından, btw, bunların hepsini uygulamak için üç sınıfa gittim: EventHost, EventClient ve MessageClient. EventHosts yuvalar yaratır, EventClients abone olur / onlara bağlanır ve MessageClients bir temsilciyi bir mesajla ilişkilendirir. Bir MessageClient'in temsilci hedefinin mutlaka birliğin sahibi olduğu aynı nesne olması gerekmediğini unutmayın. Başka bir deyişle, MessageClients yalnızca mesajları diğer nesnelere iletmek için bulunabilir. FWIW, ev sahibi / müşteri metaforu uygun değil. Kaynak / Lavabo daha iyi kavramlar olabilir.

Üzgünüm, biraz şaşırdım. Bu benim ilk cevabım :) Umarım anlamlı olmuştur.


Cevap için teşekkürler. Harika görüşler. İleti iletmeyi bitirme sebebim Lua yüzünden. Yeni C ++ kodu olmadan yeni silahlar üretebilmek istiyorum. Yani düşünceleriniz sorulmamış sorularıma cevap verdi.
deft_code

Coroutines gelince ben de coroutinler inanıyorum, ama ben C + + ile onlarla oynamak asla. Engelleme çağrılarını ele almak için lua kodunda coroutine kullanma konusunda belirsiz bir umudum vardı (örneğin, ölüm bekle). Bu çabaya değer miydi? Korkarım ki c ++ 'daki koroutinler için yoğun arzum yüzünden kör olmuştum.
deft_code

Son olarak, iphone oyunu neydi? Kullandığınız varlık sistemi hakkında daha fazla bilgi alabilir miyim?
deft_code

2
Varlık sistemi daha çok C ++ 'daydı. Dolayısıyla, örneğin, Imp'in davranışını ele alan bir Imp sınıfı vardı. Lua, Imp'in parametrelerini doğduğunda veya mesajla değiştirebilir. Lua'nın hedefi sıkı bir programa uymaktı ve Lua kodunun hata ayıklaması çok zaman alıyor. Lua'yı komut dizileri için kullandık (hangi varlıklar nereye gidiyor, tetiklere bastığınızda gerçekleşen olaylar). Yani Lua'da, Imp'in elle kaydedilmiş bir fabrika birliği olduğu SpawnEnt ("Imp") gibi şeyler söylerdik. Her zaman bir küresel varlık havuzuna dönüşecekti. Güzel ve basit. Bir sürü smart_ptr ve weak_ptr kullandık.
BRaffle,

1
Yani, BananaRaffle: Bunun cevabınızın doğru bir özeti olduğunu söyler misiniz: "Gönderdiğiniz çözümlerin her birinin kendi kullanımları vardır, diğerleri gibi. Mükemmel bir çözüm aramayın, ihtiyaç duyduğunuz her şeyi kullanın. ."
Ipsquiggle,

76
// in entity_a's code:
entity_b->takeDamage();

Ticari oyunların bunu nasıl yaptığını sordunuz. ;)


8
Aşağı oy? Cidden, normalde böyle yapılır! Varlık sistemleri mükemmeldir ancak erken dönüm noktalarına ulaşmalarına yardımcı olmazlar.
tenpn,

Flash oyunlarını profesyonelce yapıyorum ve bu şekilde yapıyorum. Enemy.damage (10) olarak adlandırıyor ve sonra halka açık alanlardan ihtiyacınız olan her türlü bilgiyi araştırıyorsunuz.
Iain,

7
Bu cidden ticari oyun motorları bunu yapıyor. Şaka yapmıyor. Target.NotifyTakeDamage (DamageType, DamageAmount, DamageDealer, vb.) Genellikle nasıl çalıştığıdır.
AA Grapsas,

3
Ticari oyunlar da "zarar" yazıyor mu? :-P
Ricket

15
Evet, diğer şeylerin yanı sıra füze hasarı da yapıyorlar. :)
LearnCocos2D

17

Daha ciddi bir cevap:

Tahtaların çok kullandığını gördüm. Basit sürümler, bir varlığın sorgulayabilmesi için varlığın HP'si gibi şeylerle güncellenen desteklerden başka bir şey değildir.

Karatahtalarınız dünyanın bu varlık görüşü olabilir (B'nin tahtasına HP'nin ne olduğunu sorun) ya da bir işletmenin dünya görüşü olabilir (A'nın hedefinin ne olduğunu görmek için yazı tahtasını sorgular).

Karatahtaları yalnızca çerçevedeki bir eşitleme noktasında güncellerseniz, daha sonra herhangi bir iş parçacığından daha sonraki bir noktada okuyabilir ve çok okuyucunun uygulanmasını oldukça basit hale getirebilirsiniz.

Daha gelişmiş karatahtalar daha fazla karaktere benzeyebilir, dizeleri değerlere eşleyebilir. Bu daha sürdürülebilir ancak açık bir çalışma süresi maliyeti var.

Bir kara tahta geleneksel olarak sadece tek yönlü bir iletişimdir - zarar görmekten vazgeçmezdi.


Yazı tahtası modelini daha önce hiç duymamıştım.
deft_code

Bağımlılıkları azaltmak için de iyidirler, aynı olay sırası veya yayınlama / abone olma modelinde olduğu gibi.
tenpn

2
Bu aynı zamanda “ideal” E / C / S sisteminin “çalışması” gerektiği kanuni bir “tanım” dır. Sistemler, üzerine etki eden koddur. (Tabii ki, varlıklar, long long intsaf bir ECS sisteminde sadece s ya da benzerler.)
BRPocock

6

Bu konuyu biraz çalıştım ve güzel bir çözüm gördüm.

Temel olarak hepsi alt sistemlerle ilgili. Tenpn tarafından belirtilen karatahta fikrine benzer.

Varlıklar bileşenlerden oluşur, ancak bunlar yalnızca mülk torbalarıdır. Varlıkların kendilerinde davranış yoktur.

Diyelim ki, işletmelerin bir Sağlık bileşeni ve bir Hasar bileşeni vardır.

O zaman bazı MessageManager ve üç alt sisteminiz var: ActionSystem, DamageSystem, HealthSystem. Bir noktada ActionSystem oyun dünyasında hesaplarını yapar ve bir etkinlik oluşturur:

HIT, source=entity_A target=entity_B power=5

Bu etkinlik MessageManager'da yayınlandı. Şimdi bir noktada MessageManager bekleyen mesajlardan geçer ve DamageSystem'in HIT mesajlarına abone olduğunu tespit eder. Şimdi, MessageManager HIT mesajını DamageSystem'a iletir. HasageSystem, Hasar bileşenine sahip varlıklar listesinden geçer, isabet gücüne veya her iki varlığın diğer bazı durumlarına bağlı olarak hasar noktalarını hesaplar ve olayı yayınlar.

DAMAGE, source=entity_A target=entity_B amount=7

HealthSystem, DAMAGE mesajlarına abone oldu ve şimdi MessageManager, DAMAGE mesajını HealthSystem'a yayınladığında, HealthSystem, hem varlık hem de varlık_A ve varlık_B'ye, Sağlık bileşenleri ile erişebiliyor, böylece tekrar HealthSystem, hesaplarını yapabilir (ve ilgili olayı yayınlayabilir) MessageManager'a).

Böyle bir oyun motorunda, mesajların formatı tüm bileşenler ve alt sistemler arasındaki tek bağlantıdır. Alt sistemler ve varlıklar tamamen bağımsızdır ve birbirinden habersizdir.

Bazı gerçek oyun motorlarının bu fikri uygulayıp uygulamadığını bilmiyorum, ama oldukça sağlam ve temiz görünüyor ve bir gün hobisi seviyedeki oyun motorum için kendimi uyguladığımı umuyorum.


Bu, IMO tarafından kabul edilen cevaptan çok daha iyi bir cevaptır. Dekolte edilmiş, bakımı kolay ve genişletilebilir (ve aynı zamanda şaka cevabı gibi bir eşleşme felaketi değil entity_b->takeDamage();)
Danny Yaroslavski

4

Neden küresel bir mesaj kuyruğuna sahip değilsiniz ki:

messageQueue.push_back(shared_ptr<Event>(new DamageEvent(entityB, 10, entityA)));

İle:

DamageEvent(Entity* toDamage, uint amount, Entity* damageDealer);

Ve oyun döngüsünün / etkinliğin sonunda:

while(!messageQueue.empty())
{
    Event e = messageQueue.front();
    messageQueue.pop_front();
    e.Execute();
}

Bence bu Komut kalıbı. Ve türevlerin bir şeyleri tanımladığı ve yaptığı Execute()saf bir sanaldır Event. Yani burada:

DamageEvent::Execute() 
{
    toDamage->takeDamage(amount); // Or of course, you could now have entityA get points, or a recognition of damage, or anything.
}

3

Oyununuz tek oyuncuysa, sadece hedef nesneleri yöntemini kullanın (tenpn'nin önerdiği şekilde).

Çok oyuncusuysanız (ya da tam olarak çok merkezli olursanız), (ya da desteklemek istiyorsanız) bir komut sırası kullanın.

  • İstemci 1'de A B'ye zarar verdiğinde, sadece hasar olayını sıraya koy.
  • Komut kuyruklarını ağ üzerinden senkronize et
  • Kuyruğa alınmış komutları her iki taraftan da kullanın.

2
Hile yapmaktan kaçınmak konusunda ciddiysen, A istemcide B'yi hiç etkilemez. A'ya sahip olan istemci, sunucuya tam olarak "tenpn" in söylediğini yapan bir "saldırı" komutu gönderir; Sunucu daha sonra bu durumu tüm ilgili istemcilerle senkronize eder.

@Joe: Evet, göz önünde bulundurulması gereken geçerli bir sunucu varsa, ancak ağır sunucu yüklenmesini önlemek için müşteriye (örneğin bir konsolda) güvenebilirsiniz.
Andreas

2

Şunu söyleyebilirim: Açıkça hasardan anında anında geri bildirim almanız gerekmediği sürece, hiçbirini kullanmayın.

Hasar alan varlık / bileşen / ne olursa olsun olayları yerel olay sırasına veya hasar olaylarını tutan eşit düzeyde bir sisteme itmelidir.

Daha sonra olayları varlık a'dan talep eden ve varlık b'ye geçiren her iki varlığa erişimi olan bir bindirme sistemi olmalıdır. Bir olayı herhangi bir zamanda bir olaya aktarmak için herhangi bir yerden herhangi bir yerde kullanabileceği genel bir olay sistemi oluşturarak, kodun hata ayıklamasını her zaman kolaylaştıracak, performansı ölçmeyi kolay, anlaşılması ve okunması kolay ve sık sık yapan açık bir veri akışı oluşturursunuz Genel olarak daha iyi tasarlanmış bir sisteme yol açar.


1

Sadece aramayı yap. Sorgu-hp sorgusu tarafından engellenmiş yapmayın-hp - bu modeli izlerseniz, acı dünyasına girersiniz.

Mono Continuations'a da bir göz atmak istersiniz. NPC'ler için ideal olacağını düşünüyorum.


1

Öyleyse, aynı güncelleme () döngüsünde A ve B oyuncusu birbirinden vurmaya çalışıyorsa ne olur? Diyelim ki A oyuncusu için Güncelle (), 1. döngüdeki B oyuncusu için () tıklatmadan önce (veya işaretleyiniz veya ne olursa olsun) gerçekleşmelidir. Aklıma gelen iki senaryo var:

  1. Bir mesajla anında işleme:

    • oyuncu A.Update (), oyuncunun B'ye çarpmak istediğini görür, B oyuncusu, hasarı bildiren bir mesaj alır.
    • B.HandleMessage (), B oyuncusunun vuruş puanlarını günceller (o ölür)
    • oyuncu B.Update () oyuncu B öldüğünü görür .. oyuncu A'ya saldıramaz

Bu haksızlık, A ve B oyuncularının birbirlerini vurmaları gerekiyor, B oyuncusu A'ya girmeden önce öldü, çünkü bu varlık / gameobject daha sonra güncelleme aldı.

  1. Mesajın sıralanması

    • Oyuncu A.Update (), oyuncunun B'ye çarpmak istediğini görür, B oyuncusu, hasarı bildiren bir mesaj alır ve sırada saklar
    • Oyuncu A.Update () sırasını kontrol eder, boş
    • Oyuncu B.Update () ilk önce hamle olup olmadığını kontrol eder, böylece B oyuncusu A oyuncusu için de hasarlı olarak mesaj gönderir
    • Oyuncu B.Update () sıradaki mesajları da ele alır, A oynatıcısının hasarını işler
    • Yeni döngü (2): Oyuncu A bir sağlık iksiri içmek ister, böylece Oyuncu A. Güncelle () denir ve hamle işlenir
    • Oyuncu A.Update () mesaj kuyruğunu kontrol eder ve oyuncu B'den gelen hasarı işler

Yine bu haksızlık .. Oyuncu A'nın aynı dönüş / döngü / kene ile vuruş noktalarını alması gerekiyor!


4
Gerçekten soruyu cevaplamıyorsunuz ama bence cevabınız mükemmel bir soru olacaktır. Neden devam etmiyor ve böyle “haksız” bir önceliklendirmenin nasıl çözüleceğini sormuyorsunuz?
serseri

Oyunların çoğunun bu adaletsizliğe önem verdiğinden şüpheliyim, çünkü nadiren bir sorun olacak kadar sık ​​güncelleme yapıyorlar. Basit bir geçici çözüm, güncelleme yaparken varlık listesi boyunca ileri ve geri yineleme arasında geçiş yapmaktır.
Kylotan

2 çağrı kullanıyorum, bu yüzden tüm varlıklara Update () 'i çağırıyorum, sonra döngüden sonra tekrar tekrar ediyorum ve benzeri bir şey çağırıyorum pEntity->Flush( pMessages );. Entity_A yeni bir etkinlik oluşturduğunda, entity_B tarafından o çerçevede okunmaz (iksiri alma şansı da vardır), sonra hem hasar alır, hem de sıradaki sonuncusu olan iksir şifa mesajını işlerler . B oyuncusu yine de iksir mesajı kuyruktaki son mesaj olduğu için hala ölür: P ancak işaretçileri ölü varlıklara silmek gibi diğer mesaj türleri için yararlı olabilir.
Pablo Ariel

Bence çerçeve seviyelerinde oyun uygulamalarının çoğu adil değil. Kylotan'ın dediği gibi.
v.oddou

Bu problemi çözmek delice kolay. Hasar mesaj ileticilerinde veya her neyse birbirlerine uygulanır. Müzikçaları mesaj işleyicisinin içinde ölü olarak işaretlememelisin. "Güncelleme ()" de, basitçe "yapın" if (hp <= 0) die (); " (örneğin, "Güncelleme ()" nin başında). Bu şekilde ikisi de aynı anda birbirlerini öldürebilir. Ayrıca: Çoğu zaman oyuncuya doğrudan zarar vermezsiniz, ancak kurşun gibi bazı ara nesnelerden.
Tara,
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.