Archive for the ‘Orientação a Objetos’ 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&#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!

Anúncios

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.

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.