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.

alex
fonte
Pergunta semelhante: gamedev.stackexchange.com/questions/13492/…
MichaelHouse

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).

Josh
fonte
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

finnw
fonte
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.

Michael
fonte
-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();
Benixo
fonte
-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.

user43609
fonte
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