Wie funktioniert die Unternehmenskommunikation?


115

Ich habe zwei Anwendungsfälle:

  1. Wie würde entity_Aich eine take-damageNachricht an senden entity_B?
  2. Wie würde die HP entity_Aabfragen entity_B?

Folgendes ist mir bisher begegnet:

  • Nachrichtenwarteschlange
    1. entity_AErstellt eine take-damageNachricht und stellt sie in entity_Bdie Nachrichtenwarteschlange.
    2. entity_AErstellt eine query-hpNachricht und sendet sie an entity_B. entity_Bim Gegenzug erstellt eine response-hpNachricht und sendet sie an entity_A.
  • Veröffentlichen / Abonnieren
    1. entity_BAbonniert take-damageNachrichten (möglicherweise mit präemptiver Filterung, sodass nur relevante Nachrichten zugestellt werden). entity_AErzeugt eine take-damageNachricht, die referenziert entity_B.
    2. entity_Aabonniert update-hpNachrichten (möglicherweise gefiltert). Jeder Frame entity_Bsendet update-hpNachrichten.
  • Signal / Slots
    1. ???
    2. entity_AVerbindet einen update-hpSteckplatz mit entity_Bdem update-hpSignal von.

Gibt es was besseres Verstehe ich richtig, wie sich diese Kommunikationsschemata in das Entity-System einer Game-Engine einfügen würden?

Antworten:


67

Gute Frage! Bevor ich zu den spezifischen Fragen komme, die Sie gestellt haben, sage ich: Unterschätzen Sie nicht die Macht der Einfachheit. Tenpn ist richtig. Denken Sie daran, dass Sie mit diesen Ansätzen nur eine elegante Möglichkeit finden, einen Funktionsaufruf aufzuschieben oder den Anrufer vom Angerufenen zu entkoppeln. Ich kann Coroutinen als einen überraschend intuitiven Weg empfehlen, um einige dieser Probleme zu lindern, aber das ist ein wenig abseits des Themas. Manchmal ist es besser, nur die Funktion aufzurufen und damit zu leben, dass Entität A direkt mit Entität B gekoppelt ist. Siehe YAGNI.

Das heißt, ich habe das Signal / Slot-Modell in Kombination mit der einfachen Nachrichtenübermittlung verwendet und war damit zufrieden. Ich habe es in C ++ und Lua für einen ziemlich erfolgreichen iPhone-Titel verwendet, der einen sehr engen Zeitplan hatte.

Wenn für den Signal- / Slot-Fall die Entität A als Reaktion auf die Aktivität der Entität B etwas tun soll (z. B. eine Tür aufschließen, wenn etwas stirbt), kann die Entität A das Todesereignis der Entität B direkt abonnieren. Oder möglicherweise abonniert die Entität A jede Entität aus einer Gruppe von Entitäten, erhöht bei jedem ausgelösten Ereignis einen Zähler und schließt die Tür auf, nachdem N von ihnen gestorben sind. Außerdem sind "Gruppe von Entitäten" und "N von ihnen" typischerweise Designer, die in den Ebenendaten definiert sind. (Nebenbei bemerkt ist dies ein Bereich, in dem Coroutinen wirklich glänzen können, z. B. WaitForMultiple ("Dying", entA, entB, entC); door.Unlock ();)

Das kann jedoch umständlich werden, wenn es um Reaktionen geht, die eng mit dem C ++ - Code verknüpft sind, oder um kurzlebige Spielereignisse: Verursachen von Schaden, Nachladen von Waffen, Debuggen und spielerbasiertes ortsbezogenes KI-Feedback. Hier kann das Weiterleiten von Nachrichten die Lücken füllen. Es läuft im Wesentlichen auf etwas hinaus: "Sagen Sie allen Einheiten in diesem Bereich, dass sie in 3 Sekunden Schaden erleiden sollen." Oder: "Wenn Sie die Physik abschließen, um herauszufinden, wen ich erschossen habe, sagen Sie ihnen, dass sie diese Skriptfunktion ausführen sollen." Es ist schwierig herauszufinden, wie dies mit Publish / Subscribe oder Signal / Slot funktioniert.

Dies kann leicht zu viel sein (im Vergleich zu Tenpns Beispiel). Es kann auch ineffizient sein, wenn Sie viel Action haben. Trotz seiner Nachteile passt dieser Ansatz von "Nachrichten und Ereignissen" sehr gut zum geskripteten Spielcode (z. B. in Lua). Der Skriptcode kann seine eigenen Nachrichten und Ereignisse definieren und darauf reagieren, ohne dass sich der C ++ - Code darum kümmert. Und der Skriptcode kann auf einfache Weise Nachrichten senden, die C ++ - Code auslösen, z. B. das Ändern von Pegeln, das Abspielen von Sounds oder sogar das Festlegen des Schadens, den die TakeDamage-Nachricht verursacht, durch eine Waffe. Es hat mir eine Menge Zeit gespart, weil ich nicht ständig mit Luabind herumalbern musste. Und so konnte ich meinen gesamten Luabind-Code an einem Ort aufbewahren, da nicht viel davon vorhanden war. Wenn richtig gekoppelt,

Außerdem ist meine Erfahrung mit Anwendungsfall Nr. 2, dass es besser ist, ihn als Ereignis in die andere Richtung zu behandeln. Anstatt nach dem Zustand der Entität zu fragen, sollten Sie ein Ereignis auslösen / eine Nachricht senden, wenn der Zustand eine signifikante Änderung vornimmt.

In Bezug auf die Schnittstellen hatte ich übrigens drei Klassen, um all dies zu implementieren: EventHost, EventClient und MessageClient. EventHosts erstellen Slots, EventClients abonnieren sie / stellen eine Verbindung zu ihnen her, und MessageClients ordnen einen Delegaten einer Nachricht zu. Beachten Sie, dass das Delegiertenziel eines MessageClient nicht unbedingt dasselbe Objekt sein muss, dem die Zuordnung gehört. Mit anderen Worten, MessageClients können nur existieren, um Nachrichten an andere Objekte weiterzuleiten. FWIW, die Host / Client-Metapher ist irgendwie unangemessen. Quelle / Senke könnten bessere Konzepte sein.

Tut mir leid, ich bin ein bisschen dort herumgewandert. Es ist meine erste Antwort :) Ich hoffe, es hat Sinn ergeben.


Danke für die Antwort. Großartige Einsichten. Der Grund, warum ich die Weitergabe der Nachricht zu Ende gestalte, liegt an Lua. Ich möchte in der Lage sein, neue Waffen ohne neuen C ++ - Code zu erstellen. Ihre Gedanken haben also einige meiner offenen Fragen beantwortet.
Deft_code

Was die Coroutinen angeht, bin auch ich ein großer Anhänger der Coroutinen, aber ich kann nie mit ihnen in C ++ spielen. Ich hatte eine vage Hoffnung, Coroutinen im Lua-Code zu verwenden, um blockierende Anrufe zu verarbeiten (z. B. Warten auf den Tod). War es die Mühe wert? Ich befürchte, dass ich von meinem intensiven Verlangen nach Coroutinen in c ++ geblendet werde.
deft_code

Was war das iPhone-Spiel? Kann ich weitere Informationen zu dem von Ihnen verwendeten Entitätssystem erhalten?
deft_code

2
Das Entitätssystem war größtenteils in C ++. So gab es zum Beispiel eine Imp-Klasse, die das Verhalten des Imps behandelte. Lua könnte Imps Parameter beim Spawn oder per Nachricht ändern. Das Ziel bei Lua war es, einen engen Zeitplan einzuhalten, und das Debuggen von Lua-Code ist sehr zeitaufwändig. Wir haben Lua verwendet, um Ebenen zu skripten (welche Entitäten wohin gehen, Ereignisse, die auftreten, wenn Sie Auslöser treffen). Also würden wir in Lua Dinge wie SpawnEnt ("Imp") sagen, bei dem Imp eine manuell registrierte Fabrikvereinigung ist. Es würde immer in einem globalen Pool von Entitäten erscheinen. Schön und einfach. Wir haben viel smart_ptr und weak_ptr verwendet.
Gewinnspiel

1
Also, BananaRaffle: Würden Sie sagen, dass dies eine genaue Zusammenfassung Ihrer Antwort ist: "Alle drei von Ihnen veröffentlichten Lösungen haben ihren Nutzen, wie auch andere. Suchen Sie nicht nach der perfekten Lösung, sondern verwenden Sie nur das, was Sie brauchen, wo es Sinn macht . "
Ipsquiggle

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

Sie haben gefragt, wie es mit kommerziellen Spielen geht. ;)


8
Eine Gegenstimme? Im Ernst, so wird es normalerweise gemacht! Entitätssysteme sind großartig, aber sie helfen nicht, die frühen Meilensteine ​​zu erreichen.
Am

Ich mache Flash-Spiele professionell und so mache ich es. Du rufst enemy.damage (10) an und suchst dann bei öffentlichen Gettern nach Informationen, die du brauchst.
Iain

7
Dies ist im Ernst, wie kommerzielle Game-Engines es tun. Er scherzt nicht. Target.NotifyTakeDamage (DamageType, DamageAmount, DamageDealer usw.) funktioniert normalerweise so.
AA Grapsas

3
"Beschädigen" kommerzielle Spiele auch durch falsche Schreibweise? :-P
Ricket

15
Ja, sie verursachen unter anderem Rechtschreibfehler. :)
LearnCocos2D

17

Eine ernstere Antwort:

Ich habe gesehen, wie Tafeln oft benutzt wurden. Einfache Versionen sind nichts anderes als Streben, die mit Elementen wie der HP einer Entität aktualisiert werden, die dann abgefragt werden können.

Ihre Tafeln können entweder die Weltsicht auf diese Entität sein (fragen Sie die Tafel von B, was ihre HP sind), oder die Weltsicht einer Entität (A fragt ihre Tafel ab, um zu sehen, welche HP das Ziel von A ist).

Wenn Sie die Blackboards nur an einem Synchronisationspunkt im Frame aktualisieren, können Sie sie zu einem späteren Zeitpunkt von jedem Thread aus lesen, was die Implementierung von Multithreading ziemlich einfach macht.

Weiterentwickelte Blackboards ähneln möglicherweise eher Hashtabellen und ordnen Zeichenfolgen Werten zu. Dies ist wartungsfreundlicher, hat aber offensichtlich Laufzeitkosten.

Eine Tafel ist traditionell nur eine Einwegkommunikation - sie würde das Austeilen von Schäden nicht bewältigen.


Ich hatte noch nie von dem Tafelmodell gehört.
deft_code

Sie eignen sich auch zum Reduzieren von Abhängigkeiten, genau wie dies bei einer Ereigniswarteschlange oder einem Publish / Subscribe-Modell der Fall ist.
Am

2
Dies ist auch die kanonische „Definition“, wie ein „ideales“ E / C / S-System „funktionieren sollte“. Die Komponenten bilden die Tafel; Die Systeme sind der Code, der darauf einwirkt. (Entitäten sind natürlich nur long long ints oder ähnlich in einem reinen ECS-System.)
BRPocock

6

Ich habe dieses Problem ein wenig untersucht und eine schöne Lösung gefunden.

Grundsätzlich dreht sich alles um Subsysteme. Es ähnelt der von tenpn erwähnten Blackboard-Idee.

Entitäten bestehen aus Komponenten, sind jedoch nur Eigentumstaschen. In Entitäten selbst ist kein Verhalten implementiert.

Angenommen, Entities haben eine Health-Komponente und eine Damage-Komponente.

Dann haben Sie einen MessageManager und drei Subsysteme: ActionSystem, DamageSystem, HealthSystem. Einmal berechnet ActionSystem die Spielwelt und generiert ein Ereignis:

HIT, source=entity_A target=entity_B power=5

Dieses Ereignis wird im MessageManager veröffentlicht. Jetzt durchläuft der MessageManager zu einem bestimmten Zeitpunkt die anstehenden Nachrichten und stellt fest, dass das DamageSystem HIT-Nachrichten abonniert hat. Jetzt liefert der MessageManager die HIT-Nachricht an das DamageSystem. Das DamageSystem durchläuft seine Liste von Entitäten, die eine Schadenskomponente haben, berechnet die Schadenspunkte abhängig von der Trefferleistung oder einem anderen Zustand beider Entitäten usw. und veröffentlicht ein Ereignis

DAMAGE, source=entity_A target=entity_B amount=7

Das HealthSystem hat die DAMAGE-Nachrichten abonniert. Wenn der MessageManager jetzt die DAMAGE-Nachricht an das HealthSystem veröffentlicht, hat das HealthSystem Zugriff auf beide Entitäten entity_A und entity_B mit ihren Health-Komponenten, sodass das HealthSystem seine Berechnungen durchführen kann (und möglicherweise das entsprechende Ereignis veröffentlichen kann zum MessageManager).

In einer solchen Game-Engine ist das Nachrichtenformat die einzige Kopplung zwischen allen Komponenten und Subsystemen. Die Subsysteme und Entitäten sind völlig unabhängig und kennen sich nicht.

Ich weiß nicht, ob eine echte Game-Engine diese Idee umgesetzt hat oder nicht, aber sie scheint ziemlich solide und sauber zu sein, und ich hoffe, dass ich sie eines Tages selbst für meine Hobby-Game-Engine umsetzen kann.


Dies ist eine viel bessere Antwort als die akzeptierte Antwort IMO. Entkoppelt, wartbar und erweiterbar (und auch keine Kopplungskatastrophe wie die Scherzantwort von entity_b->takeDamage();)
Danny Yaroslavski

4

Warum nicht eine globale Nachrichtenwarteschlange haben, so etwas wie:

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

Mit:

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

Und am Ende der Spielrunde / Eventbearbeitung:

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

Ich denke, das ist das Befehlsmuster. Und Execute()ist ein reines virtuelles In Event, welches Ableitungen definieren und Sachen machen. Also hier:

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

3

Wenn Ihr Spiel ein Einzelspieler ist, verwenden Sie einfach die Zielobjektmethode (wie von Tenpn vorgeschlagen).

Wenn Sie Multiplayer sind (oder unterstützen möchten) (um genau zu sein Multiclient), verwenden Sie eine Befehlswarteschlange.

  • Wenn A B auf Client 1 Schaden zufügt, muss das Schadensereignis in die Warteschlange gestellt werden.
  • Synchronisieren Sie die Befehlswarteschlangen über das Netzwerk
  • Behandeln Sie die Befehle in der Warteschlange auf beiden Seiten.

2
Wenn Sie es ernst meinen, dass Sie nicht betrügen, schadet A B dem Client überhaupt nicht. Der Client, der A besitzt, sendet einen "Angriff B" -Befehl an den Server, der genau das tut, was Tenpn gesagt hat. Der Server synchronisiert diesen Status dann mit allen relevanten Clients.

@Joe: Ja, wenn es einen Server gibt, den man in Betracht ziehen sollte, aber manchmal ist es in Ordnung, dem Client zu vertrauen (z. B. auf einer Konsole), um eine hohe Serverlast zu vermeiden.
Andreas

2

Ich würde sagen: Verwenden Sie keines von beiden, solange Sie keine sofortige Rückmeldung über den Schaden benötigen.

Die schadenserregende Einheit / Komponente / was auch immer sollte die Ereignisse entweder in eine lokale Ereigniswarteschlange oder in ein System auf gleicher Ebene verschieben, das Schadensereignisse enthält.

Es sollte dann ein überlagertes System mit Zugriff auf beide Entitäten geben, das die Ereignisse von Entität a anfordert und an Entität b weiterleitet. Indem Sie kein allgemeines Ereignissystem erstellen, das von jedem Ort aus verwendet werden kann, um ein Ereignis zu jedem Zeitpunkt an irgendetwas weiterzuleiten, erstellen Sie einen expliziten Datenfluss, der das Debuggen von Code, das Messen der Leistung, das Verstehen und Lesen sowie häufig vereinfacht führt zu einem allgemein besser konzipierten System.


1

Rufen Sie einfach an. Tun Sie nicht request-hp, gefolgt von query-hp - wenn Sie diesem Modell folgen, werden Sie in eine Welt voller Verletzungen geraten.

Vielleicht möchten Sie auch einen Blick auf Mono-Fortsetzungen werfen. Ich denke, es wäre ideal für NPCs.


1

Was passiert also, wenn Spieler A und B versuchen, sich im selben update () -Zyklus zu treffen? Angenommen, das Update () für Spieler A geschieht vor dem Update () für Spieler B in Zyklus 1 (oder kreuzen Sie an, oder wie auch immer Sie es nennen). Ich kann mir zwei Szenarien vorstellen:

  1. Sofortige Bearbeitung durch eine Nachricht:

    • Spieler A.Update () sieht, dass der Spieler B treffen möchte, Spieler B erhält eine Meldung, die den Schaden anzeigt.
    • Spieler B.HandleMessage () aktualisiert die Trefferpunkte für Spieler B (er stirbt)
    • Spieler B.Update () sieht, dass Spieler B tot ist. Er kann Spieler A nicht angreifen

Dies ist unfair, Spieler A und B sollten sich schlagen, Spieler B starb, bevor er A traf, nur weil diese Entität / dieses Spielobjekt später update () erhielt.

  1. Die Nachricht in die Warteschlange stellen

    • Spieler A.Update () sieht, dass der Spieler B treffen möchte, Spieler B erhält eine Meldung, die den Schaden anzeigt, und speichert ihn in einer Warteschlange
    • Der Spieler A.Update () überprüft seine Warteschlange, sie ist leer
    • Spieler B.Update () sucht zuerst nach Zügen, sodass Spieler B auch Spieler A eine Nachricht mit Schaden sendet
    • Spieler B.Update () bearbeitet auch Nachrichten in der Warteschlange und verarbeitet den Schaden von Spieler A
    • Neuer Zyklus (2): Spieler A möchte einen Heiltrank trinken, also wird Spieler A.Update () aufgerufen und der Zug ausgeführt
    • Player A.Update () überprüft die Nachrichtenwarteschlange und verarbeitet den Schaden von Player B

Auch dies ist unfair. Spieler A soll die Trefferpunkte im selben Zug / Zyklus / Tick holen!


4
Sie beantworten die Frage nicht wirklich, aber ich denke, Ihre Antwort wäre selbst eine ausgezeichnete Frage. Warum nicht einfach fragen, wie eine solche "unfaire" Priorisierung gelöst werden kann?
Mistzack

Ich bezweifle, dass sich die meisten Spiele um diese Ungerechtigkeit kümmern, da sie so häufig aktualisiert werden, dass es selten ein Problem ist. Eine einfache Problemumgehung besteht darin, beim Aktualisieren zwischen vorwärts und rückwärts durch die Entitätsliste zu wechseln.
Kylotan

Ich benutze 2 Aufrufe, also rufe ich Update () für alle Entitäten auf, dann iteriere ich nach der Schleife erneut und rufe so etwas wie auf pEntity->Flush( pMessages );. Wenn entity_A ein neues Ereignis generiert, wird es von entity_B in diesem Frame nicht gelesen (es hat die Chance, auch den Trank zu nehmen), dann erhalten beide Schaden und verarbeiten anschließend die Meldung der Trankheilung, die der letzte in der Warteschlange wäre . Spieler B stirbt trotzdem, da die Zaubertranknachricht die letzte in der Warteschlange ist: P, aber sie kann für andere Arten von Nachrichten nützlich sein, z. B. das Löschen von Zeigern auf tote Entitäten.
Pablo Ariel

Ich denke, auf der Frame-Ebene sind die meisten Spielimplementierungen einfach unfair. wie Kylotan sagte.
v.oddou

Dieses Problem ist wahnsinnig einfach zu lösen. Wenden Sie einfach den Schaden in den Nachrichtenhandlern oder was auch immer an. Sie sollten den Player innerhalb des Message-Handlers auf keinen Fall als tot markieren. In "Update ()" machen Sie einfach "if (hp <= 0) die ();" (am Anfang von "Update ()" zum Beispiel). Auf diese Weise können sich beide gleichzeitig umbringen. Außerdem: Oft schadet man dem Spieler nicht direkt, sondern durch einen Zwischengegenstand wie eine Kugel.
Tara
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.