Як працює комунікація між особами?


115

У мене є два випадки користувача:

  1. Як entity_Aнадіслати take-damageповідомлення entity_B?
  2. Як би entity_Aзапитувати entity_BHP?

Ось з чим я стикався до цього часу:

  • Черга повідомлень
    1. entity_Aстворює take-damageповідомлення та розміщує його у entity_Bчерзі повідомлень.
    2. entity_Aстворює query-hpповідомлення і публікує його entity_B. entity_Bвзамін створює response-hpповідомлення та розміщує його entity_A.
  • Опублікувати / Підписатися
    1. entity_Bпідписується на take-damageповідомлення (можливо, з попередньою фільтрацією, тому доставляється лише відповідне повідомлення). entity_Aвиробляє take-damageповідомлення, яке посилається entity_B.
    2. entity_Aпідписується на update-hpповідомлення (можливо, відфільтровано). Кожен кадр entity_Bтранслює update-hpповідомлення.
  • Сигнал / Слоти
    1. ???
    2. entity_Aз'єднує update-hpслот для entity_Bросійського update-hpсигналу.

Чи є щось краще? Чи правильно я розумію, як ці схеми комунікації вписуються в сутність системи ігрового двигуна?

Відповіді:


67

Гарне питання! Перш ніж перейти до конкретних питань, які ви задали, я скажу: не варто недооцінювати силу простоти. Тенпн прав. Майте на увазі, що все, що ви намагаєтеся зробити з цими підходами, - це знайти елегантний спосіб відкласти функціональний виклик або від'єднати абонента від виклику. Я можу порекомендувати судові процедури як напрочуд інтуїтивно зрозумілий спосіб полегшити деякі з цих проблем, але це трохи поза темою. Іноді вам краще просто викликати функцію і жити з тим, що сутність A з'єднана безпосередньо з сутністю B. Див. YAGNI.

З цього приводу я використовував і задоволений моделлю сигнал / слот у поєднанні з простою передачею повідомлення. Я використовував його в C ++ та Lua для досить вдалого заголовку iPhone, який мав дуже щільний графік.

Що стосується випадку сигналу / слота, якщо я хочу, щоб об'єкт A зробив щось у відповідь на те, що зробив об'єкт B (наприклад, розблокувати двері, коли щось помирає), я можу мати суб'єкт А підписатися безпосередньо на смерть суб'єкта В. Або, можливо, суб'єкт А підписався на кожну з груп сутностей, збільшивши лічильник на кожну розстріляну подію та відчинивши двері після того, як N з них загинуло. Крім того, "група сутностей" і "N з них", як правило, є дизайнером, визначеним у даних рівня. (Вбік, це одна область, де дійсно можуть світитися супроводи, наприклад, WaitForMultiple ("Помирає", entA, entB, entC); door.Unlock ();)

Але це може стати громіздким, коли мова йде про реакції, які щільно поєднані з кодом C ++, або по суті ефемерними ігровими подіями: нанесення шкоди, перезавантаження зброї, налагодження, ігровий AI-зворотний зв'язок на основі локації. Тут передача повідомлень може заповнити прогалини. По суті, це зводиться до чогось типу, "скажіть усім суб'єктам в цій області отримати пошкодження за 3 секунди" або "щоразу, коли ви завершите фізику, щоб з'ясувати, хто я стріляв, скажіть їм виконувати цю функцію сценарію". Важко зрозуміти, як це зробити красиво, використовуючи публікацію / підписку або сигнал / слот.

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

Крім того, мій досвід використання справи №2 полягає в тому, що вам краще поводитися з ним як з подією в іншому напрямку. Замість того, щоб запитувати про стан здоров’я суб'єкта господарювання, скасовуйте подію / надсилайте повідомлення щоразу, коли стан здоров'я змінює суттєві зміни.

Що стосується інтерфейсів, то btw, я закінчив три класи, щоб реалізувати все це: EventHost, EventClient та MessageClient. EventHosts створює слоти, EventClients підписуються / підключаються до них, а MessageClients асоціюють делегата з повідомленням. Зауважте, що ціль делегата MessageClient не обов'язково повинен бути тим самим об'єктом, який належить асоціації. Іншими словами, MessageClients можуть існувати виключно для пересилання повідомлень на інші об'єкти. FWIW, метафора хост / клієнт начебто недоречна. Джерело / раковина можуть бути кращими поняттями.

Вибачте, я якось туди проскочив. Це моя перша відповідь :) Сподіваюся, це мало сенс.


Дякую за відповідь. Чудові уявлення. Причина, над якою я розробляю повідомлення, пов'язане з Луєю. Мені б хотілося створити нову зброю без нового коду С ++. Тож ваші думки відповідали на деякі мої не задані питання.
deft_code

Що стосується супротивів, то я теж чудово вірю в супроводи, але мені ніколи не доводиться грати з ними в C ++. У мене була неясна надія використовувати супроводи в коді lua для обробки блокувальних дзвінків (наприклад, чекати смерті). Чи варто було докласти зусиль? Я боюся, що мене можуть осліпнути моє інтенсивне бажання виконувати функції c ++.
deft_code

Нарешті, якою була гра iphone? Чи можу я отримати більш детальну інформацію про систему, яку ви використовували?
deft_code

2
Система сутностей була переважно в C ++. Так, наприклад, був клас Imp, який керував поведінкою Imp. Lua може змінити параметри Imp на нересті або через повідомлення. Мета з Lua полягала в тісному розкладі, а налагодження коду Lua дуже трудомістке. Ми використовували Lua для рівня скриптів (які об'єкти куди йдуть, події, що трапляються при натисканні на тригери). Тож у Луї ми могли б сказати такі речі, як SpawnEnt ("Imp"), де Imp - це фабрична асоціація, зареєстрована вручну. Вона завжди породжувала б один глобальний пул сутностей. Приємно і просто. Ми використовували багато smart_ptr та слабкі_птр.
BRaffle

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

76
// in entity_a's code:
entity_b->takeDamage();

Ви запитали, як це роблять комерційні ігри. ;)


8
Проголосування "за"? Серйозно, ось як це нормально робиться! Системи сутності є чудовими, але вони не допомагають досягти ранніх етапів.
tenpn

Я роблю Flash ігри професійно, і це я роблю. Ви зателефонуєте на ворога.Зшкодження (10), а потім шукаєте будь-яку інформацію, яка вам потрібна, від громадських одержувачів.
Iain

7
Це серйозно, як це роблять комерційні ігрові двигуни. Він не жартує. Target.NotifyTakeDamage (DamageType, DamageAmount, DamageDealer тощо), як правило, відбувається.
АА Грапсас

3
Чи комерційні ігри неправильно написали "шкоду"? :-P
Ricket

15
Так, вони, крім іншого, завдають шкоди помилкам. :)
LearnCocos2D

17

Більш серйозна відповідь:

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

Ваші дошки можуть бути або світовим уявленням про цю сутність (запитайте на дошці Б, що таке HP), або поглядом світу на світ (A запитує свою дошку, щоб побачити, на що спрямована мета HP).

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

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

Дошка традиційно є лише односторонньою комунікацією - вона не справляється з пошкодженням шкоди.


Я ніколи раніше не чув про модель дошки.
deft_code

Вони також хороші для зменшення залежностей, так само, як це робить чергу черги подій або публікація / підписка.
tenpn

2
Це також канонічне "визначення" того, як повинна працювати "ідеальна" система E / C / S ". Компоненти складають дошку; Системи - це код, який діє на нього. (Суб'єкти, звичайно, просто long long ints або подібні, в чистій системі ECS.)
BRPocock

6

Я трохи вивчив це питання і побачив гарне рішення.

В основному це все стосується підсистем. Це схоже на ідею на дошці, яку згадує tenpn.

Суб'єкти складаються з компонентів, але вони є лише майновими сумками. Жодна поведінка не реалізується в самих суб'єктах.

Скажімо, у суб'єктів господарювання є компонент Health та компонент Damage.

Тоді у вас є кілька MessageManager і три підсистеми: ActionSystem, DamageSystem, HealthSystem. В один момент ActionSystem проводить свої обчислення в ігровому світі та генерує подію:

HIT, source=entity_A target=entity_B power=5

Ця подія публікується в Менеджері повідомлень. Зараз в один момент часу MessageManager проходить через очікувані повідомлення і виявляє, що DamageSystem підписалася на повідомлення HIT. Тепер MessageManager доставляє повідомлення HIT до DamageSystem. DamageSystem проходить перелік сутностей, у яких є компонент "Пошкодження", обчислює точки пошкодження залежно від потужності удару або якогось іншого стану обох об'єктів тощо і публікує подію

DAMAGE, source=entity_A target=entity_B amount=7

HealthSystem підписався на повідомлення DAMAGE, і тепер, коли MessageManager публікує повідомлення DAMAGE в HealthSystem, система HealthSystem має доступ до обох суб'єктів ent_A і entitet_B зі своїми компонентами Health, тому знову HealthSystem може робити свої розрахунки (і, можливо, публікувати відповідну подію до Менеджера повідомлень).

У такому ігровому механізмі формат повідомлень є єдиним з'єднанням між усіма компонентами та підсистемами. Підсистеми та утворення абсолютно незалежні і не знають одне одного.

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


Це набагато краща відповідь, ніж прийнята відповідь ІМО. Розв’язаний, ремонтопридатний та розтяжний (і теж не стихійне лихо, як жартівлива відповідь entity_b->takeDamage();)
Данні Ярославський

4

Чому б не мати глобальної черги повідомлень, наприклад:

messageQueue.push_back(shared_ptr<Event>(new DamageEvent(entityB, 10, entityA)));

З:

DamageEvent(Entity* toDamage, uint amount, Entity* damageDealer);

І наприкінці гри цикл / обробка події:

while(!messageQueue.empty())
{
    Event e = messageQueue.front();
    messageQueue.pop_front();
    e.Execute();
}

Я думаю, що це командна схема. І Execute()це чистий віртуальний в Event, де похідні визначають і роблять речі. Тож ось:

DamageEvent::Execute() 
{
    toDamage->takeDamage(amount); // Or of course, you could now have entityA get points, or a recognition of damage, or anything.
}

3

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

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

  • Коли A завдає шкоди B на клієнті 1, просто викладіть в чергу пошкодження.
  • Синхронізуйте черги команд через мережу
  • Обробляйте команди в черзі з обох сторін.

2
Якщо ви серйозно ставитеся до того, щоб уникнути обману, A зовсім не загрожує B клієнту. Клієнт, що володіє A, відправляє на сервер команду "атака B", яка виконує саме те, що сказав tenpn; сервер синхронізує цей стан із усіма відповідними клієнтами.

@Joe: Так, якщо є сервер, який є вагомим пунктом для розгляду, але іноді нормально довіряти клієнту (наприклад, на консолі), щоб уникнути великого навантаження на сервер.
Андреас

2

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

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

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


1

Просто зателефонуйте. Не робіть запит на hp, наступні за запитом-hp - якщо ви будете слідувати цій моделі, ви будете в світі.

Можливо, ви також хочете ознайомитися і з Mono Continuations. Я думаю, що це було б ідеально для NPC.


1

Що ж станеться, якщо у нас є гравці A і B, які намагаються влучити один одного в одному і тому ж циклі оновлення ()? Припустимо, що оновлення () для гравця A відбувається, перш ніж оновлення () для гравця B у циклі 1 (або позначте галочку, або як ви її називали). Я можу придумати два сценарії:

  1. Негайна обробка через повідомлення:

    • Гравець A.Update () бачить, що гравець хоче вдарити B, гравець B отримує повідомлення про пошкодження.
    • Гравець B.HandleMessage () оновлює точки вбивства гравця B (він помирає)
    • Гравець B.Update () бачить, що гравець B мертвий .. він не може атакувати гравця A

Це несправедливо, гравець A і B повинен ударити по іншому, гравець B помер, перш ніж вдарити A лише тому, що цей суб'єкт / ігровий об'єкт отримав оновлення () пізніше.

  1. Черга повідомлення

    • Гравець A.Update () бачить, що гравець хоче вдарити B, гравець B отримує повідомлення про пошкодження і зберігає його в черзі
    • Гравець A.Update () перевіряє свою чергу, вона порожня
    • Гравець B.Update () спочатку перевіряє ходи, щоб гравець B надіслав повідомлення гравцеві A з пошкодженням
    • плеєр B.Update () також обробляє повідомлення в черзі, обробляє збиток від гравця A
    • Новий цикл (2): Гравець A хоче випити зілля для здоров’я, щоб програвач Player A.Update () був викликаний і рухався процес
    • Гравець A.Update () перевіряє чергу повідомлень і обробляє пошкодження від гравця B

Знову це несправедливо. Гравець A повинен приймати точки влучання в той же оборот / цикл / галочку!


4
Ти насправді не відповідаєш на питання, але я думаю, що твоя відповідь сама по собі поставила б чудове запитання. Чому б не піти вперед і не запитати, як вирішити таку «несправедливу» пріоритетність?
bummzack

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

Я використовую 2 виклики, тому я закликаю Update () до всіх об'єктів, потім після циклу повторюю повтор і називаю щось на зразок pEntity->Flush( pMessages );. Коли entit_A генерує нову подію, вона не читається entit_B у цьому кадрі (у неї є шанс взяти зілля також), тоді обидва отримують шкоду і після цього обробляють повідомлення про загоєння зілля, яке було б останнім у черзі . Гравець B все одно вмирає, оскільки повідомлення про зілля останнє у черзі: P, але воно може бути корисним для інших типів повідомлень, таких як очищення покажчиків до мертвих осіб.
Пабло Аріель

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

Цю проблему шалено легко вирішити. Просто нанесіть пошкодження один одному в обробниках повідомлень або будь-якому іншому. Ви, безумовно, не повинні позначати програвача як мертвого всередині обробника повідомлень. У "Update ()" ви просто зробите "if (hp <= 0) die ();" (наприклад, на початку "Оновлення ()"). Таким чином обидва можуть вбивати один одного одночасно. Також: Часто ви не пошкоджуєте гравця безпосередньо, а через якийсь проміжний предмет, наприклад куля.
Тара
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.