No artigo Quando as Coisas se Tocam — Colisão e o Primeiro Jogo: Pong construímos o Pong, mas ele era feito de retângulos brancos e silêncio absoluto. Jogos de verdade têm imagens — sprites, texturas, fundos — e som — efeitos, música. Hoje aprendemos a carregar e usar esses recursos, e, fiel ao curso, a gerenciá-los com RAII (Fase 2). Porque uma textura não é um dado qualquer: ela ocupa memória da placa de vídeo, e precisa ser liberada quando não é mais usada — o LoadTexture/UnloadTexture da raylib é o fopen/fclose do Nascimento e Morte de um Objeto e o InitWindow/CloseWindow do O Primeiro Pixel de novo, e você já sabe domá-lo. Envolveremos texturas e sons em objetos RAII, para que carreguem no construtor e liberem no destrutor, sem vazamentos. E começaremos a construir o Snake, nosso segundo jogo, introduzindo a ideia de uma grade e o crescimento da cobra. O jogo vai ganhar cara e voz.
Texturas são recursos: o problema do gerenciamento manual
Uma textura é uma imagem carregada na memória da GPU. Carregá-la e liberá-la são operações explícitas na raylib, com o perigo de sempre — se você carregar e esquecer de liberar (ou uma exceção pular a liberação), a memória da GPU vaza:
// Estilo manual — PERIGOSO (o padrão que já sabemos evitar).
Texture2D textura = LoadTexture("jogador.png"); // carrega na GPU
// ... usa a textura ...
// Se um return antecipado ou exceção ocorrer aqui, o unload abaixo é pulado → VAZA.
UnloadTexture(textura); // precisa lembrar SEMPRE
É exatamente o mesmo padrão de recurso que enfrentamos com arquivos, memória, conexões de banco e a janela. E a solução é exatamente a mesma: RAII.
Envolvendo textura e som em RAII
Criamos classes que carregam o recurso no construtor e o liberam no destrutor, com cópia proibida (um recurso de dono único) e movimento permitido — a Regra dos Cinco do Quando Copiar Dá Errado aplicada a recursos gráficos:
#include <raylib.h>
#include <string>
#include <stdexcept>
// Envolve uma textura da raylib em RAII.
class Textura {
public:
explicit Textura(const std::string& caminho) {
tex_ = LoadTexture(caminho.c_str()); // carrega na GPU (adquire)
if (tex_.id == 0) // id 0 = falha ao carregar
throw std::runtime_error("falha ao carregar textura: " + caminho);
}
~Textura() {
if (tex_.id != 0) UnloadTexture(tex_); // libera automaticamente
}
// Recurso de dono único: proíbe cópia (Fase 2/8).
Textura(const Textura&) = delete;
Textura& operator=(const Textura&) = delete;
// Permite movimento: transfere a posse (Fase 2, Regra dos Cinco).
Textura(Textura&& outro) noexcept : tex_(outro.tex_) {
outro.tex_.id = 0; // o movido não deve liberar o que já não é seu
}
// Desenha a textura numa posição.
void desenha(int x, int y) const { DrawTexture(tex_, x, y, WHITE); }
const Texture2D& bruta() const { return tex_; }
private:
Texture2D tex_{};
};
A classe Som seguiria o mesmo molde, com LoadSound/UnloadSound e um método toca() que chama PlaySound. Contemple a repetição do padrão: exatamente como o BancoSQLite (Extensão) e a Janela (O Primeiro Pixel), carregamos no construtor, liberamos no destrutor, proibimos cópia e permitimos movimento. Um std::vector<Textura> ou um std::map<std::string, Textura> gerenciaria uma coleção de sprites, cada um liberado automaticamente quando o container morre (a Regra do Zero, Fase 2). Os princípios do curso não mudam com o domínio — eles domam recursos gráficos com a mesma elegância com que domaram arquivos e conexões. Você aprende o padrão uma vez e o aplica a tudo.
Um gerenciador de recursos
Jogos carregam muitos recursos e frequentemente os reutilizam (a mesma textura de tijolo aparece cem vezes). Carregar a mesma imagem cem vezes seria desperdício. O padrão é um gerenciador de recursos que carrega cada um uma vez e o compartilha — e aqui os shared_ptr da Fase 2 encontram seu uso natural, pois vários objetos de jogo compartilham a mesma textura:
#include <memory>
#include <unordered_map>
#include <string>
// Carrega cada textura uma vez e a compartilha entre quem precisar.
class GerenciadorTexturas {
public:
// Devolve a textura, carregando-a se ainda não estiver em cache.
std::shared_ptr<Textura> obter(const std::string& caminho) {
auto it = cache_.find(caminho);
if (it != cache_.end())
return it->second; // já carregada: compartilha (Fase 2)
auto tex = std::make_shared<Textura>(caminho);
cache_[caminho] = tex; // guarda no cache
return tex;
}
private:
std::unordered_map<std::string, std::shared_ptr<Textura>> cache_; // Fase 4
};
Aqui o shared_ptr (Fase 2) brilha com propósito genuíno: uma textura é legitimamente compartilhada por muitas entidades (todos os inimigos usam a mesma imagem), então a posse compartilhada é o modelo certo — a textura é liberada quando a última entidade que a usa (e o cache) some. É o caso de uso do shared_ptr que o artigo Posse Compartilhada e seus Perigos — shared_ptr e weak_ptr descreveu, encontrado na vida real. O unordered_map (Fase 4) indexa por caminho, evitando recarregar. Este pequeno gerenciador reúne ponteiros inteligentes, containers e RAII num padrão que jogos reais usam.
Começando o Snake: a grade
Vamos iniciar nosso segundo jogo, o Snake, que introduz um conceito novo: a grade. Diferente do Pong (movimento livre e contínuo), o Snake acontece numa grade de células, e a cobra se move de célula em célula. Modelamos a cobra como uma sequência de posições de grade — e um std::vector (Fase 4) é perfeito para isso:
#include <raylib.h>
#include <vector>
struct Celula { int x, y; }; // posição na GRADE (não em pixels)
const int TAM_CELULA = 20; // cada célula tem 20x20 pixels
const int COLUNAS = 40, LINHAS = 30; // grade de 40x30 células (800x600)
int main() {
Janela janela(800, 600, "Snake");
// A cobra é uma sequência de células; a cabeça é o primeiro elemento.
std::vector<Celula> cobra = {{20, 15}, {19, 15}, {18, 15}}; // começa com 3 segmentos
Celula direcao = {1, 0}; // movendo para a direita
while (!janela.deve_fechar()) {
// (a lógica de movimento e crescimento vem no próximo artigo)
// --- RENDER: desenha cada segmento como um quadrado na grade ---
BeginDrawing();
ClearBackground(BLACK);
for (const auto& seg : cobra) // percorre a cobra (Fase 4)
DrawRectangle(seg.x * TAM_CELULA, seg.y * TAM_CELULA,
TAM_CELULA - 1, TAM_CELULA - 1, GREEN);
EndDrawing();
}
return 0;
}
A cobra é um std::vector<Celula> — a cabeça no início, o rabo no fim. Cada segmento é desenhado como um quadrado, convertendo a posição de grade (coluna, linha) para pixels (multiplicando por TAM_CELULA). Essa distinção entre coordenadas de grade e de pixel é a essência do Snake, e o vector modela a cobra naturalmente: crescer é adicionar um elemento, mover é adicionar uma cabeça nova e remover o rabo. No próximo artigo completamos a lógica. Por ora, você tem a cobra desenhada e a estrutura montada — e ela já usa o vector da Fase 4 como sua espinha.
Textura e som entram na mesma lista de recursos que arquivo, memória, conexão e janela: alguém precisa liberá-los, e o destrutor é o lugar certo. A diferença aqui é a posse — a mesma textura costuma ser usada por muitos objetos ao mesmo tempo, e é isso que justifica shared_ptr num gerenciador, quando o padrão do curso inteiro foi começar pelo dono único.
Fontes e leituras recomendadas
- raylib.com/cheatsheet: as funções
LoadTexture,UnloadTexture,DrawTexture,LoadSound,PlaySound— os recursos gerenciados aqui . - Robert Nystrom, Game Programming Patterns, capítulo "Object Pool" e discussões sobre recursos: padrões de gerenciamento de recursos em jogos.
- cppreference.com/w/cpp/memory/shared_ptr: revisão do
shared_ptr, cujo uso no gerenciador de recursos é o caso canônico de posse compartilhada. - "Snake game" (a história e as variações do clássico): contexto sobre o jogo que começamos a construir.
- raylib examples de texturas e áudio (raylib.com/examples): demonstrações práticas de carregamento e uso de recursos.
Exercícios
Exercício 1
Explique por que uma textura precisa ser gerenciada com RAII, comparando-a com os outros recursos que envolvemos no curso (arquivo, memória, conexão de banco, janela). O que todos têm em comum?
Ver resposta
✓ Resposta: Uma textura precisa de RAII porque é um recurso — ela ocupa memória da placa de vídeo (GPU), que é finita e deve ser explicitamente liberada quando a textura não é mais necessária. Se você carregar texturas e esquecer de liberá-las (ou uma exceção pular a liberação), a memória da GPU vaza, e um jogo que carrega muitas texturas ao longo do tempo acabaria esgotando-a e travando. Isso é idêntico aos outros recursos do curso: um arquivo (artigo Nascimento e Morte de um Objeto — Construtores, Destrutores e o RAII na Prática) ocupa um descritor do sistema que deve ser fechado; a memória do heap (Fase 2) deve ser liberada; uma conexão de banco (Extensão) deve ser fechada; a janela (O Primeiro Pixel) deve ser destruída. O que todos têm em comum é o padrão adquirir/liberar: um recurso finito do sistema que você obtém explicitamente e é obrigado a devolver, sob pena de vazamento. E a solução é sempre a mesma — RAII: envolver o recurso num objeto que o adquire no construtor e o libera no destrutor, amarrando a liberação ao tempo de vida do objeto, garantindo-a em todos os caminhos de saída. É a universalidade do RAII: um princípio, aplicável a qualquer recurso.
Exercício 2
Escreva uma classe Som RAII análoga à Textura, com LoadSound/UnloadSound no construtor/destrutor, cópia proibida, movimento permitido, e um método toca().
Ver resposta
✓ Resposta: Classe Som RAII:
#include <raylib.h>
#include <string>
#include <stdexcept>
class Som {
public:
explicit Som(const std::string& caminho) {
som_ = LoadSound(caminho.c_str());
if (som_.frameCount == 0) // falha ao carregar
throw std::runtime_error("falha ao carregar som: " + caminho);
}
~Som() {
if (som_.frameCount != 0) UnloadSound(som_);
}
Som(const Som&) = delete;
Som& operator=(const Som&) = delete;
Som(Som&& outro) noexcept : som_(outro.som_) {
outro.som_.frameCount = 0; // o movido não libera
}
void toca() const { PlaySound(som_); }
private:
Sound som_{};
};
Segue exatamente o molde da Textura: carrega no construtor (lançando se falhar), libera no destrutor, proíbe cópia (recurso de dono único), permite movimento (transferindo a posse e zerando a origem), e oferece toca() como interface de uso. O mesmo padrão RAII, aplicado ao som.
Exercício 3
No GerenciadorTexturas, explique por que o shared_ptr é a escolha certa (e não o unique_ptr). Que característica do uso de texturas em jogos justifica a posse compartilhada?
Ver resposta
✓ Resposta: No GerenciadorTexturas, o shared_ptr é a escolha certa porque uma textura em jogos é genuinamente compartilhada por múltiplos objetos ao mesmo tempo: a mesma imagem (digamos, o sprite de um inimigo) é usada por todos os inimigos na tela simultaneamente, além de estar guardada no cache do gerenciador. Ou seja, há vários "donos" legítimos da mesma textura vivos ao mesmo tempo, e a textura só deve ser liberada quando o último deles (a última entidade que a usa, mais o cache) parar de precisar dela. Essa é exatamente a semântica de posse compartilhada que o shared_ptr implementa via contagem de referências — a textura vive enquanto houver ao menos um shared_ptr apontando para ela, e é liberada automaticamente quando a contagem chega a zero. O unique_ptr não serviria porque ele impõe um único dono: não permitiria que várias entidades e o cache compartilhassem a mesma textura, forçando ou cópias (desperdício) ou uma gestão manual de quem é o "dono verdadeiro". A característica que justifica a posse compartilhada é o reúso simultâneo por muitas entidades — precisamente o cenário para o qual o shared_ptr foi feito (artigo Posse Compartilhada e seus Perigos — shared_ptr e weak_ptr).
Exercício 4
Explique a distinção entre coordenadas de grade (coluna, linha) e de pixel no Snake. Por que a cobra é modelada em coordenadas de grade, e como se converte para pixels na hora de desenhar?
Ver resposta
✓ Resposta: Coordenadas de grade referem-se à posição em termos de células — coluna e linha (por exemplo, "coluna 20, linha 15"), como um tabuleiro de xadrez. Coordenadas de pixel referem-se à posição real na tela em pixels (por exemplo, "pixel 400, pixel 300"). A cobra é modelada em coordenadas de grade porque o Snake é, por natureza, um jogo baseado em grade: a cobra ocupa células inteiras e se move de célula em célula (não continuamente), a comida aparece numa célula, e as colisões são "a cabeça está na mesma célula que...". Pensar em células torna toda a lógica simples e discreta (números inteiros, comparações exatas), evitando a imprecisão de ponto flutuante. Na hora de desenhar, converte-se de grade para pixel multiplicando pela dimensão da célula: um segmento na célula (coluna, linha) é desenhado no pixel (coluna * TAM_CELULA, linha * TAM_CELULA). Assim, a célula (20, 15) com TAM_CELULA = 20 vira o pixel (400, 300). A grade governa a lógica; a conversão para pixel governa a exibição.
Exercício 5
A cobra é um std::vector<Celula>. Descreva, conceitualmente, como as operações de "mover a cobra" e "fazer a cobra crescer" se traduzem em operações sobre esse vetor. (Dica: mover envolve adicionar uma cabeça e remover um rabo; crescer, só adicionar a cabeça.)
Ver resposta
✓ Resposta: As operações sobre o std::vector<Celula> que representa a cobra:
- Mover a cobra: adiciona-se uma nova cabeça na direção do movimento (a célula adjacente à cabeça atual, no sentido em que a cobra vai) no início do vetor, e remove-se o último elemento (o rabo) do fim do vetor. O efeito líquido é que a cobra "desliza" uma célula: ganha uma célula na frente e perde uma atrás, mantendo o mesmo comprimento. Em código, seria algo como cobra.insert(cobra.begin(), nova_cabeca); cobra.pop_back();.
- Fazer a cobra crescer (quando come a comida): adiciona-se a nova cabeça no início, mas não se remove o rabo. Assim a cobra fica um segmento mais longa — ela avançou uma célula na frente sem encurtar atrás. Em código: cobra.insert(cobra.begin(), nova_cabeca); (sem o pop_back).
A elegância é que o std::vector modela a cobra naturalmente: a sequência ordenada de segmentos (cabeça no início, rabo no fim) é exatamente uma sequência, e crescer/mover são inserções e remoções nas pontas. (Nota de eficiência: inserir no início de um vector é O(n) porque desloca todos os elementos; num jogo real, um std::deque — fila de duas pontas — seria mais eficiente para adicionar/remover nas duas extremidades, ou manter a cabeça no fim. Para o Snake, cujo tamanho é modesto, o vector serve bem, mas é uma otimização legítima que a Fase 4 nos ensinou a considerar.)