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

No hay comentarios:

Publicar un comentario