Ich arbeite an einem 2D-Spiel, in dem Sie sich nach oben, unten, links und rechts bewegen können. Ich habe im Wesentlichen zwei Spiellogikobjekte:
- Spieler: Hat eine Position relativ zur Welt
- Welt: Zeichnet die Karte und den Spieler
Bisher hängt die Welt vom Spieler ab (dh sie hat einen Verweis darauf), und ihre Position muss ermittelt werden, wo der Spielercharakter gezeichnet werden soll und welcher Teil der Karte gezeichnet werden soll.
Jetzt möchte ich die Kollisionserkennung hinzufügen, damit sich der Spieler nicht durch Wände bewegen kann.
Der einfachste Weg, den ich mir vorstellen kann, ist, den Spieler die Welt fragen zu lassen, ob die beabsichtigte Bewegung möglich ist. Aber das würde eine zirkuläre Abhängigkeit zwischen Spieler und Welt hervorrufen (dh jeder hat einen Bezug zum anderen), die es wert ist, vermieden zu werden. Der einzige Weg, den ich mir ausgedacht habe, ist, die Welt den Spieler bewegen zu lassen , aber ich finde das etwas unintuitiv.
Was ist meine beste Option? Oder lohnt es sich nicht, eine zirkuläre Abhängigkeit zu vermeiden?
Antworten:
Die Welt sollte sich nicht selbst zeichnen. Der Renderer sollte die Welt zeichnen. Der Spieler sollte sich nicht selbst zeichnen. Der Renderer sollte den Player relativ zur Welt zeichnen.
Der Spieler sollte die Welt nach der Kollisionserkennung fragen. oder vielleicht sollten Kollisionen von einer separaten Klasse behandelt werden, die die Kollisionserkennung nicht nur gegen die statische Welt, sondern auch gegen andere Akteure prüft.
Ich denke, die Welt sollte sich des Spielers wahrscheinlich überhaupt nicht bewusst sein. Es sollte ein einfaches Primitiv sein, kein Gott-Objekt. Der Player muss wahrscheinlich einige World-Methoden aufrufen, möglicherweise indirekt (Kollisionserkennung oder Überprüfung auf interaktive Objekte usw.).
quelle
Renderer
Art von notwendig, aber das bedeutet nicht, dass die Logik für das Rendern derRenderer
einzelnen Elemente von der verarbeitet wird. Jedes zu zeichnende Element sollte wahrscheinlich von einer gemeinsamen Schnittstelle wie zIDrawable
oderIRenderable
(oder eine entsprechende Benutzeroberfläche in der von Ihnen verwendeten Sprache). Die Welt könnte die seinRenderer
, nehme ich an, aber das scheint, als würde es seine Verantwortung überschreiten, besonders wenn es bereits eineIRenderable
Selbst war.So geht eine typische Rendering-Engine mit diesen Dingen um:
Es gibt einen grundsätzlichen Unterschied zwischen dem Ort, an dem sich ein Objekt im Raum befindet, und dem Ort, an dem das Objekt gezeichnet wird.
Ein Objekt zeichnen
In der Regel verfügen Sie über eine Renderer- Klasse, die dies ausführt . Es nimmt einfach ein Objekt (Modell) und zeichnet auf dem Bildschirm. Es kann Methoden wie drawSprite (Sprite), drawLine (..), drawModel (Model) enthalten, was auch immer Sie benötigen. Es ist ein Renderer, also soll er all diese Dinge tun. Es wird auch jede API verwendet, die Sie darunter haben, sodass Sie beispielsweise einen Renderer haben können, der OpenGL verwendet, und einen, der DirectX verwendet. Wenn Sie Ihr Spiel auf eine andere Plattform portieren möchten, schreiben Sie einfach einen neuen Renderer und verwenden diesen. So einfach ist das.
Ein Objekt verschieben
Jedes Objekt ist an etwas gebunden, das wir gerne als SceneNode bezeichnen . Sie erreichen dies durch Komposition. Ein SceneNode enthält ein Objekt. Das ist es. Was ist ein SceneNode? Es ist eine einfache Klasse, die alle Transformationen (Position, Drehung, Skalierung) eines Objekts (normalerweise relativ zu einem anderen SceneNode) zusammen mit dem tatsächlichen Objekt enthält.
Objekte verwalten
Wie werden SceneNodes verwaltet? Durch einen SceneManager . Diese Klasse erstellt und verfolgt jeden SceneNode in Ihrer Szene. Sie können ihn nach einem bestimmten SceneNode (normalerweise durch einen String-Namen wie "Player" oder "Table" gekennzeichnet) oder einer Liste aller Knoten fragen.
Die Welt zeichnen
Dies sollte mittlerweile ziemlich offensichtlich sein. Gehen Sie einfach durch jeden SceneNode in der Szene und lassen Sie ihn vom Renderer an der richtigen Stelle zeichnen. Sie können es an der richtigen Stelle zeichnen, indem Sie den Renderer die Transformationen eines Objekts speichern lassen, bevor Sie es rendern.
Kollisionserkennung
Das ist nicht immer trivial. Normalerweise können Sie in der Szene abfragen, welches Objekt sich an einem bestimmten Punkt im Raum befindet oder welche Objekte ein Strahl schneidet. Auf diese Weise können Sie einen Strahl von Ihrem Player in Bewegungsrichtung erstellen und den Szenenmanager fragen, welches Objekt der erste Strahl schneidet. Sie können dann den Spieler an die neue Position bewegen, ihn um einen kleineren Betrag bewegen (um ihn neben das kollidierende Objekt zu bringen) oder ihn überhaupt nicht bewegen. Stellen Sie sicher, dass diese Abfragen von separaten Klassen verarbeitet werden. Sie sollten den SceneManager nach einer Liste von SceneNodes fragen, aber es ist eine andere Aufgabe, zu bestimmen, ob dieser SceneNode einen Punkt im Raum abdeckt oder sich mit einem Strahl schneidet. Beachten Sie, dass der SceneManager nur Knoten erstellt und speichert.
Also, was ist der Spieler und was ist die Welt?
Der Player kann eine Klasse sein, die einen SceneNode enthält, der wiederum das zu rendernde Modell enthält. Sie bewegen den Player, indem Sie die Position des Szenenknotens ändern. Die Welt ist einfach eine Instanz des SceneManager. Es enthält alle Objekte (über SceneNodes). Sie behandeln die Kollisionserkennung, indem Sie den aktuellen Status der Szene abfragen.
Dies ist keineswegs eine vollständige oder genaue Beschreibung dessen, was in den meisten Motoren vor sich geht, sondern soll Ihnen helfen, die Grundlagen zu verstehen und warum es wichtig ist, die von SOLID unterstrichenen OOP-Prinzipien zu respektieren . Geben Sie sich nicht der Idee hin, dass es zu schwierig ist, Ihren Code zu restrukturieren, oder dass es Ihnen nicht wirklich hilft. Sie werden in Zukunft viel mehr gewinnen, wenn Sie Ihren Code sorgfältig entwerfen.
quelle
Warum solltest du das vermeiden wollen? Zirkuläre Abhängigkeiten sollten vermieden werden, wenn Sie eine wiederverwendbare Klasse erstellen möchten. Aber der Player ist keine Klasse, die überhaupt wiederverwendbar sein muss. Möchten Sie den Player jemals ohne Welt verwenden? Wahrscheinlich nicht.
Denken Sie daran, dass Klassen nichts anderes als Funktionssammlungen sind. Die Frage ist nur, wie man die Funktionalität teilt. Tun Sie, was immer Sie tun müssen. Wenn Sie eine zirkuläre Dekadenz brauchen, dann sei es so. (Gleiches gilt übrigens für alle OOP-Funktionen. Codieren Sie die Dinge so, dass sie einem Zweck dienen, und folgen Sie den Paradigmen nicht einfach blind.)
Bearbeiten
Okay, um die Frage zu beantworten: Sie können vermeiden, dass der Player die Welt für Kollisionsprüfungen kennen muss, indem Sie Rückrufe verwenden:
Die Art der Physik, die Sie in der Frage beschrieben haben, kann von der Welt gehandhabt werden, wenn Sie die Geschwindigkeit der Entitäten exponieren:
Beachten Sie jedoch, dass Sie wahrscheinlich früher oder später eine Abhängigkeit von der Welt benötigen, dh wann immer Sie die Funktionalität der Welt benötigen: Sie möchten wissen, wo sich der nächste Feind befindet? Sie möchten wissen, wie weit der nächste Sims entfernt ist? Abhängigkeit ist es.
quelle
render(World)
. Die Debatte dreht sich darum, ob der gesamte Code innerhalb einer Klasse vollgestopft werden soll oder ob Code in logische und funktionale Einheiten unterteilt werden soll, die dann einfacher zu warten, zu erweitern und zu verwalten sind. Übrigens, viel Glück beim Wiederverwenden dieser Komponentenmanager, Physik-Engines und Input-Manager, die alle geschickt undifferenziert und vollständig gekoppelt sind.Ihr aktuelles Design scheint gegen das erste Prinzip des SOLID-Designs zu verstoßen .
Dieses erste Prinzip, das als "Prinzip der einmaligen Verantwortung" bezeichnet wird, ist im Allgemeinen eine gute Richtlinie, um keine monolithischen, alles tuenden Objekte zu erstellen, die Ihrem Design immer schaden.
Zum Konkretisieren ist Ihr
World
Objekt sowohl dafür verantwortlich, den Spielstatus zu aktualisieren und beizubehalten als auch alles zu zeichnen.Was ist, wenn sich Ihr Rendering-Code ändert / ändern muss? Warum sollten Sie beide Klassen aktualisieren müssen, die eigentlich nichts mit Rendern zu tun haben? Wie Liosan bereits gesagt hat, solltest du eine haben
Renderer
.Nun, um Ihre eigentliche Frage zu beantworten ...
Es gibt viele Möglichkeiten, dies zu tun, und dies ist nur eine Möglichkeit, sich zu entkoppeln:
Object
s, in der sich der Player befindet, dies hängt jedoch nicht von der Player-Klasse ab (verwenden Sie die Vererbung, um dies zu erreichen).InputManager
.Renderer
zeichnet alle Objekte.quelle
health
sie nur diese Instanz vonPlayer
hat).Der Spieler sollte die Welt nach Dingen wie Kollisionserkennung fragen. Der Weg, um die zirkuläre Abhängigkeit zu vermeiden, besteht darin, dass die Welt keine Abhängigkeit vom Spieler hat. Die Welt muss wissen, wo sie selbst zeichnet: Möglicherweise möchten Sie, dass das Objekt weiter entfernt ist, möglicherweise mit einem Verweis auf ein Kameraobjekt, das wiederum einen Verweis auf eine zu verfolgende Entität enthalten kann.
Was Sie in Bezug auf Zirkelverweise vermeiden möchten, ist nicht so sehr, dass Sie sich gegenseitig verweisen, sondern dass Sie sich im Code explizit aufeinander beziehen.
quelle
Wann immer zwei verschiedene Arten von Objekten sich gegenseitig fragen können. Sie sind voneinander abhängig, da sie einen Verweis auf den anderen enthalten müssen, um dessen Methoden aufzurufen.
Sie können eine zirkuläre Abhängigkeit vermeiden, indem Sie die Welt den Spieler fragen lassen, aber der Spieler kann die Welt nicht fragen oder umgekehrt. Auf diese Weise hat die Welt Referenzen zu den Spielern, aber die Spieler brauchen keine Referenzen zu der Welt. Oder umgekehrt. Aber das wird das Problem nicht lösen, denn die Welt müsste die Spieler fragen, ob sie etwas zu fragen haben, und sie beim nächsten Anruf informieren ...
Sie können dieses "Problem" also nicht wirklich umgehen, und ich glaube, das ist kein Grund zur Sorge. Halte das Design so einfach wie möglich.
quelle
Wenn Sie die Details zu Spieler und Welt streichen, möchten Sie in einem einfachen Fall keine zirkuläre Abhängigkeit zwischen zwei Objekten einführen (was je nach Ihrer Sprache möglicherweise nicht einmal von Bedeutung ist, siehe den Link im Kommentar von Fuhrmanator). Es gibt mindestens zwei sehr einfache strukturelle Lösungen, die für dieses und ähnliche Probleme gelten würden:
1) Fügen Sie das Singleton- Muster in Ihre Weltklasse ein . Auf diese Weise kann der Spieler (und jedes andere Objekt) das Weltobjekt leicht finden, ohne dass teure Suchvorgänge oder permanente Verknüpfungen erforderlich sind. Der Kern dieses Musters besteht lediglich darin, dass die Klasse einen statischen Verweis auf die einzige Instanz dieser Klasse hat, die bei der Instanziierung des Objekts festgelegt und beim Löschen gelöscht wird.
Abhängig von Ihrer Entwicklungssprache und der gewünschten Komplexität können Sie diese problemlos als Oberklasse oder Schnittstelle implementieren und für viele Hauptklassen wiederverwenden, von denen Sie nicht erwarten, dass sie mehr als eine in Ihrem Projekt haben.
2) Wenn die Sprache, in der Sie entwickeln, dies unterstützt (viele unterstützen dies), verwenden Sie eine schwache Referenz . Dies ist eine Referenz, die sich nicht auf Dinge wie die Speicherbereinigung auswirkt. In genau diesen Fällen ist es hilfreich, nur darauf zu achten, dass keine Annahmen darüber getroffen werden, ob das Objekt, auf das Sie schwach verweisen, noch vorhanden ist.
In Ihrem speziellen Fall könnten Ihre Spieler einen schwachen Bezug zur Welt haben. Der Vorteil davon (wie beim Singleton) ist, dass Sie nicht in jedem Frame nach dem Weltobjekt suchen müssen oder über einen permanenten Verweis verfügen müssen, der Prozesse behindert, die von zirkulären Verweisen wie Garbage Collection betroffen sind.
quelle
Wie die anderen gesagt haben, denke ich, dass Sie
World
eine Sache zu viel tun: Es wird versucht, sowohl das Spiel zu enthaltenMap
(das eine eigenständige Einheit sein sollte) als auch gleichzeitig einRenderer
zu sein.Erstellen Sie also ein neues Objekt (das
GameMap
möglicherweise aufgerufen wird) und speichern Sie die Daten auf Kartenebene darin. Schreiben Sie Funktionen, die mit der aktuellen Karte interagieren.Dann brauchen Sie auch ein
Renderer
Objekt. Sie könnten dieses machenRenderer
Objekt das Ding , das beide enthältGameMap
undPlayer
(sowieEnemies
) und ziehen sie auch.quelle
Sie können zirkuläre Abhängigkeiten vermeiden, indem Sie die Variablen nicht als Member hinzufügen. Verwenden Sie eine statische CurrentWorld () - Funktion für den Player oder ähnliches. Erfinden Sie keine andere Schnittstelle als die, die bereits in World implementiert ist. Dies ist jedoch völlig unnötig.
Es ist auch möglich, die Referenz vor / während der Zerstörung des Player-Objekts zu zerstören, um die durch Zirkelverweise verursachten Probleme effektiv zu stoppen.
quelle