Сделайте следующий кэш реализации ECS дружественным - PullRequest
0 голосов
/ 06 мая 2020

У меня есть реализация ECS sh, чтобы сделать более дружественный к кешу, и я застрял в том, что должен делать.

Позвольте мне сначала представить вам реализацию.

ECSManager.h:

class ECSManager
{
private:
    std::unordered_map<unsigned int, std::vector<std::unique_ptr<ComponentBase>>> m_Components;
    std::unordered_map<unsigned int, std::unique_ptr<Entity>> m_Entities;
    std::vector<std::unique_ptr<System>> m_Systems;
public:

    void Update()
    {
        for (const auto& system : m_Systems)
        {
            auto entities = GetEntities(system->GetComponentsList());
            system->Update(std::move(entities), this);
        }
    }

    const Entity& CreateEntity(std::string&& name)
    {
        std::unique_ptr<Entity> e(new Entity(std::forward<std::string>(name)));
        unsigned int id = e->GetID();
        m_Entities.try_emplace(id, std::move(e));
        return *m_Entities[id].get();
    }

    template <typename TSystem, typename... Args>
    const System& CreateSystem(Args&&... args)
    {
        static_assert(std::is_base_of<System, TSystem>::value, "type parameter of this class must derive from System");
        m_Systems.push_back(std::unique_ptr<TSystem>(new TSystem(std::forward<Args>(args)...)));
        return *m_Systems[m_Systems.size() - 1].get();
    }

    template <typename TComponent, typename... Args>
    void AddComponent(unsigned int entityId, Args&&... args)
    {
        unsigned int componentID = TComponent::ID;
        if (m_Components.find(componentID) == m_Components.end())
            m_Components[componentID] = std::vector<std::unique_ptr<ComponentBase>>();
        m_Components[componentID].push_back(std::unique_ptr<TComponent>(new TComponent(std::forward<Args>(args)...)));

        m_Entities[entityId]->AddComponent(componentID, m_Components[componentID].size() - 1);
    }

    template <typename TComponent>
    TComponent& GetComponent(const Entity& entity)
    {
        static_assert(std::is_base_of<ComponentBase, TComponent>::value, "type parameter of this class must derive from Component");
        auto componentsIndex = entity.GetComponentsIndex();
        return *dynamic_cast<TComponent*>(m_Components[TComponent::ID][componentsIndex[TComponent::ID]].get());
    }

    template <typename TComponent>
    std::vector<TComponent>& GetComponents()
    {
        unsigned int componentID = TComponent::ID;
        return m_Components[componentID];
    }

    std::vector<std::vector<std::reference_wrapper<Entity>>> GetEntities(const std::vector<DynamicBitset>& componentsList)
    {
        std::vector<std::vector<std::reference_wrapper<Entity>>> entities;
        for (unsigned int i = 0; i < componentsList.size(); i++)
        {
            auto& componentIDs = componentsList[i];
            size_t count = componentIDs.size();
            entities.push_back(std::vector<std::reference_wrapper<Entity>>{});
            for (const auto& entity : m_Entities)
            {
                const auto& entitySignature = entity.second->GetSignature();
                if (count > entitySignature.size())
                    continue;

                bool equal = true;
                for (std::size_t i = 0; i != count; i++)
                {
                    if (componentIDs[i]() && !entitySignature[i]())
                    {
                        equal = false;
                        break;
                    }
                }
                if (!equal)
                    continue;

                entities[i].push_back(*entity.second.get());
            }
        }
        return entities;
    }
};

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

При добавлении Компонента к Сущности ECSManager будет не только отслеживать Компонент, но также будет вызывать AddComponent(componentID, m_Components[componentID].size() - 1); Сущности, что заставит Сущность удерживать индекс этого идентификатора компонента внутри внутренней структуры данных, так что позже это можно использовать в качестве метода поиска для поиска c определенного типа компонента Entity внутри m_Components с использованием template <typename TComponent> TComponent& GetComponent(const Entity& entity).

ECSManager также вызывает Update каждый кадр и вызывает метод Update каждой Системы с соответствующими объектами, с которыми система будет работать sh, вызывая GetEntities(system->GetComponentsList()); и обнаруживая, подпись компонентов каких объектов соответствует подписи компонента, с которой Система хочет работать on.

Проблема Я думаю, что эта реализация поддерживает кеширование:

  • Система оперирует вектором совпадающих сущностей на основе сигнатуры компонента std::vector<std::vector<std::reference_wrapper<Entity>>> в то время как векторы сохраняются и передаются в Систему линейным образом, при их итерации предположим, что будет загрузка кеша одной строки для элементов вектора, но также будет кеш строк для каждого элемента, поскольку ссылки являются указателями за сценой. Таким образом, оптимально Системе можно было бы обслуживать вектор сущностей типа значения, но это будет проблемой, поскольку системы потенциально изменяют сущности, поэтому мы не можем передавать копии, и мы не можем передать весь исходный вектор m_Entities (допустим, m_Entities теперь это std::vector<Entity> вместо того, что было в моей реализации), поскольку Система должна работать только с подмножеством сущностей. Я думаю, что можно было бы передать ВСЕ сущности методу Update Системы с массивом дескрипторов того, какие сущности должны использоваться этой системой (как предложено в статье Скотта Мейера ), поэтому в кеш будет загружен весь массив дескрипторов, а не весь массив сущностей, но я обнаружил проблему с этим подходом: сущность в моей реализации можно было бы упростить до просто идентификатора типа uint32_t, например, если бы у нас было 100 объекты, скажем, мы загрузим в кеш 100 * sizeof(bool) + sizeof(Entity) * number_of_valid_entities_for_specific_system байт вместо 100 * sizeof(uint32_t), что IS - хорошая микрооптимизация, но если бы у нас было, скажем, 10000000000 объектов, мы бы все равно перебирали более 10000000000 элементов, которые Я не уверен, перевесит ли эта микрооптимизация свои преимущества по мере появления большего количества объектов.
  • Предположим, что мы каким-то образом решили первую проблему. Теперь каждая система получает линейный набор сущностей по значению для работы. Теперь каждая система будет запрашивать у каждой сущности соответствующие компоненты, с которыми она хочет работать, с помощью вызова template <typename TComponent> TComponent& GetComponent(const Entity& entity). Это также не подходит для кеширования, поскольку m_Components хранится в std::unordered_map, который не сохраняется линейно, и еще одна проблема здесь снова заключается в том, что у нас есть вектор указателей, поэтому у нас будет загрузка строки кеша для каждого разыменования компонента, который неизбежно, так как я держу вектор указателей. Я делаю это потому, что ComponentBase - абстрактный класс, и у меня не может быть std::vector<T>, где T - абстрактный. Одним из способов решения этой проблемы является то, что вместо System Update, работающей с сущностями, она будет работать с непрерывным массивом компонентов. Возможная реализация может быть следующей:
template <typename TComponent>
class ComponentManager {
std::vector<TComponent> m_Components;
std::vector<Entity> m_Entities;

    Component& Create(Entity entity)
    {
      m_Components.push_back(TComponent());
      m_Entities.push_back(entity);
      return m_Components.back();
    }
};
...
...
...
ComponentManager<Transform> transforms;
transforms.Create(entity);

// System update loop
for(size_t i = 0; i < transforms.GetCount(); ++i)
{
  Transform& transform = transforms[i];
  ...
}

Но проблема в том, что если у нас система работает с большим количеством сущностей с более чем одним компонентом, нам придется либо ссылаться на другой ComponentManager<TComponent> и используйте метод поиска с задействованной картой, снова вызывая множество промахов кеша и загрузку новых строк кеша, или если бы мы могли каким-то образом синхронизировать c индексы разных ComponentManager<TComponent>, чтобы соответствовать индексу того же объекта, но это не Это невозможно, потому что возьмем этот пример:

Entity e1;
// add Mass component to e1
// Now ComponentManager<Mass>[0] is e1 mass
Entity e2;
// add Transform component to e2
// Now ComponentManager<Transform>[0] is e2 transform
// add Mass component to e2
// Now ComponentManager<Mass>[1] is e2 mass
...
Entity e3;
// add RigidBody component to e3
// Now ComponentManager<RigidBody>[0] is e3 rigidbody
// add Mass component to e3
// Now ComponentManager<Mass>[2] is e3 mass

мы можем синхронизировать c e2 массу как первую массу с индексом 0 или e3 массу как первую массу с индексом 0, но мы не можем иметь их обе с индексом 0.

  • Помимо этих двух пунктов, которые, как мне кажется, являются основной проблемой, можно легко исправить такие проблемы, как изменение std::vector<std::unique_ptr<System>> m_Systems; на std::vector<System> m_Systems;, чтобы избежать загрузки строки кеша для каждой разыменованной системы и других проблем. действительно зависит от того, какое решение я использую go, поэтому я не хочу много о них говорить.

Итак, это в основном проблемы, такие как Я ВИЖУ ИХ , может я ошибаюсь, может быть, есть еще кое-что, я не уверен и хотел бы, чтобы люди, которые лучше понимают, высказывали мне свое мнение и выслушивали свои идеи.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...