Як правильно реалізовувати обробку повідомлень у складовій системі об'єктів?


30

Я реалізую варіант системної системи, який має:

  • Клас сутностей , який трохи більше , ніж ID , який пов'язує компоненти разом

  • Купа компонентних класів, що не мають "логіки компонентів", лише дані

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

  • Об'єкт класу MessageChannel , який є загальним для всіх ігрових систем. Кожна система може підписатися на певний тип повідомлень для прослуховування, а також може використовувати канал для трансляції повідомлень іншим системам

Початковий варіант обробки системних повідомлень був приблизно таким:

  1. Запускайте оновлення для кожної ігрової системи послідовно
  2. Якщо система робить щось з компонентом і ця дія може зацікавити інші системи, система надсилає відповідне повідомлення (наприклад, система дзвінки

    messageChannel.Broadcast(new EntityMovedMessage(entity, oldPosition, newPosition))

    щоразу, коли об'єкт переміщено)

  3. Кожна система, яка підписалася на конкретне повідомлення, отримує назву методу обробки повідомлень

  4. Якщо система обробляє подію, а логіка обробки подій вимагає трансляції іншого повідомлення, повідомлення отримує трансляцію відразу, а інший ланцюжок методів обробки повідомлень викликається

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

Кожен раз, коли суб'єкт рухається, система зіткнення перевіряє, чи стикається суб'єкт із чимось у новій позиції. Якщо це так, зіткнення виявляється. І якщо обидва зіштовхуються об'єкти - це "фізичні об'єкти" (вони обидва мають компонент RigidBody і покликані відштовхувати один одного, щоб не займати одне і те саме простір), спеціальна система жорсткого розділення тіла просить систему руху перемістити об'єкти до деяких конкретні позиції, які б їх розділили. Це, в свою чергу, змушує систему руху надсилати повідомлення, що сповіщають про змінені позиції сутності. Система виявлення зіткнень покликана реагувати, оскільки їй необхідно оновити її просторовий індекс.

У деяких випадках це спричиняє проблеми, оскільки вміст комірки (загальний список об'єктів Entity у C #) змінюється під час їх перегляду, тим самим викликаючи ітератор, який викидає виняток.

Отже ... як я можу запобігти перериванню системи зіткнення під час перевірки на зіткнення?

Звичайно, я міг би додати деяку "розумну" / "хитру" логіку, яка забезпечує вміст комірок правильно ітератуватися, але я думаю, що проблема полягає не в самій системі зіткнення (у мене також були подібні проблеми в інших системах), а в тому, як повідомлення обробляються, коли вони переходять із системи в систему. Що мені потрібно - це певний спосіб переконатися, що конкретний метод обробки подій виконує свою роботу без будь-яких перерв.

Що я спробував:

  • Черги на вхідні повідомлення . Кожен раз, коли деяка система транслює повідомлення, повідомлення додається до черг повідомлень зацікавлених у ньому систем. Ці повідомлення обробляються, коли оновлення системи викликається кожним кадром. Проблема : якщо система A додає повідомлення до черги B системи, вона працює добре, якщо система B має бути оновлена ​​пізніше системи A (у тому ж ігровому кадрі); інакше це призводить до того, що повідомлення обробляє наступний ігровий кадр (небажано для деяких систем)
  • Черги на вихідні повідомлення . Поки система обробляє подію, будь-які повідомлення, які вона транслює, додаються до черги вихідних повідомлень. Повідомлення не потрібно чекати, коли буде оброблено оновлення системи: вони отримують обробку "відразу" після того, як початковий обробник повідомлень закінчить роботу. Якщо обробка повідомлень призводить до трансляції інших повідомлень, вони також додаються у вихідну чергу, тому всі повідомлення обробляються в одному кадрі. Проблема: якщо система життєдіяльності суб'єкта господарювання (я реалізував управління життям сутності за допомогою системи) створює сутність, вона повідомляє про це деякі системи A і B. У той час як система A обробляє повідомлення, це призводить до знищення ланцюга повідомлень, які врешті-решт призводять до знищення створеної сутності (наприклад, об'єкт кулі, створений прямо там, де він стикається з деякою перешкодою, через що куля саморуйнується). Поки ланцюжок повідомлень вирішується, система B не отримує повідомлення про створення сутності. Отже, якщо система B також зацікавлена ​​у повідомленні про знищення сутності, вона отримує її, і лише після того, як «ланцюжок» закінчиться розв’язуванням, отримує початкове повідомлення про створення сутності. Це призводить до ігнорування повідомлення про знищення, повідомлення про створення - "прийнято",

РЕДАКТУВАТИ - ВІДПОВІДИ НА ЗАПИТАННЯ, КОМЕНТАРІ:

  • Хто модифікує вміст комірки, коли система зіткнення перетворюється на них?

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

  • Ви не можете працювати з глобальною чергою вихідних повідомлень?

Нещодавно я спробував одну глобальну чергу. Це спричиняє нові проблеми. Проблема: я переміщую об'єкт танка в настінну сутність (цистерною керує клавіатура). Тоді я вирішую змінити напрямок танка. Щоб відокремити резервуар і стінку кожного кадру, система CollidingRigidBodySeparationSystem відсуває танк від стіни на найменшу можливу кількість. Напрямок розділення повинен бути протилежним напрямку руху танка (коли починається ігровий розіграш, танк повинен виглядати так, ніби він ніколи не переміщувався в стіну). Але напрямок стає протилежним напрямку НОВОГО, таким чином переміщуючи танк на іншу сторону стінки, ніж це було спочатку. Чому виникає проблема: Ось як зараз обробляються повідомлення (спрощений код):

public void Update(int deltaTime)
{   
    m_messageQueue.Enqueue(new TimePassedMessage(deltaTime));
    while (m_messageQueue.Count > 0)
    {
        Message message = m_messageQueue.Dequeue();
        this.Broadcast(message);
    }
}

private void Broadcast(Message message)
{       
    if (m_messageListenersByMessageType.ContainsKey(message.GetType()))
    {
        // NOTE: all IMessageListener objects here are systems.
        List<IMessageListener> messageListeners = m_messageListenersByMessageType[message.GetType()];
        foreach (IMessageListener listener in messageListeners)
        {
            listener.ReceiveMessage(message);
        }
    }
}

Код протікає так (припустимо, це не перший ігровий кадр):

  1. Системи починають обробку TimePassedMessage
  2. InputHandingSystem перетворює натискання клавіш на дію сутності (у цьому випадку ліва стрілка перетворюється на дію MoveWest). Дія сутності зберігається в компоненті ActionExecutor
  3. SystemExecutionSystem , реагуючи на дію сутності, додає MovementDirectionChangeRequestedMessage в кінець черги повідомлень
  4. MovementSystem переміщує позицію об'єкта на основі даних компонента швидкості та додає повідомлення PositionChangedMessage до кінця черги. Рух здійснюється за допомогою напрямку руху / швидкості попереднього кадру (скажімо, на північ)
  5. Системи зупиняють обробку TimePassedMessage
  6. Системи починають обробляти MovementDirectionChangeRequestedMessage
  7. MovementSystem змінює швидкість / напрямок руху сутності за потребою
  8. Системи зупиняють обробку MovementDirectionChangeRequestedMessage
  9. Системи починають обробку PositionChangedMessage
  10. CollisionDetectionSystem виявляє, що через те, що сутність перемістилося, вона наткнулася на іншу сутність (танк зайшов всередину стіни). Він додає CollisionOcuredMessage до черги
  11. Системи припиняють обробку PositionChangedMessage
  12. Системи починають обробку CollisionOcuredMessage
  13. CollidingRigidBodySeparationSystem реагує на зіткнення поділом бака і стінки. Оскільки стінка статична, переміщується лише бак. Напрямок руху танків використовується як індикатор, звідки взявся танк. Він зміщений у зворотному напрямку

БУГ: Коли танк перемістив цей кадр, він перемістився, використовуючи напрямок руху від попереднього кадру, але коли він був відокремлений, був використаний напрямок руху від ЦЬОГО кадру, хоча він вже був іншим. Це не так, як має працювати!

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

  • Ви можете прочитати gamadu.com/artemis, щоб побачити, що вони зробили з Aspects, яка сторона вказує на деякі проблеми, які ви бачите.

Насправді я вже досить давно знайомий з Артемідою. Досліджував його вихідний код, читав форуми тощо. Але я бачив, як "Аспекти" згадуються лише в кількох місцях, і, наскільки я це розумію, вони в основному означають "Системи". Але я не можу бачити, як сторона Артеміди вирішує деякі мої проблеми. Він навіть не використовує повідомлення.

  • Дивіться також: "Спілкування між особами: Черга повідомлень проти Опублікувати / Підписатися проти сигналу / Слоти"

Я вже читав усі питання gamedev.stackexchange щодо систем сутності. Цей, схоже, не обговорює проблем, з якими я стикаюся. Я щось пропускаю?

  • Поводьтеся з двома випадками по-різному, оновлення сітки не покладається на повідомлення про рух, оскільки це частина системи зіткнення

Я не впевнений, що ти маєш на увазі. Старіші впровадження системи CollisionDetectionSystem просто перевіряли б на колізії під час оновлення (коли TimePassedMessage оброблявся), але мені довелося мінімізувати перевірки настільки, наскільки я міг завдяки продуктивності. Тому я перейшов до перевірки зіткнень, коли сутність рухається (більшість об'єктів моєї гри є статичними).


Щось мені незрозуміло. Хто модифікує вміст комірки, коли система зіткнення перетворюється на них?
Пол Манта

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

Якщо ви хочете зберегти цю складну конструкцію, тоді ви повинні слідувати @RoyT. порада, це єдиний спосіб (без складних, обґрунтованих часом обміну повідомленнями) впоратись із вашою проблемою послідовності. Ви можете прочитати gamadu.com/artemis, щоб побачити, що вони зробили з " Аспектами" , яка сторона вказує на деякі проблеми, які ви бачите.
Патрік Х'юз


2
Ви можете дізнатися, як це робив Axum , завантаживши CTP і склавши якийсь код, а потім поверніть інженеру результат на C # за допомогою ILSpy. Передача повідомлень є важливою особливістю моделей акторських моделей, і я впевнений, що Microsoft знає, що вони роблять - тому ви можете виявити, що вони мали найкращу реалізацію.
Джонатан Дікінсон

Відповіді:


12

Ви, напевно, чули про антидіапазон об'єкта God / Blob. Ну і ваша проблема - це цикл Бог / Блоб. Познайомившись із системою передачі повідомлень, у кращому випадку ви зможете забезпечити вирішення проблем, а в гіршому випадку - це марне витрачання часу. Насправді ваша проблема взагалі не має нічого спільного з розвитком ігор. Я спіймав себе, намагаючись змінити колекцію, повторюючи її кілька разів, і рішення завжди те саме: підрозділити, підрозділити, підрозділити.

Як я розумію формулювання вашого питання, ваш метод оновлення системи зіткнення в даний час виглядає в основному наступним чином.

for each possible collision
    check for collision
    handle collision
    modify collision world to reflect change // exception happens here

Написане так просто, ви можете бачити, що у вашого циклу є три обов'язки, коли він повинен мати лише одне. Щоб вирішити свою проблему, розділіть поточний цикл на три окремих циклу, що представляють три різні алгоритмічні проходи .

for each possible collision
    check for collision, record it if a collision occurs

for each found collision
    handle collision, record the collision response (delete object, ignore, etc.)

for each collision response
    modify collision world according to response

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

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


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

11

Як правильно реалізовувати обробку повідомлень у складовій системі об'єктів?

Я б сказав, що вам потрібно два типи повідомлень: Синхронні та Асинхонні. Синхронні повідомлення обробляються негайно, тоді як асинхронні обробляються не в одному кадрі стека (але можуть оброблятися в одному і тому ж ігровому кадрі). Рішення, яке зазвичай приймається на базі "за класом повідомлень", наприклад, "всі повідомлення EnemyDied є асинхронними".

Деякі події просто обробляються набагато простіше одним із цих способів. Наприклад, з мого досвіду подія ObjectGetsDeletedNow - набагато менш сексуальна, і зворотний виклик набагато складніше реалізувати, ніж ObjectWillBeDeletedAtEndOfFrame. Знову ж таки, будь-який обробник повідомлень у формі вето (код, який може скасувати або змінити певні дії під час їх виконання, як Shield-ефект модифікує DamageEvent ) в асинхронних середовищах не буде легким, але є частиною пирога в синхронні дзвінки.

Асинхронний може бути більш ефективним у деяких випадках (наприклад, ви можете пропустити деякі обробники подій, коли об’єкт буде видалено пізніше все одно). Іноді синхронність є більш ефективною, особливо коли обчислення параметра для події коштує дорого, і вам більше подобається передавати функції зворотного виклику для отримання певних параметрів замість уже обчислених значень (у випадку, якщо ніхто не зацікавлений у цьому конкретному параметрі).

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

Подумайте: у природі синхронної (негайно обробляти всі наслідки якоїсь дії) та системи повідомлень (від’єднання приймача від відправника, щоб відправник не знав, хто реагує на дії), що вам не вдасться легко зафіксуйте такі петлі. Що я говорю: будьте готові багато впоратися з подібною ітерацією, що самозмінюється. Його кіндоф "задумом". ;-)

як я можу запобігти перериванню системи зіткнення під час перевірки на зіткнення?

Для вашої конкретної проблеми з виявленням зіткнень, можливо, буде досить добре зробити події зіткнення асинхронними, тому вони ставлять у чергу до тих пір, поки диспетчер зіткнень не буде закінчений і виконаний як одна партія згодом (або в якийсь пізній момент кадру). Це ваше рішення "вхідна черга".

Проблема: якщо система A додає повідомлення до черги B системи, вона працює добре, якщо система B має бути оновлена ​​пізніше системи A (у тому ж ігровому кадрі); інакше це призводить до того, що повідомлення обробляє наступний ігровий кадр (небажано для деяких систем)

Легко:

while (! queue.empty ()) {queue.pop (). handle (); }

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


Зауважте, що я не говорив про те, як саме "коли" асинхронні повідомлення обробляються. На мій погляд, це абсолютно добре, щоб дозволити модулю виявлення зіткнення змивати свої повідомлення після його закінчення. Ви також можете подумати про це як про "синхронні повідомлення, затримані до кінця циклу", або про якийсь чудовий спосіб "просто реалізувати ітерацію таким чином, щоб вона могла бути змінена під час ітерації"
Imi

5

Якщо ви насправді намагаєтесь скористатись орієнтованими на дані характером проекту ECS, то, можливо, ви захочете подумати над самим способом DOD для цього.

Погляньте на блог BitSquid , зокрема частину про події. Представлена ​​система, яка добре поєднується з ECS. Буферизуйте всі події в хорошій чистій черзі на тип повідомлення, так само системи в ECS є компонентами. Оновлені після цього системи можуть ефективно перебирати чергу для певного типу повідомлень для їх обробки. Або просто ігнорувати їх. Що б не було.

Наприклад, система CollisionSystem буде генерувати буфер, повний подій зіткнення. Будь-яка інша система, запущена після зіткнення, може потім переглядати список та обробляти їх за потребою.

Він зберігає орієнтований на дані паралельний характер дизайну ECS без усієї складності реєстрації повідомлень тощо. Тільки системи, які насправді переймаються певним типом події, перебираються на чергу для цього типу, і роблять пряму однопрохідну ітерацію в черзі повідомлень, настільки ж ефективні, наскільки ви можете отримати.

Якщо ви постійно зберігаєте впорядковані компоненти в кожній системі (наприклад, упорядковуєте всі компоненти за ідентифікатором сутності або щось подібне), то ви навіть отримуєте приємну користь, що повідомлення будуть генеруватися в найбільш ефективному порядку для їх повторення та пошуку відповідних компонентів у система обробки. Тобто, якщо у вас є сутності 1, 2 та 3, тоді повідомлення генеруються в тому порядку і пошук компонентів, що виконуються під час обробки повідомлення, буде суворо збільшувати порядок адрес (що найшвидше).


1
+1, але я не можу повірити, що цей підхід не має недоліків. Це не змушує нас твердо кодувати взаємозалежності між системами? Чи, можливо, ці взаємозалежності мають бути так чи інакше закодовані?
Патрик Чачурський

2
@Deedalus: якщо логічній грі потрібні оновлення фізики для правильної логіки, то як ти не будеш мати цю залежність? Навіть із моделлю pubsub ви повинні явно підписатися на такий і такий тип повідомлень, який генерується лише якоюсь іншою системою. Уникнути залежностей важко, і в основному це лише з'ясування правильних шарів. Наприклад, графіка та фізика не залежать, але буде клейовий шар більш високого рівня, що забезпечує інтерпольоване оновлення фізичного моделювання, відображене на графіці тощо.
Шон Міддлічч,

Це має бути прийнятою відповіддю. Простий спосіб вирішити це - просто створити новий тип компонента, наприклад, CollisionResolvable, який буде оброблений будь-якою системою, зацікавленою у виконанні дій після зіткнення. Що б добре відповідало пропозиції Дрейка, проте існує система для кожного циклу підрозділу.
user8363
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.