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


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

Ответы:


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. Это также устраняет проблему «Если объект уничтожен, классы-обертки дублирующихся объектов не будут иметь обновленного значения ", потому что все они указывают на одни и те же данные.


Что произойдет, если вы сделаете другой EntityHandle похожим на ту же сущность, а затем попытаетесь удалить один из маркеров? Другой дескриптор будет по-прежнему иметь тот же идентификатор, что будет означать, что он «обрабатывает» мертвую сущность.
— miguel.martin

Это правда, тогда остальные оставшиеся дескрипторы будут указывать на идентификатор, который больше не «держит» сущность. Конечно, следует избегать ситуаций, когда вы удаляете объект, а затем пытаетесь получить к нему доступ из другого места. WorldМожет, например , бросить исключение при попытке манипулировать / извлечение данных , связанные с «мертвой» сущностью.
— Виджок

Хотя этого лучше избегать, в реальном мире это случится. Скрипты будут удерживать ссылки, «умные» игровые объекты (например, поиск ракет) будут удерживать ссылки и т. Д. Вам действительно нужна система, которая в любом случае способна правильно справляться с устаревшими ссылками или которая отслеживает и обнуляет слабые Ссылки.
— Шон Мидлдич

Например, Мир может выдать исключение при попытке манипулировать / получить данные, связанные с «мертвой» сущностью. Нет, если старый идентификатор теперь назначен новой сущности.
— miguel.martin
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.