1
\$\begingroup\$

How can I implement better system/viewer for every entity that has components I specify in, let's say, a variadic template? I've tried to do it myself.

What I started with:

using EntityID = size_t;
static constexpr size_t INVALID_ENTITY_ID = -1;

struct BaseComponent
{
    virtual ~BaseComponent() = default;
    EntityID entity = INVALID_ENTITY_ID;
};

template<typename T>
struct BaseComponentT : BaseComponent
{
    virtual ~BaseComponentT() override = default;

    static const size_t ID;
    static const size_t SIZE;
};

template<typename T>
const size_t BaseComponentT<T>::ID(typeid(T).hash_code());

template<typename T>
const size_t BaseComponentT<T>::SIZE(sizeof(T));

struct PositionComponent : BaseComponentT<PositionComponent>
{
    PositionComponent() = default;
    PositionComponent(float x, float y)
        : x(x), y(y)
    { }

    float x = 0.0f;
    float y = 0.0f;
};

struct VelocityComponent : BaseComponentT<VelocityComponent>
{
    VelocityComponent() = default;
    VelocityComponent(float v)
        : v(v)
    {}

    float v = 0.0f;
};

struct ECS
{
    using ComponentTypeID = size_t;

    const EntityID CreateEntity()
    {
        entities.push_back(entities.size());
        return entities.back();
    }

    template<typename T>
    void AddComponent(const EntityID entity, T* component)
    {
        auto& components = componentsPool[T::ID];
        auto& newComponent = components.emplace_back(component);
        newComponent->entity = entity;
    }

    std::unordered_map<ComponentTypeID, std::vector<BaseComponent*>> componentsPool;
    std::vector<EntityID> entities;
};

and I need to somehow to run a function on every entities' components like this

auto view = ecs.View<PositionComponent, VelocityComponent>();
view.each([](PositionComponent*, VelocityComponent*){
    //do some work...
});

I thought that what I need is some container that I can refer to by entity id and it will give me a component type and its position in a pool.

something like this:

template<typename T>
void AddComponent(const EntityID entity, T* component)
{
    auto& components = componentsPool[T::ID];

    auto& newComponent = components.emplace_back(component);
    newComponent->entity = entity;

    entityComponents[entity].emplace_back(T::ID, components.size() - 1);
}

BaseComponent* getComponentInternal(std::vector<std::pair<EntityID, ComponentTypeID>>& entityComponents, std::vector<BaseComponent*>& componentPool, ComponentTypeID componentType)
{
    for(const auto& [id, index] : entityComponents) {
        if(id == componentType) {
            return componentPool[index];
        }
    }
}

std::unordered_map<EntityID, std::vector<std::pair<ComponentTypeID, size_t>>> entityComponents;

and then my viewer class would have a vector of tuples of components. The vector, because there may be many entities, and the tuple because an entity can have many components of different types.

template<typename ... Args>
struct View
{
    template<typename FuncT>
    void each(FuncT&& func)
    {
        for(auto& components : entityComponents) {
            std::apply(func, components);
        }
    }

    std::vector<std::tuple<Args*...>> entityComponents;
}

and now I need to somehow fill this vector. In a code, a little bit above I called this function view()

template<std::size_t I = 0, typename FuncT, typename... Tp>
inline typename std::enable_if<I == sizeof...(Tp), void>::type for_each(std::tuple<Tp...>&, FuncT) {}

template<std::size_t I = 0, typename FuncT, typename... Tp>
inline typename std::enable_if<I<sizeof...(Tp), void>::type for_each(std::tuple<Tp...>& t, FuncT f)
{
    f(std::get<I>(t));
    for_each<I + 1, FuncT, Tp...>(t, f);
}

template<typename ... Args>
View<Args...> ECS::view()
{
    //I need to get ids of components to update. 
    std::array<size_t, sizeof ... (Args)> componentTypeIDs{}; {
        size_t i = 0;
        (void(componentTypeIDs[i++] = Args::ID), ...);
    }

    //get just pools of components we need
    std::vector<std::vector<BaseComponent*>*> componentVectors(componentTypes.size()));
    for(size_t i = 0; i < componentTypeIDs.size(); ++i) {
        const ComponentTypeID componentTypeID = componentTypeIDs[i];
        componentVectors[i] = &componentTypes[componentTypeID];
    }

    std::vector<std::tuple<Args*...>> componentsToUpdatePool;
    componentsToUpdatePool.reserve(c.size());

    //'c' is as big as how many entities we wanna update, so iterating over it to get components for every entity.

    const std::vector<BaseComponent*>& c = *componentVectors.at(0);
    for(const auto& comp : c) {
        auto& componentsToUpdate = componentsToUpdatePool.emplace_back();

        //thanks to that `c` iterate over the same components and components have different entities
        //we can get components id of a particular entity.
        auto& ec = entityComponents[comp->entity];

        bool isValid = true;

        size_t j = 0;
        for_each(componentsToUpdate, [&](auto& component) {
            BaseComponent* baseComponent = getComponentInternal(ec, *componentVectors[j], componentTypeIDs[j]);
            if(!baseComponent) {
                isValid = false;
                return;
            }

            component = (decltype(component))baseComponent;
            ++j;
        });
    }

    return View{ .entityComponents = std::move(componentsToUpdatePool) };
}

And it works, but I think it is very tricky done and do too many allocations. Also method AddComponent() is inefficient because of this line:

entityComponents[entity].emplace_back(T::ID, components.size() - 1);

How can I improve my system and make AddComponent more efficient, especially a call that I showed above?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.