Entidades de Verdade — Componentes e o Padrão ECS

Entidades de Verdade — Componentes e o Padrão ECS

Modelar objetos de jogo por herança quebra no primeiro inimigo que também voa e atira: cada combinação nova pede uma classe. O ECS troca a árvore por composição — a entidade é um conjunto de componentes e os sistemas operam sobre quem os tiver —, e é onde a preferência por composição fica mais evidente.
Linguagem C++

13 min de leitura

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.

Comentários

Mais em Linguagem C++

Ensinando Símbolos ao seu Tipo — Sobrecarga de Operadores
Ensinando Símbolos ao seu Tipo — Sobrecarga de Operadores

Somar com mais, comparar com igual, imprimir direto no cout: operadores são…

A Disciplina do const — Promessas que o Compilador Cobra
A Disciplina do const — Promessas que o Compilador Cobra

Em C o const é etiqueta modesta, que muita gente ignora sem prejuízo. Em C++…

Provando que Funciona — Testes Automatizados com Catch2
Provando que Funciona — Testes Automatizados com Catch2

Rodar e olhar a saída não protege contra regressão, porque ninguém repete a…