C ++의 엔터티 / 구성 요소 시스템, 유형을 검색하고 구성 요소를 어떻게 구성합니까?


37

C ++에서 엔티티 구성 요소 시스템을 작업 중이며 Artemis (http://piemaster.net/2011/07/entity-component-artemis/)의 스타일을 따르기를 희망합니다. 구성 요소는 대부분 데이터 백이며 논리가 포함 된 시스템 이 접근 방식의 데이터 중심성을 활용하고 멋진 콘텐츠 도구를 만들고 싶습니다.

그러나 내가 겪고있는 하나의 혹은 데이터 파일에서 식별자 문자열이나 GUID를 가져 와서 엔티티의 구성 요소를 구성하는 방법입니다. 분명히 하나의 큰 구문 분석 기능을 가질 수 있습니다.

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

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

    else {
        return NULL:
    }
}

그러나 그것은 정말로 추악합니다. 컴포넌트를 자주 추가하고 수정하고, Prototyping을 위해 Lua에서 컴포넌트와 시스템을 구현할 수 있도록 일종의 ScriptedComponentComponent를 빌드하려고합니다. 일부 BaseComponent클래스 에서 상속되는 클래스를 작성하고 모든 매크로를 작동시키기 위해 몇 개의 매크로를 던져서 클래스를 런타임에 인스턴스화 할 수 있기를 원합니다 .

C #과 Java에서는 클래스와 생성자를 조회하는 멋진 리플렉션 API를 얻으므로 매우 간단합니다. 그러나 C ++ 에서이 작업을 수행하고 있습니다. 그 언어에 대한 숙련도를 높이고 싶습니다.

그렇다면 이것이 C ++에서 어떻게 달성됩니까? RTTI 활성화에 대해 읽었지만 대부분의 사람들은 특히 객체 유형의 하위 집합에만 필요로하는 상황에서 그 점에 대해 경계하는 것 같습니다. 맞춤형 RTTI 시스템이 필요한 경우, 시스템을 작성하는 방법을 배우려면 어디로 가야합니까?


1
매우 관련이없는 의견 : C ++에 능숙하려면 문자열과 관련하여 C가 아닌 C ++을 사용하십시오. 미안하지만 말해야했습니다.
— Chris는 Reinstate Monica가

나는 당신에게 장난감 예제이며 std :: string api memorized가 없습니다. . . 아직!
— michael.bartnett

@ bearcdp 내 답변에 주요 업데이트를 게시했습니다. 구현은 이제보다 강력하고 효율적이어야합니다.
— Paul Manta 2012 년

@PaulManta 답변을 업데이트 해 주셔서 감사합니다! 그것으로부터 배울 작은 것들이 많이 있습니다.
— michael.bartnett

답변:


36

코멘트 :
Artemis 구현은 흥미 롭습니다. 컴포넌트를 "Attributes"와 "Behaviors"라고 부르는 것을 제외하고는 비슷한 해결책을 찾았습니다. 유형의 구성 요소를 분리하는 이러한 접근 방식은 저에게 매우 훌륭했습니다.

해결책에 관해서 :
코드는 사용하기 쉽지만 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는 주로 이전 구현 에서 컴파일러가 코드를 최적화하는 데 얼마나 많은 가정을 적용 했는지에 대해 몇 가지 좋은 점을 지적했습니다 . 이 문제는 해롭지 않았지만, 나에게도 버그가있었습니다. 또한 이전 COMPONENT_REGISTER매크로가 템플릿에서 작동하지 않는 것으로 나타났습니다 .

코드를 변경했으며 이제 모든 문제를 해결해야합니다. 이 매크로는 템플릿과 함께 작동하며 Joe가 제기 한 문제를 해결했습니다. 이제 컴파일러가 불필요한 코드를 훨씬 쉽게 최적화 할 수 있습니다.

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

component / 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 / 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
나는 이것이 기본적으로 올바른 접근 방법이지만 나에게 두 가지 중요한 점에 동의합니다. 당신은 .SO / DLL 또는 무언가를 만들고있다) 2. componentRegistry 객체는 소위 "정적 초기화 순서 fiasco"때문에 깨질 수있다 componentRegistry를 먼저 작성하려면 로컬 정적 변수에 대한 참조를 리턴하고 componentRegistry를 직접 사용하는 대신이를 호출하는 함수를 작성해야합니다.
— Lucas

@Lucas 아, 당신은 그것에 대해 완전히 맞습니다. 그에 따라 코드를 변경했습니다. 그래도 이전 코드에서 누수가 발생했다고 생각하지 않지만, shared_ptr귀하의 조언은 여전히 ​​좋습니다.
— Paul Manta

1
@Paul : 알겠습니다.하지만 이론적 인 것은 아닙니다. 최소한 심볼 가시성 누출 / 링커 불만이 발생하지 않도록 정적으로 만들어야합니다. 또한 "이 오류를 처리해야합니다"라는 주석 대신 "이것은 오류가 아닙니다"라고 표시해야합니다.

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) 저장할 수있는 표준 인수의 임의의 수의 회원을 유도을 . 내 예제는 Tim과 달리 기능 당 하나의 이름 / 키만 사용합니다. 팀의처럼, 모든 클래스는에서 파생 저장되는 기본 클래스, 광산은 호출되는 자료를 .

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.