Hệ thống thực thể / thành phần trong C ++, làm cách nào để khám phá các loại và xây dựng các thành phần?


37

Tôi đang làm việc trên một hệ thống thành phần thực thể trong C ++ mà tôi hy vọng sẽ theo phong cách của Artemis (http://piemaster.net/2011/07/entity-component-artemis/) trong đó các thành phần chủ yếu là túi dữ liệu và đó là Các hệ thống chứa logic. Tôi hy vọng sẽ tận dụng lợi thế trung tâm dữ liệu của phương pháp này và xây dựng một số công cụ nội dung hay.

Tuy nhiên, một bướu tôi gặp là cách lấy một số chuỗi định danh hoặc GUID từ tệp dữ liệu và sử dụng chuỗi đó để xây dựng thành phần cho Thực thể. Rõ ràng tôi chỉ có thể có một chức năng phân tích cú pháp lớn:

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

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

    else {
        return NULL:
    }
}

Nhưng điều đó thực sự xấu xí. Tôi dự định sẽ thường xuyên thêm và sửa đổi các thành phần và hy vọng xây dựng một số loại ScriptedComponentComponent, để bạn có thể triển khai một thành phần và hệ thống trong Lua cho mục đích tạo mẫu. Tôi muốn có thể viết một lớp kế thừa từ một số BaseComponentlớp, có thể đưa vào một vài macro để làm cho mọi thứ hoạt động, và sau đó có sẵn lớp để khởi tạo trong thời gian chạy.

Trong C # và Java, điều này sẽ khá đơn giản, vì bạn có các API phản chiếu đẹp để tra cứu các lớp và hàm tạo. Nhưng, tôi đang làm điều này trong C ++ vì tôi muốn tăng sự thành thạo ngôn ngữ đó.

Vậy làm thế nào điều này được thực hiện trong C ++? Tôi đã đọc về việc kích hoạt RTTI, nhưng có vẻ như hầu hết mọi người đều cảnh giác về điều đó, đặc biệt là trong tình huống tôi chỉ cần nó cho một tập hợp con các loại đối tượng. Nếu một hệ thống RTTI tùy chỉnh là những gì tôi cần ở đó, tôi có thể đi đâu để bắt đầu học viết?


1
Nhận xét khá không liên quan: Nếu bạn muốn thành thạo C ++, thì hãy sử dụng C ++ chứ không phải C, liên quan đến chuỗi. Xin lỗi vì điều đó, nhưng nó đã được nói.
Chris nói Phục hồi Monica

Tôi nghe thấy bạn, đó là một ví dụ về đồ chơi và tôi không có ghi nhớ std :: chuỗi api. . . chưa!
michael.bartnett

@bearcdp Tôi đã đăng một bản cập nhật lớn cho câu trả lời của tôi. Việc thực hiện bây giờ phải mạnh mẽ và hiệu quả hơn.
Paul Manta

@PaulManta Cảm ơn rất nhiều vì đã cập nhật câu trả lời của bạn! Có rất nhiều điều nhỏ để học hỏi từ nó.
michael.bartnett

Câu trả lời:


36

Một nhận xét:
Việc thực hiện Artemis rất thú vị. Tôi đã đưa ra một giải pháp tương tự, ngoại trừ tôi gọi các thành phần của mình là "Thuộc tính" và "Hành vi". Cách tiếp cận tách các loại thành phần này đã làm việc rất độc đáo đối với tôi.

Về giải pháp:
Mã rất dễ sử dụng, nhưng việc triển khai có thể khó theo dõi nếu bạn không có kinh nghiệm với C ++. Vì thế...

Giao diện mong muốn

Những gì tôi đã làm là có một kho lưu trữ trung tâm của tất cả các thành phần. Mỗi loại thành phần được ánh xạ tới một chuỗi nhất định (đại diện cho tên thành phần). Đây là cách bạn sử dụng hệ thống:

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

Việc thực hiện

Việc thực hiện không phải là xấu, nhưng nó vẫn khá phức tạp; nó đòi hỏi một số kiến ​​thức về mẫu và con trỏ hàm.

Lưu ý: Joe Wreschnig đã đưa ra một số điểm tốt trong các nhận xét, chủ yếu là về cách triển khai trước đây của tôi đưa ra quá nhiều giả định về việc trình biên dịch tốt như thế nào trong việc tối ưu hóa mã; vấn đề không gây bất lợi, imo, nhưng nó cũng làm tôi khó chịu. Tôi cũng nhận thấy rằng COMPONENT_REGISTERmacro trước đây không hoạt động với các mẫu.

Tôi đã thay đổi mã và bây giờ tất cả các vấn đề đó sẽ được khắc phục. Macro hoạt động với các mẫu và các vấn đề mà Joe nêu ra đã được giải quyết: giờ đây trình biên dịch dễ dàng hơn nhiều để tối ưu hóa mã không cần thiết.

thành phần / thành phần.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

thành phần / chi tiết.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

thành phần / thành phần.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;
}

Mở rộng với Lua

Tôi nên lưu ý rằng với một chút công việc (không khó lắm), điều này có thể được sử dụng để làm việc liền mạch với các thành phần được xác định trong C ++ hoặc Lua, mà không cần phải suy nghĩ về nó.


Cảm ơn bạn! Bạn nói đúng, tôi chưa đủ thông thạo nghệ thuật đen của các mẫu C ++ để hoàn toàn hiểu điều đó. Nhưng, macro một dòng chính xác là những gì tôi đang tìm kiếm và trên hết, tôi sẽ sử dụng điều này để bắt đầu hiểu sâu hơn các mẫu.
michael.bartnett

6
Tôi đồng ý rằng về cơ bản đây là cách tiếp cận phù hợp nhưng có hai điều phù hợp với tôi: 1. Tại sao không sử dụng hàm templated và lưu trữ bản đồ các con trỏ hàm thay vì tạo các cá thể ElementTypeImpl sẽ bị rò rỉ khi thoát (Không thực sự là vấn đề trừ khi bạn đang tạo một .SO / DLL hoặc một cái gì đó) 2. Đối tượng thành phần có thể bị phá vỡ do cái gọi là "fiasco thứ tự khởi tạo tĩnh". Để đảm bảo thành phần Phân tích được thực hiện trước tiên, bạn cần tạo một hàm trả về tham chiếu đến biến tĩnh cục bộ và gọi đó thay vì sử dụng trực tiếp thành phần.
Lucas

@Lika Ah, bạn hoàn toàn đúng về những điều đó. Tôi đã thay đổi mã cho phù hợp. Tôi không nghĩ rằng có bất kỳ rò rỉ nào trong mã trước đó, vì tôi đã sử dụng shared_ptr, nhưng lời khuyên của bạn vẫn tốt.
Paul Manta

1
@Paul: Được rồi, nhưng nó không phải là lý thuyết, ít nhất bạn nên làm cho nó tĩnh để tránh rò rỉ biểu tượng / liên kết có thể nhìn thấy biểu tượng. Ngoài ra, bình luận của bạn "Bạn nên xử lý lỗi này khi bạn thấy phù hợp" thay vào đó nên nói "Đây không phải là lỗi".

1
@PaulManta: Các chức năngloại đôi khi được phép "vi phạm" ODR (ví dụ như bạn nói, các mẫu). Tuy nhiên, ở đây chúng ta đang nói về các trường hợp và những trường hợp này luôn phải tuân theo ODR. Trình biên dịch không bắt buộc phải phát hiện và báo cáo các lỗi này nếu chúng xảy ra trong nhiều TU (nói chung là không thể) và do đó bạn nhập địa hạt của hành vi không xác định. Nếu bạn hoàn toàn phải bôi nhọ tất cả các định nghĩa giao diện của mình, làm cho nó tĩnh ít nhất giữ cho chương trình được xác định rõ - nhưng Coyote có ý tưởng đúng.

9

Có vẻ như những gì bạn muốn là một nhà máy.

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

Những gì bạn có thể làm là yêu cầu các thành phần khác nhau của bạn đăng ký với nhà máy tên chúng tương ứng, và sau đó bạn có một số bản đồ định danh chuỗi để chữ ký phương thức xây dựng để tạo các thành phần của bạn.


1
Vì vậy, tôi vẫn cần phải có một số phần mã nhận biết tất cả các Componentlớp của tôi , gọi ComponentSubclass::RegisterWithFactory(), phải không? Có cách nào để thiết lập điều này làm nó linh hoạt và tự động hơn không? Quy trình công việc tôi đang tìm kiếm là 1. Viết một lớp, chỉ nhìn vào tiêu đề và tệp cpp sửa lỗi 2. Biên dịch lại trò chơi 3. Bắt đầu trình chỉnh sửa cấp độ và lớp thành phần mới có sẵn để sử dụng.
michael.bartnett

2
Thực sự không có cách nào để nó xảy ra tự động. Mặc dù vậy, bạn có thể chia nó thành cuộc gọi macro 1 dòng trên cơ sở mỗi tập lệnh. Câu trả lời của Paul đi vào đó một chút.
Tetrad

1

Tôi đã làm việc với thiết kế của Paul Manta từ câu trả lời được chọn trong một thời gian và cuối cùng đã đến với triển khai nhà máy chung chung và ngắn gọn hơn dưới đây mà tôi sẵn sàng chia sẻ cho bất kỳ ai đến với câu hỏi này trong tương lai. Trong ví dụ này, mọi đối tượng nhà máy đều xuất phát từ Objectlớp cơ sở:

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

Lớp Factory tĩnh như sau:

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;

Macro để đăng ký một loại phụ Objectnhư sau:

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

Bây giờ sử dụng như sau:

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

Khả năng của nhiều id chuỗi trên mỗi loại phụ là hữu ích trong ứng dụng của tôi, nhưng việc hạn chế một id cho mỗi loại phụ sẽ khá đơn giản.

Tôi hy vọng điều này đã có ích!


1

Dựa trên câu trả lời của @TimStraubinger , tôi đã xây dựng một lớp nhà máy sử dụng các tiêu chuẩn C ++ 14 có thể lưu trữ các thành viên dẫn xuất với số lượng đối số tùy ý . Ví dụ của tôi, không giống như Tim, chỉ lấy một tên / khóa cho mỗi chức năng. Giống như Tim, mọi lớp được lưu trữ đều bắt nguồn từ một lớp Cơ sở , lớp của tôi được gọi là Base .

Cơ sở.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;
}

Đầu ra

Derived 1:  67
Derived 2:  6

Tôi hy vọng điều này sẽ giúp mọi người cần sử dụng thiết kế Factory không yêu cầu trình tạo nhận dạng để làm việc. Đó là thiết kế thú vị, vì vậy tôi hy vọng nó giúp mọi người cần linh hoạt hơn trong thiết kế Nhà máy của họ .

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.