A arquitetura Entity Component System (ECS) representa uma abordagem paradigmática para o design de software, especialmente proeminente no desenvolvimento de jogos, como no motor Unity com o DOTS (Data-Oriented Technology Stack). Ao dissociar a lógica de negócios dos dados, o ECS oferece um caminho robusto para otimizar o uso da memória e a eficiência do cache da CPU, resultando em ganhos substanciais de desempenho. Diversos princípios fundamentais contribuem para a eficácia do ECS:
1. Design Orientado a Dados (Data-Oriented Design)
O ECS abraça o Design Orientado a Dados (DOD), uma filosofia que prioriza a organização e o processamento dos dados em detrimento da encapsulação tradicional da Programação Orientada a Objetos (POO). Enquanto a POO frequentemente une dados e comportamento em objetos, o ECS os separa: dados são armazenados em componentes e a lógica é executada por sistemas. Essa dissociação permite que os dados sejam organizados de forma mais compacta na memória, reduzindo a fragmentação e elevando as taxas de acerto de cache.
2. Disposição Otimizada da Memória
Ao agrupar componentes do mesmo tipo em blocos contíguos de memória – uma técnica conhecida como "Structure of Arrays" (SoA) – o ECS garante uma disposição de memória ideal. Essa organização facilita o carregamento eficiente de dados no cache da CPU, minimizando as falhas de cache (cache misses) e acelerando significativamente o acesso aos dados.
3. Processamento Concorrente
O design do ECS inerentemente suporta o processamento paralelo de múltiplas entidades. Como cada sistema se concentra apenas em tipos específicos de componentes, os desenvolvedores podem empregar multithreading para executar vários sistemas simultaneamente. Essa capacidade de paralelização explora plenamente o potencial das CPUs modernas com múltiplos núcleos, impulsionando a capacidade de computação.
4. Redução de Cálculos Desnecessários
No ECS, os sistemas operam apenas sobre os componentes que exigem atualização. Isso implica que, se o estado de uma entidade específica não se alterou, os cálculos associados podem ser omitidos. Essa abordagem de cálculo sob demanda minimiza a sobrecarga computacional supérflua, melhorando o desempenho geral.
5. Maior Escalabilidade
A arquitetura do ECS simplifica a introdução de novas funcionalidades e a modificação das existentes. Os desenvolvedores podem estender a lógica do jogo adicionando novos componentes e sistemas, sem a necessidade de alterar o código existente. Essa flexibilidade aprimora a agilidade do desenvolvimento e mitiga potenciais perdas de desempenho.
6. Adaptabilidade a SIMD e Aceleração por GPU
A forma como o ECS organiza e processa os dados o torna particularmente adequado para aproveitar as instruções SIMD (Single Instruction, Multiple Data) e a aceleração por GPU. Ao estruturar os dados para processamento em paralelo, o ECS pode capitalizar as capacidades de computação paralela do hardware, elevando ainda mais o desempenho.
7. Minimização de Criação e Destruição de Objetos
Em ambientes de POO tradicionais, a criação e destruição frequente de objetos pode gerar custos significativos com alocação de memória e coleta de lixo. O ECS, por outro lado, promove a reutilização de componentes, reduzindo a necessidade de instanciar e descartar objetos constantemente, o que diminui a sobrecarga de gerenciamento de memória.
8. Facilidade de Depuração e Manutenção
Embora não seja um benefício de desempenho direto, a estrutura modular do ECS melhora a clareza e a manutenibilidade do código. A separação distinta entre componentes e sistemas permite que os desenvolvedores identifiquem e resolvam gargalos de desempenho com maior rapidez e eficácia.
Em suma, o ECS eleva o desempenho através de um design orientado a dados, disposição otimizada da memória, processamento paralelo, cálculos sob demanda, entre outras estratégias. Essa arquitetura não só aprimora a eficiência operacional de aplicações e jogos, mas também oferece maior escalabilidade e manutenibilidade para os desenvolvedores, facilitando a criação de sistemas de alto desempenho.
Exemplo Prático: Design Orientado a Dados com ECS
O Design Orientado a Dados (DOD) é uma metodologia de programação que foca na estrutura e manipulação eficiente dos dados. A arquitetura ECS é um exemplo clássico da aplicação do DOD, onde dados (componentes) e lógica (sistemas) são separados para otimizar o uso da memória e a eficiência do cache da CPU. Considere um cenário simples de um jogo 2D com múltiplos personagens (jogador, inimigos, NPCs) que se movem, cada um com sua posição, velocidade e pontos de vida.
Abordagem Tradicional (POO)
Na POO, poderíamos ter uma classe base EntidadeJogo que engloba todas essas características:
public class EntidadeJogo
{
public Ponto2D Coordenadas { get; set; }
public Vetor2D DirecaoMovimento { get; set; }
public int Vitalidade { get; set; }
public EntidadeJogo(Ponto2D pos, Vetor2D vel, int vida)
{
Coordenadas = pos;
DirecaoMovimento = vel;
Vitalidade = vida;
}
public void Atualizar(float passoTempo)
{
Coordenadas = new Ponto2D(
Coordenadas.X + DirecaoMovimento.X * passoTempo,
Coordenadas.Y + DirecaoMovimento.Y * passoTempo
);
}
}
public struct Ponto2D { public float X, Y; public Ponto2D(float x, float y) { X = x; Y = y; } }
public struct Vetor2D { public float X, Y; public Vetor2D(float x, float y) { X = x; Y = y; } }
Nesta estrutura, cada instância de EntidadeJogo contém seus próprios dados e comportamentos. Com o aumento do número de entidades, a memória pode se tornar fragmentada, e a CPU pode sofrer com a falta de localidade de cache durante as atualizações em massa.
Abordagem ECS
Com o ECS, separamos os dados em componentes e a lógica em sistemas:
Definições de Componentes (Estruturas de Dados Simples):
public struct CompPosicao
{
public Ponto2D Valor;
}
public struct CompVelocidade
{
public Vetor2D Valor;
}
public struct CompSaude
{
public int Pontos;
}
Definição de um Sistema (Lógica de Processamento):
public class SistemaMovimento
{
public void Executar(CompPosicao[] posicoes, CompVelocidade[] velocidades, float passoTempo)
{
for (int i = 0; i < posicoes.Length; i++)
{
posicoes[i].Valor = new Ponto2D(
posicoes[i].Valor.X + velocidades[i].Valor.X * passoTempo,
posicoes[i].Valor.Y + velocidades[i].Valor.Y * passoTempo
);
}
}
}
Benefícios do Design Orientado a Dados no ECS:
- Otimização do Layout de Memória: Todos os componentes
CompPosicao,CompVelocidade, etc., são armazenados em arrays contíguos. Isso permite que a CPU carregue blocos maiores de dados relevantes para o cache de uma só vez, reduzindo falhas de cache. - Minimização da Fragmentação de Memória: Componentes separados permitem alocações mais eficientes e contínuas, ao contrário de objetos POO que podem ter tamanhos variáveis e espalhar-se pela memória.
- Paralelização Intrínseca: Diferentes sistemas podem operar em diferentes conjuntos de componentes em paralelo. Por exemplo, um
SistemaMovimentopode atualizar posições enquanto umSistemaRenderizacaoprepara a exibição, tirando proveito de CPUs multi-core. - Cálculo Seletivo: Sistemas processam apenas os componentes de que necessitam, permitindo que entidades sem certas características (ex: sem velocidade) sejam automaticamente ignoradas por sistemas de movimento.
- Facilidade de Extensão: Adicionar novas características (ex: um componente de gravidade) ou comportamentos (ex: um sistema de gravidade) é feito sem modificar o código existente, promovendo modularidade.
Ao lidar com milhares de entidades, a diferença de desempenho entre essas abordagens é notável. O ECS, ao agrupar dados por tipo e processá-los em lotes, minimiza a sobrecarga de acesso à memória e maximiza o uso da largura de banda do processador, resultando em uma experiência de jogo mais fluida e reativa.
Otimização da Disposição da Memória: Agrupamento Estruturado no ECS
A disposição da memória é um fator crucial para o desempenho em aplicações que demandam alta performance, como jogos. A arquitetura ECS maximiza a eficiência do acesso a dados ao empregar o conceito de "agrupamento estruturado" ou Structure of Arrays (SoA), onde componentes de tipos idênticos são armazenados consecutivamente. Analisemos um exemplo com um grande número de entidades em um jogo 3D, cada uma com atributos como posição, velocidade e vida.
Cenário Tradicional (POO)
Em uma implementação POO, cada instância de ObjetoDinamico encapsularia todos os seus dados. Para 10.000 objetos, a memória seria organizada de forma intercalada:
+---------------------+ +---------------------+ +---------------------+
| ObjetoDinamico 1 | | ObjetoDinamico 2 | | ObjetoDinamico 3 |
|---------------------| |---------------------| |---------------------|
| Posição (x,y,z) | | Posição (x,y,z) | | Posição (x,y,z) |
| Velocidade (vx,vy,vz)| | Velocidade (vx,vy,vz)| | Velocidade (vx,vy,vz)|
| Saúde (int) | | Saúde (int) | | Saúde (int) |
| ... | | ... | | ... |
+---------------------+ +---------------------+ +---------------------+
Aqui, para processar todas as posições, a CPU teria que saltar por diferentes áreas da memória, carregando dados de velocidade e saúde desnecessariamente para o cache se o objetivo for apenas atualizar as posições.
Cenário ECS
No ECS, os componentes são armazenados separadamente, e todos os componentes de um mesmo tipo são agrupados. Para os mesmos 10.000 entidades, o layout de memória seria algo como:
+---------------------------+ +---------------------------+ +------------------------+
| ComponentePosicao | | ComponenteVelocidade | | ComponenteSaude |
|---------------------------| |---------------------------| |------------------------|
| {1.0, 2.0, 3.0} (Ent. 1) | | {0.1, 0.2, 0.3} (Ent. 1) | | 100 (Ent. 1) |
| {4.0, 5.0, 6.0} (Ent. 2) | | {0.4, 0.5, 0.6} (Ent. 2) | | 80 (Ent. 2) |
| {7.0, 8.0, 9.0} (Ent. 3) | | {0.7, 0.8, 0.9} (Ent. 3) | | 60 (Ent. 3) |
| ... | | ... | | ... |
+---------------------------+ +---------------------------+ +------------------------+
Nesse arranjo, um sistema que atualiza apenas as posições e velocidades pode carregar grandes blocos de dados de ComponentePosicao e ComponenteVelocidade diretamente para o cache da CPU, sem interrupções por dados irrelevantes.
Vantagens da Otimização do Layout de Memória:
- Alta Taxa de Acerto de Cache: Dados do mesmo tipo, processados em conjunto, residem em locais de memória próximos. A CPU pode pré-carregar esses dados no cache, resultando em menos falhas de cache e acessos mais rápidos.
- Redução da Latência de Acesso à Memória: A leitura de dados contíguos é intrinsecamente mais rápida do que a leitura de dados dispersos, já que menos tempo é gasto esperando a memória ser acessada. Isso é vital para operações em massa.
- Processamento de Dados Otimizado: Sistemas podem processar lotes de componentes de uma vez, explorando o paralelismo de dados e a eficiência do hardware.
- Melhor Suporte à Paralelização: Como os dados de diferentes tipos são separados, múltiplos sistemas podem operar em diferentes conjuntos de componentes em paralelo sem conflitos de memória, facilitando o multithreading.
Em um cenário onde 10.000 entidades precisam ter suas posições e velocidades atualizadas a cada frame, a abordagem POO levaria a acessos de memória dispersos e lentos. O ECS, ao contrário, permite que o processador trabalhe com dados em cache, resultando em atualizações significativamente mais rápidas e, consequentemente, em taxas de quadros (FPS) mais altas e uma experiência do usuário superior.
Cálculos Sob Demanda no ECS: Minimizando Processamento Inútil
Em projetos de grande escala, a otimização de performence é crucial. A arquitetura ECS se destaca por sua capacidade de realizar cálculos sob demanda, eliminando a sobrecarga de processamento de entidades que não necessitam de atualização. Analisaremos como isso funciona em um jogo de mundo aberto com um vasto número de NPCs (personagens não-jogáveis) e elementos ambientais dinâmicos, cada um com diferentes estados (patrulhando, atacando, ocioso).
Cenário Tradicional (POO)
Na POO, cada ObjetoJogo possui seu próprio estado e lógica de atualização. Se temos 10.000 NPCs, todos podem ser iterados e atualizados a cada ciclo do jogo, independentemente de seu estado:
foreach (var entidade in todasEntidadesDoMundo)
{
entidade.AtualizarEstado(); // Tenta atualizar todos os NPCs, mesmo os inativos
}
Aqui, mesmo NPCs que estão parados ou em um estado inalterado consomem ciclos de CPU, levando a cálculos desnecessários e desperdício de recursos.
Cenário ECS
No ECS, a lógica é organizada em sistemas que operam apenas em componentes relevantes. Para os mesmos 10.000 NPCs, um sistema de movimento processaria apenas aqueles que possuem um componente de Velocidade e, talvez, um componente de EstadoAtivo:
// Assumindo que temos arrays de componentes de entidades ativas
public void ProcessarMovimento(CompPosicao[] posicoes, CompVelocidade[] velocidades, float deltaTempo)
{
for (int i = 0; i < posicoes.Length; i++)
{
// A entidade já foi filtrada para ter ambos os componentes e ser "ativa"
posicoes[i].Valor = new Ponto2D(
posicoes[i].Valor.X + velocidades[i].Valor.X * deltaTempo,
posicoes[i].Valor.Y + velocidades[i].Valor.Y * deltaTempo
);
}
}
Neste modelo, o sistema de movimento só é executado para os NPCs que realmente se movem ou precisam de atualização, enquanto os inativos são ignorados, economizando ciclos da CPU.
Benefícios dos Cálculos Sob Demanda:
- Redução da Carga Computacional: Ao processar apenas os componentes que mudaram ou são relevantes, o ECS diminui drasticamente o volume de cálculos por frame. Se apenas 2.000 dos 10.000 NPCs estão ativos, o sistema processa apenas esses 2.000.
- Aumento da Performence Geral: Menos cálculos desnecessários liberam recursos da CPU para outras tarefas críticas, como renderização ou simulações físicas, resultando em maior taxa de quadros e melhor responsividade.
- Adaptação Dinâmica: O ECS permite que entidades entrem e saiam dinamicamente dos conjuntos de processamento. Um NPC pode ser "ativado" adicionando um componente
EstadoAtivo, fazendo com que ele seja automaticamente incluído nos sistemas relevantes. - Gerenciamento Eficaz de Recursos: Evita o desperdício de ciclos da CPU, crucial para jogos com grande número de entidades e lógicas complexas.
Comparando um cenário onde 10.000 NPCs são atualizados serialmente (POO, digamos, 10ms totais) com um ECS que atualiza apenas 2.000 NPCs ativos (digamos, 2ms totais), o ganho de performance é de 80%. Isso se traduz diretamente em uma experiência de jogo mais fluida e envolvente. Muitos jogos modernos demonstram melhorias significativas na taxa de quadros ao adotar o ECS, especialmente em cenas com alta densidade de entidades.
Processamento Paralelo no ECS: Aproveitando CPUs Multi-core
Em jogos complexos e simuladores, a capacidade de processar grandes volumes de dados de forma eficiente é essencial. A arquitetura ECS facilita o processamento paralelo, permitindo que os sistemas explorem a capacidade total das CPUs multi-core modernas e elevando significativamente o desempenho computacional. Vamos examinar isso em um jogo de estratégia em tempo real com milhares de unidades, cada uma com comportamentos como movimento, combate e renderização.
Cenário Tradicional (POO)
Em um sistema POO, a lógica de atualização geralmente ocorre em um único loop sequencial, onde cada UnidadeJogo é atualizada individualmente:
foreach (var unidade in todasAsUnidades)
{
unidade.AtualizarLogica(); // Lógica de movimento, combate, etc.
unidade.RenderizarObjeto(); // Lógica de renderização
}
Mesmo em CPUs com múltiplos núcleos, essa execução serial cria um gargalo, limitando o desempenho ao do núcleo mais lento e impedindo a utilização plena dos recursos de hardware disponíveis.
Cenário ECS
No ECS, a lógica é dividida em sistemas que operam sobre componentes específicos. Isso permite que diferentes sistemas sejam executados em paralelo. Para 10.000 unidades, podemos ter:
using System.Threading.Tasks;
public class GerenciadorDeSistemas
{
public SistemaMovimento sistemaMovimento = new SistemaMovimento();
public SistemaCombate sistemaCombate = new SistemaCombate();
public SistemaRenderizacao sistemaRenderizacao = new SistemaRenderizacao();
public void CicloDeAtualizacao(CompPosicao[] pos, CompVelocidade[] vel, CompAlvo[] alvos, CompGraficos[] graficos, float dt)
{
// Executar sistemas em paralelo
Parallel.Invoke(
() => sistemaMovimento.Atualizar(pos, vel, dt),
() => sistemaCombate.ProcessarAtaques(alvos, dt),
() => sistemaRenderizacao.Desenhar(pos, graficos)
);
// Outros sistemas podem ser adicionados aqui
}
}
Neste exemplo, SistemaMovimento, SistemaCombate e SistemaRenderizacao podem ser executados simultaneamente em diferentes núcleos da CPU. Como cada sistema lida com um conjunto isolado de componentes (ou acessa-os de forma que minimize contendas), a integridade dos dados é mantida e a performance é amplificada.
Vantagens do Processamento Paralelo:
- Uso Máximo de CPUs Multi-core: O ECS distribui a carga de trabalho entre os núcleos disponíveis, permitindo que a aplicação realize mais operações por unidade de tempo.
- Melhora na Taxa de Quadros (FPS): Com a capacidade de realizar múltiplos cálculos simultaneamente, o jogo pode atingir taxas de quadros mais elevadas e uma resposta mais rápida aos comandos do jogador.
- Eliminação de Gargalos: A carga é distribuída, evitando que um único sistema ou tipo de operação sobrecarregue um único thread.
- Extensão Simplificada: Novos sistemas podem ser adicionados ao pipeline de execução paralela com mínima ou nenhuma alteração nos sistemas existentes, mantendo a arquitetura modular e performática.
Se a abordagem POO leva 10ms para atualizar 10.000 unidades em um único thread, a abordagem ECS, dividindo a carga em 4 sistemas paralelos, poderia reduzir esse tempo para aproximadamente 3ms (desconsiderando overhead de threading). Isso representa um aumento de performance de mais de 70%, crucial para simulações e jogos de grande escala, onde a fluidez é um diferencial.
ECS e Aceleração por SIMD/GPU: Explorando o Hardware Moderno
Aproveitar o hardware moderno para paralelismo é uma meta fundamental na otimização de desempenho, especialmente em jogos e computação de alto desempenho. A arquitetura ECS, devido à sua organização de dados, é excepcionalmente adequada para tirar proveito das instruções SIMD (Single Instruction, Multiple Data) e da aceleração por GPU. Consideremos um jogo de estratégia em tempo real com milhares de unidades, onde a atualização de estado e a renderização são contínuas.
Cenário Tradicional (POO)
Em um modelo POO, cada ObjetoDinamico possui seus próprios atributos e método de atualização. Processar 10.000 unidades de forma tradicional implicaria em um loop sequencial onde a CPU acessa dados dispersos na memória para cada objeto:
foreach (var unidade in todasAsUnidades)
{
unidade.AtualizarPropriedades(); // Carga de dados e cálculos individuais
}
Este padrão de acesso de dados é ineficiente para as unidades de execução vetorial da CPU (SIMD) e menos ideal para a arquitetura maciçamente paralela das GPUs.
Cenário ECS
No ECS, os dados são agrupados por tipo de componente (SoA), criando arrays contíguos de dados homogêneos. Esta é a estrutura perfeita para SIMD e GPU. Se tivermos componentes de CompPosicao e CompVelocidade:
#include <immintrin.h> // Para instruções AVX/SSE
// Supondo arrays contíguos de dados para posições e velocidades
struct Vector3 { float x, y, z; };
Vector3* arrPosicoes; // Ponteiro para o início do array de posições
Vector3* arrVelocidades; // Ponteiro para o início do array de velocidades
float deltaTime = 0.016f; // Exemplo de delta de tempo
void AtualizarPosicoesSIMD(int numEntidades) {
// Processa 4 vetores Vector3 (12 floats) por iteração com AVX
for (int i = 0; i < numEntidades; i += 4) {
// Carrega 4 posições e 4 velocidades em registradores AVX
__m256 pos_x = _mm256_load_ps(&arrPosicoes[i].x);
__m256 pos_y = _mm256_load_ps(&arrPosicoes[i].y);
__m256 pos_z = _mm256_load_ps(&arrPosicoes[i].z);
__m256 vel_x = _mm256_load_ps(&arrVelocidades[i].x);
__m256 vel_y = _mm256_load_ps(&arrVelocidades[i].y);
__m256 vel_z = _mm256_load_ps(&arrVelocidades[i].z);
__m256 dt_vec = _mm256_set1_ps(deltaTime); // Vetor com delta de tempo repetido
// Calcula nova posição = posição + (velocidade * deltaTempo)
pos_x = _mm256_add_ps(pos_x, _mm256_mul_ps(vel_x, dt_vec));
pos_y = _mm256_add_ps(pos_y, _mm256_mul_ps(vel_y, dt_vec));
pos_z = _mm256_add_ps(pos_z, _mm256_mul_ps(vel_z, dt_vec));
// Armazena as novas posições
_mm256_store_ps(&arrPosicoes[i].x, pos_x);
_mm256_store_ps(&arrPosicoes[i].y, pos_y);
_mm256_store_ps(&arrPosicoes[i].z, pos_z);
}
}
Nesse exemplo, uma única instrução SIMD (_mm256_add_ps) pode somar 8 números de ponto flutuante simultaneamente, multiplicando a eficiência. Para GPU, esses arrays de componentes são facilmente transferidos para a memória da placa de vídeo, onde milhares de "threads" da GPU podem processar cada entidade em paralelo.
Vantagens da Aceleração por SIMD e GPU:
- Localidade de Dados Aprimorada: O layout SoA do ECS garante que os dados necessários para operações SIMD ou GPU sejam contíguos na memória, otimizando o uso do cache e a largura de banda.
- Processamento Paralelo Massivo: SIMD permite que múltiplas operações de dados sejam executadas por uma única instrução da CPU. As GPUs, com seus milhares de núcleos, podem processar um número colossal de entidades simultaneamente, acelerando tarefas como física, IA e renderização.
- Desempenho Elevado: Ao vetorizar cálculos com SIMD ou offload para a GPU, a carga de trabalho da CPU é reduzida dramaticamente, levando a ganhos de performance de 5x, 10x ou mais em comparação com abordagens sequenciais.
- Arquitetura Flexível: O ECS permite que os desenvolvedores escolham quais sistemas se beneficiarão mais da aceleração por hardware, adaptando-se a novas tecnologias sem refatorar toda a base de código.
Considerando 10.000 unidades, uma atualização sequencial POO pode levar 10ms. Com SIMD, esse tempo poderia cair para 2.5ms (assumindo 4 elementos por instrução). Se a tarefa for totalmente transferida para uma GPU, o tempo de processamento pode ser reduzido para 1ms ou menos. Esse salto no desempenho é crucial para jogos modernos que buscam realismo e interatividade com um grande número de elementos simultaneamente.
Minimizando a Criação e Destruição de Objetos com ECS: O Caso dos Projéteis
Em jogos, a criação e destruição contínua de objetos, como projéteis, inimigos temporários ou partículas de efeitos, é uma fonte comum de gargalos de desempenho na Programação Orientada a Objetos (POO). Essa prática gera overhead de alocação/desalocação de memória e, em linguagens com coleta de lixo, provoca interrupções (stutters). A arquitetura Entity Component System (ECS) oferece uma solução elegante através da reutilização de componentes.
1. Abordagem Tradicional (POO)
Imagine um jogo de tiro onde o jogador dispara muitas balas. Cada bala é uma instância de uma classe Projetil:
#include <vector>
#include <memory> // Para std::unique_ptr
struct Coordenada2D { float x, y; };
struct Vetor2D { float vx, vy; };
class Projetil {
public:
Coordenada2D pos;
Vetor2D vel;
bool ativo;
Projetil(Coordenada2D p, Vetor2D v) : pos(p), vel(v), ativo(true) {}
void Mover(float dt) {
if (ativo) {
pos.x += vel.vx * dt;
pos.y += vel.vy * dt;
// Lógica para desativar se sair dos limites
if (pos.x > 1000 || pos.y > 1000 || pos.x < 0 || pos.y < 0) {
ativo = false;
}
}
}
};
std::vector<std::unique_ptr<Projetil>> projeteisAtivos;
void DispararProjetil(Coordenada2D inicio, Vetor2D direcao) {
projeteisAtivos.push_back(std::make_unique<Projetil>(inicio, direcao));
}
void AtualizarProjeteisPOO(float deltaTempo) {
for (auto& proj : projeteisAtivos) {
proj->Mover(deltaTempo);
}
// Remove projéteis inativos - envolve desalocação e reorganização do vetor
std::erase_if(projeteisAtivos, [](const std::unique_ptr<Projetil>& p){ return !p->ativo; });
}
Problemas desta Abordagem:
- Alocações e Desalocações Frequentes: Cada disparo cria um novo objeto
Projetil(new Projetiloumake_unique), e cada projétil que sai da tela é desalocado (deleteimplícito porunique_ptroustd::erase_if). Isso sobrecarrega o gerenciador de memória. - Fragmentação de Memória: As alocações e desalocações em diferentes momentos podem levar à fragmentação, diminuindo a localidade de cache e o desempenho.
- Coleta de Lixo (GC): Em linguagens como C#, a criação e destruição de muitos objetos geram pressão sobre o GC, que pode pausar o jogo para limpar a memória.
2. Utilizando ECS para Reutilização de Componentes
No ECS, não criamos "objetos bala" no sentido POO. Em vez disso, gerenciamos componentes de Posicao e Velocidade que pertencem a entidades que representam projéteis. A chave é usar um "pool" de componentes para reutilização:
#include <vector>
struct CompPosicao { float x, y; };
struct CompVelocidade { float vx, vy; };
struct InfoProjetil {
CompPosicao pos;
CompVelocidade vel;
bool ativo; // Indica se este slot está em uso
};
// Gerenciador de projéteis com pooling
std::vector<InfoProjetil> poolDeProjeteis;
std::vector<size_t> indicesProjeteisAtivos; // Índices para elementos ativos no pool
void InicializarPool(size_t capacidadeMaxima) {
poolDeProjeteis.resize(capacidadeMaxima);
for (size_t i = 0; i < capacidadeMaxima; ++i) {
poolDeProjeteis[i].ativo = false;
}
}
void AtivarProjetil(CompPosicao inicio, CompVelocidade direcao) {
// Tenta encontrar um slot inativo no pool
for (size_t i = 0; i < poolDeProjeteis.size(); ++i) {
if (!poolDeProjeteis[i].ativo) {
poolDeProjeteis[i].pos = inicio;
poolDeProjeteis[i].vel = direcao;
poolDeProjeteis[i].ativo = true;
indicesProjeteisAtivos.push_back(i); // Adiciona ao conjunto de ativos
return;
}
}
// Se o pool estiver cheio, pode-se expandir ou ignorar
}
void AtualizarSistemaProjeteis(float deltaTempo) {
// Itera apenas sobre os projéteis ativos
for (size_t i = 0; i < indicesProjeteisAtivos.size(); /* sem incremento aqui */) {
size_t idx = indicesProjeteisAtivos[i];
InfoProjetil& proj = poolDeProjeteis[idx];
proj.pos.x += proj.vel.vx * deltaTempo;
proj.pos.y += proj.vel.vy * deltaTempo;
// Lógica para desativar
if (proj.pos.x > 1000 || proj.pos.y > 1000 || proj.pos.x < 0 || proj.pos.y < 0) {
proj.ativo = false;
// Remove o índice do vetor de ativos de forma eficiente
indicesProjeteisAtivos[i] = indicesProjeteisAtivos.back();
indicesProjeteisAtivos.pop_back();
// Nao incrementa 'i' pois o elemento atual foi substituido pelo ultimo
} else {
++i; // Incrementa apenas se o elemento permanecer ativo
}
}
}
Vantagens da Abordagem ECS (com Pooling):
- Alocação Única: O
poolDeProjeteisé alocado uma única vez (ou em grandes blocos), eliminando alocações e desalocações por projétil individual. - Redução de GC: Em linguagens gerenciadas, a pressão sobre o coletor de lixo é drasticamente reduzida, resultando em menos "stutters" e um jogo mais fluido.
- Localidade de Dados: Os componentes (
CompPosicao,CompVelocidadedentro deInfoProjetil) são armazenados contiguamente nopoolDeProjeteis, o que otimiza o cache da CPU para oSistemaProjeteis. - Simplicidade na Gestão de Vida: A lógica de "ativação" e "desativação" de um componente dentro do pool é mais simples e menos propenso a erros do que o gerenciamento manual de ponteiros.
Ao adotar o ECS com pooling de componentes, um jogo que dispara centenas de projéteis por segundo pode manter uma performance estável, evitando os picos de latência causados pela gestão de memória tradicional. Essa otimização é vital para a reatividade e fluidez exigidas em jogos de ação.