No artigo O Primeiro Pixel — Janela, Desenho e Input com raylib movemos um retângulo pela tela, mas ele era só duas variáveis soltas, x e y. Um jogo real tem dezenas ou milhares de objetos — jogador, inimigos, projéteis, itens, tijolos —, cada um com posição, velocidade, aparência, comportamento. Gerenciar isso com variáveis soltas vira um caos rapidamente. Hoje aprendemos a estruturar entidades de jogo, e é aqui que um dos debates centrais do curso — composição sobre herança, da Fase 3 — retorna com força total e encontra sua aplicação mais convincente. Vamos conhecer o padrão ECS (Entity-Component-System), a forma como jogos modernos organizam seus objetos: não com hierarquias de classes, mas compondo entidades a partir de peças reutilizáveis chamadas componentes. Ao fim, seu retângulo solto virará uma entidade estruturada, e você entenderá por que a indústria de jogos abandonou a herança em favor da composição.
O problema: a hierarquia de classes que não escala
A tentação natural, vinda da orientação a objetos clássica, é modelar objetos de jogo com herança: uma classe base Entidade, e derivadas Jogador, Inimigo, Projetil. Parece limpo, até a realidade bater. E se um inimigo também precisa se mover como um projétil? E se um item pode ser destruído como um inimigo mas não se move? Você acaba com uma teia de heranças impossível — InimigoQueVoaEExplode herda de quê? A herança força cada objeto a ser exatamente um tipo numa árvore rígida, e objetos de jogo raramente se encaixam numa árvore. É o problema que o artigo Uma Classe que Nasce de Outra — Herança e a Cadeia de Construção já alertou: herança expressa "é um", e cria acoplamento forte; quando os objetos precisam de combinações flexíveis de características, a herança quebra.
// A ARMADILHA da herança para jogos — não escala.
class Entidade { /* ... */ };
class Movel : public Entidade { /* movimento */ };
class Desenhavel : public Entidade { /* aparência */ };
// E um objeto que é móvel E desenhável? Herança múltipla vira um nó.
// E um que às vezes se move e às vezes não? A árvore não comporta.
// Cada nova combinação de características exige uma nova classe. Explode.
A solução: compor entidades a partir de componentes
O ECS inverte a lógica. Em vez de perguntar "que tipo este objeto é?", pergunta "quais características este objeto tem?". Uma entidade não é uma classe numa hierarquia — é um conjunto de componentes, cada componente um dado simples que confere uma característica. Isto é composição pura, exatamente o "tem um" do artigo Uma Classe que Nasce de Outra — Herança e a Cadeia de Construção:
#include <raylib.h>
// COMPONENTES: peças de dados simples, cada uma uma característica.
struct Posicao {
float x, y;
};
struct Velocidade {
float dx, dy;
};
struct Aparencia {
int largura, altura;
Color cor;
};
// Uma ENTIDADE é a COMPOSIÇÃO de componentes — ela TEM as características.
struct Entidade {
Posicao posicao;
Velocidade velocidade;
Aparencia aparencia;
};
Agora um objeto é definido pelo que tem, não pelo que é. Um jogador tem posição, velocidade e aparência. Um tijolo tem posição e aparência, mas velocidade zero (não se move). Um item invisível tem posição mas não aparência. Você compõe cada entidade escolhendo componentes, sem hierarquia nenhuma. Adicionar uma nova característica (digamos, Vida) é criar um novo componente e dá-lo às entidades que precisam — sem tocar em classe alguma. É flexibilidade que a herança jamais oferece.
Os sistemas: comportamento que opera sobre componentes
Falta a terceira letra do ECS: o System (sistema). Se os componentes são os dados, os sistemas são o comportamento — funções que operam sobre entidades que têm certos componentes. Um sistema de movimento atualiza a posição de toda entidade que tem posição e velocidade; um sistema de renderização desenha toda entidade que tem posição e aparência:
#include <raylib.h>
#include <vector>
// SISTEMA de movimento: atualiza posições usando velocidades (e delta time).
void sistema_movimento(std::vector<Entidade>& entidades, float dt) {
for (auto& e : entidades) { // percorre com a STL (Fase 4)
e.posicao.x += e.velocidade.dx * dt; // delta time (games Um Novo Território)
e.posicao.y += e.velocidade.dy * dt;
}
}
// SISTEMA de renderização: desenha cada entidade.
void sistema_render(const std::vector<Entidade>& entidades) {
for (const auto& e : entidades) // const& : só lê (Fase 1)
DrawRectangle((int)e.posicao.x, (int)e.posicao.y,
e.aparencia.largura, e.aparencia.altura, e.aparencia.cor);
}
Repare na elegância e na convergência com o curso: as entidades vivem num std::vector (Fase 4), os sistemas as percorrem com range-based for e const& (Fases 1 e 4), e cada sistema tem uma responsabilidade única (a "função focada" do artigo A Sabedoria Codificada — Core Guidelines e Código Limpo em C++). Os dados (componentes nas entidades) e o comportamento (sistemas) estão separados — uma separação de responsabilidades que torna o código fácil de estender: um novo comportamento é um novo sistema, um novo dado é um novo componente, e eles se combinam livremente.
Juntando: um mini-ECS rodando
Vamos ver o ECS vivo — várias entidades, cada uma composta de componentes, movidas e desenhadas por sistemas, dentro do game loop:
#include <raylib.h>
#include <vector>
int main() {
Janela janela(800, 600, "ECS em ação"); // classe RAII do O Primeiro Pixel
// Cria entidades COMPONDO componentes — nenhuma herança.
std::vector<Entidade> entidades;
entidades.push_back({ {100, 100}, {150, 90}, {40, 40, RED} }); // move para baixo-direita
entidades.push_back({ {400, 300}, {-80, 60}, {30, 30, BLUE} }); // move para baixo-esquerda
entidades.push_back({ {600, 200}, {0, 0}, {50, 50, GREEN} }); // parada (velocidade 0)
while (!janela.deve_fechar()) {
float dt = GetFrameTime();
sistema_movimento(entidades, dt); // atualiza todas as posições
BeginDrawing();
ClearBackground(RAYWHITE);
sistema_render(entidades); // desenha todas
EndDrawing();
}
return 0;
}
Três entidades, compostas de componentes, movidas e desenhadas pelos sistemas — a verde fica parada porque sua velocidade é zero, sem precisar ser uma classe diferente. Adicionar uma quarta entidade é uma linha; adicionar um novo comportamento (digamos, gravidade) é um novo sistema que opera sobre a Velocidade. Esta é a arquitetura que escala de três para três mil objetos sem virar caos. E ela é a vitória final da composição sobre a herança que o artigo Uma Classe que Nasce de Outra — Herança e a Cadeia de Construção pregou: em jogos, onde objetos são combinações fluidas de características, compor é incomparavelmente melhor que herdar.
A honestidade sobre ECS
Prometo a franqueza de sempre. O que mostrei é um ECS didático e simplificado — o suficiente para entender o padrão e usá-lo em jogos pequenos como os nossos. ECS "de produção", usado em engines reais, é muito mais sofisticado: as entidades costumam ser apenas identificadores (números), os componentes são armazenados em arrays separados e contíguos por tipo (para máxima eficiência de cache — a lição de "vector é rápido" da Fase 4 levada ao extremo), e há mecanismos para consultar "todas as entidades que têm os componentes X e Y". Bibliotecas como EnTT implementam isso com muita engenhosidade. Não vamos a esse nível — seria uma trilha inteira própria, e para Pong, Snake e Breakout o ECS simples basta com folga. Mas saiba que o padrão escala muito além do que mostramos, e que a ideia central — composição de dados + sistemas de comportamento, em vez de hierarquias — é a mesma, do nosso mini-ECS aos engines AAA. O princípio é o que importa; a otimização vem quando os requisitos pedem.
Hierarquia de classes para objetos de jogo quebra no primeiro inimigo que também precisa voar e atirar: cada combinação nova pede uma classe, e a árvore vira um emaranhado. O ECS troca isso por composição — a entidade é um conjunto de componentes, e os sistemas operam sobre quem tiver os componentes certos. É a preferência por composição sobre herança encontrando o caso em que ela é mais evidentemente correta.
Fontes e leituras recomendadas
- Robert Nystrom, Game Programming Patterns, capítulo "Component": a apresentação canônica do padrão de componentes que fundamenta o ECS.
- github.com/skypjack/entt: a EnTT, a biblioteca ECS de alto desempenho mais popular em C++ — para ver o padrão em nível de produção.
- "Entity Component System" (na wiki do desenvolvimento de jogos e em austinmorlan.com): explicações acessíveis da arquitetura ECS.
- ISO C++ Core Guidelines, regras sobre composição (C.129, "prefer composition"): a base teórica do que o ECS aplica na prática.
- Mike Acton, "Data-Oriented Design" (palestra na CppCon 2014): a filosofia por trás do ECS de alto desempenho, sobre organizar dados para o cache.
Exercícios
Exercício 1
Explique por que modelar objetos de jogo com uma hierarquia de herança (Jogador : Entidade, Inimigo : Entidade) tende a não escalar. Dê um exemplo concreto de combinação de características que quebraria a hierarquia.
Ver resposta
✓ Resposta: A hierarquia de herança não escala porque força cada objeto a ser exatamente um tipo numa árvore rígida, enquanto objetos de jogo são combinações fluidas de características. Exemplo concreto que quebra a hierarquia: suponha que você tem Inimigo : Entidade (se move e ataca) e Obstaculo : Entidade (é sólido e destrutível mas não se move). Agora você quer uma "torre inimiga": ela é destrutível como um obstáculo, ataca como um inimigo, mas não se move como nenhum dos dois padrões. Onde ela entra na árvore? Herdar de Inimigo traz o movimento indesejado; herdar de Obstaculo não traz o ataque; herança múltipla dos dois traz conflitos e ambiguidades. Cada nova combinação de características (móvel+destrutível, estático+atacante, invisível+colidível...) exigiria uma nova classe ou um novo galho da árvore, e o número de combinações explode. A herança modela "é um" numa árvore; jogos precisam de "tem estas características" em combinações arbitrárias, que a árvore não comporta.
Exercício 2
Adicione um novo componente Vida { int pontos; } à Entidade e um sistema_dano que reduza a vida de todas as entidades em um valor. Explique como adicionar essa característica não exigiu criar nenhuma classe nova nem tocar nos sistemas existentes.
Ver resposta
✓ Resposta: Adicionando vida e dano:
struct Vida { int pontos; };
// Adiciona-se o componente à Entidade:
struct Entidade {
Posicao posicao;
Velocidade velocidade;
Aparencia aparencia;
Vida vida; // novo componente
};
void sistema_dano(std::vector<Entidade>& entidades, int dano) {
for (auto& e : entidades)
e.vida.pontos -= dano;
}
Adicionar essa característica não exigiu criar classe nova nem tocar nos sistemas existentes porque, no ECS, uma característica é apenas um dado (o componente Vida) e um comportamento (o sistema_dano), ambos independentes do resto. O sistema_movimento e o sistema_render continuam funcionando exatamente como antes — eles operam sobre Posicao/Velocidade e Posicao/Aparencia respectivamente, e ignoram completamente o novo componente Vida. Não há hierarquia a reorganizar, nenhuma classe base a modificar. Você apenas acrescentou uma peça, e ela coexiste com as demais. É a extensibilidade que a composição oferece e a herança nega.
Exercício 3
Explique a separação entre componentes (dados) e sistemas (comportamento) no ECS. Como essa separação se relaciona com o princípio de "funções e classes focadas" do artigo A Sabedoria Codificada — Core Guidelines e Código Limpo em C++?
Ver resposta
✓ Resposta: No ECS, os componentes são estruturas de dados puros (posição, velocidade, aparência) — eles não têm comportamento, só guardam o estado que caracteriza uma entidade. Os sistemas são funções de comportamento — eles contêm a lógica que opera sobre os dados dos componentes (mover, desenhar, aplicar dano). Essa separação relaciona-se diretamente com o princípio de "funções e classes focadas" do artigo A Sabedoria Codificada — Core Guidelines e Código Limpo em C++: cada sistema tem uma única responsabilidade bem definida (o sistema_movimento só move, o sistema_render só desenha), tornando cada um pequeno, testável e fácil de entender; e cada componente representa uma única característica, sem misturar dados não relacionados. Ao manter dados e comportamento separados e cada peça focada, o código evita as classes "faz-tudo" que acumulam responsabilidades e viram difíceis de manter. A separação também permite raciocinar sobre cada aspecto isoladamente — você entende o movimento olhando só o sistema de movimento, sem se distrair com renderização ou dano.
Exercício 4
No mini-ECS da aula, a entidade verde fica parada porque sua velocidade é {0, 0}. Explique por que isso é mais elegante do que ter uma classe separada EntidadeEstatica. O que o sistema_movimento faz com ela?
Ver resposta
✓ Resposta: Ter a entidade verde parada via velocidade {0, 0} é mais elegante que uma classe EntidadeEstatica separada porque evita criar um tipo novo só para expressar uma ausência de movimento — o que seria uma proliferação de classes para cada combinação de comportamentos. No ECS, "estar parado" não é um tipo diferente de entidade; é apenas um valor de um componente que toda entidade já tem. A verde é uma entidade comum, com os mesmos componentes das outras, apenas com velocidade zero. Isso mantém todas as entidades uniformes (o mesmo tipo Entidade, o mesmo vector, os mesmos sistemas), sem exceções nem hierarquia. O que o sistema_movimento faz com ela: aplica a mesma fórmula — posicao.x += velocidade.dx * dt —, mas como velocidade.dx e velocidade.dy são zero, a posição não muda. O sistema não precisa de nenhum caso especial para entidades paradas; a matemática naturalmente as deixa imóveis. Uniformidade e simplicidade: um só caminho de código trata objetos móveis e estáticos.
Exercício 5
Descreva como você adicionaria "gravidade" ao mini-ECS: que sistema novo criaria, sobre qual componente ele operaria, e por que isso não exige modificar as entidades nem os outros sistemas. Esboce o sistema_gravidade.
Ver resposta
✓ Resposta: Para adicionar gravidade, eu criaria um sistema_gravidade que opera sobre o componente Velocidade, aumentando continuamente a velocidade vertical (dy) para baixo, simulando a aceleração da gravidade:
void sistema_gravidade(std::vector<Entidade>& entidades, float dt) {
const float g = 980.0f; // aceleração ~ pixels por segundo ao quadrado
for (auto& e : entidades)
e.velocidade.dy += g * dt; // acelera para baixo a cada frame
}
Isso não exige modificar as entidades nem os outros sistemas porque: (a) o componente sobre o qual ele opera, Velocidade, já existe — a gravidade apenas o altera, sem precisar de um novo dado; (b) os outros sistemas não são afetados — o sistema_movimento continua aplicando velocidade à posicao como sempre, e agora a velocidade vertical crescente (produzida pela gravidade) naturalmente faz as entidades caírem cada vez mais rápido, sem que o sistema de movimento saiba da existência da gravidade. Você apenas adiciona o sistema_gravidade ao game loop (chamando-o antes do sistema_movimento), e a nova física emerge da composição dos sistemas. É a extensibilidade do ECS: novos comportamentos são novos sistemas que se encaixam sem perturbar os existentes — a mesma lição da gravidade no exercício do Entidades de Verdade, agora formalizada.