19 años en Internet

04 febrero 2026

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

 


    En esta tercera entrega de la serie de programación de DIV, continuaremos con el enfoque establecido en la entrada anterior, partiendo de un prototipo básico y progresando gradualmente hacia la creación de un juego atractivo.... y en este caso tenemos incluso un juego COMPLETO (sí, en mayúsculas). Este capítulo incorpora el feedback recibido de la entrada anterior y se centra en mejorar la legibilidad del código, asegurando que los cambios entre las distintas versiones del ejercicio sean fácilmente identificables y además esta vez lo he escrito en castellano. Aún así prepararos un bocadillo y un café bien cargado, que los ejemplos finales no son sencillos.

    Si eres nuevo, o no estás familiarizado con Div Games Studio, se trata de un entorno de programación de videojuegos para MS-DOS lanzado en 1998. Este lenguaje de programación es procedural, no orientado a objetos, y ofrece numerosas características atractivas para su época, tales como el uso de estructuras, punteros, reserva dinámica de memoria (con malloc/free) y una curva de aprendizaje relativamente sencilla. De hecho, mucha gente aprendió a programar con este lenguaje en España y se hizo tan popular a finales de los 90 que incluso llegaron a comercializarse revistas comerciales dedicados a este framework.

    Uno de los aspectos más destacados del feedback recibido de la entrada anterior fueron las dudas sobre la utilidad de las estructuras, cómo diferenciar distintos tipos de enemigos o la eficiencia de implementar un proceso unificado para el dibujo, en lugar de permitir que cada instancia dibuje su propio gráfico. Esta reflexión me llevó a considerar el desarrollo de un juego de naves, similar a Space Invaders o Galaga, puesto que son elementos comunes en este tipo de juegos, donde debemos de dibujar grandes tipos de enemigos, de distintos tipos y evitando que el juego se ralentice.

    Así que empecé a trabajar en un programa llamado "vshooter" de "Vertical Shooter", en honor al género de este tipo de juegos de marcianitos. Como siempre, recordad que todos estos ejemplos de código los tengo subidos en mi Github (https://github.com/LeHamsterRuso/DIV2Examples/blob/main/PRG/VSHOOT01.PRG). 

Y vamos con el primer ejemplo:

//------------------------------------------------------------------------------
// TÍTULO: VSHOOT01.PRG - Nave Jugador Simple
// AUTOR: Sebastian J. MONCHO MAQUET
// FECHA: 18/01/2026
// DESCRIPCIÓN: Versión simplificada con solo la nave del jugador y controles.
// NOTA: Usa las flechas para mover la nave, espacio para disparar y ESC para salir.
// Requiere la paleta DIV2.PAL en el mismo directorio.
// Código y comentarios en castellano para facilitar la comprensión (por petición).
//------------------------------------------------------------------------------

PROGRAM vshooter;
// Programa principal
BEGIN
// Arrancamos el juego a 320x200 a 60 FPS con la paleta cargada
set_mode(m320x200); // 320x200
set_fps(60, 0);
load_pal("DIV2.PAL");

// Creamos la nave del jugador
jugador();

// Bucle principal del juego
LOOP
FRAME;
END
END

// Proceso: jugador
// Descripción: Controla la nave del jugador.
// - Maneja el dibujo, movimientos y disparos en un bucle continuo.
PROCESS jugador();
PRIVATE
vel = 3; // Velocidad de movimiento
tiempo_disparo = 0; // Contador para controlar la frecuencia de disparos

// El dibujo de la nave se compone de varias partes
nave_circulo; // ID del dibujo del círculo de la nave
nave_linea1; // ID del dibujo de la primera línea de la nave
nave_linea2; // ID del dibujo de la segunda línea de la nave
nave_linea3; // ID del dibujo de la tercera línea de la nave
BEGIN
// Posición inicial de la nave en el centro inferior de la pantalla
x = 160; // Centro horizontal
y = 170; // Cerca del fondo

// Bucle principal del jugador
LOOP
// Dibujamos la nave del jugador
// Borrar los dibujos anteriores
IF (nave_circulo != 0) delete_draw(nave_circulo); END
IF (nave_linea1 != 0) delete_draw(nave_linea1); END
IF (nave_linea2 != 0) delete_draw(nave_linea2); END
IF (nave_linea3 != 0) delete_draw(nave_linea3); END

// Dibujar la nave
nave_circulo = draw(5, 31, 15, 0, x - 8, y - 8, x + 8, y + 8);
nave_linea1 = draw(1, 31, 15, 0, x - 6, y + 8, x, y - 10);
nave_linea2 = draw(1, 31, 15, 0, x + 6, y + 8, x, y - 10);
nave_linea3 = draw(1, 31, 15, 0, x - 6, y + 8, x + 6, y + 8);

// Movimiento de la nave
IF (key(_left) AND x > 10) x -= vel; END
IF (key(_right) AND x < 310) x += vel; END
IF (key(_up) AND y > 10) y -= vel; END
IF (key(_down) AND y < 190) y += vel; END

// Disparar
tiempo_disparo++;
IF (key(_space) AND tiempo_disparo > 5)
// Crear bala
bala_jugador(x, y - 15);
tiempo_disparo = 0;
END

// Salir con ESC
IF (key(_esc))
EXIT("Gracias por jugar.", 0);
END

FRAME; // Esperar al siguiente frame
END
END

// Proceso: bala_jugador
// Descripción: Controla el movimiento de una bala disparada por el jugador.
// - Se mueve hacia arriba y se elimina si sale de la pantalla.
PROCESS bala_jugador(x, y);
PRIVATE
bala_draw; // ID del dibujo de la bala
BEGIN
LOOP
// Borramos dibujo anterior si existe
IF (bala_draw != 0) delete_draw(bala_draw); END

// Dibujamos la bala
bala_draw = draw(5, 10, 15, 0, x - 3, y - 3, x + 3, y + 3);

// Movemos la bala hacia arriba
y -= 5;

// Eliminamos la bala si sale de la pantalla
IF (y < -10) RETURN; END

FRAME; // Esperar al siguiente frame
END
END


     Este ejemplo que os he puesto es terriblemente sencillo: Manejamos una especie de botijo azul, que se desplaza pulsando los cursores (las teclas de arriba/abajo/izquierda/derecha), dispara balas redondas pulsando espacio y salimos del juego pulsando escape. Ya está, no tiene nada más, tienes un proceso de jugador, un proceso de bala_jugador, ambos se dibujan en pantalla con primitivas gráficas (ni siquiera sprites) y hacemos uso de una variable de "tiempo_disparo" para controlar que las balas se disparen por ráfagas.


     Cosas chulas que no suelen verse: Si os fijáis en la bala_jugador, tenemos un bucle infinito (LOOP) que paramos con un RETURN. Aquí no actúa como "BREAK" del bucle, si no que indica que la ejecución del proceso acaba ahí. Sencillamente, cuando quieras forzar que una instancia deje de "vivir", la puedes "matar" con un return.

     Ahora vamos a añadir enemigos. De hecho, vamos a añadir 2, uno de cada tipo y tendrán patrones de movimiento distinto. Uno se desplazará de derecha a izquierda y de izquierda a derecha y cada vez que llegue a un extremo de la pantalla bajará ligeramente. El otro se moverá haciendo ligeros círculos, casi imperceptibles (https://github.com/LeHamsterRuso/DIV2Examples/blob/main/PRG/VSHOOT02.PRG).

    Para ello añadiremos una instancia de cada tipo de enemigo en el main: 

PROGRAM vshooter;
// Programa principal
BEGIN
// Arrancamos el juego a 320x200 a 60 FPS con la paleta cargada
set_mode(m320x200); // 320x200
set_fps(60, 0);
load_pal("DIV2.PAL");

// Creamos la nave del jugador
jugador();

// Creamos 2 enemigos: uno tipo 1 y uno tipo 2
enemigo_tipo1(50, 30);
enemigo_tipo2(200, 40);

// Bucle principal del juego
LOOP
FRAME;
END
END


    E implementamos ambos enemigos:

// Proceso: enemigo_tipo1
// Descripción: Controla el comportamiento de un enemigo tipo 1.
// - Se mueve horizontalmente, dispara balas y verifica si muere o llega abajo.
// Parámetros:
// - x: Posición inicial x.
// - y: Posición inicial y.
PROCESS enemigo_tipo1(x, y);
PRIVATE
dir = 1; // Dirección inicial
tiempo = 0;
enemigo_box; // ID del dibujo de la caja
enemigo_circulo; // ID del dibujo del círculo
BEGIN
LOOP
// Dibujar enemigo
dibujar_enemigo_tipo1(&enemigo_box, &enemigo_circulo, x, y);

// Mover enemigo
mover_enemigo_tipo1(&x, &y, &dir);

// Salir si llega abajo
IF (y > 210) RETURN; END

FRAME;
END
END


// Proceso: enemigo_tipo2
// Descripción: Controla el comportamiento de un enemigo tipo 2.
// - Se mueve en círculos, dispara balas dirigidas y verifica si muere.
// Parámetros:
// - x: Posición inicial x.
// - y: Posición inicial y.
PROCESS enemigo_tipo2(x, y);
PRIVATE
ang = 0; // Ángulo inicial
tiempo = 0;
enemigo_linea1; // IDs de dibujos
enemigo_linea2;
enemigo_linea3;
enemigo_linea4;
enemigo_circulo;
BEGIN
LOOP
// Dibujar enemigo
dibujar_enemigo_tipo2(&enemigo_linea1, &enemigo_linea2, &enemigo_linea3, &enemigo_linea4, &enemigo_circulo, x, y);

// Mover enemigo
mover_enemigo_tipo2(&x, &y, &ang);

FRAME;
END
END


     Si os fijáis, a nivel de implementación son muy parecidos. Tienen sus parámetros y verificaciones propias, pero a su vez ambos tienen una estructura parecida, donde llamamos a un proceso de dibujo y controlamos su movimiento:

// Función: dibujar_enemigo_tipo1
// Descripción: Dibuja el enemigo tipo 1, borrando los dibujos anteriores.
// Parámetros:
// - box_offset: Puntero al ID de la caja. -> Usamos el puntero para modificar la variable original.
// - circulo_offset: Puntero al ID del círculo. -> Usamos el puntero para modificar la variable original.
// - x: Posición x.
// - y: Posición y.
// Los punteros sirven para modificar las variables originales fuera de la función.
// - Si modificas una varariable del puntero dentro de la función, se refleja fuera también.
// - Sirve para evitar el uso de variables globales innecesarias.
// Los PROCESS se ejecutan en paralelo, mientras que las FUNCTION se ejecutan de forma secuencial
// - Es decir, hasta que no termine esta función, el proceso que lo llama queda bloqueado
FUNCTION dibujar_enemigo_tipo1(INT box_offset, INT circulo_offset, INT x, INT y);
BEGIN
// Borramos los dibujos del frame anterior
// Controlamos que existe ya una box y un círculo (ID != 0) antes de borrarlos
// Si no lo comprobamos, tendríamos una excepción en el primer FRAME y el juego se colgaría
IF (*box_offset != 0) delete_draw(*box_offset); END
IF (*circulo_offset != 0) delete_draw(*circulo_offset); END

// Dibujamos el enemigo
*box_offset = draw(3, 114, 15, 0, x - 8, y - 8, x + 8, y + 8);
*circulo_offset = draw(5, 4, 15, 0, x - 3, y - 3, x + 3, y + 3);
END

// Función: mover_enemigo_tipo1
// Descripción: Maneja el movimiento horizontal del enemigo tipo 1, cambiando dirección al llegar a los bordes y descendiendo.
// Parámetros:
// - x_offset: Puntero a la posición x. -> Usamos el puntero para modificar la variable original.
// - y_offset: Puntero a la posición y. -> Usamos el puntero para modificar la variable original.
// - dir_offset: Puntero a la dirección (1 o -1). -> Usamos el puntero para modificar la variable original.
// Los punteros sirven para modificar las variables originales fuera de la función.
// - Si modificas una varariable del puntero dentro de la función, se refleja fuera también.
// - Sirve para evitar el uso de variables globales innecesarias.
// Los PROCESS se ejecutan en paralelo, mientras que las FUNCTION se ejecutan de forma secuencial
// - Es decir, hasta que no termine esta función, el proceso que lo llama queda bloqueado
FUNCTION mover_enemigo_tipo1(INT x_offset, INT y_offset, INT dir_offset);
BEGIN
*x_offset += *dir_offset * 2; // Movimiento horizontal

// Cambiar dirección al llegar a los bordes
IF (*x_offset < 10 OR *x_offset > 310)
*dir_offset = -*dir_offset;
*y_offset += 5; // Descenso
END
END


// Función: dibujar_enemigo_tipo2
// Descripción: Dibuja el enemigo tipo 2, borrando los dibujos anteriores.
// Parámetros:
// - linea1_offset: Puntero al ID de la línea 1. -> Usamos el puntero para modificar la variable original.
// - linea2_offset: Puntero al ID de la línea 2. -> Usamos el puntero para modificar la variable original.
// - linea3_offset: Puntero al ID de la línea 3. -> Usamos el puntero para modificar la variable original.
// - linea4_offset: Puntero al ID de la línea 4. -> Usamos el puntero para modificar la variable original.
// - circulo_offset: Puntero al ID del círculo. -> Usamos el puntero para modificar la variable original.
// - x: Posición x.
// - y: Posición y.
// Los punteros sirven para modificar las variables originales fuera de la función.
// - Si modificas una varariable del puntero dentro de la función, se refleja fuera también.
// - Sirve para evitar el uso de variables globales innecesarias.
// Los PROCESS se ejecutan en paralelo, mientras que las FUNCTION se ejecutan de forma secuencial
// - Es decir, hasta que no termine esta función, el proceso que lo llama queda bloqueado
FUNCTION dibujar_enemigo_tipo2(INT linea1_offset, INT linea2_offset, INT linea3_offset, INT linea4_offset, INT circulo_offset, INT x, INT y);
BEGIN
// Borramos los dibujos anteriores
IF (*linea1_offset != 0) delete_draw(*linea1_offset); END
IF (*linea2_offset != 0) delete_draw(*linea2_offset); END
IF (*linea3_offset != 0) delete_draw(*linea3_offset); END
IF (*linea4_offset != 0) delete_draw(*linea4_offset); END
IF (*circulo_offset != 0) delete_draw(*circulo_offset); END

// Dibujamos el enemigo
*linea1_offset = draw(1, 9, 15, 0, x, y - 8, x + 8, y);
*linea2_offset = draw(1, 9, 15, 0, x + 8, y, x, y + 8);
*linea3_offset = draw(1, 9, 15, 0, x, y + 8, x - 8, y);
*linea4_offset = draw(1, 9, 15, 0, x - 8, y, x, y - 8);
*circulo_offset = draw(5, 9, 15, 0, x - 2, y - 2, x + 2, y + 2);
END

// Función: mover_enemigo_tipo2
// Descripción: Maneja el movimiento circular del enemigo tipo 2, manteniéndolo dentro de la pantalla.
// Parámetros:
// - x_offset: Puntero a la posición x. -> Usamos el puntero para modificar la variable original.
// - y_offset: Puntero a la posición y. -> Usamos el puntero para modificar la variable original.
// - ang_offset: Puntero al ángulo de movimiento. -> Usamos el puntero para modificar la variable original.
// Los punteros sirven para modificar las variables originales fuera de la función.
// - Si modificas una varariable del puntero dentro de la función, se refleja fuera también.
// - Sirve para evitar el uso de variables globales innecesarias.
// Los PROCESS se ejecutan en paralelo, mientras que las FUNCTION se ejecutan de forma secuencial
// - Es decir, hasta que no termine esta función, el proceso que lo llama queda bloqueado
FUNCTION mover_enemigo_tipo2(INT x_offset, INT y_offset, INT ang_offset);
BEGIN
*ang_offset += 5000; // Incremento del ángulo

// Movimiento circular
*x_offset += get_distx(*ang_offset / 1000, 1);
*y_offset += get_disty(*ang_offset / 1000, 1);

// Mantener dentro de la pantalla
IF (*x_offset < 10) *x_offset = 10; END
IF (*x_offset > 310) *x_offset = 310; END
IF (*y_offset < 10) *y_offset = 10; END
IF (*y_offset > 190) *y_offset = 190; END
END


Cosas chulas que no se suelen ver:

- Dividir el objeto, el comportamiento y su dibujado en procesos distintos. Esto suele hacerse para para reducir la complejidad del código, haciendo que sea más sencillo de leer (no es lo mismo un proceso gigante que tres pequeños) y de mantener.

- Hago uso de punteros tanto para el dibujado como para el movimiento de los enemigos. ¿Qué es un puntero? Digamos que tanto la variable "x" de enemigo, como la variable "x_offset" de mover_enemigo, comparten la misma región de memoria. O dicho de otra forma, si modificas una de esas dos variables, estás modificando realmente ambas variables a la vez. Esto es muy útil si quieres que varios procesos "comuniquen" entre ellos en tiempo real, sin tener que recurrir a variables globales.

¿Se puede hacer todo eso por valor en vez de punteros? Sí. ¿Estoy complicando el código innecesariamente? También. Pero pensad que ahora mismo sólo tenemos dos enemigos en pantalla. Mañana tendremos 160.


      Bueno, pues ahora que ya tenemos un jugador, balas y enemigos, podemos plantearnos ya hacer un juego: Pongamos más enemigos en pantalla, que disparen con patrones distintos, explosiones y que el jugador y los enemigos mueran al colisionar con las balas. Atentos, que se vienen curvas (https://github.com/LeHamsterRuso/DIV2Examples/blob/main/PRG/VSHOOT03.PRG).

       En este ejemplo (el tercero), si hacéis un diff con el anterior veréis que hay muchísimos cambios, en gran parte porque, además de añadir todo lo que os voy a describir, he remplazado muchos "magic numbers" por constantes. Y también me lié y cambié de lugar varias funciones. ¿Qué es un "magic number"? Pues ni más ni menos que todos los valores "hardcodeados" que veis por el código. Está claro que si quieres centrar un proceso en una pantalla de 320x200, puedes directamente decir que su posición X es 160 y su posición Y es 100, pero en programación los llamamos números "mágicos" porque sacados de contexto no significan nada. Imagínate dentro de 6 meses releyendo tu código: "¿Por qué puse aquí 80? ¿Por que sí y punto?". Pues esos números mágicos o cadenas mágicas (para el caso de los strings) se corrigen poniendo constantes. La constante nos permite aplicar un nombre que le otorga un contexto en lectura y que, cuando se compila, no se trata como si fuera una variable, si no que el compilador ya mete directamente el valor esperado donde corresponda.

     En este ejemplo, para crear enemigos simplemente he creado un proceso independiente en el que llamo a un bucle FROM para crear 5 instancias de cada enemigo. Cosas a tener en cuenta: A diferencia del bucle FOR, el FROM sólo permite operar con constantes, entre otras cosas porque lo que hace el compilador es repetir X veces el bloque de instrucciones que contenga dicho bucle. O dicho de otra forma, no hay un componente condicional que haga variar el número de iteraciones que tendrá el bucle.

// Función: crear_enemigos
// Descripción: Crea los procesos de enemigos al inicio de cada nivel.
// - Genera 5 enemigos tipo 1 (a la izquierda) y 5 tipo 2 (a la derecha).
PROCESS crear_enemigos();
PRIVATE
i; // Variable para el bucle
BEGIN
// Crea 5 enemigos tipo 1 en posiciones espaciadas en la izquierda
FROM i=0 TO 4;
enemigo_tipo1(ESPACIADO_ENEMIGO1_X + i * ESPACIADO_ENEMIGO1_X, ESPACIADO_ENEMIGO1_Y_INICIAL + i * ESPACIADO_ENEMIGO1_Y); // Posición x e y calculada
END
// Crea 5 enemigos tipo 2 en posiciones espaciadas en la derecha
FROM i=0 TO 4;
enemigo_tipo2(ESPACIADO_ENEMIGO2_X_INICIAL + i * ESPACIADO_ENEMIGO2_X, ESPACIADO_ENEMIGO2_Y_INICIAL + i * ESPACIADO_ENEMIGO2_Y); // Posición x e y calculada
END
END

 

    Luego, defino también una función de de iniciar_nivel, que me permite llamar a ese crear enemigos, inicializar variables para la partida, invocar al jugador y le he implementado una cuenta regresiva de "3, 2, 1... GO!". Si os fijáis aquí, estoy definiendo una función y no un proceso, pero a dicha función le meto instancias de "FRAME". ¿Por qué? ¿Las funciones no son métodos que devuelven resultados? Pues esto es uno de los trucos que nos permite Div Games Studio: Las funciones, en la práctica, tienen un comportamiento identico a los procesos (aka "instancias"), pero bloquean la ejecución de su proceso padre. Es decir, hasta que incio_nivel no termine de ejecutarse, el proceso que lo llama (en este caso el bucle principal del juego) para su ejecución.

// Función: inicio_nivel
// Descripción: Inicializa un nuevo nivel mostrando una cuenta regresiva (3, 2, 1, GO!) y creando los procesos de enemigos y jugador.
// - Desactiva controles durante la cuenta y los activa al final.
// Los PROCESS se ejecutan en paralelo, mientras que las FUNCTION se ejecutan de forma secuencial
// - Aunque tenga FRAME dentro, es una función y bloquea el proceso que la llama (hack de DIV)
// - Es decir, hasta que no termine esta función, el proceso que lo llama queda bloqueado
FUNCTION inicio_nivel();
PRIVATE
INT contador = 3; // Cuenta regresiva: 3, 2, 1, GO!
INT tiempo = 0; // Contador de frames
STRING contador_str;// String para mostrar contador
BEGIN
// Desactivamos controles durante la cuenta regresiva
controles_activos = 0;
// Creamos naves pero sin movimiento
crear_enemigos(); // Creamos enemigos
jugador(); // Crear jugador
// Esperar un momento para que se vean las naves
FRAME;
FRAME;
// Cuenta regresiva 3, 2, 1, GO!
WHILE (contador > 0)
contador_str = itoa(contador); // Convertimos el número a string (itoi = integer to ascii)
write(0, CENTRO_X, CENTRO_Y, ALINEACION_CENTRADO, contador_str); // 4 = centrado
tiempo++;
IF (tiempo >= FPS_JUEGO)
// Ha pasado un segundo, actualizamos la cuenta atrás
contador--;
tiempo = 0;
delete_text(all_text); // Limpiamos todos los textos
mostrar_textos_hud(); // Volmemos a dibujar los textos del HUD
END
FRAME; // Siguiente frame
END
// Mostramos el GO!
write(0, CENTRO_X, CENTRO_Y, ALINEACION_CENTRADO, "GO!"); // 4 = centrado
tiempo = 0;
WHILE (tiempo < FPS_JUEGO / 2)
FRAME;
tiempo++;
END // Mostramos GO! por 0,5 segundos
delete_text(all_text); // Limpiamos todos los textos
mostrar_textos_hud(); // Volmemos a dibujar los textos del HUD
controles_activos = 1; // Activar controles para empezar el juego
END



   De hecho, también defino funciones "bloqueantes" para calcular las colisiones del jugador con las balas, con sus enemigos y de los enemigos con las balas y el jugador. El algoritmo de la colisiones o es muy complicado, pero necesita un punto de atención en get_id(type tipo_instancia). El funcionamiento de get_id es muy peculiar: Si tienes varias instancias de un tipo de enemigo, te devuelve sólo el ID de la instancia del primer enemigo... pero si vuelves a usar get_id te devuelve el del segundo enemigo... y si lo vuelves a usar te devuelve el del tercer enemigo... y llega un momento que devolverá 0 (cuando no queden más identificadores en la cola). La gracia es que cada vez que el proceso llama a "FRAME", esa cola de identificadores se reinicia y si lo usas vuelve a darte el primer enemigo de la cola. Sabiendo esto, podemos crear bucles que recorran todos los enemigos que hay en pantalla desde una función externa y comprobar si entra dentro de un área (las famosas "hitboxes) de los videojuegos:

// Función: comprobar_colision_jugador_enemigos
// Descripción: Verifica si la nave del jugador colisiona con algún enemigo.
// - Si hay colisión, suma puntos, crea explosiones y termina el juego.
// Parámetros:
// - x: Posición x de la nave (por valor, ya que no se modifica).
// - y: Posición y de la nave (por valor, ya que no se modifica).
// Los PROCESS se ejecutan en paralelo, mientras que las FUNCTION se ejecutan de forma secuencial
// - Es decir, hasta que no termine esta función, el proceso que lo llama queda bloqueado
FUNCTION comprobar_colision_jugador_enemigos(INT x, INT y);
PRIVATE
id_enemigo; // ID del enemigo para iterar
BEGIN
// Comprobamos la colisión con enemigos de tipo 1
// Si existen varios procesos del tipo que se ha especificado,
// la función get_id() devolverá el resto de los identificadores en las sucesivas llamadas que se realicen a la misma.
// Una vez se hayan devuelto todos los códigos identificadores, la función devolverá 0, hasta que se vuelva a ejecutar una sentencia FRAME
// En cada frame, la lista que devuelve get_id() se reinicia, es por eso que aqui no ponemos ningún FRAME
id_enemigo = get_id(type enemigo_tipo1);
// Bucle, recorriendo todos los enemigos tipo 1, enemigo a enemigo
WHILE (id_enemigo)
// Calculamos distancia entre jugador y enemigo
IF (fget_dist(x, y, id_enemigo.x, id_enemigo.y) < DISTANCIA_COLISION)
// Significa que ambos han chocado y ambos mueren
puntos += PUNTOS_POR_ENEMIGO; // Sumamos los puntos
explosion(id_enemigo.x, id_enemigo.y, COLOR_ENEMIGO1); // Explosión del enemigo
enemigo_muerto_id = id_enemigo; // Marcar enemigo como muerto
explosion(x, y, COLOR_NAVE); // Explosión del jugador
game_over = 1; // Marcamos game over (lo leerá el process principal y actuará en cosnecuencia)
RETURN;
END

// Siguiente enemigo, hasta que no hagamos FRAME, get_id() devolverá el siguiente ID
// cuando no queden más, devolverá 0 y saldremos del bucle
id_enemigo = get_id(type enemigo_tipo1);
END
// Comprobamos la colisión con enemigos de tipo 2
// Si existen varios procesos del tipo que se ha especificado,
// la función get_id() devolverá el resto de los identificadores en las sucesivas llamadas que se realicen a la misma.
// Una vez se hayan devuelto todos los códigos identificadores, la función devolverá 0, hasta que se vuelva a ejecutar una sentencia FRAME
id_enemigo = get_id(type enemigo_tipo2);
WHILE (id_enemigo)
// Calculamos distancia entre jugador y enemigo
IF (fget_dist(x, y, id_enemigo.x, id_enemigo.y) < DISTANCIA_COLISION)
// Significa que ambos han chocado y ambos mueren
puntos += PUNTOS_POR_ENEMIGO; // Sumar puntos
explosion(id_enemigo.x, id_enemigo.y, COLOR_ENEMIGO2); // Explosión del enemigo
enemigo_muerto_id = id_enemigo; // Marcar enemigo para morir
explosion(x, y, COLOR_NAVE); // Explosión del jugador
game_over = 1; // Terminar juego
RETURN;
END

// Siguiente enemigo, hasta que no hagamos FRAME, get_id() devolverá el siguiente ID
// cuando no queden más, devolverá 0 y saldremos del bucle
id_enemigo = get_id(type enemigo_tipo2);
END
END


// Función: comprobar_colision_bala_enemigos
// Descripción: Verifica si la bala del jugador colisiona con algún enemigo. Si hay colisión, suma puntos, crea explosión y marca al enemigo para morir.
// Parámetros:
// - x: Posición x de la bala.
// - y: Posición y de la bala.
// Retorna: 1 si hay colisión, 0 si no.
// Los PROCESS se ejecutan en paralelo, mientras que las FUNCTION se ejecutan de forma secuencial
// - Es decir, hasta que no termine esta función, el proceso que lo llama queda bloqueado
FUNCTION comprobar_colision_bala_enemigos(x, y);
PRIVATE
id_enemigo; // ID del enemigo para iterar
BEGIN
// Comprobamos si hay colisión con enemigos tipo 1
// Para ello obtenemos el primer ID de enemigo tipo 1
id_enemigo = get_id(type enemigo_tipo1);
// Bucle, recorriendo todos los enemigos tipo 1
WHILE (id_enemigo)
// Obtenemos la distancia entre bala y enemigo
IF (fget_dist(x, y, id_enemigo.x, id_enemigo.y) < DISTANCIA_COLISION)
// Hay colisión
puntos += PUNTOS_POR_ENEMIGO; // Sumamos puntos
explosion(id_enemigo.x, id_enemigo.y, COLOR_ENEMIGO1); // Creamos explosión
enemigo_muerto_id = id_enemigo; // Marcamos el enemigo como muerto
RETURN(1); // 1 -> Indicamos colisión
END
id_enemigo = get_id(type enemigo_tipo1); // Siguiente enemigo
END
// Comprobamos si hay colisión con enemigos tipo 2
// Para ello obtenemos el primer ID de enemigo tipo 2
id_enemigo = get_id(type enemigo_tipo2);
// Bucle, recorriendo todos los enemigos tipo 2
WHILE (id_enemigo)
// Calcular distancia entre bala y enemigo
IF (fget_dist(x, y, id_enemigo.x, id_enemigo.y) < DISTANCIA_COLISION)
// Hay colisión
puntos += PUNTOS_POR_ENEMIGO; // Sumamos puntos
explosion(id_enemigo.x, id_enemigo.y, COLOR_ENEMIGO2); // Creamos explosión
enemigo_muerto_id = id_enemigo; // Marcamos enemigo para morir
RETURN(1); // 1 -> Indicamos colisión
END
id_enemigo = get_id(type enemigo_tipo2); // Siguiente enemigo
END
RETURN(0); // 0 -> No hay colisión
END



    Con esto ya tenemos algo jugable. Ahora bien, queda feo tener un proceso por tipo de enemigo. Si mañana queremos tener 50, ¿creamos 50 procesos distintos en el código? Bueno, pues auqi viene el cuarto ejemplo, que a nivel funcional ofrece exactamente lo mismo que el anterior, pero con optiomizaciones (https://github.com/LeHamsterRuso/DIV2Examples/blob/main/PRG/VSHOOT04.PRG).

     Para emepzar creo una variable de tipo local llamada "tipo". Las variables locales en Div Games Studio actuan como propiedades. Es decir, todos los procesos del programa (enemigo, iniciar_nivel, jugador...) pasan a tener una variable llamada "tipo", pero esta variable, en su contexto, es independiente: Puedes crear 50 instancias de un proceso y asignarle valores disntintos a cada uno de ellos.

// Variables locales
// Las variables locales son atributos presentes en TODOS los process.
// - Su valor puede ser modificado únicamente desde su propio process
// - No obstante, podemos consultarlas desde cualquier otro process -> TIPO = enemigo.tipo
// - Sirve para falsear la orientación a objetos, actuaría como una "propiedad" del objeto en modo read only
LOCAL
INT tipo; // Tipo de enemigo (1 o 2), empleado sólo en el proceso genérico de enemigos

// Variables internas

    Dicho esto, borro las definiciones de enemigo_tipo1 y enemigo_tipo2 y creo un único proceso que haga uso de esa variable local:

 

// Proceso: enemigo
// Descripción: Controla el comportamiento de un enemigo (tipo 1 o 2).
// - Se mueve según el tipo, dispara balas y verifica si muere.
// Parámetros:
// - x: Posición inicial x.
// - y: Posición inicial y.
// - tipo: Tipo de enemigo (1 o 2).
// (OPTIMIZACIÓN: Proceso genérico para ambos tipos de enemigos.
// Reduce duplicación de código. Usa parámetro 'tipo' para diferenciar comportamiento.
// Mejora mantenibilidad: cambios en un lugar afectan a ambos tipos.)
PROCESS enemigo(x, y, tipo);
PRIVATE
dir = 1; // Para tipo 1
ang = 0; // Para tipo 2
tiempo = 0;
temp_id = 0; // Para iterar en bucles
enemigo_draws[5]; // 0: box/circulo para tipo1, 1-4: lineas para tipo2, 5: circulo tipo2
BEGIN
// Dibujo inicial del enemigo
IF (tipo == 1)
dibujar_enemigo_tipo1(&enemigo_draws[0], &enemigo_draws[1], x, y);
ELSE
dibujar_enemigo_tipo2(&enemigo_draws[0], &enemigo_draws[1], &enemigo_draws[2], &enemigo_draws[3], &enemigo_draws[4], x, y);
END

LOOP
IF (game_over == 1) RETURN; END

// Verificar muerte
IF (enemigo_muerto_id == id)
// Borrar dibujos
FROM temp_id=0 TO 4;
IF (enemigo_draws[temp_id] != 0) delete_draw(enemigo_draws[temp_id]); END
END
enemigos_vivos--;
enemigo_muerto_id = 0;
RETURN;
END
// Dibujar y mover basado en tipo solo si controles activos
IF (controles_activos == 1)
IF (tipo == 1)
dibujar_enemigo_tipo1(&enemigo_draws[0], &enemigo_draws[1], x, y);
mover_enemigo_tipo1(&x, &y, &dir);
IF (y > ALTO_PANTALLA + 10) RETURN; END
ELSE
dibujar_enemigo_tipo2(&enemigo_draws[0], &enemigo_draws[1], &enemigo_draws[2], &enemigo_draws[3], &enemigo_draws[4], x, y);
mover_enemigo_tipo2(&x, &y, &ang);
END
END
// Disparar si corresponde
IF (controles_activos == 1)
IF (tipo == 1)
disparar_enemigo_tipo1(&tiempo, x, y);
ELSE
disparar_enemigo_tipo2(&tiempo, x, y);
END
END
FRAME;
END
END

 

¿Podría haberme limitado a usar una variable privada tipo y pasarlo como parámetro a enemigo? Pues sí, pero me apetecía explicar que las variables locales existen y para qué sirven. De hecho, ahora que definimos la variable como local, puede ser consultada de forma externa por otras funciones y procesos, cosa que no podríamos hacer si la variable fuera privada:

// Función: comprobar_colision_bala_enemigos
// Descripción: Verifica si la bala del jugador colisiona con algún enemigo. Si hay colisión, suma puntos, crea explosión y marca al enemigo para morir.
// Parámetros:
// - x: Posición x de la bala.
// - y: Posición y de la bala.
// Retorna: 1 si hay colisión, 0 si no.
// Los PROCESS se ejecutan en paralelo, mientras que las FUNCTION se ejecutan de forma secuencial
// - Es decir, hasta que no termine esta función, el proceso que lo llama queda bloqueado
FUNCTION comprobar_colision_bala_enemigos(x, y);
PRIVATE
id_enemigo; // ID del enemigo para iterar
color_explosion; // Color para la explosión del enemigo
BEGIN
// Comprobamos si hay colisión con enemigos
// Para ello obtenemos el primer ID de enemigo
id_enemigo = get_id(type enemigo);
// Bucle, recorriendo todos los enemigos
WHILE (id_enemigo)
// Obtenemos la distancia entre bala y enemigo
IF (fget_dist(x, y, id_enemigo.x, id_enemigo.y) < DISTANCIA_COLISION)
// Hay colisión
puntos += PUNTOS_POR_ENEMIGO; // Sumamos puntos
// Determinamos el color de la explosión según el tipo de enemigo
IF (id_enemigo.tipo == 1)
color_explosion = COLOR_ENEMIGO1;
ELSE
color_explosion = COLOR_ENEMIGO2;
END
explosion(id_enemigo.x, id_enemigo.y, color_explosion); // Creamos explosión
enemigo_muerto_id = id_enemigo; // Marcamos el enemigo como muerto
RETURN(1); // 1 -> Indicamos colisión
END
id_enemigo = get_id(type enemigo); // Siguiente enemigo
END
RETURN(0);
END

 

     Pero no es la única optimización que podríamos aplicar. De hecho, en el ejemplo 5 seguimos sin cambiar funcionalmente el juego y nos centramos en ir aún más lejos con las optimizaciones (https://github.com/LeHamsterRuso/DIV2Examples/blob/main/PRG/VSHOOT05.PRG). ¿Cómo? Pues con dos grandes conocidos del capítulo 2 (que os ha dado pesadillas): ¡Uso de estructuras y de un único hilo para dibujar a todos los enemigos!

     Para empezar, ese hermosa variable local que habíamos creado nos la cepillamos y creamos una estructura genérica para los eneigos. En ella definimos su posición (x, y), el tipo de enemigo, si está vivo o muerto, la dirección de su desplazamiento, su ángulo de movimiento (para el tipo 2) y un array de enteros donde almacenar los ID de las primitivas gráficas que lo representan.

// Definición de struct para enemigo
STRUCT enemigo[10]
INT x; // Posición X
INT y; // Posición Y
INT tipo; // Tipo de enemigo (1 o 2)
INT activo; // Si está activo (1) o muerto/inactivo (0)
INT dir; // Dirección horizontal para tipo1 (1 derecha, -1 izquierda)
INT ang; // Ángulo para movimiento circular de tipo2
INT draw[5]; // Array de IDs de dibujos (5 slots por enemigo)
END


 

       Luego, borramos el proceso enemigo y creamos un "proceso_enemigos" para gestionar TODOS los enemigos en bucle. ¡Fijaros que reutilizamos los métodos ya existentes de dibujo y movimiento (gracias a la magia de los punteros), pero recorremos todos los enemigos que hay en la estructura para ver si están vivos o muertos y actuar en consecuencia:

// Proceso: proceso_enemigos
// Descripción: Controla el comportamiento de todos los enemigos usando un STRUCT array.
// - Maneja movimiento, disparos y eliminación de enemigos tipo 1 y 2.
// (OPTIMIZACIÓN: Proceso genérico para ambos tipos de enemigos.
// Reduce duplicación de código. Usa parámetro 'tipo' para diferenciar comportamiento.
// Mejora mantenibilidad: cambios en un lugar afectan a ambos tipos.)
PROCESS proceso_enemigos();
PRIVATE
INT i, j;
INT tiempo_disparo[10]; // Contador de tiempo para disparos de cada enemigo
BEGIN
// Dibujo inicial de todos los enemigos
FOR (i=0; i<10; i++)
IF (enemigo[i].activo == 1)
IF (enemigo[i].tipo == 1)
// Dibujamos el enemigo
dibujar_enemigo_tipo1(&enemigo[i].draw[0], &enemigo[i].draw[1], enemigo[i].x, enemigo[i].y);
ELSE
// Dibujamos el enemigo de tipo 2
dibujar_enemigo_tipo2(&enemigo[i].draw[0], &enemigo[i].draw[1], &enemigo[i].draw[2], &enemigo[i].draw[3], &enemigo[i].draw[4], enemigo[i].x, enemigo[i].y);
END
END
END

LOOP
// Si el juego ha terminado, salimos del proceso
IF (game_over == 1) RETURN; END

// Por cada enemigo
FOR (i=0; i<10; i++)
// Verificamos si está vivo
IF (enemigo[i].activo == 1)
// Verificamos si los controles están activos (p. ej., no durante la cuenta regresiva)
IF (controles_activos == 1)
// Movemos en función del tipo
IF (enemigo[i].tipo == 1)
// Movemos el enemigo tipo 1
mover_enemigo_tipo1(&enemigo[i].x, &enemigo[i].y, &enemigo[i].dir);
ELSE
// Movemos el enemigo tipo 2
mover_enemigo_tipo2(&enemigo[i].x, &enemigo[i].y, &enemigo[i].ang);
END

// Verificar si sale de pantalla
IF (enemigo[i].tipo == 1 AND enemigo[i].y > ALTO_PANTALLA + TAMANO_ENEMIGO1)
enemigo[i].activo = 0;
FROM j=0 TO 4;
IF (enemigo[i].draw[j] != 0) delete_draw(enemigo[i].draw[j]); END
END
END

// Dibujamos el enemigo en función del tipo
IF (enemigo[i].tipo == 1)
dibujar_enemigo_tipo1(&enemigo[i].draw[0], &enemigo[i].draw[1], enemigo[i].x, enemigo[i].y);
ELSE
dibujar_enemigo_tipo2(&enemigo[i].draw[0], &enemigo[i].draw[1], &enemigo[i].draw[2], &enemigo[i].draw[3], &enemigo[i].draw[4], enemigo[i].x, enemigo[i].y);
END

// Disparar en función del tipo
IF (enemigo[i].tipo == 1)
disparar_enemigo_tipo1(&tiempo_disparo[i], enemigo[i].x, enemigo[i].y);
ELSE
disparar_enemigo_tipo2(&tiempo_disparo[i], enemigo[i].x, enemigo[i].y);
END
END
END
END

FRAME;
END
END

     Fijaros que también, a su vez, el código acaba siendo más sencillo cuando manejamos estructuras. Por ejemplo, reducimos aún más el tamaño de las funciones de detección colisiones:

// Función: comprobar_colision_bala_enemigos
// Descripción: Verifica si la bala del jugador colisiona con algún enemigo. Si hay colisión, retorna el index del enemigo.
// Parámetros:
// - x: Posición x de la bala.
// - y: Posición y de la bala.
// Retorna: Index del enemigo colisionado (0-4), o -1 si no hay colisión.
// Los PROCESS se ejecutan en paralelo, mientras que las FUNCTION se ejecutan de forma secuencial
// - Es decir, hasta que no termine esta función, el proceso que lo llama queda bloqueado
FUNCTION comprobar_colision_bala_enemigos(INT x, INT y);
PRIVATE
INT i; // Variable para el bucle
BEGIN
// Comprobamos si hay colisión con enemigos iterando sobre el array
FOR (i=0; i<10; i++)
IF (enemigo[i].activo == 1)
// Obtenemos la distancia entre bala y enemigo
IF (fget_dist(x, y, enemigo[i].x, enemigo[i].y) < DISTANCIA_COLISION)
RETURN(i); // Retornamos el index del enemigo colisionado
END
END
END
RETURN(-1); // No hay colisión
END

 

     Bueno, pues ahora que tenemos un juego potable y algo optimizado, podemos empezar a trabajar con sprites y aquí entra el ejemplo 6 (https://github.com/LeHamsterRuso/DIV2Examples/blob/main/PRG/VSHOOT06.PRG). Este ejemplo es básicamente el anterior, pero adaptado para trabajar con sprites.

     El mayor cambio reside en que, como he querido animar al personaje principal, he utilizado distintos gráficos para él en función de la tecla que pulsamos y de si estamos disparando o no. Es decir, si nos desplazamos o si disparamos, el gráfico va cambiando de forma dinámica.

      Básicamente tenemos un fichero FPG para él con 16 gráficos. Los 8 primeros representan las 8 posiciones posibles en las que podemos depslazarnos (arriba, abajo, izquierda, derecha y diagonales) cuando no estamos disparando y las 8 siguientes es lo mismo, pero disparando. Sobre qué gráfico va primero, pues lo gestiono como si fueran las agujas del reloj -> 1 sería mirar arriba, 2 arriba/derecha, 3 derecha... y así hasta 8 izquierda/arriba.

// Función: calcular_grafico_jugador
// Descripción: Calcula el ID del gráfico según dirección y estado de disparo
// Retorna: ID del gráfico (1-18)
FUNCTION calcular_grafico_jugador(INT dir_x, INT dir_y, INT disparando);
PRIVATE
INT base_grafico = 0;
INT offset_direccion = 0;
BEGIN
// Base: 0 si no dispara, 9 si dispara
IF (disparando == 1)
base_grafico = 9;
ELSE
base_grafico = 0;
END

// Calcular offset según dirección
// Quieto
IF (dir_x == 0 AND dir_y == 0)
offset_direccion = 1;
END
// Arriba
IF (dir_x == 0 AND dir_y == -1)
offset_direccion = 2;
END
// Arriba derecha
IF (dir_x == 1 AND dir_y == -1)
offset_direccion = 3;
END
// Derecha
IF (dir_x == 1 AND dir_y == 0)
offset_direccion = 4;
END
// Derecha abajo
IF (dir_x == 1 AND dir_y == 1)
offset_direccion = 5;
END
// Abajo
IF (dir_x == 0 AND dir_y == 1)
offset_direccion = 6;
END
// Abajo izquierda
IF (dir_x == -1 AND dir_y == 1)
offset_direccion = 7;
END
// Izquierda
IF (dir_x == -1 AND dir_y == 0)
offset_direccion = 8;
END
// Arriba izquierda
IF (dir_x == -1 AND dir_y == -1)
offset_direccion = 9;
END

RETURN(base_grafico + offset_direccion);
END

 

    Sobre los sprites en cuestión, decidí bastame en el universo de Saga of Tanya The Evil de Carlo Zen (su mangaka, Chika Tojo, me cae genial). Su protagonista, Tanya, la he modelado varias veces en Blender y decidí hacer uso de una versión modificada que subí a sketchfab en verano y que, pese a ser uno de mis modelos preferidos y de los que más orgulloso estoy, lleva la friolera de cero likes (https://sketchfab.com/3d-models/tanya-degurechaff-from-saga-of-tanya-the-evil-fb725e63784d4839928aa8ea6fe28e56).

    Y esto me consumió batante tiempo, puesto que el modelo de Tanya no lo tenía animado conforme yo quería para este juego y tuve que "riguear" poses nuevas para luego renderizarlas.

 


      Para los enemigos fue más sencillo. Generé modelos con Meshy.AI y luego los rendericé por Blender.


     Y para los escenarios tiré de ChatGPT.


 

      Ahora bien, el proceso de adaptar los gráficos para Div Games Studio fue bastante tortuoso. Lo primero es que, por comodidad, decidí emplear una única paleta de colores para todos los niveles. Esto me obligaba, por ejemplo, a crearme una paleta en Gimp para poder hacer uso de ella en el modo "Imagen/Modo/Indexado". Además, para mitiguar la limitacion de 256 colores forcé el Dithering activando el tramado de color posicionado:


      Para crear la paleta en Gimp, lo que hice fue crear desde Div2 un dibujo random en formato PCX con la tableta "DIV2.PAL".

 Por otro lado también implementé una pequeña explosión que se vea en el fusil de Tanya cuando dispara:

// Proceso: explosion_pequena
// Descripción: Crea una animación de explosión pequeña para efectos de disparo
// - Más pequeña y rápida que la explosión normal
// Parámetros:
// - x: Posición x de la explosión.
// - y: Posición y de la explosión.
// - color: Color de la explosión.
PROCESS explosion_pequena(x, y, color);
PRIVATE
i; // Contador para el bucle
radio = 3; // Radio inicial más pequeño
exp_draw; // ID del dibujo de la explosión
BEGIN
// Animamos la explosión pequeña
FROM i=0 TO 3; // Menos iteraciones para que sea más rápida
IF (exp_draw != 0) delete_draw(exp_draw); END
exp_draw = draw(5, color, OPACIDAD_DRAW, REGION_DRAW, x - radio, y - radio, x + radio, y + radio);
radio++; // Incremento menor
FRAME;
END
delete_draw(exp_draw);
END

 


    Ahora bien, con este ejemplo 6 me di cuenta de una cosa bastante básica y es que, si desplazaba al personaje a una esquina y dejaba que los enemigos dispararan... pues habían ralentizaciones, el juego tenía bajones de frames. Y es que, aunque habíamos optimizado los enemigos, ¡pero no las balas! Y justamente en un juego de tipo bullet-hell como el que estabamos creando ahora mismo, era obvio que en pantalla tendríamos más balas que enemigos. "Lo siento mucho, me he equivocado, no volverá a ocurrir".

     Así me puse a implementar el ejemplo 7, que es, a nivel jugable, idéntico al anterior, pero optimizando las balas (https://github.com/LeHamsterRuso/DIV2Examples/blob/main/PRG/VSHOOT07.PRG). ¿Cómo? ¡Pues con estructuras y un proceso común para dibujarlas todas!

// Definición de struct para balas del jugador (OPTIMIZACIÓN: Lista enlazada separada)
// Almacena balas del jugador en array con lista enlazada para optimizar acceso
STRUCT bala_jugador[MAX_BALAS_JUGADOR]
INT x; // Posición X de la bala
INT y; // Posición Y de la bala
INT activo; // Si está activa (1) o inactiva (0)
INT dx; // Delta X precalculado para movimiento (siempre 0 para jugador)
INT dy; // Delta Y precalculado para movimiento (hacia arriba)
INT tamano; // Tamaño precalculado para dibujo
INT color; // Color precalculado para dibujo
INT draw_id; // ID del dibujo de la bala
INT siguiente; // Índice del siguiente slot en la lista (-1 = fin de lista)
END

// Definición de struct para balas enemigas (OPTIMIZACIÓN: Lista enlazada separada)
// Almacena balas enemigas en array con lista enlazada para optimizar acceso
STRUCT bala_enemigo[MAX_BALAS_ENEMIGO]
INT x; // Posición X de la bala
INT y; // Posición Y de la bala
INT tipo; // Tipo de bala enemiga (1=enemigo1, 2=enemigo2)
INT activo; // Si está activa (1) o inactiva (0)
INT angulo; // Ángulo de movimiento (para balas enemigo tipo 2)
INT dx; // Delta X precalculado para movimiento
INT dy; // Delta Y precalculado para movimiento
INT tamano; // Tamaño precalculado para dibujo
INT color; // Color precalculado para dibujo
INT draw_id; // ID del dibujo de la bala
INT siguiente; // Índice del siguiente slot en la lista (-1 = fin de lista)
END

 

Cosas a tener en cuenta: Veréis un INT de "Siguiente". Esto es debido a que, como tienen que haber tantas balas en pantalla, hace falta hacer un sistema de listas. Si digo al compilador que pueden haber 256 balas en pantalla como máximo, resulta contraproducente crear bucles donde se recorran las 256 posiciones del array de balas, existan o no, para cada verificación que tenga que ver con balas. Lo que sí que puedo hacer es marcar con un "-1" en siguiente para decirle a un bucle "no sigas recorriendo el array de balas, no ay más ahora mismo:

// Proceso: proceso_balas_jugador (OPTIMIZACIÓN: Proceso separado para balas del jugador)
// Descripción: Proceso dedicado que gestiona solo balas del jugador usando lista enlazada
// - Solo procesa balas activas del jugador siguiendo la lista enlazada
// - Mueve, dibuja y verifica colisiones con enemigos
// - Elimina balas que salen de pantalla o colisionan
PROCESS proceso_balas_jugador();
PRIVATE
INT actual; // Índice de la bala actual en la lista
INT anterior; // Índice de la bala anterior en la lista
INT siguiente; // Índice de la bala siguiente (guardado antes de posible eliminación)
INT enemigo_index; // Índice del enemigo colisionado
INT color_explosion; // Color de explosión
INT debe_eliminar; // Flag para marcar si debe eliminarse
BEGIN
LOOP
// Si el juego terminó, salimos
IF (game_over == 1) RETURN; END

// PROCESAR BALAS DEL JUGADOR
anterior = -1;
actual = primera_bala_jugador_activa;
WHILE (actual != -1)
// Guardar el siguiente antes de posibles modificaciones
siguiente = bala_jugador[actual].siguiente;
debe_eliminar = 0;
IF (bala_jugador[actual].draw_id != 0)
delete_draw(bala_jugador[actual].draw_id);
bala_jugador[actual].draw_id = 0;
END

// Mover la bala según deltas precalculados
bala_jugador[actual].x += bala_jugador[actual].dx;
bala_jugador[actual].y += bala_jugador[actual].dy;
// Verificar si sale de pantalla
IF (bala_jugador[actual].y < 0 OR bala_jugador[actual].y > ALTO_PANTALLA OR bala_jugador[actual].x < 0 OR bala_jugador[actual].x > ANCHO_PANTALLA)
debe_eliminar = 1;
ELSE
// Colisión con enemigos
enemigo_index = comprobar_colision_bala_enemigos(bala_jugador[actual].x, bala_jugador[actual].y);
IF (enemigo_index != -1)
puntos += PUNTOS_POR_ENEMIGO;
IF (enemigo[enemigo_index].tipo == 1)
color_explosion = COLOR_ENEMIGO1;
ELSE
color_explosion = COLOR_ENEMIGO2;
END
explosion(enemigo[enemigo_index].x, enemigo[enemigo_index].y, color_explosion);
enemigo[enemigo_index].activo = 0;
remover_enemigo_lista(enemigo_index);
enemigos_vivos--;
debe_eliminar = 1;
END
END
// Eliminar bala de la lista si es necesario
IF (debe_eliminar == 1)
// Sacar de lista de activas
IF (anterior == -1)
primera_bala_jugador_activa = siguiente;
ELSE
bala_jugador[anterior].siguiente = siguiente;
END
// Marcar como inactiva
bala_jugador[actual].activo = 0;
// Devolver a lista de libres
bala_jugador[actual].siguiente = primer_slot_libre_jugador;
primer_slot_libre_jugador = actual;
ELSE
// Dibujar la bala
bala_jugador[actual].draw_id = draw(TIPO_DIBUJO_CIRCULO_RELLENO, bala_jugador[actual].color, OPACIDAD_DRAW, REGION_DRAW, bala_jugador[actual].x - bala_jugador[actual].tamano, bala_jugador[actual].y - bala_jugador[actual].tamano, bala_jugador[actual].x + bala_jugador[actual].tamano, bala_jugador[actual].y + bala_jugador[actual].tamano);
// Avanzar anterior solo si no eliminamos
anterior = actual;
END
// Avanzar al siguiente
actual = siguiente;
END

FRAME; // Esperar al siguiente frame
END
END


      Ahora que hemos reoptimizado el código para evitar bajones de frames, deberíamos adaptar el juego para que sea más divertido. Quiero decir, ahora mismo tenemos un escenario inmovil con diez enemigos fijos... y en eso consiste nuestro ejemplo 8 (https://github.com/LeHamsterRuso/DIV2Examples/blob/main/PRG/VSHOOT08.PRG). En esta versión implementamos un scroll (con start_scroll, no el falso scroll del capítulo 2), repartimos 80 enemigos por todo el escenario e implementamos rutinas para revisar que los enemigos que están fuera de la pantalla no se dibujen y que las barras que salen de nuestra pantalla se destruyan (pensad que las coordenadas no son iguales si usamos scroll o no).

//------------------------------------------------------------------------------
// PROCESS: proceso_enemigos (MODIFICADO PARA SCROLL)
// DESCRIPCIÓN: Controla el comportamiento de todos los enemigos usando un STRUCT array.
// Maneja movimiento, disparos y eliminación de enemigos tipo 1 y 2.
// Ahora solo procesa y dibuja enemigos visibles en la pantalla actual del scroll.
// Usa procesos hijos con ctype=c_screen para visualizar cada enemigo.
//------------------------------------------------------------------------------
PROCESS proceso_enemigos();
PRIVATE
INT i, j;
INT tiempo_disparo[NUM_ENEMIGOS_INICIAL]; // Contador de tiempo para disparos de cada enemigo
INT enemigo_visible; // Flag para verificar si el enemigo está visible
INT procesos_enemigo[NUM_ENEMIGOS_INICIAL]; // IDs de procesos hijos para cada enemigo
BEGIN
// Inicializar array de procesos
FROM i=0 TO NUM_ENEMIGOS_INICIAL - 1;
procesos_enemigo[i] = 0;
END

LOOP
// Si el juego ha terminado, salimos del proceso
IF (game_over == 1)
// Terminar todos los procesos hijos
FOR (i=0; i<NUM_ENEMIGOS_INICIAL; i++)
IF (procesos_enemigo[i] != 0)
signal(procesos_enemigo[i], s_kill);
procesos_enemigo[i] = 0;
END
END
RETURN;
END

// Por cada enemigo
FOR (i=0; i<NUM_ENEMIGOS_INICIAL; i++)
// Verificamos si está vivo
IF (enemigo[i].activo == 1)
// OPTIMIZACIÓN: Solo procesar enemigos visibles en el scroll actual
// Un enemigo es visible si su Y está entre scroll.y0 - MARGEN y scroll.y0 + ALTO_PANTALLA + MARGEN
enemigo_visible = 0;
IF (enemigo[i].y >= scroll.y0 - MARGEN_VISIBLE_ARRIBA AND enemigo[i].y <= scroll.y0 + ALTO_PANTALLA + MARGEN_VISIBLE_ABAJO)
enemigo_visible = 1;
END

// Solo procesar si está visible
IF (enemigo_visible == 1)
// Crear proceso hijo si no existe
IF (procesos_enemigo[i] == 0)
procesos_enemigo[i] = proceso_enemigo_visual(i);
END

// Solo mover y disparar si los controles están activos
IF (controles_activos == 1)
// Movemos en función del tipo
IF (enemigo[i].tipo == 1)
// Movemos el enemigo tipo 1
mover_enemigo_tipo1(&enemigo[i].x, &enemigo[i].y, &enemigo[i].dir);
ELSE
// Movemos el enemigo tipo 2
mover_enemigo_tipo2(&enemigo[i].x, &enemigo[i].y, &enemigo[i].ang);
END

// Verificar si sale del área visible por abajo
IF (enemigo[i].y > scroll.y0 + ALTO_PANTALLA + MARGEN_VISIBLE_ABAJO)
enemigo[i].activo = 0;
remover_enemigo_lista(i);
enemigos_vivos--;
END

// Disparar en función del tipo
IF (enemigo[i].tipo == 1)
disparar_enemigo_tipo1(&tiempo_disparo[i], enemigo[i].x, enemigo[i].y);
ELSE
disparar_enemigo_tipo2(&tiempo_disparo[i], enemigo[i].x, enemigo[i].y);
END
END
ELSE
// Enemigo no visible, eliminar proceso hijo si existe
IF (procesos_enemigo[i] != 0)
signal(procesos_enemigo[i], s_kill);
procesos_enemigo[i] = 0;
END
END
ELSE
// Enemigo muerto, eliminar proceso hijo si existe
IF (procesos_enemigo[i] != 0)
signal(procesos_enemigo[i], s_kill);
procesos_enemigo[i] = 0;
END
END
END

FRAME;
END
END

//------------------------------------------------------------------------------
// PROCESS: proceso_enemigo_visual (NUEVO)
// DESCRIPCIÓN: Proceso hijo liviano que solo se encarga de visualizar un enemigo.
// Usa ctype=c_screen para coordenadas de pantalla.
// Lee los datos del enemigo desde el struct enemigo[].
//------------------------------------------------------------------------------
PROCESS proceso_enemigo_visual(INT index);
BEGIN
ctype = c_screen; // Usar coordenadas de pantalla
file = FPG_LEVEL; // Archivo de gráficos

LOOP
// Si el enemigo ya no está activo o el juego terminó, salir
IF (enemigo[index].activo == 0 OR game_over == 1)
RETURN;
END

// Actualizar posición en pantalla (convertir de coordenadas de mapa a pantalla)
x = enemigo[index].x - scroll.x0;
y = enemigo[index].y - scroll.y0;

// Actualizar gráfico según tipo
IF (enemigo[index].tipo == 1)
graph = GRAFICO_ENEMIGO1;
// Rotar según dirección de movimiento
IF (enemigo[index].dir == 1)
angle = ANGULO_ROTACION_ENEMIGO; // Rotar 15° a la derecha
ELSE
angle = ANGULO_ROTACION_ENEMIGO_OPUESTA; // Rotar 15° a la izquierda
END
size = ESCALA_ENEMIGO;
ELSE
graph = GRAFICO_ENEMIGO2;
angle = 0;
size = 100;
END

FRAME;
END
END

 

 


 

     Con todo esto ya vamos teniendo un juego más o menos potable, pero siempre podemos ir a más. En el siguiente ejemplo, el 9, (https://github.com/LeHamsterRuso/DIV2Examples/blob/main/PRG/VSHOOT09.PRG), vamos más lejos e implementamos un sistema de niveles distintos, cada uno con su propio fichero FPG. En cada nivel, además, incrementamos el número de enemigos: Empezamos con 40, después 50, después 60... y así vamos sumando de diez en diez en cada fase. Además, el reparto de enemigos lo hacemos de forma escalonada, haciendo que haya pocos enemigos de inicio y muchos al final. También se implementa un sistema de vidas, para que el personaje del jugador no muera al primer toque e incluso cambiamos su fichero FPG en función del número de toques recibido (para dibujarlo sin casco al recibir el primer balazo y sin mochila al recibir el segundo).

 

//------------------------------------------------------------------------------
// FUNCIÓN: cargar_fpg_nivel
// DESCRIPCIÓN: Carga el FPG correspondiente al nivel actual
// Descarga el FPG anterior si existe y carga el nuevo
//------------------------------------------------------------------------------
FUNCTION cargar_fpg_nivel(INT nivel_fpg);
PRIVATE
STRING ruta_fpg; // Ruta del FPG a cargar
STRING numero_str; // Número del nivel como string
BEGIN
// Descargar el FPG anterior si existe
IF (FPG_LEVEL != 0)
unload_fpg(FPG_LEVEL);
END

// Construir la ruta del FPG según el nivel
// LEVEL000.FPG, LEVEL001.FPG, ..., LEVEL009.FPG, LEVEL010.FPG, LEVEL011.FPG, LEVEL012.FPG
numero_str = itoa(nivel_fpg);

// Agregar ceros a la izquierda según sea necesario
IF (nivel_fpg < 10)
ruta_fpg = "TANYA/LEVEL00" + numero_str + ".FPG";
ELSE
ruta_fpg = "TANYA/LEVEL0" + numero_str + ".FPG";
END

// Cargar el nuevo FPG
FPG_LEVEL = load_fpg(ruta_fpg);
END

 

// Proceso: proceso_balas_jugador (OPTIMIZACIÓN: Proceso separado para balas del jugador)
// Descripción: Proceso dedicado que gestiona solo balas del jugador usando lista enlazada
// - Solo procesa balas activas del jugador siguiendo la lista enlazada
// - Mueve, dibuja y verifica colisiones con enemigos
// - Elimina balas que salen de pantalla o colisionan
PROCESS proceso_balas_jugador();
PRIVATE
INT actual; // Índice de la bala actual en la lista
INT anterior; // Índice de la bala anterior en la lista
INT siguiente; // Índice de la bala siguiente (guardado antes de posible eliminación)
INT enemigo_index; // Índice del enemigo colisionado
INT color_explosion; // Color de explosión
INT debe_eliminar; // Flag para marcar si debe eliminarse
BEGIN
LOOP
// Si el juego terminó, salimos
IF (game_over == 1) RETURN; END

// PROCESAR BALAS DEL JUGADOR
anterior = -1;
actual = primera_bala_jugador_activa;

WHILE (actual != -1)
// Guardar el siguiente antes de posibles modificaciones
siguiente = bala_jugador[actual].siguiente;
debe_eliminar = 0;

// Mover la bala según deltas precalculados
bala_jugador[actual].x += bala_jugador[actual].dx;
bala_jugador[actual].y += bala_jugador[actual].dy;

// Verificar si sale del área visible del scroll (eliminar balas fuera de pantalla)
IF (bala_jugador[actual].y < scroll.y0 - MARGEN_VISIBLE_ARRIBA OR
bala_jugador[actual].y > scroll.y0 + ALTO_PANTALLA + MARGEN_VISIBLE_ABAJO OR
bala_jugador[actual].x < 0 OR
bala_jugador[actual].x > ANCHO_PANTALLA)
debe_eliminar = 1;
ELSE
// Colisión con enemigos
enemigo_index = comprobar_colision_bala_enemigos(bala_jugador[actual].x, bala_jugador[actual].y);
IF (enemigo_index != -1)
puntos += PUNTOS_POR_ENEMIGO;
IF (enemigo[enemigo_index].tipo == 1)
color_explosion = COLOR_ENEMIGO1;
ELSE
color_explosion = COLOR_ENEMIGO2;
END
explosion(enemigo[enemigo_index].x, enemigo[enemigo_index].y, color_explosion);
enemigo[enemigo_index].activo = 0;
remover_enemigo_lista(enemigo_index);
enemigos_vivos--;
debe_eliminar = 1;
END
END

// Eliminar bala de la lista si es necesario
IF (debe_eliminar == 1)
// Sacar de lista de activas
IF (anterior == -1)
primera_bala_jugador_activa = siguiente;
ELSE
bala_jugador[anterior].siguiente = siguiente;
END

// Marcar como inactiva
bala_jugador[actual].activo = 0;

// Devolver a lista de libres
bala_jugador[actual].siguiente = primer_slot_libre_jugador;
primer_slot_libre_jugador = actual;
ELSE
// Dibujar la bala (convertir coordenadas de mapa a pantalla)
bala_jugador[actual].draw_id = draw(TIPO_DIBUJO_CIRCULO_RELLENO, bala_jugador[actual].color, OPACIDAD_DRAW, REGION_DRAW,
(bala_jugador[actual].x - scroll.x0) - bala_jugador[actual].tamano,
(bala_jugador[actual].y - scroll.y0) - bala_jugador[actual].tamano,
(bala_jugador[actual].x - scroll.x0) + bala_jugador[actual].tamano,
(bala_jugador[actual].y - scroll.y0) + bala_jugador[actual].tamano);

// Avanzar anterior solo si no eliminamos
anterior = actual;
END

// Avanzar al siguiente
actual = siguiente;
END

FRAME; // Esperar al siguiente frame
END
END

// Proceso: proceso_balas_enemigo (OPTIMIZACIÓN: Proceso separado para balas enemigas)
// Descripción: Proceso dedicado que gestiona solo balas enemigas usando lista enlazada
// - Solo procesa balas activas enemigas siguiendo la lista enlazada
// - Mueve, dibuja y verifica colisiones con el jugador
// - Elimina balas que salen de pantalla o colisionan
PROCESS proceso_balas_enemigo();
PRIVATE
INT actual; // Índice de la bala actual en la lista
INT anterior; // Índice de la bala anterior en la lista
INT siguiente; // Índice de la bala siguiente (guardado antes de posible eliminación)
INT id_player; // ID del jugador
INT debe_eliminar; // Flag para marcar si debe eliminarse
INT dx, dy; // Diferencias para cálculo de distancia
INT dist_cuadrado; // Distancia al cuadrado (optimización)
BEGIN
id_player = get_id(type jugador);
LOOP
// Si el juego terminó, salimos
IF (game_over == 1) RETURN; END

// PROCESAR BALAS ENEMIGAS
anterior = -1;
actual = primera_bala_enemigo_activa;

WHILE (actual != -1)
// Guardar el siguiente antes de posibles modificaciones
siguiente = bala_enemigo[actual].siguiente;
debe_eliminar = 0;

// Mover la bala según deltas precalculados
bala_enemigo[actual].x += bala_enemigo[actual].dx;
bala_enemigo[actual].y += bala_enemigo[actual].dy;

// Verificar si sale del área visible del scroll (eliminar balas fuera de pantalla)
IF (bala_enemigo[actual].y < scroll.y0 - MARGEN_VISIBLE_ARRIBA OR
bala_enemigo[actual].y > scroll.y0 + ALTO_PANTALLA + MARGEN_VISIBLE_ABAJO OR
bala_enemigo[actual].x < 0 OR
bala_enemigo[actual].x > ANCHO_PANTALLA)
debe_eliminar = 1;
ELSE
// Colisión con jugador (solo si no está inmune)
IF (id_player != 0 AND jugador_inmune == 0)
// Bounding box rápida
IF (abs(bala_enemigo[actual].x - id_player.x) < MARGEN_BOUNDING_BOX AND abs(bala_enemigo[actual].y - id_player.y) < MARGEN_BOUNDING_BOX)
// Calcular distancia al cuadrado
dx = bala_enemigo[actual].x - id_player.x;
dy = bala_enemigo[actual].y - id_player.y;
dist_cuadrado = dx * dx + dy * dy;
IF (dist_cuadrado < DISTANCIA_COLISION_CUADRADO)
// El jugador recibe un impacto
vidas_jugador--;
explosion(id_player.x, id_player.y, COLOR_NAVE);

// Verificar si el jugador murió
IF (vidas_jugador <= 0)
// Tercera bala = muerte
game_over = 1;
ELSE
// Activar inmunidad temporal y parpadeo
jugador_inmune = TIEMPO_INMUNIDAD;

// Cambiar FPG según vidas restantes
IF (vidas_jugador == 2)
// Primera bala: cambiar a PLAYERME.FPG
id_player.file = FPG_PLAYERME;
ELSE
// Segunda bala: cambiar a PLAYERLO.FPG
id_player.file = FPG_PLAYERLO;
END
END

debe_eliminar = 1;
END
END
END
END

// Eliminar bala de la lista si es necesario
IF (debe_eliminar == 1)
// Sacar de lista de activas
IF (anterior == -1)
primera_bala_enemigo_activa = siguiente;
ELSE
bala_enemigo[anterior].siguiente = siguiente;
END

// Marcar como inactiva
bala_enemigo[actual].activo = 0;

// Devolver a lista de libres
bala_enemigo[actual].siguiente = primer_slot_libre_enemigo;
primer_slot_libre_enemigo = actual;
ELSE
// Dibujar la bala (convertir coordenadas de mapa a pantalla)
bala_enemigo[actual].draw_id = draw(TIPO_DIBUJO_CIRCULO_RELLENO, bala_enemigo[actual].color, OPACIDAD_DRAW, REGION_DRAW,
(bala_enemigo[actual].x - scroll.x0) - bala_enemigo[actual].tamano,
(bala_enemigo[actual].y - scroll.y0) - bala_enemigo[actual].tamano,
(bala_enemigo[actual].x - scroll.x0) + bala_enemigo[actual].tamano,
(bala_enemigo[actual].y - scroll.y0) + bala_enemigo[actual].tamano);

// Avanzar anterior solo si no eliminamos
anterior = actual;
END

// Avanzar al siguiente
actual = siguiente;
END

FRAME; // Esperar al siguiente frame
END
END

 

     Por último tenemos el ejemplo 10, el que contiene el juego acabado. Aquí creamos el enemigo de tipo 3, añadimos un ranking por puntuación, cargamos cinemáticas entre niveles y añadimos efectos de sonido y música (https://github.com/LeHamsterRuso/DIV2Examples/blob/main/PRG/VSHOOT10.PRG). Este es el que presenta mayor dificultad de todos, ya que si haceís un diff con el anterior veréis que cambia casi por completo.

 Cosas a tener en en cuenta:

- El sistema de estructuras para las cinemáticas.

// Estructuras de datos para cinematicas
STRUCT cinematics[10]
INT num_cg;
INT siguiente_nivel;
STRUCT cgs[5]
STRING fpg_ruta;
STRING pal_ruta;
STRING wav_ruta;
INT num_paginas;
STRUCT paginas[10]
STRING linea1[50];
STRING linea2[50];
STRING linea3[50];
STRING linea4[50];
END
END
END

 - Las funciones ligadas al ranking:

//------------------------------------------------------------------------------
// FUNCION: cargar_top_scores
// DESCRIPCION: Carga las 10 mejores puntuaciones desde el archivo TOP.DAT
// Si el archivo no existe, inicializa con valores por defecto
// (1000, 900, 800, 700, 600, 500, 400, 300, 200, 100)
//------------------------------------------------------------------------------
FUNCTION cargar_top_scores();
PRIVATE
INT file_handle;
INT data_size;
INT i;
BEGIN
// Intentar abrir el archivo para lectura
file_handle = fopen("TOP.DAT", "r");

IF (file_handle == 0)
// El archivo no existe, inicializar con valores por defecto
FROM i = 0 TO 9;
top_scores[i].score = 1000 - (i * 100);
END
RETURN;
END

// Leer la estructura completa de puntuaciones
data_size = sizeof(top_scores);
fread(OFFSET top_scores, data_size, file_handle);

// Cerrar el archivo
fclose(file_handle);
END

//------------------------------------------------------------------------------
// FUNCION: guardar_top_scores
// DESCRIPCION: Guarda las 10 mejores puntuaciones en el archivo TOP.DAT
//------------------------------------------------------------------------------
FUNCTION guardar_top_scores();
PRIVATE
INT file_handle;
INT data_size;
BEGIN
// Abrir el archivo para escritura (crea o sobrescribe)
file_handle = fopen("TOP.DAT", "w");

IF (file_handle == 0)
// Error al crear el archivo
RETURN;
END

// Escribir la estructura completa de puntuaciones
data_size = sizeof(top_scores);
fwrite(OFFSET top_scores, data_size, file_handle);

// Cerrar el archivo
fclose(file_handle);
END

//------------------------------------------------------------------------------
// FUNCION: actualizar_top_scores
// DESCRIPCION: Actualiza el ranking de puntuaciones con una nueva puntuacion
// Inserta la puntuacion en la posicion correcta si esta entre las 10 mejores
// PARAMETROS:
// nueva_puntuacion - La puntuacion a comparar con el top 10
//------------------------------------------------------------------------------
FUNCTION actualizar_top_scores(INT nueva_puntuacion);
PRIVATE
INT i;
INT j;
INT posicion_insercion;
BEGIN
posicion_insercion = -1;

// Buscar la posicion donde insertar la nueva puntuacion
FROM i = 0 TO 9;
IF (nueva_puntuacion > top_scores[i].score)
posicion_insercion = i;
BREAK;
END
END

// Si la puntuacion esta en el top 10, insertarla
IF (posicion_insercion != -1)
// Desplazar las puntuaciones inferiores hacia abajo (de abajo hacia arriba)
FOR (j=9; j>posicion_insercion; j--)
top_scores[j].score = top_scores[j-1].score;
END

// Insertar la nueva puntuacion
top_scores[posicion_insercion].score = nueva_puntuacion;
END
END

//------------------------------------------------------------------------------
// FUNCION: mostrar_top_scores
// DESCRIPCION: Muestra las 10 mejores puntuaciones en la parte superior izquierda
// de la pantalla del menu principal
//------------------------------------------------------------------------------
FUNCTION mostrar_top_scores();
PRIVATE
INT i;
INT pos_y;
STRUCT ranking_lines[10]
STRING line;
END
BEGIN
// Titulo del ranking
write(0, ANCHO_PANTALLA - 10, 10, 5, "RANKING:"); // 5 = alineacion izquierda

// Preparar las 10 lineas del ranking
FOR (i=0; i<10; i++)
ranking_lines[i].line = itoa(i + 1) + ". " + itoa(top_scores[i].score);
END

// Mostrar cada puntuacion
FOR (i=0; i<10; i++)
pos_y = 20 + (i * 10); // Espaciado de 10 pixels entre lineas
write(0, ANCHO_PANTALLA - 10, pos_y, 5, ranking_lines[i].line); // 5 = alineacion izquierda
END
END

 

     Sobre la música, se tratan de canciones generadas con SUNO (una plataforma de música generada por IA) que he convertido a wav. En mi caso tengo un mac y esto me permite hacerlo con un simple comando:

 afconvert -f WAVE -d UI8@11025 -c 2 CANCION.MP3 CANCION.WAV

     Este comando lo que hace es generar un wav en sonido estéreo a 11025Hz. Es una calidad bastante mala para los estándares actuales, pero me permite tener canciones de menos de 3 MB (el formato wav abulta mucho y las máquinas con MS-DOS no pueden permitirse malgastar RAM).

       Y para finalizar, aquí os dejo el gameplay completo del juego:


   Prometo que intentaré que el próximo capítulo sea de un juego más fácil y menos complicado de programar.

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.