martes, 31 de mayo de 2016

Vida antes del Kenbak-1: Heathkit EC-1, o que es un ordenador analógico

Entradas relacionadas: Simon (1950), Geniac (1955), Heathkit EC-1 (1960), Minivac 601 (1961), Digi-Comp I (1963), DEC PDP-8 (1965), Paperclip Computer (1967), Honeywell Kitchen Computer (1969), Imlac PDS-1 (1970).

Índices: El camino al O.P.       Historia de la Informática


Estamos acostumbrados a los ordenadores digitales, aquellos en los que las magnitudes con las que trata se representan como ceros y unos, lo que físicamente se representa por que exista o no electricidad, y su componente básico es la puerta lógica, construida con relés, lámparas o transistores, que actualmente se encapsulan en un chip. Los ordenadores trabajan ejecutando programas en su memoria mediante un procesador, que usa micro-programación actualmente, aunque los primero usaban lógica cableada, pero eran mas calculadoras programables que ordenadores.

El ordenador analógico es muy diferente, trabaja operando con voltajes variables en un rango continuo, y puede operar sumando, restando, multiplicando o dividiendo entre si esos voltajes, usando para ello un componente llamado amplificador operacional, construido con lámparas o transistores, que actualmente se encapsula en un chip. No ejecutan un programa como tal, solo se unen los amplificadores operaciones para operar con los voltajes, usando lógica cableada únicamente. Todo ello de manera casi inmediata.

Su uso principal era por los ingenieros, para comprobar elementos como los condensadores y para probar los diseños y testear los fallos, pero su capacidad de ejecutar cálculos matemáticos rápidos tuvo un uso práctico muy temprano, anterior al de los ordenadores digitales. Para disparar un torpedo un submarino debe hacer un cálculo de distancia, velocidad y rumbo del barco al que disparar, y en función del rumbo y velocidad de submarino y de la velocidad del torpedo, usando trigonometría y física elemental, se calcula el ángulo en que se debe disparar. Se disparan varios torpedos con pequeñas diferencias en los ángulos para compensar los errores de la estimación y el posible desvío de las corrientes. Este cálculo en la primera guerra mundial se hacía mediante reglas de cálculo, y en la segunda con un ordenador analógico al que los alemanes denominaban TZR

TZR del U-564 (Fuente: tvre.org)
La Heath Company es una empresa creada en 1926, y todavía en activo, especializada en material para la ingeniería electrónica. Produjo material en forma de kits entre 1947 y 1992, principalmente aparatos de radio, en una gama a la que denominó HeathKit, muy difundida en su momento por su alto nivel de calidad. Aunque la compañía continua en activo ha pasado por muchos altibajos, quedando en el 2000 al borde del cierre, y ha cambiado de propietarios varias veces desde entonces. Actualmente su mercado principal es el educativo, y ha vuelto a entrar en el mercado de los kits, dispone de algunos en venta, pero también vende los manuales de sus antiguos kits.

El EC-1 (Fuente: technikum29.de)


En 1956 lanza el H1, un monstruo que dispone de 70 lámparas, 45 ubicadas en su exterior por temas de calor, un aparato potente y de muy buena calidad, pero muy caro, por lo que en 1960 lanza el EC-1 Educational Analog Computer, a un precio muy ajustado los aficionados podían disponer de una herramienta de calidad, que podían adquirir tanto en kit como montado. En su interior había nueve amplificadores operacionales a lámparas y dos fuentes de alimentación, una proporcionaba +300 y -300 voltios para el manejo de las lámparas, y una variable de entre +60 y -60 voltios a 25 miliamperios. La fuente variable proporcionaba los voltajes que se podían interconectar entre sí y ajustar mediante contactos, potenciómetros y conmutadores en su panel frontal. El voltímetro podía conectarse a cualquier punto para verificar el voltaje en dicho punto, y las numerosas tomas permitían su uso junto a un osciloscopio para analizar las señales. Los operacionales trabajaban pudiendo realizando operaciones en un rango de entre 15 por segundo hasta una cada 10 segundos.

Interior del aparato (fuente: old-computers.com)

Su interior es muy espartano con el chasis metálico usual en la época, sobre el aparecen los 9 tubos de los operacionales, dos transformadores y varios tubos forman las dos fuentes del aparato. Por debajo del chasis se sitúan condensadores, resistencias y un montón de cables. Al fondo se ve el frontal con sus conectores. Todo esto alcanzaba un peso de 21 kilos.

Los amplificadores trabajaban en bucle abierto, con una ganancia máxima de 1000, pudiendo manejar voltajes en el rango de -60 a +60 voltios, aunque solo soportaban señales de unos pocos miliamperios.

Como vemos, no es un ordenador como tal, es un computador, pero ayudó mucho a los aficionados en el diseño y montaje de los ordenadores posteriores.

lunes, 30 de mayo de 2016

Vida antes del Kenbak-1: El GENIAC, o como construir un ordenador sin electrónica

Entradas relacionadas: Simon (1950), Geniac (1955), Heathkit EC-1 (1960), Minivac 601 (1961), Digi-Comp I (1963), DEC PDP-8 (1965), Paperclip Computer (1967), Honeywell Kitchen Computer (1969), Imlac PDS-1 (1970).

Índices: El camino al O.P.       Historia de la Informática


En la entrada anterior hablé del Simón, el ordenador electro-mecánico a relés diseñado por Edmund Berkeley, uno de los padres de la ciencia de los ordenadores.

Junto a Oliver Garfield diseñaron un curioso "juguete" (ya que lo es, aunque también es algo mas que eso). El Geniac fue comercializado por Berkeley Enterprises entre 1955 y 1958, luego se separaron ambos creadores, Garfield lo siguió comercializando hasta mediados de los 1960, mientras que Berkeley hizo algunos cambios y lo vendió con otros nombres, por eso lo podemos encontrar como:
  • Geniac (Genius Almost-Automatic Compute, Genio Casi-Ordenador automático, aunque también se le menciona como acrónimo de Genius y de Eniac, nombre del primer ordenador americano)
  • Tyniac (Tiny Almost-Automatic Computer, Pequeño Casi-Ordenador automático)
  • Weeniac (Weeny Almost-Automatic Computer, Pequeño Casi-Ordenador automático), del que solo se fabricaron 60 unidades
  • Brainiac (Brain-Imitating Almost-Automatic Computer, Imitador-de-Cerebro Casi-Ordenador automático), que no hay que confundir con el Súper-Villano de los comics del mismo nombre.
Manual del Geniac (Fuente: blinkenlights.com)

Consistía en un tablero base y un conjunto de seis discos giratorios construidos con Masonita, un tipo de madera aglomerada con apariencia de cartón, en la parte posterior de los discos disponía de una serie de ubicaciones donde se podían insertar unos contactos metálicos. En el tablero base del equipo se insertaban los discos en ubicaciones que les permitían girar libremente, y sus insertos metálicos podían contactar con unos remaches de latón en la base, que se podían cablearse entre sí para formar los circuitos, y a través de la corriente suministrada por unas pilas podían iluminar una serie de bombillas de linterna.

Un Geniac en su caja sin montar, con todos sus elementos y sus manuales (Fuente: vintagecomputer.net)

Con esta configuración tan básica Geniac era capaz de simular circuitos de lógica combinacional, en el que sus salidas dependían solo de las entradas conectadas. Sin ningún elemento electrónico ni motores, sin ningún tipo de memoria, sin elementos de lógica binaria, aun así era un buen entrenador permitiendo crear de manera sencilla máquinas de estados, aunque los cambio de uno a otro estado se debía realizar de forma manual girando los discos en función de las instrucciones del circuito y del estado de las bombillas, lo que en ciertas máquinas de estados un poco mas complejas era bastante lioso.

El aparato se acompañaba de un libro de instrucciones que explicaba el aparato y su manejo, y de otro libro con varios circuitos, todos con los diagramas de cableado, las posiciones de los puentes y las instrucciones de manejo de la "máquina" construida, desde las mas básicas hasta otras con ecuaciones booleanas mas complejas.

Un circuito de ejemplo era una máquina detectora de una cualidad de algo que pudiera tener dos posibles, como masculino y femenino, animal o mineral, etc. Se definían cinco preguntas con dos posibles respuestas cada una, se construía y cableaba el circuito, y se ubicaban los discos en la respuesta a cada pregunta, el circuito contaba cuantas respuestas de cada tipo se habían dado y seleccionaba la que mas tuviera para dar el resultado, iluminando una u otra bombilla.

Anuncio en la revista "Ciencia popular" de Oct/1957 (fuente: blinkenlights.com)

Se publicitó mucho en revistas de la época de todo tipo, por ejemplo en "Galaxy Ciencia ficción", y es un preciado objeto para los coleccionistas de ordenadores actuales. Podéis encontrar aquí información y ejemplos de circuitos, aunque en inglés.

viernes, 27 de mayo de 2016

Vida antes del Kenbak-1: El Simon, relés y mas relés

Entradas relacionadas: Simon (1950), Geniac (1955), Heathkit EC-1 (1960), Minivac 601 (1961), Digi-Comp I (1963), DEC PDP-8 (1965), Paperclip Computer (1967), Honeywell Kitchen Computer (1969), Imlac PDS-1 (1970).

Índices: El camino al O.P.       Historia de la Informática


Según una votación que se realizó por el Computer History Museum, el título de primer ordenador personal se le dio al Kenbak-1 que se comercializó en 1971. Esta máquina era un ordenador  operativo completo, con la estructura básica de un ordenador, 255 bytes de memoria, varios registros y un conjunto variado de operadores. No era una idea plasmada en un libro o una revista, no era un kit para expertos en electrónica, era un equipo montado, funcional, perfectamente programable, con un juego de instrucciones básico pero completo, y completamente operativo.

Antes de que surgiera la primera forma de vida que podemos reconocer como tal, tuvieron que surgir formas rudimentarias incompletas, que evolucionaron, se unieron y formaron la primera célula. De igual manera, antes del Kenbak-1 existieron una serie de máquinas que podemos decir son los antecedentes de un ordenador personal, aunque no llegaban a serlo, por lo que en estas entradas quiero hablar de ellas y rendirles su merecido homenaje como pioneros de un mundo desconocido en ese momento, son el electro-mecánico Simon de 1950, del mismo autor y aunque no era un ordenador como tal el Geniac de 1955, el ordenador analógico a lámparas Heathkit EC-1 de 1959, el entrenador de electrónica digital Minivac 601 de 1961, el  descrito en un libro Paperclip Computer de 1967 (que fue construido posteriormente por una empresa en 1969), y aunque no eran ordenadores personales sino minis quiero mencionar también tres máquinas que influyeron en el momento, el DEC PDP-8 de 1965, el no fabricado Honeywell Kitchen Computer de 1966, y el Imlac PDS-1 de 1970.

En el libro guinnes de los records (igual de fiable que la Wikipedia en general) aparece esta entrada:
"El primer ordenador personal (PC), conocido como Simón, fue lanzado en 1950. Fue vendido al por menor por 600 Dólares, y tenía una memoria de seis palabras de 2 bits, disponiendo de 12 bits de memoria total. Fue desarrollado por Edmund Berkeley (EE.UU.). El Simon también es conocido como el cerebro mecánico Simon y el ordenador personal electromecánico Simon. Berkeley esbozó la idea del equipo Simon en su libro de 1959 "Giant Brains, or Machines That Think" (Grandes cerebros, o máquinas que piensan), pasando a desarrollar sus teorías en una serie de 13 artículos publicados entre 1950 y 1951 en la revista Radio-Electronics."
El Simón en la portada de Radio Electronics de octubre de 1950 (fuente: blinkenlights.com)

Al simón se le designa como computador electro-mecánico, ya que estaba construido con relés como componentes digitales, realmente le faltaban varios elementos para ser un ordenador personal como tal, aunque eso no le quita el mérito real de abrir el camino a que los aficionados pudieran experimentar con esa nueva rama de la ciencia que estaba surgiendo. La descripción publicada en 13 artículos de la revista Radio Electonics era mucho mas amplia que la máquina en sí, solo los 6 primero textos eran sobre la máquina de relés, el resto eran ampliaciones con ideas sobre máquinas electrónicas con tubos de vacío, y en general se correspondía con el contenido del libro. Luego la empresa que fundó Berkeley diseñó y construyó una máquina que se vendía como un Kit de montaje por unos 600$.
El simón, se ven todos sus relés (fuente: cedmagic.com)

Basada físicamente en 80 relés de dos y cuatro polos, a la unidad había que añadir una fuente de alimentación externa y una lectora/perforadora de cinta de papel para que funcionara. Se decía que era personal pues lo podías llevar en una mano (difícil por el peso, y mas cuando en la otra debías cargar con la fuente de alimentación y la perforadora).

Los programas estaban escritos en la cinta de papel, la unidad de cinta leía el programa y la máquina luego ejecutaba las instrucciones secuencialmente. La unidad de relés formaba por 6 registros binarios de dos bits cada uno (valores posibles 0, 1, 2 y 3 únicamente), mas la Unidad Aritmético/Lógica que era capaz de realizar cuatro operaciones únicamente: suma, negación, comparar con mayor que, y seleccionar registro, pero no disponía de instrucciones de salto por ejemplo. Para la salida disponía de 5 bombillas indicadoras, o con modificaciones se podía grabar en la cinta. La entrada de datos se podía realizar desde 5 teclas en el frontal, o desde la propia cinta. con ampliaciones la cinta podía usarse también como memoria intermedia.

Como veis, su capacidad era tan baja con solo 2 bits y 4 instrucciones, que es mas un entrenador que otra cosa. Se vendía como un Kit que costaba 600$, del que se vendieron unos 400, y se conocen aficionados que la ampliaron y publicaron esquemas de ello, lo que se conoce como los modelos Simon II y Simon III.


El libro se puede conseguir en PDF gratuito ya que ha pasado a dominio público, por ejemplo en Archive, y también hay ejemplares en venta en (al menos a fecha de 27/05/16) en Amazon. En el se mencionan todos (literalmente) los ordenadores operativos en ese momento, ya que se podían contar con los dedos en aquel momento.

Mucha mas información, incluyendo algunos esquemas, programas, descripciones y simuladores del Simón los podeis encontrar en The University of British Columbia, de Canadá

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.