Visão geral da arquitetura de plugins do VibeGame, baseada em ECS (Entity Component System) com bitecs.
Ponto de entrada principal. Um plugin registra systems, components, recipes e config em uma única interface Plugin:
export interface Plugin {
readonly systems?: readonly System[];
readonly recipes?: readonly Recipe[];
readonly components?: Record<string, Component>;
readonly config?: Config;
readonly initialize?: (state: State) => void | Promise<void>;
}Plugins são registrados em defaults.ts via DefaultPlugins array.
Dados puros (sem lógica) armazenados em SOA (Struct of Arrays) — plain objects com typed arrays:
import { MAX_ENTITIES } from '../../core/ecs/constants';
export const Fog = {
mode: new Float32Array(MAX_ENTITIES),
density: new Float32Array(MAX_ENTITIES),
colorR: new Float32Array(MAX_ENTITIES),
colorG: new Float32Array(MAX_ENTITIES),
colorB: new Float32Array(MAX_ENTITIES),
} as const;Tipos: Float32Array, Uint8Array, Uint16Array, Uint32Array, Int8Array, Int32Array.
Função update(state: State) que roda todo frame (ou a cada fixed tick). Sistemas leem/escrevem Components via queries:
export const FogSystem: System = {
group: 'draw', // 'setup' | 'simulation' | 'fixed' | 'draw'
after: [CameraSyncSystem], // ordenação opcional
update(state: State) {
const entities = fogQuery(state.world);
// ...
},
};Grupos de execução (em ordem): setup → fixed (physics) → simulation → draw (render).
Atalho para criar entidades com um conjunto pré-definido de components + overrides:
export const playerRecipe: Recipe = {
name: 'player',
components: ['player', 'transform', 'body', 'collider', 'character-controller', 'input-state'],
overrides: {
'body.type': PLAYER_BODY_DEFAULTS.type,
'collider.radius': PLAYER_COLLIDER_DEFAULTS.radius,
},
};Configuração declarativa do plugin, com 6 seções opcionais:
| Seção | Tipo | Descrição |
|---|---|---|
defaults |
Record<string, Record<string, number>> |
Valores padrão para components |
enums |
Record<string, Record<string, EnumMapping>> |
Mapeia strings → números no XML |
adapters |
Record<string, Record<string, Adapter>> |
Converte valores XML/JSON para o component |
parsers |
Record<string, Parser> |
Parsers customizados para elementos XML |
shorthands |
Record<string, Record<string, ShorthandMapping>> |
Atalhos de atributos |
validations |
ValidationRule[] |
Regras de validação para recipes |
Exemplo de adapter (converte cor hex #ff0000 em R/G/B float):
function fogColorAdapter(entity: number, value: string, state: State): void {
const num = parseInt(value.slice(1), 16);
Fog.colorR[entity] = ((num >> 16) & 0xff) / 255;
Fog.colorG[entity] = ((num >> 8) & 0xff) / 255;
Fog.colorB[entity] = (num & 0xff) / 255;
}plugins/
└── meu-plugin/
├── plugin.ts # Exporta MeuPlugin: Plugin (obrigatório)
├── components.ts # defineComponent() para dados ECS
├── systems.ts # Lógica por frame (queries + update)
├── recipes.ts # Atalhos de criação de entidades
├── index.ts # Re-exports públicos
└── context.md # Notas de contexto (opcional, para AI agents)
import { MAX_ENTITIES } from '../../core/ecs/constants';
export const MeuComponent = {
ativo: new Uint8Array(MAX_ENTITIES),
valor: new Float32Array(MAX_ENTITIES),
alvo: new Uint32Array(MAX_ENTITIES),
} as const;import { defineQuery } from '../../core';
import type { State, System } from '../../core';
import { MeuComponent } from './components';
const query = defineQuery([MeuComponent]);
export const MeuSystem: System = {
group: 'simulation',
update(state: State) {
for (const eid of query(state.world)) {
if (MeuComponent.ativo[eid] === 1) {
MeuComponent.valor[eid] += state.time.deltaTime;
}
}
},
};import type { Recipe } from '../../core';
export const meuRecipe: Recipe = {
name: 'meu-component',
components: ['meu-component', 'transform'],
overrides: {
'meu-component.ativo': 1,
'meu-component.valor': 0,
},
};import type { Plugin } from '../../core';
import { MeuComponent } from './components';
import { MeuSystem } from './systems';
import { meuRecipe } from './recipes';
export const MeuPlugin: Plugin = {
systems: [MeuSystem],
recipes: [meuRecipe],
components: { 'meu-component': MeuComponent },
config: {
defaults: {
'meu-component': { ativo: 1, valor: 0, alvo: 0 },
},
},
};Adicione MeuPlugin ao array DefaultPlugins.
export { MeuComponent } from './components';
export { MeuPlugin } from './plugin';
export { meuRecipe } from './recipes';
export { MeuSystem } from './systems';| Plugin | Descrição | Complexidade |
|---|---|---|
transforms |
Posição/rotação/escala 3D | Baixa |
physics |
Física (Rapier) + colliders | Alta |
rendering |
Three.js renderer, câmeras, cenas | Alta |
player |
Controle de jogador (movimento, pulo, câmera) | Média |
input |
Teclado/mouse/gamepad | Média |
orbit-camera |
Câmera orbital com zoom | Média |
player-controller |
Câmera em terceira pessoa (ThirdPersonCamera) | Média |
fog |
Neblina volumétrica + fog exp/linear | Média |
water |
Água com física, nado, reflexos | Alta |
terrain |
Terreno procedural com heightmaps | Alta |
gltf-xml |
Carregamento de modelos GLB/GLTF | Alta |
animation |
Sistema de animação | Média |
tweening |
Interpolações suaves (tweens) | Baixa |
spawner |
<SpawnGroup>, <GameObject place="…"> no terreno — context.md |
Média |
startup |
Execução deferida pós-inicialização | Baixa |
debug |
Debug overlays (wireframes, etc.) | Baixa |
sky |
Skybox equirectangular + IBL (PMREM) | Média |
audio |
Áudio espacial (Howler, <AudioSource>) — docs/AUDIO.md |
Média |
postprocessing |
Bloom, SMAA, dithering, tonemapping (registry) | Alta |