19 años en Internet

15 junio 2025

Programando un simulador de fútbol. Capítulo 1: El modo resultado

    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.

 

Build del juego corriendo en una Ouya.
 
 
Captura de pantalla de la build 0.6.7.G, 

---

     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.

     Así que vinculamos la función al evento "button_down()" de nuestro botón, ejecutamos la escena y verificamos que funciona como esperamos: Cada vez que pulsamos el botón, se genera un resultado aleatorio.



      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:


- Ejemplo del código de Godot adaptado al código 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:


       Ahora bien, si sois avispados, habréis notado que en Unity he añadido en la arborescencia un objeto de tipo OuyaGameOject... Y esto es debido a que símplemente quiero que la versión de Unity sea un port para mi Ouya. ¿Por qué para Ouya? Pues porque puedo y porque quiero: Tengo una Ouya y me apetece darle uso.


- 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. 

 
     Aprovechamos dicha clase para crear dos equipos en nuestra DLL: El Valencia, con una media de 70, y el Betis, con una media de 80 (así, un integer a pelo sin cálculos previos). Asimismo, actualizamos el método de simulación de partidos para calcular los goles de cada equipo en función de su media y la de su oponente. Es decir, si el oponente es fuerte, será más difícil anotar goles. Adicionalmente, se ha añadido una ventaja del 24% a los equipos locales. Esta ventaja se basa en el análisis de estadísticas de las dos últimas temporadas, donde se observa que los equipos locales mostraron una eficiencia goleadora un 24% superior a la de los visitantes.


     Ejecutamos nuestro proyecto de tests unitarios, verificamos que no hemos creado ninguna regresión y comprobamos que el juego sigue funcionando:
 


      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:



      Ok, ya tenemos algo parecido a un algoritmo, pero podemos mejorarlo: Añadamos el concepto de rol, es decir, especificar mejor la demarcación de los jugadores (definir si son centrales, líberos, mediocentros defensivos, etc) y pongamos también el concepto de táctica (que será una lista de 10 roles + portero), de forma que si el futbolista ocupa un rol que no domina, se produzca también una penalización. De la misma forma, si vemos que el equipo tiene alguna banda libre (más propenso a atacar o defender por un lado que por otro) pueda también producirse una penalización.
 
       Bueno, lo primero es lo primero, pare hacer uso de todo eso nos hace falta dos cosas: Definir demarcaciones más precisas y empezar a calcular los partidos en función de una serie de mapas de calor (zonas donde posicionamos los jugadores). La idea principal es hacer que cada demarcación tenga un mapa de rendimiento y que en función de ese mapa, nuestros futbolistas ataquen o defiendan en determinadas zonas. Además, añado el concepto de táctica, de forma que un equipo tenga su propio esquema de juego y que si un jugador está en una posición que no conoce sufra una penalización en su rendimiento.
 






     Y también he modificado la simulación de partidos en consecuencia:

 
     Y como es lógico he tenido que añadir más tests unitarios y modificar los ya existentes.
 




      Pero podemos ir más lejos, porque, si nos fijamos, podemos adaptar fácilmente nuestro algoritmo para obtener quienes han hecho los goles y los porcentajes de posesión. De esta forma, en vez de devolver un string al front (la app de Godot/Unity), podemos devolver un objeto que contenga estas informaciones:
 





 
      ¿Y funciona? Pues parece que sí, está funcionando bastante bien. Me da resultados bastante aleatorios pero son marcadores que parecen creíbles.
 





 
     ¿Podemos hacer más? ¡Sí! Podemos replicar todos los parámetros que se utilizaban en los PC Fútbol clásicos para cada futbolista, calcular distintos rendimientos según la línea (portero, defensa, centrocampista y delantero) en función de dichos "stats" y adaptar nuestro simulador en consecuencia:
 



 
     ¿Y podemos hacer más? ¡Pues sí! En vez de hacer iteraciones de tiempo en el partido basadas en porcentajes de posesión del balón, podemos hacer que el partido tenga más de 90 minutos (tiempos extra) y hacer que la oportunidad de gol o jugada funcione de un lado o de otro según los porcentajes de balón. Es decir, si un equipo A tiene 40% de posesión, durante cada minuto tendrá un 40% de probabilidad de hacer jugada.
 

 



     Y aún podemos ir más lejos: Dividir la partida en varios tramos, de forma que tengamos una pantalla de inicio, una pantalla descanso del primer tiempo y una pantalla de resultado final.
 
 
 

 
 

      ¿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.







     Y aquí, en la liga y el calendario, es cuando empiezan a salirnos las primeras dudas en el desarrollo: ¿Cómo hacemos pasa modificar un objeto (por ejemplo para cambiar la clasificación de la liga) y hacer que se mantengan estátitos (modificados) en el intercambio de escenas (pasar de una pantalla a otra) sin tener que escribir y recargar nada en la base de datos o en ficheros de texto? El tiempo de escritura y lectura en disco es más lento que el acceso de memoria, por lo que nos conviene escribir y leer de disco lo menos posible y no queremos, por ahora, recrear objetos en cada escena.

     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 si os fijáis, en esta interfaz he dejado varios huecos vacíos y mi idea es ir completándolo con un calendario real (arriba a la derecha), añadir información acerca de la asistencia al estadio (abajo a la izquierda) y dar información acerca del MVP (ésta vez sí, el jugador del partido)... Y una vez implementado, el resultado está bastante chuli.


    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?

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.