Separação de desenho e lógica em jogos


11

Eu sou um desenvolvedor que agora está começando a mexer com o desenvolvimento de jogos. Eu sou um cara .Net, então eu brinquei com o XNA e agora estou brincando com o Cocos2d para o iPhone. Minha pergunta é realmente mais geral.

Digamos que estou construindo um jogo simples de Pong. Eu teria uma Ballaula e uma Paddleaula. Vindo do desenvolvimento do mundo dos negócios, meu primeiro instinto é não ter nenhum desenho ou código de manipulação de entrada em nenhuma dessas classes.

//pseudo code
class Ball
{
   Vector2D position;
   Vector2D velocity;
   Color color;
   void Move(){}
}

Nada na classe bola lida com entrada ou lida com desenho. Eu teria outra classe, a minha Gameclasse ou a minha Scene.m(no Cocos2d), que renovaria a bola e, durante o ciclo do jogo, manipularia a bola conforme necessário.

A questão é que, em muitos tutoriais para XNA e Cocos2d, vejo um padrão como este:

//pseudo code
class Ball : SomeUpdatableComponent
{
   Vector2D position;
   Vector2D velocity;
   Color color;
   void Update(){}
   void Draw(){}
   void HandleInput(){}
}

Minha pergunta é: isso está certo? Esse é o padrão que as pessoas usam no desenvolvimento de jogos? De alguma forma, isso vai contra tudo o que estou acostumado, ter minha aula de Ball fazendo tudo. Além disso, neste segundo exemplo, onde meu Ballsabe como se mover, como eu lidaria com a detecção de colisão com o Paddle? A Ballnecessidade de ter conhecimento do Paddle? No meu primeiro exemplo, a Gameclasse teria referências ao Balle ao e Paddle, em seguida , enviaria ambos para algum CollisionDetectiongerente ou algo assim, mas como faço para lidar com a complexidade de vários componentes, se cada componente individual faz tudo sozinho? (Espero estar fazendo sentido ...)


2
Infelizmente é assim que é feito em muitos jogos. No entanto, eu não consideraria isso uma boa prática. Muitos dos tutoriais que você está lendo podem ser voltados para iniciantes e, portanto, não começarão a explicar padrões de modelo / exibição / controlador ou jugo para o leitor.
18711

@tenpn, seu comentário parece sugerir que você não acha que isso seja uma boa prática, imaginei se você poderia explicar melhor. Eu sempre pensei que era o arranjo mais adequado devido aos métodos que contêm código que provavelmente é muito específico para cada tipo; mas tenho pouca experiência, por isso estaria interessado em ouvir seus pensamentos.
sebf

1
@sebf não funciona se você gosta de design orientado a dados, e não funciona se você gosta de design orientado a objetos. Veja os princípios SOLID do tio Bob: butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
tenpn

@tenpn. Esse artigo e documento vinculado são muito interessantes, obrigado!
sebf

Respostas:


10

Minha pergunta é: isso está certo? Esse é o padrão que as pessoas usam no desenvolvimento de jogos?

Os desenvolvedores de jogos tendem a usar o que funciona. Não é rigoroso, mas ei, ele é enviado.

De alguma forma, isso vai contra tudo o que estou acostumado, ter minha aula de Ball fazendo tudo.

Portanto, um idioma mais generalizado para o que você tem lá é handleMessages / update / draw. Em sistemas que fazem uso intenso de mensagens (que tem prós e contras como seria de esperar), uma entidade do jogo pega as mensagens com as quais se importa, executa lógica nessas mensagens e depois se desenha.

Observe que essa chamada draw () pode não significar realmente que a entidade chama putPixel ou o que quer que seja dentro de si; em alguns casos, isso pode significar apenas a atualização de dados posteriormente pesquisados ​​pelo código responsável pelo desenho. Em casos mais avançados, ele "se desenha" chamando métodos expostos por um renderizador. Na tecnologia em que estou trabalhando agora, o objeto se desenha usando chamadas de renderizador e, em cada quadro, o thread de renderização organiza todas as chamadas feitas por todos os objetos, otimiza para coisas como alterações de estado e depois desenha a coisa toda (todas disso acontecendo em um thread separado da lógica do jogo, para obter uma renderização assíncrona).

A delegação de responsabilidade depende do programador; para um jogo simples de pong, faz sentido que cada objeto lide com seu próprio desenho (completo com chamadas de baixo nível) completamente. É uma questão de estilo e sabedoria.

Além disso, neste segundo exemplo, onde minha bola sabe como se mover, como eu lidaria com a detecção de colisões com o Paddle? A bola precisaria ter conhecimento da raquete?

Portanto, uma maneira natural de resolver o problema aqui é fazer com que cada objeto tome todos os outros objetos como algum tipo de parâmetro para verificar colisões. Essa escala é ruim e pode ter resultados estranhos.

Fazer com que cada classe implemente algum tipo de interface Collidable e, em seguida, tenha um código de alto nível que apenas circula todos os objetos Collidable na fase de atualização do seu jogo e lida com colisões, definindo as posições dos objetos conforme necessário.

Novamente, você apenas precisa desenvolver um sentido (ou decidir) sobre como a responsabilidade por coisas diferentes em seu jogo precisa ser delegada. Renderizar é uma atividade solitária e pode ser manipulada por um objeto no vácuo; colisão e física geralmente não podem.

No meu primeiro exemplo, a classe Game teria referências à bola e à raquete e depois as enviaria para algum gerente de CollisionDetection ou algo assim, mas como lidar com a complexidade de vários componentes, se cada componente individual tudo eles mesmos?

Veja o acima para uma exploração disso. Se você está em uma linguagem que suporta interfaces facilmente (por exemplo, Java, C #), isso é fácil. Se você estiver em C ++, isso pode ser ... interessante. Sou a favor do uso de mensagens para resolver esses problemas (as classes lidam com mensagens que podem, ignoram outras), algumas outras pessoas, como a composição de componentes.

Novamente, o que funcionar e for mais fácil para você.


2
Ah, e lembre-se sempre: YAGNI (você não vai precisar disso) é realmente importante aqui. Você pode perder muito tempo tentando criar o sistema flexível ideal para lidar com todos os problemas possíveis e nunca fazer nada. Quero dizer, se você realmente quisesse um bom backbone multiplayer para todos os resultados possíveis, usaria o CORBA, certo? :)
ChrisE 17/02

5

Recentemente, fiz um jogo simples do Space Invadors usando um 'sistema de entidades'. É um padrão que separa atributos e comportamentos extremamente bem. Levei algumas iterações para entendê-lo completamente, mas depois que alguns componentes foram projetados, torna-se extremamente simples compor novos objetos usando os componentes existentes.

Você deve ler isto:

http://t-machine.org/index.php/2007/09/03/entity-systems-are-the-future-of-mmog-development-part-1/

É atualizado com freqüência por um cara extremamente qualificado. É também a única discussão do sistema de entidades com exemplos concretos de código.

Minhas iterações foram da seguinte maneira:

A primeira iteração tinha um objeto "EntitySystem" que era como Adam descreve; no entanto, meus componentes ainda tinham métodos - meu componente 'renderizável' tinha um método paint () e meu componente de posição tinha um método move () e etc. Quando comecei a detalhar as entidades, percebi que precisava começar a passar a mensagem entre componentes e ordenando a execução de atualizações de componentes .... muito confuso.

Então, voltei e reli o blog da T-machines. Há muitas informações nos tópicos de comentários - e neles ele realmente enfatiza que os componentes não têm comportamentos - comportamentos são fornecidos pelos sistemas da entidade. Dessa forma, você não precisa passar mensagens entre componentes e atualizar as atualizações dos componentes porque a ordem é determinada pela ordem global de execução do sistema. Está bem. Talvez isso seja abstrato demais.

Enfim, para a iteração 2, é isso que eu colhi no blog:

EntityManager - atua como o componente "banco de dados", que pode ser consultado por entidades que contêm certos tipos de componentes. Isso pode até ser apoiado por um banco de dados na memória para acesso rápido ... consulte a parte 5 da t-machine para obter mais informações.

EntitySystem - Cada sistema é essencialmente apenas um método que opera em um conjunto de entidades. Cada sistema usará os componentes x, ye z de uma entidade para realizar seu trabalho. Portanto, você consultaria o gerente sobre entidades com os componentes x, ye z, em seguida, passaria esse resultado para o sistema.

Entidade - apenas um ID, como um longo. A entidade é o que agrupa um conjunto de instâncias de componentes em uma 'entidade'.

Componente - um conjunto de campos .... sem comportamentos! quando você começa a adicionar comportamentos, começa a ficar bagunçado ... mesmo em um simples jogo Space Invadors.

Edit : a propósito, 'dt' é o tempo delta desde a última invocação do loop principal

Então, meu loop principal dos Invadors é o seguinte:

Collection<Entity> entitiesWithGuns = manager.getEntitiesWith(Gun.class);
Collection<Entity> entitiesWithDamagable =
manager.getEntitiesWith(BulletDamagable.class);
Collection<Entity> entitiesWithInvadorDamagable = manager.getEntitiesWith(InvadorDamagable.class);

keyboardShipControllerSystem.update(entitiesWithGuns, dt);
touchInputSystem.update(entitiesWithGuns, dt);
Collection<Entity> entitiesWithInvadorMovement = manager.getEntitiesWith(InvadorMovement.class);
invadorMovementSystem.update(entitiesWithInvadorMovement);

Collection<Entity> entitiesWithVelocity = manager.getEntitiesWith(Velocity.class);
movementSystem.move(entitiesWithVelocity, dt);
gunSystem.update(entitiesWithGuns, System.currentTimeMillis());
Collection<Entity> entitiesWithPositionAndForm = manager.getEntitiesWith(Position.class, Form.class);
collisionSystem.checkCollisions(entitiesWithPositionAndForm);

Parece um pouco estranho no começo, mas é incrivelmente flexível. Também é muito fácil de otimizar; para diferentes tipos de componentes, você pode ter diferentes datastores de backup para acelerar a recuperação. Para a classe 'form', você pode apoiá-la com um quadtree para acelerar o acesso à detecção de colisão.

Eu sou como você; Sou um desenvolvedor experiente, mas não tive experiência em escrever jogos. Passei algum tempo pesquisando padrões de desenvolvimento, e este chamou minha atenção. Não é de forma alguma a única maneira de fazer as coisas, mas eu achei muito intuitivo e robusto. Acredito que o padrão foi discutido oficialmente no livro 6 da série "Game Programming Gems" - http://www.amazon.com/Game-Programming-Gems/dp/1584500492 . Eu não li nenhum dos livros, mas soube que eles são a referência de fato para a programação de jogos.


1

Não há regras explícitas que você deve seguir ao programar jogos. Acho os dois padrões perfeitamente aceitáveis, desde que você siga o mesmo padrão de design em todo o jogo. Você ficaria surpreso com o fato de existirem muitos outros padrões de design para jogos, muitos dos quais nem estão no topo da OOP.

Agora, de acordo com minha preferência pessoal, prefiro separar código por comportamento (uma classe / thread para desenhar, outra para atualizar), enquanto possuo objetos minimalistas. Acho mais fácil fazer um paralelismo e, muitas vezes, trabalho em menos arquivos ao mesmo tempo. Eu diria que essa é mais uma maneira processual de fazer as coisas.

Mas se você é do mundo dos negócios, provavelmente está mais à vontade para escrever aulas que sabem fazer tudo por si mesmas.

Mais uma vez, recomendo que você escreva o que considera mais natural para você.


Eu acho que você não entendeu minha pergunta. Estou dizendo que não gosto de ter aulas que fazem tudo sozinhas. I caiu como uma bola deve ser apenas uma representação lógica de uma bola, no entanto, ele deve saber nada sobre o loop do jogo, a manipulação de entrada etc ...
BFree

Para ser justo, na programação de negócios, se você resumir, existem dois trens: objetos de negócios que carregam, persistem e executam a lógica por si mesmos, e DTO + AppLogic + Camada de Repositório / Persitência ...
Nate

1

o objeto 'Bola' possui atributos e comportamentos. Acho que faz sentido incluir os atributos e comportamentos exclusivos do objeto em sua própria classe. A maneira pela qual um objeto Ball é atualizado é diferente da atualização de uma raquete, portanto, faz sentido que esses sejam comportamentos únicos que justifiquem a inclusão na classe. O mesmo com o desenho. Muitas vezes, existem diferenças únicas na maneira como uma raquete é desenhada contra uma bola e acho mais fácil modificar o método de desenho de um objeto individual para atender a essas necessidades do que realizar avaliações condicionais em outra classe para resolvê-las.

Pong é um jogo relativamente simples em termos de número de objetos e comportamentos, mas quando você se depara com dezenas ou centenas de objetos de jogos exclusivos, pode achar seu segundo exemplo um pouco mais fácil de modularizar tudo.

A maioria das pessoas usa uma classe de entrada cujos resultados estão disponíveis para todos os objetos do jogo por meio de uma classe Service ou estática.


0

Acho que o que você provavelmente está acostumado no mundo dos negócios é separar abstração e implementação.

No mundo do desenvolvimento de jogos, você geralmente quer pensar em termos de velocidade. Quanto mais objetos você tiver, mais opções de contexto ocorrerão, o que pode desacelerar as coisas, dependendo da carga atual.

Esta é apenas a minha especulação, já que sou um desenvolvedor de jogos, mas um desenvolvedor professonial.


0

Não, não é 'certo' ou 'errado', é apenas uma simplificação.

Algum código comercial é exatamente o mesmo, a menos que você esteja trabalhando com um sistema que separa forçosamente a apresentação e a lógica.

Aqui, um exemplo do VB.NET, em que "lógica de negócios" (adicionando um número) é gravada em um manipulador de eventos da GUI - http://www.homeandlearn.co.uk/net/nets1p12.html

como faço para lidar com a complexidade de vários componentes, se cada componente individual faz tudo sozinho?

Se e quando o assunto for complexo, você pode fatorá-lo.

class Ball
{
    GraphicalModel ballVisual;
    void Draw()
    {
        ballVisual.Draw();
    }
};

0

Pela minha experiência em desenvolvimento, não há maneira certa ou errada de fazer as coisas, a menos que haja uma prática aceita no grupo com o qual você trabalha. Encontrei oposição de desenvolvedores que são avessos a separar comportamentos, skins etc. E tenho certeza de que eles dirão o mesmo sobre minha reserva de escrever uma classe que inclua tudo sob o sol. Lembre-se de que você não apenas deve escrevê-lo, mas também pode ler seu código amanhã e em uma data futura, depurá-lo e possivelmente desenvolver na próxima iteração. Você deve escolher um caminho que funcione para você (e equipe). Escolher uma rota que outras pessoas usem (e é contra-intuitivo para você) o atrasará.

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.