Arquivo para a Tag ‘Programming’
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!
Comentários (1)
Comentários (1)