Ich finde es schwierig, Spielobjekte so zu organisieren, dass sie polymorph, aber nicht polymorph sind.
Hier ist ein Beispiel: Angenommen, wir wollen alle unsere Objekte zu update()
und draw()
. Dazu müssen wir eine Basisklasse definieren GameObject
, die diese beiden virtuellen reinen Methoden hat und den Polymorphismus einsetzt:
class World {
private:
std::vector<GameObject*> objects;
public:
// ...
update() {
for (auto& o : objects) o->update();
for (auto& o : objects) o->draw(window);
}
};
Die Aktualisierungsmethode muss sich um den Status kümmern, den das jeweilige Klassenobjekt für die Aktualisierung benötigt. Tatsache ist, dass jedes Objekt über die Welt um es herum Bescheid wissen muss. Beispielsweise:
- Eine Mine muss wissen, ob jemand damit kollidiert
- Ein Soldat sollte wissen, ob der Soldat eines anderen Teams in der Nähe ist
- Ein Zombie sollte wissen, wo sich das nächste Gehirn innerhalb eines Radius befindet
Für passive Interaktionen (wie die erste) habe ich gedacht, dass die Kollisionserkennung in bestimmten Fällen von Kollisionen das Objekt selbst mit einem delegieren kann on_collide(GameObject*)
.
Die meisten anderen Informationen (wie auch die beiden anderen Beispiele) konnten nur von der Spielwelt abgefragt werden, die an die update
Methode übergeben wurde. Jetzt unterscheidet die Welt Objekte nicht mehr nach ihrem Typ (sie speichert alle Objekte in einem einzigen polymorphen Container). Was also mit einem Ideal zurückkommt, world.entities_in(center, radius)
ist ein Container von GameObject*
. Aber natürlich will der Soldat keine anderen Soldaten aus seinem Team angreifen und ein Zombie macht sich keine Gedanken über andere Zombies. Wir müssen also das Verhalten unterscheiden. Eine Lösung könnte sein:
void TeamASoldier::update(const World& world) {
auto list = world.entities_in(position, eye_sight);
for (const auto& e : list)
if (auto enemy = dynamic_cast<TeamBSoldier*>(e))
// shoot towards enemy
}
void Zombie::update(const World& world) {
auto list = world.entities_in(position, eye_sight);
for (const auto& e : list)
if (auto enemy = dynamic_cast<Human*>(e))
// go and eat brain
}
aber natürlich kann die anzahl dynamic_cast<>
pro bild furchtbar hoch sein, und wir alle wissen, wie langsam es sein dynamic_cast
kann. Dasselbe Problem betrifft auch den on_collide(GameObject*)
zuvor besprochenen Delegierten.
Was ist der ideale Weg, um den Code so zu organisieren, dass Objekte andere Objekte erkennen und sie ignorieren oder Aktionen basierend auf ihrem Typ ausführen können?
quelle
dynamic_cast<Human*>
Implementieren Sie beispielsweise, anstatt vom Ergebnis von abhängig zu sein , so etwas wie abool GameObject::IsHuman()
, dasfalse
standardmäßig zurückgegeben wird, abertrue
in derHuman
Klasse überschrieben wird, um zurückzukehren .IsA
Overrides erwies sich in der Praxis für mich als nur unwesentlich besser als dynamisches Casting. Das Beste, was Sie tun können, ist, dass der Benutzer nach Möglichkeit Datenlisten sortiert, anstatt blind über den gesamten Entitätspool zu iterieren.TeamASoldier
undTeamBSoldier
ist wirklich identisch - auf jeden im anderen Team geschossen. Alles, was es von anderen Entitäten benötigt, ist eineGetTeam()
Methode in ihrer spezifischsten Form, die sich nach dem Beispiel von congusbongus noch weiter in eineIsEnemyOf(this)
Art Schnittstelle abstrahieren lässt . Der Code muss sich nicht um taxonomische Klassifikationen von Soldaten, Zombies, Spielern usw. kümmern. Konzentrieren Sie sich auf Interaktion, nicht auf Typen.Antworten:
Anstatt die Entscheidungsfindung jeder Entität für sich zu implementieren, können Sie alternativ das Controller-Pattern wählen. Sie würden zentrale Steuerungsklassen haben, die alle Objekte (die für sie von Bedeutung sind) kennen und deren Verhalten steuern.
Ein MovementController übernimmt die Bewegung aller Objekte, die sich bewegen können (Routensuche durchführen, Positionen anhand aktueller Bewegungsvektoren aktualisieren).
Ein MineBehaviorController überprüft alle Minen und alle Soldaten und befiehlt einer Mine, zu explodieren, wenn ein Soldat zu nahe kommt.
Ein ZombieBehaviorController überprüft alle Zombies und die Soldaten in ihrer Nähe, wählt das beste Ziel für jeden Zombie aus und befiehlt ihm, sich dorthin zu bewegen und ihn anzugreifen (der Zug selbst wird vom MovementController ausgeführt).
Ein SoldierBehaviorController würde die gesamte Situation analysieren und dann taktische Anweisungen für alle Soldaten ausarbeiten (Sie ziehen dorthin, Sie schießen darauf, Sie heilen diesen Kerl ...). Die eigentliche Ausführung dieser übergeordneten Befehle würde auch von untergeordneten Steuerungen übernommen. Wenn Sie sich anstrengen, können Sie die KI in die Lage versetzen, recht kluge kooperative Entscheidungen zu treffen.
quelle
std::map
s gruppiert und Entitäten sind nur IDs. Dann müssen wir eine Art Typsystem erstellen (möglicherweise mit einer Tag-Komponente, da der Renderer wissen muss, was zu zeichnen ist). Und wenn wir das nicht wollen, brauchen wir eine Zeichnungskomponente. Die Positionskomponente muss jedoch wissen, wo sie gezeichnet werden soll. Daher erstellen wir Abhängigkeiten zwischen Komponenten, die wir mit einem superkomplexen Messagingsystem lösen. Schlagen Sie das vor?Versuchen Sie zunächst, Features so zu implementieren, dass Objekte möglichst unabhängig voneinander bleiben. Dies möchten Sie vor allem für Multi-Threading tun. In Ihrem ersten Codebeispiel könnte die Menge aller Objekte in Mengen unterteilt werden, die der Anzahl der CPU-Kerne entsprechen, und sehr effizient aktualisiert werden.
Wie Sie bereits sagten, ist für einige Funktionen eine Interaktion mit anderen Objekten erforderlich. Das bedeutet, dass der Zustand aller Objekte an einigen Punkten synchronisiert werden muss. Mit anderen Worten, Ihre Anwendung muss warten, bis alle parallelen Aufgaben abgeschlossen sind, und dann Berechnungen anwenden, die eine Interaktion beinhalten. Es empfiehlt sich, die Anzahl dieser Synchronisationspunkte zu verringern, da dies immer bedeutet, dass einige Threads warten müssen, bis andere beendet sind.
Daher empfehle ich, die Informationen zu den Objekten, die in anderen Objekten benötigt werden, zu puffern. Mit einem solchen globalen Puffer können Sie alle Ihre Objekte unabhängig voneinander aktualisieren, jedoch nur abhängig von sich selbst und dem globalen Puffer, der sowohl schneller als auch einfacher zu warten ist. Aktualisieren Sie zu einem festgelegten Zeitpunkt, beispielsweise nach jedem Frame, den Puffer mit dem aktuellen Objektstatus.
Einmal pro Frame müssen Sie also 1. den aktuellen Objektstatus global puffern, 2. alle Objekte basierend auf sich selbst und dem Puffer aktualisieren, 3. Ihre Objekte zeichnen und dann den Puffer erneut laden.
quelle
Verwenden Sie ein komponentenbasiertes System, in dem Sie ein Barebones-GameObject haben, das mindestens eine Komponente enthält, die deren Verhalten definiert.
Angenommen, ein Objekt soll sich die ganze Zeit nach links und rechts bewegen (eine Plattform), dann könnten Sie eine solche Komponente erstellen und an ein GameObject anhängen.
Angenommen, ein Spielobjekt soll sich die ganze Zeit langsam drehen. Sie könnten eine separate Komponente erstellen, die genau das tut, und sie an das GameObject anhängen.
Was wäre, wenn Sie eine sich bewegende Plattform haben möchten, die sich ebenfalls dreht, in einer traditionellen Klassen-Hierarchie, die ohne das Duplizieren von Code nur schwer zu realisieren ist.
Das Schöne an diesem System ist, dass Sie anstelle einer drehbaren oder einer MovingPlatform-Klasse beide Komponenten an das GameObject anhängen und jetzt eine MovingPlatform haben, die sich automatisch dreht.
Alle Komponenten haben die Eigenschaft 'requireUpdate', die zwar wahr ist, das GameObject jedoch die 'update'-Methode für diese Komponente aufruft. Angenommen, Sie haben eine Draggable-Komponente. Diese Komponente kann bei gedrückter Maustaste (wenn sie sich über dem GameObject befand) "requireUpdate" auf "true" und bei gedrückter Maustaste auf "false" setzen. Es darf nur der Maus folgen, wenn die Maus gedrückt ist.
Einer der Tony Hawk Pro Skater-Entwickler hat das Defacto, um es zu schreiben, und es lohnt sich zu lesen: http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/
quelle
Bevorzugen Sie die Komposition gegenüber der Vererbung.
Abgesehen davon wäre mein stärkster Rat: Lassen Sie sich nicht in die Denkweise von "Ich möchte, dass dies äußerst flexibel ist" hineinziehen. Flexibilität ist großartig, aber denken Sie daran, dass es in einem endlichen System wie einem Spiel auf einer bestimmten Ebene atomare Teile gibt, die zum Aufbau des Ganzen verwendet werden. So oder so hängt Ihre Verarbeitung von diesen vordefinierten atomaren Typen ab. Mit anderen Worten, das Catering für "alle" Datentypen (wenn das möglich wäre) würde Ihnen auf lange Sicht nicht helfen, wenn Sie keinen Code zur Verarbeitung haben. Grundsätzlich muss jeder Code Daten analysieren / verarbeiten, die auf bekannten Spezifikationen basieren ... was eine vordefinierte Menge von Typen bedeutet. Wie groß ist das Set? Wie du willst.
Dieser Beitrag bietet über eine robuste und performante Entity-Component-Architektur einen Einblick in das Prinzip von Composition over Inheritance bei der Spieleentwicklung.
Indem Sie Entitäten aus (unterschiedlichen) Teilmengen einer Obermenge vordefinierter Komponenten aufbauen, bieten Sie Ihren AIs konkrete, schrittweise Möglichkeiten, die Welt und die Akteure in ihrer Umgebung zu verstehen, indem Sie die Zustände der Komponenten dieser Akteure lesen.
quelle
Persönlich empfehle ich, die Zeichenfunktion außerhalb der Object-Klasse selbst zu halten. Ich empfehle sogar, die Position / Koordinaten des Objekts außerhalb des Objekts selbst zu halten.
Diese draw () -Methode wird sich mit der Low-Level-Rendering-API von OpenGL, OpenGL ES, Direct3D, Ihrem Wrapping-Layer auf diesen APIs oder einer Engine-API befassen. Es kann sein, dass Sie zwischen diesen Einstellungen wechseln müssen (wenn Sie beispielsweise OpenGL + OpenGL ES + Direct3D unterstützen möchten).
Dieses GameObject sollte nur die grundlegenden Informationen über das Erscheinungsbild enthalten, z. B. ein Mesh oder ein größeres Bundle, einschließlich Shader-Eingaben, Animationsstatus usw.
Außerdem möchten Sie eine flexible Grafik-Pipeline. Was passiert, wenn Sie Objekte anhand ihres Abstands zur Kamera bestellen möchten? Oder ihre Materialart. Was passiert, wenn Sie ein 'ausgewähltes' Objekt in einer anderen Farbe zeichnen möchten? Was ist, wenn Sie beim Aufrufen einer Zeichenfunktion für ein Objekt nicht das Rendern durchführen, sondern es in eine Befehlsliste mit Aktionen einfügen, die das Rendern ausführen soll (möglicherweise für das Threading erforderlich)? Sie können so etwas mit dem anderen System machen, aber es ist eine PITA.
Ich empfehle, anstatt direkt zu zeichnen, alle gewünschten Objekte an eine andere Datenstruktur zu binden. Diese Bindung muss nur wirklich einen Verweis auf die Position des Objekts und die Rendering-Informationen enthalten.
Ihre Ebenen / Blöcke / Bereiche / Karten / Hubs / ganze Welt / was auch immer einen räumlichen Index erhalten, dieser enthält die Objekte und gibt sie basierend auf Koordinatenabfragen zurück und könnte eine einfache Liste oder so etwas wie ein Octree sein. Es könnte auch ein Wrapper für etwas sein, das von einer Physik-Engine eines Drittanbieters als Physikszene implementiert wird. Es ermöglicht Ihnen, Dinge wie "Alle Objekte abfragen, die sich in der Ansicht der Kamera befinden, mit einem zusätzlichen Bereich um sie herum" oder für einfachere Spiele, bei denen Sie einfach alles rendern können, greifen Sie auf die gesamte Liste zu.
Raumindizes müssen nicht die tatsächlichen Positionsinformationen enthalten. Sie speichern Objekte in Baumstrukturen in Bezug auf die Position anderer Objekte. Sie können jedoch als eine Art verlustbehafteter Cache angesehen werden, der ein schnelles Nachschlagen eines Objekts anhand seiner Position ermöglicht. Es besteht keine wirkliche Notwendigkeit, Ihre tatsächlichen X-, Y- und Z-Koordinaten zu duplizieren. Davon abgesehen könntest du, wenn du behalten wolltest
Tatsächlich müssen Ihre Spielobjekte nicht einmal ihre eigenen Standortinformationen enthalten. Zum Beispiel sollte ein Objekt, das nicht in eine Ebene gelegt wurde, keine x-, y-, z-Koordinaten haben, was keinen Sinn ergibt. Sie können das in dem speziellen Index enthalten. Wenn Sie die Koordinaten des Objekts basierend auf seiner tatsächlichen Referenz nachschlagen müssen, müssen Sie eine Bindung zwischen dem Objekt und dem Szenendiagramm herstellen (Szenendiagramme dienen zum Zurückgeben von Objekten basierend auf Koordinaten, sind jedoch beim Zurückgeben von Koordinaten basierend auf Objekten langsam). .
Wenn Sie einem Level ein Objekt hinzufügen. Es wird folgendes tun:
1) Legen Sie eine Lokationsstruktur an:
Dies kann auch ein Verweis auf ein Objekt in einer Physik-Engine eines Drittanbieters sein. Oder es könnte eine Versatzkoordinate mit einem Verweis auf einen anderen Ort sein (für eine Verfolgungskamera oder ein angefügtes Objekt oder ein Beispiel). Beim Polymorphismus kann dies davon abhängen, ob es sich um ein statisches oder ein dynamisches Objekt handelt. Wenn Sie hier beim Aktualisieren der Koordinaten einen Verweis auf den Raumindex behalten, kann dies auch der Raumindex sein.
Wenn Sie sich Sorgen über die dynamische Speicherzuweisung machen, verwenden Sie einen Speicherpool.
2) Eine Bindung / Verknüpfung zwischen Ihrem Objekt, seiner Position und dem Szenengraphen.
3) Die Bindung wird an der entsprechenden Stelle zum räumlichen Index innerhalb der Ebene hinzugefügt.
Wenn Sie sich auf das Rendern vorbereiten.
1) Holen Sie sich die Kamera (Es wird nur ein weiteres Objekt sein, außer dass der Ort den Charakter des Spielers verfolgt und Ihr Renderer einen speziellen Verweis darauf hat, das ist alles, was er wirklich benötigt).
2) Holen Sie sich die SpacialBinding der Kamera.
3) Ermitteln Sie den Raumindex aus der Bindung.
4) Fragen Sie die Objekte ab, die (möglicherweise) für die Kamera sichtbar sind.
5A) Sie müssen die visuellen Informationen verarbeiten lassen. Auf die GPU hochgeladene Texturen und so weiter. Dies geschieht am besten im Voraus (z. B. beim Laden auf einer Ebene), kann jedoch möglicherweise zur Laufzeit erfolgen (in einer offenen Welt können Sie Dinge laden, wenn Sie sich einem Block nähern, dies sollte jedoch noch im Voraus erfolgen).
5B) Erstellen Sie optional einen zwischengespeicherten Render-Baum, wenn Sie Objekte in der Nähe sortieren oder nachverfolgen möchten, die möglicherweise zu einem späteren Zeitpunkt sichtbar sind. Andernfalls können Sie den räumlichen Index jedes Mal abfragen, wenn dies von Ihren Spiel- / Leistungsanforderungen abhängt.
Ihr Renderer benötigt wahrscheinlich ein RenderBinding-Objekt, das die Koordinaten zwischen dem Objekt und den Koordinaten verknüpft
Wenn Sie dann rendern, führen Sie einfach die Liste aus.
Ich habe oben Referenzen verwendet, aber sie können intelligente Zeiger, unformatierte Zeiger, Objekthandles usw. sein.
BEARBEITEN:
Was die gegenseitige Bewusstwerdung betrifft. Das ist Kollisionserkennung. Es würde wahrscheinlich im Octree implementiert werden. Sie müssten in Ihrem Hauptobjekt einen Rückruf bereitstellen. Dieses Zeug wird am besten von einer richtigen Physik-Engine wie Bullet gehandhabt. In diesem Fall ersetzen Sie Octree einfach durch PhysicsScene und Position durch einen Link zu CollisionMesh.getPosition ().
quelle