Wie kann ich Objekte haben, die miteinander interagieren und kommunizieren, ohne eine Hierarchie zu erzwingen?

9

Ich hoffe, dass diese Streifzüge meine Frage klarstellen werden - ich würde es vollkommen verstehen, wenn sie es nicht tun. Lassen Sie mich wissen, ob dies der Fall ist, und ich werde versuchen, mich klarer zu machen.

Lernen Sie BoxPong kennen , ein sehr einfaches Spiel, das ich entwickelt habe, um mich mit der objektorientierten Spieleentwicklung vertraut zu machen. Ziehen Sie die Box, um den Ball zu kontrollieren und gelbe Gegenstände zu sammeln.
Mit BoxPong konnte ich unter anderem eine grundlegende Frage formulieren: Wie kann ich Objekte haben, die miteinander interagieren, ohne zueinander "gehören" zu müssen? Mit anderen Worten, gibt es eine Möglichkeit, dass Objekte nicht hierarchisch sind, sondern koexistieren? (Ich werde weiter unten näher darauf eingehen.)

Ich vermute, dass das Problem der Koexistenz von Objekten weit verbreitet ist, daher hoffe ich, dass es einen etablierten Weg gibt, es zu lösen. Ich möchte das Vierkantrad nicht neu erfinden, daher denke ich, dass die ideale Antwort, die ich suche, lautet: "Hier ist ein Entwurfsmuster, das üblicherweise zur Lösung Ihrer Art von Problem verwendet wird."

Insbesondere in einfachen Spielen wie BoxPong ist klar, dass es eine Handvoll Objekte gibt oder geben sollte, die auf derselben Ebene nebeneinander existieren. Es gibt eine Kiste, es gibt einen Ball, es gibt ein Sammlerstück. Alles, was ich in objektorientierten Sprachen ausdrücken kann, sind strenge HAS-A- Beziehungen. Dies geschieht über Mitgliedsvariablen. Ich kann nicht einfach anfangen ballund es sein Ding machen lassen, ich brauche es, um dauerhaft zu einem anderen Objekt zu gehören . Ich habe es so eingerichtet, dass das Hauptspiel Objekt hat eine Box, und die Box wiederum hat einen Ball und hat einen Score - Zähler. Jedes Objekt hat auch eineupdate()Methode, die Position, Richtung usw. berechnet, und ich gehe dort einen ähnlichen Weg: Ich rufe die Aktualisierungsmethode des Hauptspielobjekts auf, die die Aktualisierungsmethoden aller seiner untergeordneten Elemente aufruft, und sie rufen wiederum die Aktualisierungsmethoden aller ihrer untergeordneten Elemente auf . Dies ist der einzige Weg, den ich sehen kann, um ein objektorientiertes Spiel zu machen, aber ich denke, es ist nicht der ideale Weg. Schließlich würde ich den Ball nicht genau als zur Box gehörend betrachten, sondern als auf der gleichen Ebene und mit ihm interagierend. Ich nehme an, dass dies erreicht werden kann, indem alle Spielobjekte in Mitgliedsvariablen des Hauptspielobjekts umgewandelt werden, aber ich sehe keine Lösung dafür. Ich meine ... abgesehen von der offensichtlichen Unordnung, wie würde es für den Ball und die Box eine Möglichkeit geben, sich zu kennen , das heißt zu interagieren?

Es gibt auch das Problem, dass Objekte Informationen untereinander weitergeben müssen. Ich habe ziemlich viel Erfahrung mit dem Schreiben von Code für das SNES, bei dem Sie praktisch jederzeit auf praktisch den gesamten RAM zugreifen können. Angenommen, Sie machen einen benutzerdefinierten Feind für Super Mario World und möchten, dass alle Münzen von Mario entfernt werden. Speichern Sie dann einfach Null, um $ 0DBF zu adressieren, kein Problem. Es gibt keine Einschränkungen, die besagen, dass Feinde nicht auf den Status des Spielers zugreifen können. Ich glaube, ich bin von dieser Freiheit verwöhnt worden, weil ich mich bei C ++ und dergleichen oft frage, wie ich einen Wert für andere Objekte (oder sogar global) zugänglich machen kann.
Was wäre, wenn ich am Beispiel von BoxPong wollte, dass der Ball von den Rändern des Bildschirms abprallt? widthund heightsind Eigenschaften der GameKlasse,ballZugang zu ihnen haben. Ich könnte diese Art von Werten weitergeben (entweder über Konstruktoren oder die Methoden, wo sie benötigt werden), aber das schreit mir nur nach schlechter Praxis.

Ich denke, mein Hauptproblem ist, dass ich Objekte brauche, um mich zu kennen, aber der einzige Weg, den ich sehen kann, ist eine strenge Hierarchie, die hässlich und unpraktisch ist.

Ich habe von "Freundschaftsklassen" in C ++ gehört und weiß irgendwie, wie sie funktionieren, aber wenn sie die Endlösung sind, warum sehe ich dann nicht, dass friendSchlüsselwörter über jedes einzelne C ++ - Projekt verteilt werden, und wie kommt es, dass Konzept existiert nicht in jeder OOP-Sprache? (Gleiches gilt für Funktionszeiger, von denen ich erst kürzlich erfahren habe.)

Vielen Dank im Voraus für Antworten jeglicher Art - und wenn es einen Teil gibt, der für Sie keinen Sinn ergibt, lassen Sie es mich wissen.

vvye
quelle
2
Ein Großteil der Spielebranche hat sich der Entity-Component-System-Architektur und ihren Variationen zugewandt. Es ist eine andere Denkweise als herkömmliche OO-Ansätze, aber es funktioniert gut und macht Sinn, sobald sich das Konzept durchsetzt. Unity verwendet es. Tatsächlich verwendet Unity nur den Entity-Component-Teil, basiert jedoch auf ECS.
Dunk
Das Problem, dass Klassen ohne gegenseitige Kenntnis miteinander zusammenarbeiten können, wird durch das Mediator-Entwurfsmuster gelöst. Hast du es dir angesehen?
Fuhrmanator

Antworten:

13

Im Allgemeinen stellt sich heraus, dass Objekte derselben Ebene voneinander wissen. Sobald Objekte voneinander wissen, werden sie gebunden oder miteinander gekoppelt . Dies macht es schwierig, sie zu ändern, zu testen und zu warten.

Es funktioniert viel besser, wenn es ein Objekt "oben" gibt, das über die beiden Bescheid weiß und die Interaktionen zwischen ihnen festlegen kann. Das Objekt, das die beiden Peers kennt, kann sie durch Abhängigkeitsinjektion oder über Ereignisse oder durch Nachrichtenübermittlung (oder einen anderen Entkopplungsmechanismus) miteinander verbinden. Ja, das führt zu einer künstlichen Hierarchie, aber es ist weitaus besser als das Spaghetti-Chaos, das man bekommt, wenn die Dinge nur wohl oder übel interagieren. Dies ist in C ++ nur wichtiger, da Sie auch für die Lebensdauer der Objekte etwas benötigen.

Kurz gesagt, Sie können dies tun, indem Sie Objekte überall nebeneinander durch Ad-hoc-Zugriff miteinander verbinden, aber es ist eine schlechte Idee. Die Hierarchie sorgt für Ordnung und klare Eigenverantwortung. Das Wichtigste ist, dass Objekte im Code nicht unbedingt Objekte im wirklichen Leben (oder sogar im Spiel) sind. Wenn die Objekte im Spiel keine gute Hierarchie bilden, ist eine andere Abstraktion möglicherweise besser.

Telastyn
quelle
2

Was wäre, wenn ich am Beispiel von BoxPong wollte, dass der Ball von den Rändern des Bildschirms abprallt? Breite und Höhe sind Eigenschaften der Spielklasse, und ich würde den Ball brauchen, um Zugang zu ihnen zu haben.

Nein!

Ich denke, das Hauptproblem, das Sie haben, ist, dass Sie "Objektorientierte Programmierung" etwas zu wörtlich nehmen. In OOP stellt ein Objekt kein "Ding" dar, sondern eine "Idee", dh "Ball", "Spiel", "Physik", "Mathematik", "Datum" usw. Alle sind gültige Objekte. Es ist auch nicht erforderlich, dass Objekte über irgendetwas "Bescheid wissen". Date.Now().getTommorrow()Fragen Sie beispielsweise den Computer, welcher Tag heute ist, wenden Sie arkane Datumsregeln an, um das Datum von morgen zu ermitteln, und geben Sie dies an den Anrufer zurück. Das DateObjekt weiß nichts anderes, es muss nur Informationen nach Bedarf vom System anfordern. Außerdem Math.SquareRoot(number)muss nichts außer der Logik zur Berechnung einer Quadratwurzel bekannt sein.

In Ihrem Beispiel, das ich zitiert habe, sollte der "Ball" nichts über die "Box" wissen. Boxen und Bälle sind völlig unterschiedliche Ideen und haben kein Recht miteinander zu reden. Aber eine Physik-Engine weiß, was eine Box und ein Ball sind (oder zumindest ThreeDShape), und sie weiß, wo sie sich befinden und was mit ihnen geschehen soll. Wenn der Ball also schrumpft, weil es kalt ist, würde die Physik-Engine dieser Ballinstanz mitteilen, dass er jetzt kleiner ist.

Es ist ein bisschen wie ein Auto zu bauen. Ein Computerchip weiß nichts über einen Automotor, aber ein Auto kann einen Computerchip verwenden, um einen Motor zu steuern. Die einfache Idee, kleine, einfache Dinge zusammen zu verwenden, um ein etwas größeres, komplexeres Ding zu schaffen, das selbst als Bestandteil anderer komplexerer Teile wiederverwendbar ist.

Und was ist in Ihrem Mario-Beispiel, wenn Sie sich in einem Herausforderungsraum befinden, in dem das Berühren eines Feindes keine Marios-Münzen verbraucht, sondern ihn nur aus diesem Raum auswirft? Außerhalb des Ideenraums von Mario oder dem Feind sollte Mario Münzen verlieren, wenn er einen Feind berührt (wenn Mario einen Unverwundbarkeitsstern hat, tötet er stattdessen den Feind). Welches Objekt (Domäne / Idee), das für das verantwortlich ist, was passiert, wenn Mario einen Feind berührt, ist das einzige, das über eines von beiden Bescheid wissen muss und was auch immer es mit einem von beiden tun sollte (sofern dieses Objekt externe Änderungen zulässt) ).

Auch bei Ihren Aussagen zu Objekten, die Kinder aufrufen Update(), ist dies äußerst fehleranfällig, als würde Updatees mehrmals pro Frame von verschiedenen Eltern aufgerufen? (Selbst wenn Sie dies bemerken, ist dies verschwendete CPU-Zeit, die Ihr Spiel verlangsamen kann.) Jeder sollte nur berühren, was er braucht, wenn er muss. Wenn Sie Update () verwenden, sollten Sie eine Art Abonnementmuster verwenden, um sicherzustellen, dass alle Updates einmal pro Frame aufgerufen werden (wenn dies für Sie nicht wie in Unity behandelt wird).

Das Erlernen der Definition Ihrer Domain-Ideen in klare, isolierte, gut definierte und benutzerfreundliche Blöcke ist der wichtigste Faktor dafür, wie gut Sie OOP nutzen können.

Tezra
quelle
1

Lernen Sie BoxPong kennen, ein sehr einfaches Spiel, das ich entwickelt habe, um mich mit der objektorientierten Spieleentwicklung vertraut zu machen.

Mit BoxPong konnte ich unter anderem eine grundlegende Frage formulieren: Wie kann ich Objekte haben, die miteinander interagieren, ohne zueinander "gehören" zu müssen?

Ich habe ziemlich viel Erfahrung mit dem Schreiben von Code für das SNES, bei dem Sie praktisch jederzeit auf praktisch den gesamten RAM zugreifen können. Angenommen, Sie machen einen benutzerdefinierten Feind für Super Mario World und möchten, dass alle Münzen von Mario entfernt werden. Speichern Sie dann einfach Null, um $ 0DBF zu adressieren, kein Problem.

Sie scheinen den Punkt der objektorientierten Programmierung zu verfehlen.

Bei der objektorientierten Programmierung geht es darum, Abhängigkeiten zu verwalten, indem bestimmte Schlüsselabhängigkeiten in Ihrer Architektur selektiv invertiert werden, um Starrheit, Fragilität und Nichtwiederverwendbarkeit zu vermeiden.

Was ist Abhängigkeit? Abhängigkeit ist das Vertrauen auf etwas anderes. Wenn Sie Null speichern, um $ 0DBF zu adressieren, verlassen Sie sich auf die Tatsache, dass sich in dieser Adresse Marios Münzen befinden und die Münzen als Ganzzahl dargestellt werden. Ihr Custom Enemy-Code hängt vom Code ab, der Mario und seine Münzen implementiert. Wenn Sie ändern, wo Mario seine Münzen speichert, müssen Sie den gesamten Code, der auf den Speicherort verweist, manuell aktualisieren.

Bei objektorientiertem Code geht es darum, Ihren Code von Abstraktionen und nicht von Details abhängig zu machen. Also statt

class Mario
{
    public:
        int coins;
}

du würdest schreiben

class Mario
{
    public:
        void LoseCoins();

    private:
        int coins;
}

Wenn Sie nun ändern möchten, wie Mario seine Münzen von einem int in einen long oder einen double speichert, oder sie im Netzwerk speichern oder in einer Datenbank speichern oder einen anderen langen Prozess starten möchten, nehmen Sie die Änderung an einem Ort vor: dem Mario-Klasse und all Ihr anderer Code funktioniert ohne Änderungen.

Deshalb, wenn Sie fragen

Wie kann ich Objekte haben, die miteinander interagieren, ohne zueinander "gehören" zu müssen?

Sie fragen wirklich:

Wie kann ich Code haben, der ohne Abstraktionen direkt voneinander abhängt?

Das ist keine objektorientierte Programmierung.

Ich schlage vor, Sie lesen zunächst alles hier: http://objectmentor.com/omSolutions/oops_what.html und suchen dann auf YouTube nach allem von Robert Martin und sehen sich alles an.

Meine Antworten stammen von ihm, und einige davon werden direkt von ihm zitiert.

Mark Murfin
quelle
Vielen Dank für die Antwort (und die Seite, auf die Sie verlinkt haben; sieht interessant aus). Ich weiß eigentlich über Abstraktion und Wiederverwendbarkeit Bescheid, aber ich glaube, ich habe das in meiner Antwort nicht sehr gut ausgedrückt. Anhand des von Ihnen angegebenen Beispielcodes kann ich meinen Standpunkt jetzt besser veranschaulichen! Sie sagen im Grunde, dass das feindliche Objekt nicht tun sollte mario.coins = 0;, aber mario.loseCoins();was in Ordnung und wahr ist - aber mein Punkt ist, wie kann der Feind überhaupt Zugriff auf das marioObjekt haben? marioeine Mitgliedsvariable von zu sein enemy, scheint mir nicht richtig zu sein.
Vvye
Nun, die einfache Antwort ist, Mario als Argument für eine Funktion in Enemy zu übergeben. Möglicherweise haben Sie eine Funktion wie marioNearby () oder attackMario (), die einen Mario als Argument verwendet. Wenn also die Logik hinter der Interaktion eines Feindes und eines Mario ausgelöst wird, rufen Sie feind.marioNearby (mario) auf, das mario.loseCoins () aufruft. Später können Sie entscheiden, dass es eine Klasse von Feinden gibt, die dazu führen, dass Mario nur eine Münze verliert oder sogar Münzen gewinnt. Sie haben jetzt einen Ort, an dem Sie diese Änderung vornehmen können, ohne dass Änderungen an anderen Codes vorgenommen werden.
Mark Murfin
Indem Sie Mario an einen Feind weitergeben, haben Sie ihn gerade gekoppelt. Mario und Enemy sollten nicht wissen, dass der andere überhaupt eine Sache ist. Aus diesem Grund erstellen wir Objekte höherer Ordnung, um zu steuern, wie die einfachen Objekte miteinander gekoppelt werden.
Tezra
@Tezra Aber sind diese Objekte höherer Ordnung überhaupt nicht wiederverwendbar? Es fühlt sich so an, als ob diese Objekte als Funktionen fungieren, sie existieren nur als die Prozedur, die sie zeigen.
Steve Chamaillard
@SteveChamaillard Jedes Programm verfügt über mindestens eine bestimmte Logik, die in keinem anderen Programm sinnvoll ist. Die Idee ist jedoch, diese Logik auf einige Klassen höherer Ordnung zu beschränken. Wenn Sie eine Mario-, Feind- und Levelklasse haben, können Sie Mario und Feind in anderen Spielen wiederverwenden. Wenn Sie Feind und Mario direkt miteinander verbinden, muss jedes Spiel, das eines benötigt, auch das andere einbeziehen.
Tezra
0

Sie können die lose Kopplung aktivieren, indem Sie das Mediatormuster anwenden. Für die Implementierung müssen Sie einen Verweis auf eine Mediatorkomponente haben, die alle empfangenden Komponenten kennt.

Es erkennt den Gedanken "Spielleiter, bitte lass dies und das geschehen".

Ein verallgemeinertes Muster ist das Publish-Subscribe-Muster. Es ist angemessen, wenn der Mediator nicht viel Logik enthalten sollte. Verwenden Sie andernfalls einen handgefertigten Mediator, der weiß, wohin alle Anrufe weitergeleitet und möglicherweise sogar geändert werden müssen.

Es gibt synchrone und asynchrone Varianten, die normalerweise als Ereignisbus, Nachrichtenbus oder Nachrichtenwarteschlange bezeichnet werden. Suchen Sie nach ihnen, um festzustellen, ob sie für Ihren speziellen Fall geeignet sind.

Held wandert
quelle