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.
Sie haben gefragt, wie es mit kommerziellen Spielen geht. ;)
quelle
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.
quelle
long long int
s oder ähnlich in einem reinen ECS-System.)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:
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
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.
quelle
entity_b->takeDamage();
)Warum nicht eine globale Nachrichtenwarteschlange haben, so etwas wie:
Mit:
Und am Ende der Spielrunde / Eventbearbeitung:
Ich denke, das ist das Befehlsmuster. Und
Execute()
ist ein reines virtuelles InEvent
, welches Ableitungen definieren und Sachen machen. Also hier:quelle
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.
quelle
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.
quelle
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.
quelle
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:
Sofortige Bearbeitung durch eine Nachricht:
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.
Die Nachricht in die Warteschlange stellen
Auch dies ist unfair. Spieler A soll die Trefferpunkte im selben Zug / Zyklus / Tick holen!
quelle
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.