Warum sollte ich Objekte vom Rendern trennen?


11

Disclamer: Ich weiß , was ein Unternehmen System Muster ist , und ich bin nicht mit ihm.

Ich habe viel über das Trennen von Objekten und das Rendern gelesen. Über die Tatsache, dass die Spielelogik unabhängig von der zugrunde liegenden Rendering-Engine sein sollte. Das ist alles in Ordnung und gut und es macht vollkommen Sinn, aber es verursacht auch viele andere Schmerzen:

  • Notwendigkeit der Synchronisation zwischen dem Logikobjekt und dem Renderingobjekt (das den Status der Animation, der Sprites usw. beibehält)
  • Das Logikobjekt muss für die Öffentlichkeit geöffnet werden, damit das Rendering-Objekt den tatsächlichen Status des Logikobjekts lesen kann (was häufig dazu führt, dass sich das Logikobjekt leicht in ein dummes Getter- und Setter-Objekt verwandelt).

Das klingt für mich nicht nach einer guten Lösung. Andererseits ist es sehr intuitiv, sich ein Objekt als 3D- (oder 2D-) Darstellung vorzustellen, und es ist auch sehr einfach zu warten (und möglicherweise auch viel gekapselter).

Gibt es eine Möglichkeit, die grafische Darstellung und die Spielelogik miteinander zu verbinden (um Synchronisationsprobleme zu vermeiden), aber die Rendering-Engine zu abstrahieren? Oder gibt es eine Möglichkeit, Spielelogik und Rendering zu trennen, die die oben genannten Nachteile nicht verursacht?

(möglicherweise mit Beispielen, ich bin nicht sehr gut darin, abstrakte Gespräche zu verstehen)


1
Es wäre auch hilfreich, wenn Sie ein Beispiel dafür geben, was Sie meinen, wenn Sie sagen, dass Sie das Entitätssystemmuster nicht verwenden, und wie Sie denken, dass dies damit zusammenhängt, ob Sie das Anliegen des Renderns vom Anliegen der Entität / trennen sollten oder nicht Spielelogik.
michael.bartnett

@ michael.bartnett, ich trenne Objekte nicht in kleine wiederverwendbare Komponenten, die von Systemen verarbeitet werden, wie es die meisten Implementierungen der Muster tun. Mein Code ist eher ein Versuch, das MVC-Muster zu verwenden. Aber es spielt keine Rolle, da die Frage von keinem Code abhängig ist (nicht einmal von einer Sprache). Ich habe den Disclamer eingesetzt, weil ich wusste, dass einige versucht hätten, mich davon zu überzeugen, das ECS zu verwenden, das Krebs zu heilen scheint. Und wie Sie sehen, ist es trotzdem passiert.
Schuh

Antworten:


13

Angenommen, Sie haben eine Szene die aus einer Welt , einem Spieler und einem Boss besteht. Oh, und das ist ein Spiel für Dritte, also hast du auch eine Kamera .

Ihre Szene sieht also so aus:

class Scene {
    World* world
    Player* player
    Enemy* boss
    Camera* camera
}

(Zumindest, das ist die Grunddaten . Wie Sie die Daten enthalten, zu Ihnen auf.)

Sie möchten die Szene nur aktualisieren und rendern, wenn Sie das Spiel spielen, nicht wenn sie angehalten ist oder im Hauptmenü ... also hängen Sie sie an den Spielstatus an!

State* gameState = new State();
gameState->addScene(scene);

Jetzt hat Ihr Spielstatus eine Szene. Als Nächstes möchten Sie die Logik für die Szene ausführen und die Szene rendern. Für die Logik führen Sie einfach eine Aktualisierungsfunktion aus.

State::update(double delta) {
    scene->update(delta);
}

Auf diese Weise können Sie die gesamte Spiellogik in der behalten Scene Klasse . Und nur als Referenz könnte ein Entitätskomponentensystem dies stattdessen so machen:

State::update(double delta) {
    physicsSystem->applyPhysics(scene);
}

Wie auch immer, Sie haben es jetzt geschafft, Ihre Szene zu aktualisieren. Jetzt möchten Sie es anzeigen! Für die wir etwas Ähnliches tun wie oben:

State::render() {
    renderSystem->render(scene);
}

Los geht's. Das renderSystem liest die Informationen aus der Szene und zeigt das entsprechende Bild an. Vereinfacht ausgedrückt könnte die Methode zum Rendern der Szene folgendermaßen aussehen:

RenderSystem::renderScene(Scene* scene) {
    Camera* camera = scene->camera;
    lookAt(camera); // Set up the appropriate viewing matrices based on 
                    // the camera location and direction

    renderHeightmap(scene->getWorld()->getHeightMap()); // Just as an example, you might
                                                        // use a height map as your world
                                                        // representation.
    renderModel(scene->getPlayer()->getType()); // getType() will return, for example "orc"
                                                // or "human"

    renderModel(scene->getBoss()->getType());
}

Wirklich vereinfacht, müssten Sie beispielsweise noch eine Rotation und Übersetzung anwenden, die darauf basiert, wo sich Ihr Spieler befindet und wo er sucht. (Mein Beispiel ist ein 3D-Spiel. Wenn Sie sich für 2D entscheiden, ist dies ein Spaziergang im Park.)

Ich hoffe das ist was du gesucht hast? Wie Sie sich hoffentlich aus dem oben Gesagten erinnern können, ist das Render-System kümmert sich nicht um die Logik des Spiels . Es wird nur der aktuelle Status der Szene zum Rendern verwendet, dh es werden die erforderlichen Informationen zum Rendern daraus abgerufen. Und die Spiellogik? Es ist egal, was der Renderer macht. Heck, es ist egal, ob es überhaupt angezeigt wird!

Außerdem müssen Sie der Szene keine Rendering-Informationen hinzufügen. Es sollte ausreichen, dass der Renderer weiß, dass er einen Ork rendern muss. Du hast bereits ein Ork-Modell geladen, das der Renderer dann anzeigen kann.

Dies sollte Ihren Anforderungen entsprechen. Die grafische Darstellung und die Logik sind gekoppelt , da beide dieselben Daten verwenden. Dennoch sind sie getrennt , weil sich keiner auf den anderen verlässt!

EDIT: Und nur um zu beantworten, warum man es so machen würde? Weil es einfacher ist, ist der einfachste Grund. Sie müssen nicht darüber nachdenken, "so und so ist passiert, ich sollte jetzt die Grafiken aktualisieren". Stattdessen machst du Dinge möglich, und in jedem Frame des Spiels wird untersucht, was gerade passiert, und es wird auf irgendeine Weise interpretiert, sodass du ein Ergebnis auf dem Bildschirm erhältst.


7

Ihr Titel stellt eine andere Frage als Ihr Körperinhalt. Im Titel fragen Sie, warum Logik und Rendering getrennt werden sollten, aber im Hauptteil fragen Sie nach Implementierungen von Logik- / Grafik- / Rendering-Systemen.

Die zweite Frage wurde bereits angesprochen , daher werde ich mich auf die erste Frage konzentrieren.

Gründe für die Trennung von Logik und Rendering:

  1. Die weit verbreitete Vorstellung, dass Objekte eines tun sollten
  2. Was ist, wenn Sie von 2D zu 3D wechseln möchten? Was ist, wenn Sie sich mitten im Projekt für einen Wechsel von einem Rendering-System zu einem anderen entscheiden? Sie möchten nicht Ihren gesamten Code durchkriechen und mitten in Ihrer Spielelogik große Änderungen vornehmen.
  3. Sie hätten wahrscheinlich Grund, Codeabschnitte zu wiederholen, was im Allgemeinen als schlechte Idee angesehen wird.
  4. Sie können Systeme erstellen, um potenziell große Rendering- oder Logikbereiche zu steuern, ohne individuell mit kleinen Teilen zu kommunizieren.
  5. Was ist, wenn Sie einem Spieler einen Edelstein zuweisen möchten, das System jedoch durch die Anzahl der Facetten des Edelsteins verlangsamt wird? Wenn Sie Ihr Rendering-System gut genug abstrahiert haben, können Sie es mit unterschiedlichen Raten aktualisieren, um teure Rendering-Vorgänge zu berücksichtigen.
  6. Es ermöglicht Ihnen, über Dinge nachzudenken, die für Ihre Arbeit wirklich wichtig sind. Warum sollten Sie Ihr Gehirn um Matrixtransformationen und Sprite-Offsets und Bildschirmkoordinaten wickeln, wenn Sie lediglich eine Doppelsprungmechanik implementieren, eine Karte ziehen oder ein Schwert ausrüsten möchten? Sie möchten nicht, dass das Sprite Ihr ausgestattetes Schwert in leuchtendem Rosa darstellt, nur weil Sie es von Ihrer rechten Hand nach Ihrer linken bewegen möchten.

In einer OOP-Einstellung ist das Instanziieren neuer Objekte mit Kosten verbunden, aber meiner Erfahrung nach sind die Kosten für Systemressourcen ein geringer Preis für die Fähigkeit, über die spezifischen Dinge nachzudenken und diese zu implementieren, die ich erledigen muss.


6

Diese Antwort dient nur dazu, eine Vorstellung davon zu entwickeln, warum die Trennung von Rendering und Logik wichtig ist, anstatt direkt praktische Beispiele vorzuschlagen.

Nehmen wir an, wir haben einen großen Elefanten , niemand im Raum kann den ganzen Elefanten sehen. Vielleicht sind sich alle sogar nicht einig darüber, was es tatsächlich ist. Weil jeder einen anderen Teil des Elefanten sieht und nur mit diesem Teil umgehen kann. Aber am Ende ändert dies nichts an der Tatsache, dass es sich um einen großen Elefanten handelt.

Der Elefant repräsentiert das Spielobjekt mit all seinen Details. Aber niemand muss wirklich alles über den Elefanten (Spielobjekt) wissen, um seine Funktionalität ausführen zu können.

Das Koppeln der Spiellogik und des Renderns ist eigentlich so, als würde jeder den ganzen Elefanten sehen. Wenn sich etwas geändert hat, muss jeder davon wissen. Während sie in den meisten Fällen nur den Teil sehen müssen, an dem sie nur interessiert sind. Wenn etwas die Person verändert hat, die davon weiß, muss sie nur der anderen Person über das Ergebnis dieser Änderung erzählen, das ist nur für sie wichtig (Betrachten Sie dies als Kommunikation über Nachrichten oder Schnittstellen).

Geben Sie hier die Bildbeschreibung ein

Die Punkte, die Sie erwähnt haben, sind keine Nachteile, sondern nur Nachteile, wenn es mehr Abhängigkeiten gab, als es in der Engine geben sollte. Mit anderen Worten, Systeme sehen Teile des Elefanten mehr als sie sollten. Und das bedeutet, dass der Motor nicht "richtig" konstruiert wurde.

Sie müssen nur mit der formalen Definition synchronisiert werden, wenn Sie eine Multithread-Engine verwenden, bei der die Logik und das Rendering in zwei verschiedenen Threads zusammengefasst sind, und selbst eine Engine, die viel Synchronisation zwischen Systemen erfordert, ist nicht besonders gut ausgelegt.

Andernfalls besteht die natürliche Art, mit einem solchen Fall umzugehen, darin, das System als Eingabe / Ausgabe zu entwerfen. Das Update führt die Logik aus und gibt das Ergebnis aus. Das Rendering wird nur mit den Ergebnissen des Updates gespeist. Sie müssen nicht wirklich alles aussetzen. Sie legen nur eine Schnittstelle offen, die zwischen den beiden Stufen kommuniziert. Die Kommunikation zwischen verschiedenen Teilen der Engine sollte über Abstraktionen (Schnittstellen) und / oder Nachrichten erfolgen. Es sollten keine internen Logik oder Zustände offengelegt werden.

Nehmen wir ein einfaches Beispiel für ein Szenendiagramm, um die Idee zu erklären.

Die Aktualisierung erfolgt normalerweise über eine einzelne Schleife, die als Spieleschleife bezeichnet wird (oder möglicherweise über mehrere Spielschleifen, die jeweils in einem separaten Thread ausgeführt werden). Sobald die Schleife jemals ein Spielobjekt aktualisiert hat. Es muss nur über Messaging oder Schnittstellen mitteilen, dass die Objekte 1 und 2 aktualisiert wurden, und sie mit der endgültigen Transformation versorgen.

Das Rendering-System nimmt nur die endgültige Transformation vor und weiß nicht, was sich tatsächlich am Objekt geändert hat (z. B. ist eine bestimmte Kollision aufgetreten usw.). Um dieses Objekt zu rendern, benötigt es nur die ID dieses Objekts und die endgültige Transformation. Danach füttert der Renderer die Rendering-API mit dem Netz und der endgültigen Transformation, ohne etwas anderes zu wissen.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.