Отделение систем от менеджера ECS (Entity Component System) в C ++ - PullRequest
0 голосов
/ 04 октября 2019

В настоящее время я делаю программу Entity-Component-System как способ изучения C ++ (я знаю, что это, вероятно, не рекомендуемый способ, но мне весело). Пока все идет хорошо, но я надеюсь сделать еще несколько улучшений.

Каждая система в настоящее время добавляется в диспетчер ECS как std::function вместе с сигнатурой того, какие компоненты требуется запустить. Моя текущая стратегия заключается в итерации каждой системы и объекта, и если сигнатуры компонентов совпадают, то системная функция вызывается с идентификатором объекта в качестве параметра.

// System function
static Game::runSystem(Manager & mngr, int id) {

    ComponentA * a = mngr.getComponent<ComponentA>(id);
    ComponentB * b = mngr.getComponent<ComponentB>(id);

    // Do something with a and b

}

Эта функция присоединяется к менеджеру с помощью этого метода:

// Definition
template<typename...Args>
void Manager::addSystem(std::function<void(XManager & mngr, int id)> f); // A bitmask signature is generated based on the template

// Use
manager.addSystem<ComponentA, ComponentB>(Game::runSystem);

ComponentA и ComponentB - это просто общие типы. Я даже могу иметь компонент float или int, если мне нужно. При использовании getComponent<T>() они приводятся в соответствующие указатели.

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

Желательно, чтобы каждый процесс в конечном итоге выглядел примерно так:

static Game::runSystem(ComponentA * a, ComponentB * b) {
    // Do something with a and b
}

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

В настоящее время я храню компоненты в std::unordered_map в соответствии с битовой маской его Типа. Когда необходим компонент, шаблон предоставляет тип, из которого я могу сделать битовую маску, а затем наложить ее. Я не уверен, возможно ли сохранить тип компонента для приведения, поэтому я думаю, что каждый компонент должен быть передан в функцию как указатель void. Но C ++ не позволяет мне неявно приводить из void* к другому указателю, не зная типа. Есть ли способ, которым я могу сохранить Тип каждого компонента для приведения позже, когда это необходимо?

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

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

1 Ответ

0 голосов
/ 11 октября 2019

Проблема с ECS заключается в том, что у каждого человека есть своя собственная интерпретация того, как он работает.

Единственное, о чем Система должна знать, это указатели компонентов для конкретной сущности, над которой она работает.

Я не согласен с этим, по крайней мере, скак оно эффективно написано.

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

Это все работает отлично, но в итоге я должен включить файл заголовка для менеджера для каждой системы, и я бы предпочел, чтобы они не были привязаны друг к другу (думаю, это будетбыть разделением интересов)

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

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

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

void DeathSystem::update(const FrameContext& context) {
  const auto components = getEntitySystem().getComponents<AttributeComponent>();
  for ( const AttributeComponent &component : components ) {
    if ( component.getAttributeAsLong(HEALTH) == 0 ) {
      sendEvent( EntityDiedEvent( component.getEntityId() ) );
    }
  }
}

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

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

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

Вы можете легко написать такую ​​систему. Если что-то вроде этого является идеальным или даже полезно спорно, но делегировании, что система делает себе только делает код намного проще ИМХО.

1033 *
...