ECS Pattern em Zig: Entity Component System para Jogos

O Entity Component System (ECS) e o padrao arquitetural dominante no desenvolvimento de jogos moderno. Diferente da abordagem orientada a objetos tradicional (heranca profunda), ECS favorece composicao sobre heranca e organiza dados de forma cache-friendly. Zig, com seu foco em data-oriented design e controle de memoria, e uma linguagem ideal para implementar ECS.

Para entender melhor as estruturas de dados usadas aqui, consulte Zig Data Structures.

O Que e ECS?

ECS divide a logica do jogo em tres conceitos:

ConceitoO que eExemplo
EntityUm identificador unico (apenas um numero)Jogador, inimigo, projetil
ComponentDados puros, sem logicaPosicao, Velocidade, Sprite, Vida
SystemLogica que opera sobre componentesMovimentoSystem, RenderSystem
Abordagem OO Tradicional:     Abordagem ECS:
┌──────────────────┐           Entity 1: [Posicao, Sprite, Vida]
│  class Jogador   │           Entity 2: [Posicao, Sprite, IA]
│  ├─ posicao      │           Entity 3: [Posicao, Velocidade]
│  ├─ sprite       │
│  ├─ vida         │           Systems:
│  ├─ mover()      │           - MovimentoSystem(Posicao, Velocidade)
│  └─ renderizar() │           - RenderSystem(Posicao, Sprite)
└──────────────────┘           - VidaSystem(Vida)

Implementando um ECS Simples

Definindo Componentes

Componentes sao structs de dados puros:

const std = @import("std");

pub const Posicao = struct {
    x: f32 = 0,
    y: f32 = 0,
};

pub const Velocidade = struct {
    vx: f32 = 0,
    vy: f32 = 0,
};

pub const Sprite = struct {
    textura_id: u32 = 0,
    largura: u32 = 32,
    altura: u32 = 32,
    cor_r: u8 = 255,
    cor_g: u8 = 255,
    cor_b: u8 = 255,
};

pub const Vida = struct {
    atual: f32 = 100,
    maxima: f32 = 100,
};

pub const Colisao = struct {
    largura: f32 = 32,
    altura: f32 = 32,
    solido: bool = true,
};

pub const IA = struct {
    tipo: enum { patrulha, perseguicao, fuga } = .patrulha,
    raio_deteccao: f32 = 200,
    alvo_x: f32 = 0,
    alvo_y: f32 = 0,
};

O Mundo ECS

const std = @import("std");
const comps = @import("components.zig");

pub const EntityId = u32;
pub const MAX_ENTITIES: u32 = 10000;

pub const World = struct {
    // Armazenamento de componentes (Struct of Arrays)
    posicoes: [MAX_ENTITIES]?comps.Posicao = .{null} ** MAX_ENTITIES,
    velocidades: [MAX_ENTITIES]?comps.Velocidade = .{null} ** MAX_ENTITIES,
    sprites: [MAX_ENTITIES]?comps.Sprite = .{null} ** MAX_ENTITIES,
    vidas: [MAX_ENTITIES]?comps.Vida = .{null} ** MAX_ENTITIES,
    colisoes: [MAX_ENTITIES]?comps.Colisao = .{null} ** MAX_ENTITIES,
    ias: [MAX_ENTITIES]?comps.IA = .{null} ** MAX_ENTITIES,

    // Gerenciamento de entidades
    entidades_ativas: [MAX_ENTITIES]bool = .{false} ** MAX_ENTITIES,
    proxima_entidade: EntityId = 0,
    contagem: u32 = 0,

    pub fn criarEntidade(self: *World) !EntityId {
        // Buscar slot livre
        var id = self.proxima_entidade;
        while (id < MAX_ENTITIES) : (id += 1) {
            if (!self.entidades_ativas[id]) {
                self.entidades_ativas[id] = true;
                self.contagem += 1;
                self.proxima_entidade = id + 1;
                return id;
            }
        }
        return error.SemEspacoParaEntidades;
    }

    pub fn destruirEntidade(self: *World, id: EntityId) void {
        if (id >= MAX_ENTITIES) return;

        self.entidades_ativas[id] = false;
        self.posicoes[id] = null;
        self.velocidades[id] = null;
        self.sprites[id] = null;
        self.vidas[id] = null;
        self.colisoes[id] = null;
        self.ias[id] = null;
        self.contagem -= 1;

        if (id < self.proxima_entidade) {
            self.proxima_entidade = id;
        }
    }

    // Helpers para adicionar componentes
    pub fn adicionarPosicao(self: *World, id: EntityId, pos: comps.Posicao) void {
        self.posicoes[id] = pos;
    }

    pub fn adicionarVelocidade(self: *World, id: EntityId, vel: comps.Velocidade) void {
        self.velocidades[id] = vel;
    }

    pub fn adicionarSprite(self: *World, id: EntityId, sprite: comps.Sprite) void {
        self.sprites[id] = sprite;
    }

    pub fn adicionarVida(self: *World, id: EntityId, vida: comps.Vida) void {
        self.vidas[id] = vida;
    }
};

Sistemas

Sistemas sao funcoes que operam sobre entidades com componentes especificos:

const World = @import("world.zig").World;
const rl = @cImport(@cInclude("raylib.h"));

// Sistema de movimento: requer Posicao + Velocidade
pub fn movimentoSystem(world: *World, dt: f32) void {
    for (0..World.MAX_ENTITIES) |i| {
        if (!world.entidades_ativas[i]) continue;

        if (world.posicoes[i]) |*pos| {
            if (world.velocidades[i]) |vel| {
                pos.x += vel.vx * dt;
                pos.y += vel.vy * dt;
            }
        }
    }
}

// Sistema de renderizacao: requer Posicao + Sprite
pub fn renderSystem(world: *World) void {
    for (0..World.MAX_ENTITIES) |i| {
        if (!world.entidades_ativas[i]) continue;

        if (world.posicoes[i]) |pos| {
            if (world.sprites[i]) |sprite| {
                rl.DrawRectangle(
                    @intFromFloat(pos.x),
                    @intFromFloat(pos.y),
                    @intCast(sprite.largura),
                    @intCast(sprite.altura),
                    rl.Color{
                        .r = sprite.cor_r,
                        .g = sprite.cor_g,
                        .b = sprite.cor_b,
                        .a = 255,
                    },
                );
            }
        }
    }
}

// Sistema de vida: remove entidades com vida <= 0
pub fn vidaSystem(world: *World) void {
    for (0..World.MAX_ENTITIES) |i| {
        if (!world.entidades_ativas[i]) continue;

        if (world.vidas[i]) |vida| {
            if (vida.atual <= 0) {
                world.destruirEntidade(@intCast(i));
            }
        }
    }
}

// Sistema de limites: mantem entidades dentro da tela
pub fn limitesSystem(world: *World, largura: f32, altura: f32) void {
    for (0..World.MAX_ENTITIES) |i| {
        if (!world.entidades_ativas[i]) continue;

        if (world.posicoes[i]) |*pos| {
            if (pos.x < 0) pos.x = 0;
            if (pos.y < 0) pos.y = 0;
            if (pos.x > largura) pos.x = largura;
            if (pos.y > altura) pos.y = altura;
        }
    }
}

Montando Tudo

const std = @import("std");
const World = @import("world.zig").World;
const systems = @import("systems.zig");
const rl = @cImport(@cInclude("raylib.h"));

fn criarJogador(world: *World) !void {
    const id = try world.criarEntidade();
    world.adicionarPosicao(id, .{ .x = 400, .y = 300 });
    world.adicionarVelocidade(id, .{});
    world.adicionarSprite(id, .{ .cor_r = 0, .cor_g = 100, .cor_b = 255 });
    world.adicionarVida(id, .{ .atual = 100, .maxima = 100 });
}

fn criarInimigos(world: *World, quantidade: u32) !void {
    var prng = std.Random.DefaultPrng.init(42);
    const random = prng.random();

    for (0..quantidade) |_| {
        const id = try world.criarEntidade();
        world.adicionarPosicao(id, .{
            .x = random.float(f32) * 800,
            .y = random.float(f32) * 600,
        });
        world.adicionarVelocidade(id, .{
            .vx = (random.float(f32) - 0.5) * 100,
            .vy = (random.float(f32) - 0.5) * 100,
        });
        world.adicionarSprite(id, .{
            .cor_r = 255,
            .cor_g = 50,
            .cor_b = 50,
            .largura = 16,
            .altura = 16,
        });
        world.adicionarVida(id, .{ .atual = 50, .maxima = 50 });
    }
}

pub fn main() !void {
    rl.InitWindow(800, 600, "ECS em Zig");
    defer rl.CloseWindow();
    rl.SetTargetFPS(60);

    var world = World{};

    try criarJogador(&world);
    try criarInimigos(&world, 100);

    while (!rl.WindowShouldClose()) {
        const dt = rl.GetFrameTime();

        // Executar sistemas
        systems.movimentoSystem(&world, dt);
        systems.limitesSystem(&world, 800, 600);
        systems.vidaSystem(&world);

        // Render
        rl.BeginDrawing();
        rl.ClearBackground(rl.BLACK);
        systems.renderSystem(&world);

        var buf: [64]u8 = undefined;
        const info = std.fmt.bufPrint(&buf, "Entidades: {d}", .{world.contagem}) catch "???";
        rl.DrawText(@ptrCast(info.ptr), 10, 10, 16, rl.WHITE);
        rl.DrawFPS(10, 30);
        rl.EndDrawing();
    }
}

Por Que ECS e Cache-Friendly?

A organizacao Struct of Arrays (SoA) que usamos garante que componentes do mesmo tipo ficam contiguos na memoria:

Array of Structs (AoS) - Ruim para cache:
[Entity1{pos,vel,sprite}] [Entity2{pos,vel,sprite}] [Entity3{pos,vel,sprite}]
 ^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^^^^^^^^^^^^^^^^^^^^
 Dados dispersos: para processar todas as posicoes,
 o cache precisa carregar dados irrelevantes (vel, sprite)

Struct of Arrays (SoA) - Bom para cache:
Posicoes:    [pos1] [pos2] [pos3] ...  <- Contiguos na memoria!
Velocidades: [vel1] [vel2] [vel3] ...  <- Contiguos na memoria!
Sprites:     [spr1] [spr2] [spr3] ...  <- Contiguos na memoria!

MovimentoSystem percorre posicoes e velocidades sequencialmente
=> Excelente utilizacao de cache lines

Para entender mais sobre otimizacao de cache, veja Cache-Friendly Programming em Zig.

Exercicios

  1. Sistema de colisao: Implemente um sistema que detecte colisoes entre entidades usando AABB (Axis-Aligned Bounding Box).

  2. Spawner: Crie um sistema que gere novas entidades periodicamente em posicoes aleatorias.

  3. Archetypes: Refatore o ECS para usar archetypes — agrupamentos de entidades com os mesmos componentes para iteracao ainda mais eficiente.


Proximo Artigo

No artigo final da serie, implementamos audio e fisica e integramos todos os sistemas em um jogo completo.

Conteudo Relacionado


Duvidas sobre ECS em Zig? Junte-se a comunidade Zig Brasil!

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.