VORGS
LAB SIMULATOR
Documentação técnica abrangendo arquitetura de sistemas, padrões de código, pipeline de renderização, C# Job System com Burst Compiler e infraestrutura cloud do projeto Vorgs Lab Simulator.
Tech Stack
Todas as tecnologias, bibliotecas e pacotes utilizados no projeto
| Categoria | Tecnologia | Versão / Nota | Uso |
|---|---|---|---|
| Engine | Unity | 6.x LTS | Engine principal, build pipeline, Editor |
| Linguagem | C# (.NET) | .NET 9 / C# 13 | Toda a lógica do jogo |
| Render Pipeline | Universal Render Pipeline (URP) | 17.x | Renderização PC e Mobile com custom passes |
| Job System | C# Job System + Burst Compiler | Nativo Unity 6 | Cálculos de tradução genética e morfogênese (L-System) de alta performance |
| DI Framework | VContainer | latest | Injeção de dependência em toda a camada OOP |
| Reactive Extensions | R3 | latest | Streams reativos para binding de UI e eventos de domínio |
| Async/Await | UniTask | 2.x | Operações assíncronas (I/O, rede, animações) |
| UI System | UI Toolkit (UXML/USS) | Nativo Unity 6 | Toda a interface do usuário (in-game e editor) |
| Procedural Animation | Unity Animation Rigging | Nativo / Package | Cinemática Inversa (IK) para movimentação procedural do braço mecânico na visão micro e feedback de interações |
| Node Graph Editor | NodeGraphProcessor | open-source | Editor de vias metabólicas, configuração de genomas |
| Serialização | Newtonsoft Json.NET | 13.x | Saves, genomas, configurações exportadas |
| Backend / Cloud | Firebase (Firestore + Functions + Auth) | SDK Unity | Caçadas selvagens (RNG server-side), registro de patentes |
| Design de Templates UI | OpenDesign (open-source) | — | Prototipação de UI exportada para UXML/USS |
| Testes | Unity Test Framework + NUnit | Nativo | Testes unitários e de integração da simulação genética |
| CI/CD | GitHub Actions + GameCI | — | Build automatizado, testes, deploy |
Arquitetura do Sistema
Camadas de abstração, fluxo de dados e separação de responsabilidades
Diagrama de Camadas
O sistema é dividido em 4 camadas verticais com dependência unidirecional: camadas superiores dependem das inferiores, nunca o contrário. A comunicação entre camadas ocorre via interfaces abstratas injetadas pelo VContainer.
UnityEngine.UI, UnityEngine.UIElements ou possui referências a MonoBehaviour. Toda interação com a cena passa pela camada Presentation via thin wrappers.
Estrutura de Pastas do Projeto
Padrões de Código
SOLID, regras de MonoBehaviour e convenções do projeto
Princípios SOLID Aplicados
GeneticsService só traduz genes. MutationService só aplica mutações. Nunca os dois na mesma classe.IOrganismRenderer (3D, 2D fallback, debug) são totalmente intercambiáveis. Testes usam mocks que substituem implementações reais sem quebrar contratos.IReadable<T>, IWritable<T>, IDeletable em vez de um IRepository<T> monolítico com métodos não usados.new nem usa FindObjectOfType. Acoplamento apenas com abstrações.Regras de MonoBehaviour
[SerializeField].
// ✅ BOM — MonoBehaviour apenas como thin wrapper de rendering public sealed class OrganismRendererMono : MonoBehaviour { [SerializeField] private Renderer _renderer; [SerializeField] private Transform _root; private IOrganismRenderService _renderService; // injetado via VContainer private CompositeDisposable _disposables = new(); [Inject] public void Construct(IOrganismRenderService renderService) => _renderService = renderService; private void Start() { _renderService.OnPhenotypeChanged .Subscribe(ApplyPhenotype) .AddTo(_disposables); } private void ApplyPhenotype(Phenotype phenotype) { // apenas manipula componentes Unity — zero lógica de domínio aqui _renderer.material.SetColor("_BaseColor", phenotype.Color); _renderer.material.SetFloat("_EmissionIntensity", phenotype.LightEmission); _root.localScale = Vector3.one * phenotype.Scale; } private void OnDestroy() => _disposables.Dispose(); }
Outros Padrões Adotados
| Padrão | Onde é Usado | Benefício |
|---|---|---|
| Result<T, TError> | Retorno de todos os serviços de domínio | Sem exceções para flow control. Erros são valores explícitos. |
| Repository | Persistência e Cloud | Troca de backend (JSON ↔ Firebase) sem alterar domínio |
| Command + Undo Stack | Edições de DNA no Sequenciador | Ctrl+Z nativo para todas as operações genéticas |
| Factory | Criação de Organismos e Aquários | Lógica de construção separada, testável isoladamente |
| Strategy | Algoritmos de mutação e tradução genética | Diferentes estratégias (Random, Directed, Frameshift) intercambiáveis |
| Observer via R3 | Toda comunicação entre camadas | Desacoplamento total — publishers não conhecem subscribers |
| Spec / ValueObject | Modelos de Genoma, Fenótipo, Contrato | Imutabilidade garante consistência nos Jobs paralelos e na simulação |
Processamento de Alta Performance (Jobs)
Simulação genética de alta performance com Burst Compiler e Job System
Para evitar a complexidade do Unity ECS (DOTS) e as pontes custosas entre dados, o processamento de alto volume foi simplificado. A arquitetura de domínio mantém 100% da lógica em OOP tradicional e utiliza ativamente o C# Job System nativo com Burst Compiler apenas sob demanda para os pesados cálculos numéricos: tradução em massa de DNA, cálculos de mutação e morfogênese (L-System).
C# Job System & Burst Compiler
A separação foca no processamento orientado a dados. Classes do domínio (OOP) injetam arrays nativos nos jobs estruturados, agendam o processamento paralelo e colhem os resultados.
[BurstCompile] public struct TranslateGenomeJob : IJobParallelFor { [ReadOnly] public NativeArray<Nucleotide> Genome; public NativeArray<AminoAcid> Proteins; public void Execute(int index) { // Cálculo direto e otimizado com Burst, sem ECS overhead var codon = GetCodonAt(index); Proteins[index] = Translate(codon); } }
Camada de Domínio (OOP)
Serviços de negócio, modelos imutáveis e interfaces abstratas
A camada de domínio é composta inteiramente de Pure C# classes — sem nenhuma dependência de UnityEngine. Isso garante testabilidade total com NUnit puro, reutilização em ambos os contextos (Unity e testes headless) e separação de concerns inviolável.
// ValueObject imutável — representa o estado do genoma no domínio OOP public sealed record Genome { public string Id { get; } public string Nucleotides { get; } // ex: "ATGCGATCGATCG..." public BiomeType OriginBiome { get; } public DateTime CreatedAt { get; } public uint ComputeChecksum() => Crc32.Compute(Encoding.ASCII.GetBytes(Nucleotides)); public Genome WithMutation(int position, Nucleotide nucleotide) => this with { Nucleotides = ApplySubstitution(Nucleotides, position, nucleotide) }; } // Result<T> — sem exceções para flow control public readonly struct Result<T> { public T Value { get; } public string Error { get; } public bool IsSuccess { get; } public static Result<T> Ok(T value) => new(true, value, null); public static Result<T> Fail(string error) => new(false, default, error); }
public sealed class GeneticsService : IGeneticsService { private readonly ITranslationService _translator; private readonly IMetabolicGraph _metabolicGraph; public GeneticsService( // injeção de dependência via VContainer ITranslationService translator, IMetabolicGraph metabolicGraph) { _translator = translator; _metabolicGraph = metabolicGraph; } public Result<Phenotype> ExpressGenome(Genome genome) { var proteins = _translator.Translate(genome); if (proteins.IsSuccess == false) return Result<Phenotype>.Fail(proteins.Error); var activePathways = _metabolicGraph.Resolve(proteins.Value); var phenotype = PhenotypeBuilder.Build(activePathways); return Result<Phenotype>.Ok(phenotype); } }
Visual Rendering & Custom Shaders
Pipeline de renderização URP, custom shaders e rigging procedimental dos braços mecânicos
Dado o nível de fidelidade científica e o realismo estéril estilizado das referências visuais, o render pipeline (URP) utiliza passes de renderização customizados (Custom Render Passes) e shaders altamente otimizados para representar fluidos, vidros reflexivos e tecidos vivos.
Shaders Customizados (ShaderGraph URP)
Rigging Procedimental do Braço Robótico
Para evitar as limitações de animações baseadas em keyframes e permitir interações físicas precisas em qualquer ponto do aquário (independentemente de onde o organismo L-System cresça), o braço robótico é animado proceduralmente usando o pacote Unity Animation Rigging.
// Rigging procedimental com cinemática inversa (IK) para feedback visual public sealed class RoboticArmMono : MonoBehaviour { [SerializeField] private TwoBoneIKConstraint _armConstraint; [SerializeField] private Transform _effectorTarget; [SerializeField] private ParticleSystem _injectorMist; [SerializeField] private LineRenderer _laserRenderer; public async UniTask PerformInjectionAsync(Vector3 fluidPosition, CancellationToken ct) { // Move proceduralmente a ponta da agulha até a superfície da água await MoveTargetToAsync(fluidPosition, 1.5f, ct); _injectorMist.Play(); await UniTask.Delay(1000, cancellationToken: ct); await RetractArmAsync(ct); } public async UniTask PerformRadiationAsync(Vector3 targetPosition, CancellationToken ct) { await MoveTargetToAsync(targetPosition + Vector3.up * 0.5f, 1.0f, ct); _laserRenderer.enabled = true; // Ativa feixe UV e partículas de radiação await UniTask.Delay(2000, cancellationToken: ct); _laserRenderer.enabled = false; await RetractArmAsync(ct); } private async UniTask MoveTargetToAsync(Vector3 pos, float duration, CancellationToken ct) { /* Lerp do _effectorTarget.position */ } private async UniTask RetractArmAsync(CancellationToken ct) { /* Retorna _effectorTarget à pose inicial */ } }
Transição Focal e Foco da Câmera
O CameraRigMonoBehaviour gerencia a transição cinemática entre a visão macro (isométrica) e a visão micro focada (foco no aquário ou no equipamento modular). O pipeline de pós-processamento ativa um efeito de Depth of Field (DoF) baseado em Bokeh para isolar o objeto focado e desfocar o cenário do laboratório ao fundo, evitando fadiga visual e reforçando o realismo de câmera microscópica/macroscópica.
Arquitetura de UI
UI Toolkit + R3 + VContainer — MVC com View completamente isolada
A UI segue o padrão Controller → ViewModel → View onde a View é 100% passiva — ela só reage a Observables e nunca acessa serviços de domínio diretamente. O R3 é o único canal de comunicação entre Controller/ViewModel e View.
Fluxo de Dados na UI
public sealed class SequencerViewModel : IDisposable { // Observable properties — a View se subscreve, nunca puxa public ReadOnlyReactiveProperty<string> NucleotideSequence { get; } public ReadOnlyReactiveProperty<bool> IsSequencing { get; } public Observable<ProteinData> OnProteinDiscovered { get; } private readonly ReactiveProperty<string> _sequence = new(); private readonly ReactiveProperty<bool> _isSequencing = new(false); private readonly Subject<ProteinData> _proteinFound = new(); private readonly CompositeDisposable _disposables = new(); public SequencerViewModel(IGeneticsService genetics, Genome target) { NucleotideSequence = _sequence.ToReadOnlyReactiveProperty(); IsSequencing = _isSequencing.ToReadOnlyReactiveProperty(); OnProteinDiscovered = _proteinFound; _sequence.Value = target.Nucleotides; } public async UniTaskVoid SequenceAsync(Genome genome, CancellationToken ct) { _isSequencing.Value = true; // ... lógica de sequenciamento com delay por upgrade de equipamento _isSequencing.Value = false; } public void Dispose() => _disposables.Dispose(); }
// View — 100% passiva, zero lógica de negócio, zero dependências de domínio public sealed class SequencerView : IDisposable { private readonly Label _sequenceLabel; private readonly ProgressBar _progressBar; private readonly CompositeDisposable _disposables = new(); public SequencerView(VisualElement root) { _sequenceLabel = root.Q<Label>("sequence-label"); _progressBar = root.Q<ProgressBar>("progress-bar"); } public void Bind(SequencerViewModel vm) { vm.NucleotideSequence .Subscribe(seq => _sequenceLabel.text = seq) .AddTo(_disposables); vm.IsSequencing .Subscribe(active => _progressBar.SetVisible(active)) .AddTo(_disposables); vm.OnProteinDiscovered .Subscribe(PlayDiscoveryAnimation) .AddTo(_disposables); } private void PlayDiscoveryAnimation(ProteinData p) { /* USS animation class toggle */ } public void Dispose() => _disposables.Dispose(); }
VContainer — Injeção de Dependência
Todo o grafo de dependências é registrado em LifetimeScopes hierárquicos. Cada cena (Lab, Aquário, Sequenciador) tem seu próprio escopo, herdando os serviços globais do AppScope.
public sealed class AppScope : LifetimeScope { protected override void Configure(IContainerBuilder builder) { // ── Infrastructure ────────────────────────────────────────── builder.Register<ISaveRepository, JsonSaveRepository>(Lifetime.Singleton); builder.Register<ICloudRepository, FirebaseRepository>(Lifetime.Singleton); builder.Register<IEventBus, R3EventBus>(Lifetime.Singleton); // ── ECS Bridge ─────────────────────────────────────────────── builder.Register<IGenomeBridge, GenomeBridge>(Lifetime.Singleton); // ── Domain Services ────────────────────────────────────────── builder.Register<ITranslationService, CodonTranslationService>(Lifetime.Singleton); builder.Register<IGeneticsService, GeneticsService>(Lifetime.Singleton); builder.Register<IMutationService, MutationService>(Lifetime.Singleton); builder.Register<IEconomyService, EconomyService>(Lifetime.Singleton); builder.Register<IPatentService, PatentService>(Lifetime.Singleton); } } // LifetimeScope por cena — herda AppScope public sealed class SequencerScope : LifetimeScope { protected override void Configure(IContainerBuilder builder) { builder.Register<SequencerViewModel>(Lifetime.Scoped); builder.RegisterComponentInHierarchy<SequencerController>(); } }
Serialização & Persistência
Saves locais, exportação de genomas e integridade de dados
O save do jogo é baseado em JSON legível (Newtonsoft Json.NET), garantindo debugabilidade e facilidade de manutenção. A complexidade interna do genoma torna o JSON inacessível para cheating prático — editar nucleotídeos fora do jogo quebra proteínas de forma imprevisível.
Estrutura do Save
{
"version": "1.0.0",
"savedAt": "2025-11-01T14:32:00Z",
"lab": {
"funds": 14200,
"aquariums": [
{
"id": "aq_01",
"biome": "TROPICAL",
"genomeCrc32": "a3f8c21d", // checksum de integridade
"nucleotides": "ATGCGATCGATCGTACGATCG..."
}
]
},
"discoveredNodes": ["node_luciferase", "node_transport_mineral"],
"patents": [
{
"pathId": "path_biolum_v1",
"registeredAt": "2025-11-01T10:00:00Z",
"genomeBase64": "QVRHQ0dBVENHQVRDR1RBQ0dBVENH..."
}
]
}
Repository Pattern
public sealed class JsonSaveRepository : ISaveRepository { private static readonly JsonSerializerSettings _settings = new() { Converters = { new NucleotideConverter() }, Formatting = Formatting.Indented, NullValueHandling = NullValueHandling.Ignore }; public async UniTask<Result<SaveData>> LoadAsync(int slot, CancellationToken ct) { var path = GetSlotPath(slot); if (!File.Exists(path)) return Result<SaveData>.Fail("Save not found"); var json = await File.ReadAllTextAsync(path, ct); var data = JsonConvert.DeserializeObject<SaveData>(json, _settings); // valida integridade de cada genoma pelo CRC32 foreach (var aq in data.Lab.Aquariums) if (!ValidateChecksum(aq)) return Result<SaveData>.Fail($"Genome checksum mismatch: {aq.Id}"); return Result<SaveData>.Ok(data); } }
Editor Tools
Ferramentas de design de dados — configuração visual sem código
Toda a configuração do jogo — vias metabólicas, biomas, contratos, tabelas de tradução genética — é editada através de ferramentas visuais customizadas no Unity Editor. Designers e game designers nunca precisam editar JSON manualmente ou modificar código.
NodeGraphProcessor — Metabolic Pathway Editor
O NodeGraphProcessor (open-source) é usado como base para o editor de vias metabólicas. Cada nó no grafo representa um processo bioquímico e é implementado como uma classe C# que herda de BaseNode.
[NodeMenuItem("Metabolism/Enzyme")] public class EnzymeNode : BaseNode { [Input("Substrate")] public SubstrateData substrate; [Output("Product")] public SubstrateData product; [SerializeField] public string enzymeName; [SerializeField] public float kineticRate; [SerializeField] public float energyCost; // ATP equivalentes [SerializeField] public string requiredCodon; // códon que gera essa enzima public override string name => enzymeName; protected override void Process() { product = new SubstrateData { Id = substrate.Id + "_processed", Concentration = substrate.Concentration * kineticRate }; } }
NodeGraphProcessor custom processor converte o grafo finalizado em um MetabolicPathwaySO (ScriptableObject), que é então carregado em tempo de execução pelo MetabolicGraphService via Addressables. Designers salvam o grafo → o jogo carrega automaticamente.
Cloud, Anti-Cheat & Segurança
Arquitetura offline-first com integrações em nuvem para economia equilibrada
O jogo é primordialmente Offline-First: toda a simulação biológica, genética e de laboratório funciona completamente sem conexão. A nuvem é usada apenas para features onde o servidor precisa ser a fonte de verdade.
- ✓ Simulação biológica completa
- ✓ Frameshift, translação proteica, vias metabólicas
- ✓ Crescimento de organismos em aquários
- ✓ Gerenciamento do laboratório e contratos locais
- ✓ Save/Load completo via JSON local
- 🎲 RNG server-side para Caçadas Selvagens (genomas por bioma)
- 🔒 Validação estrutural de genomas no upload de patentes
- 📦 Registro Mundial de Patentes (Firestore)
- 💰 Cálculo de royalties server-side
- 🔄 Mercado comunitário de genomas (strings Base64)
Anti-Cheat por Complexidade
O mecanismo principal de anti-cheat não é criptografia — é a própria complexidade biológica simulada:
| Vetor de Ataque | Defesa Natural do Sistema |
|---|---|
| Editar nucleotídeos no save.json | CRC32 checksum detecta alteração. Mesmo sem CRC, editar A→C em posição aleatória muda o códon, quebra a proteína e inviabiliza o organismo. A ferramenta de debugging do jogo é necessária para qualquer edição funcional. |
| Copiar genoma lendário de outro jogador | Sistema Chave-Fechadura: proteínas do genoma externo têm baixa afinidade com enzimas nativas do hospedeiro. Inserção direta falha na simulação — é necessário trabalho real de adaptação. |
| Burlar RNG de Caçadas Selvagens | RNG é executado em Firebase Functions (server-side). O cliente recebe apenas o genoma resultante, não a semente aleatória. |
| Registrar patentes (Anti-Plágio) | As Firebase Functions (Node.js) atuam como "INPI Genético". Ao patentear uma via ou genoma de um Vorg, a Function avalia o pedido calculando a diferença genética (ex: Levenshtein distance ou alinhamento). Se não houver uma % mínima de diferença para patentes existentes, o registro é rejeitado para evitar plágio barato de genomas valiosos. |
| Injetar valores de FP no save | Transações de FP que envolvem itens premium são validadas server-side. Saldo local é reconciliado contra log de eventos no Firestore. |
Firebase Functions — Endpoints Principais
export const generateWildGenome = onCall(async (request) => { const { biomeId, playerLevel } = request.data; // RNG server-side — cliente nunca acessa a semente const seed = crypto.randomBytes(16).toString("hex"); const rarity = rollRarity(playerLevel, seed); const genome = await buildGenomeForBiome({ biomeId, rarity, seed, codonTable: await getCodonTableForBiome(biomeId) }); return { nucleotides: genome.nucleotides, rarity: rarity, checksum: crc32(genome.nucleotides), // validação pelo cliente expiresAt: Date.now() + 3600000 // 1h para aceitar }; });