Brian Bucklew está portando ‘Caves of Qud’ de Unity a Godot
(twitter.com/unormal)- ‘Caves of Qud’ inició una prueba técnica para eliminar su larga dependencia de Unity y compilar y arrancar el núcleo del juego en Godot; el objetivo inicial es ejecutarlo en modo ASCII+tiles, sin VFX ni UI modernos
- El trabajo se dividió en importar assets de tiles, portar los ensamblados principales de C# y configurar el rig de renderizado y entrada; los antiguos archivos de tiles BMP se convirtieron a PNG y se cargaron en Godot
- Los 5,641 errores de la compilación inicial se fueron reduciendo al trasladar GeneratedCode, ConsoleLib, Genkit, Language, HistoryKit, Newtonsoft JSON, CodeDom y otros componentes, mientras la superficie de dependencia de UnityEngine se fue acotando con stubs
- Los puntos donde se usaban Color, GameObject, AudioSource, Debug.Log y Screen de Unity se resolvieron con UnityEngineReplacer y clases sustitutas; las capas de chargen y presentation, cercanas a PlayFab y Unity UI, se eliminaron temporalmente o quedaron como candidatas para separarse en módulos glue
- Como resultado, un núcleo de juego en C# de unas 500 mil líneas logró arrancar en Godot, entregar frames y quedar esperando entrada, aunque todavía quedan por hacer el renderer, el arnés de entrada y el rigging de VFX, sonido y UI
Alcance del trabajo para pasar de Unity a Godot
- El port se dividió en tres líneas
- Importación de assets: llevar los assets de tiles al proyecto de Godot
- Port del ensamblado principal: mover
XRL Application, el motor principal del juego que no es Unity, y elGameManagerdel lado del motor - Configuración del rig de renderizado: conectar la salida en pantalla y la entrada una vez que el núcleo arranque
- Se creó un proyecto “mobile” de Godot y se copió el contenido de texturas de Qud a la carpeta raíz del proyecto
- Como Godot no podía procesar los antiguos archivos
.bmp, se convirtieron en lote a PNG los archivos, incluidos los tiles ASCII - Tras la conversión, los assets se cargaron, pero tardó unos 30 segundos en aparecer el indicador de progreso de importación
Reducir errores de compilación mientras se porta el ensamblado del núcleo
- Después de copiar
XRL Application, se generaron un proyecto y una solución de C# en Godot, y en el estado inicial aparecieron 5,641 errores - Las bibliotecas de uso general como
ConsoleLibyGenkitse trasladaron completas; Kobold, una solución antigua de sprites y atlasing, se decidió importar archivo por archivo revisando si hacía falta KoboldJSON, al ser una biblioteca simple de serialización JSON, se trasladó tal cual- Al hacer un reemplazo global en 85 usos de
Color, los errores bajaron a 4,700 - Al agregar la carpeta
GeneratedCodeque faltaba, se incluyeron los resultados de generación de código de clases por evento y los errores se redujeron hasta 1,557- Caves of Qud tiene una estructura que genera código para clases por evento con mucho boilerplate, con lo que obtiene rendimiento y una superficie de eventos fuertemente tipada
Ordenar dependencias de Unity y la capa glue
Embark Buildery el chargen de creación de personaje tienen mucho código de Unity UI, así que se eliminaron para la prueba técnica inicial en ASCII- Todavía no existe una versión de consola, pero la validación inicial puede probarse cargando partidas guardadas o con un inicio aleatorio
- La carpeta
Gamees en su mayor parte glue entre Unity y el juego, pero también contiene carpetas que no son glue, comoCodeGeneration, así que se eligió moverlas a una raíz separada o a una carpetaPlatform - Al trasladar las bibliotecas
LanguageyHistoryKit, los errores bajaron hasta 454, y ayudó que, aunque no fuera perfecto, existiera una separación entre game y glue - Las grandes categorías de errores restantes eran
CodeDom/Roslyn,PlayFab,Harmony, algunos problemas relacionados con el compilador y la superficie UnityEngine que se había filtrado a la capa del juego - El código relacionado con
PlayFabse comentó temporalmente y quedó como candidato para subirlo a un módulo glue
Stubs de reemplazo para la API de Unity y diferencias de Godot C#
- Como
Color32es un tipo de color basado en bytes, se escribió rápidamente una implementación sustituta - Se creó la carpeta
UnityEngineReplacery, sin consultar documentación de Unity ni referencias de código, se implementó solo la superficie necesaria observando los errores de compilación - Al crear un stub de
GameObject, los errores de “no se conoce GameObject” se convirtieron en errores sobre campos y métodos realmente necesarios, lo que permitió identificar la superficie de interfaz que usa el juego - Con
AudioSourcetambién se fueron agregando uno por uno los miembros accedidos mirando la lista de errores, hasta completar toda la interfaz del puerto de AudioSource que el juego usa realmente - Otras superficies tratadas fueron las siguientes
- shim sustituto para
Debug.Log - implementación sustituta de
Screen - port de la biblioteca de métodos de extensión
IsNullOrEmpty - tratamiento de referencias a
Mathy de la diferencia entrePI/Pi - tratamiento de la diferencia en Godot de los nombres de coordenadas en mayúscula
X,Y - limpieza de imports incorrectos de
System.Drawing.ColorySystem.Numerics.Vector3hechos por el IDE
- shim sustituto para
- Newtonsoft JSON se resolvió agregando el paquete NuGet desde Visual Studio, y CodeDom también se resolvió con el paquete NuGet
CodeDom
Hasta el arranque completo en Godot
- Cuando los errores bajaron a 11, el problema restante era código relacionado con la compilación dinámica de C# en la gestión de mods; no era imprescindible, pero era complejo y quedó hasta el final
- Luego se resolvieron la fase de enlace y los errores de campos en stubs, y se logró compilar unas 500 mil líneas de C#
- El siguiente paso fue crear un pequeño renderer y un arnés de entrada para arrancar el ensamblado del núcleo, con el objetivo de hacer que el juego pudiera ejecutarse en modo ASCII+tiles
- Se creó una scene vacía en Godot y se adjuntó
GameManager.csal node básico;GameManagerheredaba deNodey requería código partial _Readyde Godot se usó como equivalente deAwakede Unity, y_Processcomo equivalente deUpdatede Unity- Durante la inicialización se movieron el load path y la gestión de mods, y el problema de que el type resolver no encontrara tipos por nombre se rastreó con un enfoque tipo printf
- La causa era el manejo de dynamic assemblies
- Como los mod assemblies eran dynamic, se estaban excluyendo los dynamic assemblies al inspeccionar el main assembly
- En el editor de Godot, el main game assembly también es dynamic, por lo que quedaba fuera de los destinos de búsqueda de tipos
- Tras la corrección, se logró el arranque completo, y el núcleo de juego de 500 kloc quedó entregando frames y esperando entrada
- El trabajo restante se describió como “just work”, pero en la práctica todavía queda una cantidad considerable de trabajo en VFX, sonido y rigging de UI
1 comentarios
Opiniones en Hacker News
Este juego parece usar casi por completo un motor propio, y Unity solo como capa de abstracción de hardware y armazón para ports, así que en términos de dificultad para portarlo parece estar cerca del mejor escenario posible.
Hay sorprendentemente muchos juegos así, pero claro, no representan a la mayoría de los títulos de Unity.
No sé si después de la renovación de la UI sigue siendo así, pero si lo es, siempre me pareció raro que no lo portaran a MonoGame para ahorrar costos.
Con Android pasa algo parecido: el sistema operativo solo se usa para cargar bibliotecas que reemplazan cosas como detección de cámara o cifrado, hasta que un día uno se da cuenta de que el sistema operativo en realidad no es más que una delgada capa de metal y un bootloader.
https://nitter.net/unormal/status/1703163364229161236
Maldito Elon. Ahora lidiar con Twitter es realmente irritante.
Wow, qué genial. También está bueno poder ver este proceso de port paso a paso, y me sorprende que el tiempo que tomó sea bastante razonable.
Si tiene tanto código custom y usa menos las funciones del editor, parece que podrían usar una biblioteca de renderizado en lugar de un motor.
Con un motor como base obtienes todo eso gratis. Aunque con Unity quizá venga con un costo.
Si quieres saber qué piensa Brian, conviene ver este video: https://www.youtube.com/watch?v=U03XXzcThGU
Aprendí mucho de él hace tiempo.
Me recuerda a la reciente polémica de DHH sobre TypeScript.
Si uno imagina hacer un port así sin tipos estáticos, habría que compilar y ejecutar con cada cambio y buscar los crashes. En momentos así, se agradece mucho haber construido todo con un lenguaje fácil de refactorizar y una buena arquitectura.
No tengo cuenta de Twitter, ¿qué pasó?