Controle de velocidade de jogo com ticks

Muitos me perguntam – “Como posso fazer para controlar a velocidade de execução do meu jogo?” Já vi pessoas tentando resolver este problema usando timers do sistema e até mesmo threads – mas existe um jeito bem simples, porém poderoso, para controlar a velocidade do jogo – usando ticks.

A idea básica é voce definir a execução do jogo em X ticks por segundo – assim garantindo que o jogo roda na mesma velocidade em todas as máquinas, e deixando a velocidade independente do FPS (frames per second – frames por segundo). A lógica é simples – voce tem uma variável que define quantos milisegundos cada tick tem para executar, e só executa o próximo tick após esse tempo mínimo ter passado:


// alguns typesdefs - padrao q geralmente gosto de usar
typedef unsigned int dword;
typedef unsigned char byte;

// definicao de uma lista de objetos
typedef vector<Objeto*> VetorDeObjetos;

// variável q vai guardar o tempo dos ticks - em milisegundos
//     neste caso, 34milisegundos significa 30 updates por segundo
dword msTick = 34;
// variável q guarda o tempo do update anterior
dword msLastUpdate = 0;

// funcao de execução chamada dentro do loop de jogo
void rodaTick()
{
   // estou usando SDL para facilitar o exemplo
   //    esta funcao do SDL retorna o numero de milisegundos
   //    desde o inicio do programa
   dword msTempo = SDL_GetTicks();

   // devo executar o tick?
   if ( msTempo >= (msLastUpdate + msTick) )
   {
      // seta o valor de ultima atualizaçao
      msLastUpdate = msTempo;

      // chama funcao update() de todos os objetos do jogo
      for ( VetorDeObjetos::iterator it = objetos.begin(); it != objetos.end(); it++ )
         it->update(msTempo);
   }
   // agora manda desenhar todos os objetos
   for ( VetorDeObjetos::iterator it = objetos.begin(); it != objetos.end(); it++ )
      it->draw();
}

Como disse, bem simples. O update eh feito periodicamente baseado no valor de msTick, mas o draw() é chamado todas as vezes. Assim voce garante o maior FPS possivel, mas a velocidade de execução do jogo se mantem igual,

Mas e se a máquina por acaso engasga por algum tempo (mesmo q minusculo)? Como garantir que a lógica do jogo nao fique defasada?
Podemos colocar uma verificação extra para que, em vez de executar cada tick se o tempo passou do estimado, ele executar todos os ticks até que o tempo de execução esteja correto em relação ao tempo de aplicação. Vamos alterar nossa funcao um pouco para criar esse efeito:


void rodaTick()
{
   // estou usando SDL para facilitar o exemplo
   //    esta funcao do SDL retorna o numero de milisegundos
   //    desde o inicio do programa
   dword msTempo = SDL_GetTicks();

   // devo executar o tick?
   while ( msTempo >= (msLastUpdate + msTick) )
   {
      // seta o valor de ultima atualizaçao
      msLastUpdate += msTick;

      // chama funcao update() de todos os objetos do jogo
      for ( VetorDeObjetos::iterator it = objetos.begin(); it != objetos.end(); it++ )
         it->update(msTempo);
   }
   // agora manda desenhar todos os objetos
   for ( VetorDeObjetos::iterator it = objetos.begin(); it != objetos.end(); it++ )
      it->draw();
}

Com o while, todos os ticks serão executados corretamente até se atingir o tempo atual de execução, mesmo que a máquina engasgue por alguns ticks.

Mas podemos melhorar ainda mais este sistema. Imagine por um instante que temos na nossa engine vários sistemas – tais como IA, física, etc. Podemos querer atualizar a IA com metade da frequencia de que atualizamos o movimento e física. Para isso, vamos implementar o suporte a multiplos ticks, mas antes vamos olhar um esqueleto da classe objeto:


class Objeto
{
protected:
   dword _tickMascara;

   // outros membros protected e private

public:
   // construtores/destrutores

   // outros métodos

   // métodos de tick
   inline bool verificaTick(const dword tick)
   {
      return ( _tickMascara & tick );
   }

   // métodos de update e desenho
   virtual void update(const dword tick, const dword msTempo) = 0;
   virtual void draw() = 0;
};

A nossa classe Objeto tem um membro protegido, _tickMascara, que define uma mascara 32bits – essa máscara iremos usar para definir se este objeto responde, ou não, ao nosso tick. Deste jeito, podemos ter até 32 ticks diferentes, cada um correspondendo a 1 bit da mascara. O método verificaTick é usado para verificar se o tick definido no parametro é aceito pelo objeto. Mudei também o método update para que receba o tick que estamos atualizando, evitando assim a necessidade de chamar dois métodos diferentes sempre que precisamos atualizar.

Vamos agora olhar a definição da nossa variável que controla o tempo do tick:


// máximo de ticks suportados
#define MAX_TICKS 32

// definimos para q serve cada tick - define o indice e a máscara de cada tick
#define TICK_IA_INDICE        0
#define TICK_IA_MASK        0x001;
#define TICK_MOV_INDICE     1
#define TICK_MOV_MASK         0x002;

// como precisamos manter até 32 ticks, o formato é diferente
struct tickInfo_t
{
   dword numTicks;
   dword msLastUpdate[MAX_TICKS];
   dword msTick[MAX_TICKS];
} tickInfo;

// vamos usar esta funcao para inicializar os ticks
void tickInit()
{
   // estamos usando 2 ticks neste exemplo
   tickInfo.numTicks = 2;

   // IA atualiza 15 vezes por segundo
   tickInfo.msTick[TICK_IA_INDICE] = 67;
   // moviemento 30 vezes por segundo
   tickInfo.msTick[TICK_MOV_INDICE] = 34;

   // zerar os contadores de tempo de cada tick
   for ( int i = 0; i < tickInfo.numTicks; i++ )
      tickInfo.msLastUpdate&#91;i&#93; = 0;
}

// e agora, nossa funcao rodaTick()
void rodaTick()
{
   // estou usando SDL para facilitar o exemplo
   //    esta funcao do SDL retorna o numero de milisegundos
   //    desde o inicio do programa
   dword msTempo = SDL_GetTicks();

   // temos q passar por todos os ticks agora
   dword tickAtual = 1;
   for ( int i = 0; i < tickInfo.numTicks; i++ )
   {
      // devo executar o tick?
      while ( msTempo >= (tickInfo.msLastUpdate[i] + tickInfo.msTick[i]) )
      {
         // seta o valor de ultima atualizaçao
         tickInfo.msLastUpdate[i] += tickInfo.msTick[i];

         // chama funcao update() de todos os objetos do jogo
         // repare que agora estamos passando a mascara do tick juntamente com o tempo
         for ( VetorDeObjetos::iterator it = objetos.begin(); it != objetos.end(); it++ )
            it->update(tickAtual, msTempo);
      }
      // passamos para o próximo tick usando shift
      //     para quem nao conhece, este operador vai fazer com q todos os bits
      //     andem 1 casa para a esquerda
      tickAtual <<= 1;
   }
   // agora manda desenhar todos os objetos
   for ( VetorDeObjetos::iterator it = objetos.begin(); it != objetos.end(); it++ )
      it->draw();
}

Agora temos diversos ticks funcionando em tempos diferentes. Vamos olhar um exemplo de um objeto que responde a esses dois ticks que definimos:


class Pessoa : Objeto
{
private:
   // membros privados

public:
   // no construtor definimos os ticks aceites por este objeto
   Pessoa() : _tickMascara( TICK_IA_MASK | TICK_MOV_MASK ) {}
   virtual ~Pessoa() {}

   // metodo update
   virtual void update(const dword tick, const dword msTempo)
   {
      // vamos atualizar
      if ( tick & TICK_IA_MASK )
         updateIA(msTempo);
      if ( tick & TICK_MOV_MASK )
         updateMovimento(msTempo);
   }

   // os metodos de atualizaçao de ia e movimento
   void updateIA ( const dword msTempo ) {}
   void updateMovimento ( const dword msTempo ) {}

   // metodo draw...
};

Ao definir _tickMascara com os 2 tipos de tick, o metodo update será chamada a cada update dos ticks IA e MOV. Se removermos um dos ticks da definição no construtor, o metodo irá fazer nada com esse tick. Podemos melhorar consideravelmente a performance deste sistema verificando se o objeto responde ao tick especificado verificando o método verificaTick() anteriormente a chamar o método update().

Este código necessita no entanto que voce tenha na sua engine de jogo alguns requesitos necessários:

– Um passo de inicialização, onde voce pode inicializar os ticks;
– Um repositório central com todos os objetos do jogo – em formato lista, arvore, etc;

Quaisquer dúvidas ou sujestões são sempre benvindas – espero ter sido util com este artigo. Abraços e até mais!

13 comments so far

  1. vinigodoy on

    Jóia. Eu também recomendo uma leitura no algoritmo do dr. Andrew Davidson. Na verdade, o que ele faz é separar os tempos de “update” do de “draw”.

    O algoritmo dele mantém a taxa de updates constantes (o que simplifica muito as coisas em jogos mais simples), mesmo que para isso seja necessário pular uma ou outra etapa de desenho. Como a etapa de desenho geralmente é a mais demorada (o que provavelmente não é válido se você fizer um RTS), você consegue manter a taxa estável mesmo em micros mais lentos do que o seu jogo exige.

    A leitura desse algoritmo pode ser feita no livro Killer Game Programming in Java, disponível de graça no site do autor:
    http://fivedots.coe.psu.ac.th/~ad/jg/

    Ou, mais especificamente:

    Clique para acessar o ch1.pdf

    Dica. Quem baixar o batalha estelar no meu blog, vai ver que lá dentro tem a implementação dessa mesmo algoritmo em C++.

  2. Guedes on

    Artigo útil, como sempre, tenho algumas dúvidas, como novato em desenvolvimento de jogo:
    1) Por que os métodos ‘update()’ e ‘draw()’ estão separados? Não seriam a mesma coisa?
    2) Em seu código não existe uma espera no processamento, isso não acarreta sobrecarga no processador? Algo como 100% do processador sendo usado.
    3) Por que usar estrutura em ‘tickInfo_t’ ao invés de classe?
    4) Como você sabe que 67 ‘ticks’ é equivalente a 15 segundos?
    5) Não entendi o conceito da função ‘rodaTick()’.
    6) Também não entendi a classe ‘Pessoa’.

  3. dfaobolinho on

    1) os metodos update() e draw() estao separados pq, assim, estamos separando a atualizaçao logica do jogo de seu desenho, ou seja, o jogo vai ser atualizado corretamente, nao importando a velocidade do pc q estah sendo rodado, mas porem mantendo o desenho independente, fazendo com q pcs mais rapidos possam desenhar mais telas por segundo.

    2) Nao existe necessidade de espera. O jogo processa o mais rapido possivel, nao havendo necessidade de bota-lo em espera (pq quanto mais potente teu pc, mais frames ele vai desenhar)

    3) Eu uso estruturas pra definir tipos de dados complexos q nao necessitam de metodos. Isso nem sempre eh correto no ponto de vista de orientaçao a objetos, mas eh uma questao pessoal;

    4) 1000 milisegundos corresponde a 1 segundo, logo para eu garantir 30 ticks por segundo, basta eu botar no msTick o valor de 1000/30

    5) rodaTick() seria a funcao q vc teria q chamar a cada loop do jogo.. eu apenas colokei uma funcao para exemplificar – vc pode colocar akelecodigo diretamente no loop principal da engine

    6) A classe Pessoa eh apenas um exemplo de uma classe de objeto do jogo.

  4. Guedes on

    Desculpe-me, não expressei corretamente, em 5 e 6 eu tenho dúvida no código, você pode explicar passo-a-passo?

  5. dfaobolinho on

    Vamos lah entao, destrinchar a funcao rodaTick

    
    // desde o inicio do programa  
    dword msTempo = SDL_GetTicks();  
    // temos q passar por todos os ticks agora  
    dword tickAtual = 1; 
    
    

    Estas duas linhas criam as variaveis necessárias para o algoritmo. msTempo guarda o tempo atual de execução ( q eu estou pegando, neste caso, usando funcao SDL_GetTicks ), enquanto que a variavel tickAtual é inicializada em 1 – ela vai ser usada como máscara de bits para cada tick. Básicamente, imagine o valor binário de tickAtual como sendo:

    ‘0…01’ (todos bits 0, exceto o bit menos significativo)

    O significado disso já vai ser demonstrado logo logo.

    Em seguida, temos o nosso for, q começa em 0 e vai dando loop enquanto o i for menor que o numero de ticks registrados (valor de tickInfo.numTicks). Agora, dentro do for temos um outro loop, o while.

    Esse while irá executar enquanto que o tempo atual seja maior q o tempo do ultimo update (msLastUpdate) somado ao tempo do tick (msTick). Isso asegura que o update serah feito sempre correto, pois se ainda nao deu tempo suficiente para o tick, ele nao atualiza – mas se jah passou mais de um, ele atualiza kuantas vezes forem necessárias até chegar a uma situação correspondente ao tempo atual.

    Aí é simples, atualizamos o valor de msLastUpdate, e chamamos a funçao update de todos os objetos da engine. Embora eu nao tenha colocado isso no código, como explikei no final do artigo podemos colocar um controle extra para apenas chamar update de objetos q realmente aceitam akele tick, fazendo a seguinte mudança:

    
    // código original
    for ( VetorDeObjetos::iterator it = objetos.begin(); it != objetos.end(); it++ )
       it->update(tickAtual, msTempo);
    
    // nova versao
    for ( VetorDeObjetos::iterator it = objetos.begin(); it != objetos.end(); it++ )
    {
       if ( it->verificaTick(tickAtual) )
          it->update(tickAtual, msTempo);
    }
    
    

    Após esse for que chama todos os update, temos um shift na variavel tickAtual. Lembrando do estado anterior dela, ao fazer o código ‘tickAtual <<= 1; ‘, o q estamos fazendo eh “rodar” todos os bits dele uma posição para a esquerda, adicionando um 0 na posição menos significativa. ou seja, após este comando, nossa mascara fica com o seguinte formato:

    ‘0..10’

    Como cada tick tem uma máscara correspondente a ele, ou seja, o tick #0 tem como máscara 0x001, o tick #1 tem como máscara 0x002, #2 tem 0x004, etc, a verificaçao de tick eh simplesmente fazer um & (and bit-a-bit) e, se o valor retornado for diferente de 0 (nulo), significa q o teste passou, caso contrario, errou.

    Finalmente, chamamos o método draw() eh todos os objetos fora de todos os loops. Basicamente, isso faz com q , tendo ou nao update nakela passada pela função, o mundo serah desenhado. Assim, vc tendo seu pc segurando 10 ou 100 FPS, a velocidade de JOGO será a mesma.

    Daki a poko eu explico a classe Pessoa

  6. Guedes on

    Entendi, estou aguardando a próxima explicação.

  7. dfaobolinho on

    tah vo explicar a classe pessoa agora

    A classe pessoa eh um exemplo de um objeto do jogo. Ela herda da classe objeto, que apenas define uma interface padrão para todos os objetos. Essa interface define a variável membro que guarda a máscara de bits dos ticks daquele objeto, o método que verifica se um certo tick afeta aquele objeto, o método de update (q atualiza o objeto no mundo de jogo) e o método draw, que desenha o objeto na tela.

    A classe pessoa inicializa a máscara de ticks no construtor, como podemos ver:

    
    Pessoa() : _tickMascara( TICK_IA_MASK | TICK_MOV_MASK ) {}
    
    

    Neste exemplo, estamos marcando o objeto para responder aos ticks de IA e MOVIMENTAÇÃO. Se eu quisesse que o objeto apenas respondesse ao de movimentação, iria ficar da seguinte forma

    
    Pessoa() : _tickMascara( TICK_MOV_MASK ) {}
    
    

    No método update, ele chama um outro método de atualização dependendo de o tick q está sendo atualizado é o de IA ou de movimento – mas apenas por questoes de organização. Voce pode colocar toda a lógica de todos os ticks dentro de clausulas case de um switch – caso voce prefira.

    É simples assim

    Abraços!

  8. skhaz on

    Excelente artigo e explicação as duvidas de Guedes melhor ainda, parabens!

  9. Guedes on

    Realmente é simples, novamente, obrigado pela explicação.

  10. dfaobolinho on

    Opa sempre feliz por ajudar!

    agora nas proximas semanas nao estarei podendo postar – talvez eu consiga mas nao posso prometer nada.

    de qq forma, provavelmente meus proximos posts serao sobre os eventos mesmo..

    abraços!

  11. […] motivos para isso, um deles é se você implementar um sistema de ticks como descrito no artigo Controle de velocidade de jogo com ticks onde quase sempre o método draw não será chamado no mesmo ciclo que […]

  12. […] motivos para isso, um deles é se você implementar um sistema de ticks como descrito no artigo Controle de velocidade de jogo com ticks onde quase sempre o método draw não será chamado no mesmo ciclo que […]

  13. Wellington Gomes Dos Santos on

    Parabéns, artigo muito bem escrito e explicado.


Deixe um comentário