19 años en Internet

13 enero 2026

¿Aprendiendo a programar para MS-DOS con DIV2 Games Studio? ¡Hagámos un JRPG! (capítulo 2)

    ¡Hola a todos los entusiastas de la programación retro! Hoy vamos a sumergirnos de nuevo en el fascinante mundo de DIV Games Studio, un motor de juegos para MS-DOS de 1998 que nos permite crear videojuegos de la época con relativa facilidad. En esta entrada, crearemos un JRPG simple que demuestra conceptos fundamentales como el manejo de procesos, gráficos primitivos, tiles, NPCs y diálogos. El objetivo es enseñar los principios básicos de programación en DIV, ideal para principiantes que quieran dar sus primeros pasos en desarrollo de juegos. Aunque bueno, a lo largo de esta entrada veremos conceptos bastante avanzados.


¿Qué es DIV Games Studio?

    DIV Games Studio es un entorno de desarrollo gratuito para crear juegos en MS-DOS. Utiliza un lenguaje similar al PASCAL, con énfasis en procesos (como hilos ligeros) para manejar lógica de juego, gráficos y audio. Es perfecto para prototipos rápidos y aprendizaje, ya que no requiere compiladores complejos. Si te pica el gusanillo, recuerda que este es el segundo capítulo que dedico a este lenguaje de programación. El primero lo tienes en el siguiente enlace: https://www.elgeneralfailure.com/2025/06/aprendiendo-programar-para-ms-dos-con.html

 

Estructura General del Código

    El primer ejemplo que veremos es el JRPG_01.PRG (disponible en mi github: https://github.com/LeHamsterRuso/DIV2Examples/blob/main/PRG/JRPG_01.PRG). Si analizamos el código, veremos que este programa crea un mundo tileado (dividido en casillas) donde el jugador controla un personaje circular, interactúa con NPCs y navega por terrenos variados. Vamos a desglosarlo sección por sección.

¿Por qué un juego donde la única salida por pantalla son primitivas gráficas y texto? Pues para recordaros un poco los tiempos de las priemras Atari, donde rara vez los juegos venían con sprites.


1. Configuración inicial y estructuras globales

Al inicio, definimos constantes y estructuras globales:

  • Constantes y Variables Globales: MAX_NPC limita los NPCs. Variables como offset_x y offset_y manejan el desplazamiento de la cámara.
  • Structs: Son como clases simples, pero sin lógica. Puedes verlo como la definición de una clase, pero sin implementación lógica (no tienen métodos, ni constructor):
    • Controls asigna teclas que utilizaremos en el juego;
    • Map almacena los tiles (celdas) del mapa; Se trata de una matriz que representa un tablero.
    • DialogState gestiona el estado de diálogos;
    • NPCData guarda info de NPCs (posicionamiento en los ejes x e y, sus diálogos, étc.);
    • Colors define paletas (bosques, ciudades, pasto, agua, montañas...);
    • ZLevels controla capas de dibujo;
    • ScreenConfig configura resolución y FPS.
  • Inicio del Programa: set_mode() establece la resolución (320x240), set_fps() fija 30 FPS. Luego inicializamos controles, mapa y NPCs, y lanzamos procesos. 

  

    Las variables globales permiten comunicación entre procesos, mientras que el uso de structs es para organizar la capa de datos. Puedes ver los structs como clases de objetos, pero sin implementación (no poseen métodos ni constructores).

 

2. Inicialización de datos

Tres procesos inicializan datos:

  • InitNPCData(): Rellena NPCData con nombres, diálogos, posiciones y colores.
  • InitMap(): Crea un mapa tileado (32x24 tiles de 32x32 píxeles). Asigna colores basados en posiciones: campos verdes, montañas marrones, ríos azules, etc. Incluye caminos y ciudades.
  • InitControls(): Asigna constantes de teclado (_LEFT, _RIGHT, etc.).
 
 

 

    Como ya habréis intuído, DIV no es un lenguaje orientado a objetos, por lo que las estructuras que definimos no tienen un constructor, ni métodos. Esto hace que tengamos que crear procesos de inicialización, para que estos actúen como constructores de clase para dichas estructuras.

    En cuanto al uso de bucles FROM para rellenar los distintos arrays de las estructuras, ten en cuenta que, a diferencia de los bucles como FOR o WHILE, los bucles FROM no son condicionales y el valor final a alcanzar debe de ser una constante. Puedes emplear en un FROM una operación con una constante (por ejemplo "MAX_NPC - 1"), pero no una variable dinámica que cambie de valor en tiempo de ejecución. De ser el caso, el compilador te remontará un error.

    Respecto al tamaño del mapa, ¿por qué celdas de 32x32 píxeles? ¿Por qué no de 16x16 o de 8x8? Pues se trata de una decisión de diseño debido a que DIV cuenta con una limitación y es que nos deja sólo manejar al mismo tiempo un total de 256 primitivas gráficas (1 byte). Y, aunque tengamos 768 tiles (32x24), gracias al algoritmo implementando (si un tale está fuera de la pantalla, no lo dibujo), a las dimensiones de las tiles (32x32) y al tamaño de la resolución de pantalla (320x240), no nos encontraremos con esa limitación de sobrepasar las 256 primitivas simultáneas en pantalla.

    Por cierto, el uso de primitivas gráficas está muy mal optimizado en DIV y DIV2, debido a que las rutinas empleadas son las suyas propias y tienen un costo alto (parece que dibujan las geometrías pixel a pixel, lo que tiene un costo muy elevado en comparación con cargar un sprite y hacer uso de él), especialmente si se usan transparencias. En consecuencia este ejemplo debería de ir terriblemente lento en hardware real de la época, pero si tenéis un DoxBox configurado con cycles = MAX, el juego debería de iros a 30 frames por segundo (velocidad máxima que he definido con el set_fps).


3. Proceso de dibujo del mapa: map_drawer()

Este proceso renderiza el mapa visible:

  • Bucle Principal: En cada frame, borra dibujos antiguos con delete_draw() y cuenta y almacena IDs en draw_ids[].
  • Cálculo de Visibilidad: Determina qué tiles (celdas) mostrar basándose en offset_x/y, simulando un escenario con scroll.
  • Dibujo: Usa draw(3, color, 15, 0, x1, y1, x2, y2) para rectángulos rellenos y hacemos uso de draw_z con la estructura ZLevels para establecer la capa de profundidad.


     Claramente este bucle está poco optimizado, puesto que hacemos limpieza y redibujado de la pantalla en cada frame. Para distinguir los distintos niveles de profundidad (mapa, npc o jugador), utilizamos la variable global de draw_z. No obstante, el manejo de draw_z es bastante problemático en DIV. Realmente no existe un z-buffer para las funciones de primitivas en la que puedas indicar "esto va detrás", "esto va en primer plano", como sí que existe en los sprites. Draw_z es una variable global, que afecta a TODAS las primitivas gráficas a la vez y es importante que, si juegas con esta variable, su valor modificado esté muy cerca en código al draw que dibuja el objeto, puesto que, al no contar con mutex (semáforos), en caso de que el procesador priorice otro proceso, puedes encontrarte con cambios no deseados de profundidad (del estilo dibujar el mapa encima del jugador). Este problema se hace especialmente notorio en máquinas potenters, que procesan un mayor número de frames por segundo.

    El principal problema que hay con draw_z es que, al ser una variable global del propio framework, en el momento que todos los procesos ejecutan el comando FRAME, encargado de dibujar todo un fotograma completo en pantalla, todas las primitivas que vayan a dibujarse por pantalla tendrán realmente la misma profundidad. Para bypasear este problema la única solución seria que hay es forzar una impresión puramente secuencial, forzando a dibujar primero por lógica el mapeado y después los personajes (cosa que no hago en este ejemplo), lo cual entra en conflicto con la idea de usar procesos independientes para cada cosa. Por suerte este problema no lo tenemos con los sprites o imágenes de fondo. 

    Si os fijáis, este tema del z-buffer con primitivas no es algo tan raro, en juegos de Atari, por ejemplo, era bastante común tener efectos de parpadeos de personajes si esto se implementaba mal (del estilo asegurarse a dibujar el personaje en un fotograma distinto al escenario), o incluso en la primera PlayStation el manejo de los juegos 3D daban problemas notorios de perspectiva, al no saberse interpretar correctamente qué texturas tenían que sobreponerse sobre otras.


4. Proceso del jugador: player()

Maneja movimiento, colisiones y interacciones:

  • Movimiento: Ajusta velocidad por terreno (bosques/montañas ralentizan) y evita agua con checks de colisión.
  • Cámara: offset_x/y centran la vista en el jugador.
  • Dibujo: Dibuja un círculo con draw(5, 15, 15, 0, ...).
  • Interacción: Calcula la distancia a NPCs; presiona ENTER para dialogar con el más cercano.


    En player capturamos los controles y verificamos si la casilla de destino es una montaña o un bosque para relentizar la velocidad del jugador y de paso evitamos que se pueda cruzar por una casilla de agua. En lo que concierne a la rutina para calcular cual es el NPC más cercano, aunque DIV tiene funciones para calcular la distancia entre dos procesos (distancia = get_dist(<código identificador>) o distancia = fget_dist(x, y, id2.x, id2.y)), hago uso de una simple función algebraica basada en el Teorema de Pitágoras para obtenerla (distancia = sqrt((player_x - NPCData[i].x) * (player_x - NPCData[i].x) + (player_y - NPCData[i].y) * (player_y - NPCData[i].y)).


5. Proceso de NPCs: npc(id_char)

Renderiza los NPC como círculos:

  • Visibilidad: Solo dibuja si el NPC está en pantalla.
  • Dibujo: Similar al jugador, con draw_z = ZLevels.npc.


6. Sistema de diálogos: dialogue(name, text)

Abre una ventana de texto:

  • Animación: Construye una cadena de texto con el diálogo del NPC juntando letra a letra.
  • Input: ENTER para auto-completar el texto y/o cerrar el diálogo.
  • Dibujo: Uso de draw() para el fondo y write() para texto.



 

Ejemplo 2 

    Con todo esto ya tendríamos algo funcional: Un personaje que se pasea por un mapa y que habla con distintos NPC. Este código demuestra lo esencial de DIV: Procesos concurrentes, dibujo primitivo y manejo de estado. Pero a nivel jugable, poco podemos hacer y, para extenderlo y hacerlo más disfrutare,  podríamos agregar un inventario y más mapas... Y ahí surge el segundo ejemplo que os traigo hoy, el "JRPG_02.PRG" (https://github.com/LeHamsterRuso/DIV2Examples/blob/main/PRG/JRPG_02.PRG).

     En esta segundo ejemplo, nos en una versión optimizada que introduce mejoras significativas. Veremos cómo estas optimizaciones no solo mejoran el rendimiento y la jugabilidad, sino que también enseñan conceptos más avanzados de programación en DIV.

      JRPG_01.PRG era funcional, pero tenía limitaciones: redibujaba el mapa entero cada frame (causando flickering), usaba procesos separados para NPCs (ineficiente, imaginad un mapa con 100 npc), y carecía de mapas múltiples o inventario. Esta nueva resuelve estos problemas con optimizaciones que reducen el overhead de CPU y añaden más funcionalidades. Vamos a desglosar las mejoras clave:


1. Dirty Rectangles Approach para redibujar el mapa sólo si es necesario

El Dirty Rectangles Approach (enfoque de rectángulos sucios) es una técnica de optimización en programación gráfica 2D diseñada para reducir la carga de procesamiento al evitar el redibujado innecesario de toda la pantalla, siguiendo a su vez la premisa del "lazy evaluation" (no hagas trabajo innecesario).

En la función de map_drawer, el problema con nuestro enfoque inicial era que el proceso redibujaba todo el mapa visible cada frame, incluso si la cámara no se movía. Esto consumía recursos innecesariamente. Para aplicar esta estrategia, hago uso de  variables globales para rastrear cambios de estado y redibujamos el mapa sólo cuando cambian los valores de offset_x, offset_y o current_map:


2. Renderizado consolidado en un solo proceso

En el código inicial teníamos el renderizado de cada proceso por separado (map_drawer, npc y player), lo cual suele ser lo normal en los juegos que programamos con DIV, pero trae dos problemas principales en nuestro diseño: Al usar sólo primitivas gráficas en lugar de sprites, podemos sufrir de flickering y superposiciones mal gestionadas por estar todo en la misma coordenada del z-buffer; Además, trae problemas de escalabilidad, puesto que cuantos más NPC pongamos en el mapa, más llamadas ralentizaremos nuestro juego (imaginaros 100 procesos NPC compitiendo con el map_drawer o el proceso del player).

Con esta nueva versión todo el dibujo se consolida en map_drawer y eliminamos el proceso de NPC al ser redundante (ahora entendéis por qué usaba el teorema de Pitágoras para obtener el NPC más cercano al jugador).


3. Múltiples mapas y transiciones suaves

En esta nueva versión tenemos tres mapas: Un mamamundi de 32x24 tiles y dos ciudades de 8x8. Para gestionar esto transformamos el struct MapData en un array y creamos una variable global "current_map" que nos indica en qué mapa nos encontramos.


Por ahora las áreas de teletransporte (si entro en X voy al mapa Y) las defino por código hardcode y también filtro así qué NPC están en cada mapa, pero la idea es que en el código final (el del siguiente ejemplo) veáis un enfoque mejor estructurado (definiendo estas áreas y filtros directamente en las estructuras de datos). Otra cosa que me gusta señalar, es el uso de fade_on y fade_off para forzar fundidos de pantalla cuando cambiamos de mapa.


4. Sistema de inventario

    En esta nueva versión disponemos de un inventario de hasta 10 objetos. Estos objetos básicamente son un array de 10 strings (cadenas de texto). El sistema tiene mucho margen de mejora y, por ejemplo, podemos crear un struct objeto y meterle un integer de cuantía, pero no quiero adelantar acontecimientos del próximo ejemplo.

  Los strings en DIV son unidimensionales, por lo que no podemos crear un array de strings de forma implícita, ni siquiera forzando la definición de una matriz (array de arrays), pero podemos crear un array de una estructura que contenga únicamente un string para poder crear un falso array de strings:

    La gestión de strings en div requieren una mención especial, puesto que cuando hacemos una definición del estilo "string cadena[256]" no estás creando un array de 256 strings, si no un string de 256 caracteres (la talla del string). Además, cabe mencionar que el operando "=" copia por referencia en los strings, es decir, la asignación "a = b" hace que las cadenas a y b apunten a la misma región de memoria:

    string a = "123";

    string b = "456";

    a = b;

    b[0] = "5";  // -> a y b valen ahora "556".

    Esto suele ser un quebradero de cabeza para cualquiera que no esté acostumbrado a trabajar con punteros, por lo que si quieres copiar el contenido de un string sin que ambas variables apunten a la misma región de memoria, lo ideal es usar la función strcpy (uso -> strcpy(cadena_destino, cadena_origen).

    Esto por suerte sólo pasa con las cadenas de texto, el reto de tipo de datos de DIV suelen copiarse por valor y no por referencia. Y bueno, por ahora la única forma de obtener objetos es aceptando el ofrecimiento de un NPC que se encuentra en el mapamundi, pero la idea es que al final podamos recogerlos del suelo, comprarlos o obtenerlos de cofres del tesoro. Además, si pulsamos la tecla "escape" veremos un inventario con la lista de objetos que tenemos acumulados.


 


6. Música de fondo

    En este ejemplo no he añadido aún efectos de sonido, pero si os fijáis se hace uso de load_Song() y play_song() para ejecutar una de las canciones que vienen con la instalación de DIV2. La función de "load_song()" soporta formatos los MOD, S3M y XM. Estos formatos pertenecen a la familia de los archivos de módulos (Tracker music) y, a diferencia de un MP3 que es una grabación de audio digital, estos archivos funcionan como una partitura digital con sus propios instrumentos incluidos. Un archivo de módulo no contiene una onda de sonido continua (como un wav, un pcm, un mp3 o un ogg), si no que contiene "samples" de sonido (una nota de piano, un golpe de batería, etcétera) y un secuenciador (una lista de instrucciones que indica qué nota tocar, en qué momento, con qué efecto (volumen, vibración) y en qué canal). Estos ficheros ocupan muy poco espacio (KB o pocos megas) manteniendo una calidad de sonido alta, ya que el audio se genera en tiempo real.



7. Diálogos interactivos con recompensas

    Otra de las adaptaciones de este ejemplo es la inclusión de dialogos básicos con elecciones de "Sí/No" que además permiten obsequiar con objetos. Para ello hago uso de flags de estados y extiendo las estructuras de diálogos y NPC. 



Ejemplo 3

     ¿Y qué más podemos hacer para mejorar nuestro juego? Bueno, en la descripción del ejemplo anterior vimos varias posibles mejoras:

  • Definir los NPC que salen en cada mapa dentro de la estructura de mapas.
  • Definir los teletransportes (si estoy en la casilla X -> voy al mapa Y) dentro de la estructura de mapas.
  • Adaptar el sistema de inventario para permitir cuantías (en vez de ver el mismo ítem en varias líneas) y además obtener una descripción.


    Y dicho y hecho, el ejemplo 3 que encontraréis en mi github (https://github.com/LeHamsterRuso/DIV2Examples/blob/main/PRG/JRPG_03.PRG) realiza justamente todas esas adaptaciones en el código: 




Ejemplo 4

     ¿Y qué más podemos hacer para mejorar nuestro juego? Bueno, en vez de definir los mapas, los teletransportes y los NPC en hardcode dentro del código, podemos aprovechar que lo tenemos ya todo definido en una única estructura de datos (MapData) para guardar toda esa lógica en un fichero externo (imaginaros un futuro donde creamos un editor de niveles) y crear funciones en nuestro código para leer dicho fichero.

      Con esto en mente he creado este cuarto ejemplo (https://github.com/LeHamsterRuso/DIV2Examples/blob/main/PRG/JRPG_04.PRG), que a nivel funcional es idéntico al tercer ejemplo, pero que carga los mapas y los ítems a través de ficheros ".dat" que genero con un programa que he creado únicamente para ese fin (https://github.com/LeHamsterRuso/DIV2Examples/blob/main/PRG/MAPGEN.PRG). ¿Por qué es necesario crear un programa para crear el fichero ".dat"? Bueno, podríamos crear nuestro propio tipo de fichero en texto plano, modificarlo con un editor de texto y adaptar el código de nuestro juego para interpretarlo, pero lo fácil es coger una estructura ya existente y grabarlo "tal cual" para recuperarlo. El problema es que cuando escribimos una estructura de memoria al disco, estamos volcando código en binario, imposible de manipular con editores de texto.

     El hecho de guardar estructuras y objetos en formato binario dentro de un fichero no es algo raro y se hace a día de hoy (de hecho, así funciona la mayoría de los famosos ficheros "save" de los juegos comerciales). A este proceso se le conoce como serialización y desserialización de un objeto. La diferencia es que aquí, en vez de hacer un "save" de nuestra partida, lo hacemos sobre la configuración de los mapas, NPC y objetos.

 


Ejemplo 5

     ¿Y qué más podemos hacer para mejorar nuestro juego? Bueno, ¡podemos emplear sprites en vez de primitivas y añadirle animaciones a nuestro personaje.

      Sobre la estrategia a subir manejando sprites, hay que tener en cuenta que  DIV y DIV 2 trabajaban en modos VGA, que permiten mostrar en pantalla un máximo de 256 colores simultáneos, algo que en frameworks actuales de desarrollo de videojuegos se queda muy corto. Esto significa que, antes de empezar a crear un fichero FPG y añadirle a porrón todos los sprites que queramos, tenemos que pensar en cómo gestionaremos los colores de nuestra paleta.

     Por ejemplo, podemos hacer que la mitad de los colores (128) sean para dibujar los escenarios, es decir, reservarlo para los tiles. Una forma de optimizar al máximo esa limitación de colores, sería crear un sistema de diferentes ficheros FPG por nivel, es decir, hacer uso de un fichero FPG de tiles por mapa (uno para el mapamundi y otros dos para cada ciudad), cada uno de ellos con sus propios 128 colores. No obstante, por comodidad, en este ejemplo utilizaré una única paleta de colores, pero simplemente que sepáis que es algo que podéis abordar para vuestros juegos.

     Con los otros 128 colores restantes, debemos reservar uno (el de la posición 0) para el color transparente y podemos reservar otros 15 para las fuente del texto y los fondos (diálogos y menú). El resto de colores (112) los destinaríamos a 7 los personajes (1 jugador y 6 NPC), lo que nos daría un total de 16 colores por personaje. Para crear las tres paletas, podemos hacer uso de herramientas como GIMP, que nos permiten indexar un máximo de colores por imagen (Imagen> Modo> Indexado) y hacer uso de las distintas soluciones que ofrece el entorno DIV. En cuanto a los formatos, DIV y DIV2 tienen problemas para interpretar la versión actual de BMP, es incompatible con PNG y el formato JPG degrada calidad... Por lo que lo ideal es exportar los ficheros a PCX una vez ya hayas hecho en él todas las conversiones específicas (escalado e indexado).

Imagen original en HD (1920x1080) 

Me gusta activar el dithering (difuminado): Mezcla patrones de píxeles para sombrear.
 

Imagen modificada a 114x64 y 16 colores. 

     También tenemos que pensar en cómo organizar los personajes: Quiero tener dos estados, uno quieto y otro andando. El único personaje que puede andar, ahora mismo, es nuestro personaje, pero estaría bien dejar preparada la lógica para permitir andar también a los NPC en el futuro. Además, cada estado se representa gráficamente en cuatro direcciones, dependiendo de dónde esté mirando nuestro personaje (norte/sur/oeste/este, aunque cuando programamos juegos preferimos arriba/abajo/izquierda/derecha). Además, podemos hacer que cada personaje tenga su propio fichero FPG y que los códigos id de cara estado/dirección sean los mismos para cada personaje. Esto nos permite tener un código más modular y mantenible.

     Para mi solución, los personajes quietos tendrán gráficos estáticos, pero quiero que cuando anden tengan una animación de 6 sprites en bucle.  


     Con estas premisas, podemos organizar los id de los FPG de personajes de la siguiente manera:

  • 1: Idle mirando arriba -> NPC y jugador.
  • 2: Idle, mirando derecha -> NPC y jugador.
  • 3: Idle, mirando abajo -> NPC y jugador.
  • 4: Idle, mirando izquierda -> NPC y jugador.
  • 5-10: Andar, mirando arriba -> Sólo jugador por ahora.
  • 11-16: Andar, mirando derecha -> Sólo jugador por ahora.
  • 17-22: Andar, mirando abajo -> Sólo jugador por ahora.
  • 23-28: Andar, mirando izquierda -> Sólo jugador por ahora.

     ¿Por qué ese orden de arriba, derecha, abajo izquierda? Por nada en especial, por seguir simplemente el sentido horario. Otra cosa a tener en cuenta es que DIV puede espejar sprites, por lo que podemos realmente usar el mismo gráfico para mirar derecha/izquierda o andar derecha/izquierda (por eso he subrayado esas líneas). La decisión de espejar los gráficos o no, debería de depender únicamente del diseño de tus personajes. En mi caso prefiero tener gráficos separados de derecha/izquierda por si implemento personajes asimétricos (por ejemplo con una manga de cada color, o cojo o manco de una mano)... Si nuestro personaje es diestro y maneja una espada, difícilmente lo representaremos correctamente con gráficos espejados (dependiendo de dónde mire, sería diestro o zurdo).

     Una vez aclarado esto, os presento el quinto ejemplo de hoy (https://github.com/LeHamsterRuso/DIV2Examples/blob/main/PRG/JRPG_05.PRG), el cual, para funcionar, requiere de diversos ficheros FPG donde almaceno todos los sprites que utilizaremos (https://github.com/LeHamsterRuso/DIV2Examples/tree/main/FPG/DIABLO) y ejecutar previamente una nueva versión del MAPGEN para generar los ficheros MapV2.dat e ItemsV2.dat (https://github.com/LeHamsterRuso/DIV2Examples/blob/main/PRG/MAPGEN02.PRG).

 

    El salto entre este ejemplo y el anterior no solo es visual, sino también estructural y técnico. Aquí detallo las diferencias más importantes:


1. Conversión completa a sprites y utilización de ficheros FPG:

- Los anteriores ejemplos utilizaban primitivas gráficas (draw()) para renderizar el mapa, los NPCs y el jugador.
- JRPG_05: Todo el renderizado se realiza con sprites FPG (put() y xput()), permitiendo gráficos mucho más ricos, animaciones fluidas y efectos visuales como sombras transparentes. Si os fijáis, esto no suele la programación típica de DIV Games Studio con sprites, donde por lo general hacemos procesos y cada uno tiene su propio gráfico con sus primitivas locales de x/y... Si no que utilizamos un sólo hilo para renderizarlo todo. Esto puede parecer que, en principio, aportamos una capa de complejidad a nuestro código, pero al hacer uso de estructuras donde definimos qué es qué (mapa, NPC, jugador...) realmente estamos optimizando mucho el rendimiento. Otra cosa a tener en cuenta es que, al abandonar draw en detrimento de put/xput, ya no nos hace falta almacenar id's en un array para borrarlos, puesto que el funcionamiento de estas primitivas no requiere de liberación de memoria.

 

- En las versiones anteriores los NPCs eran simples círculos de color, sin orientación ni animación.
- En este nuevo ejemplo cada NPC tiene un sprite para cada una de las 4 direcciones y el motor decide hacia dónde mira el NPC según la posición del jugador (priorizando la distancia horizontal o vertical según las distancias).


- En las versiones anteriores las cajas de diálogo y el inventario eran simples rectángulos dibujados con draw().
- En este nuevo ejemplo empleamos sprites dedicados para las cajas de diálogo, el fondo del inventario e incluso implementamos sombras transparentes en los pies del personaje y de los NPC, logrando una presentación mucho más atractiva.



2. Sistema de animación del jugador

- En las versiones anteriores el jugador era un círculo estático.
- Este nuevo ejemplo implementa un sistema de animación bastante completo basado en estados: Nuestro personaje podrá estar parado ("Idle") o andando. Ambos estados cuentan a su vez con otros cuatro subestados, dependiendo de dónde esté mirando el personaje (norte/sur/este/oeste). Por otro lado, cada una de las cuatro animaciones de andar consta a su vez de 6 cuadros (sprites) que se reparten de forma cíclica durante 30 frames (cada 5 frames, cambiamos el gráfico). El tema hacer avanzar la animación 1 vez cada 5 frames lo hago únicamente para que sea más agradable al ojo humano.


3. Arquitectura data-driven para los gráficos

- En las versiones anteriores los gráficos estaban “hardcodeados” y cualquier cambio requería recompilar el juego principal.
- En esta nueva versión los nombres de los archivos FPG se cargan dinámicamente desde los datos del mapa (MapsV2.dat), permitiendo cambiar gráficos o añadir nuevos sin tocar el código fuente del motor. Aún así ahora mismo tenemos que recompilar el MAPGEN, que es donde indicamos los ficheros correspondientes, pero el día de mañana podríamos implementar un editor para el juego que nos evite este proceso.

 


  

 

Ejemplo 6

     ¿Y qué más podemos hacer para mejorar nuestro juego? Bueno, por ahora, aunque hayamos mejorado mucho la arquitectura de nuestro juego, realmente sólo tenemos a un personaje que pasea y habla con personajes. Podemos ir un paso más y crear un sistema de quests. De hecho, el ejemplo de a continuación implementa un sistema de quests que se activa hablando con un NPC (https://github.com/LeHamsterRuso/DIV2Examples/blob/main/PRG/MAPGEN03.PRGhttps://github.com/LeHamsterRuso/DIV2Examples/blob/main/PRG/JRPG_06.PRG).

    En este ejemplo, defino un sistema de quests que tienen estado (NOT_STARTED, ACTIVE y COMPLETED) y para poder explotarlo he tenido también que adaptar el sistema de diálogos, permitiendo frases condicionales en función del estado de una quest (si ha empezado, si cumplimos las condiciones para acabarla, si no las cumplimos o si la quest ha acabado). A su vez, completar una quest lleva acciones y consecuencias, como restar ítems de tu inventario y recibir dinero. Por ahora, a nivel de estructuras, las quest están embebidas por NPC, pero es algo que podríamos externalizar en el futuro.

 


     De paso he tenido que modificar la pantalla de menú (la que vemos al pulsar ESC) para que podamos seleccionar si ver ítems o las quests aceptadas (por que sí, no sólo podemos rechazar una quest, también podemos rechazarla).






 

 

    Y para acabar con este ejemplo, también he aplicado un zoom en los diálogos (con transición) para que el juego luzca más juicy. Para ello recalculamos el tamaño y la posición de los tiles y personajes que hay en pantalla y los restablecemos sus dimensiones y coordenadas originales al finalizar el diálogo.



Ejemplo 7

     ¿Y qué más podemos hacer para mejorar nuestro juego? Bueno, todo buen JRPG de la época del MS-DOS tenía un sistema de galería de imágenes y eso es lo que he implementado en este séptimo ejemplo (https://github.com/LeHamsterRuso/DIV2Examples/blob/main/PRG/MAPGEN04.PRGhttps://github.com/LeHamsterRuso/DIV2Examples/blob/main/PRG/JRPG_07.PRG y https://github.com/LeHamsterRuso/DIV2Examples/blob/main/FPG/DIABLO/CG.FPG). Como en los anteriores ejemplos, éste trae también su versión de MAPGEN que tendréis que lanzar antes de ejecutar el JRPG_07 y además trae un fichero CG.FPG que contiene 3 imágenes que iremos desbloqueando en el juego.

    Básicamente algunos diálogos lanzarán una imagen, que a modo de evento, se verá en pantalla completa, como si se tratara de una visual novel y su implementación no es nada complicada. Básicamente en las estructuras de diálogo, además de un string para el texto añadimos un INT para indicar un ID de imagen que corresponderá a la imagen que hay que mostrar dentro del fichero CG.FPG: Y si el valor de ese ID es cero, no se mostrará ninguna imagen de fondo.

 

     Además, la partida memorizará si hemos visto ya esa escena y la podremos visualizar a través del menú del juego, en una nueva sección dedicada a las CG.


 


 

Ejemplo 8

     ¿Y qué más podemos hacer para mejorar nuestro juego? Bueno, sólo nos quedan por hacer dos cosas para dar nuestro juego por acabado: Un sistema de combates por turnos y poder guardar y cargar la partida. Además de esto, el ejemplo también mejora la gestión del audio, permitiendo cambiar la canción del fondo dependiendo del mapa y cada mapa además tendrá su propia música también para los combates (si los tiene).

     Para hacer funcionar este ejemplo os hará falta:

- Bajar y ejecutar el MAPGEN05.PRG (https://github.com/LeHamsterRuso/DIV2Examples/blob/main/PRG/MAPGEN05.PRG) para generar todos los ficheros ".dat" en formato binario.

- Descargar el MONSTERS.FPG (https://github.com/LeHamsterRuso/DIV2Examples/blob/main/FPG/DIABLO/MONSTERS.FPG).

- Descargar todos los FPG previos (https://github.com/LeHamsterRuso/DIV2Examples/tree/main/FPG/DIABLO).

- Descargar y compilar el JRPG_08.PRG (https://github.com/LeHamsterRuso/DIV2Examples/blob/main/PRG/JRPG_08.PRG). 



    La lógica de los combates es demasiado complicada como para entrar al detalle a nivel de código. De hecho, la rutina ("battle_screen") ocupa unas 800 líneas. Pero en fin, a modo de resumen, dicha rutina comienza inicializando el estado de batalla: selecciona aleatoriamente entre 1 y 3 enemigos (en este caso, slimes), asignando sus estadísticas de salud máxima y actual desde una base de datos de monstruos.Se muestra un mensaje inicial de aparición de enemigos y se cambia la música a la canción de batalla. En esta rutina se emplean conceptos básicos de programación como la inicialización de arrays y estructuras, el uso de números aleatorios con rand() y el manejo de recursos gráficos y de audio con funciones como put() y ChangeSong(). Además, se establece un temporizador para mostrar el mensaje inicial, ilustrando el control de tiempo en bucles con estimatedSeconds (función que estima los segundos en función de los fps de la structura ScreenConfig) y FRAME.

    El núcleo de la rutina es un bucle principal que maneja los turnos alternos entre el jugador y los enemigos. Durante el turno del jugador, se presenta un menú con opciones como "Atacar", "Magia", "Defender" y "Huir", usando condicionales y manejo de entrada con KEY() para navegar y seleccionar. Si se elige atacar, se activa un modo de selección de objetivo, destacando el enemigo seleccionado con cambios visuales (tamaño y transparencia). Para la magia, se muestra una lista de hechizos disponibles, permitiendo elegir uno si hay suficiente MP. Los enemigos, por su parte, atacan aleatoriamente al jugador en su turno. En esta parte se hace uso de variables de estado (como player_turn, selecting_target), bucles anidados para dibujar elementos dinámicos y cálculos de daño basados en estadísticas. También incluye animaciones de muerte para enemigos (con transparencia y escalado vía xput), enseñando sobre temporizadores y flags booleanos para controlar secuencias complejas.

     La rutina verifica continuamente las condiciones de victoria (todos los enemigos derrotados) o derrota (salud del jugador en cero), actualizando el estado del juego en consecuencia. En caso de victoria, se otorgan recompensas como oro e ítems, actualizando el inventario del jugador mediante búsquedas en arrays. Se muestra un mensaje de victoria con temporizador antes de restaurar la música normal y salir del combate. Si el jugador huye o muere, se maneja la derrota de manera similar. Finalmente, se limpian todos los textos y gráficos dibujados, restableciendo flags como in_battle para volver al mapa principal. Esta parte resalta la importancia de la gestión de estado global y el manejo de inventarios con operaciones de array (como agregar ítems).
 
    En cuanto al sistema de guardado, veréis por el código que no hay mucho misterio, es aplicar el mismo mecanismo de escritura de los ficheros "dat" de los MAPGEN, pero llevado a un fichero "save".
 

 
    Y así queda nuestro 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.