Curso de Z88DK: Sprites (II)

En el artículo anterior aprendimos cómo definir sprites y dibujarlos en pantalla utilizando la librería z88dk sin recurrir a ninguna otra herramienta externa (a menos que quisiéramos dibujar los sprites utilizando algún programa como los mostrados en el número anterior). En esta entrega vamos a ir un poco más allá y vamos a permitir que el usuario pueda mover algún sprite por la pantalla, y que este sprite pueda interaccionar con otros elementos, colisionando con ellos.

Para aprender los conceptos que necesitamos vamos a programar lo que podría ser el código inicial de un juego de carreras de coches, tipo Super Sprint, en los que desde una vista cenital podemos ver el circuito y los distintos participantes. Crearemos un coche utilizando diversos sprites, según hacia donde se esté moviendo el mismo, y crearemos una pista por donde el coche podrá moverse. Evidentemente, esta pista marcará el límite de movimiento de nuestro bólido, por lo que deberemos implementar también algún mecanismo para colisiones, de tal manera que al vehículo le pase algo al contactar con los límites del circuito. Sin más, empecemos.

Hasta ahora hemos visto como crear sprites de un tamaño límite de 8×8. Sin embargo, por mucho que lo intentemos, va a ser un poco difícil crear un sprite que represente algo aproximado a un coche con una rejilla tan pequeña. Si nos fijamos en la siguiente imagen, observaremos que tanto para representar al coche en todas las posibles orientaciones, así como los neumáticos que van a formar parte de los límites de la pista, necesitamos utilizar sprites de 10×10. ¿Cómo creamos sprites mayores de 8×8 usando z88dk?

Veamos los sprites que vamos a utilizar en nuestro juego. Representan todas las posibles orientaciones del coche, excepto el último de todos, que representa un neumático. Todos ellos tienen un tamaño de 10×10 :

Sprites del juego

Según vimos anteriormente, un sprite se definía en z88dk como un array de char del tipo:

char sprite0[] = { 8, 8, 0x18 , 0x24 , 0x42 , 0x81 , 0xA5 , 0x81 , 0x7E , 0x00 };

donde los dos primeros valores indicaban la altura y la anchura, respectivamente, y los siguientes valores representaban a cada una de las filas del sprite. Si cada columna tenía asignado un valor hexadecimal (estos valores eran, para una anchura de 8, y de derecha a izquierda, los siguientes: 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80), el valor para cada fila era la suma de los valores hexadecimales para los que el pixel correspondiente valía 1.

Para sprites de más de anchura 8, y hata una anchura de 16, en ese array de caracteres cada fila se va a representar por un par de números hexadecimales. El primer valor de esa pareja se va a corresponder con los 8 primeros píxeles empezando por la izquierda de la columna, y el segundo con el resto.

En el caso de los sprites que vemos en la figura anterior, donde cada columna tiene una anchura de diez píxeles, cada fila se representaría también por una pareja de valores hexadecimales. Con la primera pareja codificamos los ocho píxeles de la izquierda, de la misma forma en que lo hacemos con sprites de anchura hasta 8, y con la segunda pareja codificamos los otros dos pixeles que quedan a la derecha (siendo el valor 0x80 el correspondiente al primero y el valor 0x40 el correspondiente al segundo. Si hubiese más de dos, seguiríamos asignando los valores 0x20, 0x10, 0x08, 0x04 y etc. a los siguientes). En la siguiente imagen vemos un ejemplo del cálculo de los valores hexadecimales correspondientes a cada una de las filas del sprite 0:

Filas del Sprite 0

Podríamos ser malvados y dejar como ejercicio al lector que calcule cómo se codificarían el resto de sprites, pero vamos a ahorrarle el mal trago. A continuación incluímos el código que codifica todos los sprites anteriores, que deberemos introducir dentro de un archivo de cabecera llamado coches.h:

char sprite0[] = { 10, 10, 0x00 , 0x00 , 0x73 , 0x80 , 0xFF , 0x40 , 0xB9 ,
     0xC0 , 0xB9 , 0xC0 , 0xB9 , 0xC0 , 0xB9 , 0xC0 , 0xFF , 0x40 , 0x73 ,
     0x80 , 0x00 , 0x00  };
char sprite1[] = { 10, 10, 0x3F , 0x00 , 0x5E , 0x80 , 0x7F , 0x80 , 0x61 ,
     0x80 , 0x21 , 0x00 , 0x3F , 0x00 , 0x7F , 0x80 , 0x7F , 0x80 , 0x61 ,
     0x80 , 0x3F , 0x00  };
char sprite2[] = { 10, 10, 0x00 , 0x00 , 0x73 , 0x80 , 0xBF , 0xC0 , 0xE7 ,
     0x40 , 0xE7 , 0x40 , 0xE7 , 0x40 , 0xE7 , 0x40 , 0xBF , 0xC0 , 0x73 ,
     0x80 , 0x00 , 0x00  };
char sprite3[] = { 10, 10, 0x3F , 0x00 , 0x61 , 0x80 , 0x7F , 0x80 , 0x7F ,
     0x80 , 0x3F , 0x00 , 0x21 , 0x00 , 0x61 , 0x80 , 0x7F , 0x80 , 0x5E ,
     0x80 , 0x3F , 0x00  };
char sprite4[] = { 10, 10, 0x0F , 0x00 , 0x1D , 0x80 , 0x13 , 0xC0 , 0x79 ,
     0x40 , 0xFC , 0xC0 , 0xBE , 0xC0 , 0xDF , 0x80 , 0x6E , 0x00 , 0x36 ,
     0x00 , 0x1C , 0x00  };
char sprite5[] = { 10, 10, 0x1C , 0x00 , 0x36 , 0x00 , 0x6E , 0x00 , 0xDF ,
     0x80 , 0xBE , 0xC0 , 0xFC , 0xC0 , 0x79 , 0x40 , 0x13 , 0xC0 , 0x1D ,
     0x80 , 0x0F , 0x00  };
char sprite6[] = { 10, 10, 0x3C , 0x00 , 0x6E , 0x00 , 0xF2 , 0x00 , 0xA7 ,
     0x80 , 0xCF , 0xC0 , 0xDF , 0x40 , 0x7E , 0xC0 , 0x1D , 0x80 , 0x1B ,
     0x00 , 0x0E , 0x00  };
char sprite7[] = { 10, 10, 0x0E , 0x00 , 0x1B , 0x00 , 0x1D , 0x80 , 0x7E ,
     0xC0 , 0xDF , 0x40 , 0xCF , 0xC0 , 0xA7 , 0x80 , 0xF2 , 0x00 , 0x6E ,
     0x00 , 0x3C , 0x00  };
char sprite8[] = { 10, 10, 0x1E , 0x00 , 0x7F , 0x80 , 0x7F , 0x80 , 0xF3 ,
     0xC0 , 0xE1 , 0xC0 , 0xE1 , 0xC0 , 0xF3 , 0xC0 , 0x7F , 0x80 , 0x7F ,
     0x80 , 0x1E , 0x00  };

Y a continuación indicamos el código de un pequeño programa, que llamaremos coches.c, que lo único que hará será mostrar por pantalla todos los sprites para comprobar que quedan bonitos. Para ello hacemos uso de la función putsprite, cuya sintaxis se explicó en el artículo anterior:

#include "games.h"
#include "coches.h"
 
void main(void)
{
        putsprite(spr_or,1,41,sprite0);
        putsprite(spr_or,21,41,sprite1);
        putsprite(spr_or,41,41,sprite2);
        putsprite(spr_or,61,41,sprite3);
        putsprite(spr_or,1,61,sprite4);
        putsprite(spr_or,21,61,sprite5);
        putsprite(spr_or,41,61,sprite6);
        putsprite(spr_or,61,61,sprite7);
        putsprite(spr_or,1,81,sprite8);
}

El archivo de cabecera games.h debe ser incluido si queremos utilizar putsprite. En la siguiente imagen vemos la salida de este programa.

Los sprites mostrándose en la pantalla de nuestro Spectrum

Hacer que un sprite se mueva por pantalla es tan sencillo como borrarlo de donde se encontraba anteriormente y volver a dibujarlo en una nueva posición. Lo interesante será hacer que nuestro coche cambie de aspecto según la orientación en la que se esté moviendo.

Para conseguir esto último, es decir, que se nos muestre un sprite distinto del coche según se esté moviendo hacia arriba, hacia abajo, etc., debemos añadir un par de cambios al archivo coches.h. Lo primero que vamos a hacer es almacenar todos los sprites en un único array de arrays de chars (es decir, en un único array de sprites). Esto lo hacemos colocando después de la definición de los diferentes sprites en coches.h una línea como la siguiente:

char *sprites[9] = { sprite0 , sprite1 , sprite2 , sprite3 , sprite4 , sprite5 ,
    sprite6 , sprite7 , sprite8 };

De tal forma que podremos acceder al sprite i utilizando, por ejemplo, sprites[i] en lugar de spritei, lo cual nos va a ser de mucha utilidad. Justo después de esta línea, introducimos las dos líneas siguientes en coches.h:

int izquierda[] = {4,6,7,5,1,0,2,3};
int derecha[] = {5,4,6,7,0,3,1,2};

Estos dos arrays los utilizaremos para saber, cada vez que giramos, cuál es el sprite que debemos dibujar en la pantalla. Si los sprites están numerados según la primera figura de este artículo, y ahora mismo se nos muestra en la pantalla el sprite 0, si giramos a la izquierda el que se debería mostrar es el 4. Si volvemos a girar a la izquierda, el que debería dibujarse entonces en la pantalla es el 1, etc. Si por el contrario, estamos mostrando el sprite 0 en la pantalla y giramos a la derecha, se nos debería mostrar el sprite 5. Si volvemos a girar a la derecha, se nos debería mostrar el sprite 3, etc.

¿Cómo representamos esto con los arrays indicados anteriormente? Para cada uno de estos dos arrays, la posición i representa, para el sprite i, cual debería ser el sprite que se debería dibujar si giramos hacia la izquierda, en el caso del primer array, o hacia la derecha, en el caso del segundo. Así, por ejemplo, si estamos mostrando el sprite 1 (el coche hacia arriba) y giramos a la izquierda, el valor para la posición 1 (recordemos que en C la primera posición de los arrays es el 0) en el array izquierda es 6, por lo que deberemos mostrar el sprite 6, correspondiente al coche dirigiéndose en diagonal hacia arriba a la izquierda. Sin embargo, si lo que hacemos es girar hacia la derecha, el valor almacenado en la posición 1 del array derecha es el 4, por lo que deberemos mostrar el sprite 4, correspondiente al coche dirigiéndose hacia arriba a la derecha.

Habiendo hecho estas dos modificaciones, ya podemos incluir aquí el código de un programa que llamaremos movimiento.c, que nos pondrá a los mandos de un coche que se mostrará en la pantalla, el cual podremos mover hacia donde queramos, sin ninguna restricción.

#include "stdio.h"
#include "ctype.h"
#include "games.h"
#include "coches.h"
 
void main(void)
{
        int x = 100;
        int y = 100;
        int posicion = 0;
 
        putsprite(spr_xor,x,y,sprites[0]);
 
        while(1)
        {
                switch(toupper(getk()))
                {
                       case 'O':
                                putsprite(spr_xor,x,y,sprites[posicion]);
                                posicion = izquierda[posicion];
                                putsprite(spr_xor,x,y,sprites[posicion]);
                                break;
                        case 'P':
                                putsprite(spr_xor,x,y,sprites[posicion]);
                                posicion = derecha[posicion];
                                putsprite(spr_xor,x,y,sprites[posicion]);
                                break;
                }
        }
}

De momento vamos a comenzar con los giros, y ya surcaremos después la pantalla a toda velocidad. El código anterior nos dibuja el coche en una zona cercana al centro de la pantalla, y nos permite girarlo hacia la izquierda y hacia la derecha empleando las teclas o y p respectivamente. Explicamos el código y más adelante indicamos un problema que se muestra durante la ejecuciñón y cómo solucionarlo.

Lo primero que hacemos es incluir algunos archivos de cabecera correspondientes a la librería z88dk para poder hacer uso de algunas funcionalidades de la librería. El segundo de ellos, ctype.h permite utilizar la función toupper, que devuelve la cadena que se le pasa como parámetro pasada a mayúsculas. El tercero que incluimos, games.h, es el que hemos utilizado hasta ahora para poder utilizar putsprite, que ya sabemos de sobra lo que hace. Y finalmente, el primero de ellos, stdio.h, lo incluimos para poder leer de teclado con la función getk. Evidentemente, también debemos incluir coches.h, pues es donde se definen nuestros sprites, el array que los contiene a todos ellos, y los arrays izquierda y derecha cuyo funcionamiento hemos explicado anteriormente.

Una vez que comienza el programa principal, empezamos a definir variables. En el caso de x e y, su cometido es tan simple como indicarnos en que posición de la pantalla se va a encontrar el coche. Ahora que vamos a tener el coche fijo girando a lo mejor no parece de utilidad, pero luego, más tarde, cuando lo movamos, sí que va a serasí. La última variable que definimos es posicion encargada de indicarnos que sprite estamos dibujando en pantalla. Si el array sprites contiene todos los sprites definidos, al hacer un putsprite de sprites[posicion], lo que estaremos haciendo es dibujar por pantalla el sprite almacenado en la posición posicion de dicho array. En nuestro caso, esta variable comienza valiendo 0, por lo que cuando ejecutemos el programa, veremos que el coche, nada más empezar, se encuentra preparado para correr orientado hacia la derecha.

Y una vez dibujamos el coche por primera vez, utilizando putsprite, ya estamos preparados para entrar en el bucle principal. Es muy importante fijarse que para el modo de dibujado (primer parámetro de putsprite) estamos utilizando spr_xor, correspondiente a la operación lógica OR EXCLUSIVA. Usamos este modo para poder borrar sprites de una forma muy cómoda. De forma intuitiva, lo único que debemos saber es que si dibujamos un sprite usando or exclusiva en una zona de la pantalla que esté vacía, el sprite se dibujará tal cual en la pantalla. Si dibujamos el sprite usando or exclusiva en una zona de la pantalla donde ese sprite ya estuviera dibujado, se borrará sin modificar el resto de los píxeles de por alrededor, solo se borrarán los píxeles que formen parte del coche.

Por lo tanto, para mover, lo que haremos será dibujar el sprite usando or exclusiva en la misma posición en la que se encontrara anteriormente, y después vovler a dibujar usando or exclusiva en su nueva posición.

El bucle principal se va a ejecutar de forma indefinida (while (1)). En el mismo, leeremos constantemente lo que el usuario introduzca por el teclado utilizando getk. Hemos de tener en cuenta dos cosas: la función getk es no bloqueante, lo cual quiere decir que si el usuario no pulsa ninguna tecla, el programa no se queda detenido en esa instrucción, y utilizamos getk combinado con toupper, que transforma lo que se le pasa como parámetro a mayúsculas, para que el movimiento funcione igual tanto si el usuario pulsa las teclas con el bloqueo mayúsculas activado o sin el. Por eso, dentro del switch, a la hora de comprobar que tecla se ha pulsado, comparamos la entrada con 'O' y 'P' en lugar de con 'o' y 'p'.

Si la tecla que se ha pulsado es la 'o', deseamos girar hacia la izquierda. Dibujamos el coche en la posición actualusando or exclusiva, de tal forma que se borra. Con la línea posicion = izquierda[posicion] lo que hacemos es averiguar que sprite es el que tenemos que dibujar a continuación, según la orientación en la que nos encontremos. Finalmente volvemos a dibujar el coche con las mismas coordenadas (x,y), pero utilizando un sprite distinto, correspondiente a haber girado el coche a la izquierda. En el caso de que se hubiera pulsado la tecla 'p' se realizaría la misma operación, pero utilizando el array derecha.

Ahora ya podemos compilar y ejecutar y comprobaremos como podemos girar nuestro coche pulsando las teclas 'o' y 'p'- Sin embargo, veremos que se produce un efecto extraño. Al mantener una tecla pulsada durante un tiempo, es como si estuviéramos escribiendo en BASIC: se gira una vez, y tras un breve tiempo, ya se gira sin interrupción. Más tarde averiguaremos como resolver este pequeño contratiempo. Antes, veamos como podemos hacer que nuestro coche acelere y frene.

Queremos que pulsando la tecla 'q' el coche vaya acelerando hasta una velocidad máxima (y por lo tanto, que se vaya moviendo conforme a esa velocidad) y que al pulsar la tecla 'a', el coche vaya frenando hasta detenerse. A continuación vemos como lo hemos resuelto, en el siguiente programa, que sustituirá a nuestro anterior movimiento.c (el código marcado como “Nuevo” es el añadido):

#include "stdio.h"
#include "ctype.h"
#include "games.h"
#include "coches.h"
 
// Nuevo
#define CICLOS_BASE 300;
 
void main(void)
{
        int x = 100;
        int y = 100;
        int posicion = 0;
 
        int velocidad = 0;            // nuevo
        int ciclos = CICLOS_BASE;     // nuevo
 
        putsprite(spr_xor,x,y,sprites[0]);
 
        while(1)
        {
                switch(toupper(getk()))
                {
                        // nuevo (Principio)
                        case 'Q':
                                if (velocidad < 50)
                                {
                                        velocidad = velocidad + 5;
                                }
                                break;
                        case 'A':
                                if (velocidad > 0)
                                {
                                        velocidad = velocidad - 5;
                               }
                                break;
                        // nuevo (Fin)
                        case 'O':
                                putsprite(spr_xor,x,y,sprites[posicion]);
                                posicion = izquierda[posicion];
                                putsprite(spr_xor,x,y,sprites[posicion]);
                                break;
                        case 'P':
                                putsprite(spr_xor,x,y,sprites[posicion]);
                                posicion = derecha[posicion];
                                putsprite(spr_xor,x,y,sprites[posicion]);
                                break;
                }
 
                // nuevo (Principio)
                if (velocidad > 0)
                {
                        ciclos = ciclos - velocidad;
                        if (ciclos < 0)
                        {
                                ciclos = CICLOS_BASE;
                                putsprite(spr_xor,x,y,sprites[posicion]);
                                switch(posicion)
                                {
                                        case 0:
                                                x = x + 1;
                                                break;
                                        case 1:
                                y = y - 1;
                                                break;
                                        case 2:
                                                x = x - 1;
                                                break;
                                        case 3:
                                                y = y + 1;
                                                break;
                                        case 4:
                                                x = x + 1;
                                                y = y - 1;
                                              break;
                                        case 5:
                                                x = x + 1;
                                                y = y + 1;
                                                break;
                                        case 6:
                                                x = x - 1;
                                                y = y - 1;
                                                break;
                                        case 7:
                                                x = x - 1;
                                                y = y + 1;
                                                break;
                                }
                                putsprite(spr_xor,x,y,sprites[posicion]);
                        }
                }
        }
        // nuevo (Fin)
}

Lo más evidente es que necesitaremos una variable, en este caso llamada velocidad, que nos permita almacenar la velocidad del coche en un momento dado. Es importante tener en cuenta que en un principio el coche estará parado, por lo que esta velocidad valdrá cero. Hemos añadido también código que hará que nuestra velocidad aumente o disminuya, hasta una velocidad máxima o mínima, al pulsar las teclas 'q' y 'a', respectivamente (este código es el que se ha añadido dentro del switch).

Lo que a lo mejor queda un poco más esotérico es ver cómo hacemos que el coche, una vez que adquiere velocidad, pueda moverse. La parte del código encargada de esto es la que se encuentra al final. Este código, como es obvio, solo se ejecutará si el coche tiene velocidad. El coche se moverá cuando una variable, llamada ciclos, y a la que se le va restando el valor de la velocidad en cada iteración del bucle principal, llegue a cero. Evidentemente, cuanta mayor sea la velocidad, más rápido llegará ciclos a 0 y cada menos iteraciones del bucle principal se moverá el coche.

Si los ciclos llegan a cero, nos disponemos a mover el coche. Lo primero es volver a hacer que ciclos valga su valor inicial, para volver a comenzar el proceso al terminar de mover. Lo siguiente es borrar el coche de su posición anterior, dibujándolo en dicha posición usando el modo or exclusiva. Y después, con un switch, cambiamos el valor de la coordenada x y/o y según la orientación del coche, dada por la variable posicion. Así, por ejemplo, si posicion vale 0, eso quiere decir que el coche está orientado hacia la derecha, por lo que aumentamos el valor de la coordenada x. Si posicion valiera 4, el coche estaría orientado hacia arriba a la derecha, por lo que disminuiríamos el valor de y y aumentaríamos el de x. Y así con el total de las 8 orientaciones. Finalmente, dibujamos el coche en su nueva posición x e y.

Aquí encontramos varias ventajas con respecto al BASIC. Primero, que los gráficos definidos por el usuario (en este caso, sprites) no tienen por qué estar limitados a un tamaño de 8 por 8, y segundo, que estos sprites pueden moverse utilizando incrementos que sean cualquier múltiplo de un pixel, mientras que en basic debemos mover los UDGs de 8 en 8.

Al ejecutar este programa, veremos como podemos acelerar y frenar, y el coche se moverá, sin ninguna restricción. Si el coche sale de la pantalla, observaremos como este entra por el extremo opuesto y el programa seguirá funcionando sin problemas. Sin embargo, se sigue produciendo el mismo efecto que comentábamos antes, el teclado parece comportarse como si estuviéramos en BASIC… vamos a solucionarlo dándole valor a dos variables del sistema, ¡pero que no se asuste nadie!.

Las variables del sistema se podrían entender como determinadas direcciones de memoría que modifican el comportamiento del sistema según su valor (más información en la Microhobby especial nº2). En nuestro caso concreto, las variables del sistema que vamos a modificar son REPDEL y REPPER, correspondientes a las direcciones de memoria 23561 y 23562. La primera de ellas indica el tiempo en cincuentavos de segundo que se debe tener pulsada una tecla para que esta se comience a repetir, y la segunda indica el tiempo en cincuentavos de segundo que tarda en producirse esta repetición una vez que se comienza. Si le damos un valor de 1 a estas variables, conseguiremos una respuesta dle teclado rápida, y nos desharemos del efecto tan fastidioso que hemos comentado antes.

En el siguiente código se muestran los cambios que deberíamos realizar al comienzo del programa movimiento.c:

#include "stdio.h"
#include "ctype.h"
#include "games.h"
#include "coches.h"
 
#define CICLOS_BASE 300;
 
void main(void)
{
        int x = 100;
        int y = 100;
        int posicion = 0;
 
        char *puntero1 = (char *) 23561;       // nuevo
        char *puntero2 = (char *) 23562;       // nuevo
 
        int velocidad = 0;
        int ciclos = CICLOS_BASE;
 
        putsprite(spr_xor,x,y,sprites[0]);
 
        while(1)
        {
                *puntero1 = 1;                 // nuevo
                *puntero2 = 1;                 // nuevo
                switch(toupper(getk()))
                {

Al mejorar la respuesta del teclado nos ha surgido un nuevo problema… ¡el coche gira demasiado rápido!.

Será conveniente añadir un contador para que el coche no gire nada más pulsar la tecla correspondiente; mejor que gire cuando la tecle lleve un rato pulsada. A continuación mostramos el código que deberíamos añadir:

        int girando = 0;                       // nuevo
        int contador_izquierda = 0;            // nuevo
        int contador_derecha = 0;              // nuevo
        int velocidad = 0;
        int ciclos = CICLOS_BASE;
 
        putsprite(spr_xor,x,y,sprites[0]);
 
        while(1)
        {
                *puntero1 = 1;
                *puntero2 = 1;
 
        girando = 0;
 
                switch(toupper(getk()))
                {
                        case 'Q':
                                if (velocidad < 50)
                                {
                                        velocidad = velocidad + 5;
                                }
                                break;
                        case 'A':
                                if (velocidad > 0)
                                {
                                        velocidad = velocidad - 5;
                                }
                                break;
                        case 'O':
                                // nuevo bloque, hasta el if (incluído)
                                contador_derecha = 0;
                                contador_izquierda = contador_izquierda + 1;
                                girando = 1;
                                if (contador_izquierda == 3)  
                                {
                                        putsprite(spr_xor,x,y,sprites[posicion]);
                                        posicion = izquierda[posicion];
                                        putsprite(spr_xor,x,y,sprites[posicion]);
                                        contador_izquierda = 0;          // nuevo
                                }
                                break;
                       case 'P':
                                // nuevo bloque, hasta el if (incluído)
                                contador_izquierda = 0;
                                contador_derecha = contador_derecha + 1;
                                girando = 1;
                                if (contador_derecha == 3)
                                {
                                        putsprite(spr_xor,x,y,sprites[posicion]);
                                        posicion = derecha[posicion];
                                        putsprite(spr_xor,x,y,sprites[posicion]);
                                        contador_derecha = 0;           // nuevo 
                                }
                                break;
                }
 
                // nuevo (Principio)
                if (girando == 0)     
                {
                        contador_izquierda = 0;
                        contador_derecha = 0;
                }
                // nuevo (Fin)
 
                if (velocidad > 0)
                {

Nos vamos a basar en tres nuevas variables, girando, contador_izquierda y contador_derecha. Las dos últimas son las que nos van a indicar cuándo hacer el giro. Cada vez que le demos a la tecla de giro a la izquierda, aumentará el valor de contador_izquierda, y cada vez que le demos a la tecla de giro a la derecha, aumentará el valor de contador_derecha. Cuando alguna de ellas valga 3, giraremos a la izquierda o a la derecha, respectivamente.

Una medida que tomamos es que al girar hacia la izquierda, ponemos a cero la variable contador_derecha, que nos dice cuanto tiempo hemos estado girando a la derecha, y viceversa, cuando giramos a la derecha, ponemos a cero la variable contador_izquierda, que nos dice cuánto hemos girado hasta la izquierda hasta el momento. Así evitamos que, en el caso de haber estado girando hacia la izquierda durante un tiempo, por ejemplo, empecemos a girar a la derecha, y que tan solo haga falta rozar la tecla de giro izquierda para volver a girar a la izquierda, lo cual no es demasiado real.

Otra medida que tomamos está relacionada con la variable girando. Si dejamos de pulsar las teclas de giro, no es demasiado real que si volvemos a pulsarlas otra vez pasado un rato y durante muy poco tiempo, giremos. Para solucionar esto, lo que hacemos simplemente es: darle a la variable girando el valor 0 cada vez que entramos en el bucle, darle el valor 1 en el caso de que pulsemos giro izquierda o giro derecha, y por último, poner contador_izquierda y contador_derecha a cero en el caso de que no se haya girado en esa iteración, y por lo tanto, en el caso de que girando valga cero… es bastante sencillo de comprender.

¿Y ya está? ¡No! Más problemas se interponen entre nosotros y un sprite en movimiento. Si ejecutamos el código anterior veremos como el coche… ¡no gira! ¿Es que hemos hecho algo mal? No, la algoritmia está bien, pero tenemos un problema de interrupciones. El programa no espera a que pulsemos una tecla, por lo que a lo mejor nosotros estamos pulsando la tecla P todo el rato, pero el programa no llega a leer el teclado a tiempo y girando vuelve a valer 0.

Vamos a hacer que el bucle principal solo avance cuando se produzca una interrupción. Dichas interrupciones se producirán en dos casos, cuando pulsemos una tecla (lo cual es lo que queremos) y cuando vaya a comenzar el refresco de la pantalla (cuando el haz de electrones se sitúe de nuevo en el punto 0,0 de la pantalla para empezar a dibujar, de izquierda a derecha y de arriba a abajo). Para ello tendremos que utilizar… ¡ensamblador!. Pero no hay que preocuparse, porque lo solucionaremos todo con una línea de código.

La instrucción en ensamblador que detiene la ejecución hasta que se produce una interrupción es HALT. Y en z88dk, ejecutar una instrucción en ensamblador en cualquier momento es tan sencillo como hacer uso de la función asm, que recibe como parámetro una cadena conteniendo la instrucción que deseamos ejecutar. Por lo tanto, resolver de una vez por todas nuestros problemas de movimiento es tan simple como colocar la instrucción asm(“HALT”) en los lugares adecuados. A continuación se muestra todo el código de movimiento.c tal como quedaría después de los cambios.

#include "stdio.h"
#include "ctype.h"
#include "games.h"
#include "coches.h"
 
#define CICLOS_BASE 20;
 
void main(void)
{
    int x = 100;
    int y = 100;
    int posicion = 0;
 
    char *puntero1 = (char *) 23561;
    char *puntero2 = (char *) 23562;
 
 
    int girando = 0;
    int contador_izquierda = 0;
    int contador_derecha = 0;
    int velocidad = 0;
    int ciclos = CICLOS_BASE;
 
    putsprite(spr_xor,x,y,sprites[0]);
 
    while(1)
    {
        *puntero1 = 1;
        *puntero2 = 1;
        asm("halt");        // nuevo
 
        girando = 0;
 
        switch(toupper(getk()))
        {
            case 'Q':
                if (velocidad < 30)
                {
                    velocidad = velocidad + 1;
                }
                break;
            case 'A':
                if (velocidad > 0)
                {
                    velocidad = velocidad - 1;
                }
                break;
            case 'O':
                contador_derecha = 0;
                contador_izquierda = contador_izquierda + 1;
                girando = 1;
                if (contador_izquierda == 3)
                {
                    asm("halt");       // nuevo
                    putsprite(spr_xor,x,y,sprites[posicion]);
                    posicion = izquierda[posicion];
                    putsprite(spr_xor,x,y,sprites[posicion]);
                    contador_izquierda = 0;
                }
                break;
            case 'P':
                contador_izquierda = 0;
                contador_derecha = contador_derecha + 1;
                girando = 1;
                if (contador_derecha == 3)
                {
                    asm("halt");       // nuevo
                    putsprite(spr_xor,x,y,sprites[posicion]);
                    posicion = derecha[posicion];
                    putsprite(spr_xor,x,y,sprites[posicion]);
                    contador_derecha = 0;
                 }
                break;
            }
 
            if (girando == 0)
            {
                contador_izquierda = 0;
                contador_derecha = 0;
            }
 
 
        if (velocidad > 0)
        {
            ciclos = ciclos - velocidad;
            if (ciclos < 0)
            {
                ciclos = CICLOS_BASE;
                asm("halt");       // nuevo
                putsprite(spr_xor,x,y,sprites[posicion]);
                switch(posicion)
                {
                    case 0:
                        x = x + 1;
                        break;
                    case 1:
                y = y - 1;
                        break;
                    case 2:
                        x = x - 1;
                        break;
                    case 3:
                        y = y + 1;
                        break;
                    case 4:
                        x = x + 1;
                        y = y - 1;
                        break;
                    case 5:
                        x = x + 1;
                        y = y + 1;
                        break;
                    case 6:
                        x = x - 1;
                        y = y - 1;
                        break;
                    case 7:
                        x = x - 1;
                        y = y + 1;
                        break;
                }
                putsprite(spr_xor,x,y,sprites[posicion]);
            }
        }
    }
}

Como se puede comprobar, se ha incluido un halt antes de la lectura de teclado, para detener el programa hasta que se pulse una tecla, y después se ha incluido un halt antes de borrar y volver a dibujar el sprite, para esperar el refresco de la pantalla. Como consecuencia, el coche irá más lento, es por ello que también hemos modificado el valor de CICLOS_BASE, la velocidad máxima, y el incremento y decremento de la velocidad. ¿Y os habeis fijado? Gracias a que hemos sincronizado el movimiento del coche con el refresco de la pantalla… ¡este ha dejado de parpadear al moverse!

Por último, una cosa curiosa: si mientras movemos el coche pasamos por encima de donde pone Bytes: movimiento (correspondiente a la carga desde cinta) veremos como el borrado y la escritura del sprite del coche no afecta en lo mas mínimo a los píxeles que forman parte de ese texto; esto es sin duda otra de las grandes ventajas de usar el modo de dibujado de sprites or exclusiva.

Por último vamos a dibujar una pista de neumáticos para limitar un poco el movimiento de nuestro bólido. Vamos además a hacerlo de tal forma que sea muy fácil modificar el trazado, y así la diversión se multiplique durante meses. Si recordamos, el último de los sprites que habíamos definido era el correspondiente a un neumático. Este sprite, com todos los anteriores, tiene un tamaño de 10×10 (quizá un poco grande, para hacer circuitos más complicados quizas no hubiera estado mal utilizar sprites de neumáticos más pequeños).

Dentro de coches.h es donde definiremos el trazado de la pista. Si consideramos la pantalla como un array de 18×24 casillas de tamaño 10×10 (el mismo que los neumáticos), podemos indicar el recorrido de la pista creando un array, donde colocaremos ceros en aquellas casillas donde no vaya a haber neumático, y unos en las casillas en las que sí. Al principio de coches.h definimos unas constantes para el tamaño del circuito:

#define ALTURA_CIRCUITO 18
#define ANCHURA_CIRCUITO 24

y al final del mismo archivo incluimos el siguiente array:

short circuito1[] =  { 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    0, 0, 0, 0, 0, 0, 0 };
short circuito2[] =  { 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    1, 1, 1, 0, 0, 0, 0 };
short circuito3[] =  { 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 1, 1, 0, 0 };
short circuito4[] =  { 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 1, 0 };
short circuito5[] =  { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 1 };
short circuito6[] =  { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 1 };
short circuito7[] =  { 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
    0, 0, 0, 0, 0, 0, 1 };
short circuito8[] =  { 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0,
    0, 0, 0, 0, 0, 0, 1 };
short circuito9[] =  { 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0,
    0, 0, 0, 0, 0, 0, 1 };
short circuito10[] = { 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0,
    0, 0, 0, 0, 0, 1, 0 };
short circuito11[] = { 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0,
    0, 0, 0, 0, 1, 0, 0 };
short circuito12[] = { 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
    0, 0, 0, 0, 1, 0, 0 };
short circuito13[] = { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 1, 0, 0, 0 };
short circuito14[] = { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 1, 0, 0, 0 };
short circuito15[] = { 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 1, 0, 0, 0, 0 };
short circuito16[] = { 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
    1, 1, 0, 0, 0, 0, 0 };
short circuito17[] = { 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0,
    0, 0, 0, 0, 0, 0, 0 };
short circuito18[] = { 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0 };
 
short int *circuito[ALTURA_CIRCUITO] = {circuito1, circuito2, circuito3, circuito4,
    circuito5, circuito6, circuito7, circuito8, circuito9, circuito10, circuito11,
    circuito12, circuito13, circuito14, circuito15, circuito16, circuito17,
    circuito18};

Con un poco de imaginación podemos vislumbrar en ese array un circuito cerrado en forma de O. Si ahora creamos un fichero llamado juego.c, que contenga el mismo código que movimiento.c, pero añadiéndole el que aparece en color rojo a continuación, podremos por fin ver nuestro circuito en la pantalla, tal como se ve en la siguiente imagen:

<codec> void main(void) {

      int x = 100;
      int y = 140;
      int posicion = 0;
      short int i, j;         // nuevo
      char *puntero1 = (char *) 23561;
      char *puntero2 = (char *) 23562;
      int girando = 0;
      int contador_izquierda = 0;
      int contador_derecha = 0;
      int velocidad = 0;
      int ciclos = CICLOS_BASE;
      // nuevo (Principio)
      for (i=0;i<30;i++)
              printf("\n");
      for (i=0;i<ALTURA_CIRCUITO;i++)
              for (j=0;j<ANCHURA_CIRCUITO;j++)
                      if (circuito[i][j] == 1)
                              putsprite(spr_xor,j*10+1,i*10+1,sprites[8]);
      // nuevo (Fin)
      while(1)
      {
              *puntero1 = 1;
              *puntero2 = 1;
              asm("halt");

</code>

Nuestro coche dispuesto a ser el rey de la pista

Hay que tener en cuenta que hemos cambiado el valor inicial de la coordenada y del coche para que éste quede dentro de la pista. También hemos creado dos variables, i y j que nos van a servir de contadores en un par de bucles.

Este par de bucles son los dos que se muestran marcados en el código. En el primero lo único que hacemos es escribir en la pantalla 30 líneas en blanco para borrarla (es por eso que en la captura anterior ya no se ve lo de Bytes: juego.tap que molestaba tanto). Es en el segundo en el que dibujamos el circuito.

Si hemos decidido anteriormente que el array circuito nos iba a indicar si en cada celda de 10×10 píxeles en las que dividíamos la pantalla había o no un neumático, lo que haremos para dibujar la pista no es más que recorrer todas las posiciones del array y dibujar en la pantalla un neumático en la posición i*10+1, j*10+1, siendo i y j las coordenadas dentro del array donde hemos leído un 1.

De acuerdo, ya tenemos un coche moviéndose y un circuito… pero cuando el coche llega hasta alguno de los neumáticos, en lugar de producirse una espectacular explosión, lo atraviesa como si nada y sigue su camino. Tenemos, sin duda, que añadir colisiones.

Para detectar cuando nuestro coche colisiona con uno de los neumáticos que forman parte de los límites de la pista, deberemos añadir una condición if antes de cada desplazamiento del mismo, de tal forma que si ese desplazamiento va a provocar que coche y neumático entren en contacto, el coche se pare totalmente (dada la poca velocidad a la que se ha programado el coche en nuestro programa, y a nuestra incapacidad de momento para crear grandes explosiones usando z88dk en el Spectrum, vamos a suponer que al contactar con un neumático el coche solamente queda parado).

A continuación mostrmos lo que sería el código completo de juego.c (con los if y elses añadidos dentro del switch(posicion)) :

#include "stdio.h"
#include "ctype.h"
#include "games.h"
#include "coches.h"
 
#define CICLOS_BASE 20;
 
void main(void)
{
    int x = 100;
    int y = 140;
    int posicion = 0;
    short int i;
    short int j;
 
    char *puntero1 = (char *) 23561;
    char *puntero2 = (char *) 23562;
 
    int girando = 0;
    int contador_izquierda = 0;
    int contador_derecha = 0;
    int velocidad = 0;
    int ciclos = CICLOS_BASE;
 
    for (i=0;i<30;i++)
        printf("\n");
 
    for (i=0;i< 30)
                {
                    velocidad = velocidad + 1;
                }
                break;
            case 'A':
                if (velocidad > 0)
                {
                    velocidad = velocidad - 1;
                }
                break;
            case 'O':
                contador_derecha = 0;
                contador_izquierda = contador_izquierda + 1;
                girando = 1;
                if (contador_izquierda == 3)
                {
                    asm("halt");
                    putsprite(spr_xor,x,y,sprites[posicion]);
                    posicion = izquierda[posicion];
                    putsprite(spr_xor,x,y,sprites[posicion]);
                    contador_izquierda = 0;
                }
                break;
            case 'P':
                contador_izquierda = 0;
                contador_derecha = contador_derecha + 1;
                girando = 1;
                if (contador_derecha == 3)
                {
                    asm("halt");
                    putsprite(spr_xor,x,y,sprites[posicion]);
                    posicion = derecha[posicion];
                    putsprite(spr_xor,x,y,sprites[posicion]);
                    contador_derecha = 0;
                }
                break;
        }
 
        if (girando == 0)
        {
            contador_izquierda = 0;
            contador_derecha = 0;
        }
 
        if (velocidad > 0)
        {
            ciclos = ciclos - velocidad;
            if (ciclos < 0)
            {
                ciclos = CICLOS_BASE;
                asm("halt");
                putsprite(spr_xor,x,y,sprites[posicion]);
                switch(posicion)
                {
                    case 0:
 
                        if (circuito[y/10][(x + 9)/10] == 1 ||
                            (y%10 != 0 && circuito[(y+7)/10][(x+9)/10] == 1))
 
                            velocidad = 0;
                        else
                            x = x + 1;
                        break;
                    case 1:
 
                        if (circuito[(y-2)/10][x/10] == 1 ||
                            (x%10 != 0 && circuito[(y-2)/10][(x+7)/10] == 1))
 
                            velocidad = 0;
                        else
                            y = y - 1;
                        break;
                    case 2:
 
                        if (circuito[y/10][(x-2)/10] == 1 ||
                            (y%10 != 0 && circuito[(y+7)/10][(x-2)/10] == 1))
 
                            velocidad = 0;
                        else
                            x = x - 1;
                        break;
                    case 3:
 
                        if (circuito[(y+9)/10][x/10] == 1 ||
                            (x%10 != 0 && circuito[(y+9)/10][(x+7)/10] == 1))
 
                            velocidad = 0;
                        else
                            y = y + 1;
                        break;
                    case 4:
 
                        if (circuito[y/10][(x + 9)/10] == 1 ||
                            (y%10 != 0 && circuito[(y+7)/10][(x+9)/10] == 1) ||
                            circuito[(y-2)/10][x/10] == 1 ||
                            (x%10 != 0 && circuito[(y-2)/10][(x+7)/10] == 1))
 
                            velocidad = 0;
                        else
                        {
                            x = x + 1;
                            y = y - 1;
                        }
                        break;
                    case 5:
 
                        if (circuito[y/10][(x + 9)/10] == 1 ||
                            (y%10 != 0 && circuito[(y+7)/10][(x+9)/10] == 1) ||
                            circuito[(y+9)/10][x/10] == 1 ||
                            (x%10 != 0 && circuito[(y+9)/10][(x+7)/10] == 1))
 
                            velocidad = 0;
                        else
                        {
                            x = x + 1;
                            y = y + 1;
                        }
                        break;
                    case 6:
 
                        if (circuito[y/10][(x-2)/10] == 1 ||
                            (y%10 != 0 && circuito[(y+7)/10][(x-2)/10] == 1) ||
                            circuito[(y-2)/10][x/10] == 1 ||
                            (x%10 != 0 && circuito[(y-2)/10][(x+7)/10] == 1))
 
                            velocidad = 0;
                        else
                        {
                            x = x - 1;
                            y = y - 1;
                        }
                        break;
                    case 7:
 
                        if (circuito[y/10][(x-2)/10] == 1 ||
                            (y%10 != 0 && circuito[(y+7)/10][(x-2)/10] == 1) ||
                            circuito[(y+9)/10][x/10] == 1 ||
                            (x%10 != 0 && circuito[(y+9)/10][(x+7)/10] == 1))
 
                            velocidad = 0;
                        else
                        {            
                            x = x - 1;
                            y = y + 1;
                        }
                        break;
                }
                putsprite(spr_xor,x,y,sprites[posicion]);
            }
        }
    }
}

Simplemente, antes de mover el coche, en la última parte del bucle principal, comprobamos si va a haber una colisión con alguno de los neumáticos. En el caso de que sea así ponemos la velocidad a cero, y en caso contrario realizamos el movimiento de forma normal. En todas las comprobaciones miramos a ver si en la posición del array circuito correspondiente a donde estaría mas o menos situado el coche (recordemos que en circuito cada posición se corresponde con 10×10 píxeles de la pantalla) hay un 1.

Analicemos, por ejemplo, para el case 0, que se corresponde con el coche moviéndose hacia la derecha, y el resto de condiciones se podrá sacar de la misma forma (hemos de aclarar que en los cuatro últimos case, al tratarse de movimientos diagonales, la comprobación debe ser en los dos sentidos, eje x y eje y):

if (circuito[y/10][(x + 9)/10] == 1 ||
    (y%10 != 0 && circuito[(y+7)/10][(x+9)/10] == 1))

Básicamente la comprobación se compone de dos partes, la que está a la izquierda del OR (que en C, recordemos, se codifica con ||) y la que está a la derecha. Si en el array circuito decíamos que cada posición se correspondía con una región de 10×10 píxeles de la pantalla, cuando el coche está en las coordenadas (y,x), tendríamos que comprobar en la posición (y/10,x/10) del array circuito (en C, las divisiones enteras se redondean hacia abajo).

La anchura y la altura del coche es 10, por lo tanto, si queremos movernos hacia la derecha, aumentando el valor de x en 1, debemos comprobar si el pixel situado en el extremo derecho, es decir, el pixel situado en x+11 , estará en contacto con algún pixel de algún neumático. Esto es equivalente a comprobar si en la posición [y/10][(x+11)/10] de circuito hay almacenado un 1. Si es así habrá colisión, por lo que no nos interesará avanzar, se cumplirá la condición, y velocidad pasará a valer 0. Lo que ocurre es que en nuestro caso, en lugar de x+11 hemos usado x+9 ya que, por ensayo y error hemos comprobado que con este valor el coche solo colisionara cuando este totalmente pegado al neumático (nuestros sprites son de 10×10, pero algunos de ellos no ocupan toda la anchura o toda la altura).

Esto estaría bien si el coche solo pudiera moverse en el eje y en múltiplos de 10, pero esto no siempre es así. Si el coche estuviera en (12,12) (valiendo 12 la coordenada y, por lo tanto), al movernos a la derecha, podríamos colisionar con un neumático almacenado en el array circuito en (1,2), pero también con el que estuviera en (2,2) (el de abajo). Por lo tanto, la segunda condición comprueba si, en el caso de que la coordenada y no tenga un valor múltiplo de 10 (es decir, cuando el resultado de la operación resto, especifacada en C con % y que calcula el resto de la división entera, devuelva un valor distinto de cero), habría colisión con un neumático situado abajo a la derecha. Como en el caso de la x, en lugar de sumar 11 a y, añadimos un valor calculado a ojo que nos asegura que el coche quedaría pegadito en el caso de una colisión vertical.

Se podrían cumplir las dos condiciones a la vez, pero nos es indiferente, con que se cumpla una de las dos ya hay colisión.

En el caso de movernos hacia la izquierda, en lugar de sumar a x restamos, y en el caso de movernos hacia arriba o hacia abajo, hacemos lo mismo pero cambiando lo que sumamos o restamos a x por lo que sumamos o restamos a y y viceversa.

¿Y eso es todo? Bueno… hemos de ser sinceros y admitir que debido a la inclusión del código que detecta las colisiones, ha vuelto el parpadeo del coche al moverse, sobre todo por la parte superior de la pantalla. No le da tiempo al programa a sincronizar el movimiento con el refresco de la pantalla, debido a lo complejo de los cálculos. Esto, a grandes rasgos, se puede mejorar, aunque solo sea un poco, creando dos nuevas variables, x_anterior e y_anterior, y dejando el código de detección de colisiones de la siguiente manera (calculando las colisiones antes de esperar el refresco de la pantalla y borrar y dibujar el sprite):

        if (velocidad > 0)
        {
            ciclos = ciclos - velocidad;
            if (ciclos < 0)
            {
                ciclos = CICLOS_BASE;
                y_anterior = y;      // nuevo
                x_anterior = x;      // nuevo
                switch(posicion)
                {
                    case 0:
                        if (circuito[y/10][(x + 9)/10] == 1 ||
                            (y%10 != 0 && circuito[(y+7)/10][(x+9)/10] == 1))
 
                            velocidad = 0;
                        else
                            x = x + 1;
                        break;
                    case 1:
                        if (circuito[(y-2)/10][x/10] == 1 ||
                            (x%10 != 0 && circuito[(y-2)/10][(x+7)/10] == 1))
 
                            velocidad = 0;
                        else
                            y = y - 1;
                        break;
                    case 2:
                        if (circuito[y/10][(x-2)/10] == 1 ||
                            (y%10 != 0 && circuito[(y+7)/10][(x-2)/10] == 1))
 
                            velocidad = 0;
                        else
                            x = x - 1;
                        break;
                    case 3:
                        if (circuito[(y+9)/10][x/10] == 1 ||
                            (x%10 != 0 && circuito[(y+9)/10][(x+7)/10] == 1))
 
                            velocidad = 0;
                        else
                            y = y + 1;
                        break;
                    case 4:
                        if (circuito[y/10][(x + 9)/10] == 1 ||
                            (y%10 != 0 && circuito[(y+7)/10][(x+9)/10] == 1) ||
                            circuito[(y-2)/10][x/10] == 1 ||
                            (x%10 != 0 && circuito[(y-2)/10][(x+7)/10] == 1))
 
                            velocidad = 0;
                        else
                        {
                            x = x + 1;
                            y = y - 1;
                        }
                        break;
                    case 5:
                        if (circuito[y/10][(x + 9)/10] == 1 ||
                            (y%10 != 0 && circuito[(y+7)/10][(x+9)/10] == 1) ||
                            circuito[(y+9)/10][x/10] == 1 ||
                            (x%10 != 0 && circuito[(y+9)/10][(x+7)/10] == 1))
 
                            velocidad = 0;
                        else
                        {
                            x = x + 1;
                            y = y + 1;
                        }
                        break;
                    case 6:
                        if (circuito[y/10][(x-2)/10] == 1 ||
                        (y%10 != 0 && circuito[(y+7)/10][(x-2)/10] == 1) ||
                        circuito[(y-2)/10][x/10] == 1 ||
                        (x%10 != 0 && circuito[(y-2)/10][(x+7)/10] == 1))
 
                            velocidad = 0;
                        else
                        {
                            x = x - 1;
                            y = y - 1;
                        }
                        break;
                    case 7:
                        if (circuito[y/10][(x-2)/10] == 1 ||
                        (y%10 != 0 && circuito[(y+7)/10][(x-2)/10] == 1) ||
                        circuito[(y+9)/10][x/10] == 1 ||
                        (x%10 != 0 && circuito[(y+9)/10][(x+7)/10] == 1))
 
                            velocidad = 0;
                        else
                        {
                            x = x - 1;
                            y = y + 1;
                        }
                        break;
                }
 
                // nuevo (Principio)
                asm("halt");
                putsprite(spr_xor,x_anterior,y_anterior,sprites[posicion]);
                // nuevo (Fin)
 
                putsprite(spr_xor,x,y,sprites[posicion]);
            }
        }

Bueno, parece que al final lo hemos conseguido: tenemos un coche moviéndose por la pista, que se choca con los bordes del circuito y puede acelerar y frenar… tomando este código como base, podríamos crear nuestro fantástico juego de coches, añadir nuevas pistas, contrincantes, etc. Sin embargo, hemos comprobado ciertas limitaciones en el código de dibujado de sprites que se suministra con z88dk, sobre todo en juego.c, donde se produce un ligero parpadeo del coche cuando este se mueve por la parte superior. En próximos artículos estudiaremos alguna librería que nos puede ayudar a superar el trance… pero mientras, ¡a conducir!.

Me gustaría agradecer tanto a Horace como a NoP por los comentarios realizados y la ayuda que me han prestado durante la realización de este artículo.