Bileşen tabanlı varlık sistemini pratikte kullanmak


59

Dün, GDC Canada’dan Attribute / Behavior varlık sistemi hakkında bir sunum okudum ve bence oldukça güzel. Ancak, pratikte nasıl kullanılacağından emin değilim, sadece teoride değil. Her şeyden önce, bu sistemin nasıl çalıştığını hızlı bir şekilde açıklayacağım.


Her oyun varlığı (oyun nesnesi) niteliklerden (= davranışlarla erişilebilen veriler, aynı zamanda 'harici kod' ile de erişilebilir) ve davranışlardan (= OnUpdate()ve içeren mantık OnMessage()) oluşur. Bu nedenle, örneğin, bir koparma klon içinde, her tuğla meydana gelecek (örneğin!): PositionAttribute , ColorAttribute , HealthAttribute , RenderableBehaviour , HitBehaviour . Sonuncusu şuna benzeyebilir (sadece C # ile yazılmış çalışmayan bir örnek):

void OnMessage(Message m)
{
    if (m is CollisionMessage) // CollisionMessage is inherited from Message
    {
        Entity otherEntity = m.CollidedWith; // Entity CollisionMessage.CollidedWith
        if (otherEntity.Type = EntityType.Ball) // Collided with ball
        {
            int brickHealth = GetAttribute<int>(Attribute.Health); // owner's attribute
            brickHealth -= otherEntity.GetAttribute<int>(Attribute.DamageImpact);
            SetAttribute<int>(Attribute.Health, brickHealth); // owner's attribute

            // If health is <= 0, "destroy" the brick
            if (brickHealth <= 0)
                SetAttribute<bool>(Attribute.Alive, false);
        }
    }
    else if (m is AttributeChangedMessage) // Some attribute has been changed 'externally'
    {
        if (m.Attribute == Attribute.Health)
        {
            // If health is <= 0, "destroy" the brick
            if (brickHealth <= 0)
                SetAttribute<bool>(Attribute.Alive, false);
        }
    }
}

Bu sistemle ilgileniyorsanız, buradan daha fazla okuyabilirsiniz (.ppt).


Benim sorum bu sistemle ilgilidir, ancak genellikle her bileşen tabanlı varlık sistemi. Bunların hiçbirinin gerçek bilgisayar oyunlarında nasıl çalıştığını hiç görmedim, çünkü iyi örnekler bulamıyorum ve birini bulursam belgelenmemiş, yorum yok ve bu yüzden anlamıyorum.

Peki ne sormak istiyorum? Davranışlar nasıl tasarlanır (bileşenler). Burada, GameDev SE'de okudum, en yaygın hatanın birçok bileşen yapmak ve basitçe "her şeyi bir bileşen yapmak" olduğunu okudum. Bir bileşenin içinde görüntülemeyi yapmamanın önerildiğini okudum, ancak bunun dışında gerçekleştirin (yani RenderableBehaviour yerine, belki de RenderableAttribute olmalı ve eğer bir varlık RenderableAttribute true olarak ayarlanmışsa, o zaman Renderer(sınıfla ilgili değil) bileşenleri, ancak motora kendisi) ekranda çekmek gerekir?).

Ancak, davranışlar / bileşenler ne durumda? Diyelim ki bir seviyem var ve seviyede bir Entity button, Entity doorsve var Entity player. Oynatıcı düğmeyle çarpıştığında (bu baskıyla değiştirilen bir zemin düğmesidir), basılır. Düğmeye basıldığında, kapıları açar. Peki, şimdi nasıl yapılır?

Bunun gibi bir şeyle karşılaştım: oyuncunun birşeyle çarpışıp çarpışmadığını kontrol eden CollisionBehaviour var . Bir düğmeyle çarpışırsa CollisionMessage, buttonvarlığa a gönderir . Mesaj gerekli tüm bilgileri içerecektir: düğme ile çarpışan. Düğme, alacak olan TogggeableBehaviour'a sahip CollisionMessage. Kiminle çarpıştığını kontrol eder ve eğer varlığın ağırlığı düğmeyi değiştirecek kadar büyükse, düğme değiştirilir. Şimdi, düğmenin ToggledAttribute özelliğini true değerine ayarlar . Tamam, peki ya şimdi?

Düğme, değiştirildiklerini bildirmek için diğer tüm nesnelere başka bir mesaj göndermeli mi? Sanırım böyle bir şey yaparsam binlerce mesajım olur ve oldukça karışık olur. Bu yüzden belki bu daha iyidir: kapılar, bunlara bağlı olan düğmeye basılıp basılmadığını kontrol eder ve OpenedAttribute özelliğini buna göre değiştirir. Fakat o zaman kapıların OnUpdate()yönteminin sürekli bir şeyler yapacağı anlamına gelir (bu gerçekten bir sorun mu?).

Ve ikinci problem: daha fazla çeşit butonum varsa. Biri baskıyla bastırılır, ikincisi şutla vurulur, üçüncüsü üzerine su dökülürse değiştirilir, bu da bunun gibi farklı davranışlara sahip olmak zorunda kalacağım anlamına gelir:

Behaviour -> ToggleableBehaviour -> ToggleOnPressureBehaviour
                                 -> ToggleOnShotBehaviour
                                 -> ToggleOnWaterBehaviour

Gerçek oyunlar böyle mi çalışıyor, yoksa ben sadece aptal mıyım? Belki sadece bir olabilir ToggleableBehaviour ve uygun davranması gerekir ButtonTypeAttribute . Yani eğer öyleyse ButtonType.Pressure, bunu yapar, eğer öyleyse ButtonType.Shot, başka bir şey yapar ...

Peki ben ne istiyorum? Doğru mu yapıyorum yoksa sormak isterim, yoksa sadece aptalım ve bileşenlerin amacını anlamadım. Bileşenlerin oyunlarda gerçekte nasıl çalıştığına dair iyi bir örnek bulamadım, bileşen sisteminin nasıl yapılacağını açıklayan bazı dersler buldum, ancak nasıl kullanılacağını öğretmedim.

Yanıtlar:


46

Bileşenler harika, ancak size iyi gelebilecek bir çözüm bulmak biraz zaman alabilir. Endişelenme, oraya gideceksin. :)

Bileşenleri düzenlemek

Neredeyse doğru yoldasınız diyebilirim. Çözümü tersten anlatmaya çalışacağım, kapıdan başlayarak ve düğmelerle bitene kadar. Uygulamam olayları yoğun şekilde kullanıyor; Aşağıda olayları nasıl daha verimli kullanabileceğinizi ve problem yaratmadıklarını açıklıyorum.

Aralarındaki varlıkları birbirine bağlayacak bir mekanizmanız varsa, şalter doğrudan basıldığını kapıya bildirir, sonra ne yapılacağına karar verir.

Varlıkları birbirine bağlayamazsanız, çözümünüz yaptığım şeye oldukça yakındır. Kapının genel bir olayı dinlemesini istiyorum ( SwitchActivatedEventbelki de). Anahtarlar devreye girdiğinde bu olayı yayınlarlar.

Birden fazla anahtar türüne sahipseniz PressureToggle, benim de yapabileceğim davranış WaterToggleve ShotToggledavranışlar varsa , üssün ToggleableBehaviouriyi olduğundan emin değilim , bu yüzden bunu yapmam gerekir (tabii ki iyi bir durum yoksa saklamak için sebep).

Behaviour -> ToggleOnPressureBehaviour
          -> ToggleOnShotBehaviour
          -> ToggleOnWaterBehaviour

Verimli olay işleme

Etrafta uçuşan çok fazla olay olduğundan endişelenmenize gelince, yapabileceğiniz bir şey var. Her bir bileşene, gerçekleşen her olaydan haberdar olmak yerine, o zaman doğru olay türü olup olmadığına bakmak için bileşen kontrolünü yapın, işte farklı bir mekanizma ...

Şuna benzeyen EventDispatcherbir subscribeyöntemle sahip olabilirsiniz (pseudocode):

EventDispatcher.subscribe(event_type, function)

Ardından, bir etkinlik yayınladığınızda gönderici türünü kontrol eder ve yalnızca belirli bir etkinliğe abone olan işlevleri bildirir. Bunu, etkinlik türlerini işlev listeleriyle ilişkilendiren bir harita olarak uygulayabilirsiniz.

Bu şekilde, sistem önemli ölçüde daha verimlidir: olay başına çok daha az işlev çağrısı vardır ve bileşenler doğru olay türünü aldıklarından ve iki kez kontrol etmek zorunda olmadıklarından emin olabilirler.

Bir süre önce StackOverflow'ta bunun basit bir uygulamasını gönderdim. Python'da yazılmıştır, ancak belki size hala yardımcı olabilir:
https://stackoverflow.com/a/7294148/627005

Bu uygulama oldukça geneldir: sadece bileşenlerden gelen işlevlerle değil, her türlü işlevle çalışır. Buna ihtiyacınız olmazsa, bunun yerine, yönteminizde functionbir behaviorparametre bulunabilir subscribe- bildirilmesi gereken davranış örneği.

Nitelikler ve davranışlar

Eski bileşenleri kullanmak yerine kendi kendime nitelikler ve davranışlar kullanmaya başladım . Ancak, bir Breakout oyunda sistemi nasıl kullanacağınızı açıklamanızdan, aşırıya kaçtığınızı düşünüyorum.

Nitelikleri yalnızca iki davranış aynı verilere erişmesi gerektiğinde kullanırım. Bu özellik, davranışları ayrı tutmaya yardımcı olur ve bileşenler arasındaki bağımlılık (bunlar özellik veya davranışlar gibi) birbirine karışmaz, çünkü çok basit ve net kurallar izlerler:

  • Öznitelikler başka bileşenler kullanmaz (ne başka öznitelikler, ne de davranışlar), kendi kendine yeterlidir.

  • Davranışlar diğer davranışları kullanmaz veya bilmez. Onlar sadece bazı nitelikler hakkında (kesinlikle ihtiyaç duyduklarıları) bilirler.

Bazı verilere davranışlardan yalnızca biri ve yalnızca biri tarafından ihtiyaç duyulduğunda, onu bir özniteliğe koymak için hiçbir neden göremiyorum, davranışın onu tutmasına izin verdim.


@ heishe adlı kullanıcının yorumu

Bu sorun normal bileşenlerde de olmaz mı?

Neyse, olay tiplerini kontrol etmek zorunda değilim çünkü her fonksiyon her zaman doğru olay tipini alacağından emin .

Ayrıca, davranışların bağımlılıkları (yani ihtiyaç duydukları nitelikler) yapım aşamasında çözümlenir, bu nedenle her güncellemede her bir özellik aramanıza gerek kalmaz.

Ve son olarak, oyun mantığı kodum için Python kullanıyorum (motor C ++ 'ta olsa da), bu yüzden döküm yapmaya gerek yok. Python ördek yazma işini yapar ve her şey yolunda gider. Ancak ördek yazarken bir dil kullanmasam bile, bunu yapardım (basitleştirilmiş örnek):

class SomeBehavior
{
  public:
    SomeBehavior(std::map<std::string, Attribute*> attribs, EventDispatcher* events)
        // For the purposes of this example, I'll assume that the attributes I
        // receive are the right ones. 
        : health_(static_cast<HealthAttribute*>(attribs["health"])),
          armor_(static_cast<ArmorAttribute*>(attribs["armor"]))
    {
        // Boost's polymorphic_downcast would probably be more secure than
        // a static_cast here, but nonetheless...
        // Also, I'd probably use some smart pointers instead of plain
        // old C pointers for the attributes.

        // This is how I'd subscribe a function to a certain type of event.
        // The dispatcher returns a `Subscription` object; the subscription 
        // is alive for as long this object is alive.
        subscription_ = events->subscribe(event::type<DamageEvent>(),
            std::bind(&SomeBehavior::onDamageEvent, this, _1));
    }

    void onDamageEvent(std::shared_ptr<Event> e)
    {
        DamageEvent* damage = boost::polymorphic_downcast<DamageEvent*>(e.get());
        // Simplistic and incorrect formula: health = health - damage + armor
        health_->value(health_->value() - damage->amount() + armor_->protection());
    }

    void update(boost::chrono::duration timePassed)
    {
        // Behaviors also have an `update` function, just like
        // traditional components.
    }

  private:
    HealthAttribute* health_;
    ArmorAttribute* armor_;
    EventDispatcher::Subscription subscription_;
};

Davranışlardan farklı olarak, özelliklerin hiçbir updateişlevi yoktur - gerek duymazlar, amaçları karmaşık oyun mantığı gerçekleştirmek için değil verileri tutmaktır.

Niteliklerinizin hala basit bir mantık göstermesini sağlayabilirsiniz. Bu örnekte, bunun daima doğru HealthAttributeolmasını sağlayabilirsiniz 0 <= value <= max_health. Aynı zamanda HealthCriticalEvent, yüzde 25'in altına düştüğünde, aynı varlığın diğer bileşenlerine de gönderebilir , ancak bundan daha karmaşık bir mantık gerçekleştiremez.


Bir nitelik sınıfı örneği:

class HealthAttribute : public EntityAttribute
{
  public:
    HealthAttribute(Entity* entity, double max, double critical)
        : max_(max), critical_(critical), current_(max)
    { }

    double value() const {
        return current_;
    }    

    void value(double val)
    {
        // Ensure that 0 <= current <= max 
        if (0 <= val && val <= max_)
            current_ = val;

        // Notify other components belonging to this entity that
        // health is too low.
        if (current_ <= critical_) {
            auto ev = std::shared_ptr<Event>(new HealthCriticalEvent())
            entity_->events().post(ev)
        }
    }

  private:
    double current_, max_, critical_;
};

Teşekkür ederim! Bu tam olarak istediğim bir saçmalık. Ayrıca EventDispatcher fikrinizi, tüm varlıklara iletilen basit mesajdan daha çok seviyorum. Şimdi, bana söylediğin son şeye: temel olarak Sağlık ve Hasar Etki'nin bu örnekteki nitelikler olması gerekmediğini söylüyorsun. Yani, nitelikler yerine, davranışların sadece özel değişkenleri olurlar mı? Bu, "Hasar Etki" nin olaydan geçeceği anlamına mı geliyor? Örneğin EventArgs.DamageImpact? Kulağa hoş geliyor ... Ama tuğlanın sağlığına göre rengini değiştirmesini isteseydim, o zaman Sağlık bir nitelik olmalı, değil mi? Teşekkür ederim!
TomsonTom

2
@TomsonTom Evet, bu kadar. Olaylara sahip olmak, dinleyicilerin bilmesi gereken verileri tutarsa ​​çok iyi bir çözümdür.
Paul Manta

3
Bu harika bir cevap! (pdf'iniz gibi) - Şansınız varken , bu sistemle işlemeyi nasıl ele aldığınız konusunda biraz bilgi verebilir misiniz ? Bu özellik / davranış modeli benim için tamamen yeni, ama çok merak uyandırıcı.
Michael,

1
@ Tomson Tom render hakkında, Michael'a verdiğim cevabı gör. Çarpışmalar gelince, ben şahsen bir kısayol aldı. Kullanımı çok kolay olan ve çarpışmaları elimden çok daha iyi ele alan Box2D adlı bir kütüphane kullandım. Ancak kütüphaneyi doğrudan oyun mantığı kodumda kullanmıyorum. Her Entitybirinin EntityBodytüm çirkin bitleri uzaklaştıran bir var. Davranışlar daha sonra pozisyonu okuyabilir, EntityBodyona kuvvet uygulayabilir, eklemleri ve vücudun sahip olduğu motorları kullanabilir.
Paul Manta

1
@thelinuxlich Yani Artemis'in geliştiricisisin! : D Component/ Systemşemasını kartlarda birkaç kez referans aldığımı gördüm . Bizim uygulamalarımızın gerçekten de birkaç benzerliği var.
Paul Manta
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.