Arquivo para a Tag ‘Orientação a Objetos’
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!
Functors – ponteiros para métodos
Me pediram para postar um exemplo de um sistema de eventos usando o assunto do post passado (ponteiros para funções e métodos), mas decidi que antes de fazer esse post, irei falar sobre functors – pois irei utilizar esse conceito no sistema de eventos.
Como falei no post anterior, um ponteiro para um método de uma classe tem uma sintaxe estranha e que pode facilmente complicar códigos, e além disso necessitamos de uma referência ao objeto do qual esse método será executado. Para facilita então o uso desses ponteiros especiais, vamos criar um conjunto simples de classes que encapsulam a funcionalidade de um ponteiro para método – esse conceito é chamado de Functor.
Para que a classe seja genérica, necessitaremos utilizar templates. Mas, como um template na prática gera uma classe diferente para cada tipo, necessitamos criar uma classe-base para que nossos functors possam ser tratados por código genérico. E aqui está o código da classe:
class Functor
{
public:
virtual void operator() () = 0;
};
É uma classe abstrata bem simples – com apenas um operador virtual, o operador (). Isso nos permite fazer algo do tipo:
void funcao(Functor &func)
{
// chama funcao encapsulada pelo functor
func();
}
É logico q a classe Functor apenas nao resolve nada, então vamos olhar agora o código da classe que realmente faz a diferença – TemplateFunctor
template <class T>
class TemplateFunctor : public Functor
{
protected:
// definindo o tipo do ponteiro
typedef void (T::*metodo)();
// o ponteiro para o objeto em questao
T *_pObj;
// o ponteiro para o método
metodo _fpMetodo;
public:
// construtor
TemplateFunctor ( T *o, metodo m ) : _pObj(o),_fpMetodo(m) {}
// operador de chamada de funcao ()
void operator() ()
{
(_pObj->*_fpMetodo) ();
}
};
Agora sim, temos um functor funcionando. A classe é relativamente simples – temos a definição do tipo do ponteiro, a definição dos ponteiros para o objeto e para o método, um construtor que garante a construção apropriada do functor, e o operador () q nos permite chamar o método através deste functor como se o objeto fosse o próprio método. Vamos exemplificar a seguir:
// uma classe qualquer
class A
{
public:
A() {}
void metodoQualquer() { /*faz algo*/ }
};
// nao podemos ter um functor sem um objeto
A a();
// criando o functor - passando objeto e método
TemplateFunctor<A> aFunc(&a, &A::metodoQualquer);
// chamando o método através do functor
aFunc();
Algo que provavelmente voce está se pergutando agora é – e se eu quiser métodos com outros tipos de retorno e parametros? Bom, para isso é necessária a criação de classes functor diferentes ou, então, planejar a sua própria functor para trabalhar com parameterização genérica (receber uma union, ou um ponteiro void). Porém, para um sistema de eventos, por exemplo, onde todos os métodos chamados terão a mesma assinatura, este tipo de functor facilita bastante a codificação.
Como sempre, postem suas dúvidas e sugestoes, estarei mais que disponivel para responde-las.
Ponteiros de função
Uma função nada mais é, em sua essência, do que apenas um conjunto de instruções binárias em alguma posição de memória. Sendo assim, então, o C/C++ permite voce criar um ponteiro para uma função, que irá guardar o endereço de memória da função que poderá ser executada. Esse tipo de funcionalidade permite técnicas como de bom exemplo sistema de eventos.
Antes de começar a demonstrar como usar ponteiro para funções, vou deixar avisado que a sintaxe para os definir é um tanto quanto esdruxula, então nao se assustem muito.
Como C (e C++) é uma linguagem fortemente tipada, nenhum dado pode ser de tipo desconhecido para ser utilizado, e funções nao sao exceçao. Alem disso, funções tem o agravante de não apenas terem um tipo de dado de retorno, mas também a possibilidade de receber n parametros. Então, ao declarar um ponteiro para função, necessitamos declarar seu tipo de retorno e os tipos de todos os parametros. Voce consegue o endereço de uma função usando o nome da função, sem o operador (). O uso do operador & antes do nome da função é, neste caso, opcional. Porém, mais além iremos ver um caso onde o seu uso é obrigatório. Vamos ver alguns exemplos:
// exemplo de função 01
void vazia() {}
// seu ponteiro e atribuiçao
void (*fpVazia)(void) = vazia;
// exemplo de função 02
int numero() { return 2; }
// seu ponteiro e atribuição
int (*fpNumero)(void) = numero;
// exemplo de função 03
int soma(int a, int b) { return a+b; }
// seu ponteiro e atribuição
int (*fpSoma)(int,int) = soma;
Como disse anteriormente, a sintaxe é ligeiramente estranha, mas funciona. Temos aqui tres ponteiros para funções. Agora para executar cada uma dessas, usamos a seguinte sintaxe:
// executa cada uma das funções anteriores fpVazia(); int num = fpNumero(); // num recebe 2 int res = fpSoma(2,2); // res recebe 4 // outras formas de executar: (fpVazia)(); (*fpVazia)();
Repare que eu coloquei algumas outras formas de executar no final do código. Embora todas elas funcionem na maioria dos compiladores, alguns compiladores mais antigos podem dar problema com um ou outro formato. Além disso, para executar ponteiros para métodos (funções membros de classe), a sintaxe para chamadas de função vai ficar menos flexivel.
Para facilitar um pouco a sintaxe de uso de ponteiros para função, podemos utilizar o operador typedef, como exemplifico a seguir:
// exemplo de declaração sem typedef void (*funcExemplo)(int,int) fpExemplo; // agora usando typedef para criar // um tipo de dado novo que é um // ponteiro para função typedef void (*funcExemplo)(int,int); // declarando o ponteiro funcExemplo fpExemplo;
Muito mais simples, não? se vc pensar na questão de passar ponteiros para função como parametro de outra função, esta facilidade de criar um tipo de dado deixa o código muito mais limpo e facil de entender.
Ponteiros para funções são muito úteis, mas voce deve estar pensando “posso usar isso para criar ponteiros de métodos de classes?”. A resposta carrega junto uma explicação:
Um método estático de uma classe é exatamente igual a uma função comum – apenas que o compilador mantém controle sobre as permissões de acesso. Um método não-estático, porém, tem uma diferença bem especial. O formato de chamada ( chamada _thiscall), envia internamente um parametro extra – um ponteiro para o objeto no qual o método está sendo chamado. Disso podemos tirar que, além de sabermos o endereço dessa função, necessitados do ponteiro (ou referência) para o objeto também. Aqui está o exemplo de um ponteiro para um método:
// vamos ver uma classe
class A
{
private:
int _a;
public:
A(int a) : _a(a) {}
~A() {}
// método a ser executado
int soma(int b) { return _a + b; }
};
// criação do tipo ponteiro 'funcSoma'
typedef int (A::*funcSoma)(int);
// criando ponteiro
funcSoma fpSoma = &A::soma;
// nao podemos chamar a função sem um
// objeto do tipo A
A a(2);
A p = new A(3);
// chamando função pelo ponteiro
int res;
res = (a.*fpSoma)(2); // res recebe 4
res = (p->*fpSoma)(2); // res recebe 5
Repare em algumas peculariedades. Primeiramente, inicializamos o ponteiro passando o caminho do método ( &A::soma – diz q soma pertence à classe A. Neste caso, o uso do operador & passa a ser obrigatório). Como disse anteriormente, chama o método sem uma referência a um objeto da classe é impossível, então criamos os objetos ‘a’ e ‘p’ do tipo A. Finalmente, vemos os métodos sendo executados. Repare bem na sintaxe: voce usa o próprio objeto para chamar o método. No caso, dependendo do tipo (se é uma referência ou um ponteiro), usamos um operador diferente. ‘ .* ‘ ou ‘ ->* ‘.
Num outro post, irei descrever a implementação de uma classe genérica (templated) que facilita a manipulação de ponteiro para métodos.
Pilha de telas
Oi povo, desculpem a demora para responder, final do ano é sempre uma correria, e no trabalho não é diferente. Estou de volta com novos posts dicas e informações.
Sei que falei que iria falar da classe BaseGame da engine, mas decidi passar pra algo mais pratico. Várias pessoas nas ultimas semanas vieram conversar comigo sobre gerenciamento de telas em engines de jogos. Aqui na Tecnodata, desenvolvemos uma solução bem elegante e de conceito simples – uma pilha.
A engine tem, internamente, uma pilha controlada por um gerenciador de telas. Esse gerenciador contem os metodos de push() e pop(), assim como metodos para acessar a tela no topo da pilha ( topScreen() ), e delegar as chamadas dos metodos Update() e Draw() à tela mais visivel. Para que isso seja viável, uma tela é um conceito encapsulado em uma classe Tela. Vamos ver um exemplo de código de como isso poderia funcionar em C++. O esqueleto da classe Tela:
class Tela
{
public:
virtual void update(dword msTime) = 0;
virtual void draw(dword msTime) = 0;
};
A classe Tela é bem simples, basicamente apenas declarando uma interface minima que todas as telas devem ter para que possam ser gerenciadas. (nota: o tipo de dados dword é simplesmente um valor 32bits sem sinal – ou seja – um unsigned int numa maquina 32bits).Agora vamos ver o esqueleto do gerenciador:
class GerenciadorTelas
{
private:
stack<Tela*> _pilha;
public:
// construtor/destrutor omitidos
void push(Tela *tela);
Tela* pop();
Tela* telaAtual();
void update(dword msTime);
void draw(dword msTime);
};
Como eu tinha dito, o gerenciador internamente trabalha com uma pilha de telas. Para adicionar uma nova tela visivel no jogo, basta criar o objeto dessa tela (ie. Tela *t = new TelaQualquer() ) e adicionar na pilha. Desse jeito, no proximo frame da engine, a tela irá trocar automaticamente da tela anterior para a nova, pois estará chamando update e draw na nova tela.
O interessante é que, como estamos falando de pilha, a tela anterior ainda existe, entao voltar para a tela anterior é tao simples quanto dar um pop() no gerenciador. Isso garante que voce possa ter uma hierarquia de telas no seu jogo faceis de gerenciar e navegar.O exemplo mostrado aqui é extremamente simples, apenas para demonstrar a idea básica. Porém, no nosso sistema, telas incluem máquina de estados, serialização e transparência (caso uma tela tenha uma transparência inferior a 1.0, a tela abaixo dela na pilha também é visivel e, portanto, também recebe chamadas a update() e draw() ).
O gerenciador de telas não apenas gerencia a pilha, como também tem opções específicas para transição entre-telas (não apenas troca), acesso a telas por índice e por tipo (não apenas ao topo da pilha), e tem todos seus métodos como virtuais, possibilitando sua extensão fácil caso necessário.
Leitura extra
Um artigo bem interessante sobre organização de telas em classes pode ser lido neste post do blog Ponto V. Nao deixem de visitar.
Comentários (12)
Comentários (3)
Comentários (3)