Trennung von Zeichnung und Logik in Spielen

11

Ich bin ein Entwickler, der gerade erst anfängt, mit der Spieleentwicklung herumzuspielen. Ich bin ein .Net-Typ, also habe ich mich mit XNA angelegt und spiele jetzt mit Cocos2d für das iPhone herum. Meine Frage ist jedoch allgemeiner.

Angenommen, ich baue ein einfaches Pong-Spiel. Ich hätte eine BallKlasse und eine PaddleKlasse. Aus der Geschäftswelt stammend, besteht mein erster Instinkt darin, in keiner dieser Klassen Zeichnungs- oder Eingabebehandlungscode zu haben.

//pseudo code
class Ball
{
   Vector2D position;
   Vector2D velocity;
   Color color;
   void Move(){}
}

Nichts in der Ballklasse verarbeitet Eingaben oder befasst sich mit Zeichnen. Ich hätte dann eine andere Klasse, meine GameKlasse oder meine Scene.m(in Cocos2d), die den Ball neu aufbaut, und während der Spielschleife würde sie den Ball nach Bedarf manipulieren.

Die Sache ist jedoch, dass ich in vielen Tutorials für XNA und Cocos2d ein Muster wie das folgende sehe:

//pseudo code
class Ball : SomeUpdatableComponent
{
   Vector2D position;
   Vector2D velocity;
   Color color;
   void Update(){}
   void Draw(){}
   void HandleInput(){}
}

Meine Frage ist, ist das richtig? Ist dies das Muster, das die Leute in der Spieleentwicklung verwenden? Es widerspricht irgendwie allem, was ich gewohnt bin, dass meine Ballklasse alles macht. BallWie würde ich in diesem zweiten Beispiel, in dem ich weiß, wie ich mich bewegen soll, mit der Kollisionserkennung mit dem umgehen Paddle? Wäre die BallNotwendigkeit, Kenntnisse über die zu haben Paddle? In meinem ersten Beispiel Gamewürde die Klasse sowohl auf die Ballals auch auf die verweisen Paddleund diese dann an einen CollisionDetectionManager oder etwas anderes senden. Aber wie gehe ich mit der Komplexität verschiedener Komponenten um, wenn jede einzelne Komponente alles für sich erledigt? (Ich hoffe ich mache Sinn .....)

BKostenlos
quelle
2
Leider ist das in vielen Spielen so. Ich würde das nicht so verstehen, als wäre es die beste Vorgehensweise. Viele der Tutorials, die Sie lesen, richten sich möglicherweise an Anfänger. Daher werden sie dem Leser keine Modell- / Ansichts- / Controller- oder Jochmuster erklären.
Tenpn
@tenpn, Ihr Kommentar scheint darauf hinzudeuten, dass Sie dies nicht für eine gute Praxis halten. Ich habe mich gefragt, ob Sie dies weiter erklären könnten. Ich habe immer gedacht, dass es die am besten geeignete Anordnung ist, da diese Methoden Code enthalten, der wahrscheinlich für jeden Typ sehr spezifisch ist. Aber ich habe selbst wenig Erfahrung, daher würde ich mich freuen, Ihre Gedanken zu hören.
18.
1
@sebf Es funktioniert nicht, wenn Sie sich für datenorientiertes Design interessieren, und es funktioniert nicht, wenn Sie objektorientiertes Design mögen. Siehe Onkel Bobs SOLID-Prinzipien: butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
Tenpn
@tenpn. Dieser Artikel und das darin verlinkte Dokument sind sehr interessant, danke!
21.

Antworten:

10

Meine Frage ist, ist das richtig? Ist dies das Muster, das die Leute in der Spieleentwicklung verwenden?

Spieleentwickler neigen dazu, alles zu verwenden, was funktioniert. Es ist nicht streng, aber hey, es versendet.

Es widerspricht irgendwie allem, was ich gewohnt bin, dass meine Ballklasse alles macht.

Eine allgemeinere Redewendung für das, was Sie dort haben, ist handleMessages / update / draw. In Systemen, in denen Nachrichten häufig verwendet werden (was erwartungsgemäß Vor- und Nachteile hat), erfasst eine Spieleinheit alle Nachrichten, die sie interessiert, führt eine Logik für diese Nachrichten aus und zeichnet sich dann selbst.

Beachten Sie, dass dieser Aufruf von draw () möglicherweise nicht bedeutet, dass die Entität putPixel oder was auch immer in sich selbst aufruft. In einigen Fällen kann dies nur bedeuten, dass Daten aktualisiert werden, die später von dem für das Zeichnen verantwortlichen Code abgefragt werden. In fortgeschritteneren Fällen "zeichnet" es sich selbst, indem es Methoden aufruft, die von einem Renderer verfügbar gemacht werden. In der Technologie, an der ich gerade arbeite, würde sich das Objekt mithilfe von Renderer-Aufrufen selbst zeichnen, und dann organisiert jeder Frame, den der Rendering-Thread erstellt, alle Aufrufe aller Objekte, optimiert für Dinge wie Statusänderungen und zeichnet dann das gesamte Objekt (alle) heraus Dies geschieht in einem Thread, der von der Spielelogik getrennt ist, sodass wir asynchrones Rendern erhalten.

Die Übertragung der Verantwortung liegt beim Programmierer. Für ein einfaches Pong-Spiel ist es sinnvoll, dass jedes Objekt seine eigene Zeichnung (komplett mit Aufrufen auf niedriger Ebene) vollständig verarbeitet. Es ist eine Frage von Stil und Weisheit.

Wie würde ich in diesem zweiten Beispiel, in dem mein Ball sich bewegen kann, mit der Kollisionserkennung mit dem Paddel umgehen? Müsste der Ball Kenntnisse über das Paddel haben?

Eine natürliche Möglichkeit, das Problem hier zu lösen, besteht darin, dass jedes Objekt jedes andere Objekt als eine Art Parameter für die Überprüfung von Kollisionen verwendet. Dies skaliert schlecht und kann seltsame Ergebnisse haben.

Lassen Sie jede Klasse eine Art kollidierbare Schnittstelle implementieren und dann ein Codebit auf hoher Ebene haben, das in der Aktualisierungsphase Ihres Spiels nur alle kollidierbaren Objekte durchläuft und Kollisionen behandelt und die Objektpositionen nach Bedarf festlegt.

Auch hier müssen Sie nur ein Gefühl dafür entwickeln (oder sich entscheiden), wie die Verantwortung für verschiedene Dinge in Ihrem Spiel delegiert werden muss. Das Rendern ist eine Einzelaktivität und kann von einem Objekt im Vakuum ausgeführt werden. Kollision und Physik können im Allgemeinen nicht.

In meinem ersten Beispiel würde die Spielklasse Verweise sowohl auf den Ball als auch auf das Paddel enthalten und diese dann an einen CollisionDetection-Manager oder etwas anderes senden. Aber wie gehe ich mit der Komplexität verschiedener Komponenten um, wenn jede einzelne Komponente dies tut? alles selbst?

Siehe oben für eine Untersuchung davon. Wenn Sie in einer Sprache arbeiten, die Schnittstellen problemlos unterstützt (z. B. Java, C #), ist dies einfach. Wenn Sie in C ++ sind, kann dies ... interessant sein. Ich bin ein Gefallen daran, Messaging zu verwenden, um diese Probleme zu lösen (Klassen behandeln Nachrichten, die sie können, andere ignorieren), einige andere Leute mögen die Komposition von Komponenten.

Auch hier funktioniert alles, was für Sie am einfachsten ist.

ChrisE
quelle
2
Oh, und denken Sie immer daran: YAGNI (Sie werden es nicht brauchen) ist hier wirklich wichtig. Sie können viel Zeit damit verschwenden, das ideale flexible System zu finden, um jedes mögliche Problem zu lösen, und dann nie etwas erledigen. Ich meine, wenn Sie wirklich ein gutes Multiplayer-Backbone für alle möglichen Ergebnisse wollen, würden Sie CORBA verwenden, oder? :)
ChrisE
5

Ich habe kürzlich ein einfaches Space Invadors-Spiel mit einem 'Entity-System' erstellt. Es ist ein Muster, das Attribute und Verhaltensweisen sehr gut voneinander trennt. Ich habe einige Iterationen gebraucht, um es vollständig zu verstehen, aber sobald Sie einige Komponenten entworfen haben, ist es äußerst einfach, neue Objekte mit Ihren vorhandenen Komponenten zu erstellen.

Sie sollten dies lesen:

http://t-machine.org/index.php/2007/09/03/entity-systems-are-the-future-of-mmog-development-part-1/

Es wird häufig von einem äußerst sachkundigen Mann aktualisiert. Es ist auch die einzige Diskussion über Entitätssysteme mit konkreten Codebeispielen.

Meine Iterationen waren wie folgt:

Die erste Iteration hatte ein "EntitySystem" -Objekt, wie Adam es beschreibt; Meine Komponenten hatten jedoch immer noch Methoden - meine 'renderbare' Komponente hatte eine paint () -Methode und meine Positionskomponente hatte eine move () -Methode usw. Als ich anfing, die Entitäten zu konkretisieren, wurde mir klar, dass ich anfangen musste, Nachrichten zwischen ihnen zu übertragen Komponenten und Reihenfolge der Ausführung von Komponenten-Updates .... viel zu chaotisch.

Also ging ich zurück und las den Blog von T-Machines erneut. Es gibt viele Informationen in den Kommentarthreads - und in ihnen betont er wirklich, dass Komponenten kein Verhalten haben - Verhalten wird von den Entitätssystemen bereitgestellt. Auf diese Weise müssen Sie keine Nachrichten zwischen Komponenten übergeben und Komponentenaktualisierungen bestellen, da die Reihenfolge durch die globale Reihenfolge der Systemausführung bestimmt wird. In Ordnung. Vielleicht ist das zu abstrakt.

Für Iteration Nr. 2 habe ich Folgendes aus dem Blog entnommen:

EntityManager - fungiert als Komponente "Datenbank", die nach Entitäten abgefragt werden kann, die bestimmte Arten von Komponenten enthalten. Dies kann sogar durch eine In-Memory-Datenbank für einen schnellen Zugriff gesichert werden. Weitere Informationen finden Sie in Teil 5 von t-machine.

EntitySystem - Jedes System ist im Wesentlichen nur eine Methode, die mit einer Reihe von Entiten arbeitet. Jedes System verwendet die Komponenten x, y und z einer Entität, um ihre Arbeit zu erledigen. Sie würden also den Manager nach Entitäten mit den Komponenten x, y und z abfragen und dieses Ergebnis dann an das System übergeben.

Entität - nur eine ID, wie eine lange. Die Entität gruppiert eine Reihe von Komponenteninstanzen zu einer 'Entität'.

Komponente - eine Reihe von Feldern .... keine Verhaltensweisen! Wenn Sie anfangen, Verhaltensweisen hinzuzufügen, wird es unordentlich ... selbst in einem einfachen Space Invadors-Spiel.

Bearbeiten : 'dt' ist übrigens die Deltazeit seit dem letzten Aufruf der Hauptschleife

Meine Haupt-Invadors-Schleife lautet also:

Collection<Entity> entitiesWithGuns = manager.getEntitiesWith(Gun.class);
Collection<Entity> entitiesWithDamagable =
manager.getEntitiesWith(BulletDamagable.class);
Collection<Entity> entitiesWithInvadorDamagable = manager.getEntitiesWith(InvadorDamagable.class);

keyboardShipControllerSystem.update(entitiesWithGuns, dt);
touchInputSystem.update(entitiesWithGuns, dt);
Collection<Entity> entitiesWithInvadorMovement = manager.getEntitiesWith(InvadorMovement.class);
invadorMovementSystem.update(entitiesWithInvadorMovement);

Collection<Entity> entitiesWithVelocity = manager.getEntitiesWith(Velocity.class);
movementSystem.move(entitiesWithVelocity, dt);
gunSystem.update(entitiesWithGuns, System.currentTimeMillis());
Collection<Entity> entitiesWithPositionAndForm = manager.getEntitiesWith(Position.class, Form.class);
collisionSystem.checkCollisions(entitiesWithPositionAndForm);

Es sieht auf den ersten Blick etwas seltsam aus, ist aber unglaublich flexibel. Es ist auch sehr einfach zu optimieren; Für verschiedene Komponententypen können verschiedene Backing-Datenspeicher verwendet werden, um das Abrufen zu beschleunigen. Für die 'Formular'-Klasse können Sie sie mit einem Quadtree sichern, um den Zugriff für die Kollisionserkennung zu beschleunigen.

Ich bin wie du; Ich bin ein erfahrener Entwickler, hatte aber keine Erfahrung mit dem Schreiben von Spielen. Ich habe einige Zeit damit verbracht, Nachforschungen über Entwicklungsmuster anzustellen, und dieses ist mir aufgefallen. Es ist keineswegs die einzige Möglichkeit, Dinge zu tun, aber ich fand es sehr intuitiv und robust. Ich glaube, das Muster wurde offiziell in Buch 6 der Reihe "Game Programming Gems" - http://www.amazon.com/Game-Programming-Gems/dp/1584500492 - besprochen . Ich habe selbst keines der Bücher gelesen, aber ich höre, dass sie de facto die Referenz für die Spielprogrammierung sind.

Homebrew
quelle
1

Es gibt keine expliziten Regeln, die Sie beim Programmieren von Spielen befolgen müssen. Ich finde beide Muster vollkommen akzeptabel, solange Sie im gesamten Spiel dem gleichen Designmuster folgen. Sie wären überrascht, dass es viel mehr Designmuster für Spiele gibt, von denen einige nicht einmal über OOP liegen.

Nach meiner persönlichen Präferenz trenne ich Code lieber nach Verhalten (eine Klasse / ein Thread zum Zeichnen, eine andere zum Aktualisieren), während ich minimalistische Objekte habe. Ich finde es einfacher zu parallelisieren, und ich arbeite oft gleichzeitig an weniger Dateien. Ich würde sagen, dies ist eher eine prozedurale Vorgehensweise.

Aber wenn Sie aus der Geschäftswelt kommen, fühlen Sie sich wahrscheinlich wohler darin, Kurse zu schreiben, die wissen, wie man alles für sich selbst macht.

Ich empfehle Ihnen erneut, das zu schreiben, was Sie für am natürlichsten halten.


quelle
Ich denke, Sie haben meine Frage falsch verstanden. Ich sage, ich mag es nicht, Klassen zu haben, die alles selbst machen. Ich bin gefallen, als ob ein Ball nur eine logische Darstellung eines Balls sein sollte, aber er sollte nichts über die Spielschleife, die Eingabebehandlung usw. wissen.
BFree
Um fair zu sein, gibt es in der Business-Programmierung, wenn Sie es auf den Punkt bringen, zwei Züge: Business-Objekte, die laden, persistieren und Logik für sich selbst ausführen, und DTO + AppLogic + Repository / Persitance Layer ...
Nate
1

Das 'Ball'-Objekt hat Attribute und Verhaltensweisen. Ich finde es sinnvoll, die einzigartigen Attribute und Verhaltensweisen des Objekts in ihre eigene Klasse aufzunehmen. Die Art und Weise, wie ein Ball-Objekt aktualisiert wird, unterscheidet sich von der Art und Weise, wie ein Paddel aktualisiert wird. Daher ist es sinnvoll, dass dies einzigartige Verhaltensweisen sind, die die Aufnahme in die Klasse rechtfertigen. Gleiches gilt für das Zeichnen. Es gibt oft einzigartige Unterschiede in der Art und Weise, wie ein Paddel gegen einen Ball gezogen wird, und ich finde es einfacher, die Zeichenmethode eines einzelnen Objekts an diese Bedürfnisse anzupassen, als bedingte Bewertungen in einer anderen Klasse auszuarbeiten, um sie zu lösen.

Pong ist ein relativ einfaches Spiel in Bezug auf die Anzahl der Objekte und Verhaltensweisen, aber wenn Sie in die Dutzende oder Hunderte von einzigartigen Spielobjekten geraten, finden Sie Ihr zweites Beispiel möglicherweise etwas einfacher, um alles zu modularisieren.

Die meisten Leute verwenden eine Eingabeklasse, deren Ergebnisse allen Spielobjekten entweder über einen Dienst oder eine statische Klasse zur Verfügung stehen.

Steve H.
quelle
0

Ich denke, was Sie in der Geschäftswelt wahrscheinlich gewohnt sind, ist die Trennung von Abstraktion und Implementierung.

In der Welt der Spieleentwickler möchten Sie normalerweise in Geschwindigkeit denken. Je mehr Objekte Sie haben, desto mehr Kontextwechsel treten auf, was je nach aktueller Last zu Verzögerungen führen kann.

Dies ist nur meine Spekulation, da ich ein Möchtegern-Spieleentwickler, aber ein professioneller Entwickler bin.

Joey Green
quelle
0

Nein, es ist nicht "richtig" oder "falsch", es ist nur eine Vereinfachung.

Einige Geschäftscodes sind genau gleich, es sei denn, Sie arbeiten mit einem System, das Präsentation und Logik zwangsweise voneinander trennt.

Hier ein VB.NET-Beispiel, in dem "Geschäftslogik" (Hinzufügen einer Nummer) in einen GUI-Ereignishandler geschrieben wird - http://www.homeandlearn.co.uk/net/nets1p12.html

Wie gehe ich mit der Komplexität verschiedener Komponenten um, wenn jede einzelne Komponente alles selbst macht?

Wenn und wann es so komplex wird, können Sie es herausrechnen.

class Ball
{
    GraphicalModel ballVisual;
    void Draw()
    {
        ballVisual.Draw();
    }
};
Kylotan
quelle
0

Nach meiner Erfahrung in der Entwicklung gibt es keinen richtigen oder falschen Weg, um Dinge zu tun, es sei denn, es gibt eine akzeptierte Praxis in der Gruppe, mit der Sie arbeiten. Ich bin auf Widerstand von Entwicklern gestoßen, die es ablehnen, Verhaltensweisen, Skins usw. zu trennen. Und ich bin sicher, dass sie das Gleiche über meinen Vorbehalt sagen werden, eine Klasse zu schreiben, die alles unter der Sonne enthält. Denken Sie daran, dass Sie es nicht nur schreiben müssen, sondern Ihren Code morgen und zu einem späteren Zeitpunkt lesen, debuggen und möglicherweise in der nächsten Iteration darauf aufbauen können. Sie sollten einen Pfad wählen, der für Sie (und Ihr Team) funktioniert. Wenn Sie eine Route auswählen, die andere verwenden (und die für Sie nicht intuitiv ist), werden Sie langsamer.

kosmoknot
quelle