Sollen die Akteure eines Spiels selbst für das Zeichnen verantwortlich sein?

41

Ich bin sehr neu in der Spieleentwicklung, aber nicht in der Programmierung.

Ich spiele (wieder) mit einem Pong-Spiel, das JavaScript- canvasElemente verwendet.

Ich habe ein PaddleObjekt mit folgenden Eigenschaften erstellt ...

  • width
  • height
  • x
  • y
  • colour

Ich habe auch ein PongObjekt, das Eigenschaften wie hat ...

  • width
  • height
  • backgroundColour
  • draw().

Die draw()Methode setzt derzeit die zurück canvasund hier ist eine Frage aufgetaucht.

Sollte das PaddleObjekt eine draw()Methode haben, die für das Zeichnen verantwortlich ist, oder sollte die Methode draw()des PongObjekts für das Zeichnen der Akteure verantwortlich sein (ich nehme an, dass dies der richtige Begriff ist, korrigieren Sie mich bitte, wenn ich falsch bin).

Ich dachte mir, dass es von Vorteil wäre, sich Paddleselbst zu zeichnen, wenn ich zwei Objekte instanziiere, Playerund Enemy. Wenn es nicht in den Pong's wäre draw(), müsste ich zweimal ähnlichen Code schreiben.

Was ist hier die beste Praxis?

Vielen Dank.

Alex
quelle
Ähnliche Frage: gamedev.stackexchange.com/questions/13492/…
MichaelHouse

Antworten:

49

Schauspieler selbst zeichnen zu lassen, ist aus zwei Hauptgründen kein gutes Design:

1) Es verstößt gegen das Prinzip der Einzelverantwortung , da diese Akteure vermutlich eine andere Aufgabe hatten, bevor Sie Render-Code in sie hineingeschoben haben.

2) es erschwert die Verlängerung; Wenn jeder Darstellertyp eine eigene Zeichnung implementiert und Sie die Art und Weise ändern müssen, in der Sie generell zeichnen , müssen Sie möglicherweise viel Code ändern. Das Vermeiden von Übernutzung der Vererbung kann dies bis zu einem gewissen Grad, jedoch nicht vollständig, lindern.

Es ist besser für Ihren Renderer, die Zeichnung zu handhaben. Schließlich bedeutet es, ein Renderer zu sein. Die draw-Methode des Renderers sollte ein "render description" -Objekt enthalten, das alles enthält, was Sie zum Rendern eines Objekts benötigen. Verweise auf (wahrscheinlich gemeinsam genutzte) Geometriedaten, instanzspezifische Transformationen oder Materialeigenschaften wie Farbe usw. Es zeichnet das dann und kümmert sich nicht darum, was diese Render-Beschreibung "sein soll".

Ihre Darsteller können sich dann an einer Render-Beschreibung festhalten, die sie selbst erstellt haben. Da es sich bei den Akteuren in der Regel um logische Verarbeitungstypen handelt, können sie nach Bedarf Statusänderungen an der Render-Beschreibung vornehmen. Wenn ein Akteur beispielsweise Schaden nimmt, kann er die Farbe seiner Render-Beschreibung auf Rot setzen, um dies anzuzeigen.

Dann können Sie einfach jeden sichtbaren Akteur iterieren, seine Render-Beschreibungen in den Renderer eingeben und ihn seine Sache machen lassen (im Grunde genommen können Sie dies sogar noch weiter verallgemeinern).

Josh
quelle
14
+1 für "Renderer Drawing Code", aber -1 für das Prinzip der einfachen Verantwortung, das Programmierern mehr schadet als nützt . Es hat die richtige Idee (Trennung der Anliegen) , aber die Art und Weise, wie es heißt ("jede Klasse sollte nur eine Verantwortung haben und / oder nur einen Grund, sich zu ändern"), ist einfach falsch .
BlueRaja - Danny Pflughoeft
2
-1 für 1), wie BlueRaja hervorhob, ist dieses Prinzip idealistisch, aber nicht praktisch. -1 für 2) weil es die Erweiterung nicht wesentlich erschwert. Wenn Sie Ihre gesamte Rendering-Engine ändern müssen, müssen Sie viel mehr Code ändern, als Sie in Ihrem Akteur-Code haben. Ich würde es viel bevorzugen actor.draw(renderer,x,y)als renderer.draw(actor.getAttribs(),x,y).
corsiKa
1
Gute Antwort! Gut "Du sollst nicht gegen das Prinzip der einmaligen Verantwortung verstoßen" ist nicht in meinem Dekalog enthalten, aber es ist immer noch ein gutes Prinzip, das zu berücksichtigen ist
FxIII
@glowcoder Das ist nicht der Punkt, Sie können actor.draw () pflegen, das definiert ist als {renderer.draw (mesh, attribs, transform); }. In OpenGL pflege ich ein struct RenderDetail { glVAO* vao; int shader; int texture; Matrix4f* transform; }Mehr oder Weniger für meinen Renderer. Auf diese Weise ist es dem Renderer egal, was die Daten darstellen, er verarbeitet sie nur. Anhand dieser Informationen kann ich dann auch entscheiden, ob die Reihenfolge der Draw-Aufrufe sortiert werden soll, um die Anzahl der GPU-Aufrufe insgesamt zu verringern.
verzögerte
16

Das Besuchermuster kann hier hilfreich sein.

Was Sie tun können, ist eine Renderer-Oberfläche, die weiß, wie jedes Objekt gezeichnet wird, und eine "Zeichne dich selbst" -Methode in jedem Akteur, die festlegt, welche (bestimmte) Renderer-Methode aufzurufen ist, z

interface Renderer {
    void drawPaddle(Player owner, Rectangle position);
    // Note: the Renderer chooses the Color based on which player the paddle belongs to

    // Also drawBackground, drawBall etc.
}

interface Actor {
    void draw(Renderer renderer);
}

class Paddle implements Actor {
    void draw(Renderer renderer) {
        renderer.drawPaddle(this.owner, this.getBounds());
    }
}

Auf diese Weise ist es immer noch einfach, auf eine andere Grafikbibliothek oder ein anderes Framework zu portieren Graphics2D.

Ein weiterer Vorteil: Der Renderer kann unabhängig von jedem Akteurcode getestet werden

finnw
quelle
2

In Ihrem Beispiel möchten Sie, dass die Pong-Objekte draw () implementieren und sich selbst rendern.

Obwohl Sie bei einem Projekt dieser Größe keine nennenswerten Gewinne bemerken, lohnt es sich, die Spielelogik und ihre visuelle Darstellung (Rendering) zu trennen.

Damit meine ich, du hättest deine Spielobjekte, die update () sein können, aber sie haben keine Ahnung, wie sie gerendert werden, sie beschäftigen sich nur mit der Simulation.

Dann hätten Sie eine PongRenderer () -Klasse, die einen Verweis auf ein Pong-Objekt hat, und sie übernimmt dann das Rendern der Pong () -Klasse. Dies könnte das Rendern der Paddles oder eine PaddleRenderer -Klasse umfassen, die sich darum kümmert.

Die Trennung von Bedenken ist in diesem Fall ziemlich natürlich, was bedeutet, dass Ihre Klassen weniger aufgebläht sein können und es einfacher ist, die Darstellung der Dinge zu ändern. Es muss nicht mehr der Hierarchie folgen, die Ihre Simulation durchführt.

Michael
quelle
-1

Wenn Sie dies mit Pongdraw () tun würden, müssten Sie den Code nicht duplizieren - rufen Sie einfach die Paddledraw () - Funktion für jedes Ihrer Paddles auf. Also in deiner PongAuslosung () hättest du

humanPaddle.draw();
compPaddle.draw();
Benixo
quelle
-1

Jedes Objekt sollte eine eigene Zeichenfunktion enthalten, die über den Spielaktualisierungscode oder den Zeichencode aufgerufen werden sollte. Mögen

class X
{
  public void Draw( )
  {
     // Do something 
  } 
}

class Y
{
  public void Draw( )
   { // Do Something 
   } 
}

class Game
{
  X objX;
  Y objY;

  @Override
  public void OnDraw( )
  {
      objX.Draw( );
      objY.Draw( ); 
  } 
}

Dies ist kein Code, sondern dient nur der Veranschaulichung, wie das Zeichnen von Objekten erfolgen soll.

user43609
quelle
2
Dies ist nicht "Wie das Zeichnen erfolgen soll", sondern eine einzige Möglichkeit, dies zu tun. Wie in den vorherigen Antworten erwähnt, weist diese Methode einige Mängel und wenige Vorteile auf, während andere Muster erhebliche Vorteile und Flexibilität aufweisen.
Troyseph
Ich weiß es zu schätzen, aber da ein einfaches Beispiel für die verschiedenen Arten des Zeichnens von Schauspielern in der Szene gegeben werden sollte, erwähnte ich diesen Weg.
user43609