Í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.
Sprites
NdT: El tema del manejo de la pantalla en modo gráfico en los
Spectrum es complejo, el diseño económico de la ULA y la memoria de vídeo hizo
que se dividiera la pantalla en tres zonas de 64 líneas, y que en cada zona las líneas estuvieran
ordenadas por el barrido de carácter y no consecutivamente, de esta forma guarda las líneas en el orden Zona 1 [1-9-17...57 - 2-10-18...58 - 3-11-19..59 - 8,16, 24...64] Zona 2[65, 73... - 65, 74... - 66, 75...] Zona 3 [129... - 130...], separando la información
de color (zona de atributos) en zonas de 8x8 pixels justo después. Las pantallas de carga de los juegos lo hacen en este orden, por lo que se aprecia la distribución de líneas durante su carga. Revise alguna explicación mas detallada
para entenderlo, es vital de aquí en adelante.
Convirtiendo Posiciones de Pixel a Dirección de Pantalla
Los UDG y gráficos de carácter están muy bien, pero los mejores juegos suelen
utilizar sprites y no hay rutinas de la ROM para ayudarnos aquí, ya que Sir
Clive no diseñó el Spectrum como una máquina de juegos. Tarde o temprano un
programador de juegos tiene que afrontar el espinoso tema del torpe diseño de
la pantalla del Spectrum. Es un tema difícil convertir las coordenadas x
e y de pixels en direcciones de la pantalla, pero hay un par de métodos
que podemos emplear para hacerlo.
Usando una tabla de consulta de direcciones de la pantalla
El primer método es usar una tabla de direcciones pre calculadas, que contiene
la dirección de la pantalla para cada una de las 192 líneas del Spectrum, tal
como este o una similar:
xor a ; limpiar bandera de acarreo y el acumulador. ld d,a ; borrar el byte alto de DE. ld a,(xcoord) ; posición x. rla ; desplazar a la izquierda para multiplicar por 2. ld e,a ; ponerlo en el bit bajo del par DE. rl d ; desplazar el bit alto al byte alto. ld hl,addtab ; tabla de direcciones de pantalla. add hl,de ; apuntar a la entrada de la tabla. ld e,(hl) ; byte bajo de la dirección de pantalla. inc hl ; apuntar al byte alto. ld d,(hl) ; byte alto de la dirección de pantalla. ld a,(ycoord) ; posición horizontal. rra ; dividir por dos. rra ; y luego por cuatro. rra ; desplazar de nuevo para dividir por ocho. and 31 ; enmascarar la distancia hasta los bits desplazados. add a,e ; sumar a la dirección de inicio de la línea. ld e,a ; nuevo valor del registro E. ret ; volver con la dirección en la pantalla en DE. . . addtab defw 16384 defw 16640 defw 16896 . . .
En el lado positivo esto es muy rápido, pero significa tener que almacenar cada
una de las direcciones de las 192 líneas en una tabla, ocupando 384 bytes que
podrían estar mejor empleados en otros usos.
Calculando la Dirección de la Pantalla
El segundo método implica el cálculo de la dirección por nosotros mismos y no requiere
una consulta en la tabla de direcciones. Al hacer esto debemos tener en cuenta
tres cosas: en que tercio de la pantalla está el punto, que línea de carácter
es la mas cercana, y la línea de pixels sobre la que cae esa celda. El uso
juicioso del operador and nos ayudará a calcular los tres. Es un tema
complicado, así que por favor ten paciencia conmigo en mi esfuerzo para explicar
cómo funciona (NdT: y con el traductor que hace lo que puede).
Podemos establecer en cuál de los tres segmentos de la pantalla está situado
un punto tomando la coordenada vertical y enmascaramiento los seis bits menos
significativos para dejar un valor de 0, 64 o 128 para cada uno de los segmentos
separados de 64 pixels de alto. Como los bytes altos de las direcciones de los 3 segmentos
de la pantalla son 64, 72 y 80, con una diferencia de 8 al pasar de un segmento
a otro, se toma este valor enmascarado y se divide entre 8, lo que nos dará
un valor de 0, 8 o 16. A continuación, añadir 64 para darnos el byte alto del
segmento de pantalla.
Cada segmento se divide en 8 posiciones en celdas de caracteres, lo que son en total
32 bytes separados, por lo que para encontrar ese valor de nuestra dirección
tomamos la coordenada vertical, la máscara de distancia de los dos bits más
significativos se utiliza para determinar el segmento junto con los tres bits
menos significativos que determinan la posición de pixel. La instrucción
and 56 lo harán muy bien. Esto nos da la posición de celda de carácter como
un múltiplo de 8, y como las líneas de caracteres son de 32 bytes separados,
multiplicamos esto por 4 y colocamos nuestro número en el byte bajo de la dirección
de la pantalla.
Por último, las celdas de caracteres se dividen en líneas de pixels de 256
bytes de diferencia, por lo que una vez más tomamos su coordenada vertical,
con la máscara de distancia excepto los bits que determinan el uso de la línea
usando and 7, y añadimos el resultado al byte alto. Eso nos dará nuestra
dirección vertical de la pantalla. A partir de ahí tomamos nuestra coordenada
horizontal, se divide por 8 y lo añadimos a nuestra dirección.
Aquí presento una rutina que devuelve una dirección de pantalla para las coordenadas
(x, y) ubicadas en el par de registro DE. Se podría modificar fácilmente
para devolver la dirección en los registros HL o BC si se desea.
scadd ld a,(xcoord) ; recuperar la coordenada vertical. ld e,a ; guardarla en e. ; Encuentra línea dentro de la celda. and 7 ; linea 0-7 en el cuadrado de caracteres. add a,64 ; 64 * 256 = 16384 = inicio de la pantalla. ld d,a ; d = linea * 256. ; Encuentra en que tercio de la pantalla estamos. ld a,e ; restaurar la vertical. and 192 ; segmento 0, 1 o 2 multiplicamos por 64. rrca ; dividirlo por 8. rrca rrca ; segmento 0-2 multiplicado por 8. add a,d ; sumar a+d obtiene dirección de inicio del segmento. ld d,a ; Encuentra la celda de carácter dentro del segmento. ld a,e ; 8 casillas de caracteres por segmento. rlca ; dividir x por 8 y multiplicarlo por 32, rlca ; siguiente cálculo: multiplicar por 4. and 224 ; enmascarar los bits que no queremos. ld e,a ; cálculo de la coordenada vertical completo. ; Agregar el elemento horizontal. ld a,(ycoord) ; coordenada y. rrca ; sólo es necesario dividir por 8. rrca rrca and 31 ; cuadrados 0 a 31 a través de la pantalla. add a,e ; añadir al total de la medida. ld e,a ; de = dirección de la pantalla. ret
Moviendo
Una vez que se ha establecido la dirección tenemos que considerar cómo desplazar
nuestros gráficos de su posición. Los tres bits bajos de la coordenada horizontal
indican cuantos cambios de píxeles se necesitan. Una forma lenta para representar
un píxel sería hacer una llamada a la rutina SCADD anterior, realizar un
and 7 en la coordenada horizontal, a continuación desplazar a la derecha
un píxel de cero a siete veces en función del resultado antes de volcar a la
pantalla
Una rutina de desplazamiento de sprites funciona de la misma manera. La imagen
gráfica se toma de la memoria una línea a la vez, se desplaza a su posición
y luego se coloca en la pantalla antes de pasar a la siguiente línea de abajo
y repetir el proceso. Podríamos escribir una rutina de sprites que calcula la
dirección de la pantalla para cada línea trazada, y de hecho, la primera rutina
de sprites que escribí trabajaba de esa manera. Un método simple consiste
en determinar si nos estamos moviendo dentro de una celda de carácter, cruzando
los carácter límites de las celdas, o cruzando un límite de segmento con un
par de instrucciones and para aumentar o disminuir la dirección de la
pantalla en consecuencia. Poner simplemente and 63 devolverá cero si
la nueva posición vertical cruza un segmento, and 7 devolverá cero si
está cruzando un límite de celda de carácter, y cualquier otra cosa significa
que una nueva línea se encuentra dentro de la misma celda de carácter que la
línea anterior.
Esta es una rutina de movimiento de sprites que hace uso de la rutina SCADD
anterior. Para usarla, simplemente configurar las coordenadas en dispx
y dispy, apuntar el par de registros bc al gráfico del sprite,
y call sprite:
sprit7 xor 7 ; complementa los últimos 3 bits. inc a ; ¡agrega uno por suerte! sprit3 rl d ; rotar izquierda... rl c ; ...en el centro del byte... rl e ; ...y a la izquierda de la celda de carácter. dec a ; contar los cambios que hemos hecho. jr nz,sprit3 ; regresar hasta que los movimientos estén completos. ; Línea de la imagen de sprite ahora en e+c+d, lo necesitamos en forma c+d+e ld a,e ; borde izquierdo de la imagen está en e. ld e,d ; poner borde derecho en su lugar. ld d,c ; bit central va en d ld c,a ; y el borde izquierdo de nuevo en c. jr sprit0 ; hemos hecho el cambio para transferir a la pantalla. sprite ld a,(dispx) ; dibuja el sprite (hl). ld (tmp1),a ; guardar vertical. call scadd ; calcular dirección de la pantalla. ld a,16 ; altura del sprite en pixels. sprit1 ex af,af' ; guardar el contador de bucles. push de ; guardar dirección de pantalla. ld c,(hl) ; primer gráfico del sprite. inc hl ; incrementar el puntero de datos del sprite. ld d,(hl) ; siguiente bit de la imagen del sprite. inc hl ; apuntar a la siguiente fila de datos del sprite. ld (tmp0),hl ; guardar en tmp0 para más adelante. ld e,0 ; byte derecho en blanco por ahora. ld a,b ; b guarda la posición y. and 7 ; ¿estamos a caballo entre celdas de caracteres? jr z,sprit0 ; no estamos a caballo, no molesta al desplazamiento. cp 5 ; ¿necesitamos 5 o más desplazamientos a la derecha? jr nc,sprit7 ; sí, desplazar a la izquierda que es más rápido. and a ; Uy, la bandera de acarreo se establece. sprit2 rr c ; rotar a la izquierda el byte derecho... rr d ; ...Hasta el byte medio... rr e ; ...la byte derecho. dec a ; un turno menos que hacer. jr nz,sprit2 ; repetir hasta que todos los turnos estén completos. sprit0 pop hl ; sacar la dirección de la pantalla en la pila. ld a,(hl) ; ya está lista. xor c ; fusionar con los datos de la imagen. ld (hl),a ; colocar en la pantalla. inc l ; siguiente celda de carácter por la derecha. ld a,(hl) ; ya estaba antes. xor d ; fusionarse con el centro de la imagen. ld (hl),a ; poner de nuevo en la pantalla. inc hl ; siguiente bit del área de la pantalla. ld a,(hl) ; lo que ya está allí. xor e ; borde derecho de los datos de imagen del sprite. ld (hl),a ; poner en pantalla. ld a,(tmp1) ; coordenada vertical temporal. inc a ; siguiente línea de abajo. ld (tmp1),a ; almacenar nueva posición. and 63 ; ¿nos movemos al siguiente tercio de pantalla? jr z,sprit4 ; sí, encontrar el próximo segmento. and 7 ; ¿entrando en celda de carácter siguiente? jr z,sprit5 ; Sí, encuentre siguiente fila. dec hl ; izquierda 2 bytes. dec l ; es está el límite a horcajadas de 256 bytes aquí. inc h ; siguiente fila de esta celda de carácter. sprit6 ex de,hl ; Dirección de pantalla en de. ld hl,(tmp0) ; restaurar la dirección del gráfico. ex af,af' ; restaurar el contador del bucle. dec a ; decrementarlo. jp nz,sprit1 ; no alcanzado el borde inferior del sprite, repetir. ret ; trabajo hecho. sprit4 ld de,30 ; el siguiente segmento es de 30 bytes. add hl,de ; añadir a la dirección de la pantalla. jp sprit6 ; repetir. sprit5 ld de,63774 ; menos 1762. add hl,de ; restar 1762 de la dirección física de la pantalla. jp sprit6 ; volver al bucle.
Como puedes ver, esta rutina utiliza la instrucción XOR para combinar el sprite
con el fondo de la pantalla, lo que funciona de la misma manera que PRINT OVER 1
en el sinclair BASIC. El sprite se fusionó con los gráficos que ya están presentes
en la pantalla, lo que puede parecer desordenado. Para borrar un sprite solo
hay que volver a mostrarlo y la imagen se desvanece mágicamente.
Si quisiéramos dibujar un sprite encima de algo que ya está en la pantalla necesitaríamos
algunas rutinas adicionales, aunque similares a la anterior. Una podría usarse para almacenar los gráficos en la pantalla en una memoria intermedia, de modo
que la parte de la pantalla podría ser redibujada cuando se elimina el sprite.
La siguiente rutina aplicaría una máscara al sprite para eliminar los píxeles
de alrededor y de detrás del sprite usando and u or, a continuación
el sprite finalmente podría ponerse encima. Otra rutina podría ser necesaria
para restaurar la parte pertinente de la pantalla a su estado anterior cuando
se suprime el sprite. Sin embargo, esto llevaría mucho tiempo de CPU para conseguirlo,
así que mi consejo sería no molestase a no ser que el juego use algo llamado
doble buffer (también conocida como la técnica de la pantalla trasera), o si
está utilizando un sprite pre-desplazado, lo que discutiremos en breve.
Otro método que puedes considerar es la posibilidad de hacer sprites que parecen
pasar por detrás de los objetos del fondo, un truco que puedes haber visto en
"Haunted House" o en "Egghead in Space". Si bien este método es útil para reducir
el choque de colores, requiere usar una parte considerable de la memoria. En
ambos juegos una pantalla de máscara ficticia de 6Kb se encuentra en la dirección
24576, y cada byte de datos del sprite se añade a los datos en la pantalla oculta
antes de forzar su salida a la pantalla física ubicada en la dirección 16384.
Debido a que la pantalla física y la pantalla de máscara ficticia eran exactamente
de 8K era posible intercambiarlas entre ellas con alternar el bit 5 del registro
h. Para hacer esto en la rutina de sprites anterior nuestro sprit0 podría tener
este aspecto:
sprit0 pop hl ; sacar la dirección de la pantalla en la pila. set 5,h ; dirección de la pantalla replicada. ld a,(hl) ; ya está lista. and c ; fusionar con los datos de la imagen. res 5,h ; dirección de la pantalla física. xor (hl) ; mezclar en los datos de la imagen. ld (hl),a ; colocar en la pantalla. inc l ; siguiente celda de carácter por la derecha. set 5,h ; dirección de la pantalla replicada. ld a,(hl) ; ya estaba antes. and d ; encarcara con el bit centrsl de la imagen. res 5,h ; dirección de la pantalla física. xor (hl) ; mezclar con los datos de la imagen. ld (hl),a ; fusionarse con el centro de la imagen. - eliminado - inc hl ; siguiente bit del área de la pantalla. set 5,h ; dirección de la pantalla replicada. ld a,(hl) ; lo que ya está allí. and e ; enmascara borde derecho de datos imagen del sprite. res 5,h ; dirección de la pantalla física. xor (hl) ; mezclar con los datos de la imagen. ld (hl),a ; poner en pantalla. ld a,(tmp1) ; coordenada vertical temporal.
Este es otro método para calcular la dirección de la siguiente línea inferior.
Como antes, lo calculamos desde nuestra dirección actual. Hacerlo así siempre
es bastante complicado por la forma en que la pantalla está estructurada. La
mayoría de las veces nuestra siguiente línea está a 256 bytes de la anterior,
pero cada vez que se cruce la frontera de celda de carácter tenemos que restar
1760, y cuando se mueve a un tercio de la pantalla a la siguiente tenemos que
añadir 32. Este pequeño fragmento calcula la dirección de la siguiente línea
en el par de registro HL sin alterar DE.
; Línea dibujada, ahora buscamos la siguiente dirección objetivo. inc h ; incrementar pixel. ld a,h ; obtener la dirección del pixel. and 7 ; ¿posición de carácter a caballo? ret nz ; no, estamos ya en la línea siguiente. ld a,h ; obtener la dirección del pixel. sub 8 ; restar 8 para el inicio del segmento. ld h,a ; nuevo byte alto de la dirección. ld a,l ; obtener byte bajo de dirección. add a,32 ; una linea abajo. ld l,a ; nuevo byte bajo. ret nc ; no se ha alcanzado todavía siguiente segmento. ld a,h ; dirección alta. add a,8 ; sumar 8 al siguiente segmento. ld h,a ; nuevo byte alto.
Sprites Pre-desplazados
Una rutina de movimiento para spriteS tiene una gran desventaja: su falta de
velocidad. Cambiar todos esos datos gráficos de posición de toma tiempo, y si
tu juego necesita muchos sprites que rebotan por la pantalla, debes considerar
el uso de sprites pre-desplazados en su lugar. Esto requiere hasta ocho copias
separadas de la imagen del sprite, una por cada una de las posiciones de pixel
desplazados (NdT: Se refiere a mover el sprite a través de los recuadros de caracter de 8x8 del Spectrum). Por lo general los sprites se mueven en saltos de 2 pixels y
así se usan solo 4 copias de cada sprite. Luego es simplemente cuestión de calcular
qué imagen del sprite utilizar en función de la alineación horizontal del sprite,
calcular la dirección de la pantalla, y copiar la imagen del sprite a la pantalla.
Si bien este método es mucho más rápido, es muy caro en términos de memoria.
Una rutina de pre-desplazamiento de sprites requiere 32 bytes para presentar un sprite de 16x16 pixels, un sprite pre-desplazado con 4 posiciones requiere
128 bytes para la misma imagen, ¡y si es de 8 posiciones la friolera de 256!
Escribir un juego de Spectrum es siempre un compromiso entre velocidad y memoria
disponible.
Puede que no quieras necesariamente la misma imagen del sprite en cada posición
pre-desplazada. Por ejemplo, cambiando la posición de las piernas de un sprite
en cada uno de las posiciones pre-desplazadas del sprite puedes animarlo para
parecer como si caminara de izquierda a derecha mientras se mueve por la pantalla.
Recuerda que deben coincidir con las piernas del personaje el número de pixels
que se mueve cada trama. Si va a mover un sprite 2 pixels cada trama, es importante
hacer que los miembros se muevan 2 pixels entre cuadros. Menos de que esto
hará que el sprite parezca como patinando sobre hielo, si es mas parecerá estar
luchando para lograr agarrarse. Te voy a contar un pequeño secreto: lo creas
o no esto puede afectar en realidad a la forma en que el juego se percibe, así
que conseguir la animación adecuada es importante.
El método de los Bytes de exploración
Si estás mostrando sprites directamente en la pantalla y deseas mostrar más
de un par, es posible que encuentres un problema. Borrar, mover y volver a mostrar
necesita tiempo para completarse, y mientras esto sucede la línea de exploración
del televisión está en movimiento. Si la línea de exploración alcanza al sprite
mientras se visualiza o se elimina, se puede experimentar un parpadeo. Una forma
de evitar esto es utilizar el método del byte de exploración o una variación
del mismo.
El método del byte de exploración consiste en dibujar el nuevo sprite mientras
se borra al mismo tiempo el antíguo, byte a byte, o línea por línea. Si la línea
de exploración alcanza a tu sprite, el único pequeño parpadeo que se ve es el
del byte o línea en que se está en ese momento. Por tanto en realidad no importa
cuántos sprites tienes en la pantalla o dónde están, porque nunca parpadean.
El lado negativo es que esto puede ser difícil de poner en práctica, ya que
consiste en mantener dos copias de cada coordenada de los sprites, la imagen,
el marco y toda la información relevante, y requiere una gran cantidad de registros.
Afortunadamente, el Z80A tiene una instrucción muy útil para intercambiar entre
los dos bancos de registros muy rápidamente: EXX.
Una vez que hemos llamado a las rutinas para elaborar los datos gráficos de tu antiguo
sprite en de, con la dirección de la pantalla en hl
y tal vez una máscara en bc, usaríamos entonces la instrucción
exx para intercambiarlos con el conjunto de registros alternativos,
para a continuación llamar a las rutinas de nuevo para poner el nuevo puntero
de datos del sprite, la dirección y la máscara de pantalla en los registros
hl y bc. A continuación, se trata de dibujar
cada línea a la vez, cálcular la dirección de la siguiente línea de la pantalla,
cambiar los registros con exx, trazar la línea del nuevo sprite,
cálculo de la siguiente dirección de pantalla, y volver a intercambiar los registros
de nuevo. Se repite en un bucle este proceso y va a reemplazar un sprite sin
tener que eliminarlo completamente de la pantalla.
No olvides que también debes disponer de una rutina sencilla de manejo de sprites
que presente un sprite sin borrar el antiguo, o que borre un sprite sin que
aparezca de nuevo. De lo contrario, no serás capaz de poner nuevos sprites en
la pantalla o eliminarlos cuando ya no son necesarios.
Genial como de costumbre,ya...
ResponderEliminarGracias!
No me deja poner nada de lo que dices que hay que poner, no sé donde hay que ponerlo, yo cojo el Spectrum, pulso enter para entrar en basic y luego cuando escribo lo que pones, me pone qwe hay un fallo.
ResponderEliminar