martes, 3 de mayo de 2016

Cómo escribir juegos para el ZX Spectrum. Capítulo 13

Índice de entradas

Esta serie de artículos han sido traducidos a partir del documento "How to Write ZX Spectrum Games" con permiso de su autor, Jonathan Cauldwell, un gran desarrollador de juegos para el Spectrum, os recomiendo visitar su Web donde está el texto original. El documento original, y por tanto esta traducción, tiene © Jonathan Cauldwell y solo puede duplicarse con permiso expreso por escrito de su autor.

Doble Buffer

Hasta ahora hemos dibujado todos nuestros gráficos directamente en la pantalla, por razones de velocidad y simplicidad. Sin embargo hay una desventaja importante de este método: si la línea de exploración del televisión está cubriendo el área de pantalla en particular en la que se va a suprimir o volver a dibujar nuestra imagen, aparecerán nuestros gráficos con parpadeo. Por desgracia, en el Spectrum no hay una manera fácil de saber donde está la línea de exploración en un momento dado, así que tenemos que encontrar una manera de evitar esto. Un método que funciona bien es borrar y volver a dibujar todos los sprites inmediatamente después de una instrucción halt, antes de que el haz de exploración tenga oportunidad de ponerse al día para seguir dibujando la imagen. La desventaja de este método es que nuestro código para los sprites tiene que ser bastante rápido, e incluso en ese caso no es recomendable eliminar y volver a dibujar más de dos sprites por cuadro porque para entonces el haz de exploración estarán ya bajo el borde superior y en el área de la pantalla. Por supuesto, colocar el panel de estado en la parte superior de la pantalla podría dar un poco más de tiempo para dibujar nuestros gráficos, y si el juego corre a 25 fotogramas por segundo podríamos emplear una segunda instrucción halt y maniobrar otro par de sprites inmediatamente después. En última instancia, llega un punto en el que esto se rompe. Si nuestros gráficos van a tomar un poco más de tiempo para dibujarse, necesitamos otra manera de ocultar el proceso al jugador, necesitamos usar una segunda pantalla de búfer. Esto significa que todo el trabajo involucrado en el dibujo y borrado de gráficos está oculto al jugador y solo son visibles los cuadros terminados una vez que se han dibujado.

Hay dos maneras de hacer esto en un Spectrum. Un método sólo funcionará en una máquina con 128K, por lo que vamos a dejarlo de lado por el momento. El otro método en la práctica es más complicado pero funcionará en cualquier Spectrum.

Creando un Búfer de Pantalla

La forma más sencilla de implementar el doble buffer en un Spectrum 48K es la creación una pantalla ficticia en otro lugar de la memoria RAM, y dibujar todos los gráficos de fondo y los sprites ahí. Tan pronto como se haya completado nuestra pantalla copiamos esta pantalla ficticia a la pantalla física en la dirección 16384 haciendo:

.
.
; código para dibujar todos nuestros sprites etc.
.
.
.
.
; ahora la pantalla se dibuja copiándola a la pantalla física.

       ld hl,49152
       ld de,16384
       ld bc,6912
       ldir

Aunque en teoría esto es perfecto, en la práctica copiar 6912 bytes de RAM (o 6144 bytes si ignoramos los atributos de color) de la visualización de la pantalla en cada cuadro es demasiado lento para los juegos de arcade. El secreto consiste en reducir la cantidad de pantalla RAM que se necesita copiar en cada cuadro y encontrar la forma más rápida para transferirla en lugar de la instrucción LDIR.

La primera vía consiste en decidir el tamaño de la pantalla de vamos a ver. La mayoría de juegos separan la pantalla en 2 zonas: un panel de estado para mostrar puntuación, vidas y otros elementos de información y una ventana donde se lleva a cabo toda la acción. Como no necesitamos actualizar el panel de estado en cada trama, nuestra pantalla ficticia sólo tiene que ser tan grande como la ventana de acción. Así que si tuvieramos un panel de estado de 80 x 192 pixel en el borde derecho de la pantalla, nos dejaría una ventana de 176x192 píxeles, es decir, nuestra pantalla simulada solamente tendría que ser de 22 caracteres de ancho por 192 píxeles de alto, o 22x192 = 4224 bytes. El desplazamiento manual de 4224 bytes de una parte a otra de la RAM es mucho menos costoso que la manipulación de 6114 bytes. El truco es encontrar un tamaño que sea lo suficientemente grande como para no restringir el juego, y lo suficientemente pequeño para ser manipulado rápidamente. Por supuesto, también es posible que desees hacer el buffer un poco más grande por los bordes. Si bien estos bordes no se muestran en la pantalla son útiles si se quiere recortar sprites a medida que avanzan en la ventana de acción por los lados.

Una vez que hemos establecido definitivamente el tamaño de nuestro búfer, necesitamos escribir una rutina para transferirlo a la pantalla física uno o dos bytes a la vez. Mientras estamos en eso, también puedes volver a ordenar nuestra pantalla intermedia utilizando un método de visualización más lógico que el utilizado por la pantalla física. Podemos hacer concesiones al peculiar ordenamiento de la memoria de pantalla del Spectrum en nuestra rutina de transferencia, es decir, cualquier rutina de gráficos que haga uso de nuestra memoria de pantalla ficticia se pueden simplificar.

Hay dos maneras muy rápidas de mover una pantalla ficticia a la pantalla de visualización. El primer y más sencillo método es el uso de una gran cantidad de instrucciones LDI desenrolladas. El segundo y más complicado hace uso de PUSH y POP para transferir los datos.

Comencemos con LDI. Si nuestro buffer es de 22 caracteres de ancho podríamos transferir una sola línea de la memoria intermedia a la pantalla de visualización con 22 instrucciones consecutivas LDI, es mucho más rápido usar una gran cantidad de instrucciones LDI en lugar de utilizar un único LDIR. Podríamos escribir una rutina para transferir nuestros datos a partir de una sola línea a la vez, apuntando con HL al comienzo de cada línea de la memoria intermedia, con DE a la línea en la pantalla donde hay que ubicarlo, y luego usar 22 instrucciones LDI para mover los datos. Sin embargo como cada instrucción LDI toma dos bytes de código, es lógico pensar que tal rutina sería al menos de dos veces el tamaño de la memoria intermedia a mover. Un considerable golpe cuando manejamos un poco más de 40K de memoria RAM útil. En su lugar, puede que desees mover las instrucciones LDI a una subrutina que copia a la vez una línea de píxeles, o tal vez un grupo de 8 líneas de píxeles. Esta rutina podría entonces ser llamada desde dentro de un bucle, desenrollado o no, lo que podría hacerse cargo de los registros HL y DE. (NdT: El desenrollado de bucles es un técnica de aceleración usada mucho en código máquina para mejorar la velocidad del programa, consisten en reemplazar el bucle por la repetición del cuerpo del mismo las veces necesarias, de esta manera se eliminan saltos que ralentizan, a cambio de ocupar mas memoria con el programa).

El segundo método consiste en transferir la pantalla virtual a la real usando instrucciones PUSH y POP. Si bien esto tiene la ventaja de ser la manera más rápida de hacerlo, hay algunas desventajas. Necesitas control completo del puntero de pila por lo no se puede producir una interrupción a mitad de la rutina. El puntero de pila debe ser almacenado en alguna parte antes de empezar, y hay que restaurarlo inmediatamente después.

La pila del Spectrum se encuentra normalmente por debajo del código de tu programa, pero este método implica el establecimiento de la pila para que apunte a una parte de la memoria intermedia, para a continuación, utilizando POP, copiar el contenido de la pantalla ficticia en cada uno de los pares de registro a su vez. El puntero de pila se mueve entonces para apuntar a la RAM en la zona de la pantalla de visualización, antes de que los registros sean empujados a la memoria en orden inverso a aquel en el que fueron introducidos. Es decir, los valores se introducen en la memoria intermedia desde el comienzo de cada línea, y se empujan a la pantalla en el orden inverso, lo que va desde el final de la línea hasta su principio.

A continuación se muestra la parte esencial de la rutina de transferencia de pantalla del juego Rallybug. Este utiliza una memoria intermedia de 30 caracteres de ancho, con 28 caracteres visibles en la pantalla. Los restantes 2 caracteres no se muestran de manera que los sprites se mueven lentamente por la pantalla desde el borde, en lugar de aparecer de repente de la nada. Como el ancho de la pantalla visible es de 28 caracteres, esto requiere 14 registros de 16 bits por línea. Obviamente, el Z80A no tiene muchos registros, incluso contando los registros alternativos y los IX e IY. Por tanto, la rutina del Rallybug divide la pantalla en dos mitades de 14 bytes cada una, lo que requiere sólo 7 pares de registros. La rutina establece el puntero de pila al principio de cada línea de la memoria intermedia, para a continuación hacer POP de los datos en AF, BC, DE y HL. A continuación, intercambia estos registros con el conjunto de registros alternativos con EXX, y hace POP de 6 bytes más en BC, DE y HL. Estos registros deben ser ahora ser descargados en el área de la pantalla, por lo que el puntero de pila se establece en el punto del final de la línea de la pantalla correspondiente, y HL, DE y BC son "empujados" con PUSH a su posición, se restauran los registros alternativos,  HL, DE, AC y AF, que son respectivamente copiados a su posición. Esto se repite una y otra vez para cada mitad de cada línea de la pantalla, para al final restaurar el puntero de pila a su posición original.

Complicado, si, pero increíblemente rápido.

SEG1   equ 16514
SEG2   equ 18434
SEG3   equ 20482

P0     equ 0
P1     equ 256
P2     equ 512
P3     equ 768
P4     equ 1024
P5     equ 1280
P6     equ 1536
P7     equ 1792

C0     equ 0
C1     equ 32
C2     equ 64
C3     equ 96
C4     equ 128
C5     equ 160
C6     equ 192
C7     equ 224


xfer   ld (stptr),sp       ; guardar puntero de pila.

; Character line 0.

       ld sp,WINDOW        ; inicio del búferde la linea.
       pop af
       pop bc
       pop de
       pop hl
       exx
       pop bc
       pop de
       pop hl
       ld sp,SEG1+C0+P0+14 ; final de la línea de la pantalla.
       push hl
       push de
       push bc
       exx
       push hl
       push de
       push bc
       push af

       .
       .

       ld sp,WINDOW+4784   ; inicio del búfer de la linea.
       pop af
       pop bc
       pop de
       pop hl
       exx
       pop bc
       pop de
       pop hl
       ld sp,SEG3+C7+P7+28 ; final de la línea de la pantalla.
       push hl
       push de
       push bc
       exx
       push hl
       push de
       push bc
       push af

okay   ld sp,(stptr)       ; restaurar el puntero de pila.
       ret

Haciendo scroll en el búfer

Ahora que tenemos nuestra pantalla ficticia, podemos hacer cualquier cosa que nos guste en ella sin riesgo de parpadeo o de otras anomalías en los gráficos, ya que sólo transferimos el contenido a la pantalla física cuando hemos terminado de construir la imagen. Podemos colocar sprites, enmascarados o no, en cualquier lugar que nos guste y en el orden que nos guste. Nosotros podemos movernos alrededor de la pantalla, animar los gráficos de fondo, y lo más importante, ahora podemos hacer scroll en cualquier dirección.

Se requieren diferentes técnicas para diferentes tipos de desplazamiento, aunque todos tienen una cosa en común: como el desplazamiento es una tarea intensiva del procesador, los bucles desenrollados están a la orden del día. El tipo más simple de desplazamiento es un desplazamiento de pixeles individuales a izquierda/derecha. Un scroll a la derecha de un solo píxel nos obliga a establecer en el registro HL el comienzo de la memoria intermedia y, a continuación, ejecutar los dos operandos siguientes una y otra vez hasta llegar a la final del búfer:

       rr (hl)             ; rotar bandera de acarreo y 8 bits a la derecha.
       inc hl              ; siguiente dirección del búfer.

Del mismo modo, para ejecutar un scroll de un solo píxel hacia la izquierda pondremos en HL el último byte de la memoria y ejecutar estas dos instrucciones hasta llegar al comienzo del búfer:

       rl (hl)             ; rotar bandera de acarreo y 8 bits a la izquierda.
       dec hl              ; siguiente dirección del búfer.

La mayoría de las veces, sin embargo, podemos hacerlo solo incrementando o disminuyendo el registro l, en lugar del par hl, acelerando la rutina aún más. Esto tiene el inconveniente de tener que saber exactamente cuando cambiar el byte alto con los cambios de dirección. Por esta razón por lo general fijo mi dirección de búfer permanentemente justo al principio del proyecto, a menudo en la parte superior de RAM, así que no tengo que volver a escribir las rutinas de desplazamiento cuando las cosas cambian de lugar durante el transcurso del proyecto. Al igual que con la rutina para transferir el búfer a la pantalla física, un bucle desenrollado masivo es muy caro en términos de RAM, por lo que es buena idea escribir un bucle desenrollado más pequeño, que desplace por ejemplo 256 bytes a la vez, luego lo llamamos más o menos 20 veces, dependiendo del tamaño del buffer elegido.

Además del scroll de un píxel a la vez, podemos desplazar cuatro píxeles bastante rápidamente también. Mediante la sustitución de RL (HL) por RLD para el desplazamiento a la izquierda, y RR (HL) por RRD para el desplazamiento a la derecha, podemos mover 4 píxeles.

El desplazamiento vertical se realiza por desplazamiento de bytes sobre la RAM, de la misma forma que la rutina para transferir la pantalla ficticia hacia la física. Para desplazarse un píxel, fijamos nuestra dirección DESDE al inicio de la segunda línea de píxeles y el registro A con la dirección de comienzo de la memoria intermedia, a continuación copiamos los datos desde la dirección DESDE hacia la dirección A hasta llegar al final del búfer. Para desplazarse hacia abajo, tenemos que trabajar en la dirección opuesta, por lo que establecemos nuestro DESDE para que apunte al final de la penúltima línea de la memoria intermedia, y ahora A apunta a la dirección de nuestra última línea, y trabajamos hacia atrás hasta que se alcancen el inicio del búfer. La ventaja añadida del desplazamiento vertical es que podemos desplazarnos hacia arriba o hacia abajo más de una línea, simplemente alterando las direcciones y la rutina se ejecutará la misma rapidez. En términos generales, no es buena idea desplazarse más de un píxel si tu velocidad de cuadros es inferior a 25 cuadros por segundo, porque parecerá que la pantalla vibra.

Hay otra técnica que puede ser empleada para el desplazamiento vertical, y es la que empleé al escribir el juego Megablast para Your Sinclair (NdT: Se refiere a la revista inglesa Tu Sinclair). Implica el tratamiento de la pantalla intermedia como envuelta sobre si misma. En otras palabras, se utiliza la misma cantidad de RAM para la memoria de pantalla intermedia, pero la parte de la memoria intermedia que se comenzar a copiar a la parte superior de la pantalla puede cambiar de un cuadro al siguiente. Cuando llega al final del búfer, se salta de nuevo al principio. Con este sistema, la rutina para copiar el búfer toma la dirección de inicio de la memoria intermedia de un puntero de 16 bits que podría apuntar a cualquier línea en el búfer, y copia los datos en la pantalla física línea por la línea hasta que llega al final de la memoria intermedia. En este punto, la rutina copia los datos desde el principio de la memoria intermedia hacia el resto de la pantalla física. Esto hace que la transferencia de la rutina sea un poco más lenta, y complica cualquier otra rutina de gráficos, que también tienen que volver a la primera línea cada vez que llega a la última línea del búfer. Hacerlo, por otro lado, significa que no hay necesidad de desplazar datos para hacer scrool vertical de la pantalla. Al cambiar el puntero de 16 bits con la primera línea que se copia a la pantalla física, el desplazamiento se realiza automáticamente cuando el búfer se transfiere.

No hay comentarios:

Publicar un comentario