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:
| Conceito | O que e | Exemplo |
|---|---|---|
| Entity | Um identificador unico (apenas um numero) | Jogador, inimigo, projetil |
| Component | Dados puros, sem logica | Posicao, Velocidade, Sprite, Vida |
| System | Logica que opera sobre componentes | MovimentoSystem, 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
Sistema de colisao: Implemente um sistema que detecte colisoes entre entidades usando AABB (Axis-Aligned Bounding Box).
Spawner: Crie um sistema que gere novas entidades periodicamente em posicoes aleatorias.
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
- Artigo anterior: Input Handling
- Zig Design Patterns — Mais padroes de projeto
- Otimizacao de Performance — Data-oriented design
Duvidas sobre ECS em Zig? Junte-se a comunidade Zig Brasil!