top of page

Desplaçant pantalla (scroll) en una pantalla de caselles

Objectiu

En un article anterior (Movent-nos per pantalla de caselles), havíem vist com crear una pantalla amb un mapa de caselles i com desplaçàvem un sprite i detectava les caselles que no podia travessar. Però què passa si el nostre mapa de caselles és més gran que les caselles que caben en pantalla? Una solució molt utilitzada en MSX és borrar la pantalla de nou i tornar a carregar la nova part que es visualitza. Una altra solució és anar desplaçant la pantalla, fent un scroll. Aquesta última és la que desenvoluparem en aquest article. En aquest vídeo podeu veure el resultat final.


Tipus de scroll en MSX

Els MSX1 utilitzen el VDP de Texas Instruments TMS9918 (o un clon d'ell). Aquest VDP no disposa de scroll, s'havia de fer per software. La següent generació utilitza el V9938 de Yamaha que incloïa scroll vertical i a la tercera, els MSX2+, ja trobem també l'scroll horitzontal amb el V9958.


Com s'ho feien doncs els MSX1 per fer scroll, com el del knightmare? Havien de fer un scroll per software. A cada refresc de pantalla han de tornar-la a dibuixar prou ràpid amb el desplaçament, i en la majoria de jocs sembla que avanci a trompicons, com es pot apreciar en aquest video del Knightmare de l'Araubi. En aquest article d'en Grauw s'explica com funciona l'scroll per software en els diferents MSX. Nosaltres, provarem les facilitats que ens dóna el 2+ per fer el nostre scroll.


Scroll hardware V9958 (MSX2+)

El xip de vídeo dels 2+ (i turbo-R) afegeix 3 registres nous per controlar l'scroll horitzontal que són #25, #26, #27, tal i com està explicat a la documentació de Yamaha.


El registre #25 és el que configura el tipus de scroll en els bits més baixos. El b0, anomenat SP2, val 0 si l'scroll és d'una sola pàgina o de dues. En una sola pàgina, la part de la pantalla de l'esquerra que no es visualitza, apareix a la dreta.

Si SP2 val 1, l'scroll és en dues pàgines, a mesura que va desapareixent la primera es va omplint per l'altre costat amb la segona.


L'altre bit de configuració, el b1, anomenat MSK, quan està a 0 els 8 punts de l'esquerra no estan emmascarats, es veuen. Quan està a 1, els 8 punts queden ocults. Això s'utilitza, perquè durant l'scroll, els 8 punts de la part esquerra tenen un valor incert durant l'scroll, d'aquesta manera es tapen i no es veuen.


En el Fusion-C, les comandes per fer l'scroll són SetScrollDouble(char n), SetScrollMask(char n), SetScrollH(int n), SetScrollV(char n). Les dues primeres serveixen per configurar el registre #25. Si passem el valor 1 a SetScrollDouble, la següent pàgina de la VRAM es posa a continuació de la pàgina activa. Amb el valor 0 l'scroll es fa només d'una pàgina. Amb SetScrollMask, si passem un 1, amaguem els 8 pixels de l'esquerra, amb un 0 es visualitzen. Les altres dues mouen la pantalla, fan l'scroll la quantitat de pixels indicats de la imatge original.


Per tenir un primer contacte amb aquestes funcions, anem a veure amb un petit programa que carrega 4 imatges a les 4 pàgines de screen5 i fa proves amb les anteriors funcions. Aquest programa el podeu trobar al GitLab de MoltSXalats.



Primer de tot comencem escrivint els includes i definim les variables per carregar les imatges i les paletes tal i com està explicat a l'article "Carrega imatges al MSX".


Continuem definint les funcions per llegir els fitxers del disquet:



I comencem a definir la funció principal del programa:



Fins la línia 156, canviem el color del text i passem a screen 5 per carregar les diferents imatges i paletes. Les imatges que tenim a cada pàgina tal i com indica el debugger són:




El primer que provarem és desplaçar la pàgina 2 de la VRAM, 125 píxels a la dreta i 15 píxels avall amb el bloc de 8 píxels de l'esquerra amagats i esperem una tecla a ser premuda. Per pantalla obtenim:



Ara fem el mateix però amb el bloc de 8 pixels visible per la pàgina 0:



El fet d'amagar o no els 8 pixels en un desplaçament espontani, sense que vagi variant, només fa que tinguem 8 pixels menys d'imatge.


Continuem amb el programa:



Ara fem un scroll concatenat (SetScrollDouble(1)) de la pàgina 2, les línies 198-209:



I què passa si la pàgina visual és la 3, quina enganxa pel darrere? Doncs enganxa la pàgina 0 com es pot veure en el següent exemple de codi, resultant en aquesta imatge:



L'única diferència que hem pogut apreciar ocultant o no el bloc dels primers 8 píxels és que perdem part de la imatge que queda fosca. Però què passa si fem un scroll d'un en un? Què és el que veiem en aquest bloc de 8 píxels? Això és el que hem intentat resoldre amb les línies de codi de la 228 a la 243.

El que passa en aquest bloc de 8 pixels és que van fent pampallugues cada 8 pixels, es va desplaçant, però quan arriben al final es posa negre de nou.

Mentre que si tenim el bloc amagat, tal i com es fa a les línies 246-254, veiem que la part visible es desplaça suaument, sense fer pampallugues a la part de més a l'esquerra.



Finalment, ens queda preguntar-nos, què passa si li diem un scroll de més de 256 píxels? És el que intentem respondre en l'última activitat (línia 256) i com es pot veure torna a començar. Entenc que el hardware accepta valors fins a 512, pel cas que fem un scroll de l'screen 7, però que aplicat a 256 el que fa és tornar a començar.



Finalment tornem a l'Screen 0, restaurem la paleta i sortim.


Si durant algun dels exemples, obriu el debugger de l'openMSX, veureu les imatges tal i com estan en memòria, sense desplaçament, aquest desplaçament dels registres del V9958 només afecta a l'hora de pintar la pantalla, les posicions de la VRAM no han variat. Amb el que si hem de copiar trossos de memòria VRAM usant les coordenades, aquestes coordenades han d'estar referenciades a la pàgina original, la que està en el debugger, no a les coordenades que veiem en pantalla.


Una última cosa que m'agradaria comentar, és que el SpaceManbow utilitza el V9958, en aquest video del yotube es pot veure la diferència entre el V9958 i el V9938 i s'aprecia l'scroll suau que fa el V9958.


Scroll de pantalla per caselles

Un cop que ja hem trastejat el que pot fer el V9958, mirem d'aplicar-ho al mapa de caselles que teníem de l'anterior article. El que volem aconseguir és que quan el caràcter principal es desplaci cap a la vora de la pantalla aparegui el següent bloc de caselles desplaçant totes les altres i que a la vegada, quan es desplaci enllà de les vores, per la zona permesa, i es trobi una casella que no pot creuar, que no la creuï tal i com feia en el mapa de caselles.


Per crear-ho s'ha optat per fer l'scroll en una sola pantalla i anar dibuixant segons ens anem desplaçant les noves caselles, utilitzant els 8 pixels del bloc que queda amagat i els pixels que estan més enllà de la línia 212.


Anem a explicar el detall el codi:



Comencem amb els includes que utilitzarem i les variables que usem per carregar les imatges i la paleta. Aleshores definim les dues variables que ens indicaran quina és la casella del mapa que s'està pintant en aquests moments a dalt a l'esquerra de la pantalla, map_tile_x i map_tile_y. Les variables x i y són les coordenades del personatge, de l'sprite que controlem i es mou per la pantalla.


Tot seguit tenim les constants de les tecles i de les coordenades de la pàgina de memòria de VRAM. Després tenim NOMBRE_RAJOLES_HOR i NOMBRE_RAJOLES_VER que és la dimensió del mapa de caselles definit en map1. Les constants NOMBRE_RAJOLES_HOR_ORIGEN_PATRONS són el nombre de rajoles que acaben en una pantalla horitzontal, i NOMBRE_RAJOLES_PANTALLA_HOR que conté el nombre de rajoles que nosaltres pintarem, ja que n'hi ha una que és l'oculta i que nosaltres anem pintant. Les següents són les equivalents en vertical: NOMBRE_RAJOLES_PANTALLA_VER que són les que es visualitzen, i NOMBRE_RAJOLES_PANTALLA_VER_SCROLL que són les que utilitzem per fer l'scroll, les que hi caben en tota la pàgina que va rotant.


Per acabar tenim la variable constant map1 que conté la informació del tipus de cada casella, igual que en l'article del mapa de caselles, però aquest cop molt més gran que el que hi cap en una visualització de pantalla. Aquí l'he definit directament en el codi, però si féssim paginació i volguéssim tenir-ho independent del codi, podríem definir la variable a una adreça char map __at 0x???? i després amb la càrrega accedir al fitxer i carregar-ho a aquella adreça de memòria. En ser tan llarga la variable, ocupa moltes captures de pantalla:



Un cop hem definit tot el mapa, definim un sprite que és el que conduirem per la pantalla i tenim després totes les funcions per carregar les imatges del disquet.



Després tenim la interrupció de VDP que com a l'article de caselles utilitzarem per disparar una bandera que ens indicarà que hem d'analitzar tota la lògica del moviment.



Passem ara a pintar la pantalla del joc, tal i com vam fer a l'article anterior: stamp_x i stamp_y tindran les coordenades a on tenim les imatges de les caselles que formaran el mapa, depenent del tipus de casella retornaran unes coordenades o unes altres per a poder ser pintades.


La funció obtenir_coordenades_rajola(map_x,map_y) retorna quin tipus de rajola ha d'anar a les coordenades que li hem passat.


Després pintem la pantalla de joc a init_pantalla_joc() carregant la imatge amb els patrons i pintant-la tota. Creem l'sprite i posem la resta a la coordenada 255 perquè no apareguin. He llegit aquest article al msx.org mentre estava escrivint aquest post, i he pensat que en comptes de la línia 255 que té el problema que quan puja l'scroll apareixen, de posar-los als 8 pixels que queden amagats de l'scroll horitzontal, i que aquests no es mouen.



Tot seguit tenim les variables que utilitzarem per controlar l'scroll. pos_scroll_x i pos_scroll_y guardaran la posició de l'scroll en l'eix horitzontal i el vertical respectivament. Usarem uns flags per determinar si encara hem de fer scroll o ja som al límit del mapa que són les variables fer_scroll_lateral i fer_scroll_vertical. A la línia 341 tenim les variables desti_x i desti_y que indicaran a la posició que hem de pintar la nova línia de caselles de 8x8. Aquestes variables estan definides com a char per poder fer l'operació sempre amb mòdul 256, ja que si fem sumes i ens passem, sempre agafa els 8 bits menys significatius. Com que l'scroll vertical i horitzontal (ja que no fem servir les dues pàgines lligades) és també de 256.


I comencem amb la primera funció per pintar l'scroll, que és la del moviment cap amunt, scroll_amunt(). El primer que fem és mirar si ja hem arribat al límit del mapa, que no hi som, doncs podem fer l'scroll vertical activant el flag fer_scroll_vertical.


A la línia 348 tenim el codi de fer l'scroll, primer de tot decrementem la variable pos_scroll_y per pujar una línia i mirem si estem a la posició de tornar a pintar una línia a la pantalla, això és cada 8 pixels i per tant comprovem que els 3 bits de menys pes siguin 0. Si hem de pintar una nova línia hem de dir al mapa que hem canviat de posició i per tant decrementem map_tile_y. El següent que hem de mirar en quina posició està l'scroll per poder saber quina posició de les coordenades de la pàgina estem veient. Això ho guardem a desti_y, que és el valor de l'scroll menys els 8 pixels que anem a pintar. El bucle de pintar s'ha de fer sobre l'eix horitzontal, per tant desti_x dependrà de la variable del bucle (n) i de la posició de l'scroll. El primer que fem és passar les coordenades en pixels a les coordenades del mapa (blocs de 8) dividint entre 8, agafant la part entera, que és el mateix que desplaçar 3 bits a la dreta, li sumem la variable del loop que està en caselles i ho tornem a passar en coordenades, ara multiplicant per 8, però seria més òptim desplaçant ara a la l'esquerra els 3 bits. Obtenim les coordenades del mapa de caselles i copiem la casella patró a les posicions calculcades de desti_x i desti_y.


A la línia 362 tornem a restar una posició de l'scroll vertical. Aquesta línia l'he trobada empíricament, si el caràcter principal s'aturava just en pintar aquesta línia i no continuava amunt, sinó que canviava de sentit, el següent mapa a pintar no es feia bé.

Acabats aquests casos, passem a fer l'scroll vertical cridant a la funció SetScrollV().


I com a última part del codi mirem si amb les noves coordenades de l'scroll ens hem passat del mapa. Si és així, indiquem al flag que ja no es fa l'scroll i reduïm la posició de la pantalla per tornar a la que ja hi havia.



Ara passem a fer el mateix per l'scroll avall, primer detectem si ja no estem al final del mapa a la línia 371. Si toca pintar (línia 375-390) només canviem la part d'obtenir coordenades, ja que si fem servir la part de dalt a l'esquerra com a indicadors d'on som (map_x i map_y) hem d'afegir un offset per saber quina és la part d'abaix que és el que fem amb NOMBRE_RAJOLES_PANTALLA_VER-1. Finalment a la línia 391 comprovem que hàgim de posar la bandera de fer scroll a 0 (no fem scroll).



Comencem ara el primer dels scrolls laterals, el de l'esquerra que segueix la mateixa estructura que l'anterior però amb les condicions noves per detectar límits i a on pintar per l'scroll lateral. També canvia, que aquest cop el bucle no el fem sobre les X sinó que pintem tota una columna.


A la línia 400 detectem si podem fer scroll lateral i activem el flag. Després actualitzem la posició de l'scroll i a la línia 406 detectem si hem de pintar una nova casella del mapa, com que són caselles de 8x8 també farem el mateix que en el vertical i detectem els 3 bits de menys pes quan sigui 0 (cada 8 vegades). Si toca pintar una nova columna, actualitzem la posició del mapa horitzontal (map_tile_x) i calculem allà a on haurem d'anar a pintar, el desti_x és el que ve determinat per l'scroll, i el desti_y passem de pixels a caselles dividint enterament per 8 (desplaçant 3 bits a la dreta).


Entre les línies 411 a 422 fem el bucle de pintar verticalment. Aquí ho hem dividit en dues parts, de la posició a on estem cap avall i després el que queda que va al principi de la pantalla.


La línia 424 és el mateix que en el vertical, que s'ha trobat empíricament que si no pintava malament just quan el caràcter deixava aquest punt i anava cap a un altre lloc de l'scroll.

A la línia 426 es realitza l'scroll horitzontal amb la funció SetScrollH().


Finalment comprovem si estem a l'inici del mapa i si és així tornem enrere el canvi en la posició de l'scroll i activem la bandera per evitar més desplaçaments a l'esquerra.



Però no podem utilitzar un sol bucle per pintar verticalment com havíem fet en els scrolls verticals? Sí que es pot, sí que podem utilitzar el fet que el desbordament del char no ens afecta i torna a començar a comptar. El bucle quedaria com:


for (int m = 0; m < NOMBRE_RAJOLES_PANTALLA_VER_SCROLL ; m++) {
        obtenir_coordenades_rajola(map_tile_x, m + map_tile_y);
		desti_y = (m + (pos_scroll_y>>3)) * 8;
        HMMM(stamp_x, stamp_y, desti_x,
             OFFSET_COORDENADAY_PAGINA_ACTIVA_2 + desti_y, 8, 8);
      }

Passem ara a fer l'equivalent amb el de la dreta, a on només canvien els valors dels límits del mapa en les comprovacions 437 i 463; en comptes de decrementar map_tile_x i pos_scroll_x incrementen; i la posició d'obtenir les coordenades a pintar té l'offset de les 31 caselles.



Aquesta funció també es podria simplificar traient els dos bucles de pintar i substituir-los per:


for (int m = 0; m < NOMBRE_RAJOLES_PANTALLA_VER_SCROLL; m++) {
        obtenir_coordenades_rajola(map_tile_x + 31, m + map_tile_y);
   		desti_y = (m + (pos_scroll_y>>3)) * 8;
        HMMM(stamp_x, stamp_y, desti_x,
             OFFSET_COORDENADAY_PAGINA_ACTIVA_2 + desti_y, 8, 8);
      }

Un cop ja hem definit les noves funcions de l'scroll tenim ara les funcions de saber si la casella a on anirem és una casella que podem travessar o no. Això ho descobrim amb la funció es_casellaParet() que segons el tipus de casella retornarà un 1 si no es pot passar.


La següent funció, calculem_tiles(), ens indica a quina casella de la pantalla ens trobem depenent de l'scroll. El que fa és passar les coordenades a caselles. La coordenada del caràcter només és afectada pels 3 bits menys significatius de l'scroll per determinar si ha canviat de casella. Per tant, fem l'and (&) per discriminar els altres bits, ho sumem a la coordenada i ho dividim enterament entre 8 (desplaçant els 3 bits menys significatius a la dreta >>3).



La funció anterior és la que utilitzarem en les diferents funcions de moviment per saber a on estem del mapa de caselles. La primera funció de moviment que tenim és la d'anar cap amunt moviment_amunt() a on restem un a la coordenava vertical, averigüem la casella a on estem i investiguem si la que estem o l'anterior (per problemes d'arrodoniment tal i com vam explicar a l'article Movent-nos per pantalla de caselles hem de consultar dues caselles) és per poder passar o no. A la línia 506 comparem si les caselles que hem buscat són bloquejants, si és així retrocedim la posició del caràcter principal. Després a la línia 509 mirem si el personatge es troba per sobre un llindar (en aquest cas 14 pixels) a on a partir del qual farem l'scroll i retrocedim el personatge, ja que avança la pantalla.


La funció moviment_avall() fa el mateix però sumant a la coordenada Y en comptes de restar. La posició de les caselles també varia i s'ha ajustat empíricament, seguint el mètode de prova i error.



Ara queda fer els mateixos passos per l'eix horitzontal a les funcions moviment_dreta() i moviment_esquerra(). Després ja tenim la funció moviment_sprite() que s'encarrega de gestionar si apretem la teclas ESC per sortir o si estem fent un moviment de joystick (o dels cursors), segons la tecla, realitzarà unes funcions de moviment o unes altres:



I finalment la funció main() que comença preparant la pantalla i les posicions inicials dins del mapa de caselles. Després a la línia 608 crida la funció per inicialitzar la pantalla (carregar imatges i crear el mapa de caselles). Acabem d'inicialitzar variables que usarem per controlar l'scroll i inicialitzem el control d'interrupcions per realitzar els moviments del caràcter principal. A la línia 623 dibuixem l'sprite del caràcter principal i no fem res més que escoltar si s'apreta la tecla de sortir i processar el moviment del caràcter principal amb la funció moviment_sprite().


Si sortim amb la tecla ESC, aturem les interrupcions i tornem a l'estat incial, després entre les línies 628 i 645 imprimim tot d'informació per ajudar-nos a debugar. I tornem al DOS amb Exit(0).



Conclusions

Hem vist com fer un scroll usant les funcions del hardware del V9958. Les funcions d'scroll són totes molt semblants, i es podrien ajuntar totes en una sola funció. És el que he intentat fer scrfusfons.c. Malauradament no he aconseguit que funcioni correctament. Si algú és capaç d'arreglar-ho, si us plau que ens ho faci saber.


Ara quedaria mirar de com mantenir la part superior de la pantalla immòbil per posar-hi el cartell informatiu del joc: vides, punts, armes, etc. Això ho podem fer copiant un bloc o utilitzant la interrupció de línia. Això ho veurem en un altre post del blog.


Clica aquí per a veure els exemples funcionant.




Comments


bottom of page