Entity Component System и несколько компонентов, использующих общий базовый тип - PullRequest
3 голосов
/ 15 марта 2019

Я пытаюсь внедрить простой ECS для моего игрового движка.Я знаю, что моя реализация не строго ECS, но я рефакторинг моего кода, чтобы быть более основанным на компонентах.Пока у меня есть следующие классы:

Entity: это контейнер компонентов, и, поскольку я хочу, чтобы моя сущность имела несколько компонентов одного типа, она сохраняет их в std::map<ComponentID,std::vector<std::unique_ptr<Component>>>.Каждый компонент имеет уникальный идентификатор (целое число без знака), полученный из простого трюка с шаблоном, который я узнал в Интернете:

Функция GetUniqueComponentID:

using ComponentID = unsigned int;

inline ComponentID GetUniqueComponentID()
{
    static ComponentID id = 0;

    return id++;
}

содержит счетчик, которыйпросто генерирует увеличивающиеся числа.Я вызываю эту функцию из шаблона функции GetComponentID:

template <typename T>
ComponentID GetComponentID()
{
    static ComponentID id = GetUniqueComponentID();

    return id;
}

этот шаблон создает отдельную функцию для каждого компонента, который я добавляю в свою сущность, поэтому код, который должен получить компонент, может индексировать карту, используя GetComponentId<Component_type>, с конкретным типом компонента в качестве аргумента шаблона для функции.

В классе сущности есть методы, такие как AddComponent и GetComponent, которые соответственно создают компонент и добавляют его к сущности, а также извлекают компонент (если имеется)):

class Entity
{
public:
    Entity();
    ~Entity();
    template <typename T, typename... TArgs>
    T &AddComponent(TArgs&&... args);
    template <typename T>
    bool HasComponent();
    //template <typename T>
    //T &GetComponent();
    template <typename T> 
    std::vector<T*> GetComponents();
    bool IsAlive() { return mIsAlive; }
    void Destroy() { mIsAlive = false; }
private:
    //std::map<ComponentID, std::unique_ptr<Component>> mComponents;              // single component per type
    std::map<ComponentID, std::vector<std::unique_ptr<Component>>> mComponents;   // multiple components per type
    bool mIsAlive = true;
};


template <typename T, typename... TArgs>
T &Entity::AddComponent(TArgs&&... args)
{
    T *c = new T(std::forward<TArgs>(args)...);
    std::unique_ptr<Component> component(c);
    component->SetEntity(this);
    mComponents[GetComponentID<T>()].push_back(std::move(component));
    return *c;
}

template <typename T>
bool Entity::HasComponent()  // use bitset (faster)
{
    std::map<ComponentID, std::vector<std::unique_ptr<Component>>>::iterator it = mComponents.find(GetComponentID<T>());
    if (it != mComponents.end())
        return true;
    return false;
}

template <typename T>
std::vector<T*> Entity::GetComponents()
{
    std::vector<T*> components;
    for (std::unique_ptr<Component> &component : mComponents[GetComponentID<T>()])
        components.push_back(static_cast<T*>(component.get()));

    return components;
}

Поскольку я хочу хранить несколько компонентов одного типа, я храню их в std::map<ComponentID,std::vector<std::unique_ptr<Component>>>.

Теперь мой вопрос:

Iнеобходимо создать иерархию компонентов для типа компонента: у меня есть компонент ForceGenerator, который является (абстрактным) базовым классом для всех видов конкретных ForceGenerator (Springs, Gravity и т. д.).Поэтому мне нужно создавать конкретные компоненты, но мне нужно использовать их полиморфно через указатель на базовый класс: моя физическая подсистема должна иметь дело только с указателями на базовый ForceGenerator, вызывая его метод Update (), который заботится об обновлении сил.

Я не могу использовать текущий подход, так как я вызываю AddComponent с другим типом каждый раз, когда создаю определенный компонент ForceGenerator, в то время как мне нужно хранить их в одном массиве (сопоставленном с идентификатором компонентабазовый ForceGenerator).

Как я мог решить эту проблему?

Ответы [ 2 ]

1 голос
/ 15 марта 2019

НОВОЕ ПРЕДЛОЖЕНИЕ

Глядя на другой ответ, я понял, что вы можете наследовать от другого базового класса CRTP для определения, где хранить (только при использовании сопоставленного хранилища).

Пример:

//Just for check class
struct StoreAs {};


//Give the store type
template<typename T>
struct StoreAsT : public StoreAs {
    using store_as_type = T;
};

//Some components
struct ComponentA {    };

struct ComponentC {    };

struct ComponentB : public StoreAsT<ComponentC> {    };


//Dummy add
template<typename T>
void Add(T&& cmp) {
    if constexpr(std::is_base_of_v<StoreAs, T>) {
        std::cout << "Store as (remap)" << GetComponentID<typename T::store_as_type>() << std::endl;
    } else {
        std::cout << "Store as " << GetComponentID<T>() << std::endl;
    }
}

//Example add
int main() {

    Add(ComponentA {});
    Add(ComponentB {});
    Add(ComponentC {});

    return 0;
}

Вывод:

Store as 0
Store as (remap)1
Store as 1

СТАРЫЕ ПРЕДЛОЖЕНИЯ:

Простой подход, но довольно многословный и не оченьуниверсальное решение, которое вы можете расширить свой трюк генерации идентификатора:

template <typename T>
ComponentID GetComponentID()
{
    static ComponentID id = GetUniqueComponentID();

    return id;
}

до

template <typename T>
struct ComponentIDGenerator {
    static ComponentID GetComponentID() {
        static ComponentID id = GetUniqueComponentID();

        return id;
    }
};

Теперь вместо использования GetComponentID вам нужно использовать ComponenteIDGenerator :: GetComponentID (), но теперь вы можете создатьконкретные специализации.

Итак, вы можете специализироваться для переназначения некоторых идентификаторов:

template<>
struct ComponentIDGenerator<SomeForce1> {
    static ComponentID GetComponentID() {
        return ComponentIDGenerator<NotRemappedForceType>::GetComponentID();
    }
};
template<>
struct ComponentIDGenerator<SomeForce2> {
    static ComponentID GetComponentID() {
        return ComponentIDGenerator<NotRemappedForceType>::GetComponentID();
    }
};

Теперь оба (SomeFroce1 и SomeForce2) возвращают идентификатор "NotRemappedForceType"

и вконец восстановить первоначальную функцию:

template<typename T>
ComponentID GetComponentID() {
    return ComponentIDGenerator<T>::GetComponentID();
}
1 голос
/ 15 марта 2019

Вы можете использовать аргументы шаблона по умолчанию, например:

class Entity
{
template <typename T,typename StoreAs=T, typename... TArgs>
    T &Entity::AddComponent(TArgs&&... args);
};
template <typename T,typename StoreAs, typename... TArgs>
T &Entity::AddComponent(TArgs&&... args)
{
     T *c = new T(std::forward<TArgs>(args)...);
     std::unique_ptr<Component> component(c);
     component->SetEntity(this);
     mComponents[GetComponentID<StoreAs()].push_back(std::move(component));
     return *c;
}

называется как

 entity.AddComponent<T>(...)//Will instatiate AddComponent<T,T,...>
 entity.AddComponent<T,U>(...)//Will instatiate AddComponent<T,U,...>

Вы можете даже пойти дальше и использовать некоторые SFINAE, чтобы включить эту функцию, только когдакомпонент может быть сохранен как этот тип: (может или не может фактически улучшить сообщение об ошибке)

template <typename T,typename StoreAs, typename... TArgs>
std::enable_if_t<std::is_base_of_v<StoreAs,T>,T&> //Return type is `T&`
Entity::AddComponent(TArgs&&... args)
{
     T *c = new T(std::forward<TArgs>(args)...);
     std::unique_ptr<Component> component(c);
     component->SetEntity(this);
     mComponents[GetComponentID<StoreAs>()].push_back(std::move(component));
     return *c;
}

Я предполагаю, что Component является базовым классом для всех компонентов.Если у вас есть конечный, известный набор компонентов, вы можете хранить их в std::variant<List types here> вместо уникальных указателей.

РЕДАКТИРОВАТЬ: Очевидно, что clang жалуется: «параметр шаблона переопределяет аргумент по умолчанию».Gcc не возражал, но, чтобы быть точным, поместите инициализацию StoreAs StoreAs=T только в классе Entity, а не в реализацию.Я редактировал исходный код.

...