Introdução: A mecânica do modding no ecossistema Unity
O motor de jogo Unity sustenta uma fração maciça da produção contemporânea de jogos para PC, desde sucessos de sobrevivência como Valheim e Lethal Company até simuladores complexos como Cities: Skylines II e Dyson Sphere Program. Ao contrário de engines proprietárias baseadas em arquivos binários monolíticos ou no sistema de registros declarativos da Bethesda, os projetos desenvolvidos na Unity fundamentam-se no ecossistema de software .NET e C#.
Modificar um jogo feito na Unity raramente exige a substituição destrutiva de arquivos de recursos compactados no disco. Em vez disso, o modding moderno na Unity opera por meio de injeção dinâmica de código em tempo de execução e interceptação de métodos na memória. No centro dessa arquitetura estão o BepInEx (Bepis Injector Extensible), um injetor e carregador de plugins de código aberto, e a biblioteca Harmony, desenvolvida por Andreas Pardeike.
No entanto, jogadores e criadores de modificações deparam-se frequentemente com incompatibilidades críticas ao tentar aplicar mods em diferentes títulos. A origem dessa complexidade reside no modelo de compilação da Unity: a divisão arquitetural profunda entre o compilador clássico Mono e o compilador antecipado IL2CPP (Intermediate Language to C++). Compreender a mecânica interna do BepInEx e do Harmony ao lidar com essas duas infraestruturas é fundamental para solucionar travamentos, ganchos inoperantes e conflitos de carregamento.
A divisão de backends de script: arquitetura Mono vs. IL2CPP
Ao exportar e compilar um jogo na Unity, o desenvolvedor escolhe obrigatoriamente um de dois destinos de compilação de scripts:
[Código-fonte C#: Scripts do Jogo]
│
▼
[Compilador C# Roslyn / Unity] ---> Common Intermediate Language (Bytecode CIL)
│
├─────────────────────────────────────────┐
▼ ▼
[Caminho do Backend Mono] [Caminho do Backend IL2CPP]
- Empacota CIL em assemblies gerenciados - Transpilador IL2CPP converte CIL em C++
- Arquivo principal: Assembly-CSharp.dll - Compilador C++ gera GameAssembly.dll
- Executado via Máquina Virtual JIT Mono - Metadados extraídos para global-metadata.dat
- Reflexão completa, fácil descompilação - Código de máquina puro, sem CLR/JIT nativo
O backend de scripts Mono
Nos jogos compilados com Mono, toda a lógica de jogo, scripts de inteligência artificial e entidades são empacotados em assemblies gerenciados .NET, principalmente em Managed/Assembly-CSharp.dll.
- Máquina virtual em tempo de execução: Ao iniciar o jogo, a máquina virtual Mono JIT (Just-In-Time) incorporada interpreta e compila o bytecode CIL sob demanda.
- Reflexão e metadados preservados: Nomes de classes, assinaturas de métodos, campos privados e definições estruturais permanecem intactos nos metadados do assembly.
- Acessibilidade para mods: Ferramentas como dnSpy e ILSpy permitem descompilar o
Assembly-CSharp.dllcom fidelidade quase total ao código-fonte C# original. O runtime permite injetar novos módulos e manipular métodos livremente via reflexão do .NET.
O backend de scripts IL2CPP
Nos jogos baseados em IL2CPP, o fluxo de compilação segue uma rota drasticamente distinta:
- Transpilação para C++: O bytecode CIL é processado pela ferramenta IL2CPP da Unity, que converte toda a lógica C# em milhares de arquivos-fonte C++ nativos.
- Binário nativo da plataforma: Esses arquivos C++ são compilados diretamente em uma biblioteca executável nativa,
GameAssembly.dll. - Extração de metadados: O ambiente gerenciado do Common Language Runtime (CLR) é totalmente eliminado. Os nomes de tipos, literais e estruturas são isolados em um arquivo binário proprietário chamado
global-metadata.dat. - Impacto no modding: Não existem assemblies gerenciados convencionais para carregar na inicialização. Inserir DLLs de mods compiladas em C# tradicional falha imediatamente, pois inexiste um interpretador .NET padrão rodando no processo.
A arquitetura de inicialização do BepInEx
Para contornar a ausência de um ponto de entrada oficial para scripts externos, o BepInEx utiliza uma técnica conhecida como sequestro de proxy de DLL (DLL proxy hijacking).
A cadeia de injeção Doorstop
No ecossistema Windows, os executáveis priorizam bibliotecas de vínculo dinâmico (DLLs) localizadas no próprio diretório de trabalho antes de consultar os diretórios do sistema operacional (C:\Windows\System32). O BepInEx aproveita essa prioridade:
- A DLL de proxy: O BepInEx instala uma biblioteca de fachada no diretório do jogo—geralmente
winhttp.dll,version.dlloudoorstop.dll. - Interceptação de execução: Quando o carregador do sistema operacional inicializa o executável do jogo, ele carrega a
winhttp.dlllocal em vez da biblioteca oficial do Windows. - Inicialização do runtime: A DLL proxy executa o Unity Doorstop, que localiza e carrega imediatamente na memória as bibliotecas
BepInEx.Preloader.dlleBepInEx.Core.dllantes que os procedimentos de inicialização da própria Unity assumam o controle. - Descoberta de plugins: Com o ambiente configurado, o BepInEx abre um console de depuração (se configurado), inicializa canais de log e faz a varredura da pasta
BepInEx/plugins/em busca de mods compatíveis.
BepInEx 5 vs. BepInEx 6: A ponte de interoperabilidade IL2CPP
- BepInEx 5: Versão amplamente consolidada voltada exclusivamente para títulos compilados em Mono. Baseia-se no próprio runtime Mono do jogo para carregar plugins herdados de
BaseUnityPlugin. - BepInEx 6: Uma reestruturação profunda projetada para oferecer suporte tanto ao Mono quanto ao IL2CPP. Nos jogos IL2CPP, o BepInEx 6 incorpora a biblioteca Il2CppInterop (evolução do Unhollower). Durante a primeira execução, o framework analisa o
GameAssembly.dlle o arquivoglobal-metadata.dat, gerando automaticamente assemblies gerenciados C# intermediários na pastaBepInEx/interop/. Essas bibliotecas intermediárias permitem que os criadores de mods escrevam código orientado a objetos em C# convencional, gerenciando ponteiros e chamadas nativas em C++ de forma transparente.
Harmony: interceptação e modificação dinâmica de métodos em memória
Após a inicialização do BepInEx, os mods utilizam a biblioteca Harmony para alterar o comportamento do jogo sem modificar nenhum byte dos arquivos originais no disco. O Harmony atua reescrevendo em tempo real os prólogos dos métodos compilados na memória JIT.
Os quatro tipos principais de ganchos do Harmony
[Chamada do Método: ex. PlayerController.TakeDamage(float amount)]
│
▼
[Gancho Prefix do Harmony] ---> Retorna true (continua) OU Retorna false (anula original)
│
▼
[Execução do Método Original: Cálculo e dedução de vida]
│
▼
[Gancho Postfix do Harmony] ---> Lê/Altera o valor retornado (__result) ou estado
│
▼
[Gancho Finalizer do Harmony] ---> Captura exceções não tratadas e evita fechamento do jogo
- Prefix (
[HarmonyPrefix]):- É disparado imediatamente antes da execução do método original do jogo.
- Pode inspecionar e alterar parâmetros de entrada (por exemplo, modificar o valor de dano
float amountantes do cálculo). - Permite cancelar a execução do código original: se o Prefix retornar
false, o método nativo do jogo é completamente ignorado.
- Postfix (
[HarmonyPostfix]):- É executado logo após o término do método original.
- Permite consultar e alterar o valor de retorno por meio do parâmetro especial
ref __result. - É o mecanismo ideal para acionar eventos secundários (como conceder uma conquista após a conclusão de uma receita de criação).
- Transpiler (
[HarmonyTranspiler]):- Representa o tipo de gancho de maior precisão e complexidade. Intercepta o bytecode CIL bruto do método como uma sequência enumerável de instruções (
IEnumerabledeCodeInstruction) antes da compilação JIT. - Permite adicionar, remover ou substituir instruções operacionais específicas (como trocar uma instrução de constante
ldc.i4.0porldc.i4.1ou alterar ramificações condicionais).
- Representa o tipo de gancho de maior precisão e complexidade. Intercepta o bytecode CIL bruto do método como uma sequência enumerável de instruções (
- Finalizer (
[HarmonyFinalizer]):- Funciona como um invólucro de proteção dinâmico
try/finallyem torno do método manipulado. Ele captura e neutraliza exceções inesperadas, impedindo que bugs internos de um mod causem o encerramento forçado do jogo para o desktop.
- Funciona como um invólucro de proteção dinâmico
Matriz de diagnóstico: falhas comuns no modding de Unity
Caso suas modificações não carreguem ou causem travamentos espontâneos, verifique os sintomas a partir desta matriz diagnóstica de arquitetura:
| Sintoma de Falha | Causa Raiz Estrutural | Indicador de Diagnóstico | Procedimento Corretivo |
|---|---|---|---|
| Console ausente / Mods ignorados | DLL de proxy bloqueada ou ignorada pelo carregador | Arquivo BepInEx/LogOutput.log não é gerado | Confirmar presença de winhttp.dll na raiz; verificar permissões da pasta |
| Travamento instantâneo no lançamento (IL2CPP) | Tentativa de utilizar BepInEx 5 em jogo IL2CPP | GameAssembly.dll existe na raiz, mas BepInEx 5 está instalado | Remover instalação antiga e instalar BepInEx 6 x64 específico para IL2CPP |
| Cache de interop defasado | Atualização do jogo sem regeneração dos assemblies | Exceção TypeLoadException ou MissingMethodException no log | Excluir a pasta BepInEx/interop/ para forçar reconstrução dos wrappers |
| Conflito entre ganchos Prefix | Múltiplos mods interceptando mesmo método com retorno false | Mod A funciona normalmente, Mod B tem funções inoperantes | Reordenar prioridade de plugins; evitar anulações agressivas no Prefix |
| Bloqueio por sistema antitrapaça | EAC ou BattlEye impedindo injeção da DLL proxy | Jogo encerra com erro de arquivo não confiável | Executar em modo offline ou utilizar parâmetros oficiais de inicialização modificada |
Ao montar ordens personalizadas de carregamento de mods, diagnosticar colisões entre assemblies ou desenvolver extensões em C# para diferentes motores de jogo, recorrer aos guias técnicos e arquiteturais do 365Finds garante que sua bancada de testes respeite a integridade dos binários nativos. Tratar o modding na Unity a partir do gerenciamento de memória, injeção de proxy e manipulação não destrutiva de bytecode preserva a estabilidade das suas instalações mesmo após atualizações de versão.