Movent enemics en una pantalla amb tiles
- MoltS Xalats
- Jun 30
- 7 min de lectura

Introducció
A l'article "Desplaçant pantalla (scroll) en una pantalla de caselles" es mostra com podem crear un scroll a partir d'una pantalla amb caselles. Ara, a part de moure el personatge principal, creerem una sèrie d'enemics que aniran apareixent des d'un dels 4 costats de la pantalla i detectaran si col·lisionen amb el fons. Si col·lisionen rebotaran en la direcció oposada.
Els sprites del MSX poden ocupar 32 plans, és a dir, només hi poden haver 32 sprites a la vegada. Com que és un número finit i els sprites es van creant de forma aleatòria, havia pensat de controlar aquest recurs a través d'una pila i així cada cop que un sprite s'elimina de la pantalla el proper pot agafar el número de pla que estigui disponible.
Una altra tècnica que necessitarem és l'atzar, perquè els sprites apareguin en posicions diferents.
Què és una pila?
Una pila (o stack en anglès) és una estructura de dades lineal que segueix el principi LIFO (Last-In, First-Out), que significa que l'últim element que s'hi afegeix és el primer que se'n treu. Imagina-la com una pila de plats: només pots afegir o treure un plat de la part superior (o la destrossa pot ser històrica).
Aquesta estructura es defineix principalment per dues operacions bàsiques:
Push (Apilar): Afegeix un nou element a la part superior de la pila.
Pop (Treure): Elimina i retorna l'element que es troba a la part superior de la pila.
En el nostre programa s'ha definit l'estructura Stack que està formada per un array amb les dades i un valor que indica la posició de dalt de la pila:
typedef struct{
char data[MAX_PLANS_SPRITE];
char top;
} Stack;
amb aquesta estructura, reservem la memòria per un array de MAX_PLANS_SPRITE elements i tenim una variable que ens va indicant quants elements hi ha dins de la pila. Partint d'aquesta estructura, podem crear la funció push que incrementarà el valor de top i guardarà l'element que se li passa per paràmetre a la posició de l'array corresponent:
void push (Stack *stack, char value) {
stack->top++;
stack->data[stack->top] = value;
}
i la funció pop que recupera l'últim element que hi ha a la pila i decrementa top perquè així apunti de nou a l'últim element:
char pop(Stack *stack) {
char val_ret = stack->data[stack->top];
stack->top --
return val_ret;
}
Amb aquestes dues funcions, ja podem operar la pila dels plans dels sprites, posant un element quan eliminem un sprite i traient-ne un altre quan el recuperem.
Números aleatoris
Per tal que el joc tingui variabilitat i que cada vegada que el juguis sigui diferent, utilitzarem números aleatoris.
Un nombre aleatori és aquell que es genera de manera que el seu valor no es pot predir abans de generar-se, i cada valor possible té una certa probabilitat d’aparèixer. En l’ideal matemàtic, aquesta generació hauria de ser totalment imprevisible i sense cap patró repetitiu.
En computació, però, aquesta idealització és gairebé impossible d’aconseguir, ja que els ordinadors són màquines deterministes: donada una mateixa entrada, sempre produeixen el mateix resultat. Per això en el cas de la informàtica parlem de números pseudoaleatoris, que són algorismes que generen una seqüència de nombres que semblen aleatoris però que són completament deterministes. Si inicialitzem el generador amb la mateixa llavor (seed), la seqüència serà exactament la mateixa. Això permet que es pugui repetir però té l'inconvenient que pot aparèixer algun patró de repetició.
En el Fusion-c hi ha la funció Rnd(unsigned char seed), però un cop que la vaig utilitzar no em van agradar els resultats (potser no la sabia aplicar prou bé) i vaig buscar en els forums del sdcc i vaig trobar aquesta funció:
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 utilitza un unsigned long (4 bytes) per anar generant els números fent un desplaçament de bits i operant-los amb xor. Per inicialitzar la sèrie de números de semblança aleatòria, utilitzo una llavor que depèn de l'hora del MSX:
void rand_xor_init() {
num_llarg_aleatori = (_Time << 15) | (unsigned long)_Time;
}
d'aquesta manera hi afegim una variabilitat més a la generació de números aleatoris. A l'hora de debugar potser no ens interessa tant la variabilitat i volem repetibilitat, aleshores el millor és començar amb un num_llarg_aleatori que sigui sempre el mateix.
Aquesta implementació es coneix com a XORShift i va ser proposada per George Marsaglia al 2003. Com que utilitzem 32 bits, el període màxim que podem aconseguir és 2^32. És a dir, com a màxim haurem geneart 2^32 nombres abans que la seqüència es torni a repetir. Aquest període dependrà del número inicial i dels desplaçaments (shift) que fem a l'algoritme.
Afegim enemics a moviment de scroll amb caselles
Un cop entenem com funciona una pila i la generació de números aleatoris, afegirem els enemics al programa que vam treballar a l'article "Desplaçant pantalla (scroll) en una pantalla de caselles". El primer que definim és l'estructura dels enemics, a on tindrem les variables agrupades necessàries per al seu control. Aquesta estructura tindrà x i y per saber la seva posició; speed_x i speed_y que tindran la quantitat de pixels que es mouen en cadascun d'aquests eixos; tamany per poder calcular segons la seva forma si col·lisionen o no amb els obstacles; eliminar per marcar els que han sortit de la pantalla i s'han d'eliminar; pintar si són necessari que es pinti aquest enemic; i finalment el num_pla_sprite que indica quin dels plans s'ha d'utilitzar per pintar.
Després definim el número màxim d'enemics que hi haurà en pantalla amb la constant NUM_ESQUIROLS i tot seguit reservem l'espai de la memòria per la repetició d'aquesta estructura.

Definim la forma que tindrà l'sprite dels enemics a la línia 146. Després definim la pila amb l'estructura que hem explicat abans, definint MAX_PLANS_SPRITE com el número màxim d'sprites que deixarem que utilitzin els enemics.
Després de les funcions de la pila, definim les funcions per generar els números pseudoaleatoris, una funció inicial rand_xor_init() que agafa el valor del rellotge del MSX com a llavor, i rand_xor() que cada cop que la crides retorna un enter (16 bits) diferent.

Una altra funció que hem d'afegir és crea_velocitat_esquirols( char num_esquirol ) que genera un número aleatori entre 0 i 2 per als dos eixos de moviment de l'enemic, comprova que no sigui 0,0 (ja que l'enemic no es mouria i estaria estàtic), en aquest cas, en comptes de tornar a generar els números de forma aleatòria que té un cost computacional, hem posat sempre el valor (-1,-1) que té un cost més baix.

A l'hora d'inicialitzar la pantalla hem d'afegir la inicialització de la pila de les línies 146-149 i tot l'array de les estructures dels enemics amb els seus valors.

Com que hem de desplaçar aquests enemics per la pantalla, haurem de calcular les seves noves posicions a cada interrupció de rellotge, tal i com fem amb el personatge principal. Per això hem definit les funcions esParet_amunt, esParet_avall, esParet_dreta i esParet_esquerra que detecten si les coordenades de la casella que se li passen corresponen a tipus de paret o no. La traducció és la mateixa que s'utilitza a l'article "Desplaçant pantalla (scroll) en una pantalla de caselles" per al caràcter principal. Com que fan la mateixa funcionalitat que el codi que hi havia per al personatge principal, també hem substituït el codi per aquestes funcions.

La funció esquirol_col_lisiona_amb_paret és la que tradueix les coordenades x,y i la posició de l'enemic dins de l'array d'enemics i utilitzant les funcions anteriors, determina si es pot avançar o no.

Finalment queda la funció que escombra tot l'array d'enemics i determina si s'han de crear de nous, si desapareixen o si han xocat contra una paret. Tot això està definit a la funció actualitza_pos_esquirols.
Aquesta funció conté el bucle per tots els enemics i primer de tot mira si l'enemic s'ha d'eliminar. Si és així, torna a crear un nou enemic mirant des de quin costat s'ha de crear i el fa de nou, generant a l'atzar un dels quatre costats i també fent a l'atzar la velocitat de l'enemic. He posat un dels quatre costats, perquè no m'agradava que apareguessin de sobte en mig de la pantalla. Després comprovem si a la posició que s'ha creat correspon a una casella de tipus paret, ja que si és així, no es podria moure i s'ha de generar de nou.
Si no s'ha d'eliminar l'enemic, hem de calcular el desplaçament segons la velocitat i la funció anterior, esquirol_col_lisiona_amb_paret, per veure a quin lloc es trobarà l'enemic al següent frame. A part de si ha col·lisionat, també hem d'esbrinar si les seves coordeandes l'han portat a sortir fora de la càmera (zona visualitzada per la pantalla) i si és així, marcar-lo perquè sigui eliminat i creat de nou a la propera iteració, frame.


Conclusions
En aquest article hem afegit una gestió bàsica d’enemics amb moviment i col·lisions dins d’un entorn de scroll per caselles per al MSX. Per gestionar el límit de 32 plans de sprites, s’ha optat per una pila com a mètode per controlar quin pla està disponible, tot i que a la pràctica potser no seria estrictament necessari: sovint n’hi hauria prou amb reutilitzar directament el pla d’un sprite eliminat en el moment de crear-ne un de nou. No obstant això, la pila facilita el control i ens serveix per conèixer aquesta estructura en cas que la volguem utilitzar en altres projectes.
Pel que fa a la generació dels enemics, inicialment es va plantejar que apareguessin des d’un dels quatre costats de la pantalla. Aquesta decisió es va prendre per evitar l’aparició sobtada d’enemics al centre del mapa, tot i que potser es podria estudiar millor, ja que realment només són dos costats, ja que hi ha continuïtat d'un costat a l'altra de la càmera.
Aquestes modificacions es poden trobar al nostre gitlab al fitxer enemScFo.c. I com sempre podeu trobar l'exemple en acció.
Clica aquí per a veure l'exemple funcionant.
Commenti