Ponto V!

Home Arquitetura Programação Animação baseada em tempo
Paulo V. W. Radtke
Animação baseada em tempoImprimir
Escrito por Paulo Vinicius Wolski Radtke

Nos “velhos tempos” as animações dos jogos eram controladas utilizando o retraço vertical do vídeo e o número máximo de atualizações por segundo, que era fixo em todos os hardwares. Nos sistemas atuais, nem sempre é possível fazer isto e, mesmo quando isso acontece, não sabemos exatamente qual a taxa de atualização configurada pelo usuário. Este artigo apresenta uma técnica simples que pode ser aplicada facilmente em lógica de animações 2D e 3D para controlar elementos na tela. Um exemplo simples em SDL ilustra a técnica discutida.

Conceito

Todo objeto animado percorre na tela uma certa distância – seja ela em distância linear ou angular. Na física a velocidade na qual um objeto se desloca é calculada pela equação clip_image002 , aonde d é a distância percorrida pelo objeto entre dois pontos – A e B, t é o tempo utilizado e v é a velocidade resultante. Supondo um exemplo no espaço 2D, se um objeto percorre 100 pixels a cada segundo, temos uma velocidade v=100.

clip_image002

Não sabemos a quantos quadros por segundo o nosso jogo rodará, mas temos como medir o tempo passado entre o quadro anterior e o quadro atual. Com esta informação e a velocidade, podemos calcular o deslocamento do objeto utilizando a equaçãoclip_image002[5]. Se no nosso exemplo tivessem passados 0.1 segundos, o deslocamento do objeto seria clip_image004pixels.

Código exemplo

Vamos analisar um exemplo em código que demonstre o efeito. Por simplicidade, o exemplo em C usa SDL e pode ser compilado sem problemas em todas as plataformas que aceitem a SDL. O código base é como mostrado na listagem inicial (inicial.c), a função main carrega uma imagem bitmap e invoca a função demo para fazer com que a imagem atravesse a tela na horizontal de um lado a outro. Você pode descarregar os códigos fontes e o arquivo da imagem utilizado, porém qualquer arquivo BMP pequeno (razoavelmente menor que a tela, de preferência) serve para testar o código.

#include <stdlib.h>

#if defined(linux)
    #include <SDL/SDL.h>
#else
    #include <SDL.h>
#endif

#define LARGURA 800
#define ALTURA 600

SDL_Surface *imagem, *tela;    // Ponteiro para o bitmap

void demo();

void demo()
{
    SDL_Rect destino;
    double x=0;
    // Preencha a parte fixa do rect
    destino.y = (ALTURA-imagem->h)/2;
    destino.w = imagem->w;
    destino.h = imagem->h;

    while(x<LARGURA)
    {     
        // Limpa a tela
        SDL_FillRect(tela, NULL, SDL_MapRGB(tela->format, 255, 255, 255));
        //  Em que posicao x desenha a imagem
        destino.x = (int)x++;
        SDL_BlitSurface(imagem, NULL, tela, &destino);
        SDL_UpdateRect(tela, 0, 0, 0, 0);
    }
}

int main(int argc, char *argv[])
{
    // Inicializa a SDL
    if ( SDL_Init(SDL_INIT_AUDIO|SDL_INIT_VIDEO) < 0 ) {
        fprintf(stderr, "Nao consegui inicializar a SDL: %s\n", SDL_GetError());
        exit(1);
    }
    atexit(SDL_Quit);

    tela = SDL_SetVideoMode(LARGURA, ALTURA, 32, SDL_SWSURFACE);
    if ( tela == NULL ) 
    {
        fprintf(stderr, "Falhou inicializacao do video: %s\n", SDL_GetError());
        exit(1);
    }
    // Carrega um bitmap (global)
    imagem = SDL_LoadBMP("imagem.bmp");
    if ( imagem == NULL ) 
    {
        fprintf(stderr, "Nao achei o arquivo %s: %s\n", "imagem.bmp", SDL_GetError());
        exit(1);
    }

    demo();
    // Apaga o bitmap
    SDL_FreeSurface(imagem);    
    exit(0);    
}

A cada vez que a tela é atualizada a imagem é desenhada um ponto à direita em relação à posição anterior, até que ela saia da tela. Esse código não é um primor da programação organizada (nem mesmo permite interromper o programa no meio), mas isola bem na função demo a animação da imagem e evita o uso de buffer duplo que poderia ativar a espera pelo retraço vertical (isto é, se ele estiver ligado no sistema). Mais tarde discutiremos por que utilizamos uma variável double para a posição x da imagem mesmo quando os pixels são sempre valores inteiros.

O problema desse exemplo é que num jogo real o processamento requerido para desenhar um quadro pode variar de acordo com o número de objetos desenhados na tela, overhead da lógica de inteligência artificial e até mesmo programas que estejam rodando em background no sistema. Em jogos que controlam a animação pelo retraço vertical isso gera os conhecidos slow downs. Experimente colocar no final da repetição do while o código SDL_Delay(50); para simular um processamento mais lento e veja que o objeto leva mais tempo para atravessar a tela.

while(x<LARGURA)
{     
   // Limpa a tela
   SDL_FillRect(tela, NULL, SDL_MapRGB(tela->format, 255, 255, 255));
   //  Em que posicao x desenha a imagem
   destino.x = (int)x++;
   SDL_BlitSurface(imagem, NULL, tela, &destino);
   SDL_UpdateRect(tela, 0, 0, 0, 0);
   SDL_Delay(50);
}

Animação baseada em tempo

O exemplo inicial desloca a imagem um pixel para a direita a cada atualização, porém sabemos que como medida de velocidade isto é ineficiente. Precisamos relacionar a animação ao tempo passado. Vamos supor que a imagem deva atravessar a tela em 4 segundos. Sabendo que a tela tem uma largura (distância entre os extremos opostos) de 800 pontos e usando a equação da velocidade mostrada anteriormente, calculamos a velocidade como 200 pixels/segundo, ou seja, esperamos que a cada segundo passado a imagem desloque-se 200 pontos (vamos usar uma conta com um define para facilitar).

A SDL, como outras bibliotecas multimídia, disponibiliza a função SDL_GetTicks() que retorna a quantidade de milisegundos passados entre a inicialização do sistema e o momento de execução da função, o número de ticks. Se esta função for chamada a cada atualização de tela, sabemos quantos milisegundos se passaram da inicialização do sistema até o início daquele quadro. Se utilizarmos uma lógica adequada com duas variáveis, podemos determinar quantos milisegundos se passaram entre o início do quadro atual e o anterior utilizando estes valores. Esse valor dividido por 1000 é o intervalo de tempo passado em segundos entre os dois quadros, que multiplicado pela velocidade nos dá o deslocamento da imagem em relação ao quadro anterior.

A listagem temporizado.c mostra o código que anima a imagem adequadamente. Criamos três variáveis novas, atual, anterior e tempo, que armazenam respectivamente o número de ticks no início do quadro anterior, no quadro atual e o tempo em milisegundos entre os quadros. A velocidade de deslocamento é um define chamado VELOCIDADE, você pode alterá-lo para deixar a animação mais rápida ou lenta. A inicialização da variável anterior é feita com os ticks imediatamente antes de começar a animação (ANTES do while). No início de cada iteração do while a variável atual é atribuída com os ticks do sistema. O número de milisegundos passados é armazenado na variável tempo, que é resultado da diferença das variáveis atual e anterior.

A variável x deve ser incrementada pela variação de distância calculada pelo tempo passado dividido por 1000, multiplicado pela velocidade – 200 (constante VELOCIDADE) pixels/segundo. Feita a conta, a variável anterior recebe o valor de atual, já que no próximo quadro os ticks do quadro atual passam a ser do anterior.

Uint32 anterior, atual;
// Tempo anterior
anterior=SDL_GetTicks();
while(x<LARGURA)
{     
   // Tempo atual
   atual=SDL_GetTicks();
   // Qual o tempo passado
   Uint32 tempo=atual-anterior;        
   // Limpa a tela
   SDL_FillRect(tela, NULL, SDL_MapRGB(tela->format, 255, 255, 255));
   //  Em que posicao x desenha a imagem
   x += (double)(tempo)*VELOCIDADE/1000.0;
   destino.x = (int)x;
   // Na próxima iteração, o tempo atual passa a ser o anterior
   anterior=atual;
   SDL_BlitSurface(imagem, NULL, tela, &destino);
   SDL_UpdateRect(tela, 0, 0, 0, 0);
}

Como prometido, vamos discutir o uso de uma variável double para armazenar a coordenada x da imagem. Isto se deve ao fato de que o deslocamento será praticamente sempre um valor com casas decimais, logo, para uma aproximação mais adequada sem arredondamentos que comprometam o efeito visual e temporal, devemos utilizar variáveis do tipo ponto flutuante. Para deixar a brincadeira mais divertida, experimente colocar o SDL_Delay(20) no código do while e note que a imagem continua levando o mesmo tempo para atravessar a tela, ou seja, a animação se adaptou à taxa de atualização.

Outras melhoras

Mesmo que não tenhamos acesso ao retraço vertical, sabemos que um display de vídeo trabalha com uma taxa de atualização fixa e limitada, logo não adianta desenhar mais quadros por segundo do que o sistema pode exibir. O nosso exemplo certamente faz isso, para evitar este problema, podemos considerar uma taxa máxima de atualização da tela, como 100 quadros por segundo.

Isso significa que entre cada quadro devem se passar pelo menos 10ms, assim basta verificar se o valor da variável tempo é pelo menos 10, como mostrado no código temporizado_melhor.c. Se o valor for maior ou igual, desenhamos o quadro, caso contrário, esperamos. Doom 3 é um jogo que faz isso, limitando a atualização a 60 quadros por segundo, independente dele usar ou não o retraço vertical nas configurações.

anterior=SDL_GetTicks();
while(x<LARGURA)
{     
   // Tempo atual
   atual=SDL_GetTicks();
   // Qual o tempo passado
   Uint32 tempo=atual-anterior;            
   // Verifica se passou o tempo mínimo necessário
   if(tempo<10)
      continue;

O método como apresentado até agora funciona muito bem, porém ele requer um desempenho mínimo do sistema para evitar que ocorra lag no jogo – percebido como falta de precisão no controle e na resposta do jogo. Como as animações são baseadas no tempo, você pode num FPS acabar mirando num inimigo do jogo que não está mais lá na próxima atualização – quem jogou Doom 3 numa máquina fraca sentiu na pele isso. No nosso exemplo com um delay em torno de 500ms, a imagem se “teleportaria” de um ponto ao outro da tela, o que não é uma ilusão muito convincente de animação.

Esse efeito de falta de precisão pode ser contornado estabelecendo-se um tempo máximo entre cada quadro desenhado – 30ms, por exemplo. Se o tempo passado for maior que isso, o jogo força o valor limite para animar os elementos sem prejuízos no jogo além de um slow down. A listagem temporizado_final.c realiza essa última etapa. Simplesmente testamos o valor da variável tempo, se este for maior que 30, o forçamos para este valor, caso contrário usamos o valor calculado normalmente.

anterior=SDL_GetTicks();
while(x<LARGURA)
{     
   // Tempo atual
   atual=SDL_GetTicks();
   // Qual o tempo passado
   Uint32 tempo=atual-anterior;
   // Verifica se passou o tempo mínimo necessário
   if(tempo<10)
      continue;
   if(tempo>30)
      tempo=30;

Vale lembrar que essa última modificação não resolve o problema de desempenho da máquina, apenas ameniza-o, deixando a animação mais lenta de maneira a compensar um processamento anormal – como quando o sistema faz swap de memória em disco ou houvesse elementos demais na tela e a lógica impusesse um overhead muito grande. Se a máquina não tiver condições de rodar o jogo, não será essa última modificação que irá resolver o problema, mas ao menos permite que o usuário espere um comportamento razoável da lógica do jogo.

Considerações finais

A técnica apresentada nesse artigo certamente não é revolucionária, existem outras variações para fazer a mesma coisa (que serão abordadas mais tarde), porém ela ilustra claramente a lógica necessária para um jogo manter as animações de maneira adequada independente do desempenho do sistema e da taxa de atualização do vídeo. Vale lembrar que a técnica pode ser utilizada tanto em jogos 2D como 3D sem restrições, então aproveite e faça isso no seu próximo projeto que vale à pena.

Todos os códigos fontes desse arquivo podem ser baixados aqui.

Comentários (8)
  • Bruno
    avatar

    O controle do tempo é essencial na grande maioria dos jogos. Muito interessante este artigo. Parabéns!

  • Paulo Vinicius Wolski Radtke
    avatar

    Valeu! No próximo eu quero falar de frame skip e fps mínimo, para terminar com interpolação. Acho que vai ficar legal com o tempo.

  • ArchV
    avatar

    Legal brother, isso realmente é um dos pilares fundamentais para o desenvolvimentos de jogos, e realmente foi muito bem mostrado como deve ser feito.

    Obrigado pelo tutorial!

    []
    ArchV.

  • Duduindo  - Legal.
    avatar

    Estou começando em Java. E estou louco pra chegar nesse "nível" e fazer meus próprios jogos, principalmente para celulares.

    Falow cara! e obrigado pela iniciação. :woohoo:

  • Lopídio Guigui  - Ótimo!
    avatar

    Espero que não demore muito para sair o próximo artigo.

  • Lucian Sturião
    avatar

    Valeu mesmo, estou fazendo um projeto de final de semestre e isso vai me ajudar bastante =D

  • Ícaro Leandro
    avatar

    Jurava que tinhamos que fazer um Vetor(Array) com as texturas e separar cada frame de acordo com o tempo do jogo...
    Interessante...

  • Maycon  - Uau.
    avatar

    Legal me ajudou bastante vou tentar implementar no meu projeto. :P :side:

Escrever um comentário
Your Contact Details:
Gravatar enabled
Comentário:
[b] [i] [u] [url] [quote] [code] [img]   
:angry::0:confused::cheer:B):evil::silly::dry::lol::kiss::D:pinch::(:shock:
:X:side::):P:unsure::woohoo::huh::whistle:;):S:!::?::idea::arrow:
Security
Por favor coloque o código anti-spam que você lê na imagem.
LAST_UPDATED2  

Busca

Linguagens

Twitter