miércoles, 13 de abril de 2016

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

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


Texto y gráficos simples

Hola Mundo

El primer programa BASIC que los programadores novatos escriben por lo general son estas líneas:

10 PRINT "Hola Mundo"
20 GOTO 10

Muy bien, el texto puede ser diferente. Tu primer esfuerzo pudo ser "David es el mejor" o "Roberto estuvo aquí", pero seamos sinceros, la visualización de texto y gráficos en pantalla es probablemente el aspecto más importante de escribir cualquier juego de ordenador y (con la excepción de las máquinas de pinball o las tragaperras) es prácticamente imposible concebir un juego sin pantalla. Con esto en mente, vamos a empezar este tutorial con algunas de las rutinas de pantalla más importantes de la ROM del Spectrum.

Entonces, ¿cómo hacemos para convertir el programa BASIC anterior a código máquina? Bueno, podemos imprimir mediante el uso de la instrucción RST 16 (lo que efectivamente es lo mismo que PRINT CHR$ a), solo que simplemente imprime el carácter que ocupa el acumulador en el canal actual. Para imprimir una cadena en la pantalla, tenemos que llamar a dos rutinas: una para abrir la pantalla superior para la impresión (canal 2), y una segunda para imprimir la cadena. La rutina en la dirección ROM 5633 abrirá el número de canal que pasamos en el acumulador, y la de 8252 imprimirá una cadena que comienza en de con la longitud indicada en bc por este canal. Una vez que se abre el canal 2, toda la impresión se envía a la pantalla superior hasta que llamamos a 5633 con otro valor para enviar la salida a otra parte. Otros canales interesantes son 1 para la pantalla inferior (como PRINT #1 en BASIC, que podemos usar para mostrar en las dos líneas inferiores) y el 3 para la ZX Printer.

       ld a,2              ; pantalla superior.
       call 5633           ; abrir canal.
loop   ld de,string        ; dirección de la cadena de caracteres.
       ld bc,eostr-string  ; longitud de la cadena a imprimir.
       call 8252           ; imprimir su cadena.
       jp loop             ; repetir hsta que se llene la pantalla.

string defb '(tu nombre) es sexi'
eostr  equ $

La ejecución de este programa llena la pantalla con el texto hasta que aparece scroll? en la parte inferior. Notarás sin embargo que, en lugar de que cada línea de texto aparezca en una línea propia como en el listado, el principio de cada cadena continúa directamente desde el final de la anterior, que no es exactamente lo que queríamos. Para lograr esto tenemos que lanzar un salto de línea nosotros mismos, utilizando un código de control ASCII. Una forma de hacer esto sería cargar el acumulador con el código para una nueva línea (13), a continuación utilizar RST 16 para imprimir ese código. Otra forma más eficiente es agregar el código ASCII al final de nuestra cadena de este modo:

cadena DEFB '(su nombre) es sexi'
       DEFB 13
eostr  equ $

Hay una serie de códigos de control ASCII como éste que altera la posición de impresión actual, los colores, etc., y la experimentación te ayudará a decidir cuáles encontrarás más útiles. Estos son los principales que utilizo:

13      (NEWLINE) Establece la posición de impresión al principio de la línea 
                  siguiente. 
16,c    (INK)     Establece el color de la tinta con el valor del siguiente byte. 
17,c    (PAPER)   Ajusta el color del fondo con el valor del siguiente byte. 
22,x,y  (AT)      Ajusta la impresión a las coordenadas x e y con los valores 
                  especificados en los dos bytes siguientes.

El código 22 es particularmente útil para establecer las coordenadas en las que se va a mostrar un texto o un carácter gráfico. En este ejemplo se mostrará un signo de exclamación en la parte inferior derecha de la pantalla:

       ld a,2              ; pantalla superior.
       call 5633           ; abrir canal.
       ld de,string        ; dirección de la cadena.
       ld bc,eostr-string  ; longitud de la cadena a imprimir.
       call 8252           ; imprimir nuestra cadena.
       ret

string defb 22,21,31,'!'
eostr  equ $

Este programa va un paso más allá y anima un asterisco desde el fondo hasta la parte superior de la pantalla:

       ld a,2          ; 2 = pantalla superior.
       call 5633       ; abrir el canal.
       ld a,21         ; fila 21 = parte inferior de la pantalla.
       ld (xcoord),a   ; establece la coordenada x inicial.
loop   call setxy      ; actualiza nuestras coordenadas x/y.
       ld a,'*'        ; queremos un asterisco aquí.
       rst 16          ; presentarlo.
       call delay      ; esperamos un retardo.
       call setxy      ; actualiza nuestras coordenadas x/y.
       ld a,32         ; código ASCII para espacio en blanco.
       rst 16          ; borrar el anterior asterisco.
       ld hl,xcoord    ; posición vertical.
       dec (hl)        ; moverla hacia arriba una línea.
       ld a,(hl)       ; ¿donde estamos ahora?
       cp 255          ; ¿nos pasamos del inicio de la pantalla?
       jr nz,loop      ; no, repetir.
       ret
delay  ld b,10         ; longitud del retardo.
delay0 halt            ; esperar una interrupción.
       djnz delay0     ; bucle.
       ret             ; regresar.
setxy  ld a,22         ; código de control ASCII para AT.
       rst 16          ; imprimir aquí.
       ld a,(xcoord)   ; posición vertical.
       rst 16          ; imprimir aquí.
       ld a,(ycoord)   ; coordenada y.
       rst 16          ; imprimir aquí.
       ret

xcoord defb 0
ycoord defb 15

Impresión de gráficos simples

Mover asteriscos alrededor de la pantalla está muy bien, pero incluso para el juego más simple lo que realmente necesitamos es mostrar gráficos. Se hablará de gráficos avanzados en capítulos posteriores, por ahora sólo vamos a usar simples invasores del espacio como tipo de gráficos, y como cualquier programador en BASIC te dirá, el Spectrum tiene un mecanismo muy simple para esto: los gráficos definidos por el usuario, generalmente abreviados como UDG.

La tabla ASCII del Spectrum contiene 21 (19 en modo 128k) caracteres gráficos definidos por el usuario, comenzando en el código 144 y llegando hasta el 164 (162 en modo 128k). En BASIC los UDG se definen introduciendo datos mediante POKE en el área de UDG de la parte superior de la RAM, pero en código de máquina que tiene más sentido cambiar la variable de sistema que apunta a la posición de memoria en la que se almacenan los UDG, lo que se realiza cambiando la valor de dos bytes en la dirección 23675. Ahora podemos modificar nuestro programa del asterisco en movimiento para mostrar en su lugar un gráfico con los cambios que están subrayados.

       ld hl,udgs      ; UDG que definimos.
       ld (23675),hl   ; actualizamos la variable del sistema para los UDG.
       ld a,2          ; 2 = pantalla superior.
       call 5633       ; abrir el canal.
       ld a,21         ; fila 21 = parte inferior de la pantalla.
       ld (xcoord),a   ; establece la coordenada x inicial.
loop   call setxy      ; actualiza nuestras coordenadas x/y.
       ld a,144        ; en lugar del asterisco aquí usamos un UDG.
       rst 16          ; presentarlo.
       call delay      ; esperamos un retardo.
       call setxy      ; actualiza nuestras coordenadas x/y.
       ld a,32         ; código ASCII para espacio en blanco.
       rst 16          ; borrar el anterior asterisco.
       ld hl,xcoord    ; posición vertical.
       dec (hl)        ; moverla hacia arriba una línea.
       ld a,(hl)       ; ¿donde estamos ahora?
       cp 255          ; ¿nos pasamos del inicio de la pantalla?
       jr nz,loop      ; no, repetir.
       ret
delay  ld b,10         ; longitud del retardo.
delay0 halt            ; esperar una interrupción.
       djnz delay0     ; bucle.
       ret             ; regresar.
setxy  ld a,22         ; código de control ASCII para AT.
       rst 16          ; imprimir aquí.
       ld a,(xcoord)   ; posición vertical.
       rst 16          ; imprimir aquí.
       ld a,(ycoord)   ; coordenada y.
       rst 16          ; imprimir aquí.
       ret

xcoord defb 0
ycoord defb 15
udgs   defb 60,126,219,153
       defb 255,255,219,219

Por supuesto, no hay razón por la que no se puedan utilizar más de 21 UDG si se desea. Sólo hay que establecer una serie de bancos de ellos en memoria y apuntar a cada una cuando se necesite.

Como alternativa, se puede redefinir el conjunto de caracteres en su lugar. Esto nos da un mayor rango de caracteres ASCII desde 32 (ESPACIO) a 127 (el símbolo de copyright). Incluso se puede mezclar texto y gráficos, se redefinen las letras y los números con una fuente del estilo de tu elección, y a continuación utilizamos los símbolos y letras minúsculas para los aliens, zombies o lo que el juego requiera. Para apuntar a otro conjunto restamos 256 de la dirección en la que se coloca la fuente y colocamos esto en la variable de sistema de dos bytes en la dirección 23606. El tipo de letra predeterminado de Sinclair, por ejemplo, se encuentra en la dirección ROM 15616, por lo que la variable de sistema en la dirección 23606 apunta a 15360 cuando el Spectrum se enciende.

Este código copia la fuente Sinclair de la ROM a la RAM y lo "pasa a negrita" a continuación, luego establece la variable de sistema para apuntar al nuevo juego de caracteres:

       ld hl,15616         ; fuente en ROM.
       ld de,60000         ; dirección de su fuente.
       ld bc,768           ; 96 caracteres * 8 filas que alterar.
font1  ld a,(hl)           ; obtiene el bitmap.
       rlca                ; lo rota hacia la izquierda.
       or (hl)             ; combina las 2 imágenes.
       ld (de),a           ; lo escribe en la nueva fuente.
       inc hl              ; siguiente byte del antiguo.
       inc de              ; siguiente byte del nuevo.
       dec bc              ; decrementar el contador.
       ld a,b              ; byte alto.
       or c                ; combinar con el byte bajo.
       jr nz,font1         ; repetir hasta que bc=zero.
       ld hl,60000-256     ; fuente menos 32*8.
       ld (23606),hl       ; apuntar a la nueva fuente.
       ret

Mostrando números

Para la mayoría de los juegos lo mejor para definir la puntuación del jugador es como una cadena de dígitos ASCII, aunque eso significa más trabajo en las rutinas de puntuación y hace que las tablas de puntuación más alta sean un verdadero grano en el culo para un programador en lenguaje ensamblador sin experiencia. Vamos a cubrir esto en un capítulo posterior, pero por ahora vamos a utilizar algunas rutinas de la ROM que están muy a mano para imprimir nuestros números.

Hay dos formas de imprimir un número en la pantalla, la primera es hacer uso de la misma rutina que la ROM utiliza para imprimir los números de línea del Sinclair BASIC. Para esto basta con cargar el par de registros bc con el número que desea imprimir, y luego llamar a 6683:

       ld bc,(puntuación)
       call 6683

Sin embargo, dado que los números de línea en el BASIC sólo pueden llegar al 9999, tiene la desventaja de que sólo será capaz de mostrar un número de cuatro dígitos. Una vez que la puntuación del jugador llega a 10000 se muestran caracteres ASCII en lugar de números. Afortunadamente, hay otro método que llega mucho más alto. En lugar de llamar a la rutina de visualización del número de línea, podemos llamar a una rutina para colocar el contenido de los registros bc en la pila de cálculo, y a continuación llamar a otra rutina que muestra el número en la parte superior de esta pila. No te preocupes de lo que es la pila de cálculo y cuál es su función, porque es de poca utilidad para un programador de juegos arcade, pero ya que podemos hacer uso de ella lo haremos. Sólo recuerda que las tres líneas siguientes mostrará un número entero del 0 al 65535 ambos inclusive:

       ld bc,(puntuación)
       call 11563          ; apilar número en bc.
       call 11747          ; mostrar la cima de la pila de cálculo.

Cambio de Colores

Para establecer la tinta, el fondo, el brillo y el flash a niveles permanentes podemos escribir directamente en la variable de sistema en 23693, a continuación borra  la pantalla con una llamada a la ROM:

; Queremos una pantalla de color amarillo.

       ld a,49             ; tinta azul (1) con fondo amarillo (6 * 8).
       ld (23693),a        ; establecer nuestros colores de pantalla.
       call 3503           ; borrar la pantalla.

La forma más rápida y sencilla de establecer el color del borde es escribir en el puerto 254. Los 3 bits menos significativos del byte que enviamos determinan el color, por lo que para establecer el borde en rojo:

       ld a,2              ; 2 es el código para el rojo.
       out (254),a         ; escribir en el puerto 254.

El puerto 254 también maneja el altavoz y el micrófono a través de los bits 3 y 4, por tanto el efecto del borde sólo durará hasta la próxima llamada a la rutina de sonido del altavoz en la ROM (más adelante hablaremos de esto), por lo que es necesaria una solución más permanente. Para ello, simplemente hay que cargar el acumulador con el color requerido y llamar a la rutina de la ROM en 8859. Esto cambiará el color y la variable de sistema BORDCR (ubicada en la dirección 23624) en consonancia. Para establecer un borde rojo permanente podemos hacer esto:

       ld a,2              ; 2 es el código para el rojo.
       call 8859           ; establecer el color del borde.

4 comentarios:

  1. Gran trabajo José Antonio. Me viene que ni pintado!! Precisamente andaba yo en estos tiempos pensando en meterme ya en código máquina (después de más de 30 años programando en Basic;) y hace unos días me decanté definitivamente por el Z80. Sobre este tema me gustaría preguntarte acerca de un programa ensamblador que va de lujo y que se ejecuta desde el propio navegador, además puedes ejecutar-ver la pantalla del Spectrum con los resultados del código compilado. He buscado por la red pero no he encontrado nada que se le aproxime. ¿tú la conoces por casualidad?

    Un abrazo.

    ResponderEliminar
    Respuestas
    1. Hay varios emuladores on-line genéricos como ASM80, y emuladores de Spectrum on-line como QAOP, JBacteria, ZSpectrumNET, puede usar uno de los primeros para desarrollar y uno de los segundos para probar, pero no es nada sencillo al ser programas diferentes. Para programar en ensamblador no son muy adecuados, mejor bájate un emulador de Spectrum que permita programar y depurar, y lo ejecutas en un PC que es mas práctico.

      Eliminar
  2. Valioso trabajo el tuyo José Antonio. He retomado un viejo proyecto dobre el desarrollo dd un videojuego abandonado hace más de 30 años y estoy combinando sencillas rutinas en código máquina con Basic compilado. Los resultados son, al menis para alguien que en su momento estuvo limitado al Basic del Spectrum, espectaculares, y he podido resolver con solvencia los obstáculos que en su día parecían insalvables.

    P.d. td recuerdo que tengo varias pocket computers averiadas para regalarte cuando pases por Alicante. Mi email es eurocamsuite@yahoo.es ya que el gmail apenas lo uso.

    ResponderEliminar
  3. Con respecto a los emuladores los probaré en cuanto pueda. De momento he estado haciendo pruebas con pasmo y un editor de textos. Lo cierto es que acabo de ver tus respuestas porque publiqué los post con una cuenta gmail q no miraba desde entonces... y hoy revisando lo he visto. Muchísimas gracias José Antonio.

    ResponderEliminar