Archive for the ‘Engine’ Tag
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!