Í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