ระบบส่วนประกอบ / ส่วนประกอบใน C ++ ฉันจะค้นหาชนิดและสร้างส่วนประกอบได้อย่างไร


37

ฉันกำลังทำงานกับระบบส่วนประกอบเอนทิตีใน C ++ ที่ฉันหวังว่าจะเป็นไปตามสไตล์ของอาร์ทิมิส (http://piemaster.net/2011/07/entity-component-artemis/) ในส่วนประกอบนั้นส่วนใหญ่เป็นถุงข้อมูลและเป็น ระบบที่มีตรรกะ ฉันหวังว่าจะใช้ประโยชน์จาก data-centess-ness ของวิธีการนี้และสร้างเครื่องมือเนื้อหาที่ดี

อย่างไรก็ตามหนึ่งโคกที่ฉันเจอคือวิธีการใช้สตริงตัวระบุหรือ GUID จากไฟล์ข้อมูลและใช้เพื่อสร้างส่วนประกอบสำหรับ Entity เห็นได้ชัดว่าฉันสามารถมีฟังก์ชั่นการแยกวิเคราะห์ขนาดใหญ่ได้หนึ่งรายการ:

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

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

    else {
        return NULL:
    }
}

แต่นั่นน่าเกลียดจริงๆ ฉันตั้งใจจะเพิ่มและแก้ไขส่วนประกอบบ่อยครั้งและหวังว่าจะสร้าง ScriptedComponentComponent บางประเภทเช่นที่คุณสามารถนำส่วนประกอบและระบบมาใช้ใน Lua เพื่อวัตถุประสงค์ในการสร้างต้นแบบ ฉันต้องการที่จะสามารถเขียนคลาสที่สืบทอดมาจากบางBaseComponentคลาสอาจโยนมาโครสองสามตัวเพื่อให้ทุกอย่างทำงานได้

ใน C # และ Java สิ่งนี้จะค่อนข้างตรงไปตรงมาเนื่องจากคุณได้รับ API การสะท้อนที่ดีเพื่อค้นหาคลาสและตัวสร้าง แต่ฉันทำสิ่งนี้ใน C ++ เพราะฉันต้องการเพิ่มความสามารถในภาษานั้น

ดังนั้นวิธีนี้สำเร็จใน C ++? ฉันได้อ่านเกี่ยวกับการเปิดใช้งาน RTTI แต่ดูเหมือนว่าคนส่วนใหญ่ระมัดระวังเกี่ยวกับเรื่องนี้โดยเฉพาะอย่างยิ่งในสถานการณ์ที่ฉันต้องการเพียงแค่ชุดย่อยของประเภทวัตถุ หากระบบ RTTI ที่กำหนดเองเป็นสิ่งที่ฉันต้องการมีฉันจะเริ่มเรียนรู้การเขียนได้ที่ไหน


1
ความคิดเห็นที่ไม่เกี่ยวข้องค่อนข้างมาก: หากคุณต้องการมีความเชี่ยวชาญในภาษา C ++ ให้ใช้ภาษา C ++ และไม่ใช่ภาษา C เกี่ยวกับสตริง ขออภัยสำหรับสิ่งนั้น แต่ต้องมีการกล่าว
คริสพูดว่า Reinstate Monica

ฉันได้ยินคุณมันเป็นตัวอย่างของเล่นและฉันไม่ได้ std :: string api จดจำ . . ๆ !
michael.bartnett

@bearcdp ฉันได้โพสต์การอัปเดตครั้งสำคัญในคำตอบของฉัน การดำเนินการในขณะนี้จะต้องแข็งแกร่งและมีประสิทธิภาพ
พอล Manta

@PaulManta ขอบคุณมากสำหรับการอัปเดตคำตอบของคุณ! มีสิ่งเล็ก ๆ น้อย ๆ มากมายให้เรียนรู้จากมัน
michael.bartnett

คำตอบ:


36

ความคิดเห็น:
การนำอาร์ทิมิสมาใช้เป็นสิ่งที่น่าสนใจ ฉันคิดวิธีแก้ปัญหาที่คล้ายกันยกเว้นฉันเรียกองค์ประกอบของฉันว่า "แอตทริบิวต์" และ "พฤติกรรม" วิธีการแยกประเภทของส่วนประกอบนี้ทำงานได้ดีมากสำหรับฉัน

เกี่ยวกับการแก้ปัญหา:
รหัสนั้นใช้งานง่าย แต่การใช้งานอาจยากที่จะติดตามหากคุณไม่เคยใช้ C ++ มาก่อน ดังนั้น...

อินเทอร์เฟซที่ต้องการ

สิ่งที่ฉันทำคือการมีที่เก็บส่วนกลางของส่วนประกอบทั้งหมด คอมโพเนนต์แต่ละชนิดถูกแม็พกับสตริงที่แน่นอน (ซึ่งแทนชื่อคอมโพเนนต์) นี่คือวิธีที่คุณใช้ระบบ:

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

การดำเนินการ

การติดตั้งใช้งานไม่ได้แย่ขนาดนั้น แต่มันก็ยังค่อนข้างซับซ้อน มันต้องมีความรู้เกี่ยวกับเทมเพลตและพอยน์เตอร์ฟังก์ชั่น

หมายเหตุ: Joe Wreschnig ได้ให้คะแนนที่ดีในความคิดเห็นส่วนใหญ่เกี่ยวกับวิธีการใช้งานก่อนหน้าของฉันทำข้อสันนิษฐานมากเกินไปเกี่ยวกับวิธีการที่ดีที่คอมไพเลอร์เป็นรหัสการเพิ่มประสิทธิภาพ; ปัญหาไม่เป็นอันตราย imo แต่มันก็ทำให้ฉันด้วย ฉันยังสังเกตเห็นว่าCOMPONENT_REGISTERมาโครก่อนหน้าไม่ทำงานกับเทมเพลต

ฉันได้เปลี่ยนรหัสและตอนนี้ปัญหาเหล่านั้นควรได้รับการแก้ไขแล้ว มาโครทำงานกับเทมเพลตและปัญหาที่ Joe ฟื้นคืนมาได้รับการแก้ไขแล้วตอนนี้การคอมไพเลอร์เพื่อเพิ่มประสิทธิภาพโค้ดที่ไม่จำเป็นออกไป

องค์ประกอบ / 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

องค์ประกอบ / 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

องค์ประกอบ / 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

ฉันควรทราบว่าด้วยการทำงานเล็กน้อย (มันไม่ยากมาก) สิ่งนี้สามารถใช้เพื่อทำงานกับส่วนประกอบที่กำหนดใน C ++ หรือ Lua อย่างราบรื่นโดยไม่ต้องคิดอะไรเลย


ขอขอบคุณ! คุณพูดถูกฉันยังไม่คล่องพอในเทมเพลตสีดำของ C ++ ที่จะเข้าใจอย่างสมบูรณ์ แต่มาโครแบบบรรทัดเดียวคือสิ่งที่ฉันกำลังมองหาและยิ่งไปกว่านั้นฉันจะใช้สิ่งนี้เพื่อเริ่มเข้าใจเทมเพลตอย่างลึกซึ้งยิ่งขึ้น
michael.bartnett

6
ฉันเห็นด้วยว่านี่เป็นวิธีการที่ถูกต้อง แต่สองสิ่งที่ยื่นออกมาจากฉัน: 1. ทำไมไม่ใช้ฟังก์ชัน templated และเก็บแผนที่พอยน์เตอร์ของฟังก์ชั่นแทนการทำอินสแตนซ์ ComponentTypeImpl ที่จะรั่วไหลออก คุณกำลังสร้าง. SO / DLL หรือบางสิ่งบางอย่าง) 2. วัตถุ componentRegistry อาจแตกเนื่องจากสิ่งที่เรียกว่า เพื่อให้แน่ใจว่ามีการสร้าง componentRegistry ก่อนคุณต้องสร้างฟังก์ชันที่ส่งคืนการอ้างอิงไปยังตัวแปรสแตติกท้องถิ่นและเรียกมันว่าแทนที่จะใช้ componentRegistry โดยตรง
ลูคัส

@Lucas Ah คุณมีสิทธิ์โดยสิ้นเชิงเกี่ยวกับสิ่งเหล่านั้น ฉันเปลี่ยนรหัสตาม ฉันไม่คิดว่ามีการรั่วไหลในรหัสก่อนหน้า แต่เนื่องจากฉันใช้shared_ptrแต่คำแนะนำของคุณยังดี
พอล Manta

1
@ พอล: โอเค แต่ไม่ใช่ในทางทฤษฎีอย่างน้อยคุณควรทำให้มันคงที่เพื่อหลีกเลี่ยงการมองเห็นสัญลักษณ์ที่เป็นไปได้ของการรั่วไหล / การร้องเรียนของลิงเกอร์ นอกจากนี้ความคิดเห็นของคุณ "คุณควรจัดการข้อผิดพลาดนี้ตามที่เห็นสมควร" แทนที่จะพูดว่า "นี่ไม่ใช่ข้อผิดพลาด"

1
@PaulManta: บางครั้งฟังก์ชั่นและประเภทได้รับอนุญาตให้ "ละเมิด" ODR (เช่นที่คุณพูดแม่แบบ) อย่างไรก็ตามที่นี่เรากำลังพูดถึงอินสแตนซ์และผู้ที่ต้องปฏิบัติตาม ODR เสมอ คอมไพเลอร์ไม่จำเป็นต้องตรวจจับและรายงานข้อผิดพลาดเหล่านี้หากเกิดขึ้นในหลาย ๆ TU (โดยทั่วไปเป็นไปไม่ได้) และเพื่อให้คุณเข้าสู่อาณาจักรของพฤติกรรมที่ไม่ได้กำหนด หากคุณต้องละเลงปูอย่างสิ้นเชิงกับคำจำกัดความอินเทอร์เฟซของคุณทำให้อย่างน้อยคงที่โปรแกรมที่กำหนดไว้อย่างดี - แต่โคโยตี้มีความคิดที่ถูกต้อง

9

ดูเหมือนว่าสิ่งที่คุณต้องการคือโรงงาน

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

สิ่งที่คุณสามารถทำได้คือให้ส่วนประกอบต่าง ๆ ของคุณลงทะเบียนกับโรงงานชื่อที่พวกเขาสอดคล้องกับและจากนั้นคุณมีแผนที่ของตัวระบุสตริงเพื่อลายเซ็นวิธีการสร้างเพื่อสร้างส่วนประกอบของคุณ


1
ดังนั้นฉันยังคงต้องมีบางส่วนของรหัสที่ตระหนักถึงทุกComponentชั้นเรียนของฉันโทรComponentSubclass::RegisterWithFactory()ใช่มั้ย มีวิธีการตั้งค่านี้ทำแบบไดนามิกและอัตโนมัติ? เวิร์กโฟลว์ที่ฉันกำลังค้นหาคือ 1. เขียนคลาสโดยดูเฉพาะส่วนหัวที่สัมพันธ์กันและไฟล์ cpp 2. รวบรวมเกมใหม่ 3. ตัวแก้ไขระดับเริ่มต้นและคลาสองค์ประกอบใหม่พร้อมใช้งาน
michael.bartnett

2
ไม่มีทางที่มันจะเกิดขึ้นโดยอัตโนมัติ คุณสามารถแยกมันออกเป็นการเรียกแมโคร 1 บรรทัดบนพื้นฐานต่อสคริปต์ได้ คำตอบของพอลเข้าไปได้นิดหน่อย
Tetrad

1

ฉันทำงานกับการออกแบบของ Paul Manta จากคำตอบที่เลือกมาระยะหนึ่งและในที่สุดก็มาถึงการดำเนินการโรงงานทั่วไปที่รัดกุมกว่าด้านล่างนี้ซึ่งฉันยินดีที่จะแบ่งปันให้กับทุกคนที่มาถึงคำถามนี้ในอนาคต ในตัวอย่างนี้วัตถุจากโรงงานทุกชิ้นมาจากObjectคลาสฐาน:

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

คลาสโรงงานแบบคงที่มีดังต่อไปนี้:

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;

แมโครสำหรับการลงทะเบียนประเภทย่อยObjectมีดังนี้:

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

ตอนนี้การใช้งานมีดังนี้:

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

ความจุของสตริง id จำนวนมากต่อประเภทย่อยนั้นมีประโยชน์ในแอปพลิเคชันของฉัน แต่การ จำกัด id เดี่ยวต่อประเภทย่อยจะค่อนข้างตรงไปตรงมา

ฉันหวังว่านี่จะเป็นประโยชน์!


1

นอกเหนือจากคำตอบของ@TimStraubingerฉันได้สร้างคลาสโรงงานโดยใช้มาตรฐานC ++ 14ซึ่งสามารถเก็บสมาชิกที่ได้มาด้วยจำนวนอาร์กิวเมนต์ที่กำหนดเอง ตัวอย่างของฉันซึ่งแตกต่างจากของทิมใช้เพียงหนึ่งชื่อ / คีย์ต่อหนึ่งฟังก์ชัน เช่นเดียวกับทิมทุกระดับการจัดเก็บมาจากฐานระดับเหมืองถูกเรียกว่าฐาน

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

เอาท์พุต

Derived 1:  67
Derived 2:  6

ฉันหวังว่าสิ่งนี้จะช่วยให้ผู้คนจำเป็นต้องใช้การออกแบบโรงงานซึ่งไม่ต้องใช้ตัวสร้างเอกลักษณ์ในการทำงาน มันสนุกกับการออกแบบดังนั้นฉันหวังว่าจะช่วยให้ผู้คนต้องการความยืดหยุ่นในการออกแบบโรงงานของพวกเขามากขึ้น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.