En la entrada anterior vimos cómo introducir disparos en el juego de Asteroids. Y una característica importante de los disparos es que los hemos implementado como caracteres, no como sprites (que son muy preciados).
Los caracteres, como ya sabemos, tienen características diferentes a las de los sprites. Entre ellas:
- Se posicionan en una pantalla de 25 filas por 40 columnas. Es decir, su coordenada X va de 0 a 39 (o menos si se sacrifica la parte derecha), y su coordenada Y va de 0 a 24.
- En el fondo, esas coordenadas (X, Y) o (columna, fila) dan lugar a una posición de memoria en la RAM de vídeo, es decir, en el rango $0400 – $07e7. Es en esa posición de memoria donde hay que hacer un “sta” para pintar el carácter, o pintar un espacio para borrarlo.
- Los caracteres no se mueven (los sprites sí). Para simular el movimiento de un carácter hay que borrarlo en su posición actual y volver a pintarlo en su nueva posición.
Debido a estas características especiales o diferentes de los caracteres, viene bien tener las dos rutinas accesorias que ya presentamos en la entrada anterior:
Rutina “pixel2Char”:
Cuando la nave dispara, tenemos que quedarnos con sus coordenadas (X, Y) y su ángulo en la tabla de disparos activos. Pero la nave es un sprite, y sus coordenadas se expresan en pixels. Sin embargo, los disparos son caracteres, y sus coordenadas se expresan en (columna, fila).
Sabiendo que un sprite colocado en la esquina superior izquierda de la pantalla ocupa la posición (24, 50), que esto equivale al carácter (0, 0), y que los caracteres ocupan 8 x 8 pixels, ya tenemos las fórmulas que hay que usar:
- charX = (pixelX – 24) / 8
- charY = (pixelY – 50) / 8
De este modo, efectivamente, si la nave ocupa la posición (pixelX = 24, pixelY = 50), entonces su disparo nacería en la posición (charX = 0, charY = 0). Correcto.
Ahora bien, esto admite varios matices y correcciones. Por ejemplo, la posición de un sprite se expresa respecto de su esquina superior izquierda. Pero un sprite ocupa una superficie de 24 x 21 pixels, es decir, de casi 3 x 3 caracteres. Y lo suyo sería que el disparo naciera del centro de la nave, no de una esquina. Por tanto:
- charX = (pixelX – 24) / 8 + 1
- charY = (pixelY – 50) / 8 + 1
Y esto da lugar a la rutina que hemos diseñado:
Obsérvese cómo se activa el acarreo con “sec” antes de las restas (“sbc”) y cómo se desactiva con “clc” antes de las sumas (“adc”). Obsérvese, también, cómo se consigue la división entre 8 mediante tres desplazamientos a la derecha (“lsr”).
Rutina “char2Mem”:
Una vez que tenemos la posición del disparo expresada en caracteres (charX, charY), el siguiente paso es pintar algo en la RAM de vídeo. Puede ser pintar un punto, es decir, propiamente un disparo, o puede ser pintar un espacio para borrarlo.
En cualquier caso, necesitamos pasar de las coordenadas (charX, charY) a la posición equivalente de la RAM de vídeo. Y esto es lo que hace la rutina “char2Mem”.
Nuevamente, sabiendo que la posición (0, 0) equivale a $0400 = 1024, y que las filas son de 40 columnas, la fórmula es sencilla:
- Posición memoria = 40 x charY + charX + 1024.
De este modo, efectivamente, la posición (0, 0) equivale a $0400 = 1024, y la posición (39, 24) equivale a $07e7 = 2023. Todo correcto.
Sabemos que en ensamblador es fácil multiplicar y dividir por dos. Para ello pueden usarse las instrucciones “asl” y “lsr”, respectivamente. Por ende, también es fácil multiplicar y dividir por ocho (8 = 2^3). Basta con repetir estas instrucciones tres veces.
Y como 40 = 8 x 5, podríamos multiplicar por ocho y sumar cinco veces: 40 x charY = (8 x 5) x charY = (8 x charY) x 5 = (8 x charY) + … + (8 x charY).
Y todavía quedaría sumar charX y 1024, que encima tiene dos bytes. Total, muy farragoso. Tiene que haber algo más fácil.
Y efectivamente lo hay: las tablas de datos. Podemos guardar en una tabla de memoria la posición inicial de cada fila de pantalla:
- $0400 = 1024.
- $0428 = 1064.
- $0450 = 1104.
- …
- $07c0 = 1984
De este modo, si cargamos charY en un registro índice, por ejemplo, en el registro Y, y lo usamos como índice para acceder a la tabla, ya tenemos el comienzo de la fila. Basta sumar charX y ya tenemos la posición final que estamos buscando.
Hay que tener cuidado con un detalle: las posiciones de memoria del C64 ocupan dos bytes. Por tanto, en realidad necesitamos dos tablas que se complementan, una para el byte menos significativo o “lo” (de low) y otra para el byte más significativo o “hi” (de high). Pero conceptualmente es como una tabla única.
Y esto es, precisamente, lo que utiliza la rutina “char2Mem”:
Al sumar charX (llamado charX2 en la rutina) no hay que olvidarse del acarreo. Por eso sumamos charX a la parte “lo” de la posición de memoria y, por si hubiera acarreo, sumamos $00 a la parte “hi”. De este modo, si no hay acarreo no se suma nada, y si lo hay, se suma el acarreo (es decir, “me llevo una”) a la parte “hi”.
Por último, CBM prg Studio nos da facilidades para generar tablas de datos. Se hace con la opción Tools > Data Generator:
Cuidado, porque Data Generator llama “x” de una forma genérica a la variable que se quiera usar para la fórmula, que en nuestro caso es la coordenada Y o fila del carácter. Que esto no produzca confusión.
Cuando los valores generados son de 16 bits (words), puede ser necesario reorganizar los valores generados en una tabla “lo” y otra “hi”, como hemos hecho en este caso.
Resultado final:
Y todo esto para obtener un resultado final, que es una nave que puede moverse y generar disparos, hasta diez a la vez, que pueden avanzar en direcciones variadas, y que desaparecen al llegar a los límites de la pantalla.
Esto puede verse aquí:
Código del proyecto: Asteroids06