¿Deben los actores en un juego ser responsables de dibujar a sí mismos?

41

Soy muy nuevo en el desarrollo de juegos, pero no en la programación.

Estoy (de nuevo) jugando con un juego de tipo Pong usando el canvaselemento de JavaScript .

He creado un Paddleobjeto que tiene las siguientes propiedades ...

  • width
  • height
  • x
  • y
  • colour

También tengo un Pongobjeto que tiene propiedades como ...

  • width
  • height
  • backgroundColour
  • draw().

El draw()método actualmente está reiniciando canvasy ahí es donde surgió una pregunta.

Si el Paddleobjeto tiene un draw()método responsable de su elaboración, o si la draw()del Pongobjeto se encargará de elaborar sus actores (supongo que es el término correcto, por favor, corríjanme si estoy incorrecto).

Pensé que sería ventajoso Paddleque se dibujara, ya que instanciaba dos objetos, Playery Enemy. Si no estuviera en los Pong's draw(), necesitaría escribir un código similar dos veces.

¿Cuál es la mejor práctica aquí?

Gracias.

alex
fuente
Pregunta similar: gamedev.stackexchange.com/questions/13492/…
MichaelHouse

Respuestas:

49

Hacer que los actores se dibujen a sí mismos no es un buen diseño, por dos razones principales:

1) viola el principio de responsabilidad única , ya que esos actores presumiblemente tenían otro trabajo que hacer antes de que ingresaran el código de representación en ellos.

2) dificulta la extensión; Si cada tipo de actor implementa su propio dibujo, y necesita cambiar la forma en que dibuja en general , es posible que deba modificar mucho código. Evitar el uso excesivo de la herencia puede aliviar esto hasta cierto punto, pero no por completo.

Es mejor para su renderizador manejar el dibujo. Después de todo, eso es lo que significa ser un renderizador. El método de dibujo del renderizador debe tomar un objeto de "descripción de render", que contiene todo lo que necesita para renderizar una cosa. Referencias a datos de geometría (probablemente compartidos), transformaciones específicas de instancia o propiedades de materiales como el color, etc. Luego dibuja eso, y no le importa qué se supone que es esa descripción de renderizado.

Sus actores pueden conservar una descripción de render que ellos mismos crean. Dado que los actores son típicamente tipos de procesamiento lógico, pueden enviar cambios de estado a la descripción del render según sea necesario; por ejemplo, cuando un actor sufre daños, podría establecer el color de su descripción de render en rojo para indicar esto.

Luego, simplemente puede iterar cada actor visible, incluir sus descripciones de renderizado en el renderizador y dejar que haga lo suyo (básicamente; podría generalizar esto aún más).

Josh
fuente
14
+1 para el "código de dibujo del renderizador", pero -1 para el principio de responsabilidad única, que ha hecho a los programadores más daño que bien . Tiene la idea correcta (separación de preocupaciones) , pero la forma en que se dice ("cada clase debe tener una sola responsabilidad y / o solo una razón para cambiar") es simplemente errónea .
BlueRaja - Danny Pflughoeft
2
-1 para 1), como señaló BlueRaja, este principio es idealista, pero no práctico. -1 para 2) porque no hace que la extensión sea significativamente más difícil. Si tiene que cambiar todo su motor de renderizado, tendrá que cambiar mucho más código que el poco que tiene en su código de actor. Preferiría mucho más actor.draw(renderer,x,y)que renderer.draw(actor.getAttribs(),x,y).
corsiKa
1
¡Buena respuesta! Bueno "No se violará el sencillo principio de responsabilidad" no está dentro de mi decálogo, pero sigue siendo un buen principio para tener en cuenta
FXIII
@glowcoder Ese no es el punto, puede mantener actor.draw (), que se define como {renderer.draw (mesh, attribs, transform); }. En OpenGL, mantengo struct RenderDetail { glVAO* vao; int shader; int texture; Matrix4f* transform; }más o menos para mi renderizador. De esta forma, al procesador no le importa lo que representan los datos, solo los procesa. Según esta información, también puedo decidir si ordenar el orden de las llamadas de extracción para reducir las llamadas generales de la GPU.
deceleratedcaviar
16

El patrón de visitante puede ser útil aquí.

Lo que puede hacer es tener una interfaz de Renderer que sepa cómo dibujar cada objeto y un método de "dibujar usted mismo" en cada actor que determina a qué método de renderizador (específico) llamar, por ejemplo

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());
    }
}

De esta manera, todavía es fácil portar a otra biblioteca gráfica o marco (una vez porté un juego de Swing / Java2D a LWJGL y me alegré mucho de haber usado este patrón en lugar de pasarlo Graphics2D).

Hay otra ventaja: el renderizador se puede probar por separado de cualquier código de actor

finnw
fuente
2

En su ejemplo, desearía que los objetos Pong implementen draw () y se rendericen.

Aunque no notarás ganancias significativas en un proyecto de este tamaño, separar la lógica del juego y su representación visual (representación) es una actividad que vale la pena.

Con esto quiero decir que tendrías tus objetos de juego que se pueden actualizar () 'pero no tienen idea de cómo se representan, solo se preocupan por la simulación.

Entonces tendrías una clase PongRenderer () que tiene una referencia a un objeto Pong, y luego se encarga de representar la clase Pong (), esto podría implicar renderizar los Paddles, o tener una clase PaddleRenderer para cuidarlo.

La separación de preocupaciones en este caso es bastante natural, lo que significa que sus clases pueden estar menos hinchadas, y es más fácil modificar cómo se representan las cosas, ya no tiene que seguir la jerarquía que hace su simulación.

Miguel
fuente
-1

Si lo hiciera en el Pong's draw (), no necesitaría duplicar el código, simplemente llame a la función Paddle' draw () para cada una de sus paletas. Así que en tu Pongsorteo () tendrías

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

Cada objeto debe contener su propia función de dibujo que debe ser invocada por el código de actualización del juego o el código de dibujo. Me gusta

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( ); 
  } 
}

Este no es un código, sino solo para mostrar cómo se deben hacer los objetos de dibujo.

usuario43609
fuente
2
Esto no es "Cómo se debe hacer el dibujo", sino una única forma de hacerlo. Como se mencionó en respuestas anteriores, este método tiene algunos defectos y pocos beneficios, mientras que otros patrones tienen beneficios y flexibilidades significativas.
Troyseph
Lo aprecio, pero dado que se daría un ejemplo simple de las diversas formas de dibujar actores en la escena, mencioné de esta manera.
user43609