inicio / blog / herramientas
Publicado: 22 de octubre de 2025 10 min de lectura herramientas
CI/CD en el desarrollo de videojuegos: builds nocturnas

CI/CD en el desarrollo de videojuegos: builds nocturnas

Montar la automatización de builds en un equipo pequeño llevó cinco días: la vida media de un commit roto bajó de 2,5 días a 9 horas y las entregas se calmaron.

El build que se rompió un viernes por la noche

El año pasado, el viernes por la tarde en que teníamos que enviar una demo a la editora, mi equipo de escritorio era la única máquina capaz de generar un build. Éramos cuatro en el proyecto y yo era el único que sabía hacerlo. A las 19:40 murió con Shader error in 'Custom/Water': undeclared identifier '_TimeScale'. Esa línea llevaba tres días subida y nadie se había dado cuenta, porque esa variante de shader nunca se compilaba en el editor.

Encontrarlo costó 25 minutos; arreglarlo y volver a compilar, otros 70. Para localizar el cambio culpable tuve que recorrer a mano un rango de 14 commits. La demo salió a las 23:10. Lo malo no fue el retraso: lo malo fue que durante tres días ninguno de nosotros sabía si el proyecto compilaba de verdad.

Nuestro CI/CD para desarrollo de videojuegos nació de esa noche. El objetivo nunca fue un pipeline elegante: era responder qué commit lo rompió en una noche en lugar de en tres días. Empezamos con GitHub Actions porque el repositorio ya estaba allí, y pasamos a Jenkins cuando añadimos una segunda máquina de build.

Qué cambia realmente un build nocturno

El único trabajo de un build automático es que, cuando te sientas por la mañana, ya te espere una versión funcional del código de anoche. El nuestro se dispara a las 03:00, termina hacia las 06:20 y deja una sola línea en Slack: hash del commit, duración, tamaño del paquete y resultado de los tests. Durante dos meses enviamos un informe más largo y nunca vi que nadie lo abriera, así que volvimos a la línea única. También corre los fines de semana: encontrarte un repositorio roto un lunes por la mañana es mucho mejor que encontrártelo un viernes por la tarde.

La ganancia más concreta que puedo medir es esta: la vida media de un commit roto pasó de 2,5 días a 9 horas. Cuanto antes se detecta la rotura, menos commits se apilan encima, así que lees un solo diff en vez de lanzar git bisect. El coste de arreglarlo también baja, porque quien escribió el código todavía recuerda el contexto.

La segunda ganancia está fuera de programación. Como la máquina produce un exe jugable cada mañana, el «en mi equipo funcionaba» prácticamente desapareció: el diseñador de niveles y QA abren exactamente el mismo build. Preparar las entregas a mano me costaba unas 4 horas por semana, y esas horas también volvieron. El build que enviamos a la editora el viernes ahora está listo el jueves por la noche, y lo único que queda para el último día es redactar las notas de versión.

Builds por línea de comandos en Unity y Unreal

La parte de Unity parece trivial, pero la primera trampa es el flag -quit. En una cadena -batchmode -nographics -quit -executeMethod el editor puede cerrarse antes de que termine tu trabajo asíncrono, y el código de salida siempre es 0. Nosotros quitamos -quit y llamamos a EditorApplication.Exit(code) al final del método, de forma que un fallo de compilación pone CI en rojo de verdad. Enviar el log a stdout con -logFile - importa lo mismo: si lo escribes en un fichero, el error no aparece nunca en la interfaz de CI.

La lista de escenas de un build la leemos de nuestro propio ScriptableObject BuildProfile en vez de EditorBuildSettings.scenes. El motivo es práctico: esa lista es un asset, generaba conflictos de merge constantemente y dos veces se coló una escena de pruebas dentro de un build sin que nadie lo notara. El mismo asset lleva los define symbols y el número de versión, así que con qué se hizo un build vive en un único sitio.

En Unreal todo pasa por RunUAT.bat BuildCookRun. Una pasada completa con -clientconfig=Development -cook -stage -pak -archive tarda 48 minutos; con -iterativecooking se saltan los assets sin cambios y baja a 11-14 minutos. No confiamos en el cook iterativo en la ejecución nocturna, solo en los builds de pull request. La pasada nocturna usa además -nocompileeditor, que ahorra otros 6-7 minutos.

build.yml
# Nightly game build - self-hosted runner, 03:00 local time name: nightly-build on: schedule: - cron: "0 3 * * *" workflow_dispatch: jobs: win64-player: runs-on: [self-hosted, windows, unity-6000] timeout-minutes: 90 steps: - uses: actions/checkout@v4 with: lfs: true # A cold Library/ import costs ~26 min, so we keep it on the machine - name: Restore Library cache run: robocopy D:\cache\Library Library /MIR /NJH /NJS /NFL /NDL; exit 0 # No -quit: BuildRunner calls EditorApplication.Exit with the real code - name: Build player run: > "$env:UNITY_PATH\Unity.exe" -batchmode -nographics -projectPath . -logFile - -buildTarget Win64 -executeMethod BuildRunner.BuildWin64 - name: Smoke test and perf gate run: | .\Build\Game.exe -runScene SmokeTest -timeout 180 python tools\check_frametime.py --budget 16.6 --p99 22.0
yamlbuild.yml

Perforce, Git LFS y disciplina de caché

Un repositorio de 40 GB no es aquello para lo que se diseñó Git. Con Git LFS nuestros ficheros .psd y .fbx se convirtieron en punteros, y aun así una máquina nueva necesitaba 55 minutos para su primer clonado. El sparse checkout más lfs.fetchexclude lo dejaron en 12 minutos. Pedir a los artistas que se bajen solo las carpetas que necesitan no funcionó: nadie que tenga que acordarse de un comando se acuerda.

El problema real es el bloqueo, no el tiempo de descarga. Cuando dos personas tocan el mismo Environment_Rocks.fbx no existe eso del merge: alguien pierde su trabajo. Nos pasamos a Perforce no porque sea técnicamente superior, sino porque los artistas ya lo conocían y allí el exclusive checkout es el comportamiento por defecto. Las licencias y el mantenimiento del servidor son un coste anual real, pero barato al lado de las horas de trabajo que perdíamos. Mi umbral es simple: por debajo de 20 GB basta con Git LFS, por encima toca Perforce.

En caché los números son brutales: regenerar una carpeta Library/ limpia cuesta 26 minutos en nuestro proyecto, mientras que la compilación real son 9. Por eso Library/ se queda en la máquina de build, pero no a ciegas: Library/PackageCache se borra cada vez que cambia la versión de Unity, y una vez por semana lanzamos un build completamente limpio. Si no, un fallo que la caché estaba tapando aparece meses después el día de la entrega. Ese build limpio lo movimos a la noche del domingo, porque no pensamos devolver esos 26 minutos en día laborable.

En Unreal el mismo papel lo hace DerivedDataCache y lo tenemos en una carpeta de red compartida. La compilación de shaders cuesta 38 minutos en frío y 4 minutos con la DDC caliente. Leer por red es más lento que el disco local, así que la máquina mantiene además una segunda capa de DDC local. Cuando la carpeta pasó de 180 GB añadimos una tarea que poda las entradas viejas: si el disco se llena, el build no falla, simplemente se vuelve más lento en silencio, y tardamos dos semanas en darnos cuenta.

Smoke tests y una puerta de regresión de rendimiento

Cuando digo pruebas automáticas no me refiero a una suite exhaustiva. Nuestra escena SmokeTest arranca el juego, entra al primer nivel desde el menú principal, reproduce 30 segundos de input grabado y sale limpiamente. Solo con eso cazamos 6 de las 9 roturas de build que tuvimos en un año, y avisó a las 07:00. Las otras 3 salían únicamente en una GPU concreta o en el segundo nivel; en vez de engordar el smoke test para eso, añadimos dos puntos a la lista matinal de QA.

La segunda puerta es el rendimiento. Recogemos los tiempos de frame en la misma escena y miramos el p99 en lugar de la media; el objetivo son 16,6 ms con un umbral de p99 de 22 ms. Una media esconde un único tirón de 40 ms, y ese tirón es justo del que se quejan los jugadores. Empezamos a medir después de los primeros 120 frames, porque el warm-up de shaders cae en esa ventana e infla el p99 hasta dejarlo sin sentido.

Un solo build por encima del umbral no pone el pipeline en rojo; hacen falta dos seguidos. Aunque la máquina no hace nada más y tomamos la mediana de tres mediciones, queda alrededor de un 6% de ruido; pon el umbral más apretado que eso y en un mes el equipo empieza a ignorar las alertas. También guardamos el artefacto de un build que falla, para poder perfilarlo contra el de la noche anterior en lugar de que desaparezca. El p99 de las últimas 14 noches lo mantenemos en un CSV plano, porque la tendencia dice más que cualquier número suelto.

Publicar builds en Steam y TestFlight

La distribución es el paso más fácil técnicamente y el más irritante en lo operativo. Subir a Steam es una línea, steamcmd +run_app_build app_build.vdf, pero si no inicias sesión a mano una vez en la máquina de build y guardas el fichero ssfn que deja Steam Guard, el pipeline se atasca ahí todas las noches. Los builds nocturnos van solo a una rama internal protegida con contraseña. Subir un depot de 9 GB tarda unos 7 minutos y, después de la primera vez, solo viajan los trozos que han cambiado.

En iOS la cadena es xcodebuild -exportArchive seguido de xcrun altool. La única regla que importa es atar CFBundleVersion al número de ejecución de CI; manda dos veces el mismo número de build y App Store Connect lo rechaza, con el error llegando por correo 20 minutos después. El procesado de TestFlight tarda entre 8 y 20 minutos, así que no está listo antes del café de la mañana. Instalar los certificados y los provisioning profiles en el llavero de la máquina y añadir un paso security unlock-keychain fue lo que más tiempo se comió esa primera semana.

El montaje completo le llevó a una persona 5 días: 1 día para los builds por línea de comandos, 1 día para la máquina y el runner, 1,5 días para caché y control de versiones, 1 día para tests y medio día para las subidas a tiendas. Mantén ese orden. La primera semana monta un pipeline que solo responda «¿compila?» y deje el artefacto en una carpeta; deja los smoke tests y las subidas a tienda para meses posteriores. El pipeline pide su propio mantenimiento, en nuestro caso medio día al mes, y si no presupuestas esa partida se pudre en seis meses. Nadie construye un sistema completo el primer día, y medio pipeline que funciona vale más que uno entero que no.

← Todas las entradas