Sistem Entitas / Komponen dalam C ++, Bagaimana cara saya menemukan tipe dan membangun komponen?

37

Saya sedang mengerjakan sistem komponen entitas di C ++ yang saya harap dapat mengikuti gaya Artemis (http://piemaster.net/2011/07/07/entity-component-artemis/) dalam komponen yang sebagian besar adalah tas data dan itu adalah Sistem yang mengandung logika. Saya berharap dapat memanfaatkan data-sentris dari pendekatan ini dan membangun beberapa alat konten yang bagus.

Namun, satu punuk yang saya temui adalah cara mengambil beberapa string pengenal atau GUID dari file data dan menggunakannya untuk membangun komponen untuk Entitas. Jelas saya hanya dapat memiliki satu fungsi parse besar:

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

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

    else {
        return NULL:
    }
}

Tapi itu sangat jelek. Saya bermaksud menambah dan memodifikasi komponen, dan mudah-mudahan membangun semacam ScriptedComponentComponent, sehingga Anda bisa mengimplementasikan komponen dan sistem di Lua untuk keperluan prototyping. Saya ingin dapat menulis kelas yang diwarisi dari beberapa BaseComponentkelas, mungkin melemparkan beberapa makro untuk membuat semuanya berfungsi, dan kemudian memiliki kelas yang tersedia untuk instantiation saat runtime.

Di C # dan Java ini akan sangat mudah, karena Anda mendapatkan API refleksi yang bagus untuk mencari kelas dan konstruktor. Tapi, saya melakukan ini di C ++ karena saya ingin meningkatkan kemahiran saya dalam bahasa itu.

Jadi Bagaimana ini dicapai dalam C ++? Saya sudah membaca tentang mengaktifkan RTTI, tetapi tampaknya kebanyakan orang khawatir tentang hal itu, terutama dalam situasi di mana saya hanya membutuhkannya untuk subset jenis objek. Jika sistem RTTI khusus adalah yang saya butuhkan di sana, di mana saya bisa mulai belajar menulis satu?

michael.bartnett
sumber
1
Komentar yang tidak berhubungan: Jika Anda ingin mahir dalam C ++, gunakan C ++ dan bukan C, tentang string. Maaf untuk itu, tetapi harus dikatakan.
Chris berkata Reinstate Monica
Saya mendengar Anda, itu adalah contoh mainan dan saya tidak menghafal string std :: string. . . namun!
michael.bartnett
@ bearcdp Saya telah mengirim pembaruan besar pada jawaban saya. Implementasinya sekarang harus lebih kuat dan efisien.
Paul Manta
@ PaulManta Terima kasih banyak telah memperbarui jawaban Anda! Ada banyak hal kecil untuk dipelajari darinya.
michael.bartnett

Jawaban:

36

Sebuah komentar:
Implementasi Artemis menarik. Saya datang dengan solusi yang serupa, kecuali saya menyebut komponen saya "Atribut" dan "Perilaku". Pendekatan pemisahan jenis komponen ini telah bekerja sangat baik untuk saya.

Mengenai solusinya:
Kode ini mudah digunakan, tetapi implementasinya mungkin sulit diikuti jika Anda tidak berpengalaman dengan C ++. Begitu...

Antarmuka yang diinginkan

Apa yang saya lakukan adalah memiliki repositori pusat dari semua komponen. Setiap tipe komponen dipetakan ke string tertentu (yang mewakili nama komponen). Ini adalah bagaimana Anda menggunakan sistem:

// 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);
}

Pelaksanaan

Implementasinya tidak terlalu buruk, tetapi masih cukup rumit; membutuhkan pengetahuan tentang template dan pointer fungsi.

Catatan: Joe Wreschnig telah membuat beberapa poin bagus dalam komentar, terutama tentang bagaimana implementasi saya sebelumnya membuat terlalu banyak asumsi tentang seberapa bagus kompiler dalam mengoptimalkan kode; masalah ini tidak merugikan, imo, tapi itu memang mengganggu saya juga. Saya juga memperhatikan bahwa COMPONENT_REGISTERmakro sebelumnya tidak berfungsi dengan templat.

Saya telah mengubah kode dan sekarang semua masalah itu harus diperbaiki. Makro bekerja dengan templat dan masalah yang diangkat Joe telah ditangani: sekarang jauh lebih mudah bagi kompiler untuk mengoptimalkan kode yang tidak perlu.

komponen / komponen.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

komponen / 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

komponen / komponen.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;
}

Memperluas dengan Lua

Saya harus mencatat bahwa dengan sedikit kerja (ini tidak terlalu sulit), ini dapat digunakan untuk bekerja dengan mulus dengan komponen yang didefinisikan dalam C ++ atau Lua, tanpa harus memikirkannya.

Paul Manta
sumber
Terima kasih! Anda benar, saya belum cukup lancar dalam seni hitam template C ++ untuk benar-benar memahami hal itu. Tapi, makro satu baris persis apa yang saya cari, dan di atas itu saya akan menggunakan ini untuk mulai lebih memahami templat.
michael.bartnett
6
Saya setuju bahwa ini pada dasarnya adalah pendekatan yang tepat tetapi dua hal yang menonjol bagi saya: 1. Mengapa tidak hanya menggunakan fungsi templated dan menyimpan peta pointer fungsi alih-alih membuat instance ComponentTypeImpl yang akan bocor saat keluar (Tidak terlalu masalah kecuali Anda sedang membuat .SO / DLL atau sesuatu) 2. Objek componentRegistry dapat rusak karena apa yang disebut "kegagalan urutan inisialisasi statis". Untuk memastikan componentRegistry dibuat terlebih dahulu, Anda perlu membuat fungsi yang mengembalikan referensi ke variabel statis lokal dan memanggilnya alih-alih menggunakan componentRegistry secara langsung.
Lucas
@ Lucas Ah, Anda benar tentang itu. Saya mengubah kode sesuai. Saya tidak berpikir ada kebocoran dalam kode sebelumnya, sejak saya gunakan shared_ptr, tetapi saran Anda masih bagus.
Paul Manta
1
@ Paul: Oke, tapi itu tidak teoretis, Anda setidaknya harus membuatnya statis untuk menghindari kemungkinan kebocoran visibilitas simbol / linker. Juga komentar Anda "Anda harus menangani kesalahan ini sesuai keinginan Anda" sebagai gantinya harus mengatakan "Ini bukan kesalahan".
1
@ PaulManta: Fungsi dan tipe kadang-kadang diizinkan untuk "melanggar" ODR (mis. Seperti yang Anda katakan, templat). Namun di sini kita berbicara tentang contoh dan yang selalu harus mengikuti ODR. Compiler tidak diharuskan untuk mendeteksi dan melaporkan kesalahan ini jika terjadi dalam beberapa TU (umumnya tidak mungkin) dan Anda memasuki ranah perilaku yang tidak terdefinisi. Jika Anda benar-benar harus mengotori kotoran di seluruh definisi antarmuka Anda, membuatnya statis setidaknya membuat program tetap terdefinisi dengan baik - tetapi Coyote memiliki ide yang tepat.
9

Sepertinya yang Anda inginkan adalah pabrik.

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

Yang dapat Anda lakukan adalah meminta berbagai komponen Anda mendaftar ke pabrik untuk nama apa mereka bersesuaian, dan kemudian Anda memiliki beberapa peta pengenal string untuk tanda tangan metode konstruktor untuk menghasilkan komponen Anda.

Tetrad
sumber
1
Jadi saya masih perlu memiliki beberapa bagian kode yang mengetahui semua Componentkelas saya , memanggil ComponentSubclass::RegisterWithFactory(), kan? Apakah ada cara untuk mengatur ini, lakukan secara lebih dinamis dan otomatis? Alur kerja yang saya cari adalah 1. Menulis kelas, hanya melihat header dan file cpp yang sesuai 2. Mengkompilasi ulang game 3. Editor level pemula dan kelas komponen baru tersedia untuk digunakan.
michael.bartnett
2
Benar-benar tidak mungkin terjadi secara otomatis. Anda dapat memecahnya menjadi panggilan makro 1 baris pada basis per skrip. Jawaban Paul sedikit berbelit-belit.
Tetrad
1

Saya bekerja dengan desain Paul Manta dari jawaban yang dipilih untuk sementara waktu dan akhirnya sampai pada implementasi pabrik yang lebih umum dan ringkas di bawah ini yang saya ingin bagikan kepada siapa pun yang datang ke pertanyaan ini di masa depan. Dalam contoh ini, setiap objek pabrik berasal dari Objectkelas dasar:

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

Kelas Pabrik statis adalah sebagai berikut:

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;

Makro untuk mendaftarkan sub-jenis Objectadalah sebagai berikut:

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

Sekarang penggunaannya adalah sebagai berikut:

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;
}

Kapasitas untuk banyak string id per sub-tipe berguna dalam aplikasi saya, tetapi pembatasan untuk satu id per sub-tipe akan cukup mudah.

Semoga ini bermanfaat!

ubah igel
sumber
1

Membangun dari jawaban @TimStraubinger , saya membangun kelas pabrik menggunakan standar C ++ 14 yang dapat menyimpan anggota turunan dengan sejumlah argumen . Contoh saya, tidak seperti Tim, hanya membutuhkan satu nama / kunci per fungsi. Seperti milik Tim, setiap kelas yang disimpan berasal dari kelas Basis , kelas saya disebut Basis .

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;
}

Keluaran

Derived 1:  67
Derived 2:  6

Saya harap ini membantu orang yang perlu menggunakan desain Pabrik yang tidak memerlukan pembangun identitas untuk bekerja. Desainnya menyenangkan, jadi saya harap ini membantu orang yang membutuhkan lebih banyak fleksibilitas dalam desain pabrik mereka .

Kenneth Cornett
sumber