Mostrando entradas con la etiqueta Sinclair. Mostrar todas las entradas
Mostrando entradas con la etiqueta Sinclair. Mostrar todas las entradas

martes, 20 de septiembre de 2022

Cómo escribir juegos para el ZX Spectrum. Apendice C

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.

APENDICE C

Este apéndice tampoco forma parte del documento original, es un añadido para explicar como se pueden probar los programas que se describen con otro sistema alternativo más integrado, que permite programar, compilar y probar los programas directamente, más sencillo que el descrito en el apéndice B.

Se basa en usar como editor de texto ConText, que además de editar permite lanzar programas externos, lo que usaremos para compilar con pasmo y ejecutar con Spectaculator. Aquí os pongo un enlace a un archivo ZIP que contiene todo lo necesario. Si no funcionara aquí os dejo otro enlace de descarga.

Dentro del ficho ZIP encontramos instrucciones para su instalación, pero os las incluyo aquí de todas formas. Hay tres programas en el fichero, hay que instalar el editor pero los otros dos no requieren instalación:

  • ConText: Este será el editor de textos que usaremos para introducir los programas 
  • Pasmo: Este es el ensamblador para convertir el ensamblador en Código Máquina del Z80
  • Spectaculator El emulador del Spectrum que usaremos para ejecutar, sencillo y práctico.

El primer paso es descomprimir el ZIP en un directorio y mover cosas a este o a otro, por ejemplo, yo usaré C:\Spectrum para todo, pero tu elije los que desees, al descomprimir crea una nueva carpeta, en su interior encontramos los tres programas y dos PDF de ayuda para su instalación, uno en inglés y otro en español. Seguiremos los pasos descritos en el fichero 

ES - CREA_UN_ENTORNO_INTEGRADO_ENSAMBLADOR_SPECTRUM_ES.pdf 

que resumo aquí:

  1. Vamos a la carpeta EDITOR_ConTEXT_WIN y ejecutamos el programa Instalar_ConTEXTv0_986.exe instalaremos dando siguiente a todas las pantallas sin preocuparnos de más. Se abrirá ConText. En options - Environement abajo está el idioma del entorno, puedes cambiarlo a español (AR) para poder seguir mejor el documento
  2. Asociamos los ficheros .ASM con el editor 
  3. Copiamos la carpeta COMPILADOR_PASMO_WIN a C:\Spectrum\Pasmo
  4. Copiamos la carpeta EMULADOR_SPECTACULATOR_WIN a C:\Spectrum\Spectaculator
  5. Configurar ConTEXT para usar ASM con los programas externos
  6. Asociar Pasmo a la tecla F9, puede probarlo con cualquier programa ASM (si abres el fichero de ejemplo incluido con Pasmo llamado programa.asm, debes añadir al final una línea con el texto END 32768). Al pulsar F9 debe compilar el programa, y debe aparecer un mensaje de Ejecución Finalizada o mensajes de error si los hay.
  7. Asociar Spectaculator a la tecla F10 (los parámetros no tienen espacio entre p y \%F). Podemos ejecutar con F10, si nos aparece una nueva pantalla para elegir entre tres opciones debemos quitar el check de la parte inferior y pulsar cualquier opción. Al cerrar el programa si aparece una nueva pantalla le debemos desmarcar el check para que no pregunte cada vez. Si estamos usando el programa de pruebas de pasmo aparece una letra o en la pantalla, la podemos mover usando las teclas o-p-q-a.


jueves, 15 de febrero de 2018

Cómo escribir juegos para el ZX Spectrum. Apendice B

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

APENDICE B

Este apéndice no forma parte del documento original, es un añadido para explicar como se pueden probar los programas que se describen, y como ejecutar los que se incluyen para su descarga.

Para programar en ensamblador para el Spectrum en nuestro PC necesitamos un editor de textos, un programa ensamblador y un emulador. Esto se puede tener de forma separada o en un solo programa, lo que siempre es más cómodo, pero explicaré ambos sistemas.

Elegir un editor para programar

Lo único que hace falta para escribir código a la antigua usanza es un programa que permita escribir texto y un ensamblador. El Notepad incluido en windows es un editor muy sencillo pero suficiente, no hace falta más realmente, aunque hoy día estamos acostumbrados al coloreado del código o a que se auto complete (eso en ensamblador es menos útil), pero no tenemos porque renunciar a ello.

Si queremos un editor un poco mas potente existen muchas mas opciones, aunque no todas van bien con ensamblador del Z80 para el coloreado, pero como editores generales funcionan muy bien, existiendo muchos gratuitos muy buenos. Yo uso Notepad++  pero solo funciona en Windows (o en Linux con Vine), me parece muy bueno y completo, pero os doy otras opciones:
  • SublimeText no es gratuito, es muy espartano de aspecto pero tiene muchos seguidores
  • UltaEdit no es gratuito pero si muy completo, tiene una extensión llamada UltraCompare que va muy bien para comparar el contenido de ficheros.
  • Atom está basado en Sublime pero es gratuito.
  • Crimson Editor es gratuito y muy recomendado por programadores retro.
  • Syncplify es gratuito, menos potente que los otros pero tiene un modo para su uso con pantallas táctiles, por lo que va muy bien en tablets.
  • Textwrangler sería una buena alternativa si usamos Apple.
  • Visual Studio Code de Microsoft es la alternativa mas profesional, y además es gratuito pero no lo recomiendo para principiantes, es un buen IDE pero tiene demasiadas cosas que no vamos a usar, dispone de varias extensiones para trabajar en ensamblador del Z80, incluso una para trabajar en el Basic del Spectrum.

Configurar el Notepad++ para ensamblador del Z80

Notepad++ dispone de configuración específica para muchos lenguajes de programación, incluyendo ensamblador de Intel, pero como siempre se le pueden añadir mas, existen variantes para la sintaxis del Z80. Hay un fichero de configuración que podéis localizar en los foros de CPCWiki (está en inglés pero es muy sencillo de entender), que funciona muy bien. Está desarrollado para usar fondo negro en la pantalla, yo he modificado el fichero para usar fondo blanco, podéis descarga de aquí los dos ficheros:


y según mas os guste el fondo con el que trabajar ponéis uno u otro, incluso podéis trastear un poco y cambiar los colores a vuestro gusto, aquí tenéis el mismo ejemplo con ambos fondos para que elijáis mejor:


Para instalarlo, guardamos el que queramos (o los dos) en un directorio de nuestra maquina, abrimos Notepad++, vamos en el menú superior a "Lenguaje", opción "Defina su lenguaje...", en la pantalla nueva damos a importar, elegimos el fichero que queremos subir, y  cerramos esa ventana. Ahora cerramos y abrimos Notepad++, y en la opción de "Lenguaje" nos aparecerá la opción importada, la seleccionamos y ya tenemos coloreado de sintaxis en el editor. También recomiendo quitar la vinificación gramatical, ya que cualquier directiva no la reconocerá y nos dirá que está mal escrita, para ello en la opción "Plugins", vamos a "DSpellCheck" y desmarcamos la opción "Spell Check Document Automatically".

Ahora podemos abrir un fichero existente o crear uno nuevo y comenzar a programar en ensamblador del Z80 con nuestro Notepad++

Generando el código máquina

Ahora que tenemos un programa escrito, hay que convertirlo en código máquina que pueda ejecutarse en el Spectrum, para lo que se usa un programa ensamblador específico para el Z80. Existen varios programas disponibles, el mas usado siempre ha sido PASMO, pero existe una buen alternativa llamada SjASMPlus, ambos están diseñado para Windows pero se pueden compilar para ejecutar en Linux o Apple ya que incluyen los fuentes, y como no usan ventanas sino que funcionan a partir de la línea de comando, son sencillos de portar. Los dos programas son antiguos y no se actualizan hace años, pero funcionan a la perfección.

Los ejemplos los he probado con ambos compiladores, la diferencia principal es que SjASMPlus admite mas directivas que PASMO, pero no permite que las directivas estén al principio de la línea por lo que hay que dejar espacios (se puede usar un parámetro al lanzarlo para oviarlo), y que es necesario añadir al inicio del programa una línea en la que se indica el nombre del fichero de destino como por ejemplo:

OUTPUT "destino.com"

Ambos funcionan igual, hay que ir a la línea de comando de windows (usar buscar y escribir CMD), y lanzar la ejecución con estos parámetros, para PASMO origen y destino, para SJ solo origen:

  • pasmo fuente.asm destino.com
  • sjplusasm fuente.asm

Probando el programa en un emulador

Para ejecutar sirve cualquier emulador, Spectaculator tiene fama de ser el mejor, pero es de pago, yo prefiero usar ZXSpin (ya no tiene página propia, pero lo podéis descargar de aquí por ejemplo), ya que dispone de utilidades para desarrollo de programas en ensamblador, incluyendo editor, ensamblador y debuger, pero hay muchos donde elegir.

Descargas en programa, lo descomprimes y lo ejecutas, se abre una pantalla de Spectrum. En el menú de "File" seleccionas la opción "Load Binary File..." y se abre una pantalla, la rellenamos con el fichero generado y la dirección de comienzo:


Luego hay que ejecutar el programa, por ejemplo para la dirección que hemos puesto debemos escribir: CLEAR 25000 : RANDOMICE USR 25000 lo que se consigue en un spectrum con las teclas X (CLEAR), el número deseado, Simbol Shift mas Z (:), T (RANDOMICE), Caps Shift mas Simbol Shift mas L (USR), y luego el número otra vez.


Esto ejecuta el programa cargado y podemos ver ya el resultado en la pantalla, este ejemplo es el hola mundo del primer capítulo:


Todo en uno con el emulador

si usamos ZXSpin o hace falta nada mas, ya que el emulador tiene su propio editor y ensamblador, para ello hay que ir a la opción "Tools", elegir "Z80 Assembler", y aparece una nueva ventana con el editor, podemos cargar allí el fuente, editarlo, ensamblarlo, ejecutarlo, incluso ejecutarlo paso a paso, lo que nos ahorra un poco de trabajo. el editor no es la quinta maravilla, pero tiene coloreado y lista de símbolos navegable, podemos cargar o guardar programas en la opción File, ensamblarlos en la opción File (cuidado de poner en esa pantalla la dirección de comienzo para evitar errores de ejecución), y ejecutarlos en la opción Program (permite ejecutar normal, paso a paso, detener la ejecución en cualquier momento, o detener y reiniciar los contadores):


Un todo en uno, que da a veces problemas con el uso de la memoria del PC si a la hora de ensablar no le ponemos la dirección de comienzo, pues el programa no funciona todo lo fino que debería, pero una gran ayuda para el desarrollo y pruebas de los programas.

jueves, 12 de mayo de 2016

Cómo escribir juegos para el ZX Spectrum. Apendice

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

Direcciones útiles de ROM y RAM

ROM

    0: inicio de la ROM.
  654: rutina ROM que devuelve la tecla pulsada (0-39) en el registro e,
       o 255 si nada presionado.
  949: rutina BEEPER.  Ajustar la duración en DE y el tono en el HL.
 3503: rutina para borrar la pantalla, estableciendo el color en (23693).
 6683: muestra en pantalla el valor numérico del par de registros BC, 
       hasta un valor máximo de 9999.
 8252: muestra en pantalla una cadena de longitud BC ubicada en la dirección DE.
 8859: rutina para configurar el color del borde con el valor del acumulador.
15616: dirección de las fuentes en la ROM, 96 caracteres * 8 bytes.

RAM. Zona de pantalla

16384: 256x192 zona de pixeles.
22528:  32x24  zona de atributos de color.

RAM. Zona de variables

23296: variables del sistema.
23606: puntero a las fuentes, menos 256. (256 = 32 * 8 bytes, 32 es el código 
       para el primer carácter imprimible)
23560: código ASCII de la última tecla pulsada.
23672: reloj, se incrementa 50 veces por segundo.
23693: PAPER/INK/BRIGHT color.
23695: PAPER/INK/BRIGHT color.
23734: canales de E/S.
23755: área del BASIC, seguido de espacio para programas en código máquina. 
       Si usas hardware adicional puede moverlo.
24000: Podría decirse que es el punto de partida realista mas bajo para 
       un juego, permitiendo 41536 bytes.
32767: último byte de RAM en un Spectrum de 16K.
32768: Principio de la zona no contenida, la RAM más rápida.
65535: último byte de RAM en un Spectrum de 48K.

miércoles, 11 de mayo de 2016

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

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

Diseño de juegos

Cincuenta por cien Arte, Cincuenta por cien Ciencia

Ahora ya debes tener suficientes conocimientos para ser capaz de juntar las instrucciones para formar el motor de un juego. Felicitaciones, ahora conoces el cincuenta por ciento de la manera de escribir un juego. Este capítulo tiene como objetivo hacer frente a la otra mitad. Es fácil caer en la trampa de pensar que la escritura de un juego es tan simple como aprender cómo ordenar una enorme variedad de instrucciones en una secuencia coherente, con el fin de llevar a cabo un gran plan. El aspecto técnico es, por supuesto, esencial. Sin embargo, un buen desarrollador de juegos tiene que ser mucho más que un técnico especializado. Tiene que ser también un artista. Eso no significa que debas ser un artista de los gráficos, aunque estos deben ser agradables visualmente no es a lo que me refiero. Estoy, por supuesto, hablando sobre el arte del diseño de juegos.

Se podría optar por seguir el camino de la técnica, escribit tu código para deslumbrar a otros "techies" que saben cómo funciona el Spectrum y lo que puede hacer normalmente. Sin embargo eso es poco probable que impresione a todo el mundo. Para empezar, el juego puede parecer increíble pero podría ser aburrido de jugar y no mantener la atención más de cinco minutos. Lo que es más, no todos los fans del Spectrum conoce las limitaciones de la máquina. Si realmente deseas maximizar tu audiencia, necesitas apelar a los que no saben ni le importa lo que el Spectrum puede hacer. Considera esto: imagina que dos programadores de Commodore 64 liberan sus juegos el mismo día. Uno de ellos escribió un clon del Space Invaders, que sorprendió a los chicos mas técnicos de la escena de Commodore, y el otro escribió un juego técnicamente nada especial pero adictivo, diferente a todo lo que se ha visto antes. Como propietario de un Spectrum que tienes poco conocimiento de los límites de hardware del C64, ¿a cual sería más probable que echaras un vistazo? Los clones del Space Invaders pueden mostrar una increíble magia técnica para esa máquina, pero ¿no es probable que no veas nada que no hayas visto antes en algún otro formato? Consideremos ahora ese escenario al revés: ¿por qué un fan de Commodore, Amstrad, Acorn u Oric quiere descargar tu juego? ¿Que lo hace tan especial que no puedes encontrar algo similar o mejor en su máquina favorita?

Si deseas maximizar el interés, es buena idea ofrecer algo único.

Mecánicas de juegos

Escribir el código del programa es como hacer una gran construcción de Lego formada por miles de minúsculos ladrillos, las instrucciones individuales que dicen a la CPU qué hacer. La mecánica del juego es más afin a Duplo; piezas más grandes pero aún con infinitas maneras de reorganizarlas para alcanzar el efecto que deseas. Un enfoque para el diseño de juegos podría ser considerar los siguientes pasos.

En primer lugar, empieza por considerar el método de control del jugador. ¿Cómo se mueve? Piensa si se limita de alguna manera, y si es así, ¿cómo? ¿Será capaz de realizar acciones o completar tareas para aumentar o modificar su capacidad de maniobra de alguna manera? En líneas generales, cualquier método de control que puedas concebir probablemente ya se ha hecho antes, pero eso no debe impedir que te hagas estas preguntas y busques interesantes variaciones.

En segundo lugar, ten en cuenta las tareas que el jugador tiene que asumir. ¿Cuál es su objetivo? ¿Cómo alcanza la victoria? ¿Está disparando a cosas, procede a su recogida, las coloca en un lugar, previene que suceda algo? ¿Está moviendo cosas alrededor de la zona del juego con algún propósito? La respuesta a estas preguntas unidas con una técnica de control diferente es donde el juego puede empezar a diferenciarse de los innumerables juegos de plataformas, shoot-em-ups, juegos de laberinto o de puzle que hay desarrollados. Empieza de forma sencilla en un primer momento y ve añadiendo ideas a medida que avanzas. No tengas miedo de llegar a puntos donde ya no funciona, incluso si has pasado horas agonizando sobre tu código. Si una mecánica no funciona, no tiene cabida en tu juego.

El tercer paso es considerar los peligros que enfrentará el jugador. ¿Hay un tiempo límite? ¿Hay enemigos que se mueven por el juego? ¿Cómo se mueven, de manera predecible o de otra forma más interesante? ¿Son mortales al contacto o solo hacen que disminuya la energía del jugador? ¿Deben ser destruidos, deben evitarse o ambas cosas? ¿Cambian a medida que avanza el juego? ¿Se pueden utilizar de alguna manera?

Cuando hayas respondido a estas preguntas es necesario considerar si el jugador puede recoger pequeñas bonificaciones, o debe completar mini-tareas que le dan habilidades extra, aumentar su puntuación o su bonificación, aumentar vidas o ayudarlo de alguna otra manera. ¿Esto se debe lograr en un orden determinado para obtener una ventaja aún mayor? Puedes también explorar el camino contrario: ¿existen elementos que pueden ser recogidos que dificultan al jugador?, por ejemplo, con la inversión de sus controles o su fuerte desaceleración. Un error común (que he cometido yo mismo) es reducir las oportunidades de bonificación del jugador conforme el juego progresa. A medida que el juego se vuelve más difícil de jugar, generalmente es buena idea aumentar las bonificaciones con el fin de dar al jugador una oportunidad en el combate.

Si realmente quieres ir a la ciudad, es posible que desees añadir un poco más de profundidad. ¿Debe el jugador considerar una imagen más grande de la que ve en una sola pantalla? ¿Necesita pensar en algo mientras está esquivando, saltando o disparando? ¿Sus logros están llenando un mapa, o un cuadro? ¿Está jugando un juego gigante de tres en raya, de animal, vegetal o mineral, de batallas navales? ¿Puedes incluir elementos en el jardín mientras el jugador sigue su camino por su juego?

Juega mucho con tu juego mientras lo estás desarrollando y sigue haciéndote preguntas. Sé inseguro.

Cuando hayas hecho todo esto, es posible que desees considerar los gráficos, la historia o tal vez incluso la música. Evitar la tentación de comenzar con la historia cuando estás desarrollando un juego para una máquina de ocho bits que, simplemente, no tiene el manejo de gráficos o memoria suficientes para producir la atmósfera que haga justicia a una historia brillante. No vas a escribir el siguiente Grim Fandango. Adapta tu historia en torno al diseño del juego, y no al revés.

Por último, no importa lo bueno y original que sea tu juego, recuerda que no se puede contentar a siempre a todo el mundo. Simplemente disfruta de lo que estás haciendo, diviértete y no te lo tomes demasiado en serio.

¡Feliz desarrollo!

martes, 10 de mayo de 2016

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

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

Juegos que carguen y se ejecuten automáticamente

Aunque esto es bastante simple para un programador experimentado en Sinclair BASIC, se trata de un área que se suele pasar por alto. En particular, los programadores que se han pasado al Spectrum desde otras máquinas no estarán familiarizados con la forma como se hace.

Para poder ejecutar una rutina en código de máquina tenemos que empezar desde el BASIC. Usando este escribimos un pequeño programa cargador básico, que reserva sitio para el código máquina, carga dicho código y luego lo ejecuta. El tipo más simple de cargador sería el contenido es estas líneas:

10 CLEAR 24575: LOAD ""CODE: RANDOMIZE USR 24576

El primer comando, CLEAR, establece la variable RAMTOP por debajo de la zona ocupada por el código máquina, de modo que el BASIC no lo sobrescriba. También borra la pantalla y mueve la pila fuera del camino. El número que sigue por lo general debe estar un byte por debajo del primer byte de tu juego. LOAD ""CODE carga el siguiente archivo de código desde la cinta, y RANDOMIZE USR llama a la rutina en código máquina en la dirección especificada, en este caso 24576. Este debe ser el punto de entrada para tu juego. En un Spectrum, la ROM se encuentra en los primeros 16K, y va seguido por varias cosas como la memoria de video, las variables del sistema y del BASIC. Un lugar seguro para tu código es por encima de esta zona, en cualquier lugar hasta el final de la RAM en la dirección 65535. Solo con un pequeño cargador en BASIC a la dirección de inicio 24576, o incluso 24000, te dará un montón de espacio para su juego.

Este programa cargador se guarda a continuación en cinta con un comando como este

SAVE "name" LINE 10

LINE 10 indica que cuando termine de cargar, el programa en BASIC se auto-ejecuta desde la línea 10.

Después del cargador BASIC viene el archivo de código. Puedes guardar un archivo de código así:

SAVE "name" CODE 24576,40960

CODE le dice al Spectrum que guarde un archivo de código, en lugar de en BASIC. El primer número de después es la dirección de inicio del bloque de código, y el último número es su longitud.

Esto es bastante simple, pero ¿y si queremos añadir una pantalla de carga? Bueno, es bastante sencillo. Podemos cargar una pantalla utilizando

LOAD ""SCREEN$

Lo que esto va a hacer es cargar un bloque de código de 6912 bytes de longitud, al inicio de la dirección de pantalla en 16384. Poner el archivo de pantalla en memoria es un poco más complicado, ya que no podemos simplemente guardar la pantalla como un archivo ya que las dos líneas de la parte inferior se pueden sobrescribir con mensajes del tipo "Start tape, then press any key". Así que cargaremos nuestra imagen en otro punto de la memoria RAM, como 32768, para a continuación utilizar

SAVE "name" CODE 32768,6912

6912 es el tamaño de la RAM de pantalla del Spectrum. Cuando volvemos a cargar el bloque de cinta utilizando LOAD ""SCREEN$, estamos especificando que queremos forzar a que el archivo de código se cargue en la memoria de pantalla. En estas circunstancias, no importa donde se encontraba el archivo de código cuando se guardó.

Ahora tenemos otro posible problema: ¿el mensaje Bytes: name que se imprime antes de la carga del código no aparecerá tapando parte de nuestra pantalla? Bueno, sí que lo hará. Podemos superar esto pokeando en el flujo de salida.

POKE 23739,111

Esto hará el truco para nosotros. Así que nuestro cargador BASIC ahora se ve así:

10 CLEAR 24575: LOAD ""SCREEN$: POKE 23739,111: LOAD ""CODE: RANDOMIZE USR 24576

lunes, 9 de mayo de 2016

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

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

Interrupciones

La creación de tus propias interrupciones puede ser una pesadilla la primera vez que lo intentes, ya que es un asunto complicado. Con práctica se convierte en un poco más fácil. Para hacer que el Spectrum ejecute nuestra propia rutina de interrupción hay que decirle en que ubicación está nuestra rutina, poner la máquina en modo 2 de interrupción, y garantizar que las interrupciones están habilitadas. ¿Suena bastante simple? La parte difícil es decirle al Spectrum, donde se encuentra nuestra rutina.

Con la máquina en modo 2, el Z80 utiliza el registro i para determinar el byte alto de la dirección del puntero a la dirección de la rutina de servicio de interrupción. El byte bajo es suministrado por el hardware. En la práctica, nunca se sabe lo que va a contener el byte bajo, ¿ves el problema? El byte bajo igual podría ser 0, que podría ser 255, o que podría estar en cualquier posición intermedia. Esto significa que necesitamos todo un bloque de 257 bytes de punteros a la dirección de inicio de nuestra rutina de servicio. Como el byte bajo suministrado por el hardware puede ser par o impar, tenemos que asegurarnos de que el byte bajo y el byte alto de la dirección de nuestra rutina de servicio son idénticos. Esto limita seriamente donde podemos localizar nuestra rutina. También debemos localizar nuestra tabla de punteros y nuestra rutina sólo en la memoria RAM no contenida. No lo coloques por debajo de la dirección 32768. Incluso ubicándolo en la memoria RAM no contenida, como por ejemplo en el banco 1, producirá problemas en ciertos modelos del Spectrum. Personalmente, encuentro el banco 0 un lugar tan bueno como cualquier otro.

Digamos que elegimos la dirección 51400 como la ubicación de nuestra rutina de interrupción. Esto es válido ya que tanto el byte alto como el byte bajo son 200, ya 200*256+200=51400. A continuación, necesitamos una tabla de 129 punteros que apunten todos a esta dirección, o 257 veces DEFB 200, situada en el inicio de un límite de página de 256 bytes. Suponiendo que lo ponemos en lo alto del camino, podríamos empezar por 254*256=65024.

Debemos hacer esto

       org 51400

int    ; rutina de servicio de interrupción.


       org 65024

       ; punteros a rutina de interrupción.

       defb 200,200,200,200
       defb 200,200,200,200
       .
       .
       defb 200,200,200,200
       defb 200

¡Uf! Aún así, ahora llegamos a nuestra rutina de interrupción. Las interrupciones pueden ocurrir durante cualquier período, por lo que tenemos que conservar los registros que es probable que se utilicen, ejecutar nuestro código, llamar opcionalmente a  la rutina de servicio de la ROM, restaurar los registros, volver a habilitar las interrupciones y por último regresar de la interrupción con un reti. Nuestra rutina podría asemejarse a esta:

int    push af             ; preservar registros.
       push bc
       push hl
       push de
       push ix
       call 49158          ; hacer sonar la música.
       rst 56              ; rutina ROM, leer teclas y actualizar el reloj.
       pop ix              ; restaurar registros.
       pop de
       pop hl
       pop bc
       pop af
       ei                  ; Volver a habilitar interrupciones antes de regresar.
       reti                ; echo.
       ret

Si no estás leyendo el teclado a través de las variables del sistema, puedes desear prescindir del rst 56. Si lo haces, vas a liberar los registros iy. Sin embargo, si el temporizador de tu juego cuenta los cuadros usando el método descrito en el capitulo de temporización, tendrás que incrementar el contador de cuadros por ti mismo:

       ld hl,23672         ; contador de cuadros.
       inc (hl)            ; aumentarlo.

Con todo esto en su lugar, estamos listos para deshabilitar las interrupciones. Hay que apuntar al registro i en la tabla de punteros y seleccionar el modo 2 de interrupción. Este código va a hacer el trabajo por nosotros:

       di                  ; deshabilitar las interrupciones por precaución.
       ld a,200            ; byte alto del puntero a la tabla de ubicaciones.
       ld i,a              ; establecer el byte alto.
       im2                 ; seleccionar el modo2 de interrupción.
       ei                  ; habilitar las interrupciones.

viernes, 6 de mayo de 2016

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

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

Música y efectos AY

NdT: Mira esta entrada que hice sobre el sonido en 8 bits que puede ayudarte a enter esta parte.

 El AY-3-8912

Introducido con los modelos 128K y prácticamente estándar desde entonces, fue un chip de sonido muy popular que se utilizaba en otros muchos equipos, por no hablar de vídeo juegos y máquinas de pinball. Básicamente dispone de 14 registros que se pueden leer o escribir a través de instrucciones in y out.

Los primeros seis registros controlan el tono para cada uno de los tres canales, son parejas en modo little-endian (NdT: primero el byte bajo) como es de esperar, es decir, el registro 0 es para byte bajo para el tono del canal A, el registro 1 es el byte alto del tono del canal A, el registro 2 es el byte bajo para el tono del canal B, y así sucesivamente. El registro 6 controla el periodo del ruido blanco, con valores válidos de 0 a 31. 0 significa ruido de las frecuencias más alta, 31 de las más bajas. El registro 7 es el control del mezclador. Los bits D0-D5 seleccionar el ruido blanco, el tono, ninguno o ambos. Para activar un tono o el ruido blanco, se debe establecer un bit, siendo 0 salida de los tres canales de tono mas el ruido blanco, y 63 no pone nada a la salida. Los registros 8, 9 y 10 son para controles de amplitud y envolvente. Un valor de 16 indica al chip utilizar el generador de envolventes, y 0-15 ajusta el volumen de ese canal directamente. En la práctica, es mejor controlar el volumen por ti mismo junto con el tono. Mediante la variación de estos de un cuadro al siguiente es posible producir una variedad muy buena de efectos de sonido. Si deseas utilizar el generador de envolventes, los dos siguientes, registros 11 y 12, se emparejan para formar el período de 16 bits y el registro 13 determina el patrón del envolvente. El manual del 128K explica la lista completa de los patrones, pero no lo cubro aquí ya que nunca los encontré particularmente útiles.

Para leer un registro, se escribe el número del registro en el puerto 65533, a continuación se lee ese puerto. Para escribir en un registro, se envía de nuevo el número del registro al puerto 65533, y luego el valor al puerto 49149. Para los no iniciados, los opcodes del Z80 no parecen ser capaces de escribir direcciones de puerto de 16 bits. No dejes que te confunda, es sólo que la forma en que se escriben es engañosa. En realidad out (c),a realiza out (bc),a, y out (n),a en realidad hace out (a*256+n),a.

La lectura de un registro del chip de sonido tiene varios usos. Es posible que desees leer los registros de volumen 8, 9 y 10 y presentar una barra de volumen. Hice algo similar en Egghead 5 Además, aunque no lo creas, la pistola de Sinclair se lee a través del registro 14 del chip de sonido. Sí, de verdad. En realidad solo genera dos informaciones, si se presiona el gatillo o no, y si el arma está apuntando a una parte brillante de la pantalla (o de hecho a cualquier objeto brillante). Es responsabilidad del programador lo que hace con esa información.

Aquí algo de código básico para escribir en el chip de sonido:

;Escribir el contenido de nuestro buffer AY en los registros el AY.

w8912  ld hl,snddat        ; inicio de los registros del AY-3-8912.
       ld e,0              ; comenzar con el registro 0.
       ld d,14             ; escribir 14.
       ld c,253            ; byte bajo del puerto a escribir.
w8912a ld b,255            ; 255*256+253 = puerto 65533 = seleccionar registro.
       out (c),e           ; indicar al chip en que registro estamos escribiendo.
       ld a,(hl)           ; valor a escribir.
       ld b,191            ; 191*256+253 = puerto 49149 = escribir en registro.
       out (c),a           ; esto es lo que estamos escribiendo allí.
       inc e               ; siguiente registro del chip de sonido.
       inc hl              ; siguiente byte a escribir.
       dec d               ; decrementar contador de bucle.
       jp nz,w8912a        ; repetir hasta completar.
       ret

snddat defw 0              ; registro de tono, canal A.
       defw 0              ; registro de tono, el canal B.
       defw 0              ; igual para el canal C.
sndwnp defb 0              ; período del ruido blanco.
sndmix defb 60             ; Tono/ruido control del mezclador.
sndv1  defb 0              ; canal A generador de amplitud/envolvente.
sndv2  defb 0              ; canal B amplitud/envolvente.
sndv3  defb 0              ; canal C amplitud/envolvente.
sndenv defw 600            ; duración de cada nota.
       defb 0

Llamando a w8912 en cada iteración del bucle principal el sonido es actualizado constantemente. Tu solo debes actualizar la memoria intermedia, ya que el sonido cambia de un cuadro al siguiente. Piensa en ello como en un "animador" del sonido. Sin embargo, sólo porque dejes de actualizar el registro de sonido este no va a dejar de sonar. El chip AY mantendrá sonando tu tono o ruido hasta que le indiques que se detenga. Una forma rápida de hacer esto es establecer los tres registros de amplitud a 0. En el ejemplo anterior, escribir un cero en sndv1, sndv2 y sndv3 y luego llamar w8912.

Usando drivers para la Música

La mayoría de los driver de música de los 128K (y de algunos 48K), tienen dos puntos de entrada. Una rutina de inicialización/terminación que detiene todo sonido y restablece el driver al comienzo de la melodía, y una rutina de servicio para ser llamada varias veces, por lo general 50 veces por segundo. Un buen lugar para almacenar música suele ser 49152, el inicio del banco de RAM conmutable. Si conoces la dirección de inicio del controlador, o puedes determinarla por ti mismo, el principio general es el de inicializar la dirección en la que se carga la música. La mayoría de las veces el código en este punto es simplemente saltar a otra dirección, o bien carga un registro o un par de registros antes de saltar a otra parte. La rutina de servicio tiende a realizar inmediatamente este jp. Si el driver no tiene otra documentación, es posible tener que desmontar el código para encontrar esta dirección, por lo general en 3-6 Bytes.

Para utilizar un driver de música, llamar a la dirección de inicialización antes de comenzar y cuando quieras apagarla. Entre esos puntos es necesario llamar a la rutina de servicio en varias ocasiones. Esto puede hacerse o bien manualmente, o mediante el establecimiento de una interrupción para hacer el trabajo de forma automática. Si decides hacerlo de forma manual, por ejemplo en el código de menú, ten en cuenta que la limpieza de la pantalla, mostrar el menú, la tabla de puntuación mas alta, las instrucciones, etc. tardarán más de 1/50 de segundo, así que esto retrasará su rutina y podría sonar de forma extraña. Podría ser mejor escribir una rutina para borrar la pantalla durante varios cuadros con algún tipo de efecto especial, marcar cuando se detiene y llama a la rutina de servicio en cada cuadro.

jueves, 5 de mayo de 2016

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

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

Matemáticas

Sumar y restar es bastante sencillo para la CPU del Spectrum, tenemos una gran cantidad de instrucciones para llevar a cabo estas tareas. Pero a diferencia de algunos procesadores posteriores en su misma serie, el programador del Z80A tiene que hacer por si mismo la multiplicación y la división. Aunque estos cálculos son poco frecuentes, tienen sus usos en ciertos tipos de juego y hasta que tengas rutinas para hacerlo, ciertas cosas son difíciles de hacer. Por ejemplo, sin el teorema de Pitágoras puede ser difícil programar para que un sprite enemigo dispare al jugador con cierto grado de precisión.

Supongamos que un sprite A necesita hacer un disparo al sprite B. Tenemos que encontrar el ángulo en el que el sprite A debe disparar y necesitamos algo de trigonometría para hacerlo. Sabemos las coordenadas de los sprites, Ax, Ay, Bx y By, y las distancias entre estos, Bx-Ax y By-Ay, eso nos darán la longitud de los catetos opuesto y contiguo. Por desgracia, la única manera de calcular el ángulo entre opuesto y contiguo es el uso del arco tangente, y las tangentes solo son adecuadas para ciertos ángulos, por lo que es mejor usar senos o cosenos en su lugar. Así que con el fin de encontrar el ángulo entre el sprite A y el sprite B, tenemos que encontrar la longitud de la hipotenusa.

La hipotenusa se calcula elevando al cuadrado la distancia en x, añadiéndola al cuadrado de la distancia en y, para a continuación encontrar la raíz cuadrada de la suma. Existen rutinas en la ROM de Sinclair para hacer todo esto, pero hay un serio inconveniente: como puede decirte cualquier persona que haya usado Sinclair BASIC alguna vez, las rutinas matemáticas son increíblemente lentas como para usarlas en la escritura de juegos. Así que tenemos que poner a trabajar los dedos y escribir nuestras propias rutinas.

Los cuadrados de nuestras distancias x e y se consiguen usando una rutina de multiplicación, multiplicando los números por sí mismos. Afortunadamente, esta parte es relativamente indolora. La multiplicación se logra de la misma manera como se haría la multiplicación larga en papel, aunque esta vez estamos trabajando en binario. Todo lo que se requiere es rotaciones, pruebas de bits y sumas. Cuando un bit exista en nuestro primer factor, añadimos el segundo factor al total. Luego desplazamos el segundo factor a la izquierda, y probamos el siguiente bit de nuestro primer factor. La rutina de abajo, tomada del juego Kuiper Persecution, demuestra esta técnica, multiplica h por d y devuelve el resultado en hl.

imul   ld e,d              ; HL = H * D
       ld a,h              ; poner en el acumulador el primer multiplicando.
       ld hl,0             ; poner a cero el resultado.
       ld d,h              ; poner a cero el byte alto ya que de=multiplicador.
       ld b,8              ; repetir 8 veces.
imul1  rra                 ; rotar el bit mas a la derecha en el acarreo.
       jr nc,imul2         ; no se estableció.
       add hl,de           ; se estableció el bit, por lo que añadir a de.
       and a               ; restablecer acarreo.
imul2  rl e                ; aumentar 1 bit izquierdo.
       rl d
       djnz imul1          ; repetir 8 veces.
       ret

Ahora necesitamos una raíz cuadrada, que es donde empiezan los problemas. Las raíces cuadradas son mucho más complicadas. Esto significa hacer una gran cantidad de divisiones, por lo que primero necesitamos una rutina de división. Esto se puede ver como trabajar de manera opuesta a la multiplicación, desplazando y restando. La siguiente rutina, también del juego Kuiper Persecución, divide hl por d y devuelve el resultado en hl.

idiv   ld b,8              ; bits a comprobar.
       ld a,d              ; número por el que dividir.
idiv3  rla                 ; comprobar bit de la izquierda.
       jr c,idiv2          ; no requiere más desplazamientos.
       inc b               ; necesita desplazamientos extra.
       cp h
       jr nc,idiv2
       jp idiv3            ; repetir.

idiv2  xor a
       ld e,a
       ld c,a              ; resultado.
idiv1  sbc hl,de           ; restarlo.
       jr nc,idiv0         ; no hay acarreo, mantener el resultado.
       add hl,de           ; restaurar el valor original de hl.
idiv0  ccf                 ; invertir bit de acarreo.
       rl c                ; rotar en ac.
       rla
       rr d                ; dividir por 2.
       rr e
       djnz idiv1          ; repetir.
       ld h,a              ; copiar el resultado a hl.
       ld l,c
       ret

De la misma manera que la multiplicación se compone de desplazamiento y suma, y la división se realiza mediante desplazamiento y resta, las raíces cuadradas se pueden calcular por desplazamiento y división. Simplemente estamos tratando de encontrar el número que "mejor se ajuste", de forma que multiplicado por sí mismo nos da el número con el que empezamos. No voy a entrar en la explicación detallada de cómo trabaja la siguiente rutina, si estás muy interesado sigue mis comentarios y haz una ejecución paso a paso con un depurador. Tomado del juego Blizzard's Rift, devuelve la raíz cuadrada de hl en el acumulador.

isqr   ld (sqbuf0),hl      ;  número del que queremos encontrar la raíz cuadrada.
       xor a               ; poner a cero el acumulador.
       ld (sqbuf2),a       ; buffer resultado.
       ld a,128            ; comenzar con la división con el bit alto.
       ld (sqbuf1),a       ; siguiente divisor.
       ld b,8              ; 8 bits a dividir.
isqr1  push bc             ; guardar contador de bucles.
       ld a,(sqbuf2)       ; resultado actual.
       ld d,a
       ld a,(sqbuf1)       ; siguiente bit a comprobar.
       or d                ; combinar con divisor.
       ld d,a              ; almacenar byte bajo.
       xor a               ; HL = HL / D
       ld c,a              ; poner a cero c.
       ld e,a              ; poner a cero e.
       push de             ; recordar divisor.
       ld hl,(sqbuf0)      ; número original.
       call idiv4          ; dividir el número por d.
       pop de              ; restaurar divisor.
       cp d                ; ¿el divisor es mayor que el resultado?
       jr c,isqr0          ; sí, no guardar este bit entonces.
       ld a,d
       ld (sqbuf2),a       ; guardar nuevo divisor.
isqr0  ld hl,sqbuf1        ; bit que comprobamos.
       and a               ; limpiar bandera de acarreo.
       rr (hl)             ; siguiente bit a la derecha.
       pop bc              ; restaurar el contador del bucle.
       djnz isqr1          ; repetir.
       ld a,(sqbuf2)       ; retornar resultado en hl.
       ret
sqbuf0 defw 0
sqbuf1 defb 0
sqbuf2 defb 0

Con la longitud de la hipotenusa calculada, podemos dividir simplemente el cateto opuesto por la hipotenusa para encontrar el coseno del ángulo. Una búsqueda rápida en nuestra tabla de senos nos dará el ángulo. ¡Uf!

Este es todo el cálculo tomado del juego Blizzard's Rift. Debes notar que usa la longitud del cateto contiguo en lugar del opuesto, por lo que se encuentra el arco coseno en lugar del arco seno. Solo se utiliza cuando la nave está por encima del cañón de la torreta, dando al jugador la oportunidad de acercarse sigilosamente y atacar desde abajo. Sin embargo demuestra cómo un sprite puede disparar a otros con precisión mortal. Si has jugado alguna vez al Blizzard's Rift, sabrás exactamente lo letales que pueden ser los cañones de las torretas.

; La nave debe estar por encima del cañón para que podamos emplear trigonometría 
; básica para apuntar.
; Tenemos que encontrar el ángulo, para ello dividimos el cateto contiguo por
; la hipotenusa y encontramos el arco coseno.

; En primer lugar ponemos la distancia al oponente en la pila:

mgunx  ld a,(nshipy)       ; coordenada y de la nave.
       ld hl,guny          ; coordenada y del cañón.
       sub (hl)            ; encontrar la diferencia.
       jr nc,mgun0         ; el resultado fue positivo.
       neg                 ; negativo, hacerlo positivo.
mgun0  cp 5                ; ¿diferencia y menor de 5?
       jr c,mgunu          ; sí, apuntar hacia arriba.
       push af             ; colocar longitud del oponente en la pila.

; A continuación se necesita la longitud de la hipotenusa para lo que
; podemos usar el viejo teorema de Pitágoras.

       ld h,a              ; copiar a en h.
       ld d,h              ; copiar h en d.
       call imul           ; multiplicar parte entera obtener resultado 16 bits.
       push hl             ; guardar el valor al cuadrado.

       ld hl,nshipx        ; cañón coordenada x.
       ld a,(gunx)         ; nave coordenada x.
       sub (hl)            ; encontrar la diferencia, siempre será positiva.
       ld h,a              ; poner la diferencia de x en h.
       ld d,h              ; copiar h en d.
       call imul           ; multiplicar h por d para obtener el cuadrado.

       pop de              ; obtener el último resultado al cuadrado.
       add hl,de           ; necesitamos la suma de los dos.
       call isqr           ; encontrar la raíz cuadrada, hipotenusa en a.
       pop de              ; cateto opuesto ahora en el registro d..

       ld h,a              ; longitud de la hipotenusa.
       ld l,0              ; ninguna fracción o signo.
       ex de,hl            ; intercambiarlos.

; Cateto opuesto e hipotenusa se encuentran ahora en de y hl.
; Ahora dividimos la primera por la segunda y encontramos el arco seno.
; Recuerda: seno = cateto opuesto sobre la hipotenusa.

       call div            ; la división nos dará el seno.
       ex de,hl            ; queremos el resultado en de.
       call asn            ; obtener arco seno para encontrar el ángulo.
       push af

; Bien, tenemos el ángulo, pero está sólo entre 0 y PI/2 radianes (64 ángulos)
; por lo que tenemos que hacer un ajuste en base al cuadrante del círculo.
; Podemos establecer en qué cuadrante del círculo está nuestro ángulo
; examinando la diferencia entre las coordenadas de la nave y el arma.

       ld a,(guny)         ; cañón posición y.
       ld hl,shipy         ; nave y.
       cp (hl)             ; ¿está la nave a la derecha?
       jr nc,mgun2         ; jugador a la izquierda, ángulo en segundo cuadrante.

; Ángulo del jugador en el primer cuadrante, por lo que debemos restar de 64.

       ld a,64             ; pi/2 radianes = 64 ángulos.
       pop bc              ; ángulo en b.
       sub b               ; restarlos.
       ld (ix+1),a         ; nuevo ángulo.
       ret                 ; tenemos nuestro ángulo.

; Segundo cuadrante, añadir el literal 64 a nuestro ángulo.

mgun2  pop af              ; ángulo original.
       add a,192           ; sumar pi/2 radianes.
       ld (ix+1),a         ; nuevo ángulo.
       ret                 ; ¡buen trabajo!

miércoles, 4 de mayo de 2016

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

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

Movimiento sofisticado

Hasta ahora hemos movido sprites arriba, abajo, a la izquierda y a la derecha por píxeles. Sin embargo, muchos juegos requieren una manipulación de sprites más sofisticada. Los juegos de plataformas requieren manejar la gravedad, los juegos de carreras top-down utilizan el movimiento de rotación, otros juegos utilizan la inercia.

Tablas de salto y de inercia

La forma más sencilla de manejar la gravedad o la inercia es disponer de una tabla de valores. Por ejemplo, los Egghead games hacen uso de una tabla de saltos y mantienen un puntero a la posición actual. Dicha tabla puede tener un aspecto como la siguiente.

; Tabla de saltos.
; Valores mayores de 128 son de subida, menores de 128 de bajada.
; Conforme el valor se aleja de 128 se aumenta la velocidad.

jptr   defw jtabu
jtabu  defb 250,251,252
       defb 253,254,254
       defb 255,255,255
       defb 0,255,0
jtabd  defb 1,0,1,1,1,2
       defb 2,3,4,5,6,6
       defb 6,6,128

Con el puntero almacenado en jptr, podríamos hacer algo como esto:

       ld hl,(jptr)        ; recuperar el puntero del salto.
       ld a,(hl)           ; siguiente valor.
       cp 128              ; ¿alcanzado el final de la tabla?
       jr nz,skip          ; no, estamos bien.
       dec hl              ; atrás aumentamos la velocidad.
       ld a,(hl)           ; recuperar la velocidad máxima.
skip   inc hl              ; avanzar el puntero.
       ld (jptr),hl        ; establecer la siguiente posición del puntero.
       ld hl,verpos        ; posición vertical del jugador.
       add a,(hl)          ; agregar la cantidad correspondiente.
       ld (hl),a           ; Establecer la nueva posición del jugador.

Para iniciar un salto, debemos establecer jptr a jtabu. Para empezar a caer, hay que configurarlo con jtabd.

Bien, esta descripción es muy simple. En la práctica, debemos utilizar el valor de la tabla de saltos como un contador de bucles, mover al jugador arriba o hacia abajo un píxel cada vez, comprobar colisiones con plataformas, paredes, elementos mortales, etc. como siempre. También podríamos utilizar el elemento final (128) para indicar que el jugador ha caído desde demasiado lejos, y establecer un indicador para que la próxima vez que el jugador golpee contra algo sólido pierda una vida. Considerando esto, ya tienes tu rutina.

Coordenadas fraccionarias

Si queremos manejar de forma más sofisticada la gravedad, la inercia o el movimiento de rotación, necesitamos coordenadas fraccionarias. Hasta ahora, con la resolución del Spectrum de 256x192 píxeles, sólo hemos tenido que utilizar un byte por coordenadas. Si por el contrario utilizamos un par de registros de dos bytes, el byte alto para la parte entera y el byte bajo para la fraccionaria, se abre un nuevo mundo de posibilidades. Esto nos da 8 bits para cifras decimales binarias, lo que permite movimientos muy precisos y sutiles. Con una coordenada en el par hl, podemos configurar el desplazamiento en de, y unir los dos. Al presentar nuestros sprites, simplemente usamos los bytes altos como nuestra coordenadas X e Y para nuestro cálculo de la dirección de la pantalla, y desechamos los bytes inferiores que contienen las fracciones. El efecto de sumar una fracción a una coordenada no será visible cada cuadro, pero incluso la fracción más pequeña, 1/256, moverá lentamente un sprite con el tiempo.

Ahora podemos echar un vistazo a la gravedad. Esta es una fuerza constante, en la práctica acelera un objeto hacia el suelo a 9,8 m/s2. Para simularlo en un juego de Spectrum, haremos que nuestra coordenada vertical sea una palabra de 16 bits. A continuación, establecemos una segunda palabra de 16 bits para la inercia. Cada cuadro añadimos una pequeña fracción a la inercia, y a continuación añadimos la inercia a la posición vertical. Por ejemplo:

       ld hl,(vermom)      ; inercia.
       ld de,2             ; fracción pequeña, 1/128.
       add hl,de           ; incrementar inercia.
       ld (vermom),hl      ; guardar inercia.
       ld de,(verpos)      ; posición vertical.
       add hl,de           ; añadir inercia.
       ld (verpos),hl      ; guardar nueva posición.
       ret
verpos defw 0              ; posición vertical.
vermom defw 0              ; inercia vertical.

A continuación, para mostrar nuestros sprites, simplemente tomamos el byte alto de nuestra posición vertical, verpos+1, que nos dan el número de píxeles desde la parte superior de la pantalla. Diferentes valores de de variarán la fuerza de la gravedad, de hecho, incluso podemos cambiar la dirección restando de con hl, o añadiendo una distancia negativa (65536-distancia). Podemos aplicar lo misma también a la coordenada y para mantener el sprite sujeto a la inercia en todas direcciones. Así es como nos gustaría ir escribiendo un juego de estilo similar a Thrust.

Movimiento rotatorio

Otro tema que podemos necesitar para juegos del estilo al Thrust, carreras top-down, u otros en los que círculos o trigonometría básica están involucrados es una tabla de senos y cosenos. Las matemáticas no son del agrado de todo el mundo, y si tu trigonometría está un poco oxidada te sugiero leer sobre senos y cosenos antes de continuar con el resto del capítulo.

En matemáticas, podemos encontrar la distancia x e y desde el centro de un círculo dado el radio y el ángulo mediante el uso de senos y cosenos. Sin embargo, mientras que en matemáticas un círculo se compone de 360 grados o lo que es lo mismo de 2 PI radianes, es más conveniente para el programador de Spectrum representar un ángulo como, por ejemplo, un valor de 8 bits de 0 a 255, o incluso usar menos bits, dependiendo del número de posiciones que el sprite del jugador pueda tomar. Podemos utilizar este valor para mirar su valor fraccionario de 16 bits para el seno y el coseno de una tabla. Suponiendo que tenemos un ángulo de 8 bits almacenado en el acumulador y deseamos encontrar el seno, simplemente accedemos a la tabla de manera similar a la siguiente:

       ld de,2             ; fracción pequeña - 1/128.
       ld l,a              ; ángulo en el byte bajo.
       ld h,0              ; ponemos a cero byte alto.
       add hl,hl           ; doble desplazamiento las entradas son de 16 bits.
       ld de,sintab        ; dirección de tabla de senos.
       add hl,de           ; añadir el desplazamiento de este ángulo.
       ld e,(hl)           ; fracción del seno.
       inc hl              ; apunto a la segunda mitad.
       ld d,(hl)           ; parte entera.
       ret                 ; volver con el seno en DE.

Realmente el BASIC de Sinclair ya nos proporciona los valores que necesitamos, con sus funciones SIN y COS. Usándolas, podemos almacenar con POKE en la memoria RAM, o bien guardarlos en cinta, o guardarlos en binario utilizando un emulador como SPIN. Alternativamente, es posible que prefieras utilizar otro lenguaje de programación en el PC para generar una tabla formateada de valores de senos, para importarlos en tu archivo de origen o incluirlos como valores binarios. Para una tabla de senos con 256 ángulos equidistantes se necesitaría un total de 512 bytes, pero debemos que tener cuidado para convertir el número devuelto por SIN en uno que nuestro juego reconozca. Multiplicando el seno por 256 nos dará nuestros valores positivos, pero cuando SIN devuelve un resultado negativo, podrías tener que multiplicar el valor absoluto del seno por 256, y luego restarlo de 65536 o poner el bit d7 del byte a uno para indicar que el número debe ser restado en lugar de sumado a nuestras coordenadas. Con una tabla de senos construida de esta manera no necesitamos una tabla separada para los cosenos, solo hemos de añadir o restar 64, o un cuarto de vuelta, al ángulo antes de obtener el valor en nuestra tabla. Para mover un sprite en un ángulo de A, se añade el seno de A a una coordenadas, y el coseno de A a la otra coordenada. Cambiando si sumamos o restamos un cuarto de vuelta para obtener el coseno, y viendo que cuadrante utiliza senos y cual cosenos, podemos empezar nuestro círculo en cualquiera de los 4 puntos cardinales y hacer que se mueva en sentido horario o anti horario.

martes, 3 de mayo de 2016

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

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

Doble Buffer

Hasta ahora hemos dibujado todos nuestros gráficos directamente en la pantalla, por razones de velocidad y simplicidad. Sin embargo hay una desventaja importante de este método: si la línea de exploración del televisión está cubriendo el área de pantalla en particular en la que se va a suprimir o volver a dibujar nuestra imagen, aparecerán nuestros gráficos con parpadeo. Por desgracia, en el Spectrum no hay una manera fácil de saber donde está la línea de exploración en un momento dado, así que tenemos que encontrar una manera de evitar esto. Un método que funciona bien es borrar y volver a dibujar todos los sprites inmediatamente después de una instrucción halt, antes de que el haz de exploración tenga oportunidad de ponerse al día para seguir dibujando la imagen. La desventaja de este método es que nuestro código para los sprites tiene que ser bastante rápido, e incluso en ese caso no es recomendable eliminar y volver a dibujar más de dos sprites por cuadro porque para entonces el haz de exploración estarán ya bajo el borde superior y en el área de la pantalla. Por supuesto, colocar el panel de estado en la parte superior de la pantalla podría dar un poco más de tiempo para dibujar nuestros gráficos, y si el juego corre a 25 fotogramas por segundo podríamos emplear una segunda instrucción halt y maniobrar otro par de sprites inmediatamente después. En última instancia, llega un punto en el que esto se rompe. Si nuestros gráficos van a tomar un poco más de tiempo para dibujarse, necesitamos otra manera de ocultar el proceso al jugador, necesitamos usar una segunda pantalla de búfer. Esto significa que todo el trabajo involucrado en el dibujo y borrado de gráficos está oculto al jugador y solo son visibles los cuadros terminados una vez que se han dibujado.

Hay dos maneras de hacer esto en un Spectrum. Un método sólo funcionará en una máquina con 128K, por lo que vamos a dejarlo de lado por el momento. El otro método en la práctica es más complicado pero funcionará en cualquier Spectrum.

Creando un Búfer de Pantalla

La forma más sencilla de implementar el doble buffer en un Spectrum 48K es la creación una pantalla ficticia en otro lugar de la memoria RAM, y dibujar todos los gráficos de fondo y los sprites ahí. Tan pronto como se haya completado nuestra pantalla copiamos esta pantalla ficticia a la pantalla física en la dirección 16384 haciendo:

.
.
; código para dibujar todos nuestros sprites etc.
.
.
.
.
; ahora la pantalla se dibuja copiándola a la pantalla física.

       ld hl,49152
       ld de,16384
       ld bc,6912
       ldir

Aunque en teoría esto es perfecto, en la práctica copiar 6912 bytes de RAM (o 6144 bytes si ignoramos los atributos de color) de la visualización de la pantalla en cada cuadro es demasiado lento para los juegos de arcade. El secreto consiste en reducir la cantidad de pantalla RAM que se necesita copiar en cada cuadro y encontrar la forma más rápida para transferirla en lugar de la instrucción LDIR.

La primera vía consiste en decidir el tamaño de la pantalla de vamos a ver. La mayoría de juegos separan la pantalla en 2 zonas: un panel de estado para mostrar puntuación, vidas y otros elementos de información y una ventana donde se lleva a cabo toda la acción. Como no necesitamos actualizar el panel de estado en cada trama, nuestra pantalla ficticia sólo tiene que ser tan grande como la ventana de acción. Así que si tuvieramos un panel de estado de 80 x 192 pixel en el borde derecho de la pantalla, nos dejaría una ventana de 176x192 píxeles, es decir, nuestra pantalla simulada solamente tendría que ser de 22 caracteres de ancho por 192 píxeles de alto, o 22x192 = 4224 bytes. El desplazamiento manual de 4224 bytes de una parte a otra de la RAM es mucho menos costoso que la manipulación de 6114 bytes. El truco es encontrar un tamaño que sea lo suficientemente grande como para no restringir el juego, y lo suficientemente pequeño para ser manipulado rápidamente. Por supuesto, también es posible que desees hacer el buffer un poco más grande por los bordes. Si bien estos bordes no se muestran en la pantalla son útiles si se quiere recortar sprites a medida que avanzan en la ventana de acción por los lados.

Una vez que hemos establecido definitivamente el tamaño de nuestro búfer, necesitamos escribir una rutina para transferirlo a la pantalla física uno o dos bytes a la vez. Mientras estamos en eso, también puedes volver a ordenar nuestra pantalla intermedia utilizando un método de visualización más lógico que el utilizado por la pantalla física. Podemos hacer concesiones al peculiar ordenamiento de la memoria de pantalla del Spectrum en nuestra rutina de transferencia, es decir, cualquier rutina de gráficos que haga uso de nuestra memoria de pantalla ficticia se pueden simplificar.

Hay dos maneras muy rápidas de mover una pantalla ficticia a la pantalla de visualización. El primer y más sencillo método es el uso de una gran cantidad de instrucciones LDI desenrolladas. El segundo y más complicado hace uso de PUSH y POP para transferir los datos.

Comencemos con LDI. Si nuestro buffer es de 22 caracteres de ancho podríamos transferir una sola línea de la memoria intermedia a la pantalla de visualización con 22 instrucciones consecutivas LDI, es mucho más rápido usar una gran cantidad de instrucciones LDI en lugar de utilizar un único LDIR. Podríamos escribir una rutina para transferir nuestros datos a partir de una sola línea a la vez, apuntando con HL al comienzo de cada línea de la memoria intermedia, con DE a la línea en la pantalla donde hay que ubicarlo, y luego usar 22 instrucciones LDI para mover los datos. Sin embargo como cada instrucción LDI toma dos bytes de código, es lógico pensar que tal rutina sería al menos de dos veces el tamaño de la memoria intermedia a mover. Un considerable golpe cuando manejamos un poco más de 40K de memoria RAM útil. En su lugar, puede que desees mover las instrucciones LDI a una subrutina que copia a la vez una línea de píxeles, o tal vez un grupo de 8 líneas de píxeles. Esta rutina podría entonces ser llamada desde dentro de un bucle, desenrollado o no, lo que podría hacerse cargo de los registros HL y DE. (NdT: El desenrollado de bucles es un técnica de aceleración usada mucho en código máquina para mejorar la velocidad del programa, consisten en reemplazar el bucle por la repetición del cuerpo del mismo las veces necesarias, de esta manera se eliminan saltos que ralentizan, a cambio de ocupar mas memoria con el programa).

El segundo método consiste en transferir la pantalla virtual a la real usando instrucciones PUSH y POP. Si bien esto tiene la ventaja de ser la manera más rápida de hacerlo, hay algunas desventajas. Necesitas control completo del puntero de pila por lo no se puede producir una interrupción a mitad de la rutina. El puntero de pila debe ser almacenado en alguna parte antes de empezar, y hay que restaurarlo inmediatamente después.

La pila del Spectrum se encuentra normalmente por debajo del código de tu programa, pero este método implica el establecimiento de la pila para que apunte a una parte de la memoria intermedia, para a continuación, utilizando POP, copiar el contenido de la pantalla ficticia en cada uno de los pares de registro a su vez. El puntero de pila se mueve entonces para apuntar a la RAM en la zona de la pantalla de visualización, antes de que los registros sean empujados a la memoria en orden inverso a aquel en el que fueron introducidos. Es decir, los valores se introducen en la memoria intermedia desde el comienzo de cada línea, y se empujan a la pantalla en el orden inverso, lo que va desde el final de la línea hasta su principio.

A continuación se muestra la parte esencial de la rutina de transferencia de pantalla del juego Rallybug. Este utiliza una memoria intermedia de 30 caracteres de ancho, con 28 caracteres visibles en la pantalla. Los restantes 2 caracteres no se muestran de manera que los sprites se mueven lentamente por la pantalla desde el borde, en lugar de aparecer de repente de la nada. Como el ancho de la pantalla visible es de 28 caracteres, esto requiere 14 registros de 16 bits por línea. Obviamente, el Z80A no tiene muchos registros, incluso contando los registros alternativos y los IX e IY. Por tanto, la rutina del Rallybug divide la pantalla en dos mitades de 14 bytes cada una, lo que requiere sólo 7 pares de registros. La rutina establece el puntero de pila al principio de cada línea de la memoria intermedia, para a continuación hacer POP de los datos en AF, BC, DE y HL. A continuación, intercambia estos registros con el conjunto de registros alternativos con EXX, y hace POP de 6 bytes más en BC, DE y HL. Estos registros deben ser ahora ser descargados en el área de la pantalla, por lo que el puntero de pila se establece en el punto del final de la línea de la pantalla correspondiente, y HL, DE y BC son "empujados" con PUSH a su posición, se restauran los registros alternativos,  HL, DE, AC y AF, que son respectivamente copiados a su posición. Esto se repite una y otra vez para cada mitad de cada línea de la pantalla, para al final restaurar el puntero de pila a su posición original.

Complicado, si, pero increíblemente rápido.

SEG1   equ 16514
SEG2   equ 18434
SEG3   equ 20482

P0     equ 0
P1     equ 256
P2     equ 512
P3     equ 768
P4     equ 1024
P5     equ 1280
P6     equ 1536
P7     equ 1792

C0     equ 0
C1     equ 32
C2     equ 64
C3     equ 96
C4     equ 128
C5     equ 160
C6     equ 192
C7     equ 224


xfer   ld (stptr),sp       ; guardar puntero de pila.

; Character line 0.

       ld sp,WINDOW        ; inicio del búferde la linea.
       pop af
       pop bc
       pop de
       pop hl
       exx
       pop bc
       pop de
       pop hl
       ld sp,SEG1+C0+P0+14 ; final de la línea de la pantalla.
       push hl
       push de
       push bc
       exx
       push hl
       push de
       push bc
       push af

       .
       .

       ld sp,WINDOW+4784   ; inicio del búfer de la linea.
       pop af
       pop bc
       pop de
       pop hl
       exx
       pop bc
       pop de
       pop hl
       ld sp,SEG3+C7+P7+28 ; final de la línea de la pantalla.
       push hl
       push de
       push bc
       exx
       push hl
       push de
       push bc
       push af

okay   ld sp,(stptr)       ; restaurar el puntero de pila.
       ret

Haciendo scroll en el búfer

Ahora que tenemos nuestra pantalla ficticia, podemos hacer cualquier cosa que nos guste en ella sin riesgo de parpadeo o de otras anomalías en los gráficos, ya que sólo transferimos el contenido a la pantalla física cuando hemos terminado de construir la imagen. Podemos colocar sprites, enmascarados o no, en cualquier lugar que nos guste y en el orden que nos guste. Nosotros podemos movernos alrededor de la pantalla, animar los gráficos de fondo, y lo más importante, ahora podemos hacer scroll en cualquier dirección.

Se requieren diferentes técnicas para diferentes tipos de desplazamiento, aunque todos tienen una cosa en común: como el desplazamiento es una tarea intensiva del procesador, los bucles desenrollados están a la orden del día. El tipo más simple de desplazamiento es un desplazamiento de pixeles individuales a izquierda/derecha. Un scroll a la derecha de un solo píxel nos obliga a establecer en el registro HL el comienzo de la memoria intermedia y, a continuación, ejecutar los dos operandos siguientes una y otra vez hasta llegar a la final del búfer:

       rr (hl)             ; rotar bandera de acarreo y 8 bits a la derecha.
       inc hl              ; siguiente dirección del búfer.

Del mismo modo, para ejecutar un scroll de un solo píxel hacia la izquierda pondremos en HL el último byte de la memoria y ejecutar estas dos instrucciones hasta llegar al comienzo del búfer:

       rl (hl)             ; rotar bandera de acarreo y 8 bits a la izquierda.
       dec hl              ; siguiente dirección del búfer.

La mayoría de las veces, sin embargo, podemos hacerlo solo incrementando o disminuyendo el registro l, en lugar del par hl, acelerando la rutina aún más. Esto tiene el inconveniente de tener que saber exactamente cuando cambiar el byte alto con los cambios de dirección. Por esta razón por lo general fijo mi dirección de búfer permanentemente justo al principio del proyecto, a menudo en la parte superior de RAM, así que no tengo que volver a escribir las rutinas de desplazamiento cuando las cosas cambian de lugar durante el transcurso del proyecto. Al igual que con la rutina para transferir el búfer a la pantalla física, un bucle desenrollado masivo es muy caro en términos de RAM, por lo que es buena idea escribir un bucle desenrollado más pequeño, que desplace por ejemplo 256 bytes a la vez, luego lo llamamos más o menos 20 veces, dependiendo del tamaño del buffer elegido.

Además del scroll de un píxel a la vez, podemos desplazar cuatro píxeles bastante rápidamente también. Mediante la sustitución de RL (HL) por RLD para el desplazamiento a la izquierda, y RR (HL) por RRD para el desplazamiento a la derecha, podemos mover 4 píxeles.

El desplazamiento vertical se realiza por desplazamiento de bytes sobre la RAM, de la misma forma que la rutina para transferir la pantalla ficticia hacia la física. Para desplazarse un píxel, fijamos nuestra dirección DESDE al inicio de la segunda línea de píxeles y el registro A con la dirección de comienzo de la memoria intermedia, a continuación copiamos los datos desde la dirección DESDE hacia la dirección A hasta llegar al final del búfer. Para desplazarse hacia abajo, tenemos que trabajar en la dirección opuesta, por lo que establecemos nuestro DESDE para que apunte al final de la penúltima línea de la memoria intermedia, y ahora A apunta a la dirección de nuestra última línea, y trabajamos hacia atrás hasta que se alcancen el inicio del búfer. La ventaja añadida del desplazamiento vertical es que podemos desplazarnos hacia arriba o hacia abajo más de una línea, simplemente alterando las direcciones y la rutina se ejecutará la misma rapidez. En términos generales, no es buena idea desplazarse más de un píxel si tu velocidad de cuadros es inferior a 25 cuadros por segundo, porque parecerá que la pantalla vibra.

Hay otra técnica que puede ser empleada para el desplazamiento vertical, y es la que empleé al escribir el juego Megablast para Your Sinclair (NdT: Se refiere a la revista inglesa Tu Sinclair). Implica el tratamiento de la pantalla intermedia como envuelta sobre si misma. En otras palabras, se utiliza la misma cantidad de RAM para la memoria de pantalla intermedia, pero la parte de la memoria intermedia que se comenzar a copiar a la parte superior de la pantalla puede cambiar de un cuadro al siguiente. Cuando llega al final del búfer, se salta de nuevo al principio. Con este sistema, la rutina para copiar el búfer toma la dirección de inicio de la memoria intermedia de un puntero de 16 bits que podría apuntar a cualquier línea en el búfer, y copia los datos en la pantalla física línea por la línea hasta que llega al final de la memoria intermedia. En este punto, la rutina copia los datos desde el principio de la memoria intermedia hacia el resto de la pantalla física. Esto hace que la transferencia de la rutina sea un poco más lenta, y complica cualquier otra rutina de gráficos, que también tienen que volver a la primera línea cada vez que llega a la última línea del búfer. Hacerlo, por otro lado, significa que no hay necesidad de desplazar datos para hacer scrool vertical de la pantalla. Al cambiar el puntero de 16 bits con la primera línea que se copia a la pantalla física, el desplazamiento se realiza automáticamente cuando el búfer se transfiere.

lunes, 2 de mayo de 2016

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

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

Temporización

El tiempo lo es todo. Un juego se arruina fácilmente si se ejecuta demasiado rápido o demasiado lento. La mayoría de los juegos de Spectrum se ejecutarán con demasiada rapidez si lo único que hacen es manipular unos sprites, y necesitan ser ralentizados.

La instrucción Halt

Medimos la velocidad de un juego de Spectrum por la cantidad de tiempo que tarda un recorrido completo del bucle principal, incluyendo todos los trabajos realizados por las rutinas llamadas dentro de este bucle. La forma más sencilla de introducir un retardo es insertar instrucciones halt, que espera por una interrupción, en ciertos puntos del bucle principal para que espere hasta una interrupción. Como el Spectrum genera 50 interrupciones por segundo, esto significa que el bucle principal que tenga 1, 2 ó 3 de tales pausas se ejecutará a 50, 25 o 17 cuadros por segundo respectivamente, en tanto que el resto de procesamiento no ocupa más de un cuadro para completarse. En términos generales, no es buena idea tener el sprite del jugador moviendo más lentamente de 17 cuadros por segundo.

En realidad, la instrucción halt puede ser muy útil. En efecto, espera a que la línea de exploración del televisión llegue al final de la pantalla. Esto significa que un buen momento para borrar, mover y volver a mostrar un sprite es inmediatamente después de un halt, porque la línea de exploración no va a encontrarse con la imagen y no hay posibilidad de parpadeo. Si tienes un panel de estado de tu juego en la parte superior de la pantalla, esto significa que hay aún más tiempo para que la línea de exploración viaje antes de que alcance la zona de sprites, y a menudo puedes manipular un par de sprites después de una interrupción sin mucho peligro de parpadeo.

La instrucción halt también se puede utilizar en un bucle para hacer una pausa durante períodos más largos. El siguiente código hará una pausa de 100 cincuentavos de segundo (lo que son dos segundos).

       ld b,100            ; duración de la pausa.
delay  halt                ; esperar una interrupción.
       djnz delay          ; repetir.

El reloj del Spectrum y la rutina Vsync

Por desgracia, halt es un instrumento sin punta. Siempre espera a la siguiente interrupción independientemente del tiempo que quede para la siguiente. Imagina una situación en la que el bucle principal tarda 3/4 del tiempo del cuadro para hacer su procesamiento la mayor parte del tiempo, pero de vez en cuando tiene períodos en los que hay un procesamiento adicional que tarda medio tiempo de cuadro adicional. En estas circunstancias, un halt mantendrá el juego a la constantes de 50 cuadrod por segundo la mayoría de las veces, pero cuando el procesamiento adicional entra en acción, la primera interrupción ha pasado y halt esperará hasta la siguiente, lo que significa que el juego se ralentiza a 25 cuadros por segundo periódicamente.

Hay una manera de evitar este problema, consiste en contar el número de cuadros que han transcurrido desde la última iteración del bucle principal. En el Spectrum, la rutina de servicio de interrupción de la ROM actualiza el contador de cuadros de 24 bits del Spectrum 50 veces por segundo, y tambien hace otras cosas. Este contador es almacenado en las variables del sistema en la dirección 23672, por lo que mediante la comprobación de esta ubicación una vez en cada iteración del bucle, podemos saber cuántas interrupciones se han producido desde la última vez que estuvimos en ese mismo punto. Naturalmente, si quieres escribir tu propia rutina de manejo de interrupción, puedes utilizar o bien en primer lugar rst 56 para actualizar el reloj, o bien incrementar un contador de cuadros por ti mismo si deseas utilizar este método.

La siguiente rutina vsync está diseñada para estabilizar un juego y hacerlo funcionar a un nivel más o menos constantes de 25 cuadros por segundo:

wait   ld hl,pretim        ; carga el temporizador anterior.
       ld a,(23672)        ; carga el temporizador actual.
       sub (hl)            ; diferencia entre los dos.
       cp 2                ; ¿ya han transcurrido dos cuadros?
       jr nc,wait0         ; sí, no más demora.
       jp wait
wait0  ld a,(23672)        ; carga el temporizador actual.
       ld (hl),a           ; guardar su valor como anterior.
       ret
pretim defb 0

En lugar de simplemente esperar en un bucle, se podría realizar alguna procesamiento adicional no esencial. Este es un buen punto en el que poner cualquier efecto de sonido del altavoz. Una buena idea es cuando lo necesites establecer un byte para indicar el tipo de efecto de sonido a reproducir, y a continuación comprobar este octeto en tu rutina vsync y llamar a la rutina correspondiente al efecto de sonido. Tus rutinas de efectos de sonido también necesitarán controles periódicos para ver si el contador de cuadros ha llegado a su fin, y salir cuando lo ha hecho.

Hay otras cosas que tal vez quiera hacer con este tiempo de CPU. Yo a veces renuevo mis sprites en la tabla que los mantienen, cambiando el orden en que se muestran en cada bucle para ayudar a prevenir el parpadeo.

Semilla de Números Aleatorios

El contador de tramas del Spectrum es útil para otra cosa: se puede utilizar para inicializar la semilla de los números aleatorios. Utilizando el generador de números aleatorios propuesto en el capítulo de números aleatorios, podemos hacer esto:

       ld a,(23672)        ; temporizador actual.
       ld (seed),a         ; establece primer byte de la semilla de aleatorios.

Esto va bien si estamos trabajando con el hardware real y nos asegurará que un juego no comience con la misma secuencia de números aleatorios cada vez que se juega. Por desgracia, los autores de los emuladores tienen la mala costumbre de cargar automáticamente los archivos de cinta una vez abiertos, una práctica que no sólo hace difícil el desarrollo, da lugar a que la máquina esté siempre en el mismo estado cada vez que el juego se cargue, es decir, los números aleatorios pueden seguir la misma secuencia cada vez que se juega a ese juego. La solución para el programador de juegos es esperar a que se pulse una tecla tan pronto como nuestro juego ha cargado, después de lo cual podemos configurar nuestra semilla. Esto introduce un elemento humano y asegura que el generador de números aleatorios es diferente cada vez.

viernes, 29 de abril de 2016

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

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

Movimientos de enemigos

Ya tenemos nuestro fondo cargado y permitimos al jugador mover un sprite a su alrededor, lo que ahora necesitamos son algunos sprites de enemigos que el jugador deba evitar. Un programador novato podría pelear mucho con esto, pero en realidad es mucho más simple de lo que parece.

Enemigos que patrullan

El tipo de enemigo más fácil de programar es el que usa un algoritmo fijo a seguir, o patrulla una ruta predeterminada. Hemos cubierto una de estas técnicas en el juego del ciempiés anterior. Otro ejemplo muy sencillo es la que se encuentra en juegos tales como Jet Set Willy, donde un sprite se desplaza en una sola dirección hasta que llega al final de su zona de patrulla, a continuación cambia de dirección y se dirige de nuevo a su punto de partida, antes de cambiar de dirección otra vez e iniciar el ciclo de nuevo. Como se puede imaginar, estas rutinas son increíblemente fáciles de escribir.

En primer lugar hemos de crear en nuestra tabla con la estructura del alien una coordenada de posición mínima y máxima, y la dirección actual. Es generalmente buena idea comentar estas tablas, así que vamos a hacerlo.

; tabla de datos del Alien, 6 bytes por alien.
; ix     = tipo de gráfico, como pollo/medusa etc.
; ix + 1 = dirección, 0=arriba, 1=derecha, 2=abajo, 3=izquierda.
; ix + 2 = coordenada x actual.
; ix + 3 = coordenada y actual.
; ix + 4 = mínima coordenada x o y , dependiendo de la dirección.
; ix + 5 = máxima coordenada x o y , dependiendo de la dirección.

altab  defb 0,0,0,0,0,0
       defb 0,0,0,0,0,0
       defb 0,0,0,0,0,0

luego para manejar su sprite podríamos escribir algo como esto

       ld a,(ix+1)         ; dirección del movimiento del alien.
       rra                 ; rotar bit bajo con acarreo.
       jr nc,movav         ; si no hay acarreo = 0 o 2, debe ser vertical.

; dirección es 1 o 3 por lo que es horizontal.

       rra                 ; rotar siguiente bit con para probar.
       jr nc,movar         ; dirección 1 = mover a la derecha el alien.

; Mover alien a la izquierda.

moval  ld a,(ix+3)         ; obtener la coordenada y.
       sub 2               ; mover hacia la izquierda.
       ld (ix+3),a
       cp (ix+4)           ; ¿se ha llegado al mínimo?
       jr z,movax          ; sí, cambio de dirección.
       jr c,movax          ; Hay, nos hemos pasado.
       ret

; Mover alien a la derecha.

movar  ld a,(ix+3)         ; obtener la coordenada y.
       add a,2             ; mover hacia la derecha.
       ld (ix+3),a
       cp (ix+5)           ; ¿se ha llegado al máximo?
       jr nc,movax         ; sí, cambio de dirección.
       ret

; Mover alien verticalmente.

movav  rra                 ; probar la dirección.
       jr c,movad          ; dirección 2 es abajo.

; Mover alien hacia arriba.

movau  ld a,(ix+2)         ; obtener la coordenada x.
       sub 2               ; mover hacia arriba.
       ld (ix+2),a
       cp (ix+4)           ; ¿se ha llegado al mínimo?
       jr z,movax          ; sí, cambio de dirección.
       ret

; Mover alien hacia abajo.

movad  ld a,(ix+2)         ; obtener la coordenada x.
       add a,2             ; mover hacia abajo.
       ld (ix+2),a         ; nueva coordenada.
       cp (ix+5)           ; ¿se ha llegado al máximo?
       jr nc,movax         ; sí, cambio de dirección.
       ret

; Cambiar dirección del alien.

movax  ld a,(ix+1)         ; indicador de dirección.
       xor 2               ; cambiar dirección, ya sea
                           ; horizontal o verticalmente.
       ld (ix+1),a         ; establecer una nueva dirección.
       ret

Si quisiéramos ir más lejos podríamos introducir una bandera extra en nuestra tabla, ix + 6, para controlar la velocidad del sprite, y sólo moverlo, por ejemplo, cada dos cuadros si se establece el indicador. Aunque es simple de escribir y con bajo uso de memoria, este tipo de movimiento es bastante básico, predecible y de uso limitado. Para enemigos con patrullaje más complicado, por ejemplo, las olas de ataque de alienígenas en un shoot-em-up, necesitamos tablas de coordenadas, y mientras que el código es también fácil de escribir, coordinan las tablas rápidamente se come la memoria, sobre todo si se almacenan ambas coordenadas x e y. Para acceder a una tabla de este tipo se necesitan dos bytes por sprites que actúan como un puntero a la tabla de coordenadas.

Una sección típica de código se vería así:

                           ; NdT: little endian = primero byte bajo
       ld l,(ix+2)         ; puntero de byte bajo, little endian.
       ld h,(ix+3)         ; puntero byte alto.
       ld c,(hl)           ; poner coordenada x en c.
       inc hl              ; punto a la coordenada y.
       ld b,(hl)           ; poner la coordenada y en b.
       inc hl              ; apuntar a la siguiente posición.
       ld (ix+2),l         ; siguiente puntero al byte bajo.
       ld (ix+3),h         ; siguiente puntero al byte alto.

A continuación un ejemplo un poco más complicado, muestra una oleada de 8 naves de ataque usando una tabla de coordenadas verticales. La posición horizontal de cada sprite se mueve a la izquierda a una velocidad constante de 2 píxeles por imagen, así que no hay necesidad de preocuparse de guardarla. Se utiliza la rutina de sprites precargados del capítulo 8, de manera que los sprites son un poco "peliculeros", pero eso no es importante ahora.

mloop  halt                ; esperar al haz de TV.
       ld ix,entab         ; apuntar a las naves impares.
       call mship          ; mover las naves.
       halt
       ld ix,entab+4       ; apuntar a las naves pares.
       call mship          ; mover las naves de nuevo.
       call gwave          ; generar ondas frescas.
       jp mloop            ; volver al inicio del bucle.

; Mover las naves enemigas.

mship  ld b,4              ; Número a procesar.
mship0 push bc             ; guardar contador.
       ld a,(ix)           ; obtener puntero bajo.
       ld l,a              ; ponerlo en l.
       ld h,(ix+1)         ; obtener byte alto.
       or h                ; comprobar que el puntero está configurado.
       and a               ; ¿lo está?
       call nz,mship1      ; sí, procesarlo.
       ld de,8             ; saltar a la siguiente solo-pero-una entrada.
       add ix,de           ; apuntar al siguiente enemigo.
       pop bc              ; restaurar contador.
       djnz mship0         ; repetir para todos los enemigos.
       ret

mship1 push hl             ; guardar puntero a la coordenada.
       call dship          ; Eliminar esta nave.
       pop hl              ; restaurar coordenadas.
       ld a,(hl)           ; recuperar la siguiente coordenada.
       inc hl              ; mover el puntero allí.
       ld (ix),l           ; nuevo puntero al byte bajo.
       ld (ix+1),h         ; puntero al byte alto.
       ld (ix+2),a         ; establecer coordenada x.
       ld a,(ix+3)         ; recuperar la posición horizontal.
       sub 2               ; mover hacia la izquierda 2 píxeles.
       ld (ix+3),a         ; establecer nueva posición.
       cp 240              ; ¿alcanzamos el borde de la pantalla?
       jp c,dship          ; no por el momento, mostrar en la nueva posición.
       xor a               ; poner a cero el acumulador.
       ld (ix),a           ; borrar el byte bajo del puntero.
       ld (ix+1),a         ; limpiar el byte alto del puntero.
       ld hl,numenm        ; número de enemigos en pantalla.
       dec (hl)            ; uno menos a los que hacer frente.
       ret

gwave  ld hl,shipc         ; contador de naves.
       dec (hl)            ; una menos.
       ld a,(hl)           ; comprobar nuevo valor.
       cp 128              ; ¿esperando el siguiente ataque?
       jr z,gwave2         ; ataque es inminente por lo que hay que configurarlo.
       ret nc              ; si.
       and 7               ; ¿es hora de generar una nueva nave?
       ret nz              ; aún no lo es
       ld ix,entab         ; tabla de enemigos.
       ld de,4             ; tamaño de cada entrada.
       ld b,8              ; Número a comprobar.
gwave0 ld a,(ix)           ; byte bajo del puntero.
       ld h,(ix+1)         ; byte alto.
       or h                ; ¿están a cero?
       jr z,gwave1         ; sí, esta entrada está vacía.
       add ix,de           ; apuntar a la siguiente nave.
       djnz gwave0         ; repetir hasta que encontremos una.
       ret
gwave2 ld hl,wavnum        ; presentar el numero de la oleada.
       ld a,(hl)           ; recuperar la configuración actual.
       inc a               ; siguiente a lo largo.
       and 3               ; empezar de nuevo después de la 4ª oleada.
       ld (hl),a           ; escribir nuevo ajuste.
       ret
gwave1 ld hl,numenm        ; número de enemigos en pantalla.
       inc (hl)            ; uno más con que pelear.
       ld a,(wavnum)       ; numero de oleada.
       ld hl,wavlst        ; punteros de datos de oleadas.
       rlca                ; multiplico por 2.
       rlca                ; multiplico por 4.
       ld e,a              ; desplazamiento en e.
       ld d,0              ; sin byte alto.
       add hl,de           ; encontrar la dirección de la oleada.
       ld a,(shipc)        ; contador de naves.
       and 8               ; ¿ataque par o impar?
       rrca                ; hacerlo múltiplo de 2 en consecuencia.
       rrca
       ld e,a              ; desplazamiento en e.
       ld d,0              ; sin byte alto.
       add hl,de           ; apunto a la primera o a la segunda mitad del ataque.
       ld e,(hl)           ; byte bajo del puntero del ataque.
       inc hl              ; segundo byte.
       ld d,(hl)           ; byte alto del puntero del ataque.
       ld (ix),e           ; byte bajo del puntero.
       ld (ix+1),d         ; byte alto.
       ld a,(de)           ; Recuperar la primera coordenada.
       ld (ix+2),a         ; establecer x.
       ld (ix+3),240       ; comenzar en el borde derecho de la pantalla.

; Visualización de las naves enemigas.

dship  ld hl,shipg         ; Dirección de los sprites.
       ld b,(ix+3)         ; coordenada y.
       ld c,(ix+2)         ; coordenada x.
       ld (xcoord),bc      ; establecer coordenadas de la rutina de sprites.
       jp sprite           ; llamar a la rutina de sprites.
shipc  defb 128            ; contador de naves.
numenm defb 0              ; número de enemigos.


; Coordenadas de las oleadas de ataque.
; Sólo la coordenada vertical se almacena ya que todas las naves
; se mueven hacia la izquierda 2 píxeles en cada fotograma.

coord0 defb 40,40,40,40,40,40,40,40
       defb 40,40,40,40,40,40,40,40
       defb 42,44,46,48,50,52,54,56
       defb 58,60,62,64,66,68,70,72
       defb 72,72,72,72,72,72,72,72
       defb 72,72,72,72,72,72,72,72
       defb 70,68,66,64,62,60,58,56
       defb 54,52,50,48,46,44,42,40
       defb 40,40,40,40,40,40,40,40
       defb 40,40,40,40,40,40,40,40
       defb 38,36,34,32,30,28,26,24
       defb 22,20,18,16,14,12,10,8
       defb 6,4,2,0,2,4,6,8
       defb 10,12,14,16,18,20,22,24
       defb 26,28,30,32,34,36,38,40
coord1 defb 136,136,136,136,136,136,136,136
       defb 136,136,136,136,136,136,136,136
       defb 134,132,130,128,126,124,122,120
       defb 118,116,114,112,110,108,106,104
       defb 104,104,104,104,104,104,104,104
       defb 104,104,104,104,104,104,104,104
       defb 106,108,110,112,114,116,118,120
       defb 122,124,126,128,130,132,134,136
       defb 136,136,136,136,136,136,136,136
       defb 136,136,136,136,136,136,136,136
       defb 138,140,142,144,146,148,150,152
       defb 154,156,158,160,162,164,166,168
       defb 170,172,174,176,174,172,170,168
       defb 166,164,162,160,158,156,154,152
       defb 150,148,146,144,142,140,138,136


; Lista de oleadas de ataque.

wavlst defw coord0,coord0,coord1,coord1
       defw coord1,coord0,coord0,coord1

wavnum defb 0              ; puntero actual a la oleada wave pointer.


; Sprite de la nave.

shipg  defb 248,252,48,24,24,48,12,96,24,48,31,243,127,247,255,247
       defb 255,247,127,247,31,243,24,48,12,96,24,48,48,24,248,252

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,(xcoord)        ; dibuja el sprite (hl).
       ld (tmp1),a         ; guardar vertical.
       call scadd          ; calcular dirección de la pantalla.
       ld a,16             ; altura del sprite en pixeles.
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               ; hay, 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.

scadd  ld a,(xcoord)       ; recuperar la coordenada vertical.
       ld e,a              ; guardarla en e.

; Encuentra línea dentro de la celda.

       and 7               ; línea 0-7 en el cuadrado de caracteres.
       add a,64            ; 64 * 256 = 16384 = inicio de la pantalla.
       ld d,a              ; d = línea * 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

xcoord defb 0              ; coordenada de pantalla.
ycoord defb 0              ; coordenada de pantalla.
tmp0   defw 0              ; espacio de trabajo.
tmp1   defb 0              ; posición vertical temporal.

; tabla de naves enemigas, 8 entradas x 4 bytes cada una.

entab  defb 0,0,0,0
       defb 0,0,0,0
       defb 0,0,0,0
       defb 0,0,0,0
       defb 0,0,0,0
       defb 0,0,0,0
       defb 0,0,0,0
       defb 0,0,0,0

 Enemigos inteligentes

Hasta ahora nos hemos ocupado de drones predecibles, pero ¿queremos dar al jugador la ilusión de que los sprites enemigos piensan por sí mismos? Una vía que podemos utilizar para esto sería para darles una decisión totalmente al azar en el momento de su creación.

Aquí está el código fuente de Turbomania, un juego escrito originalmente para la 1K coding competition (concursos de código en 1K) de 2005. Es muy simple, pero incorpora movimiento puramente aleatorio. Los coches enemigos viajan en una dirección hasta que ya no se pueden mover, a continuación seleccionan otra dirección al azar. Además, un coche puede cambiar de dirección al azar incluso si puede continuar en su dirección actual. Es muy primitivo por supuesto, solo echa un vistazo a la rutina MCAR y verá exactamente lo quiero decir.

       org 24576

; Constantes.

YELLOW equ 49              ; atributo de color amarillo normal.
YELLOB equ YELLOW + 64     ; atributo de color amarillo brillante.

; Código principal del juego.

; Borrar la pantalla para dar color verde alrededor de los bordes.

       ld hl,23693         ; variable de sistema para atributos.
       ld (hl),36          ; quiero fondo verde.

waitk  ld a,(23560)        ; leer teclado.
       cp 32               ; ¿pulsado ESPACIO?
       jr nz,waitk         ; no, espera.
       call nexlev         ; jugar,
       jr waitk            ; ESPACIO para reiniciar el juego.


; Borrar los datos por nivel.

nexlev call 3503           ; borrar la pantalla.
       ld hl,rmdat         ; Datos de las salas.
       ld de,rmdat+1
       ld (hl),1           ; configurar un bloque en la sombra.
       ld bc,16            ; longitud de sala menos el primer byte.
       ldir                ; copiar al resto de la primera fila.
       ld bc,160           ; longitud de la sala menos la primera fila.
       ld (hl),b           ; borrar primer byte.
       ldir                ; borrar datos de la sala.

; Configurar los bloques predeterminados.

       ld c,15             ; última posición del bloque.
popbl0 ld b,9              ; última fila.
popbl1 call filblk         ; llenar el bloque.
       dec b               ; una columna hacia arriba.
       jr z,popbl2         ; columna hecha, seguir adelante.
       dec b               ; y de nuevo.
       jr popbl1
popbl2 dec c               ; moverse en la fila.
       jr z,popbl3         ; columna completa, seguir adelante.
       dec c               ; siguiente fila.
       jr popbl0

; Ahora dibujar los bits únicos para este nivel.

popbl3 ld b,7              ; número de bloques para insertar.
popbl5 push bc             ; guardar contador.
       call random         ; obtener un número aleatorio.
       and 6               ; números en el rango 0-6 por favor.
       add a,2             ; cambiar a 2-8.
       ld b,a              ; esa es la columna.
popbl4 call random         ; otro número.
       and 14              ; buscamos números pares 0-12.
       cp 14               ; ¿más alto de lo que queremos?
       jr nc,popbl4        ; si, inténtelo de nuevo.
       inc a               ; colocarlo en el rango 1-13.
       ld c,a              ; esa es la fila.
       call filblk         ; llenar bloque.
popbl6 call random         ; otro número aleatorio.
       and 14              ; Sólo queremos 0-8.
       cp 9                ; ¿por encima de número que queremos?
       jr nc,popbl6        ; inténtelo de nuevo.
       inc a               ; convertirlo a 1-9.
       ld b,a              ; coordenada vertical.
       call random         ; obtener bloque horizontal.
       and 14              ; par, 0-14.
       ld c,a              ; posición y.
       call filblk         ; llenar en ese cuadrado.
       pop bc              ; restaurar el contador.
       djnz popbl5         ; uno menos que hacer.

       xor a               ; cero.
       ld hl,playi         ; dirección deseada del jugador.
       ld (hl),a           ; dirección por defecto.
       inc hl              ; apuntar a la dirección a presentar.
       ld (hl),a           ; dirección por defecto.
       inc hl              ; siguiente dirección del jugador.
       ld (hl),a           ; dirección por defecto.
       out (254),a         ; establecer el color del borde mientras tanto.
       call atroom         ; mostrar disposición del nivel actual.

       ld hl,168+8*256     ; coordenadas.
       ld (encar2+1),hl    ; establecer la posición del segundo coche.
       ld h,l              ; coordenada y a la derecha.
       ld (encar1+1),hl    ; establecer la posición del primer coche.
       ld l,40             ; x en la parte superior de la pantalla.
       ld (playx),hl       ; iniciar al jugador aquí.
       ld hl,encar1        ; primer coche.
       call scar           ; presentarlo.
       ld hl,encar2        ; segundo coche.
       call scar           ; presentarlo.
       call dplayr         ; mostrar el sprite del jugador.
       call blkcar         ; hacer el coche del jugador de color negro.

; Retardo de dos segundos antes de empezar.

       ld b,100            ; longitud del retardo.
waitt  halt                ; esperar una interrupción.
       djnz waitt          ; repetir.

mloop  halt                ; haz de electrones en la parte superior izquierda.
       call dplayr         ; Eliminar jugador.

; Poner como atributo tinta azul de nuevo.

       call gpatts         ; obtener atributo del jugador.
       defb 17,239,41      ; retirar el fondo verde, añadir fondo y tinta azul.
       call attblk         ; establecer el color de la carretera.

; Mover el coche del jugador.

       ld a,(playd)        ; dirección .
       ld bc,(playx)       ; coordenadas del jugador.
       call movc           ; mover coordenadas.
       ld hl,(dispx)       ; nuevas coordenadas.
       ld (playx),hl       ; establecer una nueva posición del jugador.

; ¿Podemos cambiar de dirección?

       ld a,(playi)        ; dirección deseada del jugador.
       ld bc,(playx)       ; coordenadas del jugador.
       call movc           ; mover coordenadas.
       call z,setpn        ; establecer la nueva dirección del jugador.

; Cambia la dirección.

       ld a,(nplayd)       ; nueva dirección del jugador.
       ld (playd),a        ; fijar la dirección actual.

       call dplayr         ; volver a mostrar en la nueva posición.

; Establecer los atributos de los coches.

       call blkcar         ; hacer el coche del jugador negro.

; Controles.

       ld a,239            ; fila del teclado 6-0 = 61438.
       ld e,1              ; dirección derecha.
       in a,(254)          ; leer teclas.
       rra                 ; ¿jugador mueve a la derecha?
       call nc,setpd       ; sí, establecer la dirección del jugador.
       ld e,3              ; dirección izquierda.
       rra                 ; ¿jugador movió a la izquierda
       call nc,setpd       ; sí, establecer la dirección del jugador.
       ld a,247            ; 63486 es el puerto para la fila 1-5.
       ld e,0              ; dirección hacia arriba.
       in a,(254)          ; leer teclas.
       and 2               ; comprobar segunda tecla en (2).
       call z,setpd        ; establecer la dirección.
       ld a,251            ; 64510 es el puerto para la fila Q-T.
       ld e,2              ; dirección hacia abajo.
       in a,(254)          ; leer teclas.
       and e               ; comprobar segunda tecla del borde (W)..
       call z,setpd        ; establecer la dirección.

; Coches enemigos.

       ld hl,encar1        ; coche del enemigo 1.
       push hl             ; guardar puntero.
       call procar         ; procesar el coche.
       pop hl              ; recuperar el puntero del coche.
       call coldet         ; comprobar colisiones.
       ld hl,encar2        ; coche del enemigo 2.
       push hl             ; guardar puntero.
       halt                ; sincronizar con pantalla.
       call procar         ; procesar el coche.
       pop hl              ; recuperar el puntero del coche.
       call coldet         ; comprobar colisiones.

; Contar espacio amarillo restante.

       ld hl,22560         ; dirección.
       ld bc,704           ; atributos que contar.
       ld a,YELLOB         ; atributos que estamos buscando.
       cpir                ; contar caracteres.
       ld a,b              ; byte alto del resultado.
       or c                ; combinan con el byte bajo.
       jp z,nexlev         ; nada a la izquierda, ir al siguiente nivel.

; Fin del bucle principal.

       jp mloop

; Coche negro sobre fondo cian.

blkcar call gpatts         ; obtener atributos de los jugadores.
       defb 17,232,40      ; quitar el fondo rojo/tinta azul, añadir fondo azul.

; Establecer los atributos de bloque de 16x16 píxeles.

attblk call attlin         ; pintar línea horizontal.
       call attlin         ; pintar otra línea.
       ld a,c              ; posición vertical.
       and 7               ; ¿está a caballo entre celdas?
       ret z               ; no, entonces no hay tercera línea.

attlin call setatt         ; pintar la carretera.
       call setatt         ; y otra vez.
       ld a,b              ; posición horizontal.
       and 7               ; ¿a caballo entre los bloques?
       jr z,attln0         ; no, dejar tercera celda como está.
       call setatt         ; establecer atributo.
       dec l               ; atrás de nuevo una celda.
attln0 push de             ; conservar los colores.
       ld de,30            ; distancia al siguiente.
       add hl,de           ; puntero a la siguiente fila hacia abajo.
       pop de              ; restaurar máscaras de color.
       ret

; Establecer el atributo de una sola celda.

setatt ld a,(hl)           ; recuperar el atributo de la celda que lo contiene.
       and e               ; eliminar los elementos de color en el registro c.
       or d                ; añadir los de b para formar nuevo color.
       ld (hl),a           ; establecer color.
       inc l               ; siguiente celda.
       ret

; Detección de colisiones, basada en coordenadas.

coldet call getabc         ; obtener coordenadas.
       ld a,(playx)        ; posición horizontal.
       sub c               ; comparar con el coche x.
       jr nc,coldt0        ; resultado fue positivo.
       neg                 ; era negativo, revertir el signo.
coldt0 cp 16               ; ¿dentro de los 15 píxeles?
       ret nc              ; no hubo colisión.
       ld a,(playy)        ; jugador y.
       sub b               ; comparar con el coche y.
       jr nc,coldt1        ; resultado fue positivo.
       neg                 ; era negativo, revertir el signo.
coldt1 cp 16               ; ¿dentro de los 15 píxeles?
       ret nc              ; no hubo colisión.
       pop de              ; eliminar la dirección de retorno de la pila.
       ret

setpd  ex af,af'
       ld a,e              ; dirección.
       ld (playi),a        ; fijar la dirección prevista.
       ex af,af'
       ret

setpn  ld a,(playi)        ; nueva dirección deseada.
       ld (nplayd),a       ; establecer siguiente dirección.
       ret

; Mover las coordenadas del sprites en la dirección correspondiente.

movc   ld (dispx),bc       ; posición por defecto.
       and a               ; dirección 0.
       jr z,movcu          ; mover hacia arriba.
       dec a               ; dirección 1.
       jr z,movcr          ; mover hacia arriba.
       dec a               ; dirección 2.
       jr z,movcd          ; mover hacia arriba.
movcl  dec b               ; izquierda un píxel.
       dec b               ; izquierda de nuevo.
movc0  call chkpix         ; comprobar los atributos de píxeles.
       ld (dispx),bc       ; nuevas coordenadas.
       ret
movcu  dec c               ; arriba un pixel.
       dec c               ; y de nuevo.
       jr movc0
movcr  inc b               ; derecha un píxel.
       inc b               ; derecha de nuevo.
       jr movc0
movcd  inc c               ; abajo un píxel.
       inc c               ; una vez más.
       jr movc0


; Comprobar los atributos del pixel por colisión.
; Cualquier celda con tinta verde es sólidas.

chkpix call ataddp         ; obtener la dirección del atributo de píxeles.
       and 4               ; comprobar color de la tinta.
       jr nz,chkpx0        ; inválida, bloquear el movimiento.
       inc hl              ; siguiente cuadrado a la derecha.
       ld a,(hl)           ; obtener atributos.
       and 4               ; comprobar colores de la tinta.
       jr nz,chkpx0        ; inválida, bloquear el movimiento.
       inc hl              ; siguiente cuadrado a la derecha.
       ld a,b              ; posición horizontal.
       and 7               ; ¿a caballo entre las celdas?
       jr z,chkpx1         ; no, mirar abajo entonces.
       ld a,(hl)           ; obtener atributos. 
       and 4               ; comprobar color de la tinta.
       jr nz,chkpx0        ; inválida, bloquear el movimiento.
chkpx1 ld de,30            ; distancia hasta la siguiente celda hacia abajo.
       add hl,de           ; apuntar aquí.
       ld a,(hl)           ; obtener atributos.
       and 4               ; comprobar color de la tinta.
       jr nz,chkpx0        ; inválida, bloquear el movimiento.
       inc hl              ; siguiente cuadrado a la derecha.
       ld a,(hl)           ; obtener atributos.
       and 4               ; comprobar color de la tinta.
       jr nz,chkpx0        ; inválida, bloquear el movimiento.
       inc hl              ; siguiente cuadrado a la derecha.
       ld a,b              ; posición horizontal.
       and 7               ; ¿a caballo entre las celdas?
       jr z,chkpx2         ; no, mirar abajo entonces.
       ld a,(hl)           ; obtener atributos.
       and 4               ; comprobar color de la tinta.
       jr nz,chkpx0        ; inválida, bloquear el movimiento.
chkpx2 ld a,c              ; distancia desde la parte superior de la pantalla.
       and 7               ; ¿estamos a caballo entre celdas verticales?
       ret z               ; no, el movimiento es bueno.
       add hl,de           ; apuntar allí.
       ld a,(hl)           ; obtener atributos.
       and 4               ; comprobar color de la tinta.
       jr nz,chkpx0        ; inválida, bloquear el movimiento.
       inc hl              ; siguiente cuadrado a la derecha.
       ld a,(hl)           ; obtener atributos.
       and 4               ; comprobar color de la tinta.
       jr nz,chkpx0        ; inválida, bloquear el movimiento.
       inc hl              ; siguiente cuadrado a la derecha.
       ld a,b              ; posición horizontal.
       and 7               ; ¿a caballo entre las celdas?
       ret z               ; no, el movimiento es correcto.
       ld a,(hl)           ; obtener atributos.
       and 4               ; comprobar color de la tinta.
       jr nz,chkpx0        ; inválida, bloquear el movimiento.
       ret                 ; adelante.

chkpx0 pop de              ; eliminar la dirección de retorno de la pila.
       ret

; Llenar un bloque en el mapa.

; Obtener dirección del bloque.

filblk ld a,b              ; número de fila.
       rlca                ; multiplicar por 16.
       rlca
       rlca
       rlca
       add a,c             ; añadir al desplazamiento total.
       ld e,a              ; este desplazamiento es pequeño.
       ld d,0              ; no necesita byte alto.
       ld hl,rmdat         ; dirección de datos de la sala.
       add hl,de           ; añadir al bloque de dirección.

; dirección de bloque está en hl, vamos a llenarlo.

       ld (hl),2           ; poner el bloque en on.
       ld de,16            ; distancia al siguiente bloque de abajo.
       add hl,de           ; apuntar allí.
       ld a,(hl)           ; chequearlo.
       and a               ; ¿está establecido?
       ret nz              ; sí, no sobrescribir.
       ld (hl),1           ; ajustar la sombra.
       ret

; Dibujar una pantalla compuesta enteramente de bloques de atributos.

atroom ld hl,rmdat         ; Datos de la sala.
       ld a,1              ; comenzar en la fila 1.
       ld (dispx),a        ; establecer coordenadas.
       ld b,11             ; contar filas.
atrm0  push bc             ; guardar el contador.
       ld b,15             ; contador de columnas.
       ld a,1              ; número de columna.
       ld (dispy),a        ; ajustado a la izquierda de la pantalla.
atrm1  push bc             ; guardar el contador.
       ld a,(hl)           ; obtener siguiente tipo de bloque.
       push hl             ; guardar dirección de datos.
       rlca                ; numero de bloque doblado.
       rlca                ; y de nuevo para múltiplo de 4.
       ld e,a              ; desplazamiento para dirección del bloque.
       ld d,0              ; no necesita byte alto.
       ld hl,blkatt        ; atributos del bloque.
       add hl,de           ; apuntar al bloque que queremos.
       call atadd          ; obtener dirección de la posición de la pantalla.
       ldi                 ; transferir primer bloque.
       ldi                 ; y el segundo.
       ld bc,30            ; distancia hasta la siguiente fila.
       ex de,hl            ; conmutar celda y dirección de pantalla.
       add hl,bc           ; apuntar a la siguiente fila por abajo.
       ex de,hl            ; intercambiarlas de nuevo.
       ldi                 ; hacer la tercera celda.
       ldi                 ; atributo de la cuarta celda.
       ld hl,dispy         ; número de columna.
       inc (hl)            ; moverse a través de una celda.
       inc (hl)            ; y otra.
       pop hl              ; restaurar la dirección de la sala.
       pop bc              ; restablecer contador de columna.
       inc hl              ; apuntar al siguiente bloque.
       djnz atrm1          ; hacer resto de la hilera.
       inc hl              ; saltar un carácter, las líneas son múltiplos 16.
       ld a,(dispx)        ; posición vertical.
       add a,2             ; mirar 2 celdas hacia abajo.
       ld (dispx),a        ; nueva fila.
       pop bc              ; restablecer contador de columna.
       djnz atrm0          ; hacer las filas restantes.
       ret

; Atributos del bloque de fondo.

blkatt defb YELLOB,YELLOB  ; espacio.
       defb YELLOB,YELLOB
       defb YELLOW,YELLOW  ; espacio sombreado.
       defb YELLOB,YELLOB
       defb 124,68         ; Modelo blanco/negro bandera de cuadros.
       defb 68,124

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

atadd  push hl             ; necesitamos preservar el par hl.
       ld hl,(dispx)       ; coordenadas a comprobar, en coordenadas de carácter.
       add hl,hl           ; multiplicar  x e y por 8.
       add hl,hl
       add hl,hl
       ld b,h              ; copiar coordenada y a b.
       ld c,l              ; poner coordenada x en c.
       call ataddp         ; obtener la dirección del pixel.
       ex de,hl            ; poner la dirección en de.
       pop hl              ; restaurar hl.
       ret


; Obtener atributos del jugador.

gpatts ld bc,(playx)       ; coordenadas del jugador.

; Calcular la dirección del atributo para el píxel en (c, b).

ataddp ld a,c              ; mira primero la vertical.
       rlca                ; divida por 64.
       rlca                ; mas rápido que 6 operaciones rrca.
       ld l,a              ; almacenar en el registro l por ahora.
       and 3               ; enmascarar para encontrar el segmento.
       add a,88            ; los atributos comienzan a partir de 88*256=22528.
       ld h,a              ; así está ordenado el byte alto.
       ld a,l              ; vertical/64 es igual que vertical*4.
       and 224             ; buscamos un múltiplo de 32.
       ld l,a              ; elemento vertical calculado.
       ld a,b              ; obtener la posición horizontal.
       rra                 ; dividir por 8.
       rra
       rra
       and 31              ; queremos el resultado en el rango 0-31.
       add a,l             ; añadir a byte bajo existente.
       ld l,a              ; completado el byte bajo.
       ld a,(hl)           ; obtener contenido de la celda.
       ret                 ; dirección del atributo ahora en hl.

; Mover el coche - cambio de dirección requerida.

mcarcd ld a,(hl)           ; dirección actual.
       inc a               ; gira en el sentido de las manecillas del reloj.
       and 3               ; sólo 4 direcciones.
       ld (hl),a           ; nueva dirección.

; Mover un coche enemigo.

mcar   push hl             ; preservar puntero al coche.
       call getabc         ; recuperar coordenadas y dirección.
       call movc           ; mover el coche.
       pop hl              ; refrescar puntero del coche
       jr nz,mcarcd        ; no se puede moverse mas, dar la vuelta.
       inc hl              ; apuntar a x.
       ld a,c              ; guardar posición x en c.
       ld (hl),a           ; posición x.
       inc hl              ; apuntar a y.
       ld (hl),b           ; nueva colocación.
       or b                ; combinar ambos.
       and 31              ; encontrar celdas de posición a caballo.
       cp 8                ; ¿estamos en un punto de giro válido?
       ret nz              ; no, no se puede cambiar de dirección.
       ld a,r              ; falso número aleatorio.
       cp 23               ; compruebe que está por debajo de este valor.
       ret nc              ; no lo está, no cambia.
       push hl             ; guardar puntero del coche.
       call random         ; obtener número aleatorio.
       pop hl              ; restaurar coche.
       dec hl              ; volver a la coordenada x.
       dec hl              ; volver de nuevo a la dirección.
       and 3               ; dirección en el rango de  0-3.
       ld (hl),a           ; nueva dirección.
       ret

; Obtener coordenadas de automóviles y su dirección.

getabc ld a,(hl)           ; obtener dirección.
       inc hl              ; apuntar a la posición x.
       ld c,(hl)           ; coordenada x.
       inc hl              ; apuntar a y.
       ld b,(hl)           ; posición y.
       ret


; Procesar coche en el punto hl.

procar push hl             ; guardar puntero.
       push hl             ; guardar puntero.
       call scar           ; Eliminar coche.
       pop hl              ; restablecer puntero a coche.
       call mcar           ; mover coche.
       pop hl              ; restablecer puntero a coche.


; Mostrar coche enemigo.

scar   call getabc         ; obtener coordenadas y dirección.
       jr dplay0

; Presentar sprite del jugador.

dplayr ld bc,(playx)       ; coordenadas del jugador.
       ld a,(playd)        ; dirección del jugador.
dplay0 rrca                ; multiplicar por 32.
       rrca
       rrca
       ld e,a              ; sprite * 32 en el byte bajo.
       ld d,0              ; sin byte alto.
       ld hl,cargfx        ; gráfico del coche.
       add hl,de           ; añadir desplazamiento al sprite.
       jr sprite           ; mostrar el sprite.


; Esta es la rutina de sprites y espera las coordenadas en la forma (c, b),
; donde c es la coordenada vertical desde la parte superior de la pantalla 
; (0-176), y b es la coordenada horizontal desde la izquierda de la pantalla
; (0 a 240). Los datos del sprite se almacenan como se espera en su forma no
; desplazada ya que esta rutina se encarga de todo el desplazamiento por sí 
; misma. Esto significa que direccionar el sprite no es especialmente rápido,
; pero los gráficos sólo ocupan 1/8 del espacio que requerirían en forma
; pre-desplazada.

; La entrada HL debe apuntar a los datos del sprite sin desplazamiento.

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 (dispx),bc       ; guardar ahora coordenadas en dispx.
       call scadd          ; calcular dirección de la pantalla.
       ld a,16             ; altura del sprite en pixeles.
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 (sprtmp),hl      ; guardar 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               ; hay, 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 l               ; 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,(dispx)        ; coordenada vertical temporal.
       inc a               ; siguiente línea de abajo.
       ld (dispx),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 l               ; 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,(sprtmp)      ; 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.

; Esta rutina devuelve la dirección de pantalla de (c, b) en de.

scadd  ld a,c              ; obtener la posición vertical.
       and 7               ; línea 0-7 en el cuadro del carácter.
       add a,64            ; 64 * 256 = 16384 (inicio de la pantalla)
       ld d,a              ; línea * 256.
       ld a,c              ; obtener verticales de nuevo.
       rrca                ; multiplicar por 32.
       rrca
       rrca
       and 24              ; byte alto del desplazamiento de segmento.
       add a,d             ; añadir a byte alto de pantalla existente.
       ld d,a              ; ese es el byte alto ordenado.
       ld a,c              ; 8 casillas de carácter por segmento
       rlca                ; 8 píxeles por celda, multiplicado por 4 = 32.
       rlca                ; celda x 32 da la posición dentro del segmento.
       and 224             ; asegurarse de que es un múltiplo de 32.
       ld e,a              ; cálculo de coordenada vertical hecho.
       ld a,b              ; coordenada y.
       rrca                ; Sólo es necesario dividir por 8.
       rrca
       rrca
       and 31              ; cuadrados 0 - 31 a través de la pantalla.
       add a,e             ; añadirse al total de la medida.
       ld e,a              ; hl = dirección de la pantalla.
       ret

; Generador de números seudo-aleatorio.
; Llevando un puntero a través de la ROM (a partir de una semillas), 
; devuelve el contenido del byte en esa ubicación.

random ld hl,(seed)        ; puntero a la ROM.
       res 5,h             ; mantenerse dentro de los primeros 8K de ROM.
       ld a,(hl)           ; obtener el número "aleatorio" de esa ubicación.
       xor l               ; más aleatoriedad.
       inc hl              ; incrementar el puntero.
       ld (seed),hl        ; nueva posición.
       ret


; Datos gráficos del Sprite.
; Primero apunta subiendo.

cargfx defb 49,140,123,222,123,222,127,254,55,236,15,240,31,248,30,120
       defb 29,184,108,54,246,111,255,255,247,239,246,111,103,230,3,192

; Segunda imagen apunta a la derecha.

       defb 60,0,126,14,126,31,61,223,11,238,127,248,252,254,217,127
       defb 217,127,252,254,127,248,11,238,61,223,126,31,126,14,60,0

; La tercera apunta hacia abajo.

       defb 3,192,103,230,246,111,247,239,255,255,246,111,108,54,29,184
       defb 30,120,31,248,15,240,55,236,127,254,123,222,123,222,49,140

; La última el coche apunta a la izquierda.

       defb 0,60,112,126,248,126,251,188,119,208,31,254,127,63,254,155
       defb 254,155,127,63,31,254,119,208,251,188,248,126,112,126,0,60

; Variables usadas por el juego.

       org 32768

playi  equ $               ; dirección deseada cuando el giro es posible.
playd  equ playi+1         ; dirección actual del jugador.
nplayd equ playd+1         ; siguiente dirección jugador.
playx  equ nplayd+1        ; coordenada x del jugador
playy  equ playx+1         ; coordenada y del jugador.

encar1 equ playy+1         ; coche enemigo 1.
encar2 equ encar1+3        ; coche enemigo 2.

dispx  equ encar2+3        ; coordenadas de uso general.
dispy  equ dispx+1
seed   equ dispy+1         ; semilla de números aleatorios.
sprtmp equ seed+2          ; dirección temporal del sprite.
termin equ sprtmp+2        ; fin de variables.

rmdat  equ 49152



Si has ensamblado este juego y lo has probado, te darás cuenta de que rápidamente se vuelve aburrido. Es muy fácil mantenerse fuera del alcance de los enemigos cubriendo un lado de la pista, y luego esperar hasta que se muevan y cubrir el otro lado. Este algoritmo no tiene aspecto de cazador-asesino por lo que el jugador nunca es perseguido. Es más, esta rutina es de simples vehículos que no cambian de dirección sin advertencia. En la mayoría de juegos esto sólo es aceptable si un sprite llega a un callejón sin salida y no se puede mover en cualquier otra dirección.


Tal vez debemos en su lugar escribir rutinas en que los aliens interactúan con el jugador, y lo persiguen. Así, el algoritmo más básico sería seguir una línea comprobando las coordenadas base x/y, y mover el sprite del alien hacia el jugador. La rutina siguiente muestra cómo podría lograrse este objetivo, la rutina buscadora de blancos almov es la que mueve el sprite alrededor del jugador. Intente guiar al bloque número 1 por la pantalla con las teclas A, S, D y F, y el bloque número 2 te seguirá alrededor de la pantalla. Sin embargo, al hacer esto pronto descubrimos el defecto básico con este tipo de persecución, es muy fácil atrapar el sprite enemigo en una esquina porque la rutina no es lo suficientemente inteligente para moverse hacia atrás con el fin de conseguir rodear los obstáculos.

; Aleatoriamente cubrir la pantalla con bloques amarillos.

       ld de,1000          ; dirección en la ROM.
       ld b,64             ; número de celdas de color.
yello0 push bc             ; almacenas el registro.
       ld a,(de)           ; obtener la primera coordenada al azar.
       and 127             ; la mitad de la altura de la pantalla.
       add a,32            ; al menos 32 píxeles hacia abajo.
       ld c,a              ; coordenada x.
       inc de              ; siguiente byte de ROM.
       ld a,(de)           ; recuperar valor.
       inc de              ; siguiente byte de ROM.
       ld b,a              ; coordenada y.
       call ataddp         ; encuentra la dirección del atributo.
       ld (hl),48          ; establecer atributos.
       pop bc              ; restaurar el contador del bucle.
       djnz yello0         ; repetir varias veces.

       ld ix,aldat         ; datos del alien.
       call dal            ; mostrar alien.
       call dpl            ; presentar jugador.

mloop  halt                ; esperar al haz de electrones.
       call dal            ; eliminar alien.
       call almov          ; movimiento del alien.
       call dal            ; mostrar alien.
       halt                ; esperar al haz de electrones.
       call dpl            ; eliminar jugador.
       call plcon          ; controles del jugador.
       call dpl            ; mostrar al jugador.
       jp mloop            ; volver al inicio del bucle principal.

aldat  defb 0,0,0          ; datos del alien.

; Visualizar/eliminar alien.

dal    ld c,(ix)           ; posición vertical.
       ld b,(ix+1)         ; posición horizontal.
       ld (xcoord),bc      ; establecer las coordenadas del sprite.
       ld hl,algfx         ; gráfico del alien.
       jp sprite           ; XOR del sprite con la pantalla.

; Visualizar/eliminar sprite del jugador.

dpl    ld bc,(playx)       ; coordenadas.
       ld (xcoord),bc      ; establecer las coordenadas de pantalla.
       ld hl,plgfx         ; gráfico del jugador.
       jp sprite           ; xor del sprite dentro o fuera de la pantalla.

; control del jugador.

plcon  ld bc,65022         ; puerto para la fila del teclado.
       in a,(c)            ; leer teclado.
       ld b,a              ; almacenar el resultado en el registro b.
       rr b                ; verificación la tecla más externa.
       call nc,mpl         ; jugador a la izquierda.
       rr b                ; comprobar siguiente tecla.
       call nc,mpr         ; jugador de la derecha.
       rr b                ; comprobar siguiente tecla.
       call nc,mpd         ; jugador abajo.
       rr b                ; comprobar siguiente tecla.
       call nc,mpu         ; jugador arriba.
       ret
mpl    ld hl,playy         ; coordenada.
       ld a,(hl)           ; comprobar el valor.
       and a               ; ¿en el borde de la pantalla?
       ret z               ; sí, no se puede mover en esa dirección.
       sub 2               ; moverse 2 pixeles.
       ld (hl),a           ; nuevo ajuste.
       ret
mpr    ld hl,playy         ; coordenada.
       ld a,(hl)           ; comprobar el valor.
       cp 240              ; ¿en el borde de la pantalla?
       ret z               ; sí, no se puede mover en esa dirección.
       add a,2             ; moverse 2 pixeles.
       ld (hl),a           ; nuevo ajuste.
       ret
mpu    ld hl,playx         ; coordenada.
       ld a,(hl)           ; comprobar el valor.
       and a               ; ¿en el borde de la pantalla?
       ret z               ; sí, no se puede mover en esa dirección.
       sub 2               ; moverse 2 pixeles.
       ld (hl),a           ; nuevo ajuste.
       ret
mpd    ld hl,playx         ; coordenada.
       ld a,(hl)           ; comprobar el valor.
       cp 176              ; ¿en el borde de la pantalla?
       ret z               ; sí, no se puede mover en esa dirección.
       add a,2             ; moverse 2 pixeles.
       ld (hl),a           ; nuevo ajuste.
       ret


; Rutina de movimiento del alien.

almov  ld a,(playx)        ; coordenada x del jugador.
       ld c,(ix)           ; alien x.
       ld b,(ix+1)         ; alien y.
       cp c                ; revisar x del alien
       jr z,alv0           ; son iguales, seguir en horizontal.
       jr c,alu            ; el alien está más abajo, moverlo hacia arriba.
ald    inc c               ; el alien está más arriba, moverlo hacia abajo.
       jr alv0             ; Ahora comprobar la posición de las paredes.
alu    dec c               ; mover hacia abajo.
alv0   call alchk          ; comprobar atributos.
       cp 56               ; ¿están bien?
       jr z,alv1           ; Sí, establecer la coordenada x.
       ld c,(ix)           ; restaurar la anterior coordenada x.
       jr alh              ; ahora ir en horizontal.
alv1   ld (ix),c           ; nueva coordenada x.
alh    ld a,(playy)        ; horizontal del jugador.
       cp b                ; comprobar horizontal del alien.
       jr z,alok           ; son iguales, verificar la colisión.
       jr c,all            ; alien a la derecha, mover a la izquierda.
alr    inc b               ; alien a la izquierda, mover a la derecha.
       jr alok             ; comprobar las paredes.
all    dec b               ; mover a la derecha.
alok   call alchk          ; comprobar atributos.
       cp 56               ; ¿están bien?
       ret nz              ; no, establecer la nueva coordenada y.
       ld (ix+1),b         ; establecer nueva y.
       ret


; Comprueba atributos en la posición de alien (c,b).

alchk  call ataddp         ; conseguir dirección del atributo.
       ld a,3              ; celda de arriba.
alchk0 ex af,af'           ; guardar el contador de bucles.
       ld a,(hl)           ; verificar color de la celda.
       cp 56               ; ¿es negro sobre blanco?
       ret nz              ; no, no se puede mover aquí.
       inc hl              ; celda de la derecha.
       ld a,(hl)           ; verificar color de la celda.
       cp 56               ; ¿es negro sobre blanco?
       ret nz              ; no, no se puede mover aquí.
       inc hl              ; celda de la izquierda.
       ld a,(hl)           ; verificar color de la celda.
       cp 56               ; ¿es negro sobre blanco?
       ret nz              ; no, no se puede mover aquí.
       ld de,30            ; distancia hasta la siguiente celda hacia abajo.
       add hl,de           ; mira aquí.
       ex af,af'           ; contador de alturas.
       dec a               ; una menos a la que ir.
       jr nz,alchk0        ; repetir para todas las filas.
       ld (ix),c           ; establecer nueva x.
       ld (ix+1),b         ; establecer nueva y.
       ret

; Calcular la dirección del atributo para el píxel en (c,b).

ataddp ld a,c              ; Mira primero en vertical.
       rlca                ; dividir por 64.
       rlca                ; más rápido que 6 instrucciones rrca.
       ld l,a              ; almacenar en el registro l por ahora.
       and 3               ; enmascarar para encontrar segmento.
       add a,88            ; atributos comienzan a partir de 88*256=22528.
       ld h,a              ; ese es nuestro byte alto ordenado.
       ld a,l              ; vertical/64, es lo mismo que vertical*4.
       and 224             ; deseamos un múltiplo de  32.
       ld l,a              ; elemento vertical calculado.
       ld a,b              ; obtener la posición horizontal.
       rra                 ; dividir por 8.
       rra
       rra
       and 31              ; queremos dar lugar a rango 0-31.
       add a,l             ; añadir al byte bajo existente.
       ld l,a              ; tenemos el byte bajo hecho.
       ret                 ; dirección de atributo ahora en hl.


playx  defb 80             ; coordenadas del jugador.
playy  defb 120
xcoord defb 0              ; coordenadas generales multi propósito.
ycoord defb 0

; Rutina de sprites desplazados.

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,(xcoord)       ; dibuja el sprite (hl).
       ld (tmp1),a         ; guardar vertical.
       call scadd          ; calcular dirección de la pantalla.
       ld a,16             ; altura del sprite en pixeles.
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               ; hay, 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.

scadd  ld a,(xcoord)       ; recuperar la coordenada vertical.
       ld e,a              ; guardarla en e.
       
; Encuentra la línea dentro de la celda.

       and 7               ; línea 0-7 en el cuadro del carácter.
       add a,64            ; 64 * 256 = 16384 = inicio de la pantalla.
       ld d,a              ; línea * 256.

; Encuentra en que tercio de pantalla estamos.

       ld a,e              ; restaurar la vertical.
       and 192             ; segmento 0, 1 o 2 multiplicado por 64.
       rrca                ; dividirlo por 8.
       rrca
       rrca                ; segmento 0-2 multiplicado por 8.
       add a,d             ; añadir a d da la dirección inicial del segmento.
       ld d,a

; Encuentra celda de carácter dentro del segmento.

       ld a,e              ; 8 casillas de caracteres por segmento.
       rlca                ; dividir por 8 y multiplicar por 32,
       rlca                ; cálculo neto: multiplicar por 4.
       and 224             ; enmascarar los bits que no queremos.
       ld e,a              ; cálculo de coordenada vertical hecho.

; Añadir el elemento horizontal.

       ld a,(ycoord)       ; coordenada y.
       rrca                ; sólo es necesario dividir por 8.
       rrca
       rrca
       and 31              ; cuadrados 0 - 31 por la pantalla.
       add a,e             ; añadirse al total de la medida.
       ld e,a              ; de = dirección de la pantalla.
       ret

tmp0   defw 0
tmp1   defb 0


plgfx  defb 127,254,255,255,254,127,252,127,248,127,248,127,254,127,254,127
       defb 254,127,254,127,254,127,254,127,248,31,248,31,255,255,127,254
algfx  defb 127,254,254,63,248,15,240,135,227,231,231,231,255,199,255,15
       defb 252,31,248,127,241,255,227,255,224,7,224,7,255,255,127,254

Las mejores rutinas de movimiento de aliens usan una combinación de elementos aleatorios y algoritmos de cazador-asesino. Para superar el problema en el listado superior necesitamos un nuevo indicador adicional para indicar el estado actual del enemigo o en su caso su dirección. Podemos mover el sprite a lo largo de una dirección determinada hasta que sea posible y cambiar el curso vertical u horizontal, con lo cual se selecciona una nueva dirección dependiendo de la posición del jugador. Sin embargo, en caso de que no sea posible mover en la dirección deseada iremos en la dirección opuesta en su lugar. Utilizando este método un sprite puede encontrar su propio camino en la mayoría de los laberintos sin colgarse con demasiada frecuencia. De hecho, para garantizar absolutamente que el sprite no permanecerá atrapados podemos añadir un elemento al azar de modo que cada cierto tiempo la nueva dirección es elegido de forma aleatoria en lugar de la diferencia en las coordenadas x e y

Subiendo la dificultad por niveles

La ponderación aplicada a la decisión sobre los cambios de dirección determinarán los niveles de inteligencia de los sprites. Si la nueva dirección tiene una probabilidad del 90% de ser elegido de manera aleatoria y una probabilidad del 10% sobre la base de coordenadas, el alien paseará sin rumbo por un tiempo y sólo se acerca cuando el jugador se mueve lento. Así, una decisión aleatoria a veces puede ser la más adecuada cuando persigue al jugador. Un alien en una pantalla más difícil podría tener una probabilidad del 60% de elegir una nueva dirección al azar, y un 40% de posibilidades de elegir la dirección en base a la posición relativa de jugador. Este alien hará un seguimiento un poco más de cercano al jugador. Ajustando estos niveles porcentuales, es posible determinar los niveles de dificultad a lo largo de un partido y asegurar una transición suave desde el las más simples pantallas del inicio de la partida a niveles finales diabólicamente difíciles.