jueves, 14 de abril de 2016

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

Í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.
ACTUALIZADO 16/02/2018: Se añaden fuentes verificados para su descarga. Ver nuevo apéndice B
ACTUALIZADO 20/09/2022: Se actualizan los fuentes, ahora ejecutables según el nuevo apéndice C

Control del teclado y el Joystick

Una tecla a la vez

A condición de que no hayas deshabilitado las interrupciones o te hayas entrometido con ellas, por defecto la ROM del Spectrum leerá automáticamente el teclado y actualizará varias variables del sistema situadas en la posición de memoria 23552 cincuenta veces por segundo. La forma más sencilla de comprobar si hay una pulsación de tecla es primero cargar la dirección 23560 con un valor nulo, y luego interrogar esta ubicación hasta que cambie, el resultado es el valor ASCII de la tecla pulsada. Esto es muy útil para aquellas situaciones de "presione cualquier tecla para continuar", para la elección de elementos de un menú, o para la entrada del nombre en las rutinas de puntuación más alta. Una rutina de este tipo podría tener este aspecto:

       ld hl,23560         ; variable del sistema LAST K.
       ld (hl),0           ; poner un valor nulo aquí.
loop   ld a,(hl)           ; nuevo valor de LAST K.
       cp 0                ; ¿sique a cero?
       jr z,loop           ; si, no se ha pulsado nada.
       ret                 ; tecla pulsada.

Pulsación de varias teclas

Las pulsaciones de teclas individuales son raras de usar para la rápida acción de los juegos arcade, en su lugar necesitamos detectar más de una pulsación simultánea, y aquí es donde las cosas se ponen un poco más complicadas. En lugar de leer direcciones de memoria tenemos que leer uno de ocho puertos, cada uno de los cuales corresponde a una fila de cinco teclas. Por supuesto, la mayoría de modelos de Spectrum parecen tener muchas mas teclas ¿como se conectan? Bueno, en realidad no lo hacen. La distribución del teclado del Spectrum original consistía en sólo cuarenta teclas, dispuestas en ocho grupos de filas de cinco teclas. Con el fin de acceder a algunas de las funciones era necesario presionar ciertas combinaciones de teclas al mismo tiempo, por ejemplo para borrar la combinación requerida eran CAPS SHIFT y 0 a la vez. Sinclair añadió estas teclas extra cuando llegó el Spectrum Plus a escena en 1985, y funcionan mediante la simulación de la pulsación simultánea de las combinaciones de pulsaciones de teclas necesarias en los modelos de teclado de goma original.

La distribución del teclado original se separa en estas agrupaciones:

Puerto  Teclas                      
32766   B, N, M, Symbol Shift, Space
49150   H, J, K, L, Enter
57342   Y, U, I, O, P
61438   6, 7, 8, 9, 0
63486   5, 4, 3, 2, 1
64510   T, R, E, W, Q
65022   G, F, D, S, A
65278   V, C, X, Z, Caps Shift

Para descubrir qué teclas están siendo presionadas leemos el número del puerto adecuado, cada tecla en la fila está asignanda a uno de los cinco bits bajos D0-D4 (valores 1, 2, 4, 8 y 16) donde D0 representa la tecla exterior y D4 la más interna. Curiosamente, cada bit está en alto (1) cuando no se presiona, y en bajo (0) cuando lo está, al contrario de lo que cabría esperar.

Para leer una fila de cinco teclas simplemente cargamos el número de puerto en el par de registros BC, a continuación realizar la instrucción in a,(c). Como sólo queremos los bits de menor valor podemos ignorar los bits que no deseamos, ya sea con una and 31 o mediante la rotación de los bits de salida del acumulador usando la bandera de acarreo mediante cinco instrucciones rra: call c,(dirección).

Si esto es difícil de entender revisa el siguiente ejemplo:

       ld bc,63486         ; fila del teclado 1-5/puerto de joystick 2.
       in a,(c)            ; ver que tecla se ha pulsado.
       rra                 ; bit externo = key 1.
       push af             ; recordar el valor.
       call nc,mpl         ; si se ha pulsado, moverse a la izquierda.
       pop af              ; restaurar el acumulador.
       rra                 ; siguiente bit (valor 2) = key 2.
       push af             ; recordar el valor.
       call nc,mpr         ; se ha pulsado, moverse a la derecha.
       pop af              ; restaurar el acumulador.
       rra                 ; siguiente bit (valor 4) = key 3.
       push af             ; recordar el valor.
       call nc,mpd         ; se ha pulsado, moverse hacia abajo.
       pop af              ; restaurar el acumulador.
       rra                 ; siguiente bit (valor 8) reads key 4.
       call nc,mpu         ; se ha pulsado, moverse hacia arriba.

Ver mas abajo apartado "un juego simple" para un ejemplo de esto

Joysticks

Los puertos de joystick Sinclair 1 y 2 fueron simplemente asignados a cada una de las filas de las teclas numéricas, lo que se puede ver fácilmente desde el editor de BASIC, usando el joystick para escribir números. El puerto 1 (interface 2) se asigna a las teclas 6, 7, 8, 9 y 0, el puerto 2 (interface 1) a las teclas 1, 2, 3, 4 y 5. Para detectar la entrada de joystick leemos el puerto del mismo modo que para la lectura del teclado. Los joysticks Sinclair utilizan los puertos 63486 (interface 1 / puerto 2) y 61438 (interface 2 / puerto 1), los bits D0-D4 darán un 0 para el presionado, 1 para no presionado.

El popular formato de joystick Kempston no está asignada al teclado y se puede leer en su lugar mediante el puerto 31. Esto significa que podemos usar un sencillo in a,(31). Una vez más, se utilizan los valores de los bits D0-D4, aunque esta vez los valores son como se podría esperar, con un valor alto si se está aplicando el joystick en una dirección particular. Los valores de los bits resultantes serán 1 para presionado, 0 para no presionado.

; Ejemplo de rutina de control del joystick.

joycon ld bc,31            ; puerto del joystick Kempston.
       in a,(c)            ; leer la entrada.
       and 2               ; verificar el bit "izquierda".
       call nz,joyl        ; moverse a la izquierda.
       in a,(c)            ; leer la entrada.
       and 1               ; verificar el bit "derecha".
       call nz,joyr        ; moverse a la derecha.
       in a,(c)            ; leer la entrada.
       and 8               ; verificar el bit "arriba".
       call nz,joyu        ; moverse arriba.
       in a,(c)            ; leer la entrada.
       and 4               ; verificar el bit "abajo".
       call nz,joyd        ; moverse abajo.
       in a,(c)            ; leer la entrada.
       and 16              ; verificar el bit de disparo.
       call nz,fire        ; disparo pulsado.

Un juego simple

Ahora podemos ir un paso más allá y, poniendo en práctica lo que ya hemos cubierto, escribir la sección de control principal para un juego básico. Esta será la base de una simple variante del Centipede (ciempiés) que vamos a desarrollar en los próximos capítulos. No hemos cubierto todo lo necesario para un juego todavía, pero podemos hacer un comienzo con un pequeño bucle de control que permite al jugador manipular una pequeña base de disparo por la pantalla. Te lo advierto, este programa no tiene salida al BASIC así que asegúrate de haber guardado una copia del código fuente antes de ejecutarlo.

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

       call basexy         ; establecer las posiciones x e y del jugador.
       call splayr         ; mostrar símbolo de la base del jugador.
       
; 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á 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á 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á está siendo pulsada, moverse hacia abajo.
       pop af              ; restaurar el acumulador.
       rra                 ; siguiente bit (valor 8) = tecla 4.
       call nc,mpu         ; si está 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.

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

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.


Rápido, ¿no es así? De hecho, hemos reducido el ritmo con una instrucción de interrupción, pero todavía funciona a unos veloces 50 cuadros por segundo, lo que es probablemente un poco demasiado rápido. No te preocupes, ya que seguiremos añadiendo más características al código que comenzarán a reducir la velocidad. Si te sientes cómodo puedes intentar adaptar el programa anterior para trabajar con un joystick Kempston. No es difícil, requiere simplemente cambiar el puerto 63486 por el puerto 31, y sustituir los cuatro posteriores call nc,(dirección) por call c,(dirección) (¿recuerdas que los bits se invierten?)

Usar teclas redefinibles es un poco más complicado. Como probablemente sabes, el teclado del Spectrum original se divide en 8 filas de 5 teclas cada uno, leyendo el puerto asociado con una determinada fila de teclas, y a continuación probando los bits D0-D4 podemos saber si se pulsa una tecla en particular. Si vas a sustituir ld bc,31 en el fragmento de código anterior con ld bc,49150 podría acceder a saber si la fila de teclas se ha usado, aunque esto no lo hace una buena rutina de teclas redefinibles. Afortunadamente, hay otra manera de hacer las cosas.

Podemos establecer el puerto necesario para cada fila de teclas utilizando la fórmula del manual del Spectrum . Donde n es el número de fila 0-7, la dirección del puerto será 254 + 256 * (255 - 2n). Hay una rutina ROM en la dirección 654 que hace un montón de trabajo duro para nosotros, devolviendo el número de la tecla pulsada en el registro e, en el rango de 0-39. Un valor 0-7 corresponde a la tecla más interna de cada fila (eso es B, H, Y, 6, 5, T, G y V), un valor 8-15 a la siguiente tecla a lo largo de cada fila, y así hasta el 39 para la tecla externa en la última fila (CAPS SHIFT). El estado de la tecla de maúsculas a su vez también se devuelve en el registro d. Si no se pulsa ninguna tecla retorna en e el valor 255.

La rutina de la ROM sólo puede devolver un único número de tecla, lo que no es bueno para la detección de más de una tecla a la vez. Para determinar si una tecla específica está siendo presionada o no en cualquier momento, es necesitamos convertir el número de nuevo en un puerto y, a continuación, leer ese puerto y comprobar el bit individual por nosotros mismos. Hay una rutina muy útil que utilizo para mis trabajo, y es la única rutina en mis juegos que no he escrito yo mismo. El crédito por eso debe ir a Stephen Jones, un programador que escribió excelentes artículos para el Spectrum Club Discovery hace muchos años. Para utilizar su rutina, cargar el acumulador con el número de la tecla que deseas probar, llama a ktest, a continuación, comprueba la bandera de acarreo. Si se ha establecido (1) no está siendo presionada la tecla, si no hay acarreo (0) es que se pulsa la tecla. Si eso es demasiado confuso y parece como una forma incorrecta de funcionar, pon una instrucción ccf justo antes del ret.

; rutina de prueba del teclado del Sr. Jones.

ktest  ld c,a              ; tecla a probar en c.
       and 7               ; mascara de bits d0-d2 para la fila.
       inc a               ; en el rango de 1-8.
       ld b,a              ; colocarlo en b.
       srl c               ; dividir c por 8,
       srl c               ; encontrar la posición dentro de la fila.
       srl c
       ld a,5              ; sólo 5 teclas por fila.
       sub c               ; restar de la posición.
       ld c,a              ; ponerlo en c.
       ld a,254            ; byte alto del puerto a leer.
ktest0 rrca                ; rotar en su posición.
       djnz ktest0         ; repetir hasta que hemos encontrado la fila correspondiente.
       in a,(254)          ; leer el puerto (a=alto, 254=bajo).
ktest1 rra                 ; rotar el bit fuera del resultado.
       dec c               ; bucle del contador.
       jp nz,ktest1        ; repetir hasta que el bit se posicione por acarreo.
       ret

No hay comentarios:

Publicar un comentario