Распределение сущностей в системе сущностей

9

Я совершенно не уверен, как я должен распределять / напоминать свои сущности в моей системе сущностей. У меня есть разные варианты, но у большинства из них, похоже, есть минусы, связанные с ними. Во всех случаях сущности похожи на идентификатор (целое число), и, возможно, с ним связан класс-оболочка. Этот класс-обертка имеет методы для добавления / удаления компонентов в / из сущности.

Прежде чем я упомяну опции, вот основная структура моей системы сущностей:

  • сущность
    • Объект, который описывает объект в игре
  • Составная часть
    • Используется для хранения данных для объекта
  • система
    • Содержит объекты с определенными компонентами
    • Используется для обновления сущностей с конкретными компонентами
  • Мир
    • Содержит сущности и системы для сущностной системы
    • Может создавать / уничтожать объекты и иметь / добавлять / удалять системы

Вот мои варианты, о которых я подумал:

Опция 1:

Не храните классы-обертки Entity, а просто сохраняйте следующие идентификаторы / удаленные идентификаторы. Другими словами, сущности будут возвращаться по значению, например так:

Entity entity = world.createEntity();

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

Cons

  • Могут быть дублирующиеся классы-обертки сущностей (поскольку должен быть реализован copy-ctor, а системы должны содержать сущности)
  • Если объект уничтожен, классы-оболочки дубликатов сущностей не будут иметь обновленного значения.

Вариант 2:

Сохраните классы обертки сущности в пуле объектов. т.е. объекты будут возвращены указателем / ссылкой, например так:

Entity& e = world.createEntity();

Cons

  • Если есть повторяющиеся объекты, то когда объект уничтожается, тот же объект объекта может быть повторно использован для выделения другого объекта.

Вариант 3:

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

т.е. чтобы добавить компонент с этим дизайном, он будет выглядеть так:

Entity e = world.createEntity();
world.addComponent<Position>(e, 0, 3);

Как применительно к этому:

Entity e = world.createEntity();
e.addComponent<Position>(0, 3);

Cons

  • Синтаксис
  • Дубликаты ID
miguel.martin
источник

Ответы:

12

Ваши идентификаторы должны быть смесью индекса и версии . Это позволит вам эффективно повторно использовать идентификаторы, использовать идентификатор для быстрого поиска компонентов и значительно упростит реализацию вашего «варианта 2» (хотя вариант 3 можно сделать гораздо более приятным с некоторой работой).

struct entity {
  uint16 version;
  /* and other crap that doesn't belong in components */
};

std::vector<entity> pool;
std::vector<uint16> freelist;
typedef uint32 entity_id; /* this shoudl be a wrapper class */

entity_id createEntity()
{
  uint16 index;
  if (!freelist.empty())
  {
    pool.push_back(entity());
    freelist.push_back(pool.size() - 1);
  }
  index = freelist.pop_back();

  return (pool[id].version << 16) | index;
}

void deleteEntity(entity_id id)
{
   uint16 index = id & 0xFFFF;
   ++pool[index].version;
   freelist.push_back(index);
}

entity* getEntity(entity_id id)
{
  uint16 index = id & 0xFFFF;
  uint16 version = id >> 16;
  if (index < pool.size() && pool[index].version == version)
    return &pool[index];
  else
    return NULL;
 }

Это выделит новое 32-разрядное целое число, которое является комбинацией уникального индекса (который уникален среди всех живых объектов) и тега версии (который будет уникальным для всех объектов, которые когда-либо занимали этот индекс).

При удалении объекта вы увеличиваете версию. Теперь, если у вас есть какие-либо ссылки на этот идентификатор, он больше не будет иметь тот же тег версии, что и объект, занимающий это место в пуле. Любые попытки позвонить getEntity(или по isEntityValidвашему желанию) потерпят неудачу. Если вы разместите новый объект в этой позиции, старые идентификаторы все равно потерпят неудачу.

Вы можете использовать что-то вроде этого для своего «варианта 2», чтобы убедиться, что оно просто работает, не беспокоясь о старых ссылках на сущности. Обратите внимание, что вы никогда не должны хранить, entity*так как они могут перемещаться ( pool.push_back()могут перераспределять и перемещать весь пул!) И использовать только entity_idдля долгосрочных ссылок. Используется getEntityдля получения объекта с более быстрым доступом только в локальном коде. Вы также можете использовать std::dequeили подобное, чтобы избежать аннулирования указателя, если хотите.

Ваш «вариант 3» - совершенно правильный выбор. Нет ничего плохого в том, world.foo(e)чтобы использовать вместо него e.foo(), тем более что вы, вероятно, worldвсе равно хотите ссылку на него, и не обязательно лучше (хотя и не обязательно хуже) хранить эту ссылку в самой сущности.

Если вы действительно хотите, чтобы e.foo()синтаксис оставался неизменным, рассмотрите «умный указатель», который обрабатывает это для вас. Создавая пример кода, который я дал выше, вы можете получить что-то вроде:

class entity_ptr {
  world* _world;
  entity_id _id;

public:
  entity_ptr() : _id(0) { }
  entity_ptr(world& world, entity_id id) : _world(&world), _id(id) { }

  bool empty() const { return _world != NULL && _world->getEntity(_id) != NULL; }
  void clear() { _world = NULL; _id = 0; }
  entity* get() { assert(!empty()); return _world->getEntity(_id); }
  entity* operator->() { return get(); }
  entity& operator*() { return *get(); }
  // add const method where appropriate
};

Теперь у вас есть способ сохранить ссылку на объект, который использует уникальный идентификатор и который может использовать ->оператор для доступа к entityклассу (и любому методу, который вы создаете для него) вполне естественным образом. _worldЧлен может быть одноплодным или глобальной, тоже, если вы предпочитаете.

Ваш код просто использует entity_ptrвместо любых других ссылок на сущности и идет. Вы можете даже добавить автоматический подсчет ссылок к классу, если хотите (несколько более надежно, если вы обновите весь этот код до C ++ 11 и используете семантику перемещения и ссылки на rvalue), так что вы можете просто использовать entity_ptrвезде и больше не задумываться о ссылках и собственности. Или, и это то, что я предпочитаю, сделать отдельное owning_entityи weak_entityтипы только с прежними управляющими подсчетами ссылок, чтобы вы могли использовать систему типов для разграничения между дескрипторами, которые поддерживают работу сущности, и теми, которые просто ссылаются на нее, пока она не будет уничтожена.

Обратите внимание, что накладные расходы очень низкие. Бит манипуляции дешево. Дополнительный поиск в пуле не является реальной стоимостью, если вы в entityлюбом случае получите доступ к другим полям . Если ваши сущности на самом деле просто идентификаторы и больше ничего, тогда могут быть дополнительные издержки. Лично мне кажется, что идея ECS, в которой сущности - это просто идентификаторы, а ничто иное, немного академична. По крайней мере, есть несколько флагов, которые вы хотите сохранить в общей сущности, а в более крупных играх, вероятно, понадобится коллекция компонентов сущности какого-либо вида (встроенный связанный список, если ничего больше) для поддержки инструментов и сериализации.

Как довольно последнее замечание я намеренно не инициализировал entity::version. Это не важно Независимо от того, что является начальной версией, при условии, что мы увеличиваем ее каждый раз, когда все в порядке. Если это закончится близко к 2^16этому, это просто обернется. Если вам придётся обойтись так, чтобы старые идентификаторы остались действительными, переключитесь на более крупные версии (и 64-битные идентификаторы, если вам нужно). Чтобы быть в безопасности, вам, вероятно, следует очищать entity_ptr каждый раз, когда вы проверяете его, и он пуст. Вы можете сделать empty()это для вас с изменчивым _world_и _id, просто будьте осторожны с потоками.

Шон Миддледич
источник
Почему бы не содержать идентификатор в структуре объекта? Я в замешательстве. Также не могли бы вы использовать std :: shared_ptr / weak_ptr для owning_entityи weak_entity?
miguel.martin
Вы можете вместо этого содержать идентификатор, если хотите. Единственный момент заключается в том, что значение идентификатора изменяется, когда объект в слоте уничтожается, тогда как идентификатор также содержит индекс слота для эффективного поиска. Вы можете использовать shared_ptrи weak_ptrне знать , что они предназначены для индивидуально выделенных объектов (хотя они могут иметь собственный удалившие , чтобы изменить это) , и поэтому не являются наиболее эффективными типами для использования. weak_ptrв частности, может не делать то, что вы хотите; он останавливает полное освобождение / повторное использование объекта до тех пор, пока каждый из них не weak_ptrбудет сброшен weak_entity.
Шон Мидлдич
Было бы намного проще объяснить этот подход, если бы у меня была доска, или я не слишком ленив, чтобы нарисовать ее в Paint или еще что-нибудь. :) Я думаю, что визуализация структуры делает ее чрезвычайно ясной.
Шон Мидлдич
gamesfromwithin.com/managing-data-relationships В этой статье, кажется, представлено кое-что - то же самое, что вы сказали в своем ответе, это то, что вы имеете в виду?
miguel.martin
1
Я автор EntityX , и повторное использование индексов на какое-то время беспокоило меня. На основании вашего комментария я обновил EntityX, чтобы также включить версию. Спасибо @SeanMiddleditch!
Алек Томас
0

Я сейчас работаю над чем-то похожим и использую решение, которое ближе всего к вашему номеру 1.

У меня есть EntityHandleэкземпляры, возвращенные из World. У каждого EntityHandleесть указатель на World(в моем случае, я его просто называю EntityManager), а методы манипулирования / поиска данных в них на EntityHandleсамом деле являются вызовами World: например, чтобы добавить объект Componentк сущности, вы можете вызвать EntityHandle.addComponent(component), что, в свою очередь, вызовет World.addComponent(this, component).

Таким образом, Entityклассы-обертки не сохраняются, и вы избегаете дополнительных накладных расходов в синтаксисе, которые вы получаете с вашим вариантом 3. Это также устраняет проблему «Если объект уничтожен, классы-обертки дублирующихся объектов не будут иметь обновленного значения ", потому что все они указывают на одни и те же данные.

vijoc
источник
Что произойдет, если вы сделаете другой EntityHandle похожим на ту же сущность, а затем попытаетесь удалить один из маркеров? Другой дескриптор будет по-прежнему иметь тот же идентификатор, что будет означать, что он «обрабатывает» мертвую сущность.
miguel.martin
Это правда, тогда остальные оставшиеся дескрипторы будут указывать на идентификатор, который больше не «держит» сущность. Конечно, следует избегать ситуаций, когда вы удаляете объект, а затем пытаетесь получить к нему доступ из другого места. WorldМожет, например , бросить исключение при попытке манипулировать / извлечение данных , связанные с «мертвой» сущностью.
Виджок
Хотя этого лучше избегать, в реальном мире это случится. Скрипты будут удерживать ссылки, «умные» игровые объекты (например, поиск ракет) будут удерживать ссылки и т. Д. Вам действительно нужна система, которая в любом случае способна правильно справляться с устаревшими ссылками или которая отслеживает и обнуляет слабые Ссылки.
Шон Мидлдич
Например, Мир может выдать исключение при попытке манипулировать / получить данные, связанные с «мертвой» сущностью. Нет, если старый идентификатор теперь назначен новой сущности.
miguel.martin