19 años en Internet
Mostrando entradas con la etiqueta godot. Mostrar todas las entradas
Mostrando entradas con la etiqueta godot. Mostrar todas las entradas

12 septiembre 2026

[DIV2/Godot] Replicando el sistema de combates de Breath of Fire 2

     Últimamente he estado jugando al Brave Battle Saga de Mega Drive (Genesis en América), un juego cuyo sistema de combates me ha recordado mucho al Breath of Fire 2 de SNES. Así que me picó el gusanillo y me propuse programar un sistema de combates parecido y en esta entrada lo enfocaremos desde dos frameworks distintos: DIV2 (para programar juegos en MS-DOS) y Godot 4.7 para .NET (multiplataforma).

    Ahora bien, para los incautos, que sepáis que en esta entrada hay muuucha IA (decisiones, generación de gráficos y audios) y no habrá código fuente explicado, porque el proyecto, pese a ser sólo un combate, ha acabado siendo enorme (en el caso de DIV2, el PRG ocupa más de 5.200 líneas). Lo que sí podéis hacer es leer los readmes de sendos proyectos que he subido en GitHub (https://github.com/LeHamsterRuso/BOF2LikeGodot y https://github.com/LeHamsterRuso/BOF2LikeDIV2), donde explico de forma global la arquitectura del proyecto y para qué sirve cada cosa.

    En el caso de DIV2, al estar todo el código fuente en el mismo fichero, he escrito también un fichero ".md" de unas 500 líneas que detalla cómo funciona el código y está escrito en castellano exprésamente (https://github.com/LeHamsterRuso/BOF2LikeDIV2/blob/main/PRG/BOF2.md)... Por cierto, los assets de la versión de DIV2 son autogenerados con un script de parsing (es decir, un script que pilla los assets de Godot y los bakea a imagen/sonido, generando los ficheros FPG necesarios, más info https://github.com/LeHamsterRuso/BOF2LikeDIV2/tree/main/tools).

     Lo primero es lo primero, enfocar un escenario para el "poc" (proof of concept) y para ello pregunté a varias inteligencias artificiales cual era, en Breath of Fire 2, el combate preferido o memorable según los jugadores y la crítica, para intentar replicar dicho combate. Y bueno, si lo que queréis ver es cómo ha quedado y no leer todo esto, pues os animo a ir directamente al final de esta entrada.

    Valga decir que he jugado bastante al Breath of Fire 2, por lo que lo conozco bastante bien: Lo compré de lanzamiento, me hice todos los finales y lo he vuelto a jugar varias veces a lo largo de mi vida. Con esto dicho, a mi memoria vienen varios combates épicos:

  • En el prólogo tenemos dos reseñables: El primero contra Gonghead, donde Ryu niño intenta salvar a su hermana y acaba rescatado por Ganer; El combate en la cueva contra Barubary, donde nos aniquila de un golpe; El primero resulta interesante porque saltan eventos (Yua nos va curando y Ganer nos salva), pero desde el punto de vista práctico, no lanzamos magias y estamos solos, por lo que no podemos aprovechar todas las mecánicas del sistema de combates.


  • En el inicio de la aventura tenemos el combate contra Baba y contra Katt, pero tenemos el mismo problema, son combates donde Ryu se enfrenta solo a ellos, sin compañeros en la party. No obstante, el de Katt es muy interesante puesto que, además de producirse en un coliseo y esto siempre es épico, saltan eventos en los que Katt lanza frases aleatorias (algunas incluso son guiños a personajes de Clint Eastwood). También está el combate contra las tres arpías, que sería una buena opción (tenemos a Bow en la party y saltan eventos de diálogo).



  • Ya en la parte avanzada en la historia tenemos el combate contra Tiga, el "noviete" de Katt, pero también es a descartar puesto que es un 1 vs 1 y encima tiene vida infinita (el combate está programado para ser una derrota obligatoria por exigencias del guion); Y el combate contra Ganer, donde dependiendo del resultado la historia diverge, pudiendo obtener finales distintos.
  • Y por último, ya en la fase final tenemos la revancha con Barubary y el enfrentamiento final contra Deathevn, que son probablemente las dos batallas más épicas, puesto que tenemos que hacer alarde de todo nuestro potencial mágico. Además, en el de Barubary se nos da a elegir entre pelear solo contra él o con la party, en plan "chicos, dejádmelo a mí, esto es personal" y el mero hecho de que exista esa opción ya nos muestra el mimo con lo que cuidaban los JRPG en la época de los 16 bits.

 Respecto a las IA, esta ha sido su respuesta:

  • Google: El combate contra Deathevan, el duelo individual contra Barubary, la pelea contra Ray y la pelea contra Augus. La pelea contra Ray es interesante, pero no la consideraría memorable... aunque es cierto de que se trata de uno de los pocos duelos de dragón vs dragón que existen en la saga de Breath of Fire; En cambio, la pelea contra Augus, el director del coliseo, me parece interesante, porque se produce al inicio del juego, es uno de los primeros jefes difíciles y en ella tenemos a Ryu y Rand en la party (y opcionalmente a Katt, según los eventos).
  • Grok: Sorpresivamente Grok me propuso los mismos combates más el de Kuwadora, pero priorizando el duelo contra Barubary antes que el de Deathevan. El combate contra Kuwadora es interesante por varios motivos: Transcurre en la mitad del juego, su combate se divide en fases, tiene vigor "infinito" (por lo que siempre ataca primero) y cuando lo programaron le dejaron varios bugs (nunca pasa a su "fase 3" por programar mal la gestión de números negativos en una rutina de ensamblador).


  • ChatGPT: Por su lado ChatGPT me sorprende para bien. A los típicos Barubary y Deathevan, le suma el Golden Fly (una mosca que tenemos que capturar a mitad del juego), el trío de las arpías y boses como Terapin, Trubo y Augus. La del Golden Fly no la vi venir. Terapin es una especie de hormiga reina gigante y azul, que, al más puro estilo reina alien, pobló el pozo de Captian de facehugger y creons. Por su lado Trubo era el Guardia Imperial del clan de los monos de Highfort (la ciudad natal de Sten).


  • Deepseek: En cuanto a Deepseak, a los ya típicos combates contra Barubary y Deathevan, propone también el combate contra Terapin. 

    Uff... cuantas opciones. Los combates contra Deathevan y Barubary son interesantes, pero me da miedo que tenga que implementar demasiadas cosas para un simple tutorial. Visto lo visto, creo que me quedaré con el combate contra Augus.

 


    En ese combate deberíamos de estar entre los niveles 7 y 10, por lo que necesitaré verificar qué magias y stats tienen Ryu, Rand y Katt en esos niveles. Ahora bien, el primer problema que identifico es que stats en Breath of Fire 2 es que son semi-aleatorios.

Por un lado se definen una serie de franjas:

  • Ryu en el nivel 1 debe de tener: 30 HP, 10 MP, 20 STR, 16 STA, 16 AGI, 8 WIS y 12 LUCK.
  • Ryu en el nivel  25 debe de tener: 209 HP, 53 MP, 84 STR, 64 STA, 71 AGI, 77 WIS y 74 LUCK.
  • Esto nos da que en cada nivel debería de crecer +7,083 HP, +2,667 MP, +2 AGI, +2,291 AGI, +2,875 WIS y +2,583 LUCK.

    Ahora bien, las consolas de 16 bits y los decimales no se llevan muy bien y queda muy raro decir que has subido +7,083 HP. Para facilitar los cálculos, se metía alguna variación aleatoria, del estilo subir entre 6 y 8 HP, por ejemplo... y el sistema del juego iba cuadrando las subidas de nivel para que al llegar a las franjas (niveles 25, 40, etc...) los valores convergieran de forma aproximada a los valores esperados.

    Otra cosa graciosa de este sistema es que las magias también se desbloqueaban por nivel, algo bastante común en los JRPG de aquella época y que poco a poco hemos ido substituyendo en juegos modernos por sistemas de perks costumizables en árboles de habilidades.

    Una cosa graciosa con estos dos sistemas de franjas y aprendizaje de magias daba como lugar que personajes como Katt podían aprender a usar Firebolt en el nivel 11... pero no tenía suficiente puntos de magia para usarla hasta el nivel 25 aproximadamente. Es decir, podías llegar hasta la segunda mitad del juego sin poder lanzar un magia que aprendió Katt justo al inicio.

     Bueno, con todo esto dicho, ya tenemos definido el rango de nuestra demo:

  • El enemigo será Augus (https://bof.fandom.com/wiki/Augus_(Boss)).
    • Según las wikis, sus stats son:  680 HP, 16 AP, 43 OFF, 18 DEF, -1 MAGIC VULN, 12 AGI, 28 LUCK, puede atacar doble en el mismo turno, puede conjurar Cure 1 y dropea Herb (hierbas medicinales), 150 puntos de experiencia y 300 monedas.
  • Nuestra party será Ryu, Rand y Katt, todos de nivel 9.
    • Ryu debería de tener aprox: 90 HP, 24 MP, 41 STR, 32 STA, 34 AGI, 31 WIS, 33 LUCK y debería de poder conjurar TimeWarp, Cure 1 (Heal en GBA) y Cure 2 (Restore en GBA).
    • Rand debería de tener aprox: 130 HP, 40 MP, 48 STR, 42 STA, 17 AGI, 33 WIS, 24 LUCK y debería de poder conjugar Cure 1 (Heal en GBA) y CurePsn (Purify en GBA).
    • Y por último Katt debería de tener aprox: 87 HP, 9 MP, 53 STR, 25 STA, 55 AGI,  19 WIS, 41 LUCK y no debería de poder conjurar ninguna magia (aunque en la versión de GBA puede conjurar Slow).
  • Sobre las magias:
    • Cure 1 (Augus, Ryu y Rand), también conocida como Heal en la versión de GBA, sirve para curar vida: Consume 4 AP y reestablece 40 HP.
    •  TimeWarp (Ryu) no consume puntos de magia, pero no se puede usar en combate (sirve únicamente para pasar de noche al día y del día a la noche) y sólo puede usarse en el mapapundi, que no implementaremos en esta demo.
    • Cure 2 (Ryu), también conocida como Restore en GBA, es una magia de curación más potente que Cure 1 / Heal: Consume 12 AP y reestablece 100 HP.
    • CurePsn (Rand) sirve para quitar los estados alterados (veneno, zombie, etc) y consume 8 AP.
  • Además de todo esto, los personajes tienen habilitades únicas y su efecto depende de distintos factores:
    •  Ryu tiene la habilidad única Gust, que le permite recargar vida sin consumir puntos de AP. Su éxito depende de la cantidad de HP que tengamos en ese instante. Cuanto menos porcentaje de vida tengamos, será más probable ejecutar esta habilidad con éxito: Si fallamos, sólo "recargaremos" un punto de HP; Si tenemos éxito recuperaremos entre un 10% y un 50% de nuestros HP (cuanto menos vida tengamos, más alto será el porcentaje).
    • Rand tiene Wake, que permite resucitar un compañero muerto. Su porcentaje de éxito es del 50% aprox, pero intentarlo varias veces suele ser más barato que consumir un LifePL (que si la memoria no me falla tiene un precio de 500 monedas). Realmente su tasa de éxito depende del atributo Gust ("agallas").
    • Katt tiene Dare, que, en caso de éxito, permite provocar al enemigo para que éste centre sus ataques en ella. Tiene una tasa de éxito del 100%, pero funciona siempre a menos que te enfrentes a ciertos jefes finales de mazmorra o enemigos especiales que cuentan con inmunidad total a las provocaciones o alteraciones de comportamiento por fases. Augus, por ejemplo, es una de esas excepciones y la habilidad de Dare no le hace efecto.
  • Los contra-ataques:
    • Ryu y Katt son los dos únicos personajes jugables del juego que pueden realizar contra-ataques al ser atacado. Remarco lo de jugables, puesto que también pueden ejecutarlos determinados enemigos y, por suerte, Augus no es uno de esos.
    • Cabe destacar que sólo pueden activarse como respuesta a ataques de típico físico y no a magias. Su porcentaje de éxito (el que se produzca o no el evento de contra-ataque) depende de cada personaje:
      • La tasa de probabilidad de Ryu es de 1 entre 8, o lo que es lo mismo, del 12,50%. Es decir, cada 100 ataques físicos Ryu debería de haber realizado 12,50 contra-ataques aprox.
      • La tasa de probadilidad de Katt es de 1 entre 16, o lo que es lo mismo, del 6,25%. Esto se ve que se hizo aposta para que los jugadores no abusaran de "Dare".
  • Las agallas. Los puntos de "Guts" suelen ser los grandes desconocidos de este juego:
    •  Cuando un personaje recibe un golpe mortal que debería dejarlo inconsciente (0 HP), el juego realiza una tirada oculta de probabilidad basada en su valor de Guts. Si la tirada tiene éxito, el personaje sobrevive al impacto y resucita instantáneamente con 1 HP, mostrando una animación especial de su sprite levantándose con furia.
      • La probabilidad máxima (7 llamas de guts) es del 25%. Cuantas más llamas de Guts tengas, más cercano estarás del 25% de éxito en esa tirada. 
    • Por un lado los puntos de guts varían según el nivel del personaje y se representan con 7 llamas en la pantalla del estado del personaje. No obstante, al igual que los otros parámetros, realmente son valores que van del 0 al 255 (un byte) y su representación gráfica es una regla de 3. En el nivel 9 Ryu debería de tener unos 128 puntos de Guts (4 llamas), Rand 90 puntos (2 llamas) y Katt unos 75 puntos (2 llamas).
      • Es decir, si teines 75 puntos como es el caso de Katt, tu probabilidad de "resucitar" tras un golpe letal es del 7,35% (75/255*100*0,25) y si tienes 255 puntos tu probabilidad será del 25% (255/255*100*0,25).
    • Por otro, varían en función del combate:
      • Si un personaje muere en combate, su valor de Guts se reduce a 0 inmediatamente. Si el personaje resucita durante esa misma batalla, lo hará sin llamas de Guts.
      • Sufrir un daño masivo o un golpe crítico por parte de un enemigo puede disminuir temporalmente el valor de agallas del personaje afectado.
      • No existen condiciones que permitan ganar puntos de Guts (como hacer críticos al enemigo o realizar un contra-ataque).
      • Al terminar el combate, el valor temporal de Guts se restablece automáticamente a su valor base permanente. 
    •  Habilidades como Wake, de Rand, tienen una bonificación en función de los puntos de Guts que tenga en ese momento. La fórmula para obtener la probabilidad de éxito es: (128 + Guts de Rand)/256. Esto significa que será más fácil revivir personajes con Wake siempre que Rand no haya muerto antes durante el combate.
  • Otro punto "desconocido" para la mayoría de usuarios es la condición, que tiene cuatro valores: Poor, Normal, Good y Super.
    • En personajes como Ryu y Katt influyen, por ejemplo, en la tasa de activación de contra-ataques:
      • Poor -> 1 a 16 (valor por defecto de Katt)
      • Normal -> 1 a 8 (valor por defecto de Ryu)
      • Good -> 1 a 4 (25%)
      • Super -> 1 a 2 (50%)
    • Influye en la probabilidad de evasión, de críticos y de ser afectados por estados alterados.

    Uf... vaya tela. Tremenda especificación acabamos de largar en esta parrafada. Pero aún no hemos acabado:

  • Los combates son por turnos, pero en cada turno el orden de acción de los personajes depende de sus puntos de agilidad. Es decir, en cada turno tendrás que seleccionar las acciones de toda tu party (Ryu ataca al enemigo A, Rand recarga vida, Katt usa Dare...) pero en cuanto definas la última acción de tu grupo es cuando empezará la ronda y, tanto el orden de ejecución de tus personajes como el de la CPU dependerá de los puntos de agilidad de cada uno. Si tuvieramos un personaje con agilidad máxima (255), un enemigo con agilidad 128 y un compañero de agilidad 20, el orden de ejecución será 255, 128 y 20 en todos los turnos. Esto difiere de juegos como Brave Battle Saga o Final Fantasy VII, donde hay una barra de tiempo y los personajes con una agilidad absurdamente alta pueden atacar varais veces más que un personaje de agilidad baja. Por ejemplo, en estos dos juegos que acabo de mencionar, un personaje de agilidad 255 seguramente habría atacado 12 veces más que el de agilidad 20.
  • Además de atacar, hacer uso de habilidades únicas y mágias, los combates tienen permiten consumir ítems (de ataque y de curación) y seleccionar auto (auto-atacar) y huir (que contra jefes tienen 0% de éxito). Pero también tienen otras opciones ocultas como "defenderse" o "cambiar estrategia" (que cambia el posicionamiento de los miembros del equipo) que son accesibles pulsando R y L respectivamente en el menú de selección durante el combate. 
    • Defender reduce el daño en un 50% e incrementa la probabilidad de contra-ataque de Ryu y Katt, llegando a duplicarla e incluso tripicarla en función del contexto. Es decir, que si tienes a Ryu o a Katt en condición Super y los pones a defender, es muy probable que devuelvan con contra-ataques todo el dáño físico que reciben. Y si pones en vanguardia a Katt y ejecutas Dare, muy seguramente será siempre atacada por los enemigos.
  • Existen otros parámetros que hemos mencionado pero no descrito:
    • WIS (Inteligencia), afecta al daño que ejerces con lanzando conjuros/magias. Aunque diga que Cure 1 cura 40 puntos de vida, ese es el valor máximo que puedes curar en función del valor de WIS.
    • LUK (Suerte) es una stat "rota", no sirve para nada y el juego nunca la emplea. Debería de utilizarse en el sistema de chamanes, pero no es el caso por errores de código (tiene varios el juego) y no influye tampoco para el dropeo de ítems, ni en en la prevención de estados (como caer envenenado o ser dormido) y no se emplea tampoco para calcular críticos, ni para calcular si tienes éxito para huir. Para la mayoría de personajes de la party, sólo sirve para el daño de los ataques normales: Cuando se calcula el daño, hay varios rangos posibles (Low / Mid / High). Dentro de cada rango el juego elige entre varios multiplicadores, y cuanto más alto sea el Luck del atacante, más probabilidad tiene de salir el multiplicador más alto. En enemigos influye en la probabilidad de hacer un ataque "Slammed" (el crítico/especial de los enemigos). En Bow incluso pasa una cosa curiosa, se utiliza el luck del enemigo para alterar la precisión del habilidad de Shot / Snipe. Es decir, en vez de mirar la suerte de Bow, el juego mira la suerte del que va a recibir el disparo: A más suerte, menor será la precisión de Bow con su habilidad.
    • STA (Stamina) sirve para mitigar el daño físico en los combates.
    • DEF (Defensa) es la defensa TOTAL, contando stamina y los valores defensivos de tu equipamiento (armadura/casco/escudo).
    • De la misma forma, STR (fuerza) es el daño físico de tu personaje, pero OFF (ataque) es la suma de STR con el daño físico que ejerce tu arma.
    • Antes he dicho que el turno de quién ataca primero es de la agilidad: Mentí como un bellaco. Realmente este valor depende de VIGOR y se calcula con "Agilidad + Bonos de chamán - Peso total de equipamiento" y está restringido para tener valor mínimo 0 y máximo 511. En la práctica, VIGOR atrapa valores entre el 0 y el 511 y los personajes con más agilidad suelen ser los personajes con más vigor. En caso de empate (mismo punto de vigor), el orden se decide por la posición de la party del personaje (el personaje en posición 1 ataca antes que el de posición 2, el 2 antes que el 3 y el 3 antes que el 4). La fórmula varía para los enemigos: Agilidad + número al azar entre el 0 y el 7. En caso de empate de Vigor entre un personaje del jugador y un enemigos, siempre atacan antes los personajes de los jugadores.
    • VIGOR, OFF y DEF son los únicos stats que tienen valores entre 0 y 511. El resto de campos ocupan un sólo byte (de 0 a 255). 
    • Los ítems de ataque y curación son realmente "magias", pero con otro nombre. Herb en el fondo es Cure 1, por ejemplo, pero de un sólo uso, sin quitar puntos de AP y sin ser afectados por WIS.
    • Existen 4 tipos de estrategia: Scramble (ataque), Normal, Defense y Parallel... y a pesar de sus nombres, los modificadores que aportan son bastante engañosos. Por ejemplo, "Normal" penaliza bastante a casi toda la party para que el líder sea la estrella y Parallel no aplica modificadores idénticos a todos los miembros del grupo de vanguardia o a todos los miembros del grupo de defensa:
      • Scramble añade un 20% de ataque en las posiciones 1 y 2, pero también reciben un 20% más de daño. La posición 4 recibe una penalización del 5% en ataque, pero también recibe un 10% menos de daño. La posición 3 no recibe modificadores.
      • La normal aplica un bonificador de 30% de ataque en la posición 1, pero recibe también un 30% más de daño. En la posición 2 recibe una penalización del 5% de ataque, pero recibe una bonificación del 10% en defensa. La posición 3 recibe una penalización del 10% de ataque, pero una bonificación del 15% de defensa. La posición 4 recibe una penalización del 25% de ataque, pero también una bonificación del 25% de defensa.
      • Defense aplica penalizaciones del 17% de ataque en las posiciones 2 y 3, pero a su vez las bonifica un 20% en defensa, mientras que en la posición 4 recibe una penalización del 25% de ataque y una bonificación del 25% en defensa. La posición 1 no recibe modificadores.
      • Por último, Parallel, aplica un bonificador del 20% en ataque y una penalización del 20% de defensa; la posición 2 no recibe modificadores; la posición 3 recibe una penalización del 10% de ataque y una bonificación del 15% de defensa y la posición 4 recibe una penalización del 17% de ataque y una bonificación del 20% de defensa.
      • Sea cual sea la táctica, la probabilidad de recibir un ataque debería ser:
        • Posición 1: 50%
        • Posición 2: 31%
        • Posición 3: 12%
        • Posición 4: 6% 
      • Con estos números parece que Normal es peor que Scramble, pero nada más lejos de la realidad, si calculamos cálculos de ratio de modificadores ofensivos/defensivos, el que tiene mejor ratio sería Normal (1,026), Parallel (1,021), Defense (1,018) y por último Scramble (1,012). Pero claro, como todos los personajes tienen stats distintas, el ratio da absolutamente igual y lo que te importa es poder usar una estrategia que sea acorde a los puntos fuertes y débiles de tu parte. Por ejemplo, en una táctica donde manejamos a Ryu, Katt y Rand, conviene usar Scramble.

 

    Flipando, ¿verdad? Es increíble la cantidad de detalles que no salen explicados en el manual de instrucciones. Tenemos ya las reglas definidas y ahora nos haría falta fabricar los assets:

Si nos fijamos en la escena, nos haría falta:

  • Modelar o dibujar el escenario de combate. En este caso es el despacho de Augus.
  • Dibujar una interfaz (las ventanas).
    • La ventana principal de combate donde vemos los miembros de nuestra party, sus puntos de vida, de magia, una barra de vida su nivel.
    • Una popup con la selección de comandos.
    • Una popup con los diálogos que salen durante el combate. 
  • Modelar o dibujar a los personajes:
    • Augus.
    • Katt.
    • Ryu.
    • Rand.
  • Dibujar las partículas de las magias y habilidades:
    • Cure 1 y Cure 2.
    • CurePsn.
    • Gust.
    • Dare.
    • Wake.
  • Música del combate, música de victoria y efectos de sonido.

     Ahora analicemos las reglas de gestión a implementar para un combate, que básicamente es enumerar todo lo que he explicado arriba, pero con reglas claras y concisas. Ahora bien, me ha salido un documento gigantesco de casi 3.000 líneas que tendréis disponible en mi GIT (https://github.com/LeHamsterRuso/BOF2LikeGodot/blob/main/Conception/Functional-Specification.md), pero digamos que estos son los puntos más importantes a destacar:

Las entidades

    Tenemos dos, Personaje (jugable) y Enemigo, ambas bastante parecidas:

 Character {

  ID, Name, Level
  HP, MaxHP
  AP, MaxAP
  Str, Stmna, Agi, bWis, bLuck, Guts          // básicos
  Off, Def, Vigor, Wis, Luck                  // derivados
  Condition (0-255) → visual: Ill/Poor/OK/Good/Super
  ElementalResistances[0..3]
  Equipment
  FormationPosition (1-4)
  StatusEffects[]
  Skills[]
  CanCounter : bool

}

 Enemy {

  ID, Name
  HP, MaxHP
  AP, MaxAP
  Off, Def, Agi, Wis, Luck
  MagicSusceptibility (Ms)   // -3 a +4
  ElementType
  StatusImmunities
  Actions[]
  AI
  Rewards (Exp, Zenny, Items)

}

- Básicos: Str, Stmna, Agi, bWis, bLuck, Guts 

- Derivados:

  • Off = [Str + poder de arma + bonos chamán + equipo] ×2 si Pwr.Up o Rotting (máx. 511) 
  • Def = [Stmna + poder armadura/escudo/casco + bonos] ÷2 si Def.Dn / ×1.2 si Def.Up (máx. 511) 
  • Vigor = [Agi + bonos chamán + equipo] (máx. 511) − peso de armadura ÷2 si Agi.Dn 
  • Wis = [bWis + bonos chamán + equipo] ×2 si Wis.Up 
  • Luck = [bLuck + bonos de equipo] 


Flujo de combate:

  • Fase 0 – Inicio de ronda
    • Actualizar Condition de todos los miembros del party.
    • Procesar status de fin de ronda anterior (Rotting → Zombie, etc.).
    • Comprobar quién puede actuar.   
  • Fase 1 – Selección de acciones del party
    • El jugador elige acción y objetivo para cada personaje que pueda actuar.
    • Se puede cambiar formación en cualquier turno con Swch (actualiza Risk Levels inmediatamente).
  • Fase 2 – Selección de acciones enemigas
    •  Cada enemigo ejecuta su AI y guarda su acción + objetivo.
  • Fase 3 – Cálculo del orden de ejecución
    • Calcular Vigor de todos los combatientes vivos.
      • Si "humano": [Agi + bonos chamán + equipo] (máx. 511) − peso de armadura ÷2 si Agi.Dn.
      • Si "enemigo":
        • BOSS: Agi ÷2 si Agi.Dn.
        • Por defecto: (Agi + rand(0, 7)) ÷2 si Agi.Dn.
    • Ordenar de mayor a menor Vigor.
    • Aplicar reglas de empate: Party pos. 1 → 2 → 3 → 4 → Enemigos.
  • Fase 4 – Resolución de acciones (Turno A → Turno B)
    • FOR each Action in TurnOrder:
      • IF Actor no puede actuar (p. ej. acaba de morir) → CONTINUE
      • ValidateAction()
      • ExecuteAction()
      • ApplyDamage / Healing / Status
      • Mostrar números y animaciones
      • // Eventos inmediatos entre Turno A y Turno B
      • CheckDeath()
      • CheckCounterAttack() // solo ataques físicos enemigos
      • CheckWakeUpFromSleep()
      • CheckBattleEnd()
      • Actualizar objetivos de acciones pendientes
  • Fase 5 – Fin de ronda
    • Si el combate continúa → volver a Fase 0. 

Acción de defenderse

// 1. El personaje renuncia a su acción en esta ronda
No realiza ningún ataque ni hechizo este turno.

// 2. Durante toda la ronda, el daño que reciba se reduce según el tipo:

Si el daño es físico (normal o especial físico):
  Damage = Damage × uno de los siguientes valores (aleatorio):
    { 0.85 , <0.75 , 0.65 , <0.50 }

Si el daño es mágico o especial defendible:
  Damage = Damage × uno de los siguientes valores (aleatorio):
    { 0.95 , 0.85 , 0.75 , 0.65 }
  (Cuanto más alto sea el Wis del defensor, más probabilidad tiene de obtener el multiplicador más bajo)

// 3. Casos en los que Defense NO tiene efecto:
Si el ataque está marcado como “indefendible” (Undefendable):
  Damage no se modifica.

 

Ataque físico de la party:

Pwr = Off_atacante - Def_objetivo
si Pwr < 0 → Pwr = 0

si Pwr < 63:     Damage = LowRangeTable(Pwr, Luck)
si 63 ≤ Pwr < 99: Damage = Pwr × {0.85, 0.90, 0.97, 1.03}   (Luck)
si Pwr ≥ 99:     Damage = Pwr × {0.58, 0.64, 0.70, 0.76}   (Luck)

si puede ser Special y éxito → Damage ×= 2

si objetivo usa Defense → Damage ×= DefendPhysical()   // {0.85, <0.75, 0.65, <0.50}

Damage ×= attack_modifier(riesgo atacante)
Damage ×= defend_modifier(riesgo defensor)

Aplicar elementos
si Pwr.Dn → Damage ×= 7/8

Damage = MAX(1, MIN(999, floor(Damage)))

 

Ataque físico de los enemigos:

Pwr = Off_atacante - Def_objetivo
si Pwr < 0 → Pwr = 0

si Pwr < 63:     Damage = LowRangeTable(Pwr, Luck)
si 63 ≤ Pwr < 99: Damage = Pwr × {0.85, 0.90, 0.97, 1.03}   (Luck)
si Pwr ≥ 99:     Damage = Pwr × {0.58, 0.64, 0.70, 0.76}   (Luck)

// Ajuste exclusivo de enemigos
Damage = Damage + (Luck_enemigo × {1/8, 2/8, 3/8, 4/8}) + Random(0,1)


si puede ser Special y éxito → Damage ×= 2

si objetivo usa Defense → Damage ×= DefendPhysical()   // {0.85, <0.75, 0.65, <0.50}

// Los enemigos siempre tienen Risk = 0, por lo que no aplican attack_modifier
Damage ×= defend_modifier(riesgo del defensor)


Aplicar resistencias elementales (si el ataque tiene elemento)

si el atacante tiene Pwr.Dn → Damage ×= 7/8

Damage = MAX(1, MIN(999, floor(Damage)))
 

Magia

// 1. Cálculo del daño base (igual para todos)
BaseDamage = SpellPower + Random(0..7)

// 2. Si el objetivo es un ENEMIGO:
Damage = BaseDamage × (1.0 + Ms × 0.04)

  Donde Ms (Magic Susceptibility) del enemigo puede ser:
  -3 → ×0.88
  -2 → ×0.92
  -1 → ×0.96
   0 → ×1.00
  +1 → ×1.04
  +2 → ×1.08
  +3 → ×1.12
  +4 → ×1.16

// 3. Si el objetivo es un PERSONAJE del party:
Damage = BaseDamage
(No se aplica el modificador de Ms)

// 4. Well Cast (solo cuando el lanzador es del party)
Si el hechizo es de daño y se produce Well Cast:
  Damage = Damage × 1.5

// 5. Aplicar elementos
Si el objetivo es enemigo → aplicar debilidades/resistencias del tipo del enemigo
Si el objetivo es del party → aplicar resistencias elementales del equipamiento

// 6. Defensa
Si el objetivo está usando Defense y el hechizo es defendible:
  Damage = Damage × uno de {0.95, 0.85, 0.75, 0.65}

// 7. Límite final
Damage = MAX(1, MIN(999, floor(Damage)))

 

Estados alterados principales

  • Poison: No daña en combate, pero te coloreaba morado. Restaba 1 HP por paso en el mapa.
  • Sleep: El personaje no actúa. Se despierta con daño físico
  • Rotting: Tras 1 ronda completa → Zombie
  • Zombie: Puede atacar aliados. Al final del combate → Dead
  • Pwr.Up: Off ×2 (máx 511)
  • Pwr.Dn: Daño físico × 7/8
  • Def.Up: Def ×1.20 (máx 511)
  • Def.Dn: Def ÷2.
  • Agi.Dn: Vigor ÷2 
  • Agi.Up: Estaba bugeado en SNES (no hacía nada), pero lo corrigieron en GBA. Aplica Vigor x2 (máx 511).
  • Hushed: Impide magia
  • Curse: Fuerza Condition a ILL 

 

Los modelos 3D

   Y con todos estos ingredientes me puse a trabajar en los gráficos. Ahora bien, no me calenté la cabeza, hice con Blender el escenario 3D y delegué en Meshy.AI la generación de los personajes (entrada NO patrocinada). Respecto al escenario, aunque lo modelé yo, también delegué en Meshy para su texturización.

    Usar IA para generar modelos 3D tiene sus contras. Además del debate moral sobre saber de dónde han sacado los datos para entrenar los modelos, está el tema de la optimización. Como podréis observar cada personaje tiene unos 15.000 polígonos, lo cual sería inviable si hiceramos un JRPG de verdad (con sus aldeas y sus NPC paseando y comprando), pero en nuestro contexto es viable teniendo en cuenta que la escena sólo tendrá 4 personajes y un escenario simple. Otro punto importante es el RIG, como podréis observar en la demo, éste hace agua para cualquiera que sepa modelar en 3D.

    Además está el tema de la comodidad: A mí me gusta modelar en 3D, es una de mis aficiones, pero cada modelo me puede llevar varias semanas. Hacer una demo con 4 personajes que apenas me dará visibilidad, que sólo la modelación me puede llevar meses y que encima no puedo comercializar, pues fue una de las motivaciones por las que me hico apostar por Meshy.AI (repito, entrada NO patrocinada).






Las meoldías

   Para la música he tirado de Suno, utilizando prompts como:

- "classic SNES rock, 16-bit boss battle, fast shredding guitar riff intro, heavy palm-muted power chords, layered guitar harmonies, tight drums, groovy bassline, raw SNES sound chip, energetic and thrusting, short and repetitive, instrumental" para el tema principal;

- "16-bit SNES RPG victory fanfare, upbeat and cheerful, triumphant brass and synth melody, playful sound, short looping jingle, energetic and rewarding, retro SNES samples, light and joyful, instrumental, Treasures Won vibe" para la música de victoria; 

- "16-bit SNES RPG game over theme, somber and melancholic, sad piano and soft strings, emotional and defeated atmosphere, slow tempo, retro sound, poignant melody, instrumental, Give Me a Chance vibe" para el Game Over.


La demo

   Y con todos estos elementos, os presento cómo han quedado ambas versiones:

16 junio 2025

¿Aprendiendo a programar en Godot 4 .NET? Ejemplos sencillos (capítulo 1)

    Esta entrada tiene como objetivo enseñar a utilizar Godot 4 para .NET a través de una serie de ejemplos sencillos. El motivo de esta publicación es que mi entorno de desarrollo integrado (IDE) para la programación de videojuegos preferido y me gustaría fomentar su uso. Y bueno, siendo sinceros, existen numerosos aspectos que abordaré en esta serie de entradas que habría deseado que me fueran explicados en su día.

     En este primer capítulo me centraré únicamente en tres sencillos ejercicios: un “Hola Mundo”, un “Hola Mundo” en tres dimensiones con texto giratorio (la cámara va rotando alrededor del texto) y un sistema básico de novela visual (donde codificaremos unas sencillas librerías DLL para separar la solución por capas). A lo largo del capítulo se explicarán paso a paso las acciones a realizar, pero como habréis notado por el resumen de dichos ejercicios, la dificultad será incremental. No obstante, la solución a los ejercicios se encuentra disponible en mi perfil de GitHub, por si fuera necesario consultarlo para resolver dudas (https://github.com/LeHamsterRuso/Godot4.NetExamples).

    Por cierto, aunque veáis que las capturas de pantalla de esta entrada corresponden a la versión de Mac de Godot 4, que sepáis que esta guía sirve también tanto para Windows, como Linux.

 

Material necesario:

  

Primer ejemplo, el "Hola Mundo".

Objetivos:

  1. Mostrar un texto en pantalla.
  2. El texto debe de estar centrado.
  3. La ventana debe de ejecutarse maximizada.
  4. El tamaño del texto y su posición debe de adaptarse al tamaño de la pantalla.

Pasos: 

Abre Godot .NET y en la lista de proyectos clica en "+ Crear" (arriba a la izquierda).


 

    En la popup que se abrirá clica en "Examinar" para indicar dónde quieres guardar tu proyecto y ponle un nombre obvio, del estilo "HelloWorld", en rederizador selecciona el modo "compatibilidad", deja el resto de opcioens activadas por defecto y clica en "Crear".


 

     Verás la ventana siguiente:

    A la izquierda, en la pestaña de escena, selecciona "Escena 2D":

 

     Al hacerlo, la vista central cambiará (veremos una especie de canvas 2D en vez de un espacio euclidiano en 3D):


    En la pestaña de Escena, a la izquierda, haz clic derecho sobre el objeto Node2D. Se abrirá una lista desplegable. En esa lista, selecciona la opción "+ Añadir Nodo Hijo...". 


    Se te abrirá una popup de creación de nodos. En la barra de buscar teclea "label" para que se valla filtrando la lista de componentes seleccionables. Selecciona "Label" y dale a "Crear".

 

    Te saldrá algo parecido a esto:


     Con tu componente "Label" seleccionado en la pestaña de "Escena" (a la izquierda), gira tu vista al inspector de propiedades que sale a la derecha. Este inspector te permite, básicamente, alterar las propiedades y atributos del componente que tengas seleccionado en ese instante. Verás que por defecto la primera propiedad propuesta para un label es el "Text" y que éste está vacío. Haz clic en la propiedad y teclea "Hello World" (se trata de un campo de texto editable).

    Acto seguido, en la barra de filtrado de propiedades, busca la palabra "size", despliega el apartado "Theme Overrides" y el subapartado "Font Sizes". En Font Sizes, marca la opción y asigna el valor 32.

 

    Tras cambiar el tamaño de la fuente notarás que el texto pasa a ser legible en el editor (área central de la ventana), pero por desgracia este no está centrado.


     En el editor de escenas, selecciona el botón de modo movimiento. Esto nos permitirá desplazar componentes "a ojo".


     Verás que a nuestro label le han salido dos flechas, una roja y una verde. Clica en él y arrastra el objeto hasta el centro del rectángulo (que por cierto sirve para demarcar el alto y ancho de la pantalla). Da igual si no queda perfectamente centrado, sólo busco enseñarte que el modo movimiento existe.


   Dale al botón de "Play" que hay arriba a la derecha.

 

    La primera vez que ejecutes un proyecto Godot te preguntará cual es su escena principal. Dale "Seleccionar actual" y guarda la escena en curso con un nombre lógico (por ejemplo "HelloWorld.tscn").


     Si lo has hecho bien deberías de ver una ventana que dice "Hello World".

 

    Ahora vamos a hacer que quede más profesional. Cierra la ventana, haz clic derecho en el objeto Node2D, dale a "+ Añadir Nodo Hijo...", busca el componente llamado "CanvasLayer" y añádelo. Haciendo "drag&drop", arrastra el objeto "Label" dentro de tu nuevo "CanvasLayer". La arborescencia de la escena debería de quedar así: Node2D > CanvasLayer > Label.

    Selecciona el label, clica en en el icono de ajustes de anclaje (en el centro) y en la mini popup que se abrirá pulsa en "completo" (un cuadrado blanco).


 

    Verás que ahora ahora tu label ocupa todo el área de la pantalla, pero por desgracia no está centrado. Ve al inspector (a la derecha), borra la barra de filtros (habíamos escrito en ella "size") y selecciona "Center" como valor de las propiedades "Horizontal Alignment" y "Vertical Alignment". Verás que ahora nuestro texto sí que sale perfectamente centrado en la ventana.

 

    Ahora vamos a hacer que nuestro ejemplo ocupe toda la pantalla y no una simple ventana. Accede a "Proyecto> Configuración del Proyecto".

 

    Se te abrirá una nueva popup con toda la configuración de tu proyecto. En la pestaña de "General", selecciona "Visualización > Ventana" y en ella selecciona el modo "Maximized" en la sección de "Tamaño" y "Canvas_items" en el de "Estirar". El "Maximized" nos permite que la ventana arranque maximizada por defecto, mientras que el "Canvas_items" nos permite que todo lo que esté en el canvas conserve sus proporciones cada vez que la ventana sea redimensionada.

 


    Haz clic en "Cerrar" y dale otra vez al botón de "Play". Deberías de ver ahora tu "Hola Mundo" en una ventana que ocupa la totalidad de la ventana. Cabe destacar que podríamos haber seleccionado el modo "FullScreen" en vez del "Maximized" para poder ver el ejemplo a pantalla completa, pero por defecto esto te esconde la barra de la ventana y si no tienes maña con los ordenadores esto suele dar problemas para cerrar la aplicación.
 

 

 

Segundo ejemplo, el "Hola Mundo" en 3D.

Objetivos:

  1. Mostrar un texto 3D en pantalla.
  2. La cámara debe de girar alrededor del texto.
  3. Manejo de assets.
  4. Creación de scripts.
  5. Vínculo de componentes entre el IDE y los scripts.
  6. Introducción al método _Process.

 

    Ahora que hemos entrado en harina iré dando las directrices más rápido. Si tienes maña con Blender 3D puedes crear un objeto 3D de tipo "Text" que diga "Hello World", aplicarle un modificador de "Solidify", ponerle un material de textura negra o gris y exportarlo como objeto fbx o glTF. En la práctica los objetos glTF me dan menos problemas. No obstante, también puedes directamente descargar mi asset 3D desde GitHub (https://github.com/LeHamsterRuso/Godot4.NetExamples/blob/master/002---helloworld-3d/Assets/3D/HelloWorld.glb).


     Una vez tengas tu asset 3D o hayas conseguido el mío, crea un nuevo proyecto y llámalo HelloWorld3D. En la pestaña de "Escena", selecciona "Escena 3D" y en el navegador de recursos (abajo a la izquierda) crea una carpeta "Assets", una subcarpeta "3D" y dentro mete el objeto 3D que has creado o que te has descargado.

 


 

     Haciendo uso de "drag&drop", selecciona el fichero "HelloWorld.glb" y arrástralo dentro de la escena.


     Selecciona el objeto "Node3D" en la pestaña de escena y añádele un objeto WorldEnvironment. Después selecciónalo y en el inspector (a la derecha) créale un nuevo "Environment". Despliega "Background", selecciona en "Mode" el valor "Custom Color" y asigna algún color claro.


     Ahora añade una cámara 3D ("Camera 3D") a tu Node3D y haz uso de las flechas direccionales para centrar el texto (mueve la cámara, no el texto). Si te fijas, en el inspector de la cámara, puedes ver qué es lo que se está viendo a través de ella.



 

     Dale a "Play", deberías de ver tu "Hello World" en 3D.


    Ahora vamos a hacer que la cámara gire al rededor del texto. En tu navegador de recursos crea una carpeta llamada Scripts, haz clic derecho en ella y crea un nuevo script. Te saldrá una nueva popup, en ella selecciona el lenguaje "C#" (muy importante, por defecto te selecciona el "GDScript"), llámalo CameraMovement.cs y haz clic en "Crear".


     Ábrelo con tu editor de código preferido (recomiendo VSCode) y edítalo para que quede así:

 

     Si te fijas en el código, el "Export" espera que el editor le pase un objeto que será por el cual tiene que rotar nuestra cámara. Al mismo tiempo, el método "_Process" se disparará en cada frame, indicandónos en la variable "delta" el lapso de tiempo que ha pasado entre frames.

      Guarda el fichero, vuelve a Godot y dale a la llave inglesa (en la versión de .NET es necesario compilar antes de poder asignar parámetros a nuestro script). Después, selecciona el script que hemos creado y arrástralo a la cámara. Notarás que a nuestra cámara le saldrá un nuevo icono con forma de pergamino (esto nos indica que el objeto tiene ligado un script).



 

     Selecciona la cámara, ve al inspector y en la sección de "CameraMovement" selecciona "HelloWorld" como "Target".



     Dale al botón de "Play", ahora tendrías que ver el texto de "Hello World" girando.



 

Tercer ejemplo, una novela visual.

Objetivos:

  1. Carga dinámica de recursos en escena.
  2. Creación de librerías DLL. 
  3. Uso de .gdignore. 
  4. Reemplazo de tipo de componentes. 
  5. Separación por capas (front, back, datos).
  6. Introducción a los métodos _Ready e _Input.

 

    Crea un nuevo proyecto y llámalo VisualNovelWithDll (por ejemplo). En la pestaña de "Escenas" selecciona una escena en 2D y añade al Node2D un CanvasLayer. A ese CanvasLayer, añádele a su vez un un TextureRect (con ajuste de anclaje "completo", para que ocupe todo el área) y dos labels (uno para el nombre del personaje y otro para su diálogo). Edita las propiedades de las labels para cambiar el tamaño de sus fuentes (por ejemplo 24 para el nombre el NPC y 32 para el diálogo) y juega con los ajustes de anclaje para dejar el layout a tu gusto. Edita también la propiedad "Expand mode" del TextureRect a "Fit Width Proportional", para que las imágenes que le carguemos luego pueden ser re-escaladas de forma proporcional.


     En el navegador de recursos, crea una carpeta "Assets" y dentro de ella otra llamada "Backgrounds". En ella pondremos tres imágenes, una donde veamos un escenario vacío, otro con un primer plano de un personaje feliz y otro donde el mismo personaje se vea triste. Deben de llamarse respectivamente: 001.png, 002.png y 003.png. Igual que antes, si no puedes dibujarlos, puedes recuperarlos de mi GitHub para hacer el ejercicio (https://github.com/LeHamsterRuso/Godot4.NetExamples/tree/master/003---visualnovelwithdll/Assets/Backgrounds). Crea también una carpeta Scripts y añade en ella un nuevo script de tipo C# llamado "Dialogs.cs". La arborescencia del proyecto debería de quedarte así:


 

     Abre VSCode, accede al explorador (el icono de los dos documentos a la izquierda y clica en "Crear proyecto de .NET".

 

 

    En la ventanita que se te abrirá empieza a teclear "biblioteca" y selecciona "Biclioteca de clases". Acto seguido selecciona un subdirectorio de tu proyecto Godot donde guardarla, asígnale el nombre DLL y selecciona "crear un nuevo proyecto".


     Una vez creado, en la raíz de la carpeta dll crea un fichero vacío llamado ".gdignore". Esto hará que el editor de Godot ignore todos los ficheros que añadas en tu carpeta de DLL.


    En nuestra DLL, crearemos una clase llamada "Dialog.cs" que contendrá únicamente 3 strings: Uno para definir el fondo de imagen, otro para definir el nombre de quién habla y otro para mostrar el texto que dirá.


 

     También crearemos una clase principal que se llamará Game, de naturaleza estática, y que contendrá una lista de diálogos (para hacerla puedes renombrar la clase Class1 que se crea por defecto):


     Si os fijáis en el código de la clase, esta dll realmente carga los datos a mostrar y gestiona la lógica de navegación entre diálogos. Ahorra cierra el VSCode, vuelve a Godot y abre el script de Dialogs.cs que habíamos creado. Verás que la arborescencia del explorador del VSCode ha cambiado y que ahora te ha abierto directamente la carpeta donde tienes todo el proyecto de Godot. Ahí deberías de identificar dos ficheros "csproj": El fichero principal del juego y el de la DLL que acabamos de crear. Edita el primero y haciendo uso de las primitivas "ItemGroup" y "ProjectReference", añade manualmente una dependencia de la DLL en el proyecto principal. Debería de quedarte algo así, en función de cómo hayas montado la arborescencia:

   De hecho, si os fijas cómo VSCode ha creado el proyecto DLL, la organización queda un poco fea (con un subdirectorio DLL donde metemos las clases. Podemos jugar con la arborescencia a nuestro gusto, siempre que actualicemos los "csproj" en consecuencia y que manejemos con coherencia los namespaces de nuestras clases.

    En esa captura he desplazado el contenido DLL/DLL en DDL/ y he actualizado el csproj raíz para apuntar a la ruta actualizada del csproj de la DLL.

     Para verificar que no hemos roto nada, puedes hacer uso de la terminal de VSCode para compilar la solución y la DLL a través del comando "dotnet build":


 

    Bueno, ya falta poco. Ahora ya puedes usar tu DLL en el proyecto de Godot... De hecho, como la DLL sale referenciada en el fichero csproj raíz, para hacer uso de sus clases nos bastará con utilizar el "using DLL" en la cabecera de nuestros scripts.

    Ahora abre el fichero "Scripts> Dialogs.cs" que habíamos creado antes, dentro de Godot, y edítalo para recuperar los tres componentes que hemos empleado en nuestra escena (un texturerect y dos labels) y alimentarlos en función de la lógica que hemos implementado en nuestra DLL. Para hacer eso tenemos dos métodos de Godot que nos serán útiles: El método _Ready, que se lanza al arrancar una escena, y el método _Input, que se lanza cuando se detecta alguna interacción por teclado, ratón o gamepad.

    De hecho, lo que haremos será crear una función FillScreen que se encargará de alimentar los labels y el texture rect y llamaremos a ésta desde los métodos _Ready (para acceder al primer diálogo) y _Input (para ir avanzando en los diálogos).


    Vuelve a Godot, dale al icono de la llave inglesa para compilar, arrastra el script al nodo raíz y asocia los dos labels y el texture rect al script:



 

     Dale a "Play", deberías de ver la siguiente escena en bucle (al ir al último diálogo volvemos al primero):





 

    Volvamos a nuestro script y aplica los siguientes cambios:


     Ahora si te fijas el comportamiento cambia, el texto se va mostrando letra a letra, como en una novela visual comercial:



 

    Ahora vamos a aplicar un fondo básico a nuestro texto. Para ello duplica los dos labels que tenemos (selecciona uno, cópialo en el portapapeles, ve al nodo raíz y pégalo... y haz lo mismo con el otro). Acto seguido selecciona una de las copias y haciendo clic derecho cambia su tipo a "ColorRect". Esto nos permite, de forma simple, clonar las coordenadas y dimensiones del componente original.




     Ahora, a través del inspector, asigna a tus dos ColorRect un color azulado con un valor de Alpha (letra A) cercano al 80, según tu gusto. El "alpha" es la capa de transparencia del componente, un valor cercano al 0 lo convierte en invisible y el valor 255 (el máximo) lo convierte en opaco. Un valor entre el 0 y el 255 significa que nuestro componente será más o menos transparente (el nivel de transparencia depende de si está más cercano al valor 0 o al 255).

 

    Acto seguido desplaza los dos labels dentro de sus respectivos ColorRects: Esto nos permitirá que el texto se vea por encima del fondo. En caso contrario, el texto nos saldría azulado, debido a que la capa de transparencia se le estaría aplicando por encima.

 

     Dale a play, ahora los textos deberían de verse con nuestro fondo básico:


 

    Ahora vamos a reorganizar la DLL para separar la lógica del modelado de datos. La idea será que nuestra DLL pase a llamarse "Core" y crear una segunda DLL que llamaremos "Data". Esto nos permitirá, por ejemplo, separar el motor del modelo de datos, haciendo que el día de mañana sea más fácil migrar nuestro sistema a una base de datos o a un sistema de carga por archivos (json, csv, etc).

     Para ello, vamos a duplicar la carpeta DLL (copiar+pegar en el explorador de ficheros), renombraremos el original a Core, renombraremos la copia a Data, tocaremos los tres csproj para corregir la nueva arborescencia, borraremos el Dialog.cs del proyecto Core, borraremos Game.cs de Data y actualizaremos los namespaces de ambos proyectos. Como Core dependerá de Data, tendremos también que referenciarlo en su csproj y hacer uso de "Using Data;" en la clase Game.cs.








 

      Y ahora, para acabar con la entrada, nos queda sólo sanear la clase Game.cs de la dll "Core". Si nos fijamos, en ella hacemos una iniciación de datos mockeados, lo ideal sería llevarnos esa iniciación en la librería "Data", puesto que en el futuro queremos recuperar datos a través de algún tipo de datasource, hacia un fichero físico o una base de datos.

     Para facilitar esta tarea de migración, atentos a la jugada, nos crearemos una clase estática Mock dentro de la DLL "Data", que contendrá por ahora nuestro guion. 

 
 
     No obstante, nuestra clase Game no accederá directamente al Mock, si no a través de una clase intermedia llamada Data, que hará uso del mock como si fuera un "data source" (fichero fuente). Así, el día de mañana, cuando tengamos nuestros dto y esquemas bien montados, bastará con apuntar a otra clase estática (bastaría con cambiar el tipo Mock a otro).
 




     Por último compila y verifica que no hayamos roto nada. Espero que hayas disfrutado con esta entrada. ¡Hasta la próxima!