Introducción: Los fundamentos del modding en el motor Unity
El motor gráfico Unity sustenta una porción masiva de los videojuegos independientes y de escala media para PC, desde éxitos cooperativos como Valheim y Lethal Company hasta simuladores complejos como Cities: Skylines II y Dyson Sphere Program. A diferencia de otros motores que dependen de paquetes de recursos monolíticos o de sistemas de registros declarativos de plugins (como los archivos esp/esm de Bethesda), las obras desarrolladas en Unity están profundamente integradas en el ecosistema .NET y el lenguaje C#.
Modificar un juego de Unity rara vez implica sobrescribir archivos originales en el disco duro. El modding moderno en este entorno opera mediante la interceptación de métodos en memoria y el parcheo dinámico de ensamblados en tiempo de ejecución. En el centro de esta arquitectura convergen dos herramientas fundamentales: BepInEx (Bepis Injector Extensible), un entorno modular de carga e inyección de plugins, y la conocida biblioteca de ganchos dinámicos Harmony, desarrollada por Andreas Pardeike.
No obstante, los usuarios y creadores de mods se topan a menudo con bloqueos críticos y fallos al cargar modificaciones. El origen de estos problemas reside en una bifurcación técnica capital en el compilador de Unity: la insalvable distancia arquitectónica entre el clásico backend administrado Mono y el backend precompilado a código nativo IL2CPP (Intermediate Language to C++).
La división de backends de scripting: Mono frente a IL2CPP
Cuando un estudio exporta un proyecto de Unity para PC, debe elegir entre dos métodos de compilación radicalmente distintos:
[Código fuente C#: Lógica del juego]
│
▼
[Compilador C# de Unity / Roslyn] ---> Lenguaje Intermedio Común (Bytecode CIL)
│
├─────────────────────────────────────────┐
▼ ▼
[Vía del backend Mono] [Vía del backend IL2CPP]
- Empaqueta el CIL en DLLs administradas - El transpilador IL2CPP convierte CIL a código C++
- Genera Assembly-CSharp.dll estándar - Compilador nativo genera GameAssembly.dll
- Ejecutado en runtime por la VM Mono JIT - Metadatos extraídos a global-metadata.dat
- Metadatos intactos y fácil descompilación - Código máquina puro; carece de CLR en runtime
El backend de scripting Mono
En los juegos compilados bajo Mono, toda la lógica de juego, las rutinas de inteligencia artificial y los parámetros de entidades se compilan como lenguaje intermedio estándar de .NET (CIL) en bibliotecas administradas, destacando Managed/Assembly-CSharp.dll.
- Máquina virtual en ejecución: Al arrancar el juego, la máquina virtual Mono JIT (Just-In-Time) integrada ejecuta directamente el bytecode CIL.
- Metadatos completos: Las definiciones de clases, nombres de métodos, campos privados y tipos se preservan íntegramente en los metadatos del ensamblado.
- Accesibilidad para el modding: Mediante utilidades como dnSpy o ILSpy, los desarrolladores pueden descompilar el ensamblado original y leer el código en C# con absoluta claridad, lo que facilita interceptar cualquier método mediante reflexión en memoria.
El backend de scripting IL2CPP
En los títulos generados con IL2CPP, la cadena de montaje de software cambia por completo:
- Transpilación a C++: El bytecode CIL es procesado por las herramientas AOT (Ahead-of-Time) de Unity, transformando todo el código C# en decenas de miles de ficheros fuente en C++ nativo.
- Compilación directa a código máquina: Esos archivos en C++ son compilados directamente por el compilador del sistema en una biblioteca nativa de plataforma:
GameAssembly.dll. - Extracción y ofuscación de metadatos: El entorno de ejecución de .NET (CLR) se elimina por completo. La información de tipos y tablas de cadenas de texto se comprime en un archivo binario separado llamado
global-metadata.dat. - Barrera para las modificaciones: En memoria solo residen instrucciones en ensamblador de la CPU; no existen DLLs administradas que puedan ejecutarse directamente. Introducir un mod estándar de C# en un juego IL2CPP provocará su inoperancia inmediata al no existir una máquina virtual que lo interprete.
La arquitectura de inyección de BepInEx y el secuestro de DLLs
Para lograr que un proceso cerrado de Unity cargue código de terceros, BepInEx recurre al secuestro de proxies de librerías dinámicas (DLL Proxy Hijacking) de Windows.
La cadena de intercepción con Doorstop
El cargador de Windows busca las librerías dinámicas en el directorio raíz de la aplicación antes de consultar las carpetas del sistema operativo (C:\Windows\System32). BepInEx aprovecha esta prioridad:
- La DLL proxy: Se ubica en la raíz del juego una librería con el mismo nombre de un componente del sistema, habitualmente
winhttp.dll,version.dllodoorstop.dll. - Desvío del hilo de ejecución: Al iniciarse el ejecutable, Windows carga la librería falsa local
winhttp.dllen lugar del componente oficial de Windows. - Arranque del entorno de ejecución: Esta librería ejecuta Unity Doorstop, que a su vez introduce
BepInEx.Preloader.dllyBepInEx.Core.dllen la memoria del proceso antes de que la rutina principal de Unity asuma el control. - Carga y despacho de plugins: Una vez establecido el entorno, BepInEx habilita la consola de depuración, activa los registros y explora la carpeta
BepInEx/plugins/para cargar los ensamblados de los mods instalados.
BepInEx 5 frente a BepInEx 6: El puente de interoperabilidad
- BepInEx 5: La versión clásica para juegos basados en Mono. Todos los plugins heredan de
BaseUnityPluginy se ejecutan directamente en la máquina virtual de Mono provista por el juego. - BepInEx 6: Una reescritura arquitectónica diseñada para dar soporte tanto a Mono como a IL2CPP. Para este último, BepInEx 6 incorpora Il2CppInterop (anteriormente Unhollower). En el primer inicio, analiza
GameAssembly.dllyglobal-metadata.daty genera enBepInEx/interop/miles de ensamblados sustitutos en C#. De este modo, los creadores de mods pueden programar código orientado a objetos convencional en C#, y el puente interop traduce automáticamente esas llamadas a direcciones de memoria de C++ nativo.
Ganchos dinámicos con Harmony: Modificación en memoria sin alterar archivos
Una vez que BepInEx ha inyectado el entorno en memoria, la alteración de funciones concretas se confía a la biblioteca Harmony. Harmony prescinde de la modificación destructiva de archivos en disco, reescribiendo directamente los prólogos de los métodos compilados por el JIT en la memoria RAM mediante instrucciones de salto.
Las cuatro modalidades de parches de Harmony
[Invocación del método original: ej. PlayerController.TakeDamage(float amount)]
│
▼
[Parche Prefix de Harmony] ---> Devuelve true (continúa) o false (omite original)
│
▼
[Ejecución del método original del juego: Cálculo de daño]
│
▼
[Parche Postfix de Harmony] ---> Lee o modifica el valor de retorno (__result)
│
▼
[Parche Finalizer de Harmony] ---> Captura excepciones y evita el cuelgue del juego
- Parche previo (
[HarmonyPrefix]):- Se ejecuta instantes antes del método original.
- Permite inspeccionar y alterar los argumentos de entrada (por ejemplo, reducir a la mitad el parámetro
amountantes de aplicar el daño). - Control de flujo: Si el Prefix devuelve
false, la lógica original del juego se omite por completo.
- Parche posterior (
[HarmonyPostfix]):- Se ejecuta justo al finalizar la rutina original.
- Permite consultar y modificar el valor devuelto mediante el parámetro especial
ref __result. - Resulta ideal para desencadenar eventos secundarios (como entregar un logro tras fabricar un objeto).
- Parche transpilador (
[HarmonyTranspiler]):- Es el método de mayor complejidad técnica. Expone el bytecode CIL original como una secuencia enumerable de instrucciones (
IEnumerabledeCodeInstruction). - Permite sustituir, insertar o suprimir instrucciones operativas (OpCodes) individuales antes de la compilación JIT.
- Es el método de mayor complejidad técnica. Expone el bytecode CIL original como una secuencia enumerable de instrucciones (
- Parche finalizador (
[HarmonyFinalizer]):- Opera como un bloque
try/finallydinámico alrededor del método. Captura y neutraliza excepciones imprevistas, impidiendo que errores en el código del mod provoquen el cierre abrupto del juego al escritorio.
- Opera como un bloque
Matriz de diagnóstico: Resolución de problemas en mods de Unity
Si sus modificaciones no se ejecutan o provocan el cierre del juego, analice el problema siguiendo esta guía de incidencias:
| Síntoma de fallo | Causa arquitectónica raíz | Indicador en el sistema | Solución técnica |
|---|---|---|---|
| No abre consola / Mods no cargan | La DLL proxy es omitida o bloqueada por Windows | No se genera el archivo BepInEx/LogOutput.log | Verificar la presencia de winhttp.dll en raíz y permisos |
| Cierre instantáneo al abrir (IL2CPP) | Instalación de BepInEx 5 en un juego que usa IL2CPP | Existe GameAssembly.dll, pero se usó versión Mono | Eliminar la carpeta BepInEx e instalar BepInEx 6 IL2CPP |
| Errores masivos tras parche del juego | Desfase entre el nuevo binario y la caché de interop | Registro con abundantes TypeLoadException | Borrar la subcarpeta BepInEx/interop/ para forzar su regeneración |
| Un mod bloquea a otros silenciosamente | Varios mods parchean la misma función y usan false | Un mod opera pero otros dejan de responder | Reordenar la carga o usar parches Transpiler no invasivos |
| El sistema antitrampas aborta el inicio | EAC o BattlEye detectan la inyección de la DLL proxy | Mensaje «Untrusted system file» o proceso terminado | Iniciar en modo offline oficial o con banderas para mods |
Al armar paquetes de modificaciones complejos o programar extensiones personalizadas en C#, consultar los análisis de ingeniería en 365Finds garantiza diagnósticos basados en la realidad de la memoria de ejecución. Abordar el modding de Unity conociendo la interacción entre Mono, IL2CPP y los ganchos de Harmony es el único método fiable para mantener instalaciones estables y resistentes a las actualizaciones.