Ваши идентификаторы должны быть смесью индекса и версии . Это позволит вам эффективно повторно использовать идентификаторы, использовать идентификатор для быстрого поиска компонентов и значительно упростит реализацию вашего «варианта 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
, просто будьте осторожны с потоками.
owning_entity
иweak_entity
?shared_ptr
иweak_ptr
не знать , что они предназначены для индивидуально выделенных объектов (хотя они могут иметь собственный удалившие , чтобы изменить это) , и поэтому не являются наиболее эффективными типами для использования.weak_ptr
в частности, может не делать то, что вы хотите; он останавливает полное освобождение / повторное использование объекта до тех пор, пока каждый из них неweak_ptr
будет сброшенweak_entity
.Я сейчас работаю над чем-то похожим и использую решение, которое ближе всего к вашему номеру 1.
У меня есть
EntityHandle
экземпляры, возвращенные изWorld
. У каждогоEntityHandle
есть указатель наWorld
(в моем случае, я его просто называюEntityManager
), а методы манипулирования / поиска данных в них наEntityHandle
самом деле являются вызовамиWorld
: например, чтобы добавить объектComponent
к сущности, вы можете вызватьEntityHandle.addComponent(component)
, что, в свою очередь, вызоветWorld.addComponent(this, component)
.Таким образом,
Entity
классы-обертки не сохраняются, и вы избегаете дополнительных накладных расходов в синтаксисе, которые вы получаете с вашим вариантом 3. Это также устраняет проблему «Если объект уничтожен, классы-обертки дублирующихся объектов не будут иметь обновленного значения ", потому что все они указывают на одни и те же данные.источник
World
Может, например , бросить исключение при попытке манипулировать / извлечение данных , связанные с «мертвой» сущностью.