viernes, 22 de abril de 2016

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

Í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.

1 comentario: