Arquivo para a Tag ‘Engine’
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[i] = 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!
Comentários (12)