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.

jueves, 21 de abril de 2016

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

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

Detección simple de colisiones con aliens

Comprobación de coordenadas

La comprobación de coordenadas deberían poder explicarla por sí mismo la mayoría de los programadores, pero se ha incluido aquí en aras de la exhaustividad. También es el siguiente paso en el desarrollo de nuestro juego del ciempiés.

El tipo más simple de detección de colisión sería algo como esto, que se utiliza para detectar si dos UDG han chocado:

       ld a,(playx)        ; coordenada x del jugador.
       cp (ix+1)           ; comparar con x del alien.
       ret nz              ; no es la misma, no hay colisión.
       ld a,(playy)        ; coordenada y del jugador.
       cp (ix+2)           ; comparar con y del alien.
       ret nz              ; no es la misma, no hay colisión.
       jp collis           ; tenemos una colisión.

Bueno, es bastante simple, pero la mayoría de los juegos no utilizan una sola celda de carácter para los gráficos. ¿Qué pasaría si los aliens son de cuatro celdas de caracteres de ancho por dos de alto, y el personaje del jugador es de tres celdas de alto por tres de ancho? Tenemos que comprobar si alguna parte del alien ha colisionado con cualquier parte del jugador, por lo que tenemos que comprobar que las coordenadas están dentro de un cierto rango. Si el alien está más cerca de dos cuadrados por encima del jugador o menos de 3 por debajo, entonces las coordenadas verticales coinciden. Si el alien está también a menos de cuatro celdas a la izquierda del jugador, y menos de tres celdas a la derecha, la posición horizontal también se comparte y tenemos una colisión.

Vamos a escribir algo de código para hacer esto. Podemos empezar por tomar la coordenada vertical del jugador:

       ld a,(playx)        ; coordenada x del jugador.

Luego restar la posición vertical del alien:

       sub (ix+1)          ; restar la x del alien.

A continuación, resta uno de la altura del jugador y sumárselo.

       add a,2             ; el jugador es de 3 de alto, luego añadir 3 - 1 = 2

Si el alien se encuentra dentro del rango, el resultado será menor que la altura combinada del jugador y el alien, por lo que realizamos la comprobación:

       cp 5                ; la altura combinada es 3 + 2 = 5.
       ret nc              ; no en el rango vertical.

De forma similar, podemos seguir este código para comprobar la horizontal:

       ld a,(playy)        ; coordenada y del jugador.
       sub (ix+2)          ; restar la y del alien.
       add a,2             ; el jugador es de 3 de ancho, luego añadir 3 - 1 = 2
       cp 7                ; el ancho combinado es 3 + 4 = 7.
       ret nc              ; no en el rango horizontal.
       jp collis           ; tenemos una colisión.

Por supuesto, este método no solo trabaja con gráficos basados en caracteres, también funciona perfectamente bien con los sprites, pero eso lo veremos más adelante. Es hora de FINALIZAR nuestro juego del Centipede con un poco de detección de colisiones. Como nuestros gráficos son todos UDG de un solo carácter no necesitamos mucha fantasía, una rápida verificación de si x = x y de si y = y es lo que necesitamos.
numseg equ 8               ; numero de segmentos del ciempiés.

; Queremos una pantalla en negro.

       ld a,71             ; tinta blanca (7) en fondo negro (0),
                           ; con brillo (64).
       ld (23693),a        ; establecer nuestros colores de pantalla.
       xor a               ; forma rápida de cargar el acumulador con cero.
       call 8859           ; establecer el colore del borde permanente.

; Configurar los gráficos.

       ld hl,blocks        ; dirección de los datos para los UDG.
       ld (23675),hl       ; apuntar los UDG hacia aquí.

; De acuerdo, vamos a empezar el juego.

       call 3503           ; rutina ROM - borra la pantalla, abre el canal 2.            

       xor a               ; poner a cero el acumulador.
       ld (dead),a         ; poner el flag que indica que el jugador está vivo.

; Inicializar coordenadas.

       ld hl,21+15*256     ; cargar el par hl con las coordenadas iniciales.
       ld (plx),hl         ; fijar las coordenadas del jugador.
       ld hl,255+255*256   ; disparo del jugador por defecto.
       ld (pbx),hl         ; poner las coordenadas del disparo.

       ld b,10             ; número de segmentos a inicializar.
       ld hl,segmnt        ; tabla de segmentos.
segint ld (hl),1           ; comienza moviéndose a la derecha.
       inc hl
       ld (hl),0           ; comienza arriba.
       inc hl
       ld (hl),b           ; usa el registro B para la coordenada y.
       inc hl
       djnz segint         ; repetir hasta que todos se inicialicen.
       
       call basexy         ; establecer las posiciones x e y del jugador.
       call splayr         ; mostrar símbolo de la base del jugador.
       
; Ahora queremos llenar la zona de juegos con setas.

       ld a,68             ; tinta verde (4) en fondo negro (0),
                           ; con brillo (64).
       ld (23695),a        ; establecer nuestros colores temporales.
       ld b,50             ; comenzar con unas pocas.
mushlp ld a,22             ; código del carácter de control para AT.
       rst 16
       call random         ; obtener un número 'aleatorio'.
       and 15              ; en vertical en rango de 0 a 15.
       rst 16
       call random         ; obtener otro número seudo-aleatorio.
       and 31              ; horizontal en el rango de 0 a 31.
       rst 16
       ld a,145            ; el UDG 'B' es el gráfico de las setas.
       rst 16              ; poner la seta en la pantalla.
       djnz mushlp         ; bucle hasta que todas las setas aparezcan.

; Este es el bucle principal.

mloop  equ $

; Borrar el jugados.

       call basexy         ; establecer las posiciones x e y del jugador.
       call wspace         ; mostrar un espacio sobre el jugador.

; Ahora hemos eliminado el jugador y lo movemos antes de volverlo a mostrar
; en sus nuevas coordenadas.

       ld bc,63486         ; fila del teclado 1-5/joystick puerto 2.
       in a,(c)            ; ver que teclas están pulsadas.
       rra                 ; bit mas externo = tecla 1.
       push af             ; recordar el valor.
       call nc,mpl         ; si está siendo pulsada, moverse a la izquierda.
       pop af              ; restaurar el acumulador.
       rra                 ; siguiente bit (valor 2) = tecla 2.
       push af             ; recordar el valor.
       call nc,mpr         ; si está siendo pulsada, moverse a la derecha.
       pop af              ; restaurar el acumulador.
       rra                 ; siguiente bit (valor 4) = tecla 3.
       push af             ; recordar el valor.
       call nc,mpd         ; si está siendo pulsada, moverse hacia abajo.
       pop af              ; restaurar el acumulador.
       rra                 ; siguiente bit (valor 8) = tecla 4.
       push af             ; recordar este valor.
       call nc,mpu         ; si está siendo pulsada, moverse hacia arriba.
       pop af              ; restaurar el acumulador.
       rra                 ; ultimo bit (valor 16) leer tecla 5.
       call nc,fire        ; se ha pulsado, moverse arriba.

; Ahora que se ha movido podemos volver a mostrar al jugador.

       call basexy         ; establecer las posiciones x e y del jugador.
       call splayr         ; mostrar al jugador.

; Ahora para el disparo.  Primero vemos si ha golpeado algo.

       call bchk           ; ver la posición del disparo.

       call dbull          ; borrar disparo.
       call moveb          ; mover el disparo.
       call bchk           ; calcular nueva posición del disparo.
       call pbull          ; presentar el disparo en su nueva posición.

; Ahora los segmentos del ciempiés.

       ld ix,segmnt        ; tabla de datos del segmento.
       ld b,10             ; número de segmentos en la tabla.
censeg push bc
       ld a,(ix)           ; ¿está el segmento activado?
       inc a               ; 255=desactivado, incrementar a cero.
       call nz,proseg      ; si está activo, procesar el segmento.
       pop bc
       ld de,3             ; 3 bytes por segmento.
       add ix,de           ; poner el nuevo segmento en el registro ix.
       djnz censeg         ; repetir para todos los segmentos.
       
       halt                ; retardo.

       ld a,(dead)         ; ¿ha muerto el jugador por un segmento?
       and a
       ret nz              ; jugador muerto, perder una vida.

; Saltar de nuevo al principio del bucle principal.

       jp mloop

; Mover al jugador a la izquierda.

mpl    ld hl,ply           ; recordar, ¡y es la coordenada horizontal!
       ld a,(hl)           ; ¿cuál es el valor actual?
       and a               ; ¿es cero?
       ret z               ; sí - no podemos seguir hacia la izquierda.

; antes comprobar que no hay una seta en el camino.

       ld bc,(plx)         ; coordenadas actuales.
       dec b               ; mirar una posición a la izquierda.
       call atadd          ; obtener la dirección del atributo en esta posición.
       cp 68               ; las setas son brillo (64) + verde (4).
       ret z               ; hay una seta, no podemos movernos aquí.

       dec (hl)            ; restar 1 a la coordenada y.
       ret

; Mover al jugador a la derecha.

mpr    ld hl,ply           ; recordar, ¡y es la coordenada horizontal!
       ld a,(hl)           ; ¿cuál es el valor actual?
       cp 31               ; ¿está en el borde derecho (31)?
       ret z               ; sí - no podemos seguir hacia la derecha.

; antes comprobar que no hay una seta en el camino.

       ld bc,(plx)         ; coordenadas actuales.
       inc b               ; mirar una posición a la derecha.
       call atadd          ; obtener la dirección del atributo en esta posición.
       cp 68               ; las setas son brillo (64) + verde (4).
       ret z               ; hay una seta, no podemos movernos aquí.

       inc (hl)            ; sumar 1 a la coordenada y.
       ret

; Mover al jugador hacia arriba.

mpu    ld hl,plx           ; recordar, ¡x es la coordenada vertical!
       ld a,(hl)           ; ¿cuál es el valor actual?
       cp 4                ; ¿está en el límite superior (4)?
       ret z               ; sí - no podemos seguir hacia arriba.

; antes comprobar que no hay una seta en el camino.

       ld bc,(plx)         ; coordenadas actuales.
       dec c               ; mirar una posición hacia arriba.
       call atadd          ; obtener la dirección del atributo en esta posición.
       cp 68               ; las setas son brillo (64) + verde (4).
       ret z               ; hay una seta, no podemos movernos aquí.

       dec (hl)            ; restar 1 a la coordenada x.
       ret

; Mover al jugador hacia abajo.

mpd    ld hl,plx           ; recordar, ¡x es la coordenada vertical!
       ld a,(hl)           ; ¿cuál es el valor actual?
       cp 21               ; ¿está en el límite inferior (21)?
       ret z               ; sí - no podemos seguir hacia abajo.

; antes comprobar que no hay una seta en el camino.

       ld bc,(plx)         ; coordenadas actuales.
       inc c               ; mirar una posición hacia abajo.
       call atadd          ; obtener la dirección del atributo en esta posición.
       cp 68               ; las setas son brillo (64) + verde (4).
       ret z               ; hay una seta, no podemos movernos aquí.

       inc (hl)            ; sumar 1 a la coordenada x.
       ret

; Disparar un misil.

fire   ld a,(pbx)          ; coordenada vertical del proyectil.
       inc a               ; 255 es el valor por defecto, se incrementa a cero.
       ret nz              ; proyectil en pantalla, no puede volver a disparar.
       ld hl,(plx)         ; coordenadas del jugador.
       dec l               ; l esquina superior.
       ld (pbx),hl         ; establece las coordenadas del proyectil.
       ret

bchk   ld a,(pbx)          ; proyectil vertical.
       inc a               ; ¿Está a 255 (defecto)?
       ret z               ; si, no hay proyectil en la pantalla.
       ld bc,(pbx)         ; obtiene las coordenadas.
       call atadd          ; buscar el atributo de aquí.
       cp 68               ; seta es brillo (64) + verde (4).
       jr z,hmush          ; ¡Alcanzada una seta!
       ret

hmush  ld a,22             ; código de control para AT.
       rst 16
       ld a,(pbx)          ; proyectil vertical.
       rst 16
       ld a,(pby)          ; proyectil horizontal.
       rst 16
       call wspace         ; establecer el color de la tinta a blanco.
kilbul ld a,255            ; coordenada x a 255 = apagar el proyectil.
       ld (pbx),a          ; destruir el proyectil.
       ret

; Mover el proyectil arriba de la pantalla 1 carácter a la vez.

moveb  ld a,(pbx)          ; proyectil vertical.
       inc a               ; ¿Está a 255 (defecto)?
       ret z               ; si, no hay proyectil en la pantalla.
       sub 2               ; subir una línea.
       ld (pbx),a
       ret

; Configurar las coordenadas X e Y de la posición de la base del jugador,
; se le llama antes de la visualización y supresión de la base.

basexy ld a,22             ; código para AT.
       rst 16
       ld a,(plx)          ; coordenada vertical del jugador.
       rst 16              ; fijar la posición vertical del jugador.
       ld a,(ply)          ; coordenada horizontal del jugador.
       rst 16              ; fijar la posición horizontal del jugador.
       ret

; Mostrar al jugador en la posición de impresión actual.

splayr ld a,69             ; tinta cían (5) en fondo negro (0),
                           ; brillante (64).
       ld (23695),a        ; establecer nuestros colores temporales de pantalla.
       ld a,144            ; código ASCII para el UDG 'A'.
       rst 16              ; dibujar jugador.
       ret
       
pbull  ld a,(pbx)          ; proyectil vertical.
       inc a               ; ¿Está a 255 (defecto)?
       ret z               ; si, no hay proyectil en la pantalla.
       call bullxy
       ld a,16             ; carácter de control para la tinta.
       rst 16
       ld a,6              ; 6 = amarillo.
       rst 16
       ld a,147            ; el UDG 'D' se usa para presentar el disparo.
       rst 16
       ret

dbull  ld a,(pbx)          ; proyectil vertical.
       inc a               ; ¿Está a 255 (defecto)?
       ret z               ; si, no hay proyectil en la pantalla.
       call bullxy         ; establece las coordenadas del proyectil.

wspace ld a,71             ; tinta blanca (7) en fondo negro (0),
                           ; brillante (64).
       ld (23695),a        ; establecer nuestros colores temporales de pantalla.
       ld a,32             ; carácter de ESPACIO.
       rst 16              ; dibujar espacio.
       ret
       
; Establece las coordenadas x e y de la posición del  proyectil del jugador,
; se llama a esta rutina antes de presentar y borrar el proyectil.

bullxy ld a,22             ; código para AT.
       rst 16
       ld a,(pbx)          ; proyectil del jugador coordenada vertical.
       rst 16              ; establece la posición vertical del jugador.
       ld a,(pby)          ; posición horizontal del proyectil.
       rst 16              ; establece la coordenada horizontal.
       ret
       
segxy  ld a,22             ; código ASCII para el carácter de AT.
       rst 16              ; presentar código AT.
       ld a,(ix+1)         ; obtener la coordenada x del segmento.
       rst 16              ; posicionarse en esa coordenada.
       ld a,(ix+2)         ; obtener la coordenada y del segmento.
       rst 16              ; posicionarse en esa coordenada.
       ret

proseg call segcol         ; detección de colisión con un segmento.
       ld a,(ix)           ; verificar si el segmento está activo.
       inc a               ; para la rutina de detección de colisiones.
       ret z               ; está activo, por tanto pasa a muerto.
       call segxy          ; actualizar las coordenadas del segmento.
       call wspace         ; presentar un espacio, blanco sobre fondo negro.
       call segmov         ; mover el segmento.
       call segcol         ; mirar la colisión de nueva posición del segmento.
       ld a,(ix)           ; verificar si el segmento está activado.
       inc a               ; para la rutina de detección de colisiones.
       ret z               ; está activo, por tanto pasa a muerto.
       call segxy          ; actualizar las coordenadas del segmento.
       ld a,2              ; código de atributo = 2, segmento rojo.
       ld (23695),a        ; actualizar los atributos temporales.
       ld a,146            ; UDG 'C' para presentar el segmento.
       rst 16
       ret
segmov ld a,(ix+1)         ; coordenada x.
       ld c,a              ; área x GP.
       ld a,(ix+2)         ; coordenada y.
       ld b,a              ; área y GP.
       ld a,(ix)           ; indicador de estado.
       and a               ; ¿está el segmento en el borde izquierdo?
       jr z,segml          ; ir a la izquierda, saltar según ese bit de código.

; ¡ahora el segmento se mueve a la derecha!

segmr  ld a,(ix+2)         ; coordenada y.
       cp 31               ; ¿está en el borde derecho de la pantalla?
       jr z,segmd          ; si, mover el segmento hacia abajo.
       inc a               ; ver la izquierda.
       ld b,a              ; actualizar la coordenada y GP.
       call atadd          ; mirar el atributo de esa dirección.
       cp 68               ; setas son brillo (64) + verde (4).
       jr z,segmd          ; seta a la derecha, moverse hacia abajo.
       inc (ix+2)          ; sin obstáculos, seguir hacia la derecha.
       ret

; ¡ahora el segmento se mueve a la izquierda!

segml  ld a,(ix+2)         ; coordenada y.
       and a               ; ¿está en el borde izquierdo de la pantalla?
       jr z,segmd          ; si, mover el segmento hacia abajo.
       dec a               ; ver la derecha.
       ld b,a              ; actualizar la coordenada y GP.
       call atadd          ; mirar el atributo en la dirección (dispx,dispy).
       cp 68               ; setas son brillo (64) + verde (4).
       jr z,segmd          ; seta a la izquierda, moverse abajo.
       dec (ix+2)          ; sin obstáculos, seguir a la izquierda.
       ret

; ¡ahora el segmento se mueve hacia abajo!

segmd  ld a,(ix)           ; dirección del segmento.
       xor 1               ; cambiarla.
       ld (ix),a           ; guardar la nueva dirección.
       ld a,(ix+1)         ; coordenada y.
       cp 21               ; ¿llegamos al final de la pantalla?
       jr z,segmt          ; si, mover el segmento al inicio.


; En este momento nos estamos moviendo hacia abajo independientemente de las
; setas que pueden bloquear la trayectoria del segmento. Cualquier cosa en el
; camino del segmento será borrada.

       inc (ix+1)          ; no ha llegado a la parte inferior, bajar.
       ret

; mover segmento a la parte superior de la pantalla.

segmt  xor a               ; igual que ld a,0 pero ahorra 1 byte.
       ld (ix+1),a         ; nueva coordenada x = inicio de la pantalla.
       ret

; Detección de colisiones segmento.
; Comprueba la existencia de colisiones con el jugador y las balas del jugador.

segcol ld a,(ply)          ; bala posición y.
       cp (ix+2)           ; ¿es idéntico a la coordenada y del segmento?
       jr nz,bulcol        ; coordenada y diferente, mirar la bala en su lugar.
       ld a,(plx)          ; coordenada x del jugador.
       cp (ix+1)           ; ¿igual a la del segmento?
       jr nz,bulcol        ; coordenada x diferente, mirar la bala en su lugar.

; Así que tenemos una colisión con el jugador.

killpl ld (dead),a         ; Establecer indicador de jugador muerto.
       ret

; Vamos a ver de una colisión con el disparo del jugador.

bulcol ld a,(pbx)          ; bala posición x.
       inc a               ; ¿tiene el valor por defecto?
       ret z               ; si, no hay disparo que mirar.
       cp (ix+1)           ; ¿es coordenada x del disparo igual al segmento?
       ret nz              ; no, entonces no hay colisión.
       ld a,(pby)          ; bala posición y.
       cp (ix+2)           ; ¿es igual a la y del segmento?
       ret nz              ; no, esta vez no hay colisión.

; Tenemos una colisión con el disparo del jugador.

       call dbull          ; borrar disparo.
       ld a,22             ; código para AT.
       rst 16
       ld a,(pbx)          ; coordenada vertical de la bala del jugador.
       inc a               ; 1 linea abajo.
       rst 16              ; establecer posición vertical de la seta.
       ld a,(pby)          ; posición horizontal de la bala.
       rst 16              ; establecer la coordenada horizontal.
       ld a,16             ; código ASCII para controlar tinta.
       rst 16
       ld a,4              ; 4 = color verde.
       rst 16              ; ¡tenemos todas las setas de ese color!
       ld a,145            ; UDG 'B' es el gráfico de la seta.
       rst 16              ; poner la seta en la pantalla.
       call kilbul         ; matar el proyectil.
       ld (ix),a           ; matar al segmento.
       ld hl,numseg        ; numero de segmentos.
       dec (hl)            ; decrementarlo.
       ret

; Sencillo generador de números seudo-aleatorio.
; Seguir un puntero a través de la ROM (a partir de una semilla),
; retornando el contenido del byte en esa posición.

random ld hl,(seed)        ; puntero
       ld a,h
       and 31              ; mantenerlo en los primeros 8Kb de ROM.
       ld h,a
       ld a,(hl)           ; Obtener el número "aleatorio" de esa ubicación.
       inc hl              ; Incrementar el puntero.
       ld (seed),hl
       ret
seed   defw 0

; Calcular la dirección del atributo de carácter en (dispx, dispy).

atadd  ld a,c              ; coordenada vertical.
       rrca                ; multiplicar por 32.
       rrca                ; desplazar a la derecha con acarreo 3 veces
       rrca                ; mas rápido que desplazar izquierda 5 veces.
       ld e,a
       and 3
       add a,88            ; 88x256=dirección de los atributos.
       ld d,a
       ld a,e
       and 224
       ld e,a
       ld a,b              ; posición horizontal.
       add a,e
       ld e,a              ; de=dirección de los atributos.
       ld a,(de)           ; devolver atributo en el acumulador.
       ret

plx    defb 0              ; coordenada x del jugador.
ply    defb 0              ; coordenada y del jugador.
pbx    defb 255            ; coordenadas del disparo del jugador.
pby    defb 255
dead   defb 0              ; flag - el jugador muere cuando no sea cero.

; gráficos UDG.

blocks defb 16,16,56,56,124,124,254,254    ; base del jugador.
       defb 24,126,255,255,60,60,60,60     ; seta.
       defb 24,126,126,255,255,126,126,24  ; segmento.
       defb 0,102,102,102,102,102,102,0    ; proyectil del jugador.

; Tabla de segmentos.
; Formato: 3 bytes por entrada, 10 segmentos.
; byte 1: 255=segmento apagado, 0=izquierda, 1=derecha.
; byte 2: coordenada x (vertical).
; byte 3: coordenada y (horizontal).

segmnt defb 0,0,0          ; segmento 1.
       defb 0,0,0          ; segmento 2.
       defb 0,0,0          ; segmento 3.
       defb 0,0,0          ; segmento 4.
       defb 0,0,0          ; segmento 5.
       defb 0,0,0          ; segmento 6.
       defb 0,0,0          ; segmento 7.
       defb 0,0,0          ; segmento 8.
       defb 0,0,0          ; segmento 9.
       defb 0,0,0          ; segmento 10.
       


NdT: Esta es la última versión del juego, a partir de este punto lo que se habla en el resto del documento ya no se aplica a este juego, que se da por finalizado.

Pero, un momento, ¿por qué hay dos pruebas de detección de colisiones en lugar de una? Bueno, imagina que la base de la pistola del jugador está una celda de carácter a la izquierda de un segmento del ciempiés. El jugador se mueve a la derecha y el segmento se mueve hacia la izquierda. En la siguiente trama el segmento se movería a la celda ocupada por el jugador, y el jugador se movería a la posición ocupada por el segmento en el cuadro anterior, jugador y segmento de ciempiés pasarían directamente uno a través del otro y una comprobación de detección de colisión no puede dejar de tener esto en cuenta. Mediante la comprobación de colisión después de que el jugador se mueve, y luego otra vez después de que los segmentos de ciempiés se han movido, podemos evitar el problema.

Colisiones entre Sprites

Esto es suficiente para nuestro juego, pero la mayoría de juegos de Spectrum utilizan sprites en lugar de UDG, por lo que en el siguiente capítulo veremos cómo se pueden usar los sprites. Para la detección de colisión, el mismo principio de comprobación de coordenadas se puede utilizar para detectar colisiones entre sprites. Restar las coordenadas del primero de los sprites de los del segundo, verificar la diferencia y si está dentro de la gama de tamaños de los dos sprites combinados tenemos una colisión en ese eje. Una prueba de colisión simple para dos sprites de 16x16 pixel podría ser algo como esto:

; Verificar (l,h) para su colisión con (c,b), aplicación estricta.

colx16 ld a,l              ; coordenada x.
       sub c               ; restar x.
       add a,15            ; añadir distancia máxima.
       cp 31               ; ¿está en rango de x?
       ret nc              ; no,  están separados.
       ld a,h              ; coordenada y.
       sub b               ; restar y.
       add a,15            ; añadir distancia máxima.
       cp 31               ; ¿está en rango de y?
       ret                 ; poner el indicador de colisión desde aquí.

Hay una desventaja con este método. Si los sprites no se rellenan por completo a sus límites de 16x16 píxeles la detección de colisiones parece ser demasiado estricta, por lo que las colisiones se producen cuando los sprites están muy cerca, pero no cuando en realidad se están tocando. Una verificación un poco menos sensible implicaría recortar las esquinas de los sprites en una forma más octogonal, sobre todo si tienen tus sprites esquinas redondeadas. El siguiente proceso funciona mediante la adición a las coordenadas x e y de las diferencias y la comprobación de que están por debajo de un cierto límite. Para una colisión entre dos sprites de 16 x 16 la coordenada de máxima distancias son 15 píxeles para cada eje, mediante la comprobación de que las diferencias entre x e y son de 25 o menos estamos efectivamente recortando un triángulo de 5 x 5 x 5 píxeles de cada esquina.

; Verificar (l, h) para colisión con (c, b), recortando las esquinas.

colc16 ld a,l              ; coordenada x.
       sub c               ; restar x.
       jr nc,colc1a        ; ¿resultado positivo?
       neg                 ; pasar negativo a positivo.
colc1a cp 16               ; ¿en el rango de x?
       ret nc              ; no, están separados.
       ld e,a              ; guardar la diferencia.

       ld a,h              ; coordenada y.
       sub b               ; restar y.
       jr nc,colc1b        ; ¿resultado positivo?
       neg                 ; pasar negativo a positivo.
colc1b cp 16               ; ¿en el rango de y?
       ret nc              ; no, están separados.

       add a,e             ; añadir la diferencia en x.
       cp 26               ; ¿solo los 5 pixels de la esquina se tocan?
       ret                 ; actuar cuando hay colisión.

miércoles, 20 de abril de 2016

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

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

Tablas

Los aliens no llegan de uno en uno

Digamos, por poner un ejemplo, que estábamos escribiendo un juego de space invaders que muestra once columnas, cada una con cinco filas de invasores. No sería práctico escribir el código para cada uno de los cincuenta y cinco aliens en cada turno, lo que necesitamos es montar una tabla. En Sinclair BASIC podríamos hacerlo mediante la definición de tres tablas de cincuenta y cinco elementos (una para la coordenada x de los invasores, otra para la coordenada y, más una tercera con el estado). Podríamos hacer algo similar en ensamblador mediante la creación en memoria de tres tablas de cincuenta y cinco bytes cada una, y a continuación añadir el número de cada alien al inicio de cada tabla para acceder al elemento individual. Desafortunadamente, eso sería lento y engorroso.

Un método mucho mejor es agrupar los tres elementos de datos para cada invasor en una estructura, y luego montar cincuenta y cinco de estas estructuras en una tabla. Podemos entonces apuntar con hl a la dirección de cada invasor, y sabemos que hl apunta al byte de estado, hl más uno apunta a la coordenada x, hl mas dos apunta a la coordenada y. El código para mostrar un alien podría ser algo como esto:

       ld hl,aliens        ; estructura de datos para los aliens.
       ld b,55             ; número de aliens.
loop0  call show           ; presentar este alien.
       djnz loop0          ; repetir para todos los aliens.
       ret
show   ld a,(hl)           ; obtener el estado del alien.
       cp 255              ; ¿está el alien desactivado?
       jr z,next           ; si, entonces no presentarlo.
       push hl             ; guardar la dirección del alien en la pila.
       inc hl              ; apuntar a la coordenada x.
       ld d,(hl)           ; obtener la coordenada.
       inc hl              ; apuntar a la coordenada y.
       ld e,(hl)           ; obtener la coordenada.
       call disply         ; mostrar el alien en (d,e).
       pop hl              ; refrescar la dirección del alien desde la pila.
next   ld de,3             ; tamaño de cada alien en la tabla de entradas.
       add hl,de           ; apuntar al siguiente alien.
       ret                 ; mantiene hl apuntando al siguiente.

Usando los registros índice

El inconveniente con esta rutina es que tenemos que tener mucho cuidado de a donde está apuntando hl todo el tiempo (NdT: se refiere a que usando este método no podemos usar los registros HL para otra cosa) por lo que podría ser una buena idea almacenar hl en una posición de la memoria temporal de dos bytes antes de llamar a show, y al final del bucle restaurarla sumandole tres, para a continuación realizar la instrucción djnz. Si estábamos escribiendo para la Nintendo Game Boy con su Z80 recortado, esta sería probablemente nuestra mejor opción. En máquinas con procesadores más avanzados, como el Spectrum y CPC 464 podemos utilizar los registros índice ix para simplificar nuestro código un poco. Debido a que el par de registro ix nos permite desplazamos usando direccionamiento indirecto, podemos apuntar ix al comienzo de la estructura de datos de un alien y acceder a todos los elementos en ella sin la necesidad de cambiar de nuevo ix. Usando ix nuestra rutina de visualización de aliens podría tener este aspecto:

       ld ix,aliens        ; estructura de datos para los aliens.
       ld b,55             ; número de aliens.
loop0  call show           ; presentar este alien.
       ld de,3             ; tamaño de cada alien en la tabla de entradas.
       add ix,de           ; apuntar al siguiente alien.
       djnz loop0          ; repetir para todos los aliens.
       ret
show   ld a,(ix)           ; obtener el estado del alien.
       cp 255              ; ¿está el alien desactivado?
       ret z               ; si, entonces no presentarlo.
       ld d,(ix+1)         ; obtener la coordenada.
       ld e,(ix+2)         ; obtener la coordenada.
       jp disply           ; mostrar el alien en (d,e).

Usar ix significa que sólo la primera vez tienes que señalar el comienzo de la estructura de datos de un alien, por lo que siempre ix + 0 podrá devolver el estado para el invasor actual, ix + 1 la coordenada x, y así sucesivamente. Este método permite al programador utilizar estructuras de datos complejas para sus aliens de hasta 128 bytes de largo, sin confundirse en cuanto de a que punto de la estructura de nuestros registros se está apuntando en un momento dado, como en el ejemplo anterior con hl. Desafortunadamente, el uso de ix es un poco más lento que hl, por lo que no se debe utilizar para las tareas de procesamiento más intensivas, como la manipulación de gráficos.

Vamos a aplicar este método a nuestro juego del Centipede. En primer lugar, tenemos que decidir cuantos segmentos se necesitan, y qué datos almacenar sobre cada segmento. En nuestro juego los segmentos tendrán que desplazarse hacia la izquierda o hacia la derecha hasta que lleguen a una seta, y luego moverse hacia abajo y volver a la inversa. Así que parece que necesitaremos una variable bandera para indicar la dirección en que un segmento está viajando, además de una coordenada x o y. Nuestra variable también se puede utilizar para indicar que un segmento particular ha sido destruido. Con esto en mente, podemos establecer una estructura de datos de tres bytes:

centf  defb 0              ; flag, 0=izquierda, 1=derecha, 255=muerto.
centx  defb 0              ; coordenada x del segmento.
centy  defb 0              ; coordenada y del segmento.

Si optamos por tener diez segmentos en nuestro Centipede, hay que reservar espacio para una tabla de treinta bytes. Cada segmento tiene que ser inicializado al principio del juego, para después moverlo, mostrarlo y eliminarlo durante el juego.

La inicialización de los segmentos es probablemente la tarea más simple, por lo que podemos utilizar un simple bucle para incrementar el par de registros HL para que apunte a cada byte antes de ajustarlo. Algo parecido a lo esto se usa en este truco:

       ld b,10             ; número de segmentos a inicializar.
       ld hl,segmnt        ; tabla de segmentos.
segint ld (hl),1           ; comienza moviéndose a la derecha.
       inc hl
       ld (hl),0           ; comienza arriba.
       inc hl
       ld (hl),b           ; usa el registro B para la coordenada y.
       inc hl
       djnz segint         ; repetir hasta que todos se inicialicen.

El procesamiento y la visualización de cada segmento va a ser un poco más complicado, por lo que para eso vamos a utilizar los registros ix. Necesitamos escribir un algoritmo simple que manipula un solo segmento hacia la izquierda o hacia la derecha hasta que llega a una seta, y luego se mueve hacia abajo y cambia de dirección. Llamaremos a esta rutina PROSEG (por "procesar segmento"), y establecer un bucle que apunte a su vez a cada segmento y llame a PROSEG. Proporcionando el correcto algoritmo de movimiento, podremos ver a continuación un ciempiés que serpentea su camino a través de las setas. Aplicar esto a nuestro código es sencillo: comprobamos el byte de bandera para cada segmento (ix) para ver de qué manera el segmento se está moviendo, incremento o decremento  según la dirección de la coordenada horizontal (ix + 2), a continuación comprobar el atributo en esa celda de carácter. Si es verde y negro incrementamos la coordenada vertical (ix + 1) y cambiamos el indicador de dirección (ix).

De acuerdo, hay algunas cosas más a considerar, como cuando golpea contra los lados o el fondo de la pantalla, pero eso es sólo un caso mas al comprobar las coordenadas del segmento y cambiar su dirección, o su traslado a la parte superior de la pantalla cuando sea necesario. Los segmentos también necesitan ser borrados de sus antiguas posiciones antes de ser trasladados y mostrarlos en sus nuevas posiciones, pero ya se han cubierto los pasos necesarios para realizar esas tareas.

Nuestro nuevo código es el siguiente:

; Queremos una pantalla en negro.

       ld a,71             ; tinta blanca (7) en fondo negro (0),
                           ; con brillo (64).
       ld (23693),a        ; establecer nuestros colores de pantalla.
       xor a               ; forma rápida de cargar el acumulador con cero.
       call 8859           ; establecer el colore del borde permanente.

; Configurar los gráficos.

       ld hl,blocks        ; dirección de los datos para los UDG.
       ld (23675),hl       ; apuntar los UDG hacia aquí.

; De acuerdo, vamos a empezar el juego.

       call 3503           ; rutina ROM - borra la pantalla, abre el canal 2.

; Inicializar coordenadas.

       ld hl,21+15*256     ; cargar el par hl con las coordenadas iniciales.
       ld (plx),hl         ; fijar las coordenadas del jugador.

       ld b,10             ; número de segmentos a inicializar.
       ld hl,segmnt        ; tabla de segmentos.
segint ld (hl),1           ; comienza moviéndose a la derecha.
       inc hl
       ld (hl),0           ; comienza arriba.
       inc hl
       ld (hl),b           ; usa el registro B para la coordenada y.
       inc hl
       djnz segint         ; repetir hasta que todos se inicialicen.

       call basexy         ; establecer las posiciones x e y del jugador.
       call splayr         ; mostrar símbolo de la base del jugador.
       
; Ahora queremos llenar la zona de juegos con setas.

       ld a,68             ; tinta verde (4) en fondo negro (0),
                           ; con brillo (64).
       ld (23695),a        ; establecer nuestros colores temporales.
       ld b,50             ; comenzar con unas pocas.
mushlp ld a,22             ; código del carácter de control para AT.
       rst 16
       call random         ; obtener un número 'aleatorio'.
       and 15              ; en vertical en rango de 0 a 15.
       rst 16
       call random         ; obtener otro número seudo-aleatorio.
       and 31              ; horizontal en el rango de 0 a 31.
       rst 16
       ld a,145            ; el UDG 'B' es el gráfico de las setas.
       rst 16              ; poner la seta en la pantalla.
       djnz mushlp         ; bucle hasta que todas las setas aparezcan.

; Este es el bucle principal.

mloop  equ $

; Borrar el jugados.

       call basexy         ; establecer las posiciones x e y del jugador.
       call wspace         ; mostrar un espacio sobre el jugador.

; Ahora hemos eliminado el jugador y lo movemos antes de volverlo a mostrar
; en sus nuevas coordenadas.

       ld bc,63486         ; fila del teclado 1-5/joystick puerto 2.
       in a,(c)            ; ver que teclas están pulsadas.
       rra                 ; bit mas externo = tecla 1.
       push af             ; recordar el valor.
       call nc,mpl         ; si está siendo pulsada, moverse a la izquierda.
       pop af              ; restaurar el acumulador.
       rra                 ; siguiente bit (valor 2) = tecla 2.
       push af             ; recordar el valor.
       call nc,mpr         ; si está siendo pulsada, moverse a la derecha.
       pop af              ; restaurar el acumulador.
       rra                 ; siguiente bit (valor 4) = tecla 3.
       push af             ; recordar el valor.
       call nc,mpd         ; si está siendo pulsada, moverse hacia abajo.
       pop af              ; restaurar el acumulador.
       rra                 ; siguiente bit (valor 8) = tecla 4.
       call nc,mpu         ; si está siendo pulsada, moverse hacia arriba.

; Ahora que se ha movido podemos volver a mostrar al jugador.

       call basexy         ; establecer las posiciones x e y del jugador.
       call splayr         ; mostrar al jugador.

; Ahora los segmentos del ciempiés.

       ld ix,segmnt        ; tabla de datos del segmento.
       ld b,10             ; número de segmentos en la tabla.
censeg push bc
       ld a,(ix)           ; ¿está el segmento activado?
       inc a               ; 255=desactivado, incrementar a cero.
       call nz,proseg      ; si está activo, procesar el segmento.
       pop bc
       ld de,3             ; 3 bytes por segmento.
       add ix,de           ; poner el nuevo segmento en el registro ix.
       djnz censeg         ; repetir para todos los segmentos.
       
       halt                ; retardo.

; Saltar de nuevo al principio del bucle principal.

       jp mloop

; Mover al jugador a la izquierda.

mpl    ld hl,ply           ; recordar, ¡y es la coordenada horizontal!
       ld a,(hl)           ; ¿cuál es el valor actual?
       and a               ; ¿es cero?
       ret z               ; sí - no podemos seguir hacia la izquierda.

; antes comprobar que no hay una seta en el camino.

       ld bc,(plx)         ; coordenadas actuales.
       dec b               ; mirar una posición a la izquierda.
       call atadd          ; obtener la dirección del atributo en esta posición.
       cp 68               ; las setas son brillo (64) + verde (4).
       ret z               ; hay una seta, no podemos movernos aquí.

       dec (hl)            ; restar 1 a la coordenada y.
       ret

; Mover al jugador a la derecha.

mpr    ld hl,ply           ; recordar, ¡y es la coordenada horizontal!
       ld a,(hl)           ; ¿cuál es el valor actual?
       cp 31               ; ¿está en el borde derecho (31)?
       ret z               ; sí - no podemos seguir hacia la derecha.

; antes comprobar que no hay una seta en el camino.

       ld bc,(plx)         ; coordenadas actuales.
       inc b               ; mirar una posición a la derecha.
       call atadd          ; obtener la dirección del atributo en esta posición.
       cp 68               ; las setas son brillo (64) + verde (4).
       ret z               ; hay una seta, no podemos movernos aquí.

       inc (hl)            ; sumar 1 a la coordenada y.
       ret

; Mover al jugador hacia arriba.

mpu    ld hl,plx           ; recordar, ¡x es la coordenada vertical!
       ld a,(hl)           ; ¿cuál es el valor actual?
       cp 4                ; ¿está en el límite superior (4)?
       ret z               ; sí - no podemos seguir hacia arriba.

; antes comprobar que no hay una seta en el camino.

       ld bc,(plx)         ; coordenadas actuales.
       dec c               ; mirar una posición hacia arriba.
       call atadd          ; obtener la dirección del atributo en esta posición.
       cp 68               ; las setas son brillo (64) + verde (4).
       ret z               ; hay una seta, no podemos movernos aquí.

       dec (hl)            ; restar 1 a la coordenada x.
       ret

; Mover al jugador hacia abajo.

mpd    ld hl,plx           ; recordar, ¡x es la coordenada vertical!
       ld a,(hl)           ; ¿cuál es el valor actual?
       cp 21               ; ¿está en el límite inferior (21)?
       ret z               ; sí - no podemos seguir hacia abajo.

; antes comprobar que no hay una seta en el camino.

       ld bc,(plx)         ; coordenadas actuales.
       inc c               ; mirar una posición hacia abajo.
       call atadd          ; obtener la dirección del atributo en esta posición.
       cp 68               ; las setas son brillo (64) + verde (4).
       ret z               ; hay una seta, no podemos movernos aquí.

       inc (hl)            ; sumar 1 a la coordenada x.
       ret

; Configurar las coordenadas X e Y de la posición de la base del jugador,
; se le llama antes de la visualización y supresión de la base.

basexy ld a,22             ; código para AT.
       rst 16
       ld a,(plx)          ; coordenada vertical del jugador.
       rst 16              ; fijar la posición vertical del jugador.
       ld a,(ply)          ; coordenada horizontal del jugador.
       rst 16              ; fijar la posición horizontal del jugador.
       ret

; Mostrar al jugador en la posición de impresión actual.

splayr ld a,69             ; tinta cían (5) en fondo negro (0),
                           ; brillante (64).
       ld (23695),a        ; establecer nuestros colores temporales de pantalla.
       ld a,144            ; código ASCII para el UDG 'A'.
       rst 16              ; dibujar jugador.
       ret

wspace ld a,71             ; tinta blanca (7) en fondo negro (0),
                           ; brillante (64).
       ld (23695),a        ; establecer nuestros colores temporales de pantalla.
       ld a,32             ; carácter de ESPACIO.
       rst 16              ; dibujar espacio.
       ret
       
segxy  ld a,22             ; código ASCII para el carácter de AT.
       rst 16              ; presentar código AT.
       ld a,(ix+1)         ; obtener la coordenada x del segmento.
       rst 16              ; posicionarse en esa coordenada.
       ld a,(ix+2)         ; obtener la coordenada y del segmento.
       rst 16              ; posicionarse en esa coordenada.
       ret

proseg ld a,(ix)           ; verificar si el segmento está activo.
       inc a               ; para la rutina de detección de colisiones.
       ret z               ; está activo, por tanto pasa a muerto.
       call segxy          ; actualizar las coordenadas del segmento.
       call wspace         ; presentar un espacio, blanco sobre fondo negro.
       call segmov         ; mover el segmento.
       ld a,(ix)           ; verificar si el segmento está activado.
       inc a               ; para la rutina de detección de colisiones.
       ret z               ; está activo, por tanto pasa a muerto.
       call segxy          ; actualizar las coordenadas del segmento.
       ld a,2              ; código de atributo = 2, segmento rojo.
       ld (23695),a        ; actualizar los atributos temporales.
       ld a,146            ; UDG 'C' para presentar el segmento.
       rst 16
       ret
segmov ld a,(ix+1)         ; coordenada x.
       ld c,a              ; área x GP.
       ld a,(ix+2)         ; coordenada y.
       ld b,a              ; área y GP.
       ld a,(ix)           ; indicador de estado.
       and a               ; ¿está el segmento en el borde izquierdo?
       jr z,segml          ; ir a la izquierda, saltar según ese bit de código.

; ¡ahora el segmento se mueve a la derecha!

segmr  ld a,(ix+2)         ; coordenada y.
       cp 31               ; ¿está en el borde derecho de la pantalla?
       jr z,segmd          ; si, mover el segmento hacia abajo.
       inc a               ; ver la izquierda.
       ld b,a              ; actualizar la coordenada y GP.
       call atadd          ; mirar el atributo de esa dirección.
       cp 68               ; setas son brillo (64) + verde (4).
       jr z,segmd          ; seta a la derecha, moverse hacia abajo.
       inc (ix+2)          ; sin obstáculos, seguir hacia la derecha.
       ret

; ¡ahora el segmento se mueve a la izquierda!

segml  ld a,(ix+2)         ; coordenada y.
       and a               ; ¿está en el borde izquierdo de la pantalla?
       jr z,segmd          ; si, mover el segmento hacia abajo.
       dec a               ; ver la derecha.
       ld b,a              ; actualizar la coordenada y GP.
       call atadd          ; mirar el atributo en la dirección (dispx,dispy).
       cp 68               ; setas son brillo (64) + verde (4).
       jr z,segmd          ; seta a la izquierda, moverse abajo.
       dec (ix+2)          ; sin obstáculos, seguir a la izquierda.
       ret

; ¡ahora el segmento se mueve hacia abajo!

segmd  ld a,(ix)           ; dirección del segmento.
       xor 1               ; cambiarla.
       ld (ix),a           ; guardar la nueva dirección.
       ld a,(ix+1)         ; coordenada y.
       cp 21               ; ¿llegamos al final de la pantalla?
       jr z,segmt          ; si, mover el segmento al inicio.


; En este momento nos estamos moviendo hacia abajo independientemente de las
; setas que pueden bloquear la trayectoria del segmento. Cualquier cosa en el
; camino del segmento será borrada.

       inc (ix+1)          ; no ha llegado a la parte inferior, bajar.
       ret

; mover segmento a la parte superior de la pantalla.

segmt  xor a               ; igual que ld a,0 pero ahorra 1 byte.
       ld (ix+1),a         ; nueva coordenada x = inicio de la pantalla.
       ret


; Sencillo generador de números seudo-aleatorio.
; Seguir un puntero a través de la ROM (a partir de una semilla),
; retornando el contenido del byte en esa posición.

random ld hl,(seed)        ; puntero
       ld a,h
       and 31              ; mantenerlo en los primeros 8Kb de ROM.
       ld h,a
       ld a,(hl)           ; Obtener el número "aleatorio" de esa ubicación.
       inc hl              ; Incrementar el puntero.
       ld (seed),hl
       ret
seed   defw 0

; Calcular la dirección del atributo de carácter en (dispx, dispy).

atadd  ld a,c              ; coordenada vertical.
       rrca                ; multiplicar por 32.
       rrca                ; desplazar a la derecha con acarreo 3 veces
       rrca                ; mas rápido que desplazar izquierda 5 veces.
       ld e,a
       and 3
       add a,88            ; 88x256=dirección de los atributos.
       ld d,a
       ld a,e
       and 224
       ld e,a
       ld a,b              ; posición horizontal.
       add a,e
       ld e,a              ; de=dirección de los atributos.
       ld a,(de)           ; devolver atributo en el acumulador.
       ret

plx    defb 0              ; coordenada x del jugador.
ply    defb 0              ; coordenada y del jugador.

; gráficos UDG.

blocks defb 16,16,56,56,124,124,254,254    ; base del jugador.
       defb 24,126,255,255,60,60,60,60     ; seta.
       defb 24,126,126,255,255,126,126,24  ; segmento.

; Tabla de segmentos.
; Formato: 3 bytes por entrada, 10 segmentos.
; byte 1: 255=segmento apagado, 0=izquierda, 1=derecha.
; byte 2: coordenada x (vertical).
; byte 3: coordenada y (horizontal).

segmnt defb 0,0,0          ; segmento 1.
       defb 0,0,0          ; segmento 2.
       defb 0,0,0          ; segmento 3.
       defb 0,0,0          ; segmento 4.
       defb 0,0,0          ; segmento 5.
       defb 0,0,0          ; segmento 6.
       defb 0,0,0          ; segmento 7.
       defb 0,0,0          ; segmento 8.
       defb 0,0,0          ; segmento 9.
       defb 0,0,0          ; segmento 10.

NdT
: El direccionamiento indexado necesita sumar siempre una cantidad al puntero, por lo que si no se indica será cero, pero algunos ensambladores no soportan la instrucción (ix) a secas, debes usar en su lugar (ix+0), sería poner por ejemplo lo siguiente:


segmd  ld a,(ix+0)         ; dirección del segmento.
       xor 1               ; cambiarla.
       ld (ix+0),a         ; guardar la nueva dirección.
       ld a,(ix+1)         ; coordenada y.
       cp 21               ; ¿llegamos al final de la pantalla?
       jr z,segmt          ; si, mover el segmento al inicio.

NdT: Recomiendo no usar valores fijos en el código, en su lugar es mejor definir contantes, de esta manera es muy sencillo cambiar la tabla sin alterar los resultados. Por ejemplo en Pasmo podemos usar EQU para las constantes, y reservar el espacio de la tabla con DEFS (o su equivalente DS)

ld b,NTabla         ; número de segmentos a inicializar. Mejor usar una constante
..........
ld de,LTabla        ; 3 bytes por segmento, pero mejor usar una constante
..........
; Tabla de segmentos.
; Formato: 3 bytes por entrada, 10 segmentos.
; byte 1: 255=segmento apagado, 0=izquierda, 1=derecha.
; byte 2: coordenada x (vertical).
; byte 3: coordenada y (horizontal).

LTabla equ  3               ; cada entrada de la tabla tiene 3 elementos
NTabla equ 10               ; tenemos 10 entradas en la tabla
segmnt defs NTabla*LTabla,0 ; Reservamos espacio para la tabla, rellena a ceros
............