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