Por que devo separar objetos da renderização?

11

Disclamer: Eu sei o que é um padrão de sistema entidade é e eu estou não usá-lo.

Eu li muito sobre a separação de objetos e renderizações. Sobre o fato de que a lógica do jogo deve ser independente do mecanismo de renderização subjacente. Tudo isso é bom e elegante, e faz todo o sentido, mas causa muitas outras dores também:

  • necessidade de sincronização entre o objeto lógico e o objeto de renderização (aquele que mantém o estado da animação, os sprites etc.)
  • é necessário abrir o objeto lógico ao público para que o objeto de renderização leia o estado real do objeto lógico (geralmente levando o objeto lógico a se transformar facilmente em um objeto getter e setter burro)

Isso não parece uma boa solução para mim. Por outro lado, é muito intuitivo imaginar um objeto como sua representação em 3D (ou 2D) e também é muito fácil de manter (e possivelmente muito mais encapsulado).

Existe uma maneira de manter a representação gráfica e a lógica do jogo acopladas (evitando problemas de sincronização), mas abstraindo o mecanismo de renderização? Ou existe uma maneira de separar a lógica do jogo e a renderização que não causa as desvantagens acima?

(possivelmente com exemplos, não sou muito bom em entender conversas abstratas)

Sapato
fonte
1
Também seria útil se você fornecer um exemplo do que você quer dizer quando diz que não está usando o padrão do sistema de entidades e como acha que isso se relaciona se você deve ou não separar a preocupação de renderizar da preocupação da entidade / lógica do jogo.
Michael.bartnett
@ michael.bartnett, não estou separando objetos em pequenos componentes reutilizáveis ​​que são manipulados por sistemas, da mesma forma que a maioria das implementações dos padrões. Meu código é mais uma tentativa do padrão MVC. Mas isso realmente não importa, pois a pergunta não depende de nenhum código (nem mesmo de um idioma). Eu coloquei o divulgador porque sabia que alguns tentariam me convencer a usar o ECS, que parece curar o câncer. E, como você pode ver, aconteceu de qualquer maneira.
Shoe

Respostas:

13

Suponha que você tenha uma cena composta por um mundo , um jogador e um chefe. Ah, e este é um jogo de terceira pessoa, então você também tem uma câmera .

Portanto, sua cena fica assim:

class Scene {
    World* world
    Player* player
    Enemy* boss
    Camera* camera
}

(Pelo menos, esses são os dados básicos . Como você os contém, é com você.)

Você só deseja atualizar e renderizar a cena quando estiver jogando, não quando estiver em pausa ou no menu principal ... para anexá-la ao estado do jogo!

State* gameState = new State();
gameState->addScene(scene);

Agora o seu estado do jogo tem uma cena. Em seguida, você deseja executar a lógica na cena e renderizar a cena. Para a lógica, você acabou de executar uma função de atualização.

State::update(double delta) {
    scene->update(delta);
}

Dessa forma, você pode manter toda a lógica do jogo na Sceneclasse. E apenas por uma questão de referência, um sistema de componentes de entidade pode fazê-lo assim:

State::update(double delta) {
    physicsSystem->applyPhysics(scene);
}

De qualquer forma, agora você conseguiu atualizar sua cena. Agora você deseja exibi-lo! Para o qual fazemos algo semelhante ao acima:

State::render() {
    renderSystem->render(scene);
}

Ai está. O renderSystem lê as informações da cena e exibe a imagem apropriada. Simplificado, o método para renderizar a cena pode ser assim:

RenderSystem::renderScene(Scene* scene) {
    Camera* camera = scene->camera;
    lookAt(camera); // Set up the appropriate viewing matrices based on 
                    // the camera location and direction

    renderHeightmap(scene->getWorld()->getHeightMap()); // Just as an example, you might
                                                        // use a height map as your world
                                                        // representation.
    renderModel(scene->getPlayer()->getType()); // getType() will return, for example "orc"
                                                // or "human"

    renderModel(scene->getBoss()->getType());
}

Realmente simplificado, você ainda precisará, por exemplo, aplicar uma rotação e tradução com base em onde seu jogador está e onde ele está. (Meu exemplo é um jogo em 3D, se você for em 2D, será um passeio no parque).

Espero que isto seja o que você estava procurando? Como você pode se lembrar do exposto acima, o sistema de renderização não se importa com a lógica do jogo . Ele usa apenas o estado atual da cena para renderizar, ou seja, extrai as informações necessárias para renderizar. E a lógica do jogo? Não se importa com o que o renderizador faz. Caramba, não importa se é exibido!

E você também não precisa anexar informações de renderização à cena. Deve ser suficiente que o renderizador saiba que precisa renderizar um orc. Você já carregou um modelo orc, que o renderizador sabe exibir.

Isso deve satisfazer seus requisitos. A representação gráfica e a lógica são acopladas , porque ambas usam os mesmos dados. No entanto, eles são separados , porque nenhum deles depende do outro!

EDIT: E apenas para responder por que alguém faria assim? Porque é mais fácil é a razão mais simples. Você não precisa pensar em "tal e tal aconteceu, agora devo atualizar os gráficos". Em vez disso, você faz as coisas acontecerem, e cada quadro do jogo examina o que está acontecendo no momento e o interpreta de alguma forma, fornecendo um resultado na tela.

Culpa
fonte
7

Seu título faz uma pergunta diferente do conteúdo do seu corpo. No título, você pergunta por que lógica e renderização devem ser separadas, mas no corpo solicita implementações de sistemas de lógica / gráficos / renderização.

A segunda questão já foi abordada anteriormente , portanto, vou me concentrar na primeira.

Razões para separar a lógica e a renderização:

  1. A noção amplamente aceita de que os objetos devem fazer uma coisa
  2. E se você quiser ir de 2D para 3D? E se você decidir mudar de um sistema de renderização para outro no meio do projeto? Você não deseja rastrear todo o seu código e fazer grandes mudanças no meio da lógica do jogo.
  3. Você provavelmente teria motivos para repetir seções do código, o que geralmente é considerado uma má ideia.
  4. Você pode criar sistemas para controlar faixas potencialmente enormes de renderização ou lógica sem se comunicar individualmente com pequenos pedaços.
  5. E se você quiser atribuir uma gema a um jogador, mas o sistema estiver lento por quantas facetas a gema possui? Se você abstraiu seu sistema de renderização suficientemente bem, você pode atualizá-lo em taxas diferentes para contabilizar operações caras de renderização.
  6. Ele permite que você pense sobre coisas que realmente importam para o que você está fazendo. Por que enrolar seu cérebro em torno de transformações matriciais, desvios de sprites e coordenadas de tela quando tudo que você quer fazer é implementar um mecânico de salto duplo, comprar uma carta ou equipar uma espada? Você não quer que o sprite represente sua renderização de espada equipada em rosa brilhante apenas porque você queria movê-la da sua mão direita para a esquerda.

Em uma configuração de POO, instanciar novos objetos tem um custo, mas, na minha experiência, o custo para os recursos do sistema é um preço pequeno a pagar pela capacidade de pensar e implementar as coisas específicas que preciso executar.

Dragonsdoom
fonte
6

Essa resposta é apenas para criar uma intuição de por que é importante separar renderização e lógica, em vez de sugerir diretamente exemplos práticos.

Vamos supor que temos um elefante grande , ninguém na sala pode ver o elefante inteiro. talvez todo mundo discorde do que realmente é. Porque todo mundo vê uma parte diferente do elefante e só pode lidar com essa parte. Mas no final, isso não muda o fato de ser um grande elefante.

O elefante representa o objeto do jogo com todos os seus detalhes. Mas ninguém realmente precisa saber tudo sobre o elefante (objeto de jogo) para poder fazer sua funcionalidade.

Acoplar a lógica do jogo e a renderização é como fazer todo mundo ver o elefante inteiro. Se algo mudou, todo mundo precisa saber. Embora na maioria dos casos, eles só precisem ver a parte na qual estão interessados. Se algo mudou a pessoa que o conhece, precisa apenas contar à outra pessoa sobre o resultado dessa mudança, é isso que é importante apenas para ela. (pense nisso como comunicação via mensagens ou interfaces).

insira a descrição da imagem aqui

Os pontos que você mencionou não são desvantagens, são apenas desvantagens se houver mais dependências do que deveriam existir no mecanismo; em outras palavras, os sistemas veem partes do elefante mais do que deveriam. E isso significa que o mecanismo não foi "corretamente" projetado.

Você só precisa sincronizar com sua definição formal se estiver usando um mecanismo multithread em que ele coloque a lógica e a renderização em dois threads diferentes, e mesmo um mecanismo que precise de muita sincronização entre sistemas não seja especialmente projetado.

Caso contrário, a maneira natural de lidar com esse caso é projetar o sistema como entrada / saída. A atualização faz a lógica e gera o resultado. A renderização é alimentada apenas com os resultados da atualização. Você realmente não precisa expor tudo. Você expõe apenas uma interface que se comunica entre os dois estágios. A comunicação entre diferentes partes do mecanismo deve ser via abstrações (interfaces) e / ou mensagens. Nenhuma lógica ou estados internos devem ser expostos.

Vamos dar um exemplo simples de gráfico de cena para explicar a ideia.

A atualização geralmente é feita através de um único loop chamado loop do jogo (ou possivelmente através de vários loops de jogos, cada um rodando em um thread separado). Uma vez que o loop atualizou o objeto de qualquer jogo. Ele só precisa dizer por meio de mensagens ou interfaces que os objetos 1 e 2 foram atualizados e alimentá-lo com a transformação final.

O sistema de renderização leva apenas a transformação final e não sabe o que realmente mudou sobre o objeto (por exemplo, colisão específica aconteceu etc). Agora, para renderizar esse objeto, ele precisa apenas do ID desse objeto e da transformação final. Depois disso, o renderizador alimentará a API de renderização com a malha e a transformação final sem saber mais nada.

concept3d
fonte