Moviendo enemigos en una pantalla con tiles
- MoltS Xalats
- 30 jun
- 7 Min. de lectura

Introducción
En el artículo "Scroll en una pantalla de casillas" se muestra cómo podemos crear un scroll a partir de una pantalla con casillas. Ahora, además de mover al personaje principal, crearemos una serie de enemigos que irán apareciendo desde uno de los 4 lados de la pantalla y detectarán si colisionan con el fondo. Si colisionan, rebotarán en la dirección opuesta.
Los sprites del MSX pueden ocupar 32 planos, es decir, solo puede haber 32 sprites a la vez. Como es un número finito y los sprites se van creando de forma aleatoria, había pensado controlar este recurso a través de una pila, de modo que cada vez que un sprite se elimina de la pantalla, el siguiente pueda tomar el número de plano que esté disponible.
Otra técnica que necesitaremos es el azar, para que los sprites aparezcan en posiciones diferentes.
Qué es una pila?
Una pila (o stack en inglés) es una estructura de datos lineal que sigue el principio LIFO (Last-In, First-Out), lo que significa que el último elemento que se añade es el primero que se extrae. Imagínala como una pila de platos: solo puedes añadir o quitar un plato de la parte superior (o el desastre puede ser histórico).
Esta estructura se define principalmente por dos operaciones básicas:
Push (Apilar): Añade un nuevo elemento en la parte superior de la pila.
Pop (Extraer): Elimina y devuelve el elemento que se encuentra en la parte superior de la pila.
En nuestro programa se ha definido la estructura Stack que está formada por un array con los datos y un valor que indica la posición superior de la pila:
typedef struct{
char data[MAX_PLANS_SPRITE];
char top;
} Stack;
Con esta estructura, reservamos la memoria para un array de MAX_PLANS_SPRITE elementos y tenemos una variable que nos va indicando cuántos elementos hay dentro de la pila. Partiendo de esta estructura, podemos crear la función push que incrementará el valor de top y guardará el elemento que se le pasa por parámetro en la posición correspondiente del array:
void push (Stack *stack, char value) {
stack->top++;
stack->data[stack->top] = value;
}
Y la función pop que recupera el último elemento que hay en la pila y decrementa top para que así apunte de nuevo al último elemento:
char pop(Stack *stack) {
char val_ret = stack->data[stack->top];
stack->top--;
return val_ret;
}
Con estas dos funciones, ya podemos operar la pila de los planos de los sprites, colocando un elemento cuando eliminamos un sprite y extrayendo otro cuando lo recuperamos.
Números aleatorios
Para que el juego tenga variabilidad y que cada vez que lo juegues sea diferente, utilizaremos números aleatorios.
Un número aleatorio es aquel que se genera de forma que su valor no se puede predecir antes de generarse, y cada valor posible tiene una cierta probabilidad de aparecer. En el ideal matemático, esta generación debería ser totalmente imprevisible y sin ningún patrón repetitivo.
En computación, sin embargo, esta idealización es casi imposible de lograr, ya que los ordenadores son máquinas deterministas: dada una misma entrada, siempre producen el mismo resultado. Por eso en el caso de la informática hablamos de números pseudoaleatorios, que son algoritmos que generan una secuencia de números que parecen aleatorios pero que son completamente deterministas. Si inicializamos el generador con la misma semilla (seed), la secuencia será exactamente la misma. Esto permite que se pueda repetir, pero tiene el inconveniente de que puede aparecer algún patrón de repetición.
En Fusion-C existe la función Rnd(unsigned char seed), pero una vez que la utilicé no me gustaron los resultados (quizá no supe aplicarla suficientemente bien) y busqué en los foros de SDCC y encontré esta función:
int rand_xor() {
num_llarg_aleatori = num_llarg_aleatori ^ (num_llarg_aleatori << 13);
num_llarg_aleatori = num_llarg_aleatori ^ (num_llarg_aleatori >> 17);
num_llarg_aleatori = num_llarg_aleatori ^ (num_llarg_aleatori << 5);
return (num_llarg_aleatori & 0x7fff);
}
que utiliza un unsigned long (4 bytes) para ir generando los números haciendo desplazamientos de bits y operándolos con XOR. Para inicializar la serie de números de apariencia aleatoria, utilizo una semilla que depende de la hora del MSX:
void rand_xor_init() {
num_llarg_aleatori = (_Time << 15) | (unsigned long)_Time;
}
De este modo añadimos más variabilidad a la generación de números aleatorios. A la hora de depurar, quizá no nos interesa tanto la variabilidad y queremos repetibilidad, entonces lo mejor es comenzar con un num_llarg_aleatori que sea siempre el mismo.
Esta implementación se conoce como XORShift y fue propuesta por George Marsaglia en 2003. Como utilizamos 32 bits, el periodo máximo que podemos conseguir es 2^32. Es decir, como máximo habremos generado 2^32 números antes de que la secuencia vuelva a repetirse. Este periodo dependerá del número inicial y de los desplazamientos (shift) que hacemos en el algoritmo.
Añadimos enemigos a scroll con casillas
Una vez entendemos cómo funciona una pila y la generación de números aleatorios, añadiremos los enemigos al programa que trabajamos en el artículo "Scroll en una pantalla de casillas". Lo primero que definimos es la estructura de los enemigos, donde tendremos agrupadas las variables necesarias para su control. Esta estructura tendrá x e y para saber su posición; speed_x y speed_y que indicarán la cantidad de píxeles que se mueven en cada uno de estos ejes; tamany para poder calcular, según su forma, si colisionan o no con los obstáculos; eliminar para marcar los que han salido de la pantalla y deben eliminarse; pintar si es necesario dibujar este enemigo; y finalmente num_pla_sprite que indica cuál de los planos debe utilizarse para dibujarlo.
Después definimos el número máximo de enemigos que habrá en pantalla con la constante NUM_ESQUIROLS y a continuación reservamos el espacio de memoria para la repetición de esta estructura.

Definimos la forma que tendrá el sprite de los enemigos en la línea 146. Después definimos la pila con la estructura que hemos explicado antes, definiendo MAX_PLANS_SPRITE como el número máximo de sprites que permitiremos que utilicen los enemigos.
Después de las funciones de la pila, definimos las funciones para generar los números pseudoaleatorios: una función inicial rand_xor_init() que toma el valor del reloj del MSX como semilla, y rand_xor() que cada vez que se llama devuelve un entero (16 bits) diferente.

Otra función que debemos añadir es crea_velocitat_esquirols(char num_esquirol), que genera un número aleatorio entre 0 y 2 para los dos ejes de movimiento del enemigo, comprueba que no sea 0,0 (ya que el enemigo no se movería y quedaría estático), y en ese caso, en lugar de volver a generar los números de forma aleatoria, lo cual tiene un coste computacional, hemos asignado siempre el valor (-1,-1), que tiene un coste más bajo.

A la hora de inicializar la pantalla debemos añadir la inicialización de la pila de las líneas 146-149 y todo el array de las estructuras de los enemigos con sus valores.

Como tenemos que desplazar a estos enemigos por la pantalla, tendremos que calcular sus nuevas posiciones en cada interrupción de reloj, tal y como hacemos con el personaje principal. Por eso hemos definido las funciones esParet_amunt, esParet_avall, esParet_dreta y esParet_esquerra, que detectan si las coordenadas de la casilla que se les pasa corresponden a un tipo de pared o no. La traducción es la misma que se utiliza en el artículo "Scroll en una pantalla de casillas" para el personaje principal. Como realizan la misma funcionalidad que el código que había para el personaje principal, también hemos sustituido ese código por estas funciones.

La función esquirol_col_lisiona_amb_paret es la que traduce las coordenadas x,y y la posición del enemigo dentro del array de enemigos y, utilizando las funciones anteriores, determina si se puede avanzar o no.

Finalmente queda la función que recorre todo el array de enemigos y determina si se deben crear nuevos, si desaparecen o si han chocado contra una pared. Todo esto está definido en la función actualitza_pos_esquirols.
Esta función contiene el bucle para todos los enemigos y, lo primero de todo, comprueba si el enemigo debe eliminarse. Si es así, se vuelve a crear un nuevo enemigo comprobando desde qué lado debe aparecer y se genera de nuevo, eligiendo al azar uno de los cuatro lados y también generando aleatoriamente la velocidad del enemigo. He elegido uno de los cuatro lados porque no me gustaba que aparecieran de repente en medio de la pantalla. Después comprobamos si la posición en la que se ha creado corresponde a una casilla de tipo pared, ya que en ese caso no podría moverse y debe volver a generarse.
Si el enemigo no debe eliminarse, debemos calcular el desplazamiento según la velocidad y la función anterior, esquirol_col_lisiona_amb_paret, para ver en qué lugar se encontrará el enemigo en el siguiente frame. Además de comprobar si ha colisionado, también tenemos que averiguar si sus coordenadas lo han llevado a salir fuera de la cámara (zona visualizada por la pantalla) y, si es así, marcarlo para que sea eliminado y creado de nuevo en la siguiente iteración, frame.


Conclusiones
En este artículo hemos añadido una gestión básica de enemigos con movimiento y colisiones dentro de un entorno de scroll por casillas para MSX. Para gestionar el límite de 32 planos de sprites, se ha optado por una pila como método para controlar qué plano está disponible, aunque en la práctica quizá no sería estrictamente necesario: a menudo bastaría con reutilizar directamente el plano de un sprite eliminado en el momento de crear uno nuevo. No obstante, la pila facilita el control y nos sirve para conocer esta estructura en caso de que queramos utilizarla en otros proyectos.
En cuanto a la generación de enemigos, inicialmente se planteó que aparecieran desde uno de los cuatro lados de la pantalla. Esta decisión se tomó para evitar la aparición repentina de enemigos en el centro del mapa, aunque quizá podría estudiarse mejor, ya que en realidad solo son dos lados, dado que hay continuidad de un lado al otro de la cámara.
Estas modificaciones se pueden encontrar en nuestro GitLab en el archivo enemScFo.c. Y como siempre, podéis ver el ejemplo en acción.
Haz clic aquí para ver el ejemplo en funcionamiento.
Comments