viernes, 1 de julio de 2016

VOLCADO DEL BUFFER CON LA PILA (IV): RUTINA DE VOLCADO II







En esta entrada voy a explicar la subrutina VULVU.
Antes que nada, decir que es una "libre adaptación" de la que usa Mike Lamb en algunos de sus juegos, como Batman: The Movie o WEC Le Mans. Yo la uso en Knightmare 2 ZX.

En el .GIF de abajo puedes ver la forma en la que vuelco el buffer en la pantalla real. Va volcando las líneas de derecha a izquierda y primero vuelca un doce caracteres en horizontal y luego los otros doce.

Volcado del buffer en la pantalla real

Para la explicación, voy a tomar como base que volcamos desde la dirección 8004h del buffer a la 4090h de la pantalla real.

IX va a ser el puntero que lleva las direcciones del buffer, por ello lo cargamos con 8000h (inicio del buffer) + 4 que son los caracteres que dejamos de marco a la izquierda.
HL es el puntero de la pantalla real. Vamos a imprimir a partir del cuarto caracter vertical, dejando un marco de 4 caracteres a la izquierda: 4000h+80h+4=4084h. ¿Por qué lo cargamos con 4090h?, pues porque hay que sumarle 12 (0Ch). 4084h+0Ch=4090h. Ahora entenderás por qué.

 

Guardamos SP y lo cargamos con IX que es el inicio del buffer que queremos volcar (8004h).
Salvamos los registros. El que nos interesa es HL que lleva las direcciones de la pantalla real.
Cargamos 6 registros con las primeras 12 direcciones de la primera línea a volcar:

HL'=contenido de las direcciones 8004h,8005h
DE'=contenido de las direcciones 8006h,8007h
BC'=contenido de las direcciones 8008h,8009h
AF= contenido de las direcciones 800Ah,800Bh
DE= contenido de las direcciones 800Ch,800Dh
BC= contenido de las direcciones 800Eh,800Fh

Ahora cargamos SP con HL (SP=4090h) y hacemos PUSH con todos los registros que antes hemos cargado. Ya he comentado que al hacer POP, SP se incrementa, pero al hacer PUSH, primero se decrementa y luego se mete el dato en la dirección donde apunta SP. Por eso hay que sumarle 12 a HL. Al hacer PUSHes, éste se va decrementado. Así que los registros se "PUSHEAN" en orden inverso a como se "POPARON"

El valor de BC se mete en 408Fh,408Eh
El valor de DE se mete en 408Dh,408Ch
El valor de AF se mete en 408Bh,408Ah

En este momento incrementamos H para pasar a la siguiente línea de la pantalla (4190h)
y salvamos los registros.

El valor de BC' se mete en 4089h,4088h
El valor de DE' se mete en 4087h,4086h
El valor de HL' se mete en 4085h,4084h

Como puedes ver, cada byte se ha colocado en su sitio y de la forma como se ve en el .GIF.

Ahora tenemos que pasar a la siguiente línea del buffer, por lo que hay que incrementar el byte alto de IX. Hacer algo así como INC Ix. Este nemónico como tal no existe, pero si ponemos el valor DDh antes de INC H, el Z80 lo interpreta como INC Ix.

Esto lo hacemos cuatro veces para volcar las cuatro primeras líneas.
Después del último volcado miro a ver si hemos llegado a la cuarta línea, con lo que repetimos el proceso, o si hemos acabado con el caracter en curso. Para eso se hace BIT 2,H. Si el resultado es uno, significa que estamos en la línea 4, por lo que repite.

Hemos acabado de imprimir el primer rectángulo de 1 caracter de alto por 12 de ancho.
¿Qué valor tiene IX?: Partíamos de 8004h. Hemos incrementado Ix 8 veces, por lo que IX=8804h.
¿Qué valor queremos que tenga ahora? Queremos que apunte a la posición de inicio (8004h) más los doce caracteres que ya hemos metido, osea 8004h+0Ch=8010h.
La cuenta es sencilla. Restamos 8 a Ix y sumamos 0Ch a iX. Osea, le restamos (800h-0Ch).
Para HL la cuenta es la misma.



Ya podemos volcar los otros 12 caracteres.

¿Cuánto vale ahora IX? Empezó en 8010h y hemos incrementado Ix 8 veces. IX=8810h.
¿Cuánto queremos que valga? Pues queremos que se nos posicione en el siguiente caracter en vertical de donde empezó en un principio. Empezó en 8004h y queremos que valga 8024h.
Podemos restar 8 a Ix, restar 0Ch a iX y sumarle 20h. (800h-20h=7E0h.)
Basta con restar a IX (7E0h+0Ch). 8810h-(7E0h+0Ch)=8024h.

Ya tenemos volcado el primer rectángulo de 1 caracter de alto por 24 de ancho. HL e IX apuntan al inicio del segundo rectángulo... siempre que no se cambie de tercio. Este problema ya se solucinó en la anterior entrada.


Si he conseguido que entiendas esto, no te será muy complicado cambiar el tamaño del buffer que se vuelca. De todas formas he dejado preparada "alguna pista". Si queremos un buffer 4 caracteres más ancho, tenemos que volcar dos caracteres más por vuelta. Para eso hay que usar un registro doble más. Yo he elegido AF'. Si quitas los ";" delante de EX AF,AF' y de los PUSH AF y POP AF volcamos 4 datos más, por lo que el buffer volcado será de 28 caracteres de ancho. Pero recuerda darle a MITAN el valor de 14. Para centrar algo mejor el buffer puedes dar  a CHAR el valor de 2.
Si queremos menos caracteres verticales, tenemos que quitar VOLVUs de nuestra pila "artificial".

Como siempre te digo, experimenta, que es la mejor manera de entender las cosas.

La rutina de restablecimiento del fondo no tiene nada de especial.

lunes, 27 de junio de 2016

VOLCADO DEL BUFFER CON LA PILA (III): RUTINA DE VOLCADO I



Vamos a ir volcando el buffer en la pantalla real en bloques de un caracter de alto por 24 de ancho.
Para que quede centrada en la pantalla dejamos un marco de 4 caracteres a la izquierda.
Estos valores se establecen al principio de CONTENIDA.ASM. Así podrás cambiarlos para hacer tus pruebas sin comerte mucho la cabeza.




MITAN es el ancho en caracteres del recuadro que vamos a volcar partido por 2.
CHAR es el número de caracteres que dejamos en blanco.

EL corazón de la rutina es VOLVU que vuelca primero los 12 primeros caracteres y luego los otros 12  desde la dirección que le marca IX (buffer) hasta que le marca HL. Además, retorna con la dirección del siguiente caracter tanto en IX como en HL. Pero no comprueba si cambiamos de tercio, por lo que tendremos que comprobarlo nosotros "a mano".

Por esto es por lo que hay que llamar a VOLVU varias veces (tantas como caracteres verticales tenga el trozo de buffer que queremos imprimir). Esto supondría realizar 16 CALL VOLVU. Nosotros vamos a utilizar otro método:  construimos una "pila artificial" donde ponemos a qué dirección tiene que saltar cada vez que se ejecute un RET (en concreto con el que termina VOLVU).

Esta es nuestra pila:



Si hacemos LD SP,STACCR cuando se ejecuten los RETs, saltará a la dirección que indique SP:

RET: salta a VOLVU
RET: salta a VOLVU
RET: salta a VOLVU
RET: salta a VOLT
RET: salta a VOLVU
RET: salta a VOLVU
RET: salta a VOLVU
RET: salta a VOLT1
RET: salta a VOLVU
RET: salta a VOLVU
RET: salta a VOLVU
RET: salta a VOLTF
etc...
RET: salta a STACV que es el final de la rutina.
De esta manera nos ahorramos las llamadas.



Pues lo dicho, ponemos SP apuntando a STACCR (nuestra pila artificial).
HL va a llevar las direcciones de la pantalla real donde se va a volcar nuestro rectángulo e IX lleva las direcciones del buffer desde donde se vuelcan.
Los datos  se van cogiendo del buffer con POP (al hacer POP se coge el dato de la pila y ésta se incrementa) y se van metiendo en la pantalla real con PUSH (al hacer PUSH, se decrementa primero la pila y luego se mete el dato).

Por eso IX se carga con $8000+CHAR: inicio del buffer más el marco de 4 caracteres a la izquierda.
HL se carga con $4080+CHAR+MITAN: vamos a volcar a partir del 4 caracter horizontal (dirección $4080) más el marco (CHAR) más los doce primeros caracteres  horizontales a volcar. Ya he comentado que al hacer PUSH la pila se va decrementando, por eso a HL se le suma MITAN.

Comenté que VOLVU vuelca un rectángulo de un caracter de alto por 24 de ancho desde IX hasta HL y devuelve IX y HL con el valor para el siguiente caracter. Osea, que si cambiamos de tercio no vale.

Si te das cuenta, HL empieza en el cuarto caracter vertical. Podemos llamar a VOLVU cuatro veces hasta que llegue al cambio de tercio. Así que hacemos JP VOLVU (1 vez) y en nuestro stack tenemos VOLVU (2 veces),VOLVU (3 veces),VOLVU (4 veces) y VOLT.
En VOLT cargamos HL con la dirección del principio del segundo tercio $4800 más el marco más MITAN.

Pero los caracteres verticales también van pasando para el buffer. Empezó en $8000. Cuando VOLT se haya ejecutado 8 veces tenemos que cambiar de tercio a mano.
Hasta ahora VOLVU se ha ejecutado 5 veces (VOLT también salta a ella), así que en nuestro stack hemos puesto VOLVU (6 veces),VOLVU (7 veces),VOLVU (8 veces) y VOLT1.
En VOLT1 cargamos IX con el inicio del segundo tercio del buffer $8800 más el marco y saltamos otra vez a VOLVU. Despúes de cuatro VOLVUs tenemos que actualizar HL con la dirección del tercer tercio de la pantalla más el marco más MITAN.

Si quisiéramos posicionar el volcado del buffer en la pantalla real en otra dirección, por ejemplo a partir del segundo caracter vertical ($4040+CHAR+MITAN) deberíamos cambiar nuestra pila artificial. Si has entendido esto, deberías ser capaz de hacerlo.

martes, 21 de junio de 2016

VOLCADO DEL BUFFER CON LA PILA (II): GUARDANDO EL FONDO



Este programa es idéntico al VOLCADO DEL BUFFER CON LDI en lo concerniente al control de enemigos y prota. Pero cambia sustancialmente a la hora del guardado de fondo, impresión de sprites, volcado y restauración del fondo.





Esta primera parte de la rutina es muy parecida. Vamos metiendo los datos que necesitamos en la tabla de recuperación del sprite, calculamos la dirección del buffer donde  lo imprimimos y colocamos los valores que vamos a utilizar. Sólo cambia que el contador de sprites a imprimir va a ser el registro B, ya que el A se va a usar para hallar la dirección de pantalla inferior a una dada.







Con este código nos aseguramos de que después de sumar el ancho a la dirección donde se va guardando el trozo de buffer, (SUMLDR y SUMLDD), E no pase de FFh.  En esta rutina el tamaño máximo de los sprites lo he fijado en 3x3. El trozo a guardar puede ser 3x4, por lo que el tamaño máximo en bytes a guardar será de 3X4X8=96 bytes. Si al sumar la dirección donde voy a guardar el cacho más 96 tiene acarreo, significa que E ha pasado de 255. Así que lo ponemos a 0 e incrementamos D.







En esta rutina de guardado, va haciendo LDI para guardar el fondo. Al final tiene que decrementar L ya que con el último LDI se ha incrementado de más. Por eso, no podemos usar todo el ancho de la pantalla, ya que hay zonas en las que el LDI ha hecho que HL pase de 40FFh a 4100h.(por ejemplo). Si decrementamos sólo L, nos daría 41FFh y lo correcto sería 40FFh.

Incrementamos H para pasar a la siguiente línea del buffer. Como la coordenada es par, sabemos que no va a cambiar ni de caracter ni de tercio.
Sumamos a DE el ancho ya que ahora vamos a guardar de decrecha a izquierda con LDD. Incrementamos L ya que el último LDD nos lo ha decrementado de más y se vuelve a sumar el ancho a DE para volver a guardar de izquierda a derecha.

Ahora sí que tenemos que comprobar si al pasar a la siguiente línea cambia de caracter o de tercio.
Se repite el bucle y cuando se guarda todo el fondo del sprite se actualiza GFONDI+1 para el siguiente.

En cuanto a la rutina de impresión de sprites es muy parecida a la que uso en el VOLCADO DEL BUFFER CON LDI. Simplemente cambia el método de hallar la dirección de la línea inferior a una dada, ya que aquí estamos usando la estructura real de la pantalla.

jueves, 16 de junio de 2016

VOLCADO DEL BUFFER CON LA PILA (I)




En esta serie de entradas voy a explicar otra forma de usar un buffer de impresión. Es bastante más rápido que el anterior, y a mi modo de ver más elegante, si bien ocupa algo más de memoria.

Te puede bajar el código de AQUI:
He metido un fondo más aparente.

Este buffer que vamos a usar copia literalmente la estructura de la pantalla del spectrum. No es como en las entradas anteriores, que lo creábamos a nuestra conveniencia. En este caso, todas las operaciones a realizar serán las mismas que si estuviéramos imprimiendo directamente en la pantalla. Por ello, es muy importante que entiendas la estructuración de la misma
Parece muy enrevesada para nuestra mente decimal, pero si piensas en Hexadecimal, tiene toda su lógica.
Ya comenté que viene muy bien explicado en el curso de Romero. Por favor, échalo un vistazo, ya que si no dominas cómo funciona el asunto, estas entradas se te pueden hacer un poco duras.

De todas formas, voy a dar un repaso rápido, que nunca viene mal.

La pantalla del Spectrum tiene 192 líneas.
Está dividida en tres tercios de 24 líneas.
Cada tercio está dividido en 8 caracteres.
Cada caracter está dividido en 8 líneas.
Cada línea mide 32 caracteres de ancho.
La pantalla empieza en la dirección 4000h y termina en la 57ffh.


Los tres tercios tienen la misma estructura, por lo que nos fijaremos sólo en uno.

Seguro que has visto cargar una pantalla en el spectrum muchas veces, y no lo hace de forma secuencial.






Carga las 32 direcciones de memoria de la primera línea, y la siguiente dirección es la primera línea del segundo caracter. Carga las 32 direcciones y la siguiente será la primera línea del tercer caracter... Así hasta que ha cargado las 32 direcciones de memoria de la primera línea del octavo caracter, que es el último del primer tercio.

Las siguiente dirección será la segunda línea del primer caracter y todo vuelve a empezar pero esta vez con las segundas líneas de cada caracter. Así hasta que carga el tercio.

Si estamos en la primera dirección de la primera línea del primer caracter (4000h) y queremos pasar a la dirección que está inmediatamente debajo de ésta (primera dirección de la segunda línea del primer caracter), ¿qué debemos hacer? He dicho antes que después de la dirección 32 de la primera línea del octavo caracter se pasa a la dirección 0 de la segunda línea del primer caracter. Osea que hemos avanzado 8 líneas*32 direcciones por línea=256 (100h) direcciones.  Sumamos 4000h+100h=4100h.
Osea, si el registro HL=4000h, nos basta con INC H y tendremos HL=4100h.

Para hallar la segunda línea, incrementamos H otra vez, y así con todas las líneas del caracter.
El problema es que si estamos en la última (4700h) e incrementamos H, nos da HL=4800h como dirección de la primera línea del segundo caracter. Y eso no es así. La primera línea del segundo caracter, como hemos visto, es 4000h+32 direcciones de memoria que ocupa la primera línea, o lo que es lo mismo en hexadecimal 4020h.

Esto nos supone ir comprobando cada vez que incrementemos H para pasar de línea, si nos hemos pasado al siguiente caracter. Esto se hace viendo si H es múltiplo de 8. Si lo hemos hecho, basta con restar 8 a H, y sumarle 20h a L.
48h-8=40h. 00h+20h=20h. Total que HL=4020h. Ya tenemos la dirección que buscábamos.

Así podemos hallar la dirección que está debajo de una dada dentro de un tercio, pero ¿qué pasa si nos pasamos de tercio? Con nuestro método, cuando estemos en el octavo caracter del primer tercio y en su última línea estaremos en la dirección 48E0h, H es múltiplo de 8, por lo que nos hemos pasado de caracter. Hacemos 48h-8=40h. E0h+20h=00. HL=4000h. Nos vuelve a la primera línea del primer caracter. Para cambiar de tercio basta sumarle 20h a L, sin restar 8 a H.
E0h+20h=00. HL=4800h, que es la primera dirección de la primera línea del segundo tercio.
4800h-4000h=800h. 800h no es otra cosa que lo que nos ocupa un tercio entero.

¿Cómo sabemos si al incrementar H nos hemos pasado de tercio además de caracter?
Pues porque al sumar 20h a L, nos va dando esto:

4700h     última línea primer caracter
4720h     última línea segundo caracter
4740h     última línea tercero caracter
4760h     última línea cuarto caracter
4780h     última línea quinro caracter
47A0h    última línea sexto caracter
47C0h    última línea séptimo caracter
47E0h    última línea octavo caracter

HL=47E0h. INC H para cambiar de línea. HL=48E0h. H es múltiplo de 8, por lo que hay que cambiar de caracter. Sumo 20h a L. HL=4800. Si te das cuenta L vuelve a ser igual a cero, por lo que en la operación de la suma ha habido acarreo (FLAG C=1). Esto significa que se nos ha acabado el tercio y no tenemos que restar 8 a H.

Todo esto lo hace la rutina SIGLIN de CONTENIDA.ASM, que ya medio expliqué, pero que seguro que ahora la entiendes perfectamente.





Como puedes ver, pensando en Hexadecimal, la distribución de la pantalla no es tan "rara" como parece.

En esta rutina se va a usar todo este planteamiento constantemente.

Como ya sabes, guardo los sprites en zigzag, así cuando termino de imprimir el último caracter del sprite, me basta con hallar la dirección que está debajo y volver de a imprimir de derecha a izquierda.

Esta rutina tiene alguna limitación. La coordenada Y tiene que ser siempre par. Esto es por lo siguiente: Sólo va a haber cambio de caracter al hallar la dirección que está debajo de una dada cuando la coordenada de partida sea impar (condición necesaria pero no suficiente). Si nuestra coordenada es siempre par, nos ahorramos una comprobación de cambio de caracter, ya que sabemos que nos basta con incrementar H para cambiar de línea.

Por la distribución de la pantalla, para pasar de una dirección a la contigua cuando imprimamos el sprite, sabemos que L nunca va a ser FFh ni 0. Por ello, todos estos incrementos y decrementos se pueden hacer con INC/DEC L en vez de con INC/DEC HL, con lo que ganamos mucha velocidad.
El problema de usar INC/DEC L cuando lo que queremos es INC/DEC HL ya lo hemos visto. Tenemos que estar seguros de que L nunca sea FFh al incrementar ni 0 al decrementar ya que éste pasaría a ser 0/FFh y H se quedaría con el mismo valor.

Esta ganancia en velocidad no es oro todo lo que reluce. En realidad, L sí que va a ser 0 o FFh, pero sólo en determinados puntos de los bordes de la pantalla. Por eso, la coordenada X nunca puede ser menor que 8 ni mayor que 248.
En este ejemplo volcamos 16 caracteres de alto por 24 de ancho.

Todo esto se verá  con el examen de la rutina.

Intenta comprender todo esto muy bien.

lunes, 13 de junio de 2016

VOLCADO DEL BUFFER CON LDI ( Y VIII): VOLCADO DEL BUFFER Y RESTAURACIÓN DEL FONDO



Esta rutina es muy sencilla: simplemente cogemos cada línea del buffer y la volcamos a la pantalla real con tantos LDIs como ancho es el buffer (ANPAN).

Lo único que tenemos que saber es la dirección de la pantalla real que se corresponde con el primer carácter de cada línea.
Para eso, en CONTENIDA.ASM creamos otra tabla:



Cargo IX con el incio de la tabla.
HL con la dirección de la pantalla real que corresponde con la coordenada (0,0), osea, con la esquina superior izquierda del cuadro donde voy a volcar el buffer.
B con el número de líneas.
En el bucle voy metiendo las direcciones en la tabla.
En SIGLIN hallo la dirección inmediatamente inferior a una dada en la pantalla real:



Si no estás familiarizado con la estructura de la pantalla del Spectrum, te recomiendo que eches un vistazo aquí.

Ahora vemos la rutina de volcado:



Cargamos SP con la tabla.
BC con el tamaño del buffer.
HL con el inicio del buffer.
Al hacer POP DE, DE tiene la dirección de la primera línea donde vamos a volcar.
REPT y ENDM no son nemónicos del Z80. Es una forma de crear un bucle en PASMO: repite la instrucción LDI ANPAN veces. Osea, ensambla ANPAN LDIs, que es la anchura del buffer.
Como ya hemos visto, se ejecuta JP PE hasta que BC=0 (tamaño del buffer).

No tiene más misterio.


RECUPERACION DEL FONDO DEL BUFFER

Este es el último paso de nuestra rutina. Vamos a ir restaurando los trozos de fondo del buffer donde hemos imprimido un sprite. Para eso usamos las tablas que hemos ido creando al guardar los cachos. Esta recuperación hay que hacerla en orden inverso a como se guardaron. Y precisamente, IY apunta a la tabla del último cacho.



En CSPIMR+1 se guardó en su momento el número de sprites.
Guardamos AF como contador.
Vamos cargando los distintos registros con los valores guardados en la tabla de recuperación.
Y el resto es muy parecido a la rutina que guarda los trozos del fondo.


Ahora es cuando tú puedes cambiar distintos parámetros, tales como sprites, tamaño del buffer, de la pantalla real donde se vuelca el buffer... en fin, todo lo que se te ocurra para que estés seguro de entender su funcionamiento.


miércoles, 8 de junio de 2016

VOLCADO DEL BUFFER CON LDI (VI): IMPRESIÓN DE SPRITES





Antes de proceder a estudiar la rutina, voy a intentar explicar algunos conceptos en el movimiento a pixeles (alta resolución).

A la hora de imprimir gráficos en el spectrum, lo que se hace es meter ciertos valores en las direcciones de pantalla (en nuestro caso en el buffer).
Si yo quiero imprimir el sprite del esqueleto en las coordenadas[(Y,X))] (0,0) tendré que empezar a meterlo en la dirección 8000h que es donde le corresponde (Gráfico sin desplazar). Para imprimirlo en la coordenada (0,2), lo meto también a partir de la dirección 8000h, pero desplazado 2 pixeles a la derecha. Y lo mismo en (0,4) y (0,6). Para imprimirlo en (0,8) se empezará a meter en la dirección 8001h sin desplazarlo y así sucesivamente.


8000h     8001h      8002h 



Gráfico sin desplazar (0,0)




Gráfico desplazado dos pixeles a la derecha (0,2)




Gráfico desplazado cuatro pixeles a la derecha (0,4)




Gráfico desplazado seis pixeles a la derecha (0,6)





Esto significa que si la coordenada X es cero o múltiplo de 8, el gráfico se mete sin desplazar y si no lo es, hay que desplazarlo el número de veces que la coordenada X exceda del múltiplo de 8 inmediatamente anterior.

Como podrás imaginar, para conseguir esto hay que hacer muchos desplazamientos y esto enlentece la rutina de impresión.

Yo uso un método explicado por Pablo Ariza en una Microhobby Especial.



Esta rutina nos crea una tabla de tal manera que:


  • En FANNh nos mete el número NN desplazado 2 veces a la derecha y en FBNN nos mete el sobrante de este desplazamiento.
  • En FCNNh nos mete el número NN desplazado 4 veces a la derecha y en FDNN nos mete el sobrante de este desplazamiento.
  • En FENNh nos mete el número NN desplazado 4 veces a la derecha y en FFNN nos mete el sobrante de este desplazamiento.
¿Qué quiero decir con sobrante?
Fíjate en este gráfico:

8000h                   8001h

Arriba tenemos un cuadrado sin desplazar que ocupa a partir de la posición 8000h. Abajo lo hemos desplazado dos pixeles a la derecha que nos ocupa la dirección 8000h y la 8001h. ¿Qué parte del gráfico nos ocupa la dirección 8001h? Pues lo que nos ha sobrado al desplazarlo dos pixeles. Aquí verás más claro lo que decía en la entrada anterior sobre que si la coordenada X no es cero o múltiplo de 8 (si hay desplazamiento) el cacho de buffer a guardar ocupa un caracter más de ancho que el sprite.

Esta tabla se usa así: Supongamos que un byte del sprite es 11111111b (FFh) y queremos imprimirlo en la coordenada (0,2). Necesitamos desplazarlo 2 veces a la derecha. Cargamos H con FAh y L con FFh. Al hacer LD A,(HL) en A obtenemos 00111111b y lo metemos en 8000h. Ahora necesitamos la parte sobrante. Pues  cargamos H con FBh y L con FFh. Al hacer LD A,(HL) obtenemos 11000000h y lo metemos en 8001h. Si hacemos lo mismo con los 8 bytes del gráfico lo habremos imprimido en la coordenada X=2.

Este sistema es rápido, pero supone reservar desde FA00h hasta FFFFh y el movimiento mínimo horizontal va a ser de 2 pixeles. Creo que estas dos limitaciones merecen la pena.


MÁSCARAS

Como ya he comentado, esta rutina imprime sprites, osea que no alteran el fondo por donde pasa (para eso guardamos los trozos de buffer) y tiene partes transparentes, por lo que se ve el fondo que hay a través de ellos.
Para conseguir esto último se usa la máscara: a cada byte del gráfico le corresponde su máscara, que es otro byte en el cual los unos son las partes transparentes, y los ceros las opacas. Por eso los sprites ocupan el doble que los gráficos.

Aquí podemos ver un cuadrado con su máscara correspondiente:


GRÁFICO                                                              MASCARA

Como puedes, ver he puesto unos en la máscara en los pixeles que quiero que sean transparentes; es decir en los bordes y en el centro. Suelo dejar un pixel opaco entre la máscara y el gráfico para que éste se defina mejor la imprimirlo sobre el fondo.


En este gráfico de abajo he fusionado el gráfico con la máscara para que lo veas más claro. En amarillo está el gráfico y en negro la máscara. En blanco están los pixeles que dejo de borde entre la máscara y el gráfico.



¿Cómo se maneja esto a la hora de imprimir?: Pues cogemos el byte de la máscara y hacemos un AND con el byte del fondo y a esto le hacemos un OR con el byte del gráfico.

Para crear los sprites uso el editor SevenuP de Metalbrain, que funciona de maravilla. Te explico lo fundamental de su uso para lo que nos ocupa.

En File pulsas New y te pedirá el tamaño del gráfico a crear en pixeles. Te aparecerá una cuadrícula como la de estos ejemplos. Al pulsar la flecha blanca te cambia el modo del cursor. Pinta el gráfico que quieras y luego pulsa en Mask y activa Use mask y View mask. Aquí pintas la máscara.

Yo para mi rutina de sprites necesito la máscara invertida (ya lo explicaré) por lo que tienes que pulsar en el botón INV. Ya podemos salvar el sprite.

En File pulsas Output options y tienes que dejar las opciones como en esta figura:





El byte de la máscara va antes que el del gráfico y lo guardo en Horizontal Zig Zag osea, que se guarda la primera línea del gráfico de izquierda a derecha y después la segunda de derecha a izquierda, la tercera de izquierda a derecha y la cuarta de derecha a izquierda, etc. Esta forma de guardar los sprites nos supone que podamos programar una rutina de impresión más rápida.

Finalmente  en File, pulsamos en Export data y elegimos que nos lo grabe con extensión .BIN.


Con todo esto vamos a ver la rutina de impresión de sprites.




En el registro B metemos la altura del gráfico dividido por 2.
Cargamos DE con la dirección del buffer donde va el gráfico (este dato lo metimos en DIRPBUF+1 cuando lo hallamos en la rutina de guardar los fondos).
Cargamos HL con la dirección del gráfico a imprimir.
Guardamos el registro SP y lo cargamos con HL. Los datos del gráfico los vamos a ir cogiendo a base de POPs, como los datos de la cola de impresión de sprites.
Al hacer EX DE,HL, metemos en HL la dirección del buffer donde vamos a imprimir.
En A metemos la anchura del sprite y lo guardamos en las direcciones que nos hace falta para que la rutina sea más rápida.
Si la coordenada X es 0 o múltiplo de 8 significa que no hay rotaciones por lo que saltamos a IMPBUFCARACTERJUSTO, que es una rutina semejante pero sin rotar nada y más rápida.

Lo que nos ha sobrado de ser múltiplo de 8 se suma a F8h. Así ya sabemos qué tabla de rotaciones tenemos que usar y guardo este dato en la rutina: si X=12 (00001100b) y hacemos AND 7, nos da como resultado 4 (00000100b) osea que hay que usar la tabla de 4 desplazamientos. F8h+4=FCh que es donde está esta tabla. Lo guardo donde la voy a usar y lo incremento donde voy a usar los restos.
Cargo DE con el ancho del buffer, C con el ancho del sprite y con EXX paso a los registros alternativos.
Con POP DE cargo E con la máscara del primer byte del sprite y D con el gráfico.
En H meto el inicio de la tabla de desplazamientos. Al hacer "LD L,E",  en (HL) tengo la máscara rotada.
La cargo en A (LD A,(HL)) y la invierto con CPL, ya que las máscaras las salvamos invertidas desde SevenuP.

¿Por qué salvo la máscara invertida y luego la vuelvo a invertir en la rutina en vez de salvarla normal? Pues porque esta máscara que estoy invirtiendo es la original desplazada. Como usamos las tablas, a la izquierda nos ha metido ceros y eso no nos interesa ya que la parte desplazada ha de ser transparente. Por eso al hacer CPL nos convierte en unos los bits transparentes de la máscara y en ceros los opacos que es lo que nos hace falta.

Al hacer EXX, HL contiene la dirección del buffer, por lo que hacemos un AND (HL) como expliqué antes.
Volvemos a hacer EXX, por lo que H contiene la tabla de rotaciones. Con LD L,D conseguimos que en (HL) esté el gráfico rotado (D tenía el gráfico) y hacemos el OR.

Hasta este punto hemos imprimido el gráfico con la máscara desplazado. En la siguiente dirección de memoria (INC HL) meteremos el resto.
Para eso hacemos EXX  e incrementamos HL. Así H tiene el inicio de la tabla de los restos de los desplazamientos y hacemos lo mismo que antes.
Repetimos lo que nos ocupa de ancho el sprite y a partir de ANF1 nos ponemos a imprimir la siguiente línea del sprite. Para eso sumamos ANPAN (que está en DE) a la dirección del buffer donde terminó de imprimirse la primera línea del sprite y hacemos el mismo proceso pero de derecha a izquierda.

Por eso el dato de la tabla del sprite que guarda el alto del mismo se divide por la mitad: hacemos dos pasadas por cada iteración del bucle.

Recupero el Stack y repito todo el proceso con el siguiente sprite: guardo el fondo y lo imprimo.

Aquí he puesto un .GIF para que se vea más claro el proceso (espero).






Cuando la coordenada X es cero o múltiplo de 8, no hay rotaciones, así que saltamos a una rutina especial que imprime más rápido el sprite: IMPBUFCARACTERJUSTO




Una vez imprimidos todos los sprites, tenemos que volcar el buffer a la pantalla real para que se vea lo que hemos hecho.










lunes, 6 de junio de 2016

VOLCADO DEL BUFFER CON LDI (V): GUARDANDO EL FONDO DEL BUFFER



Una vez que hemos actualizado las coordenadas de todos los sprites vamos a proceder a su impresión. Esto requiere unos cuantos pasos:


  • Primero guardamos el fondo existente en el buffer donde vamos a meter el sprite para que no se modifique a su paso.
  • Luego imprimimos el sprite en el buffer.
  • Cuando hemos hecho esto con todos los sprites, se vuelca el buffer en la pantalla real.
  • Por último recuperamos todos los fondos del buffer que se modificaron al pintar el sprite. Así el buffer queda totalmente limpio para la próxima impresión de sprites.

En esta entrada voy a explicar cómo guardo el fondo del buffer donde voy a imprimir los sprites.

Lo primero que tenemos que saber es cómo hallar la dirección del buffer correspondiente a unas coordenadas dadas. Yo uso una tabla, donde meto la dirección del primer carácter de cada línea del buffer. Esta tabla se crea en CONTENIDA.ASM.

La tabla empieza en TABCOOR (F800h). Metemos el byte bajo de las direcciones del primer caracter de cada línea del buffer en F8nn y el byte bajo en F9nn.



Cargamos DE con PANTVIR, que es el inicio del buffer (8000h) y HL con TABCOOR (F800h). Esto corresponde con (coordY=0, coorX=0). Iniciamos B con el número de líneas que tiene el buffer.

En F800h metemos el byte bajo de la dirección del buffer (00h) incrementamos H (HL=F900h) y metemos el byte alto (80h). Decrementamos H, incrementamos L y le sumamos el ancho del buffer (ANPAN) para que HL tenga la dirección de la siguiente línea del buffer (coordY=1, coorX=0) y repetimos esto con todas las líneas del buffer.

Una vez que sabemos la dirección del primer carácter de la línea del buffer donde está el sprite, sólo tenemos que sumarle la coordenada X para hallar su dirección exacta. En realidad su coordenada X dividida por 8, ya que tenemos que sumarle caracteres y no pixeles.

Veamos un ejemplo práctico. Supongamos que un sprite está en las coordenadas Y=20h X=18h.
Primero vamos a hallar la dirección del primer carácter de la línea del buffer donde está el sprite.
Para eso hacemos lo siguiente;




El registro L se carga con la coordenada Y del sprite. L=20h. H se carga con la tabla que tiene los bytes altos de las direcciones del buffer. H=F9h. Por lo que HL=F920h Al hacer LD D,(HL) en D metemos el byte alto de la dirección del primer carácter del buffer correspondiente a la coordenada 20h.

Ahora metemos en A la coordenada X del sprite. A=18h y la dividimos por 8. A=3, ya que la coordenada X son pixeles y a nosotros nos hace falta saber el número de caracteres a sumar a la dirección del primer caracter de la línea. Al hacer DEC H, HL=F820h que es donde está el byte bajo de la dirección del primer caracter de la línea 20h del fuffer. Se lo sumamos a la coordenada X en caracteres (teniendo cuidado de incrementar D si ha habido acarreo) y así lo metemos en E. De esta manera, DE tiene la dirección donde se va a imprimir el sprite.

Con este método, aunque ocupa algo de memoria, las direcciones de impresión en el buffer se hallan rápidamente.

Una vez guardados los "cachos" del buffer que ocupan los sprites hay que recuperarlos. Para eso se crea una tabla para cada cacho. Cada una consta de 6 bytes:

byte 0: Altura del cacho dividida por 2
byte 1: Anchura del cacho
byte 3 y 4: dirección del buffer donde se debe reponer el cacho
byte 5 y 6: dirección donde se ha guardado el cacho

Estos datos se empiezan a guardar en DATFON (al final de APRINCIPAL.ASM) y se manejan con el registro IY.


El cacho de buffer que se guarda de un sprite no ocupa siempre lo mismo. Debido a que en el Spectrum siempre se imprime en baja resolución, si queremos que su movimiento (byte 12 de la tabla) horizontal sea de menos que 8 pixeles (en este caso puede ser de 2,4 ó 6; siempre par), debido a que hay que desplazar los gráficos, el trozo a guardar ocupará un caracter más si su coordenada X no es cero o múltiplo de 8.
En resumen, un sprite que tenga n caracteres de ancho, si su coordenada X no es cero o múltipo de 8, ocupa n+1 caracteres.
Para saber si un número es múltiplo de 8, basta con hacerle un AND 7. Si este resultado es igual a cero, es que lo es y por lo tanto ocupa n caracteres. Eso es lo que se hace con la coordenada X.
De todas formas, el tema de las rotaciones de los gráficos lo explicaré en la rutina de impresión.


Una última consideración. Como ya he explicado, las tablas de los sprites que se tienen que imprimir se van metiendo en una cola que empieza en SPIMP. Para ir cogiendo estos datos uso la pila, que es un método muy rápido.

Supongamos que en la cola de impresión de sprites he metido: ENEM2,ENEM3,ENEM4,PROTA.
Primero cargo SP con SPIMP (LD SP,SPIMP). Para recuperar el primer dato hago POP IX con lo que IX=ENEM2. Una vez que termino con este sprite hago de nuevo POP IX con lo que ahora IX=ENEM3 y así sucesivamente hasta IX=PROTA.

Para usar la pila de esta manera hay que tener en cuenta que:

  • Las interrupciones han de estar deshabilitadas, ya que al saltar una interrupción se guarda la dirección de retorno en la pila, con lo que se nos machacarían los datos y a saber a dónde retorna.
  • No se puede usar ninguna instrucción que maneje la pila, tal como CALL, hacer PUSH para guardar registros, etc. Obviamente esto también nos machacaría los datos. 

Después de todo este rollo, vamos a ver la rutina en sí.




Cargamos en IY la dirección donde se van a ir guardando las tablas de los cachos del buffer-6.
Guardamos el registro de la pila, ya que lo vamos a usar para ir sacando las tablas de los sprites de la cola de impresión.
Metemos en GFONDI+1 el inicio de la zona donde vamos a guardar los cachos.
Apuntamos la pila a la cola de impresíón.
Aquí aparece CSPIM que es el número de sprites que hay en la cola y que se actualiza en METEIMPS cada vez que metemos uno.
Ya que está en A, lo metemos también en CSPIMR+1 que es el contador del bucle para restaurar los fondos.
Con POP IX cogemos el primer dato de la cola de impresión, osea, IX=tabla del primer sprite a imprimir.
Después hallamos la dirección del buffer a partir de las coordenadas como vimos antes, y la metemos en la tabla. Luego metemos en la tabla la dirección a partir de donde se guarda el cacho.
Miramos si la coordenada X es múltipo de 8. Si no lo es, el trozo a guardar tiene un caracter más que el ancho del sprite.
Antes comenté que para hallar la dirección inmediatamente inferior a una dada en el buffer había que sumarla ANPAN. Como al ir guardando líneas esta dirección se va a incrementar en el ancho del trozo a guardar, la dirección inmediatamente inferior a la  del inicio del trozo será de ANPAN-(ancho del cacho).
Guardamos la (altura/2) en la tabla .

Ponemos B=0 ya que el bucle en GF2 se repetirá hasta que BC=0  y volcamos la primera línea con LDI. Para ir a la segunda, se le suma ANPAN-(ancho del trozo).
Se vuelve a repetir el asunto (ya que el bucle dura  (altura del sprite/2)) y se guarda la dirección de inicio donde se va a meter el siguiente trozo.


Ya estamos preparados para imprimir el sprite.






jueves, 2 de junio de 2016

VOLCADO DEL BUFFER CON LDI (IV): CONTROL DE ENEMIGOS Y DISPARO


Siguiendo el bucle principal, nos encontramos con el control de los los enemigos:


 

NUMEN es una etiqueta en la que indicamos el número de los enemigos que vamos a controlar. En este ejemplo será de 1 a 6.

En CONTE se inicia un bucle con ese número ya que vamos a hacer lo mismo con todos.
Metemos en IX la tabla del primer enemigo.

En CONTS miramos si el bit 7 del byte 19 de la tabla está a uno. Si es así, significa que este sprite va a mitad de velocidad que el prota.
Voy a usar el bit 0 de este mismo byte para saber si hay que mover al sprite o no. Para eso ponemos el bit 7 a cero (ya que sólo nos interesa el bit 0), hacemos un XOR 1 para usarlo como contador y volvemos a poner el bit 7 a uno. Si el resultado del XOR 1 es cero saltamos a PONSP, osea no se mueve el sprite, pero sí que se imprime. En este ejemplo el sprite que va a mitad de velocidad es el esqueleto de arriba.
Al hacer XOR 1, si A vale cero pasa a valer 1 y si A vale 1 pasa a valer cero. Por eso es cero una de cada dos veces, por lo que el sprite va a mitad de velocidad. Primero ponemos a cero el bit 7 para que no influya en el resultado del XOR 1 y luego lo ponemos a 1 para que nos siga valiendo de testigo como que es un sprite que se mueve a mitad de velocidad.

EN CONTS1 mira si ha llegado al límite izquierdo o derecho del buffer. Si es así, salta a CHANGE para que cambie el sentido de la marcha del sprite. Si no, actualiza su posición con el incremento (o decremento) que está en el byte 12 de la tabla y llama a GESTFAS para que cambie de fase.

Para el moviento vertical vuelve a comprobar si está en los límites de arriba o abajo del buffer. Para el de arriba basta ver si está en la coordenada Y=0. Para el de abajo cogemos el valor del byte 3 de la tabla que es la altura del sprite dividida por 2 y la multiplicamos por 2 (SLL). La sumamos a la coordenada Y, y si se pasa del número máximo de líneas verticales del buffer salta a CHANGEV para que cambie el sentido del movimiento vertical del sprite.
En caso de que esté dentro del buffer, simplemente actualiza la coordenada Y con el incremento (o decremento) vertical (byte 11 de la tabla).

Una vez hecho esto, llamamos a la rutina que nos mete la dirección de la tabla del sprite en la cola de impresión de sprites (METEIMPS), pasamos a la siguiente tabla sumando el tamaño de la tabla a IX y continuamos con el bucle hasta que el registro B sea igual a cero.

CHANGE Y CHANGEV están en ARUTINAS.ASM



Cogemos el incremento (o decremento) de la posición horizontal del sprite (byte 12 de la tabla) y lo negamos.
Si al negarlo da un número negativo, es que pasa de moverse a la derecha a moverse a la izquierda. Por ello ponemos el JP M, NEGAT que significa algo así como "si el registro A es negativo, salta a NEGAT".
Si es positivo, la cosa queda en POSIT donde metemos el gráfico mirando a la derecha (bytes 14 y 15) y saltamos a PONSP. Aquí llamamos a METEG que ya explicamos en el control del prota y actualizamos la coordenada X con el incremento corresponsiente.



CHANGEV es muy parecida. Aquí no nos interesa si hay incremento o decremento, ya que el gráfico no cambia si sube o baja.


LA COLA DE IMPRESIÓN

Más arriba he comentado que si queremos que un sprite se imprima, hay que llamar a METEIMPS para que lo incluya en la cola de impresión.
Esto funciona de la siguiente manera: tenemos una cola de impresión FIFO (el primero que entra es el primero que sale) donde vamos metiendo las direcciones de las tablas de los sprites que queremos imprimir cada vez.

Si queremos imprimir los sprites ENEM1, EMEM4, ENEM6, el disparo y el prota, cuando llegue el turno a la rutina de impresión, ésta busca en la cola qué sprite imprimir en ese momento. Así nuestra cola quedará // ENEM1, EMEM4, ENEM6,DISPARO,PROTA//. Esta rutina los va "sacando" en orden y los imprime. Los sprites que se imprimen primero van quedando por debajo de los siguientes. En realidad no hace falta usar la cola, ya que podemos ir mandando imprimir los sprites a medida que se van actualizando, pero a efectos prácticos es más interesante usarla. Además de que teniendo los sprites en una cola, se pueden ordenar para que se impriman en orden descendente de la coordenada Y que ocupa cada uno, por lo que nos da sensación de profundidad. Esto se usa en el juego Renegade y es fundamental en los juegos isométricos. En este ejemplo no se ordenan.

METEIMPS
Está en ARUTINAS.ASM

Si recuerdas cuando comenté la estructuración de la memoria había una etiqueta que se llamaba SPIMP en la cual reservaba 20 bytes con un DS 20.
SPIMP es el inicio de la cola. Cada dato que voy a meter en la cola es de 2 bytes (las direcciones de las tablas de los sprites)



Metemos IX en la cola e incrementamos el puntero de la cola en 2. Este incremento se hace de una manera "especial". Si es el primer sprite, DIRAMET+2 vale SPIMP, ya que es el principio de la cola. Para incrementarlo 2 veces deberíamos hacer INC HL, INC HL y luego volver a guardar este dato (SPIMP+2). Por qué hago INC L, INC L? Pues porque he colocado SPIMP en la dirección F900h+192=F9C0h. La cola nunca va a llegar a FA00h (F9C0h+20=F9D4h) así que podemos incrementar sólo el byte bajo de HL que es más rápido que incrementar HL entero.

A ver si me explico. Imagina que llegamos a un punto que SPIMP vale F9FEh. Tenemos que incrementar dos veces. Si hacemos INC HL, INC HL, el valor será F9FFh,FA00h. Hemos pasado de F9FEh a FA00h al incrementar HL dos veces. Si queremos usar INC L dos veces porque es más rápido, si partimos de SPIMP=F9FEh qué valor nos daría? F9FFh con el primer incremento y al incrementar otra vez el registro L sería F900h ya que H no lo actualiza, por lo que lo estaríamos haciendo mal.
Por eso es fundamental estar seguros de que al incrementar direcciones de una tabla no se nos incremente nunca el byte alto si queremos incrementar sólo el byte bajo para ganar velocidad.

Aquí no es muy importante, pero en rutinas de impresión que se mueven mucho datos, la diferencia de velocidad es significativa. Lo veremos.

Después de actualizar SPIMP, incrementamos el contador de sprites a imprimir (CSPIM).  Este contador lo utiliza la rutina de impresión para saber cuántos sprites tiene que imprimir y ese contador lo carga en el registro A:

CSPIM LD A,0

Para incrementarlo lo que hacemos es LD HL,CSPIM+1 ya que la instrucción LD A,n ocupa dos bytes e incrementamos el valor que contiene la dirección de HL; osea, hacemos INC (HL).

Por último, decir que cada vez que se ejecuta el bucle principal, hay que poner a cero el número de sprites a imprimir y poner la cola de impresión de los sprites al principio (SPIMP):



Esto se hace en INICIO justo antes de leer las teclas.



CONTROL DEL DISPARO



El control del disparo se realiza de la siguiente manera:
Si el disparo no está activado, miramos si se ha pulsado la tecla de disparo. Si lo está, metemos el opcode de JP DISPON en PONQDIS, establecemos la duración del disparo (el disparo está activo siempre que su contador de duración sea mayor que cero o no llegue a los límites del buffer), y dependiendo de la dirección a la que mira el prota ponemos el disparo a su derecha o a su izquierda y le damos el sentido del movimiento que corresponda.
Cuando el disparo está activado, decrementamos la duración del mismo y si es ésta llega a cero, lo desactiva llamando a FINDISP que está en ARUTINAS.ASM. Si se nos sale de los límites del buffer lo desactivamos también, y si no, actualizamos su coordenada X (el disparo se mueve de 4 en cuatro pixeles). Por último lo mandamos a la cola de impresión.

Lo interesante de esta rutina es la manera en que se comprueba si el disparo está activado o no.
Lo más normal sería comprobar si (DISPON+1)=0, ya que en ese caso el disparo estaría desactivado y procederíamos a comprobar la tecla. Pero las comprobaciones no son demasiado demasiado rápidas. Lo que hago es cambiar el código del programa directamente.

En CONTENIDA.ASM, antes de saltar a INICIO he metido este código para desactivar el disparo, que es el mismo que FINDISP:



¿Qué hace esto?

Si te das cuenta, para saber si se ha pulsado la tecla del disparo tenemos que meter en el registro A el valor de (JOY+1) o lo que es lo mismo, que se ejecute esta instrucción: LD A,(JOY+1). El código de esta instrucción no es otro que 3Ah,JOY+1. Pues eso es lo que metemos en PONQDIS. Así, cuando el disparo está desactivado, directamente se ejecuta la lectura de la tecla.

Cuando la tecla se pulsa, ponemos (DISPON+1)=20 con lo que se activa el disparo y sabemos que en ese caso no hay que comprobar la tecla hasta que (DISPON+1)=0.
Pues volvemos a cambiar el código y en PONQDIS ponemos directamente JP DISPON poniendo el disparo con:



Esto lo que hace es poner C3h en (PONQDIS) y DISPON en (PONQDIS+1) con lo que en PONQDIS nos queda JP DISPON y directamente pasa a controlar el disparo sin leer la tecla.

Esto de cambiar el código es muy práctico para ganar velocidad, pero éste se complica y puede llegar a ser muy lioso de depurar. Joffa lo usa mucho en Cobra, por ejemplo.

Para terminar, después de la gestión del disparo, mandamos al prota a la cola de impresión. Lo meto el último para que quede por encima de todos los sprites al imprimirlos:




En esta entrada hemos visto algunos conceptos un poco complicados (más de explicar que de entender).
Prueba a cambiar parámetros en las tablas, como coordenadas de los sprites, sentido de la marcha al principio, velocidades, gráficos...
Ten en cuenta que la comprobación de si un sprite llega al límite izquierdo o superior del buffer no es demasiado fina. Simplemente mira si ha llegado a la coordenada cero. Esto puede dar problemas dependiendo del incremento que se le de a la coordenada. Por ello, sitúa al sprite en una coordenada múltiplo de la velocidad que le des.






lunes, 30 de mayo de 2016

VOLCADO DEL BUFFER CON LDI (III): TABLAS DE SPRITES Y CONTROL DEL PROTA



En esta entrada voy a explicar cómo se realiza el control de los sprites. Para poder hacer esto, necesitamos unas tablas que nos digan las características del sprite, su posición y toda la información necesaria.

Estas tablas se encuentran en ATABLAS.ASM.
En este ejemplo voy a manejar al PROTA, al disparo (si existe) y a 6 enemigos.

Aquí vemos una tabla tipo de un sprite enemigo:



El significado de cada byte de la tabla es el siguiente:

byte 0: Coordenada Y del sprite
byte 1: Coordenada X del sprite
byte 2: Ancho del sprite en caracteres
byte 3: Alto del sprite en líneas dividido por dos.
Si un sprite tiene un tamaño de 3 caracteres de ancho por 3 de ancho, el byte 3 valdrá 3             caracteres*8 líneas que tiene cada caracter dividido por 2, osea: 12. Se divide por dos por el  método que usamos para imprimir el sprite.
byte 4 y 5: Dirección donde se guarda el gráfico a imprimir con la fase actual
byte 6: Pausa que se hace cuando cambiamos antes de cambiar la fase (o frame) a un sprite. Si no se    pone  pausa, parece que va bailando y si se pone demasiada, parece que va patinando.
byte 7: Fase actual del sprite
byte 8 y 9: Gráfico de la primera fase del sprite.
byte 10: Número de fases que tiene el sprite menos 1.
byte 11: Número de pixels que se mueve el sprite en la coordenada Y. Siempre tienen que ser pares.
byte 12: Número de pixels que se mueve el sprite en la coordenada X. Siempre tienen que ser pares.
byte 13: Tamaño que tiene el sprite para cambiar de fase. Si pasamos de la fase 1 a la fase 2, tenemos que saber cuánto ocupa el sprite y así sumarlo a la fase anterior.
byte 14 y 15: Gráfico del sprite mirando a la derecha
byte 16 y 17: Gráfico del sprite mirando a la izquierda
byte 18: Anchura del sprite en pixels. Esto lo uso para saber cuando llegamos al límite derecho de la   pantalla y así darle la vuelta y cambiar el sentido de movimiento. Este dato lo podemos hallar dividiento por dos el byte 2, pero así es más rápido y ocupa muy poco.
byte 19: Este byte se usa para que el sprite vaya a la mitad de velocidad (el esqueleto que sale arriba   del todo). Si el bit 7=1 (10000000b=128) es que este sprite va a la mitad de velocidad que el prota.

No te asustes con toda esta información. A medida que se vaya usando en el programa lo iré explicando más exhaustivamente.

CONTROLANDO AL PROTA






Lo primero que hacemos es cargar IX con el inicio de la tabla del prota.
Como vimos en otra entrada, las teclas pulsadas se guardan en JOY+1.
Si el bit 1 de este valor es uno, es que la tecla izquierda está pulsada.
Al pulsar la tecla izquierda, el sprite se tiene que mover para la izquierda y además poner el gráfico mirando en esta dirección...si no lo está ya. Para eso usamos HORIEN. Yo no suelo usar direcciones de memoria para guardar variables como tal. Si te das cuenta, directamente pongo:

HORIEN  LD A,0

LD A,n ocupa dos bytes, por lo que mi variable se va a guardar en HORIEN+1.

Si HORIEN+1 vale cero, es que está mirando a la derecha, por ello tenemos que meter el gráfico de la izquierda y si vale cero, ya está metido, así que nos ahorramos este paso.

Antes de saltar a INICIO, en contenida ponemos al prota mirando a la derecha:




Con OR A sabemos si vale cero o no. Si vale cero, incrementamos A y actualizamos la variable HORIEN+1 para saber que ya está mirando para la izquierda. Metemos el gráfico de la izquierda y llamamos a METEG, que está en ARUTINAS.ASM:





Aquí, ponemos a cero el contador de frames del sprite para que no cambie enseguida y metemos el gráfico de la izquierda en el sitio correspondiente de la tabla.

Luego cogemos la coordenada X del sprite (como sé que IX=PROTA, en vez de usar LD A,(IX+1), uso LD A,(PROTA+1) que es más rápido) y miro que no haya llegado al límite izquierdo de la pantalla (coordenada X=0). Si no lo ha hecho, le resto dos y salto a METX1 para que lo guarde en la tabla.

Veamos el control de la derecha:




Básicamente es lo mismo. Si el valor de HORIEN+1 es cero, significa que ya estaba mirando a la derecha, por lo que no cambiamos el gráfico de sentido. Si es uno se procede como cuando se pulsa la tecla izquierda. Luego se mira si hemos llegado al borde derecho (vemos si la coordenada X es el ancho del buffer-16 que es el ancho del sprite en pixels) y si no lo ha hecho le sumo 2 y guardamos la coordenada actualizada.

Después llamamos a la rutina que nos cambia la fase del sprite, ya que se ha movido . Esta rutina se llama GESTFAS y por supuesto, está en ARUTINAS.ASM.

Voy a explicar cómo funciona el tema de las FASES del sprite o FRAMES.
A la hora de mover un sprite y para que la sensación de movimiento sea más real se usan fases. Por ejemplo, nuestro prota tiene 2. Así, al alternarlas mientras se mueve parece que anda.



(Llamaré a la fase con los pies juntos, FASE A y a la de los pies separados, FASE B)

Si echas un vistazo en ASPRITES.ASM verás que los gráficos se guardan en orden de fases



PROTAD1.BIN es la FASE A del prota mirando a la derecha y PROTAD2.BIN es la FASE B.
Esta es la manera de guardar todos los gráficos de los sprites.


Los bytes de la tabla de sprites que controlan las fases son:
  • Byte 6: es una pausa que se hace para que no cambie de fase cada vez que se mueve el sprite. En este caso cambia cada 3 veces que se mueve. Esto es para que el efecto de movimiento sea más "real".
  • Byte 7: aquí se guarda el número de la fase en curso. Cuando es igual al número total de fases-1, se pone otra vez a cero.
  • Byte 10: número total de fases -1
  • Bytes 4 y 5: En estos bytes se guarda la dirección donde está el gráfico del sprite que se tiene que imprimir. 
  • Bytes 8 y 9: En estos se guarda la dirección del primer gráfico del sprite.
  • Byte 13: guarda los bytes que ocupa cada gráfico del sprite. Cada fase.
Empezamos con la FASE A. El byte 7=0 y no tenemos que hacer pausa. En FASE miramos si ya estamos en la última fase (byte 10). Si no es así, seguimos en LA3 con lo que incrementamos el contador de fase (byte 7) y sumamos al gráfico que tenemos que imprimir el tamaño del mismo. Osea, a la FASE A le sumamos 64, que es lo que nos ocupa el gráfico y lo guardamos otra vez en los bytes 4 y 5 para que se imprima ese gráfico, que no es otro que FASE B.
Si tuviera más fases esto se repetiría con la FASE C, FASE D, FASE E... Cuando el contador (byte 7) sea igual al número total de fases -1 (byte 10) pasaríamos a FASDER. Aquí se pone a cero el contador y en los bytes 4 y 5 se mete la primera fase del sprite (FASE A) que está guardada en  los bytes 8 y 9.


RUTINA GESTFAS



En cuanto al contador de pausa que se gestiona en GESTFAS funciona como sigue:

Lo primero que hacemos es ver si hay que cambiar la fase, ya que como comenté más arriba, no conviene cambiarla cada vez que se mueve el sprite. En este caso se cambia cada tres veces.
Metemos en A la pausa lo incrementamos y hacemos AND 3. Esto es un contador que cada tres veces vale cero.

 Los valores que puede tener son los siguientes:

0=00000000b
1=00000001b
2=00000010b
3=00000011b
4=00000100b

Como hacemos AND 3 (00000011b), el registro A sólo va a ser distinto de cero cuando valga 1,2 o 3. Si vale 4 al hacer el AND nos queda cero. Por lo que el contador vuelve a empezar y podemos cambiar de fase. Esto es una manera de usar contadores más rápida.


Con todo este lío no sé si te habrás dado cuenta de un detalle. El sprite del prota es un gráfico de 2x2 caracteres, por lo que debería ocupar: 2 caracteres de alto * 8 líneas que tiene cada caracter * 2 caracteres de ancho=32 bytes. Pero yo he puesto 64. Esto es porque esta rutina no maneja gráficos, maneja sprites (ya sé que hay controversia con estos términos, pero a mí me gusta esta distinción).
Los sprites son gráficos que se imprimen en la pantalla fusionándose con el fondo. Es decir tienen partes transparentes.Además, al moverse, respetan el fondo. Si te das cuenta, a través del agujero del donut se ven las líneas del fondo, lo mismo que en los bordes. Para conseguir este efecto, necesitamos lo que se llama máscara que nos indica qué parte del sprite es transparente. Esta máscara nos ocupa los mismos bytes que el gráfico. De ahí los 64 bytes.
Todo esto de la máscara se verá más adelante.


Para el control vertical del prota creo que no es necesaria ninguna explicación, más que los comentarios del código.







viernes, 27 de mayo de 2016

VOLCADO DEL BUFFER CON LDI (II): INICIO Y LECTURA DEL TECLADO



Como ya he dicho, el programa se ejecuta desde la dirección 24000. Esto corresponde a la memoria contenida. Lo primero que hacemos es deshabilitar las interrupciones, ya que no las vamos a usar de momento y colocar la pila.





Luego hay una serie de rutinas que sirven para crear las tablas que necesitamos para que el invento funcione y de momento no nos interesan.

A partir de la línea 102, tenemos lo siguiente:




Con XOR A, ponemos A=0 y con OUT (FEh),A ponemos el borde negro.

Llamamos a la rutina que nos borra la pantalla real:




Cargamos HL con el inicio de la pantalla real (4000h) y DE con HL+1 (4001h). Metemos en la primera dirección un cero y hacemos LDIR con BC igual al tamaño de la zona de los gráficos.

Para los atributos hacemos lo mismo desde el inicio de la zona de atributos, pero metemos un 6, con lo que pintamos la pantalla de amarillo.

Este método no es que sea muy optimizado, pero me da lo mismo, ya que aquí la velocidad no importa demasiado. Al borrar la pantalla real conviene hacer un HALT antes, para que el efecto sea más uniforme.

Después de esto, pintamos las rallas en el buffer y saltamos a INICIO que ya pertenece a la zona no contenida y que está en APRINCIPAL.ASM.

Para no liar mucho el asunto, voy a ir directamente a la lectura del teclado.




Todo lo concerniente a la teoría sobre la lectura del teclado lo puedes ver aquí.

Yo lo que hago es ir leyendo las letras que me interesan, OPQA y disparo y dependiendo de si están pulsadas o no, pongo a 1 un determinado bit del registro L. Ni que decir tiene que primero hay que poner L a cero...



Por ejemplo, si pulsamos a la vez la tecla derecha, la de arriba y el disparo, L=00011001b

En FINTEC paso el valor resultante de L al registro A, y lo guardo en la dirección JOY+1.

He dejado por ahí un código que en este ejemplo no uso, y es la lectura del Joystick Kempston en LEEJOY. Lo usaré más adelante.

El valor que voy dando al registro L según la tecla que esté pulsada no es trivial. Coincide con el valor que resulta de leer el Joystick, por lo que así unifico el resultado independientemente de que el personaje se maneje con teclado o Joystick.


jueves, 26 de mayo de 2016

VOLCADO DEL BUFFER CON LDI (I): ORGANIZACIÓN DE LA MEMORIA




Este primer ejemplo va a consistir en realizar un programa que nos maneja (e imprime) a un sprite protagonista que podemos mover con las teclas OPQA y dispara con espacio; seis sprites (cinco de 2x2 caracteres y uno de 3x3) que podríamos llamar enemigos y el disparo.

Si no lo has hecho antes, bájate el código fuente de aquí VOLCADO DEL BUFFER CON LDI.

¿Qué significa el título de esta entrada?
A la hora de mover gráficos por la pantalla del spectrum, la forma más habitual de hacerlo es imprimir el gráfico, borrar el gráfico, mover el gráfico y volverlo a imprimir. Esto es lo que da sensación de movimiento. El problema es que la pantalla del Spectrum se refresca cada cierto tiempo, y si el haz de electrones coincide (que es muy probable) con la zona de tu gráfico cuando estás borrando e imprimiendo, se produce un parpadeo.
Esto te viene muy bien explicado aquí.
En la mayoría de juegos antiguos (digamos antes de 1984) se pueden ver estos molestos parpadeos.
Se crearon técnicas para disimularlos, pero aún así se notaba algo raro en los gráficos.

Una manera de solucionar esto es realizar todas las operaciones necesarias que se hagan en un buffer. Reservamos una zona de la memoria para estos menesteres y cuando hemos acabado con todos los sprites la volcamos a la pantalla real.

El problema de esto es que es mucho más lento que imprimir directamente en la pantalla, ya que hay que mover muchos datos. Por ello, la zona de juego no conviene que sea muy grande.

Para este volcado se podría usar la instrucción LDIR, pero es más rápido usar tantos LDI como caracteres de ancho tenga el buffer.

Todo esto es lo que voy a intentar explicar en esta sección.

Para que veas cómo va la rutina, ensambla el programa ejecutando AENSAMBLA.BAT y ejecútalo en un emulador desde la dirección 24000. Podrás mover a nuestro héroe, disparar y ver a los enemigos.

ORGANIZACIÓN DE LA MEMORIA

A la hora de programar un juego es muy importante organizar bien la memoria para que luego no nos dé problemas. Echa un vistazo en APRINCIPAL.ASM.





  • Al principio del todo aparece un INCLUDE "CONTENIDA.ASM". Como he comentado anteriormente, la memoria contenida llega hasta la dirección 32768 (8000h). Yo uso desde la dirección 24000 (5dc0h). Es cierto que hay más memoria contenida que se puede usar, en concreto desde el buffer de la impresora 23296 (5B00h), pero no suelo usar esas direcciones. Es una manía. Sólo apuré tanto en Knightmare 2ZX, ya que anduve muy justo de memoria. Pero si nos hace falta, podremos tirar de ellas.

  • Después pongo el origen del código en 32768. A partir de aquí va el buffer de la pantalla que lo he llamado PANTVIR. Su tamaño es TAMPAN que si miras al principio de CONTENIDA no es otra cosa que multiplicar le número del columnas por el de filas de nuestro buffer.  


    En este caso, el ancho de pantalla es de 24 caracteres, el alto es de 16, por lo tanto, el tamaño del buffer será 24*16*8=3072 bytes. Si buscas la etiqueta TAMPAN en APRINCIPAL.TXT, verás que tiene el valor C00h que en decimal es 3072. Vamos bien.

  • Al final de la memoria es donde coloco el resto del código que no está en el bucle principal.


    Incluyo las rutinas, los gráficos y las tablas para el control de los sprites.

  • Después va TVOLCADO. Es una tabla que creo en CONTENIDA con la primera dirección del cada línea de la pantalla real para que el volcado del buffer sea más rápido. Reservo 320 bytes.
  • Desde F500h hasta F800h está FONDOS, donde guardo el fondo del buffer que se machaca al pintar el sprite y que se recuperará después de volcar el buffer. Son 768 bytes pero depende del número de sprites que pinte y de su tamaño.
  • De F800h a FA00 están TABCOOR y TABCOO1, donde creo la tabla de direcciones de la primeras primeras líneas del buffer. Así, sabiendo la coordenada Y del sprite es muy rápido hallar la dirección donde imprimirle.
  • Para aprovechar memoria, y ya que de F800h a F900h hay 255 y sólo uso 192 como mucho (es la coordenada Y máxima que puedo usar) meto DATFON. Es una tabla que voy creando para meter los datos de los fondos guardados al imprimir cada sprite. Ocupa 6 bytes por sprite con lo que, en principio, con 60 bytes tenemos de sobra.
  • Lo mismo pasa con con SPIMP. Aprovecho 20 bytes para crear la cola de los sprites que tengo que imprimir. Son 2 bytes por sprite, osea que con 20 me vale
  • A partir de FA00h y hasta el final meto una tabla con los desplazamientos y rotaciones necesarios para mover los gráficos de dos en dos pixels como mínimo. Aunque nos ocupa memoria (unos 1500 bytes), nos ahorra una gran cantidad de tiempo.
  • El stack lo pongo al principio de CONTENIDA en FA00h ya que a partir de F900h sólo uso 192+20=212 bytes, con lo que tengo 43 para manejar la pila. SP siempre lo pongo en memoria NO contenida.

    Todo esto es sólo para comentar la estructuración de la memoria. En sucesivas entregas explicaré para que sirven todas estas tablas y cómo se usan