C ++ 'da Entity / Component Systems, Türleri ve yapı bileşenlerini nasıl keşfederim?


37

C ++ 'daki bir varlık bileşen sistemi üzerinde çalışıyorum, bu bileşenlerde Artemis stilini (http://piemaster.net/2011/07/entity-component-artemis/) takip etmeyi umuyorum. Mantığı içeren sistemler. Bu yaklaşımın veri merkezli özelliklerinden yararlanmayı ve bazı güzel içerik araçları oluşturmayı umuyorum.

Bununla birlikte, karşılaştığım bir huzursuzluk, bir veri dosyasından bazı tanımlayıcı dizgilerin veya GUID'lerin nasıl alınacağı ve bir Varlık için bileşen oluşturmak için bunu kullanmadır. Açıkçası sadece büyük bir ayrıştırma işlevine sahip olabilirim:

Component* ParseComponentType(const std::string &typeName)
{
    if (typeName == "RenderComponent") {
        return new RenderComponent();
    }

    else if (typeName == "TransformComponent") {
        return new TransformComponent();
    }

    else {
        return NULL:
    }
}

Ama bu gerçekten çirkin. Bileşenleri sık sık ekleme ve değiştirme ve prototip oluşturma amacıyla Lua'da bir bileşen ve sistem uygulayabilmeniz için umarım bir tür ScriptedComponentComponent oluşturma niyetindeyim. Bazı sınıflardan miras alan bir sınıf yazabilmek BaseComponent, belki de her şeyin işe yarayabilmesi için birkaç makro fırlatıp sonra sınıfın çalışma zamanında başlatılması için hazır olmasını istiyorum.

C # ve Java'da bu oldukça basit olacaktır, çünkü sınıfları ve yapıcıları aramak için güzel yansıma API'leri alırsınız. Fakat bunu C ++ 'da yapıyorum çünkü bu dilde yetkinliğimi artırmak istiyorum.

Peki bu C ++ 'da nasıl başarılır? RTTI'yı etkinleştirme hakkında okudum, ancak çoğu insan, özellikle de yalnızca nesne türleri alt kümesi için ihtiyaç duyduğum bir durumda buna karşı temkinli görünüyor. Özel bir RTTI sistemi orada ihtiyacım olan şeyse, bir tane yazmayı öğrenmeye başlamak için nereye gidebilirim?


1
Çok ilgisiz yorum: Eğer C ++ 'da uzman olmak istiyorsanız, C ++' ı değil, C 'yi kullanın. Bunun için üzgünüm ama söylenmesi gerekiyordu.
Chris diyor ki

Seni duyuyorum, bu bir oyuncak örneğiydi ve std :: string api'yi ezberledim. . . hala!
michael.bartnett

@bearcdp Cevabımda önemli bir güncelleme yayınladım. Uygulama şimdi daha sağlam ve verimli olmalı.
Paul Manta

@PaulManta Yanıtınızı güncellediğiniz için çok teşekkür ederiz! Ondan öğrenecek çok şey var.
michael.bartnett

Yanıtlar:


36

Yorum:
Artemis uygulaması ilginçtir. Benzer bir çözüm buldum; bileşenlerimi "Öznitelikler" ve "Davranışlar" olarak adlandırmam dışında. Bu bileşen türlerini ayırma yaklaşımı benim için çok iyi çalıştı.

Çözümle ilgili olarak:
Kod kullanımı kolaydır, ancak C ++ ile deneyimli değilseniz uygulamanın izlenmesi zor olabilir. Yani...

İstenen arayüz

Yaptığım, tüm bileşenlerin merkezi bir havuzuna sahip olmak. Her bileşen türü belirli bir dizeyle eşleştirilir (bileşen adını temsil eder). Sistemi böyle kullanıyorsunuz:

// Every time you write a new component class you have to register it.
// For that you use the `COMPONENT_REGISTER` macro.
class RenderingComponent : public Component
{
    // Bla, bla
};
COMPONENT_REGISTER(RenderingComponent, "RenderingComponent")

int main()
{
    // To then create an instance of a registered component all you have
    // to do is call the `create` function like so...
    Component* comp = component::create("RenderingComponent");

    // I found that if you have a special `create` function that returns a
    // pointer, it's best to have a corresponding `destroy` function
    // instead of using `delete` directly.
    component::destroy(comp);
}

Hayata geçirme

Uygulama o kadar da kötü değil, ama yine de oldukça karmaşık; bazı şablonlar ve işlev işaretçileri hakkında bilgi gerektirir.

Not: Joe Wreschnig, yorumlarda, özellikle önceki uygulamamın derleyicinin kodu optimize etme konusunda ne kadar iyi olduğu konusunda çok fazla varsayımda bulunduğunu; sorun zararlı değildi, imo, ama beni de rahatsız etti. Ayrıca eski COMPONENT_REGISTERmakronun şablonlarla çalışmadığını da fark ettim .

Kodu değiştirdim ve şimdi tüm bu sorunların giderilmesi gerekiyor. Makro şablonlarla çalışır ve Joe'nun gündeme getirdiği konular ele alınmıştır: şimdi derleyicilerin gereksiz kodu en iyi duruma getirmeleri çok daha kolaydır.

bileşen / component.h

#ifndef COMPONENT_COMPONENT_H
#define COMPONENT_COMPONENT_H

// Standard libraries
#include <string>

// Custom libraries
#include "detail.h"


class Component
{
    // ...
};


namespace component
{
    Component* create(const std::string& name);
    void destroy(const Component* comp);
}

#define COMPONENT_REGISTER(TYPE, NAME)                                        \
    namespace component {                                                     \
    namespace detail {                                                        \
    namespace                                                                 \
    {                                                                         \
        template<class T>                                                     \
        class ComponentRegistration;                                          \
                                                                              \
        template<>                                                            \
        class ComponentRegistration<TYPE>                                     \
        {                                                                     \
            static const ::component::detail::RegistryEntry<TYPE>& reg;       \
        };                                                                    \
                                                                              \
        const ::component::detail::RegistryEntry<TYPE>&                       \
            ComponentRegistration<TYPE>::reg =                                \
                ::component::detail::RegistryEntry<TYPE>::Instance(NAME);     \
    }}}


#endif // COMPONENT_COMPONENT_H

bileşen / detail.h

#ifndef COMPONENT_DETAIL_H
#define COMPONENT_DETAIL_H

// Standard libraries
#include <map>
#include <string>
#include <utility>

class Component;

namespace component
{
    namespace detail
    {
        typedef Component* (*CreateComponentFunc)();
        typedef std::map<std::string, CreateComponentFunc> ComponentRegistry;

        inline ComponentRegistry& getComponentRegistry()
        {
            static ComponentRegistry reg;
            return reg;
        }

        template<class T>
        Component* createComponent() {
            return new T;
        }

        template<class T>
        struct RegistryEntry
        {
          public:
            static RegistryEntry<T>& Instance(const std::string& name)
            {
                // Because I use a singleton here, even though `COMPONENT_REGISTER`
                // is expanded in multiple translation units, the constructor
                // will only be executed once. Only this cheap `Instance` function
                // (which most likely gets inlined) is executed multiple times.

                static RegistryEntry<T> inst(name);
                return inst;
            }

          private:
            RegistryEntry(const std::string& name)
            {
                ComponentRegistry& reg = getComponentRegistry();
                CreateComponentFunc func = createComponent<T>;

                std::pair<ComponentRegistry::iterator, bool> ret =
                    reg.insert(ComponentRegistry::value_type(name, func));

                if (ret.second == false) {
                    // This means there already is a component registered to
                    // this name. You should handle this error as you see fit.
                }
            }

            RegistryEntry(const RegistryEntry<T>&) = delete; // C++11 feature
            RegistryEntry& operator=(const RegistryEntry<T>&) = delete;
        };

    } // namespace detail

} // namespace component

#endif // COMPONENT_DETAIL_H

bileşen / component.cpp

// Matching header
#include "component.h"

// Standard libraries
#include <string>

// Custom libraries
#include "detail.h"


Component* component::create(const std::string& name)
{
    detail::ComponentRegistry& reg = detail::getComponentRegistry();
    detail::ComponentRegistry::iterator it = reg.find(name);

    if (it == reg.end()) {
        // This happens when there is no component registered to this
        // name. Here I return a null pointer, but you can handle this
        // error differently if it suits you better.
        return nullptr;
    }

    detail::CreateComponentFunc func = it->second;
    return func();
}

void component::destroy(const Component* comp)
{
    delete comp;
}

Lua ile genişletme

Biraz çalışmakla (çok zor değil) bunun C ++ veya Lua'da tanımlanan bileşenlerle sorunsuz bir şekilde çalışmak için kullanılabileceğini, bunun hakkında hiç düşünmek zorunda olmadığını belirtmeliyim.


Teşekkür ederim! Haklısın, C ++ şablonlarının kara sanatlarında henüz yeterince akıcı değilim. Ancak, tek satırlık makro tam olarak aradığım şeydi ve bunun üzerine, şablonları daha derinden anlamaya başlamak için bunu kullanacağım.
michael.bartnett

6
Bunun temelde doğru yaklaşım olduğu ve bana yapışan iki şey olduğu konusunda hemfikirim: 1. Neden yalnızca şablonlanmış bir işlev kullanmıyor ve çıkışta sızacak ComponentTypeImpl örnekleri yapmak yerine işlev işaretçilerinin bir haritasını saklamıyorsunuz (Gerçekten sorun değil Bir .SO / DLL veya başka bir şey yapıyorsanız) 2. componentRegistry nesnesi, sözde "statik başlatma sırası fiyasko" nedeniyle bozulabilir. Önce bileşenRegistry'nin yapıldığından emin olmak için önce yerel bir statik değişkene referans döndüren bir işlev yapmalı ve doğrudan bileşenRegistry kullanmak yerine çağırmalısınız.
Lucas

@Lucas Ah, bu konuda tamamen haklısınız. Kodu buna göre değiştirdim. Eski kodda herhangi bir sızıntı olduğunu sanmıyorum shared_ptr, ancak kullandım , ancak tavsiyen hala iyi.
Paul Manta

1
@ Paul: Tamam, ama teorik değil, olası sembol görünürlük sızıntısı / linker şikayetlerini önlemek için en azından statik yapmalısınız. Ayrıca "Bu hatayı uygun gördüğünüz gibi halletmelisiniz" yorumunuz yerine "Bu bir hata değil" demeli.

1
@PaulManta: İşlevlerin ve türlerin bazen ODR'yi "ihlal etmesine" izin verilir (örneğin, dediğiniz gibi, şablonlar). Ancak burada örneklerden bahsediyoruz ve bunlar daima ODR'yi takip etmeli. Derleyicilerin, birden fazla TU'da meydana gelirlerse (genellikle imkansız) bu hataları tespit etmesi ve bildirmesi gerekmez ve bu nedenle tanımsız davranış alanına girersiniz. Arabirim tanımınızın her yerine kesinlikle kaka atmanız gerekiyorsa, statik olması en azından programı iyi tanımlanmış halde tutar - ancak Coyote'un doğru fikri vardır.

9

İstediğiniz bir fabrika gibi görünüyor.

http://en.wikipedia.org/wiki/Factory_method_pattern

Yapabilecekleriniz, çeşitli bileşenlerin fabrikaya hangi isme karşılık geldiklerini kaydetmeleri ve ardından bileşenlerinizi oluşturmak için yapıcı yöntem imzasını gösteren bir dizi dize tanımlayıcı haritasına sahip olmanızdır.


1
Bu yüzden hala tüm Componentsınıflarımın farkında olan bir kod bölümüne ihtiyacım var , arayabilirim ComponentSubclass::RegisterWithFactory(), değil mi? Bunu ayarlamanın daha dinamik ve otomatik olarak yapması için bir yol var mı? Aradığım iş akışı 1'dir. Bir sınıf yazın, sadece ilgili başlığa ve cpp dosyasına bakın. Oyunu yeniden derleyin 3. Başlama seviyesi editörü ve yeni bileşen sınıfı kullanıma hazır.
michael.bartnett

2
Otomatik olarak gerçekleşmesi için gerçekten bir yolu yoktur. Yine de, komut satırına göre 1 satırlık bir makro çağrısına bölebilirsiniz. Paul'un cevabı biraz içine giriyor.
Tetrad

1

Paul Manta'nın tasarımı ile seçilen cevaptan bir süre çalıştım ve sonunda gelecekte bu soruya gelen herkes için paylaşmaya istekli olduğum bu daha genel ve özlü fabrika uygulamasına geldim. Bu örnekte, her fabrika nesnesi Objecttemel sınıftan türemiştir :

struct Object {
    virtual ~Object(){}
};

Statik Fabrika sınıfı aşağıdaki gibidir:

struct Factory {
    // the template used by the macro
    template<class ObjectType>
    struct RegisterObject {
        // passing a vector of strings allows many id's to map to the same sub-type
        RegisterObject(std::vector<std::string> names){
            for (auto name : names){
                objmap[name] = instantiate<ObjectType>;
            }
        }
    };

    // Factory method for creating objects
    static Object* createObject(const std::string& name){
        auto it = objmap.find(name);
        if (it == objmap.end()){
            return nullptr;
        } else {
            return it->second();
        }
    }

    private:
    // ensures the Factory cannot be instantiated
    Factory() = delete;

    // the map from string id's to instantiator functions
    static std::map<std::string, Object*(*)(void)> objmap;

    // templated sub-type instantiator function
    // requires that the sub-type has a parameter-less constructor
    template<class ObjectType>
    static Object* instantiate(){
        return new ObjectType();
    }
};
// pesky outside-class initialization of static member (grumble grumble)
std::map<std::string, Object*(*)(void)> Factory::objmap;

Bir alt türünü kaydetme makrosu Objectaşağıdaki gibidir:

#define RegisterObject(type, ...) \
namespace { \
    ::Factory::RegisterObject<type> register_object_##type({##__VA_ARGS__}); \
}

Şimdi kullanım şu şekilde:

struct SpecialObject : Object {
    void beSpecial(){}
};
RegisterObject(SpecialObject, "SpecialObject", "Special", "SpecObj");

...

int main(){
    Object* obj1 = Factory::createObject("SpecialObject");
    Object* obj2 = Factory::createObject("SpecObj");
    ...
    if (obj1){
        delete obj1;
    }
    if (obj2){
        delete obj2;
    }
    return 0;
}

Alt tür başına birçok dize kimliğinin kapasitesi uygulamamda kullanışlıdır, ancak alt tür başına tek bir kimliğe sınırlama oldukça basit olurdu.

Umarım bu yararlı olmuştur!


1

Off Bina @TimStraubinger 'ın cevabı, ben kullanarak fabrika sınıfını inşa C ++ 14 saklayabileceği standartları argümanların rastgele bir sayı ile üye türetilmiş . Benim örneğim, Tim’lerin aksine, işlev başına yalnızca bir ad / anahtar alır. Tim'in gibi her sınıf bir türetilmiştir depolanan Taban sınıf, maden çağrılan Base .

base.h

#ifndef BASE_H
#define BASE_H

class Base{
    public:
        virtual ~Base(){}
};

#endif

EX_Factory.h

#ifndef EX_COMPONENT_H
#define EX_COMPONENT_H

#include <string>
#include <map>
#include "Base.h"

struct EX_Factory{
    template<class U, typename... Args>
    static void registerC(const std::string &name){
        registry<Args...>[name] = &create<U>;
    }
    template<typename... Args>
    static Base * createObject(const std::string &key, Args... args){
        auto it = registry<Args...>.find(key);
        if(it == registry<Args...>.end()) return nullptr;
        return it->second(args...);
    }
    private:
        EX_Factory() = delete;
        template<typename... Args>
        static std::map<std::string, Base*(*)(Args...)> registry;

        template<class U, typename... Args>
        static Base* create(Args... args){
            return new U(args...);
        }
};

template<typename... Args>
std::map<std::string, Base*(*)(Args...)> EX_Factory::registry; // Static member declaration.


#endif

main.cpp

#include "EX_Factory.h"
#include <iostream>

using namespace std;

struct derived_1 : public Base{
    derived_1(int i, int j, float f){
        cout << "Derived 1:\t" << i * j + f << endl;
    }
};
struct derived_2 : public Base{
    derived_2(int i, int j){
        cout << "Derived 2:\t" << i + j << endl;
    }
};

int main(){
    EX_Factory::registerC<derived_1, int, int, float>("derived_1"); // Need to include arguments
                                                                    //  when registering classes.
    EX_Factory::registerC<derived_2, int, int>("derived_2");
    derived_1 * d1 = static_cast<derived_1*>(EX_Factory::createObject<int, int, float>("derived_1", 8, 8, 3.0));
    derived_2 * d2 = static_cast<derived_2*>(EX_Factory::createObject<int, int>("derived_2", 3, 3));
    delete d1;
    delete d2;
    return 0;
}

Çıktı

Derived 1:  67
Derived 2:  6

Umarım bu , çalışmak için bir kimlik kurucusu gerektirmeyen bir Fabrika tasarımı kullanmaya ihtiyaç duyan insanlara yardımcı olur . Eğlenceli bir tasarımdı, umarım Fabrika tasarımlarında daha fazla esnekliğe ihtiyaç duyan insanlara yardımcı olur .

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.