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.