Компонентна конструкція: обробка взаємодії об'єктів


9

Я не впевнений, як саме об’єкти роблять речі для інших об’єктів у складі, що базується на компонентах.

Скажіть, у мене Objклас. Я згоден:

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

Як я міг тоді мати інший об'єкт не тільки рухати м'ячем, але й застосовувати ті фізики. Я не шукаю деталей реалізації, а досить абстрактно, як об’єкти спілкуються. У дизайні, заснованому на юридичній особі, ви можете просто мати:

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

Будь-яка стаття чи пояснення, щоб краще зрозуміти дизайн, орієнтований на компоненти, і як робити основні речі були б дуже корисними.

Відповіді:


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);

У цьому випадку, obj2's Physicsвибере повідомлення і зробить будь-яку обробку, яку він повинен зробити. Після закінчення він буде:

  • Надішліть повідомлення "SetPosition" самому, що Positionкомпонент вибере;
  • Або безпосередньо звертайтеся до Positionкомпонента для модифікацій (зовсім неправильно для чистого дизайну на основі компонентів, оскільки ви не можете припустити, що кожен об'єкт має Positionкомпонент, але Positionкомпонент може бути вимогою Physics).

Як правило, добре затримати фактичну обробку повідомлення до оновлення наступного компонента. Обробка його негайно може означати надсилання повідомлень іншим компонентам інших об'єктів, тому надсилання лише одного повідомлення може швидко означати нерозривний стек спагетті.

Пізніше вам, мабуть, доведеться піти на більш вдосконалену систему: асинхронні черги повідомлень, надсилання повідомлень групі об'єктів, реєстрація / відреєстрація за допомогою компонентів та ін.

MessageКлас може бути універсальним контейнером для простої рядки , як показано вище, але обробка рядків під час виконання не надто ефективні. Ви можете знайти контейнер із загальними значеннями: рядки, цілі числа, поплавці ... Ім'я або ще краще - ідентифікатор, щоб розрізняти різні типи повідомлень. Або ви також можете отримати базовий клас відповідно до конкретних потреб. У вашому випадку ви можете уявити собі, EmitForceMessageщо походить від Messageі додає бажаний вектор сили - але остерігайтеся витрат на виконання RTTI, якщо ви це зробите.


3
Я б не переймався "чистотою" прямого доступу до компонентів. Компоненти використовуються для задоволення функціональних та конструкторських потреб, а не для наукових груп. Ви хочете перевірити наявність компонента (наприклад, перевірити значення повернення не є нульовим для виклику get компонент).
Шон Міддлічч

Я завжди думав про це так, як ви востаннє говорили, використовуючи RTTI, але так багато людей сказали стільки поганого про RTTI
jmasterx

@SeanMiddleditch Звичайно, я би зробив це таким чином, лише зазначивши це, щоб зрозуміти, що ви завжди повинні двічі перевірити, що ви робите, коли отримуєте доступ до інших компонентів того самого об'єкта.
Лоран Кувіду,

@Milo Запропонований компілятором RTTI і його dynamic_cast може стати вузьким місцем, але я б зараз не турбувався про це. Ви все ще можете оптимізувати це згодом, якщо це стане проблемою. Ідентифікатори класів на основі CRC працюють як шарм.
Лоран Кувіду

´template <typename T> uint32_t class_id () {статичний uint32_t v; return (uint32_t) & v; } ´ - RTTI не потрібно.
руль

3

Що я зробив для вирішення проблеми, подібної до тієї, яку ви показуєте, - це додати деякі конкретні обробники компонентів та додати якусь систему вирішення подій.

Так, у випадку з вашим об'єктом "Фізика", коли це ініціалізується, він додасть себе до центрального менеджера об'єктів Physics. У циклі ігор такі менеджери мають свій власний крок оновлення, тому коли цей PhysicsManager оновлюється, він обчислює всі фізичні взаємодії та додає їх у чергу подій.

Після того, як ви виробляєте всі свої події, ви можете вирішити свою чергу подій, просто перевіривши те, що сталося, і вчинити відповідні дії відповідно, у вашому випадку повинна бути подія, яка каже, що об'єкти A і B якось взаємодіяли, тому ви викликаєте свій метод emitForceOn.

Плюси цього методу:

  • Концептуально наслідувати насправді просто.
  • Дає вам можливість для конкретних оптимізацій, таких як використання квадратури або все, що вам потрібно.
  • Це в кінцевому підсумку є насправді "підключи і грай". Об'єкти з фізики не взаємодіють з нефізичними об'єктами, оскільки вони не існують для менеджера.

Мінуси:

  • У вас з'являється безліч посилань, тому ви можете трохи забруднити, щоб правильно очистити все, якщо ви не будете обережні (від свого компонента до власника компонента, від менеджера до компонента, від події до учасників тощо). ).
  • Ви повинні поставити особливу думку до порядку, в якому ви вирішуєте все. Я думаю, це не ваш випадок, але я зіткнувся з більш ніж нескінченним циклом, де подія створила ще одну подію, і я просто додавав її до черги подій безпосередньо.

Я сподіваюся, що це допомагає.

PS: Якщо хтось має більш чистий / кращий спосіб вирішити це, я дуже хотів би це почути.


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, you-name-it сумісний. Жодного синтаксичного цукру, який би нічого не спростив обробку повідомлення.
  • Один рядовий параметр: передача нативних типів не є дешевшим з точки зору потреби в пам'яті, оскільки вам потрібно підтримувати невідому кількість параметрів відносно невідомого типу.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.