Archive for the ‘SDL’ Tag

Gerenciamento de conteúdo (parte 2)

No post passado demonstrei algumas funções para criação e leitura de um pacote de arquivos binários extremamente simples. Neste artigo, iremos colocar as funcionalidades explicadas lá numa classe, e fazer um pequeno gerenciador de conteúdo para que fique fácil recuperar imagens desse arquivo como surfaces SDL.

(NOTA: Pode acessar a parte 1 desta série AQUI)

Inicialmente, vamos olhar a definição da classe responsável por LER arquivos Pack:


class PackReader
{
private:
   FILE            *mFile;
   PackHeader      mHeader;
   PackFileEntry   *mEntries;

public:
   // construtor padrao
   PackReader() : mFile(0), mEntries(0) { }
   ~PackReader() { close(); }

   // abre/fecha arquivo
   void open ( const string &file_name );
   void close ();

   // pega surface SDL
   SDLSurface* getSurface ( const dword id );
};

E agora a implementação:


void PackReader::open ( const string &file_name )
{
   mFile = fopen(file_name.c_str(), "rb");
   if ( !mFile ) { /*ERRO*/ }

   // pega dados do arquivo
   fread(&mHeader, sizeof(PackHeader), 1, mFile);
   mEntries = new PackFileEntry[mHeader.numFiles];
   fread(mEntries, sizeof(PackFileEntry), mHeader.numFiles, mFile);
}

void PackReader::close ()
{
   if ( mFile )
   {
      fclose(mFile);
      mFile = 0;
   }
   memset(&mHeader, 0, sizeof(PackHeader));
   if ( mEntries )
   {
      delete[] mEntries;
      mEntries = 0;
   }
}

SDLSurface* PackReader::getSurface ( const dword id )
{
   if ( !mFile ) { /*ERRO*/ }
   if ( id >= mHeader.numFiles ) { /*ERRO*/ }

   // cria buffer de dados e le do arquivo
   byte *buffer = new byte[mEntries[id].size];
   fseek(pack, mEntries[id].offset, SEEK_SET);
   fread(buffer, sizeof(byte), mEntries[id].size, mFile);
   // transforma em RWOps
   SDL_RWops *rw = SDL_RWFromMem(buffer, mEntries[id].size);
   SDLSurface *surf = <span>IMG_Load_RW ( rw, 1 );</span>

   // apaga dados lidos e retorna imagem
   delete[] buffer;
   return surf;
}

Agora que temos a classe para ler os dados de forma encapsulada, vamos criar o nosso primeiro gerenciador.

Um gerenciador de conteúdo é responsavel por ler e controlar todo o conteúdo de um jogo. Este nosso gerenciador de exemplo vai nos fornecer métodos para ler e liberar surfaces SDL. Iremos nos aprofundar nele e no sistema de arquivamento ao longo dos artigos, implementando diversas técnicas e sistemas de apoio importantes. Por hora, vamos ver o básico:


// estrutura de apoio
struct SurfaceContainer
{
   SDLSurface *surface;
   int        refCount;
};

// o gerenciador em si
class ContentManager
{
private:
   // o nosso leitor de pack
   PackReader                     mPackReader;
   // este map vai conter os conteúdos já em uso
   map<dword,SurfaceContainer*>   mContent;

public:
   ContentManager();
   ~ContentManager();

   void init ();
   SDLSurface *getSurface ( const dword id );
   void releaseSurface ( const SDLSurface *surface );
};

A classe inclui apenas 3 métodos: init(), q vai inicializar o gerenciador, e os dois métodos para pegar e liberar surfaces. Vamos olhar rapidamente a sua implementação:


void ContentManager::init ()
{
   mPackReader.open("arquivo.pck");
}

SDLSurface *ContentManager::getSurface ( const dword id )
{
   // primeiramente, verificamos se já existe esta surface na memória
   map<dword, SurfaceContainer*>::iterator it = mContent.find(id);
   if ( it != mContent.end() )
   {
      // já temos essa surface. Incrementa contador e
      // retorna surface
      SurfaceContainer *container = it->second;
      container->refCount++;
      return container->surface;
   }

   // nao encontramos nenhuma surface, entao vamos ler do arquivo
   SurfaceContainer *container = new SurfaceContainer();
   container.surface = mPackReader.getSurface(id);
   // inicializar contador de referencia
   container.refCount = 1;
   // colocar no map
   mContent[id] = container;
   // e retornar
   return container.surface;
}

void ContentManager::releaseSurface ( const SDLSurface *surface  )
{
   // vamos procurar o container desta surface
   dword id;
   SurfaceContainer *container = 0;
   map<dword, SurfaceContainer*>::iterator it = mContent.begin();
   map<dword, SurfaceContainer*>::iterator itEnd = mContent.end();
   for ( ; it != itEnd; ++it )
   {
      if ( it->second->surface == surface )
      {
         id = it->first;
         container = it->second;
      }
   }

   // checar erros
   if ( !container ) { /*ERRO*/ }

   // decrementa contador e verifica se podemos remover da memoria
   container->refCount--;
   if ( !(container->refCount) )
   {
      mContent.remove(id);
      SDL_FreeSurface(container->surface);
      delete container;
   }
}

Pronto. Este gerenciador apenas controla surfaces SDL a partir de imagens estocadas no nosso arquivo Pack binário. O controle de uso é simples e com alta probabilidade de erro (ie. necessita que o programador lembre de liberar as surfaces), e seus algoritmos são ineficientes.

Porém, agora temos uma estrutura básica em cima da qual poderei explicar e implementar detalhadamente um sistema de empacotamento e gerenciamento de conteúdo de nível profissional. No próximo artigo, irei me aprofundar em algumas técnicas de empacotamento “reais”, para que a partir daí poderemos implementar um empacotador interessante, rápido prático e útil.

Até mais garotada!

Gerenciamento de conteúdo (parte 1)

Opa galera, após uma (extensa) ausendia da minha parte no blog, estamos de volta em ação!

Ultimamente estou trabalhando num pequeno projeto pessoal. Esse projeto consiste em uma biblioteca que permite juntar vários arquivos binários (imagens, sons, etc) num arquivo só, e os acessar através de um localizador.

Essa idea vem desde a época em que estava trabalhando na minha engine 2D (PaperCut). Passou por vários estágios, onde eu tentei me virar sozinho inicialmente, e depois comecei a pesquisar técnicas mais avançadas para a criação do tal sistema. Este post inicia uma série que vai retratar essa experiência e passar algumas ideas e dicas de como fazer um sistema assim.. Iremos começar bem simples, e depois ir elaborando por algumas técnicas e ideas eficientes até chegar num sistema avançado de gerenciamento de conteúdo para games.

Vamos começar?

Todo jogo hoje em dia tem algum tipo de sistema de gerenciamento de conteúdo (content ou assets). Esse gerenciador é responsável por gerenciar a leitura dos dados de arquivos (seja a cada abertura de fase, ou load-on-demand), liberação dessa memória quando os dados não são mais necessários, fazer cache e muitos até mesmo streaming. Todos eles incluem também algum sistema de empacotamento dos arquivos de dados do jogo. Alguns usam bibliotecas como zlib, e outros tem formatos complexos (MPQ da Blizzard, por exemplo). Vamos começar pelo sistema de empacotamento (e um bem simples).

O mais simples possivel que podemos fazer é (lol) simples. Basicamente, criamos um arquivo binário que vai conter algumas informações sobre os arquivos que ele contém, e esses arquivos copiados na integra em ordem. Os cabeçalhos de tal arquivo seria algo do genero:

( NOTA: neste artigo em específico nao estou utilizando POO por razões de simplicidade. Porém exemplos mais avançados quando começar a ficar mais complexo serão sim, orientados a objeto)


// cabeçalho do arquivo binario
struct PackHeader
{
   dword version; // guarda a versao do software de empacotamento
   dword numFiles; // numero de arquivos empacotados
};

// cabeçalho q define cada arquivo interno
struct PackFileEntry
{
   dword offset; // offset para o começo do arquivo interno
   dword size; // tamanho do arquivo interno
};

Mais simples impossivel haha. Inclui apenas o minimo de dados necessários: o numero de versao da biblioteca (sempre importante pois ao voce precisar alterar o formato do arquivo binario, pode facilmente manter retro-compatibilidade com outras versoes) e o número de arquivos empacotados.

Para criar o empacotador deste formato, nada mais simples:


dword packFile(FILE *outFile, string &inFileName)
{
   byte buffer[8192];
   dword bytes, size = 0;
   FILE *inFile = fopen(inFileName.c_str(), "rb");

   // copia conteudo de arquivo requesitado para dentro do pack
   while( !feof(outFile) )
   {
      bytes = (dword)fread(buffer, sizeof(byte), 8192, inFile);
      fwrite(buffer, sizeof(byte), 8192, outFile);
      size += bytes;
   }
   return size;
}
void packFiles( string &pack, vector<string> &files )
{
   // abre arquivo
   FILE *outFile = fopen(pack.c_str(), "wb");
   // cria cabeçalhos
   PackHeader header;
   header.version = 0x0001; // versao 1
   header.numFiles = files.count();
   PackFileEntry *entries = new PackFileEntry[header.numFiles];

   // escreve cabeçalhos
   fwrite(&header, sizeof(PackHeader), 1, outFile);
   fwrite(entries, sizeof(PackFileEntry), header.numFiles, outFile);

   // calcula o offset inicial para o primeiro arquivo
   dword currentOffset = sizeof(PackHeader) + (sizeof(PackFileEntry) * header.numFiles);

   // itera arquivos a incluir
   int i = 0;
   vector<string>::iterator it;
   vector<string>::iterator itEnd = files.end();
   for ( it = files.begin(); it != itEnd; ++it )
   {
      // seta o offset deste arquivo
      entries[i].offset = currentOffset;
      // empacota arquivo (seta tamanho)
      entries[i].size = packFile(outFile, *it);
      // atualiza offset
      currentOffset += entries[i].size;
   }

   // escreve de novo os cabeçalhos das imagens, pois eles foram atualizados com os offsets
   fseek(outFile, sizeof(PackHeader)+(sizeof(PackFileEntry*header.numFiles)), SEEK_SET);
   fwrite(entries, sizeof(PackFileEntry), header.numFiles, outFile);
}

// exemplo de uso
int main ( int argc, char **argv )
{
   vector<string> files;
   files.push_back("arquivo1.bmp");
   files.push_back("arquivo2.bmp");
   files.push_back("arquivo3.bmp");

   packFiles("arquivo.pck", files);
}

Com esse simples código criamos um arquivo binário (ao qual dei a extensao de pck, para Pack), que inclui os tres arquivos definidos no código. Sim mas , como os acessar agora? Todo sistema de empacotamento tem alguma forma de identificar e recuperar os arquivos empacotados. Geralmente usa-se uma string com o nome e caminho do arquivo, mas como estamos falando de algo extremamente simples, iremos usar identificadores numéricos. Tá mas, nao defini em nenhum lugar no código esses identificadores. Bom, neste exemplo, o identificador do arquivo é o número de ordem (com base 0 – zero), em que ele foi incluido. Então, o arquivo1.bmp teria identificador 0, o arquivo2.bmp seria 1 e por aí vai (como disse, extremamente simples). Vamos olhar rapidamente o código de recuperação dos arquivos.


byte *unpack(FILE *pack, dword id)
{
   // vai para posição do cebeçalho correta
   fseek(pack, sizeof(PackHeader) + (sizeof(PackFileEntry)*id), SEEK_SET);

   // le cabeçalho
   PackFileEntry entry;
   fread(&entry, sizeof(PackFileEntry), 1, pack);

   // cria buffer de leitura
   byte *buffer = new byte[entry.size];

   // vai para posição do arquivo interno
   fseek(pack, entry.offset, SEEK_SET);
   fread(buffer, sizeof(byte), entry.size, pack);

   // retorna buffer
   return buffer;
}

// exemplo de uso
int main ( int argc, char **argv )
{
   FILE *file = fopen("arquivo.pck", "rb");
   byte *bmp2 = unpack(file, 1);

   // faz algo com o bmp2
   algo(bmp2);

   // deleta
   delete[] bmp2;
}

De novo extremamente simples. Este sistema integra muito bem com o meu antigo post sobre leitura de surfaces SDL a partir de dados na memória.

Claro que este sistema nao inclui nada demais, e nao é sequer prático. Porém, a partir do próximo post, irei evoluir a idea exemplificada aqui até chegarmos num sistema prático, útil, flexivel que irá incluir coisas como encriptação e compactação, streaming, caching, etc.

Então crianças, não percam o próximo episódio!

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!

Surfaces SDL a partir de dados na memória

Hoje falaremos sobre surfaces SDL, e como criá-las a partir de dados binarios em memória.

Como sempre, estarei usando as seguintes definições de tipos de dados:


typedef unsigned char byte;
typedef unsigned short word;
typedef unsigned int dword;

A criação de surfaces SDL a partir de arquivos de imagem é muito simples. Basta usar a função SDL_LoadBMP ou então a função IMG_Load da biblioteca SDL_Image (para poder ler imagens com outros formatos alem do BMP), como está exemplificado no codigo a seguir:


SDLSurface *surf;

// usando funçao padrao de load da biblioteca SDL 
surf = SDL_LoadBMP("imagem.bmp"); 

// usando funçao de load da biblioteca SDL_Image
surf = IMG_Load("imagem.png");

Porém, se voce usa no teu jogo um formato de arquivo binário que aglomera todos seus arquivos num só (um .dat, por exemplo), não dá para fazer load de uma surface desse jeito. Geralmente, voce extrai os dados binários dessa imagem a partir do arquivo e os coloca em uma posição de memoria. Como criar uma surface a partir desses dados?  A biblioteca SDL inclui estruturas e funções para exatamente este tipo de situação.

Primeiramente, temos a estrutura SDL_RWops. Esta estrutura vai guardar informações para que a SDL possa criar a surface. A SDL tem várias funções para criar uma SDL_RWops a partir de várias fontes, incluindo arquivos, ponteiros para estrutura FILE e, no nosso caso o que iremos usar, de dados da memória. Para tal, usamos a função SDL_RWFromMem(void*,int). Essa função recebe 2 parametros – um ponteiro para a posição de memória do vetor, e o tamanho desse vetor:


SDL_RWops *rw;
rw = SDL_RWFromMem ( (void*)dados, tamanho );

Agora que temos os dados processados, podemos criar a surface usando a função SDL_LoadBMP_RW(SDL_RWops*,int), ou entao a IMG_Load_RW(SDL_RWops*,int). Estas funções recebem 2 parametros também – o ponteiro para a estrutura SDL_RWops, e um inteiro que define se a função vai liberar automaticamente a estrutura passada (valor = 1) ou voce vai fazer isso manualmente (valor = 0).


SDL_Surface *surf;
surf = IMG_Load_RW ( rw, 1 );

Caso se passe 0 como valor, ou seja, a função não irá liberar a memoria da estrutura, usa-se a função SDL_FreeRW(SDL_RWops*) para liberar a memória.

Pronto, agora temos uma surface SDL criada com dados a partir da memória. Agora quero apenas mostrar um código extra que otimiza essa surface, melhorando a performance. Fazemos isso usando a função SDL_DisplayFormat, que transforma a surface que acabamos de criar em uma nova com o mesmo formato da tela:


SDL_Surface *surfOtimizada;
// criando surface otimizada
surfOtimizada = SDL_DisplayFormat ( surf );
// liberando memoria da surface pre-otimizada
SDL_FreeSurface ( surf );

E pronto – temos um código simples. Aqui está o código completo, dentro de uma função de uso fácil:


SDL_Surface* criaSurface ( const byte *dados, const int tamanho )
{
    SDL_RWops *rw;
    SDL_Surface *surf = 0, *surfOtimizada;

    // cria estrutura de controle
    rw = SDL_RWFromMem ( (void*)dados, tamanho );
    // cria surface pre-otimizada
    surf = IMG_Load_RW ( rw, 1 );
    // otimiza surface
    if ( surf )
    {
        surfOtimizada = SDL_DisplayFormat(surf);
        // libera memória de surface pre-otimizada
        SDL_FreeSurface(surf);
    }

    // retorna surface
    return surfOtimizada;
}