Советы по связыванию между компонентами системы в C ++

10

Прочитав несколько документов о сущности-компонентной системе, я решил реализовать свою. Пока у меня есть класс World, который содержит сущности и системный менеджер (системы), класс Entity, который содержит компоненты в виде std :: map, и несколько систем. Я держу сущности как std :: vector в Мире. Пока проблем нет. Что меня смущает, так это итерация сущностей, у меня не может быть кристально чистого разума, поэтому я все еще не могу реализовать эту часть. Должна ли каждая система иметь локальный список сущностей, в которых они заинтересованы? Или я должен просто выполнить итерацию по сущностям в классе World и создать вложенный цикл для итерации по системам и проверить, есть ли у сущности компоненты, которые интересуют систему? Я имею в виду :

for (entity x : listofentities) {
   for (system y : listofsystems) {
       if ((x.componentBitmask & y.bitmask) == y.bitmask)
             y.update(x, deltatime)
       }
 }

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

дениз
источник
Почему вы ожидаете, что подход с битовой маской препятствует связыванию скриптов? Кроме того, используйте ссылки (const, если возможно) в циклах for-each, чтобы избежать копирования сущностей и систем.
Бенджамин Клостер
использование битовой маски, например, int, будет содержать только 32 различных компонента. Я не имею в виду, что будет более 32 компонентов, но что если у меня есть? мне нужно будет создать другой int или 64bit int, он не будет динамическим.
Дениз
Вы можете использовать std :: bitset или std :: vector <bool>, в зависимости от того, хотите ли вы, чтобы он был динамическим во время выполнения.
Бенджамин Клостер

Ответы:

7

Наличие локальных списков для каждой системы увеличит использование памяти для классов.

Это традиционный компромисс пространства-времени .

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

Тем не менее, этот подход все еще может быть достаточно хорошим в зависимости от ваших целей.

Хотя, если вы беспокоитесь о скорости, есть, конечно, другие решения для рассмотрения.

Должна ли каждая система иметь локальный список сущностей, в которых они заинтересованы?

Точно. Это стандартный подход, который должен дать вам достойную производительность и достаточно прост в реализации. Накладные расходы памяти, на мой взгляд, ничтожны - мы говорим о хранении указателей.

Теперь, как поддерживать эти «списки интересов», может быть не так очевидно. Что касается контейнера данных, то std::vector<entity*> targetsвнутри класса системы вполне достаточно. Теперь то, что я делаю, это:

  • При создании сущность пуста и не принадлежит ни одной системе.
  • Всякий раз, когда я добавляю компонент к объекту:

    • получить свою текущую битовую подпись ,
    • сопоставить размер компонента с мировым пулом адекватного размера чанка (лично я использую boost :: pool) и выделить там компонент
    • получить новую битовую подпись объекта (которая является просто «текущей битовой подписью» плюс новый компонент)
    • переберите все мировые системы, и если есть система, чья подпись не соответствует текущей подписи сущности и действительно совпадает с новой подписью, становится очевидным, что мы должны отсылать туда указатель на нашу сущность.

          for(auto sys = owner_world.systems.begin(); sys != owner_world.systems.end(); ++sys)
                  if((*sys)->components_signature.matches(new_signature) && !(*sys)->components_signature.matches(old_signature)) 
                          (*sys)->add(this);

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

Теперь вы можете рассмотреть возможность использования std :: list, потому что удаление из вектора - это O (n), не говоря уже о том, что вам придется сдвигать большой кусок данных каждый раз, когда вы удаляете из середины. На самом деле, вам не нужно - поскольку нам не нужен порядок обработки на этом уровне, мы можем просто вызвать std :: remove и согласиться с тем фактом, что при каждом удалении нам нужно только выполнить O (n) поиск нашего подлежащее удалению лицо.

std :: list даст вам O (1) удалить, но с другой стороны у вас есть немного дополнительной памяти. Также помните, что большую часть времени вы будете обрабатывать объекты, а не удалять их - и это, безусловно, выполняется быстрее с использованием std :: vector.

Если вы очень критичны к производительности, вы можете рассмотреть даже другой шаблон доступа к данным , но в любом случае вы поддерживаете какие-то «списки интересов». Однако помните, что если вы сохраняете свой API-интерфейс Entity System достаточно абстрагированным, не должно возникнуть проблем с улучшением методов обработки сущностей систем, если из-за них снижается частота кадров - поэтому пока выберите метод, который проще всего для кода - только затем профиль и улучшить, если это необходимо.

Патрик Чачурски
источник
5

Есть подход, который стоит рассмотреть, когда каждая система владеет компонентами, связанными с ней, а сущности ссылаются только на них. По сути, ваш (упрощенный) Entityкласс выглядит так:

class Entity {
  std::map<ComponentType, Component*> components;
};

Когда вы говорите, что RigidBodyкомпонент подключен к Entity, вы запрашиваете его у своей Physicsсистемы. Система создает компонент и позволяет объекту сохранять указатель на него. Ваша система выглядит так:

class PhysicsSystem {
  std::vector<RigidBodyComponent> rigidBodyComponents;
};

Теперь это может показаться немного противоречивым на первый взгляд, но преимущество заключается в том, как системы сущностей компонентов обновляют свое состояние. Часто вы будете перебирать свои системы и запрашивать обновление соответствующих компонентов.

for(auto it = systems.begin(); it != systems.end(); ++it) {
  it->update();
}

Сила того, что все компоненты, принадлежащие системе, находятся в смежной памяти, состоит в том, что когда ваша система выполняет итерацию по каждому компоненту и обновляет его, она в основном должна

for(auto it = rigidBodyComponents.begin(); it != rigidBodyComponents.end(); ++it) {
  it->update();
}

Он не должен перебирать все объекты, которые потенциально не имеют компонента, который необходимо обновить, и он также имеет потенциал для очень хорошей производительности кэша, поскольку все компоненты будут храниться непрерывно. Это одно, если не самое большое преимущество этого метода. Вы часто будете иметь сотни и тысячи компонентов в одно и то же время, с таким же успехом можете попробовать и быть максимально производительными.

В этот момент ваши Worldединственные циклы проходят по системам и вызывают updateих без необходимости итерации сущностей. Это (imho) лучший дизайн, потому что тогда обязанности систем намного яснее.

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

pwny
источник
хороший ответ, спасибо. но компоненты не имеют функций (например, update ()), только данные. и система обрабатывает эти данные. Итак, согласно вашему примеру, я должен добавить виртуальное обновление для класса компонента и указатель объекта для каждого компонента, я прав?
Дениз
@deniz Все зависит от вашего дизайна. Если у ваших компонентов нет каких-либо методов, а есть только данные, система все равно может выполнять их итерацию и выполнять необходимые действия. Что касается обратной связи с сущностями, да, вы можете сохранить указатель на сущность-владелец в самом компоненте или сделать так, чтобы ваша система поддерживала карту между дескрипторами компонента и сущностями. Как правило, вы хотите, чтобы ваши компоненты были как можно более автономными. Компонент, который вообще не знает о своей родительской сущности, является идеальным. Если вам нужно общение в этом направлении, предпочитайте события и тому подобное.
pwny
Если вы скажете, что это будет лучше для эффективности, я буду использовать ваш шаблон.
Дениз
@deniz Убедитесь, что вы действительно профилируете свой код рано и часто, чтобы определить, что работает, а что нет для вашего конкретного
движка
хорошо :) я сделаю своего рода стресс-тест
Дениз
1

На мой взгляд, хорошей архитектурой является создание слоя компонентов в объектах и ​​разделение управления каждой системой на этом уровне компонентов. Например, логическая система имеет некоторые логические компоненты, которые влияют на их сущность, и хранят общие атрибуты, которые являются общими для всех компонентов в сущности.

После этого, если вы хотите управлять объектами каждой системы в разных точках или в определенном порядке, лучше создать список активных компонентов в каждой системе. Все списки указателей, которые вы можете создавать и управлять в системах, содержат менее одного загруженного ресурса.

superarce
источник