Game Developers Conference 2009

Bom galera, estamos a meio do ano e, como sempre, começo minhas preparações financeiras e de calendário para a próxima GDC. Muitos já me perguntaram informações sobre como ir, quanto custa, etc, então decidi postar algumas informações do eventos, assim como algumas curiosidades, aqui.

Existem várias GDC durante o ano, incluindo San Francisco-CA, Paris e Austin-TX. Eu costumo ir na de San Francisco, e 2009 não será exceção.Vou falar rapidamente de valores, e depois vou explicar detalhadamente como funciona o evento, e algumas experiências próprias.

A GDC acontece no Moscone Center em São Francisco, Califórnia. Em termos de valores, não fica uma viagem barata para brasileiros. Porém, eu considero muito importante. Se aprende muito, se conhece muitas pessoas diferentes e sempre saio de lá com bons contatos. A viagem+hospedagem costuma sair por volta de 6 mil reais, mas eu costumo ficar em algum hotel na proximidade, que são ligeiramente caros (de 150 a 250 dolares a noite). Tem no entanto alguns albergues na região (inclusive sei de um perto do hotel Grand Hyatt, umas 4 quadras do Moscone Center) nos quais voce consegue uma semana por valores proximos a 500 dolares.

Em relação ao evento em si (que também é caro), existem vários tipos de passes. Geralmente eu pego o “All Access Pass”, que me permite participar de todos os tutoriais, eventos, palestras, keynotes, etc. Esse costuma ficar por volta de US$1500 com desconto de pagamento adiantado, ou US$2000 depois de janeiro. Existem outros passes mais baratos, incluindo o “Main Conference Pass” (US$1400 sem desconto), GDC Mobile Pass (US$1050 sem desconto), e “Expo Pass” (US$ 200). A cada ano, passes diferentes são criados ou somem, então é geralmente interessante dar uma boa olhada na lista antes de decidir (no primeiro ano, fui com um passe especial de “Independent Game Summit”). Esses preços incluem almoço nos dias correspondentes.

Falando um pouco da conferência agora. A GDC é na verdade o conjunto de vários eventos simultâneos. Segunda e terça são voltados para negócios e profissionais. Envolvem vários ‘summits’ (pequenos eventos), com palestras e atividades específicas a certas áreas do desenvolvimento de games. Por exemplo, a GDC Mobile, voltada para desenvolvimento de jogos para portáteis, Independent Game Summit, voltado para a comunidade de desenvolvedores independentes, etc. Acontecem também sessões de tutoriais e workshops de durações que vão de 1 hora até os 2 dias. Na de 2007, eu participei dos workshops de Game Design, e posso dizer que foram muito interessantes e divertidos.

De quarta até sexta, a chamada “main conference” acontece. Nesses dias, a feira com os stands das empresas e universidades é aberta aos participantes. É muito interessante tirar um par de horas para passear calmamente pela exposição, tem bastante coisa interessante. Como nota de interesse, este ano teve um pequeno concurso de programação de IA no stand da Microsoft, achei muito bacana. Em outro bloco, existe outra “feira” com stands – o Pavilhão de Carreira. Ali, voce pode conversar com o pessoal de RH, e as vezes representantes das diferentes áreas das empresas. Se voce pretende seriamente passar pelo pavilhão com intensões de arrumar uma possibilidade de emprego, leia as dicas no final deste post. Ao mesmo tempo que existem os dois pavilhões de exposição e carreira (acredite, acontece tanta coisa ao mesmo tempo que é facil se sentir perdido), acontecem as palestras. São um volume monstruoso de palestras , geralmente com várias da mesma área (programação, audio, design gráfico, game design, etc) acontecendo simultaneamente. E acredite, voce terá que escolher e não será facil. De novo, algumas dicas no final ajudam um pouco essa questão.

Acontecem também keynotes (alguem importante dando sua visão sobre algo), que eu recomendo dependendo do palestrante. Eu tive a honra de participar da keynote do Ray Kurzweil sobre os proximos 50 anos da indústria de jogos. Além disso, todo ano tem o evento/festa de premiação da IGF (Independent Game Awards), que reconhece os melhores jogos independentes do ano (o pessoal que criou Portal veio da cena independente). Depois, no mesmo evento, tem as premiações para os melhores jogos “normais” do ano – a Game Developer’s Choice Awards. É um evento muito interessante de participar, e eu provavelmente estarei lá ano que vem.

Paralelamente a tudo isso, existem pequenos eventos e festas acontecendo o tempo todo. Desde a tradicional festa da IGDA (International Game Developers Association), para participar voce deve ser membro (custa 100 dolares por ano). Tem também diversas festas patrocinadas pelas grandes empresas como Microsoft, Sony, etc. Muito importante estar atendo à programação do evento para saber escolher o que quer fazer (um LIVRO com grossura considerável que voce recebe ao se registrar no evento). Como uma curiosidade, este ano teve campeonato beneficiente de Poker, com custo minimo de 100 dolares. Eu participei e foi uma experiência incrivel. Além de tudo isso, a cidade de San Francisco oferece entretenimento e coisas pra fazer sem conta.

Dentro do Moscone Center costuma ser montada também uma pequena loja oficial do evento, vendendo desde camisetas até livros, DVDs, camisas, bolsas, canetas, esqueiros e o que mais voce possa imaginar. As coisas não são muito caras não, vale a pena comprar algumas lembrancinhas pra fazer inveja para seus colegas e amigos. Tem também todo ano uma especie de jogo interno ao evento que qualquer um pode participar. É um jogo focado em networking e me falaram que é bem interessante, porém nunca participei..hmm, quem sabe desta vez? rsrs

Bom, nao sei mais o que poderia falar do evento, então vou deixar aqui algumas dicas e informações:

Informações:

  • site oficial: www.gdconf.com (o site costuma ser atualizado mais perto do final do ano, é bom ficar ligado)
  • Data: 23 a 27 Março 2009
  • Local: Moscone Center, San Francisco – CA

Algumas dicas:

  • Tente chegar sabado ou domingo antes do evento. É bom para poder descançar, se habituar ao clima/horário, e poder encontrar pessoal antes do evento (caso combine algo) e mesmo passear um pouco pela cidade.
  • Mesmo para a hora de voltar pra casa. Tente passar pelo menos o sabado na cidade. Viajar para tao longe e nao poder conhecer a cidade decentemente é triste. De preferência e se tiver condições, pegue uma semana a mais porque realmente vale muito a pena.
  • Participe do máximo de festas e eventos noturnos da conferência. Se possível, vá com alguém (ou sozinho mesmo, acredite voce consegue puxar papo facil com pessoal de games) a algum dos bares da região. Um bar bastante badalado pelo pessoal da GDC é no hotel W (é do lado do Moscone Center). É uma ótima hora para socializar e fazer networking. Além do mais, é bom dar aquela relaxada tomando uma boa cerveja. Não precisa gastar muito pra beber e comer bem.
  • Não perca nenhum horário de palestra. Se realmente não tem nenhuma palestra interessante em algum horário (o que vai ser raro), aproveite esse tempo para fazer algo como visitar o pavilhão de exposição.
  • Seja simpático e comunicativo. Todo mundo ali tá com espírito de festa e diversão, quase certeza que se você puxar papo com o pessoal na sua mesa de almoço ou na descida da escada rolante, a pessoa vai conversar com você tranquilamente, e talvez até trocar cartões de visita.
  • Faça e leve cartões de visita. Se você trabalha em uma empresa, peça para seu chefe. Se recusarem ou simplesmente você ainda nao trabalha/não quer levar cartão da empresa – faça um pessoal. Um cartão apresentável com nome, profissão, email e telefone é obrigatório num evento como este.
  • Se voce pretende procurar oportunidades, faça um currículo padrão internacional e leve. Não precisa levar impresso, existem alguns locais baratos em que  você pode imprimir por lá. Se você nao pretende procurar oportunidade, leve seu currículo mesmo assim – nunca se sabe o que pode acontecer.
  • Tire o pó do seu inglês. É sério, se voce está na area de desenvolvimento de jogos, inglês é obrigatório. Se voce enrola no inglês, dê um jeito de desenferrujar. Se não sabe, tá numa boa hora de aprender já.
  • Leve dinheiro extra ou cartão de crédito com um bom limite. Você vai querer sair, comprar bugiganga, etc – se você for de grana contada vai perder oportunidade de se divertir melhor, e quem sabe de passar na BestBuy comprar aquela placa de vídeo ou então na Apple Store comprar seu iPod Touch ou MacBook (tem uma dessas a duas quadras do Moscone Center).
  • Ao se registrar no evento, você ganha uma entrada para o site de relacionamento do evento (MyGDC). Entre nele, fuce bastante, tente descubrir pessoas com interesses parecidos e puxe papo. O networking pode começar mesmo antes de ir!!
  • Se você tiver conhecidos pelo evento, tente saber que palestras eles vão participar. Não é muito interessante você ir numa mesma palestra que um conhecido seu – muito melhor vocês combinarem a ponto de cada um ir em uma palestra diferente, e depois trocar notas.
  • Leve (ou compre por lá) uma camera digital. Documentar sua viagem é muito importante!
  • No evento voce ganha uma sacola com presentinhos folhetos e outras coisas interessantes e uteis para o evento e depois. Não se acanhe de pegar o bloco e caneta que vem dentro e de anotar bastante durante as palestras. Sua memória pode ser infalível mas depois de dois dias durmindo 3~4 horas com palestras durante o dia e cerveja de noite, ela pode começar a lhe faltar.

Bom, agora nao consigo me lembrar de mais nenhuma dica. Se lembrar, eu posto hehehe.

Se algum de voces tiver dicas, informações ou simplesmente estiver pensando em ir e tiver interesse em se encontrar por lá, deixe um comentário. Se tiver dúvidas, quiser conversar sobre isso por msn/gtalk/email, ou quiser mais informações também é benvindo a colocar comentários ou me contatar.

Abraços pessoal!

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!

Meme – Como é meu desktop (terceira parte)

E ai pessoal, aqui está a terceira e ultima parte da serie Meme heehehe..

Desta vez, temos um screen do desktop do meu MacBook Pro. Nao preciso nem dizer que é limpo e sóbrio hehe.

MacOS X é um sistema operacional maravilhoso. Como eu queria poder usa-lo sempre…

Bom, aqui está a lista de softwares que eu tenho nele (nao listo os softwares padrao do MacOS como iTunes):

  • Vidalia (possibilita usar a rede Tor para burlar proxies e firewalls – muito util num laptop)
  • VMWare Fusion com 2 VMs: Windows Vista 32bits (instalado no BootCamp também) e um Debian GNU/Linux 64bit
  • NeoOffice (adaptação do OpenOffice para ficar mais bonito no Mac OS)
  • Adobe Creative Suite Premium CS3
  • Skype
  • Xcode 3.0
  • Mono com MonoDevelop
  • svnX
  • MS Office 2008
  • Starcraft
  • World of Warcraft
  • Diablo 2

É isso ai gente.. Abraços de novo para o pessoal.

Meme – Como é meu desktop (segunda parte)

Dae povo .. de novo postando a segunda (de tres) partes com screenshot dos meus desktops.

este aqui é o meu desktop caseiro (windows). Infelizmente o meu HD que tinha meu linux queimou e ainda nao substitui.

Novamente, uma interface extremamente limpa e sóbria. A pedidos do Skhaz, vou listar o que tenho instalado no meu trabalho e em casa:

Em ambos (desenvolvimento):

  • Visual Studio 2005 Professional com XNA Game Studio 2.0
  • Visual Studio 2008 Professional
  • AddOn Studio for World of Warcraft
  • SQL Server 2005 Express Edition
  • Adobe CS3
  • Windows 2003 Platform SDK
  • VMWare
  • DirectX SDK

(entretenimento):

  • Counter-Strike 1.6
  • World of Warcraft
  • Steam (com Orange Box, Half-Life 2, HL2:Episode 1, CounterStrike:Source)
  • Need For Speed: Most Wanted

É isso aí. Ainda falta screen do meu MacBook Pro (aquele que aparece na foto do meu trabalho no post anterior), que eu prometo postar amanha sem falta. Abraços pessoal!

Meme – Como é meu desktop (primeira parte)

Respondendo ao Meme do ViniGodoy, estou postando o meu desktop.

Este aqui é um screen do meu desktop na Tecnodata Educacional (onde trabalho). Uso duas telas, com um desktop limpo e sóbrio. Uso o Yahoo Widgets , porem praticamente soh uso para ter notas no desktop. (clique pra ver melhor)

E esta é a foto do meu canto de trabalho

Mais tarde eu irei postar tambem fotos dos desktops do meu laptop e do meu computador em casa.

Abraço para o ViniGodoy e o resto do pessoal de desenvolvimento de jogos.

Adendo:

Aqui estao as outras partes do meme

Viajando – CampusParty e GDC ’08

Opa povo,

Estou postando aqui para avisar que provavelmente nao poderei postar nas próximas duas semanas pois estarei indo à CampusParty na Bienal em São Paulo, e logo depois a Game Developers Conference em San Francisco. Tentarei postar sobre ambos os eventos aqui durante ou, mais provável, após a viagem.

Bom par de semanas para todos voces, nao se preocupem os artigos técnicos estarão voltando logo logo. Aqui vou eeeeu!

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!

Como detectar furos de memória no C++

Furos de memória são um dos tipos de erros mais frequentes no C++, e estão entre os mais dificeis de depurar. Existem várias bibliotecas de garbagge collector, e para aqueles que nao querem seus objetos gerenciados por um GC, existem várias bibliotecas que ajudam a detectar memória não liberada (Valgrind – por exemplo). Escrever seu proprio alocador é uma outra solução para ajudar a detectar esses erros.

Porém aqui irei mostrar um pequeno sistema que, em modo DEBUG, avisa se esquecemos de liberar algum ponteiro no final da execução. Básicamente o que iremos fazer é montar uma lista que, ao alocarmos memória, adicionamos na lista um bloco com informações dessa alocação, e removemos esse bloco da lista ao liberarmos a memória. Ao final da execução do programa, se a lista nao estiver vazia, teremos todas as informações sobre o que nao foi liberado. Vamos olhar entao o código do ‘memory.h’:


// soh queremos essa funcionalidade em modo DEBUG
#ifdef _DEBUG

// definimos uma funcao de log de memoria - aqui por exemplo usamos printf mesmo
#define memlog printf

// mais uma vez, alguns typedefs
typedef unsigned char byte;
typedef unsigned int dword;

// estrutura que guarda informações da alocação
typedef struct _alloc_info
{
   void *address;
   dword size;
   dword line;
   bool isArray;
   char *file;
} alloc_info_t;

// elemento da lista
typedef struct _alloc_node
{
   alloc_info_t data;
   _alloc_node *next;
} alloc_node_t;

// funcoes que vao fazer todo o trabalho sujo
void registerMemBlock ( const void *addr, const dword size, const char *file, const dword line, const bool isArray = false );
void removeMemBlock ( void *addr );
void checkMemoryLeaks ();

// override dos operadores new e delete
inline void* operator new ( size_t size, const char *file, const dword line )
{
   void *ptr = malloc(size);
   registerMemBlock ( ptr, (dword)size, file, line );
   return ptr;
}
inline void* operator new[] ( size_t size, const char *file, const dword line )
{
   void *ptr = malloc(size);
   registerMemBlock( ptr, (dword)size, file, line, true);
   return ptr;
}
inline void* operator delete ( void *p )
{
   removeMemBlock(p);
   free(p);
}
inline void* operator delete[] ( void *p )
{
   removeMemBlock(p);
   free(p);
}

#define debug_new  new(__FILE__,__LINE__)
#else
#define debug_new new
#endif

// um pequeno truque
//  ( nao funciona com a STL - memory.h deve ser incluido sempre DEPOIS de inclusao da STL )
#define new debug_new

Definimos neste código duas estruturas – que serão utilizadas para armazenar as informações sobre a alocação – e as tres funções que mantem a lista. Tambem redefinimos os operadores new e delete para q seja mais transparente utilizar esse sistema. Aquele pequeno truque no final faz com q todo new use nossa versao, bastando adicionar nosso .h. Esse truque, no entanto, dá problemas se vc incluir o memory.h antes de incluir qualquer biblioteca da STL (a STL se perde com o novo formato do new), entao cuidado com isso. Voce tambem pode criar uma outra macro para new, evitando esse problema.

Vamos agora olhar o código interno das funcoes (memory.cpp):


#ifdef _DEBUG

// cabeça-de-lista
alloc_node_t *_memlist = 0;

void registerMemBlock ( const void *addr, const dword size, const char *file, const dword line, const bool isArray = false )
{
   alloc_node_t *node = (alloc_node_t*)malloc ( sizeof(alloc_node_t) );

   // coloca informacoes no node
   alloc_info_t *data = &(node->data);
   data->address = (void*)addr;
   data->file = (char*)fle;
   data->line = (dword)line;
   data->size = (dword)size;
   data->isArray = (bool)isArray;

   // coloca na lista
   if ( !_memlist )
   {
      _memlist = node;
      node->next = 0;
   } else {
      node->next = _memlist;
      _memlist = node;
   }
}

void removeMemBlock ( void *addr )
{
   // pega o cabeça-de-lista
   alloc_node_t *last = 0, *node = _memlist;

   // verificacao de sanidade
   if ( !addr || !node ) return;

   // se addr e cabeça de lista, remove corretamente
   if ( node->data->address == addr )
   {
      _memlist = node->next;
      free(node);
      return;
   }

   // procura o nó correto
   do
   {
      last = node;
      node = node->next;
      if ( !node ) return;
   } while ( node->data->address != addr );

   // remove da lista
   last->next = node->next;
   free(node);
}

// neste aqui está a mágica
void checkMemoryLeaks ()
{
   dword totalWasted = 0;
   alloc_info_t *info;
   alloc_node_t *node = _memlist;

   // verifica se temos furo de memoria
   if ( node ) memlog ( "\nFURO DE MEMORIA!!\n" );
   else return;

   while ( node )
   {
      // simplifica acesso aos dados
      info = node->data;
      // adiciona ao contador de desperdicio
      totalWasted += info->size;
      // log
      memlog ( "File: '%s' Line: %d Address: 0x%p Unfreed: %d bytes %s\n",
                             info->file, info->line, info->address, info->size,
                             (info->isArray ? "(Array)" : "") );
      // remove o node e libera a memoria
      node = node->next;
      free(node->address);
      free(node);
   }
   memlog ( "\nTotal de memória desperdiçada: %d bytes", totalWasted );
}

#endif

Tá aí toda a mágica. as primeiras duas funções apenas mantêm a lista de alocações. a terceira função vasculha toda a lista e loga todas as alocações ainda existentes na lista – esta funcao eh para ser usada APENAS no final da execução do programa (seja com at_exit, ou como ultima linha de comando da main).

Este sistema é extremamente simples e rude – sistemas avançados de profiling e análise de execução sao mais apropriados mas, para sistemas mais simples e/ou de aprendizagem, este código é muito util.

Existe tambem outra questao – se voce escreveu seu próprio alocador de memória, o controle já pode ir incluido diretamente na lógica desse alocador, nao sendo necessario entao este aqui. Em outro post irei explicar como fazer alguns tipos diferentes de alocadores de memória que podem nao só proteger o seu programa de furos de memória, mas também aumentar sensivelmente a performance da alocação e liberação

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.