Os atores de um jogo devem ser responsáveis ​​por se desenhar?


41

Eu sou muito novo no desenvolvimento de jogos, mas não na programação.

Estou (de novo) brincando com um jogo do tipo Pong usando o canvaselemento JavaScript .

Eu criei um Paddleobjeto que possui as seguintes propriedades ...

  • width
  • height
  • x
  • y
  • colour

Eu também tenho um Pongobjeto que possui propriedades como ...

  • width
  • height
  • backgroundColour
  • draw().

O draw()método atualmente está redefinindo o canvase é aí que surgiu uma pergunta.

Se o Paddleobjeto tiver um draw()método responsável por seu desenho ou draw()o Pongobjeto for responsável por desenhar seus atores (presumo que esse seja o termo correto, corrija-me se estiver incorreto).

Imaginei que seria vantajoso Paddledesenhar, como instanciamos dois objetos, Playere Enemy. Se não estivesse no Pong's draw(), eu precisaria escrever código semelhante duas vezes.

Qual é a melhor prática aqui?

Obrigado.


Respostas:


49

Ter atores desenhando a si mesmos não é um bom design, por duas razões principais:

1) viola o princípio da responsabilidade única , pois esses atores provavelmente tinham outro trabalho a fazer antes de você inserir o código para eles.

2) dificulta a extensão; se todo tipo de ator implementa seu próprio desenho, e você precisa alterar a maneira como desenha em geral , pode ser necessário modificar muito código. Evitar o uso excessivo de herança pode aliviar isso em certa medida, mas não completamente.

É melhor para o seu renderizador lidar com o desenho. Afinal, é isso que significa ser um renderizador. O método de desenho do renderizador deve usar um objeto "descrição da renderização", que contém tudo o que você precisa para renderizar uma coisa. Referências a dados de geometria (provavelmente compartilhados), transformações específicas da instância ou propriedades do material, como cor, etc. Ele então desenha isso e não se importa com o que essa descrição de renderização deveria "ser".

Seus atores podem se apegar a uma descrição de renderização que eles mesmos criam. Como os atores são tipicamente tipos de processamento lógico, eles podem enviar alterações de estado para a descrição da renderização conforme necessário - por exemplo, quando um ator sofre dano, ele pode definir a cor da descrição da renderização para vermelho para indicar isso.

Em seguida, você pode simplesmente iterar todos os atores visíveis, colocar suas descrições de renderização no renderizador e deixá-lo funcionar (basicamente; você pode generalizar isso ainda mais).


14
+1 para "código de desenho do renderizador", mas -1 para o princípio de responsabilidade única, que causou mais mal do que bem aos programadores . Ele tem a idéia certa (separação de preocupações) , mas o modo como é declarado ("toda classe deve ter apenas uma responsabilidade e / ou apenas um motivo para mudar") está simplesmente errado .
BlueRaja - Danny Pflughoeft

2
-1 para 1), como BlueRaja apontou, esse princípio é idealista, mas não prático. -1 para 2) porque não torna a extensão significativamente mais difícil. Se você precisar alterar todo o mecanismo de renderização, terá muito mais código para alterar do que pouco que você tem no código do ator. Eu preferiria muito do actor.draw(renderer,x,y)que renderer.draw(actor.getAttribs(),x,y).
corsiKa

1
Boa resposta! Bem "Você não deve violar o princípio da responsabilidade única" não é dentro do meu decálogo, mas ainda é um bom princípio de levar em conta
FXIII

@glowcoder Esse não é o ponto, você pode manter o ator.draw (), que é definido como {renderer.draw (mesh, attribs, transform); } No OpenGL, mantenho struct RenderDetail { glVAO* vao; int shader; int texture; Matrix4f* transform; }mais ou menos para o meu renderizador. Dessa forma, o renderizador não se importa com o que os dados representam, apenas os processa. Com base nessas informações, também posso decidir se classificar a ordem das chamadas de empate para reduzir as chamadas gerais da GPU.
Deceleratedcaviar

16

O Padrão do Visitante pode ser útil aqui.

O que você pode fazer é ter uma interface Renderer que saiba desenhar cada objeto e um método "desenhe você mesmo" em cada ator que determine qual método (específico) de renderizador chamar, por exemplo

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

Dessa forma, ainda é fácil portar para outra biblioteca ou estrutura gráfica (uma vez eu portava um jogo do Swing / Java2D para o LWJGL e fiquei muito feliz por ter usado esse padrão em vez de dar uma Graphics2Dvolta.)

Há outra vantagem: o renderizador pode ser testado separadamente de qualquer código de ator


2

No seu exemplo, você deseja que os objetos Pong implementem draw () e se renderizem.

Embora você não note nenhum ganho significativo em um projeto desse tamanho, geralmente separar a lógica do jogo e sua representação visual (renderização) é uma atividade que vale a pena.

Com isso, quero dizer que você teria seus objetos de jogo que podem ser atualizados (), mas eles não têm idéia de como são renderizados, eles se preocupam apenas com a simulação.

Então você teria uma classe PongRenderer () que tem uma referência a um objeto Pong e, em seguida, se encarrega de renderizar a classe Pong (), isso pode envolver renderizar os Paddles ou ter uma classe PaddleRenderer para cuidar dele.

A separação de preocupações nesse caso é bastante natural, o que significa que suas classes podem ser menos inchadas e é mais fácil modificar a maneira como as coisas são renderizadas, não é mais necessário seguir a hierarquia que sua simulação faz.


-1

Se você o fizesse no Pongdraw (), não seria necessário duplicar o código - basta chamar a Paddlefunção draw () de cada um dos seus remos. Então, no seu Pongsorteio () você teria

humanPaddle.draw();
compPaddle.draw();

-1

Cada objeto deve conter sua própria função de desenho, que deve ser chamada pelo código de atualização do jogo ou pelo código de desenho. Gostar

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 não é um código, mas apenas para mostrar como os objetos de desenho devem ser feitos.


2
Isso não é "Como o desenho deve ser feito", mas uma maneira única de fazê-lo. Conforme mencionado nas respostas anteriores, este método apresenta algumas falhas e poucos benefícios, enquanto outros padrões apresentam benefícios e flexibilidades significativos.
Troyseph

Eu aprecio, mas como um exemplo simples deveria ser dado das várias maneiras de desenhar atores na cena, mencionei dessa maneira.
user43609
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.