Nota del autor: La entrada actual es muy técnica, muestra el avance de un desarrollo de apenas dos semanas y apenas contiene la pantalla de "resultado" y la clasificación de liga.
Build del juego corriendo en un Mac M1.
---
Es innegable que la controversia generada en torno al desarrollo de PC Fútbol 8 ha tenido un efecto inesperado: En el último año se ha producido un auge en la creación de videojuegos de gestión de fútbol desarrollados por programadores independientes (como Pro Football de Nokenny o del World Season Footballde Pablo Palma) e incluso de pequeños estudios independientes (como “DMT: Director Manager Total” de Nightwolf Games). En definitiva, el fracaso de PC Fútbol 8 parece haber impulsado una nueva era para los videojuegos de gestión de fútbol independientes. Así que para no ser menos, en este blog nos subimos a la ola y también vamos a crear uno y de paso os voy a documentar todo el proceso.
Lo primero es lo primero: Tu y yo queremos hacer un juego, no somos nadie y queremos empezar en el mundillo. Desde mi punto de vista, la mejor estrategia sería comenzar organizando un esquema de prototipo incremental. Es decir, desarrollar una pequeña funcionalidad inicial, mejorarla en cada iteración e ir añadiendo nuevas funcionalidades gradualmente.
En este tipo de organización, generalmente se define un MVP, acrónimo de “minimal value product” en inglés, que consiste en definir los elementos esenciales para considerar que se tiene un producto viable. En este sentido, algunas personas tienden a extenderse y comenzar a modelar la base de datos y la relación entre objetos (jugadores, equipos, ligas, amonestaciones, sanciones, entrenadores, selecciones, etc.). Si bien esto no es negativo, al iniciar desde cero es recomendable ser más prudentes. Es importante recordar que el modelo iterativo permite desarrollar la aplicación gradualmente y que, con el tiempo, se podrán implementar todas las funcionalidades deseadas. En resumen, es preferible avanzar con cautela en las etapas iniciales y definir un primer objetivo modesto.
Consideremos ahora el desarrollo de un simulador de fútbol. ¿Cuáles son los elementos esenciales para empezar a programar? No pienses en algo vendible o una demo pública: Piensa en por dónde debes de empezar a programar. ¿Empezarías por la configuración del once inicial de un equipo? ¿En el mercado de fichajes? ¿En la pantalla de inicio al arrancar la partida? Personalmente, yo opino que la emoción principal radicaba en ganar o perder partidos. Por lo tanto, enfoqué esta primera iteración para centrarla en una pantalla de modo resultado: dos equipos predefinidos, sin posibilidad de configurar la alineación ni la táctica, pero aplicando la lógica necesaria para determinar el equipo superior y presentar una serie de resultados consecuentes.
En mi caso he comenzado a prototipar utilizando la versión 4.4 de Godot (mi entorno de desarrollo para juegos preferido) en su variante para .NET. La razón de esta elección reside en la versatilidad que ofrece en comparación con GD (el lenguaje por defecto de este IDE): Permite la utilización de bibliotecas DLL propias, el uso de proyectos de pruebas unitarias como NUnit e incluso me simplifica la reutilización de código para la portabilidad del juego a otros entornos de desarrollo integrado, como Unity (spoiler: vais a flipar).
Para comenzar con el prototipado únicamente necesitamos crear una escena vacía en 2D y añadir dos componentes: Un label para mostrar el resultado y un botón que, al ser pulsado, actualizará el marcador. En esta fase inicial no se requieren elementos adicionales: No es necesario enfocarse en la estética, ya que posteriormente analizaremos qué información añadir y pensaremos cómo optimizar la interfaz. Quieres empezar a programar, limítate a eso.
Para empezara a aportar la lógica (calcular resultados al pulsar un botón), creamos un script dedicado a ello. Personalmente prefiero mantener una organización clara: las escenas las almaceno en una carpeta llamada “Screens” y los scripts en una carpeta llamada “Scripts”, manteniendo la misma nomenclatura para los dos casos. No hace falta que te adaptes a mi forma de trabajar, pero que sepas que ésta es mi forma de trabajar.
La primera versión del script, como veis, no reinventa la rueda: Recuperamos el label del marcador en la definición de la clase y creamos una función "_on_button" donde ofrecemos un resultado aleatorio. Tranquilos, esto evolucionará y lo haremos más complicado, pero el objetivo de esta primera versión del script es únicamente verificar que al pulsar el botón de "Jugar Partido" el marcador se actualiza.

Ahora que tenemos algo básico pero funcional es cuando podemos pensar en complicar nuestra solución. Atentos a la jugada: Si queremos que nuestro juega sea portable a varias plataformas (y migrar por ejemplo a Unity, WPF, Windows Form o Xamarin), nos conviene separar la lógica de la interfaz. Es decir, crear un esquema de aplicación frontal y back, donde nuestro back sería un juego de librerías en formato DLL.
Lo que vais a ver a continuación hace exáctamente lo mismo, pero pasando por una DLL casera.
Código fuente de la DLL:
Nueva versión del script de Godot:
Básicamente hemos pasado el peso del backend a una DLL totalmente independiente de Godot, mientras que en el fronend (Godot) hacemos llamadas a dicha DLL para hacer uso de los objetos y cálculos del juego. Este enfoque no reinventa la rueda, es una mera separación de capas y se ha utilizado, por ejemplo, en juegos como The Elder Scrolls IV Oblivion (de ahí que el resmaster en Unreal de 2025 utilice gran parte de la lógica del juego original de 2006).
Y no contentos con esto, también podemos crear un proyecto de tests unitarios para verificar el correcto funcionamiento de nuestra DLL, para evitar las molestas pruebas manuales de "pulso un botón y se actualiza un formulario".
(El "tick verde" de la izquierda significa que la prueba se ha ejecutado y que el resultado es OK).
¿Por qué el máximo de goles es 90? Pues porque no creo viable meter más de un gol por minuto.
- De todas formas iremos adaptando los tests en función de cómo avance el desarrollo -
En pocas palabras, acabo de separar la solución en tres módulos: Los scripts de Godot que tienen una lógica básica, una librería en formato dll que tiene toda la lógica pesada y un proyecto de tests unitarios que prueba que nuestra dll funciona correctamente. Lo que es más gracioso es que depurando los tests unitarios puedes conocer el resultado de los partidos sin siquiera ejecutar el juego, lo cual te ahorra bastante tiempo en pruebas.
Y ahora viene la magia: Podemos pillar nuestra dll e inyectarla en un proyecto de Unity:
Como veis, en el script de Unity hacemos exactamente lo mismo: Recuperamos el marcador y llamamos a nuestra dll casera y, como ésta ya la hemos probado en nuestro proyecto de tests unitarios, la aplicación funciona a la primera como se espera:
- Código de Unity adaptado para funcionar en Ouya -
Cabe destacar que para asegurar su correcto funcionamiento, la biblioteca DLL debe ser compilada en .NET 4.0, debido a las limitaciones inherentes a mi versión de Unity (la última versión de Unity compatible con el SDK de Ouya es la 2019.2 y su cliente de Android utiliza Mono, una reimplementación independiente del framework .NET). En la práctica esta limitación no representa un obstáculo significativo: Debido a la retrocompatibilidad, Godot y nuestro proyecto de pruebas unitarias pueden continuar operando en .NET 8.0 (o superior), simplemente debemos de asegurarnos que nuestra DLL casera sea compilable en .NET 4.0.
(Api compatibility Level -> .Net 4.X)
Pero volvamos al lío: Dejemos de filosofear y volvamos a programar nuestro juego. De hecho, vamos a dar un paso más, quitar ese absurdo random que nos calcula resultados aleatorios y añadamos un poco más de lógica a los partidos.
Dentro de nuestra DLL creamos una clase de tipo Squad y le añadimos un nombre y una media global. De normal la capa aplicativa se genera en otro tipo de solución, en alguna específica al ORM, pero recordad que estamos prototipando. ¿Por qué squad y no team o footballClub? Puff, no sé, me apetecía llamarlo squad y punto.
Pero no paremos aquí: Hagamos además que los los equipos tengan 11 jugadores (un once) con tres líneas bien demarcadas: Defensas, centrocampistas y delanteros.
Aquí os dejo los once ideales del Valencia y Betis con medias calculadas por ChatGPT:
De esta forma abandonamos el simple integer donde decíamos la media de un equipo y pasamos a analizar qué equipos tienen mejor defensa, mejores delanteros, más posesión y adaptar nuestro algoritmo en consecuencia. Básicamente, la suma de medias de los centrocampistas nos permitirá saber qué equipo tendrá más posesión del balón (a más media y/o más jugadores jugando de centrocampistas, mayor posesión), a más medias/jugadores de defensa más robusta será nuestra zaga y en cuento más delanteros o mejor media tengan, más daño haremos en ataque:
Como es de esperar, al añadir más lógica en nuestra DLL, tenemos que añadir más tests unitarios:
Y también he modificado la simulación de partidos en consecuencia:
¿Y podemos hacer más? Sí, metamos información sobre el estadio donde se juega el partido y que la asistencia del público vaya en función de la calidad del delta entre el equipo local y el equipo visitante.
¿Y podemos hacer más? ¡Sí! Podemos separar la capa de datos (modelos y base de datos) y
lógica, es decir, crearnos otra DLL para la separación de la lógica
(librería actual, que podemos renombrar a "Core") y de la parte de
modelado y acceso a la base de datos (que podemos nombrar "Data"). Lo
bonito de este enfoque es que podemos reemplazar la librería "Data" en
función de si mañana tenemos que recuperar los datos de un fichero json,
de una base de datos SQLite o de un fichero XML, sin tener que realizar
ninguna modificación en la libería "Core" en caso de tener que bascular
de un formato a otro.
¿Y podemos hacer más? ¡Sí! Podemos definir un calendario de partidos e ir basculando de uno a otro (si uno de los dos equipos está marcado como "jugable") o simularlos directamente (si los dos equipos del partido son de la CPU). De hecho, aprovechemos que hemos separado la capa de datos y lógica para crear más equipos.
¿Y podemos hacer más? Sí, podemos simular todos los partidos, incluso los que no vemos en nuestra pantalla de resultados e ir calculando una clasificación en liga.
De hecho, Ppra la transferencia de objetos entre escenas, los desarrolladores en Godot generalmente utilizan una clase personalizada cargada al inicio del juego, la cual hereda parcialmente de Node (a través de "Configuración del proyecto > Globales > Autoload"). Considerando que necesito asegurar una compatibilidad entre Unity y Godot, he optado por un enfoque alternativo: la creación de una clase estática. En .NET una clase estática es un tipo de clase que no puede ser instanciada, pero que cuyos atributos pueden ser usados como "globales" mientras la carga de ensamblados de .NET (assemblies) de la aplicación sigan en memoria. Este tipo de clase se utiliza comúnmente para almacenar métodos utilitarios o constantes independientes del estado de una instancia, por lo que podemos emplearlo para almacenar en memoria los objetos críticos del juego. En otras palabras, si se define una clase estática “Game”, podré modificar todos sus atributos en tiempo de ejecución (equipos, partidos, ligas...) y recuperar los valores actualizados en cualquier otra escena.
Pero aún podemos hacer más. Podemos aprovechar que ya tenemos algo parecido a un prototipo para hacer una interfaz gráfica potable en Blender y ponérsela a nuestro juego. Y como veréis, está muy inspirado en la partida de resultado de PC Fútbol 6, de Dinamic (1997/1998).

Y aplicando las mismas metodologías (acceso a la clase estática y modelación en Blender para la interfaz), podemos hacer también una pantalla de clasificación en Liga e incluso una sencilla pantalla de selección de equipos:
E incluso, para que sea más ameno el paso de darle todo el rato a "Siguiente, siguiente, siguiente..." (sí, a nivel jugable nuestro juego aún no es muy top), también he implementado un hilo de música que muestra la información del autor y el tema que suena.
¿Y cómo queda por ahora? Bueno, os muestro un gameplay de cómo ha quedado esta primera iteración:
Sobre el código fuente, me habría gustado liberarlo y publicarlo en mi GitHub (en especial el port de Ouya), pero para ello quiero antes limpiar las referencias a los equipos reales (jugadores, escudos, nombres) y no es algo que considere prioritario.
Sobre las iteraciones, quiero hacer sprints de 3 semanas, donde la primera me enfoco a hacer todas las mejoras posibles, la segunda al pulido y la tercera simplemente es de descanso La primera iteración dio comienzo la semana del lunes 26 mayo y la segunda dará comienzo mañana, lunes 16 de junio... Así que, si todo va bien, dentro de tres semanas deberíais ver una nueva entrada con los avances del desarrollo de este juego.
¿Qué he aprendido de este primer sprint?
- Es más divertido programar en Godot que en Unity. De hecho, el port de Unity me ha dado bastantes problemas y he tenido que sacrificar el fader (degradado entre escenas) porque me daban problemas en las builds que no se mostraban en el editor.
- Programar un port en Android (Ouya) ha sido un reto que ha consumido más tiempo de lo que esperaba. Me ha obligado incluso a reimplementar cómo obtenía números aleatorios (objeto Random en .NET) para evitar sorpresas sobre las semillas que por defecto usan las librerías de Mono.
- Empecé con un triste marcador aleatorio y he acabado haciendo varias pantallas de fondo que resultan atractivas a simple vista. La metodología de trabajo que estoy empleando parece ideal para equipos de una o pocas personas.
- Hacer tests unitarios me ha salvado la vida, puesto que me ha permitido debuguear y corregir bugs que no eran evidentes a simple vista... antes incluso de buildear el juego (si el test no pasa, el juego no compila). ¿Tienes un problema "aleatorio" (guiño, guiño)? Pues haces un test unitario sobre la funcionalidad que da el problema y analizas su comportamiento. De hecho, tener el reflejo de hacer uno o varios tests unitarios por cada nueva funcionalidad te ahorra tiempo en testing.
- Queda más bonito renderizar los menús en Blender y cargarlos como fondos 2D en el juego que cargarlos en 3D directamente en Godot/Unity (he hecho la prueba). Esto también te asegura que los menús se vean igual en ambas versiones y minimiza el consumo de recursos en el juego.
No hay comentarios:
Publicar un comentario
Si te ha gustado la entrada o consideras que algún dato es erróneo o símplemente deseas dar algún consejo, no dudes en dejar un comentario. Todo feedback es bienvenido siempre que sea respetuoso. También puedes contactarme por estas redes sociales https://linktr.ee/hamster_ruso si lo consideras necesario.