Quais são algumas maneiras de separar a lógica do jogo das animações e do loop de empate?

9

Eu criei apenas jogos em flash anteriormente, usando MovieClips para separar minhas animações da lógica do jogo. Agora estou tentando fazer um jogo para Android, mas a teoria da programação de jogos em torno da separação dessas coisas ainda me confunde. Eu venho de um histórico de desenvolvimento de aplicativos da web que não são de jogos, por isso sou versado em mais padrões de MVC e fico preso nessa mentalidade ao abordar a programação de jogos.

Quero fazer coisas como abstrair meu jogo, tendo, por exemplo, uma classe de tabuleiro de jogo que contém os dados para uma grade de blocos com instâncias de uma classe de blocos que contêm propriedades. Posso dar acesso ao meu loop de desenho e fazer com que ele desenhe o tabuleiro de jogo com base nas propriedades de cada bloco no tabuleiro, mas não entendo para onde exatamente a animação deve ir. Até onde eu sei, a animação fica entre a lógica abstraída do jogo (modelo) e o loop de empate (exibição). Com minha mentalidade MVC, é frustrante tentar decidir para onde a animação deve ir. Teria um bocado de dados associados a ele como um modelo, mas aparentemente precisa estar intimamente associado ao loop draw para ter coisas como animação independente de quadro.

Como posso sair dessa mentalidade e começar a pensar em padrões que fazem mais sentido para os jogos?

TMV
fonte

Respostas:

6

A animação ainda pode ser perfeitamente dividida entre lógica e renderização. O estado de dados abstratos da animação seria a informação necessária para a API de gráficos renderizar a animação.

Em jogos 2D, por exemplo, pode ser uma área retangular que marca a área que exibe a parte atual da sua planilha de sprite que precisa ser desenhada (quando você tem uma planilha composta por, digamos, 30 desenhos 80x80 contendo as várias etapas do seu personagem pulando, sentando, movendo etc.). Também pode ser qualquer tipo de dados que você não precise renderizar, mas talvez para gerenciar os estados da animação, como o tempo restante até a etapa atual da animação expirar ou o nome da animação ("walking", "standing" etc.) Tudo isso pode ser representado da maneira que você desejar. Essa é a parte da lógica.

Na parte de renderização, você faz como de costume, obtém esse retângulo do seu modelo e usa seu renderizador para realmente fazer as chamadas para a API de gráficos.

No código (usando a sintaxe C ++ aqui):

class Sprite //Model
{
    private:
       Rectangle subrect;
       Vector2f position;
       //etc.

    public:
       Rectangle GetSubrect() 
       {
           return subrect;
       }
       //etc.
};

class AnimatedSprite : public Sprite, public Updatable //arbitrary interface for classes that need to change their state on a regular basis
{
    AnimationController animation_controller;
    //etc.
    public:
        void Update()
        {
            animation_controller.Update(); //Good OOP design ;) It will take control of changing animations in time etc. for you
            this.SetSubrect(animation_controller.GetCurrentAnimation().GetRect());
        }
        //etc.
};

Esses são os dados. Seu renderizador pegará esses dados e os desenhará. Como os Sprites normais e os animados são desenhados da mesma maneira, você pode usar a polimorfia aqui!

class Renderer
{
    //etc.
    public:
       void Draw(const Sprite &spr)
       {
           graphics_api_pointer->Draw(spr.GetAllTheDataThatINeed());
       }
};

TMV:

Eu vim com outro exemplo. Digamos que você tenha um RPG. Seu modelo que representa o mapa do mundo, por exemplo, provavelmente precisaria armazenar a posição do personagem no mundo como coordenadas de bloco no mapa. No entanto, quando você move o personagem, eles caminham alguns pixels por vez para o próximo quadrado. Você armazena essa posição "entre blocos" em um objeto de animação? Como você atualiza o modelo quando o personagem finalmente "chega" à próxima coordenada do bloco no mapa?

O mapa-múndi não sabe diretamente a posição dos jogadores (não possui um Vector2f ou algo parecido que armazena diretamente a posição dos jogadores =; em vez disso, ele tem uma referência direta ao próprio objeto do jogador, que por sua vez deriva do AnimatedSprite para que você possa transmiti-lo facilmente ao renderizador e obter todos os dados necessários.

No entanto, em geral, seu mapa de tile não deve ser capaz de fazer tudo - eu teria uma classe "TileMap" que cuida do gerenciamento de todos os tiles, e talvez ele também detecte colisão entre os objetos que eu entrego a ele e as peças no mapa. Então, eu teria outra classe "RPGMap", ou como você gostaria de chamá-la, que tem uma referência ao seu mapa de blocos e a referência ao jogador e faz as chamadas Update () reais para o seu jogador e para o seu tilemap.

Como você deseja atualizar o modelo quando o player se move depende do que você deseja fazer.

É permitido ao seu jogador mover-se entre blocos de forma independente (estilo Zelda)? Basta manipular a entrada e mover o reprodutor de acordo com cada quadro. Ou você quer que o jogador pressione "para a direita" e seu personagem move automaticamente uma peça para a direita? Deixe sua classe RPGMap interpolar a posição dos jogadores até que ele chegue ao seu destino e, enquanto isso, bloqueie todo o manuseio das teclas de movimento.

De qualquer forma, se você quiser facilitar as coisas, todos os seus modelos terão os métodos Update () se eles realmente precisarem de alguma lógica para se atualizar (em vez de apenas alterar os valores das variáveis) - Você não abre mão do controlador no padrão MVC dessa maneira, basta mover o código de "um passo acima" (o controlador) para o modelo, e tudo o que o controlador faz é chamar esse método Update () do modelo (o controlador no nosso caso seria o RPGMap). Ainda é possível trocar facilmente o código de lógica - você pode alterar diretamente o código da classe ou, se precisar de um comportamento completamente diferente, pode derivar da classe de modelo e substituir apenas o método Update ().

Essa abordagem reduz muito as chamadas de método e coisas assim - que costumava ser uma das principais desvantagens do padrão MVC puro (você acaba chamando GetThis () GetThat () com muita frequência) - torna o código mais longo e mais um pouco mais difícil de ler e também mais lento - embora isso possa ser resolvido pelo seu compilador, que otimiza muitas coisas assim.

TravisG
fonte
Você manteria os dados da animação na classe que contém a lógica do jogo, na classe que contém o loop do jogo ou separaria os dois? Além disso, cabe inteiramente ao loop ou à classe que o contém entender como converter dados de animação para desenhar a tela, certo? Muitas vezes, não seria tão simples quanto obter um retângulo que representa uma seção da folha de sprite e usá-lo para cortar um desenho de bitmap da folha de sprite.
TMV
Eu vim com outro exemplo. Digamos que você tenha um RPG. Seu modelo que representa o mapa do mundo, por exemplo, provavelmente precisaria armazenar a posição do personagem no mundo como coordenadas de bloco no mapa. No entanto, quando você move o personagem, eles caminham alguns pixels por vez para o próximo quadrado. Você armazena essa posição "entre blocos" em um objeto de animação? Como você atualiza o modelo quando o personagem finalmente "chega" à próxima coordenada do bloco no mapa?
TMV
Editei na resposta à sua pergunta, pois os comentários não permitem caracteres suficientes para isso.
TravisG
Se eu entendi tudo corretamente:
TMV
Você poderia ter uma instância de uma classe "Animator" dentro da sua View, e ela teria um método público "update" que é chamado de cada quadro pela view. O método update chama os métodos "update" de instâncias de vários tipos de objetos de animação individuais dentro dele. O animador e as animações dentro dele têm uma referência ao Modelo (transmitido por meio de seus construtores) para que eles possam atualizar os dados do modelo se uma animação o alterar. Em seguida, no loop de desenho, você obtém dados das animações dentro do animador de uma maneira que possa ser entendida pela Visualização e desenhada.
TMV
2

Posso desenvolver isso, se desejar, mas tenho um renderizador central que é instruído a desenhar no loop. Ao invés de

handle input

for every entity:
    update entity

for every entity:
    draw entity

Eu tenho um sistema mais parecido

handle input (well, update the state. Mine is event driven so this is null)

for every entity:
    update entity //still got game logic here

renderer.draw();

A classe renderer simplesmente contém uma lista de referências a componentes desenháveis ​​de objetos. Eles são atribuídos nos construtores por simplicidade.

Para o seu exemplo, eu teria uma classe GameBoard com vários Tiles. Obviamente, cada ladrilho conhece sua posição e presumo algum tipo de animação. Fatore isso em algum tipo de classe de Animação que o bloco possua e faça com que ele passe uma referência para uma classe Renderer. Lá, todos separados. Quando você atualiza o Tile, ele chama Atualização na animação ... ou atualiza ele próprio. Quando Renderer.Draw()é chamado, desenha a animação.

A animação independente de quadro não deve ter muito a ver com o loop de desenho.

O Pato Comunista
fonte
0

Ultimamente, tenho aprendido os paradigmas, por isso, se esta resposta estiver incompleta, tenho certeza que alguém irá adicionar a ela.

A metodologia que parece fazer mais sentido para o design de jogos é separar a lógica da saída da tela.

Na maioria dos casos, você gostaria de usar uma abordagem multithread, se você não estiver familiarizado com esse tópico, é uma pergunta própria, aqui está a cartilha do wiki . Essencialmente, você deseja que sua lógica de jogo seja executada em um thread, bloqueando variáveis ​​que ele precisa acessar para garantir a integridade dos dados. Se o seu loop lógico é incrivelmente rápido (super mega pong 3d animado?), Você pode tentar fixar a frequência que o loop executa, adormecendo o thread por pequenas durações (120 hz foi sugerido neste fórum para loops de física de jogos). Simultaneamente, o outro encadeamento está redesenhando a tela (60 hz foi sugerido em outros tópicos) com as variáveis ​​atualizadas, novamente solicitando um bloqueio nas variáveis ​​antes de acessá-las.

Nesse caso, animações ou transições, etc., entram no segmento de desenho, mas você deve sinalizar através de uma bandeira de algum tipo (talvez uma variável de estado global) que o segmento de lógica do jogo não deve fazer nada (ou fazer algo diferente ... configure o novos parâmetros do mapa, talvez).

Depois de entender a concorrência, o resto é bastante compreensível. Se você não tem experiência com concorrência, sugiro fortemente que você escreva alguns programas de teste simples para entender como o fluxo acontece.

Espero que isto ajude :)

[edit] Em sistemas que não suportam multithreading, a animação ainda pode entrar no loop de desenho, mas você deseja definir o estado de forma a sinalizar para a lógica que algo diferente está ocorrendo e não continuar processando o nível atual / mapa / etc ...

Stephen
fonte
11
Eu discordo aqui. Na maioria dos casos, você não deseja multi-thread, principalmente se for um jogo pequeno.
The Duck Comunista
@TheCommunistDuck De maneira justa, a sobrecarga e a complexidade do multithreading podem definidamente torná-lo um exagero, além disso, se o jogo for pequeno, ele poderá ser atualizado rapidamente.
Stephen