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




martes, 24 de mayo de 2016

ESTRUCTURA DEL CODIGO FUENTE Y ENSAMBLADOR

Las herramientas principales con las que trabajo son el editor de texto de Windows, el emulador Spectaculator, el ensamblador PASMO y para los gráficos, el editor SevenuP de Metalbrain.

Antes de seguir, te puedes descargar el PRIMER EJEMPLO que he realizado para el curso. De momento sólo vale para que entiendas PASMO y cómo organizo el código fuente.

Voy a explicar un poco cómo funciona PASMO.
Los archivos que pertenecen al ensamblador dentro de la carpeta BUFFER VOLCADO CON LDI, son: bin2tap.exe, pasmo.exe y AENSAMBLA.BAT.

Si editas AENSAMBLA.BAT verás la siguiente línea de comandos:





Cuando lo ejecutas, ensambla el código existente en  APRINCIPAL.ASM creando el código binario APRINCIPAL.bin y el fichero de texto APRINCIPAL.TXT que contiene las etiquetas usadas y su dirección correspondiente. Esto último nos será muy útil para depurar.

Prueba a borrar APRINCIPAL.bin y APRINCIPAL.TXT, y ejecuta  AENSAMBLA.BAT.

Si no ha habido ningún error, podrás ejecutar APRINCIPAL.bin en el emulador. En este caso debes poner la dirección de inicio de ejecución en 24000 que es donde empieza el programa.


En el directorio descargado ves que además hay distintos tipos de ficheros:


Ficheros *.ASM: Son los que ensambla PASMO

  • APRINCIPAL.ASM: Es el bucle principal. Dentro de él se pueden incluir otros *.ASM para que no resulte tan largo y se vea todo más claro. Para ello se usa la instrucción INCLUDE "*.ASM" donde se quiera meter el código. Por ejemplo al principio aparece INCLUDE "CONTENIDA.ASM".

  • CONTENIDA.ASM: Es el código que se va a incluir en la memoria contenida. Yo uso desde 24000 hasta 32768 que es donde acaba. El código contenido en estas direcciones va más lento, por lo que todo lo que se tenga que ejecutar con rapidez o a una velocidad constante se debe ensamblar por encima de 32768 (8000h). En CONTENIDA yo meto los menús, tablas de records, rutinas de preparación de tablas, borrado de pantalla...

  • ARUTINAS.ASM: En este fichero meto las rutinas y subrutinas que van fuera del bucle principal. Así el código está más claro y no me lío buscando dónde ponerlas.

  • ASPRITES.ASM: Aquí meto los gráficos de los Sprites

  • ATABLAS.ASM: Donde van las tablas para controlar los distintos Sprites, que ya explicaré más adelante
                   

Ficheros *.BIN: Son ficheros binarios que se incluyen en una determinada dirección del código.

     Se introducen en el código con la instrucción INCBIN   "*.BIN".
     En ASPRITES.ASM  aparecen unos cuantos metiendo los gráficos.


Bueno, pues si ejecutas APRINCIPAL.bin desde la dirección 24000, te aparecerá algo así como esto:




En las siguientes entradas veremos cómo funciona





INTRODUCCION

Mi intención al crear este blog es mostrar cómo se pueden hacer juegos en el ZX Spectrum. Quiero dejar bien claro desde el principio que voy a intentar explicar cómo los programo yo. Evidentemente, existen múltiples formas mejores y peores de hacerlo. Por eso me interesaría mucho que si encuentras cómo mejorar algo (que seguro lo encontrarás), lo compartas y así todos iremos aprendiendo más.

Otra cosa que me gustaría aclarar es que no pretendo hacer un "creador de juegos para Spectrum". Ya hay varios por ahí y algunos muy buenos como la Churrera de los Mojon Twins. Lo que me gustaría es que si te interesa este mundo APRENDAS a hacer juegos y a partir de ahí, crees tus propias rutinas. Sólo puedo decirte que esto engancha... y mucho.

En este blog no voy a entrar en cuestiones teóricas muy profundas. Primero porque no soy el más indicado para explicarlas, y segundo porque hay mucha información sobre ello en la web. Yo siempre uso como referencia el curso El wiki de speccy.org de Santiago Romero. Una verdadera joya para todo aquel interesado en saber cómo programar en el Spectrum. Muchas veces me remitiré a él. Si no tienes muchos conocimientos sobre lenguaje ensamblador del Z80 y el Spectrum échale un vistazo.

Mi filosofía a la hora de hacer juegos es que me interesa mucho la velocidad y fluidez de los mismos. Una buena idea y unos gráficos estupendos se pueden ir al traste si el juego va muy lento. En el Spectrum la memoria es escasa, pero yo creo que la velocidad lo es más. Por eso verás que uso técnicas como crear todo el programa lo más lineal posible sin saltos y con muy pocas subrutinas, nunca uso saltos relativos (JR n) ya que los absolutos (JP nn) son más rápidos aunque ocupen más memoria y me gusta depurar mucho las rutinas para conseguir velocidad. La experiencia me ha enseñado que llenar toda la memoria del Spectrum para un juego es complicado y  conseguir un poco más de velocidad lo es aún más. Osea, que programo usando mucha memoria pero con unos niveles de velocidad aceptables y si al final me faltan bytes, es cuando empiezo a recortar.

Algo muy interesante es ver cómo otros hacen los juegos. Cógete un emulador con un buen depurador (yo uso Spectaculator y me va muy bien) y destripa tus juegos favoritos. 

Espero que este blog te sea de utilidad y lo pases al menos tan bien como lo paso yo programando.