Компонентный дизайн: взаимодействие объектов

9

Я не уверен, как именно объекты делают вещи с другими объектами в компонентном дизайне.

Скажи, у меня есть Objкласс. Я делаю:

Obj obj;
obj.add(new Position());
obj.add(new Physics());

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

obj1.emitForceOn(obj2,5.0,0.0,0.0);

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

jmasterx
источник

Ответы:

10

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

Чтобы ответить на ваш конкретный пример, можно определить небольшой Message класс, который могут обрабатывать ваши объекты, например:

struct Message
{
    Message(const Objt& sender, const std::string& msg)
        : m_sender(&sender)
        , m_msg(msg) {}
    const Obj* m_sender;
    std::string m_msg;
};

void Obj::Process(const Message& msg)
{
    for (int i=0; i<m_components.size(); ++i)
    {
        // let components do some stuff with msg
        m_components[i].Process(msg);
    }
}

Таким образом, вы не «загрязняете» Objинтерфейс вашего класса компонентными методами. Некоторые компоненты могут выбрать обработку сообщения, некоторые могут просто игнорировать его.

Вы можете начать с вызова этого метода непосредственно из другого объекта:

Message msg(obj1, "EmitForce(5.0,0.0,0.0)");
obj2.ProcessMessage(msg);

В этом случае obj2s Physicsвыберет сообщение и выполнит любую необходимую ему обработку. Когда это будет сделано, оно будет либо:

  • Отправьте сообщение «SetPosition» себе, что Position выберет компонент;
  • Или напрямую получить доступ к Positionкомпоненту для модификаций (что совершенно неверно для дизайна, основанного исключительно Positionна Positionкомпонентах , поскольку нельзя предполагать, что каждый объект имеет компонент, но компонент может быть требованиемPhysics ).

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

Возможно, позже вам придется перейти на более продвинутую систему: асинхронные очереди сообщений, отправка сообщений в группу объектов, регистрация / отмена регистрации для каждого компонента и т. Д.

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

Лоран Кувиду
источник
3
Я бы не стал беспокоиться о «не чистоте» прямого доступа к компонентам. Компоненты используются для удовлетворения функциональных и дизайнерских потребностей, а не для научных кругов. Вы хотите проверить, существует ли компонент (например, проверьте, что возвращаемое значение не является нулевым для вызова компонента get).
Шон Мидлдитч
Я всегда думал об этом, как вы в последний раз говорили, используя RTTI, но многие люди говорили так много плохого о RTTI
jmasterx
@SeanMiddleditch Конечно, я бы сделал это так, просто упомянув это, чтобы дать понять, что вы всегда должны перепроверять, что вы делаете, когда получаете доступ к другим компонентам того же объекта.
Лоран Кувиду
@Milo Реализованный компилятором RTTI и его dynamic_cast могут стать узким местом, но я пока не буду об этом беспокоиться. Вы все еще можете оптимизировать это позже, если это станет проблемой. Основанные на CRC идентификаторы классов работают как шарм.
Лоран Кувиду
Шаблон <typename T> uint32_t class_id () {static uint32_t v; return (uint32_t) & v; } ´ - RTTI не требуется.
Арул
3

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

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

После того, как вы создадите все свои события, вы можете разрешить свою очередь событий, просто проверив, что произошло, и предприняв соответствующие действия; в вашем случае должно быть событие, говорящее о том, что объекты A и B как-то взаимодействовали, поэтому вы вызываете свой метод emitForceOn.

Плюсы этого метода:

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

Минусы:

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

Надеюсь, это поможет.

PS: Если у кого-то есть более чистый / лучший способ решить эту проблему, я бы очень хотел это услышать.

Carlos
источник
1
obj->Message( "Physics.EmitForce 0.0 1.1 2.2" );
// and some variations such as...
obj->Message( "Physics.EmitForce", "0.0 1.1 2.2" );
obj->Message( "Physics", "EmitForce", "0.0 1.1 2.2" );

Несколько вещей, чтобы отметить на этом дизайне:

  • Имя компонента является первым параметром - это позволяет избежать слишком большой работы кода над сообщением - мы не можем знать, какие компоненты может вызвать любое сообщение - и мы не хотим, чтобы все они жевали сообщение с ошибкой 90% скорость, которая преобразуется во множество ненужных веток и strcmp .
  • Имя сообщения является вторым параметром.
  • Первая точка (в № 1 и № 2) не нужна, она просто облегчает чтение (для людей, а не для компьютеров).
  • Это Sscanf, Iostream, вы-имя-это совместимо. Нет синтаксического сахара, который ничего не делает для упрощения обработки сообщения.
  • Один строковый параметр: передача нативных типов не обходится дешевле с точки зрения требований к памяти, поскольку необходимо поддерживать неизвестное количество параметров относительно неизвестного типа.
snake5
источник