Táticas para mover a lógica de renderização da classe GameObject

10

Ao criar jogos, você geralmente cria o seguinte objeto de jogo do qual todas as entidades herdam:

public class GameObject{
    abstract void Update(...);
    abstract void Draw(...);
}

Assim, ao atualizar o loop, você itera sobre todos os objetos do jogo e dá a eles a chance de mudar de estado; depois, no próximo loop, você itera novamente sobre todos os objetos do jogo e dá a eles a chance de se desenhar.

Embora isso funcione razoavelmente bem em um jogo simples com um renderizador de encaminhamento simples, geralmente leva a alguns objetos de jogo gigantescos que precisam armazenar seus modelos, várias texturas e, pior ainda, um método de desenho de gordura que cria um forte acoplamento entre o objeto de jogo, a estratégia de renderização atual e quaisquer classes relacionadas à renderização.

Se eu alterasse a estratégia de renderização de adiada para adiada, precisaria atualizar muitos objetos do jogo. E os objetos do jogo que eu faço não são tão reutilizáveis ​​quanto poderiam ser. É claro que a herança e / ou composição podem me ajudar a combater a duplicação de código e facilitar um pouco a alteração da implementação, mas ainda falta.

Uma maneira melhor, talvez, seria remover completamente o método Draw da classe GameObject e criar uma classe Renderer. O GameObject ainda precisaria conter alguns dados sobre o visual, como qual modelo representá-lo e quais texturas devem ser pintadas no modelo, mas como isso é feito, será deixado para o renderizador. No entanto, muitas vezes há muitos casos de borda na renderização. Embora isso remova o acoplamento rígido do GameObject ao Renderer, o Renderer ainda precisa saber tudo sobre todos os objetos do jogo que o tornariam gordo, todo o conhecimento e fortemente acoplado. Isso violaria algumas boas práticas. Talvez o Design Orientado a Dados possa fazer o truque. Objetos de jogo certamente seriam dados, mas como o renderizador seria movido por isso? Não tenho certeza.

Então, estou perdida e não consigo pensar em uma boa solução. Tentei usar os princípios do MVC e, no passado, tive algumas idéias sobre como usá-lo em jogos, mas recentemente não parece tão aplicável quanto eu pensava. Gostaria muito de saber como todos vocês lidam com esse problema.

De qualquer forma, recapitulando, estou interessado em saber como os seguintes objetivos de design podem ser alcançados.

  • Nenhuma lógica de renderização no objeto do jogo
  • Acoplamento fraco entre os objetos do jogo e o mecanismo de renderização
  • Nenhum processador que conhece tudo
  • De preferência alternando o tempo de execução entre os mecanismos de renderização

A configuração ideal do projeto seria uma 'lógica de jogo' separada e renderizaria um projeto de lógica que não precisa se referir um ao outro.

Todo esse pensamento começou quando ouvi John Carmack dizer no twitter que ele tem um sistema tão flexível que ele pode trocar os mecanismos de renderização em tempo de execução e pode até dizer ao sistema para usar os dois renderizadores (um renderizador de software e um renderizador acelerado por hardware) ao mesmo tempo, para que ele possa inspecionar as diferenças. Os sistemas que eu programei até agora não são nem tão flexíveis

Roy T.
fonte

Respostas:

7

Um primeiro passo rápido para desacoplar:

Objetos de jogo fazem referência a um identificador de quais são os visuais, mas não os dados, digamos algo simples como uma string. Exemplo: "human_male"

O renderizador é responsável por carregar e manter as referências "human_male" e retornar aos objetos um identificador para usar.

Exemplo em pseudocódigo horrível:

GameObject( initialization parameters )
  me.render_handle = Renderer_Create( parameters.render_string )

- elsewhere
Renderer_Create( string )

  new data handle = Resources_Load( string );
  return new data handle

- some time later
GameObject( something happens to me parameters )
  me.state = something.what_happens
  Renderer_ApplyState( me.render_handle, me.state.effect_type )

- some time later
Renderer_Render()
  for each renderable thing
    for each rendering back end
        setup graphics for thing.effect
        render it

- finally
GameObject_Destroy()
  Renderer_Destroy( me.render_handle )

Desculpe por essa bagunça, de qualquer maneira, suas condições são atendidas por essa simples mudança do POO puro, com base na observação de objetos como objetos do mundo real e no POO, com base nas responsabilidades.

  • Nenhuma lógica de renderização no objeto do jogo (pronto, tudo o que o objeto sabe é um identificador para que ele possa aplicar efeitos a si mesmo)
  • Acoplamento fraco entre objetos de jogo e mecanismo de renderização (pronto, todo contato é feito por um identificador abstrato, estados que podem ser aplicados e não o que fazer com esses estados)
  • Nem todo renderizador conhecido (pronto, só sabe sobre si mesmo)
  • De preferência, alternando o tempo de execução entre os mecanismos de renderização (isso é feito no estágio Renderer_Render (), você precisa escrever os dois back-ends)

As palavras-chave nas quais você pode procurar para ir além de uma simples refatoração de classes seriam "sistema de entidade / componente" e "injeção de dependência" e padrões de GUI potencialmente "MVC" apenas para fazer girar as velhas engrenagens do cérebro.

Patrick Hughes
fonte
Isso é extremamente diferente do que já fiz antes, parece que tem um grande potencial. Felizmente, não estou constrangido por nenhum mecanismo existente, para que eu possa mexer. Também procurarei os termos que você mencionou, embora a injeção de dependência sempre faça meu cérebro doer: P.
Roy T.
2

O que eu fiz para o meu próprio mecanismo é agrupar tudo em módulos. Então, eu tenho minha GameObjectclasse e ela possui uma alça para:

  • ModuleSprite - desenhando sprites
  • ModuleWeapon - armas de tiro
  • ModuleScriptingBase - script
  • ModuleParticles - efeitos de partículas
  • ModuleCollision - detecção e resposta de colisão

Então eu tenho uma Playeraula e uma Bulletaula. Ambos derivam GameObjecte são adicionados ao Scene. Mas Playerpossui os seguintes módulos:

  • ModuleSprite
  • ModuleWeapon
  • ModuleParticles
  • ModuleCollision

E Bulletpossui estes módulos:

  • ModuleSprite
  • ModuleCollision

Essa maneira de organizar as coisas evita o "Diamante da Morte", onde você tem a Vehicle, a VehicleLande a VehicleWatere agora deseja a VehicleAmphibious. Em vez disso, você tem um Vehiclee ele pode ter um ModuleWatere um ModuleLand.

Bônus adicional: você pode criar objetos usando um conjunto de propriedades. Tudo o que você precisa saber é o tipo de base (Player, Inimigo, Bullet, etc.) e, em seguida, crie identificadores para os módulos necessários para esse tipo.

Na minha cena, faço o seguinte:

  • Ligue Updatepara todas as GameObjectalças.
  • Faça a verificação de colisão e a resposta de colisão para aqueles que têm um ModuleCollisionidentificador.
  • Ligue UpdatePostpara todas as GameObjectalças para informar sobre sua posição final após a física.
  • Destrua objetos que tenham seu sinalizador definido.
  • Adicione novos objetos da m_ObjectsCreatedlista à m_Objectslista.

E eu poderia organizá-lo ainda mais: por módulos em vez de por objeto. Então eu renderizava uma lista de ModuleSprite, atualizava várias ModuleScriptingBasee fazia colisões com uma lista de ModuleCollision.

knight666
fonte
Parece composição ao máximo! Muito agradável. No entanto, não vejo muitas dicas específicas sobre renderização aqui. Como você lida com isso, apenas adicionando módulos diferentes?
Roy T.
Ai sim. Essa é a desvantagem deste sistema: se você tem um requisito específico para um GameObject(por exemplo, uma maneira de renderizar uma "cobra" de Sprites), será necessário criar um filho ModuleSpritepara essa funcionalidade específica ( ModuleSpriteSnake) ou adicionar um novo módulo completamente ( ModuleSnake) Felizmente, são apenas indicadores, mas vi código onde GameObjectliteralmente tudo o que um objeto poderia fazer.
Knight666