Commodore 64C: Bitmaps Multicolor

Publicado el 17 junio 2024 por Alejsanc @cdrninformatica

En el artículo anterior añadí un fondo multicolor a un juego para Commodore 64 utilizando caracteres multicolor personalizados. Otra forma de añadir gráficos a los juegos es usando bitmaps (mapas de bits). Los dos métodos tienen sus ventajas e inconvenientes. Una de las ventajas de los bitmaps es que permiten utilizar más colores individuales, pero tienen la desventaja de que no es posible utilizar a la vez el juego de caracteres de texto incluido en el ordenador.

La pantalla se divide en 1000 bloques de 8x8 puntos, 40 filas por 25 columnas. Los puntos de los caracteres multicolor solo pueden tener el color de fondo, dos colores comunes a todos los caracteres y un color individual limitado al rango 8-15. Los puntos de los bloques de los bitmaps multicolor pueden tener el color de fondo y tres colores individuales sin limitaciones. Para dos de los colores se utiliza la RAM de pantalla, el mismo rango de memoria usado para indicar los caracteres a mostrar en los modos de caracteres. Para el otro color se utiliza la RAM de color, igual que con los caracteres. Los 16 colores que permite el ordenador se codifican utilizando 4 bits (2 4 = 16), por lo que en cada byte se pueden escribir dos colores.

Al usar caracteres cada bloque puede estar ocupado por uno de los 256 caracteres disponibles. Con los bitmaps cada bloque puede ser completamente distinto, ya que se escriben en la memoria RAM todos los puntos de los 1000 bloques. Con los bitmaps se utiliza más memoria RAM, no se pueden usar los caracteres de texto incluidos en el ordenador, pero es posible hacer gráficos con mayor detalle, más realistas.

Para crear bitmaps lo más conveniente es utilizar un editor como por ejemplo Ditheridoo. Este editor muestra la imagen dividida en bloques mediante una rejilla e informa de los cuatro colores usados en cada bloque. También permite el uso de tramado (dithering en inglés) para simular más colores.

Los bitmaps se guardan en archivos con el formato usado por el editor Koala Painter. Este programa es un editor de bitmaps creado para el Commodore 64 y otros ordenadores en los años 1980. Para dibujar en el programa se podía utilizar la tableta gráfica Koala Pad. Su formato de archivo es muy utilizado por todo tipo de programas gráficos para Commodore 64 y se compone de 5 partes:

  • 2 bytes - Dirección de memoria en la que cargar los datos. Por defecto el programa utiliza la dirección 0x6000 - 24576, pero se puede modificar.
  • 8000 bytes - Puntos de los bloques. 1000 bloques x 64 puntos x 1 bit = 64000 bits = 8000 bytes.
  • 1000 bytes - Colores en la RAM de pantalla. Cada byte define dos colores de un bloque.
  • 1000 bytes - Colores en la RAM de color. Cada byte define un color de un bloque.
  • 1 byte - Color de fondo.

Para utilizar bitmaps multicolor se deben activar los gráficos de alta resolución en el registro 53265 y el modo multicolor en el registro 53270. El chip gráfico solo puede acceder a 16 KiB de memoria y se puede configurar para utilizar cualquiera de las cuatro regiones de 16 KiB en las que se pueden dividir los 64 KiB de memoria RAM del ordenador. Por defecto el chip usa la primera región entre 0 y 16383 bytes. En el registro 53272 se puede configurar si la información de los puntos del bitmap comienza al principio o a la mitad de la región. En la primera región es necesario configurarlo para que empiece a la mitad, en el byte 8192, porque múltiples direcciones del principio se utilizan para otros propósitos.

6020 POKE 53265,PEEK(53265) OR 32
6030 POKE 53270,PEEK(53270) OR 16
6040 POKE 53272,PEEK(53272) OR 8

Si utilizamos sprites debemos cargar sus datos en una dirección fuera del rango usado por el bitmap pero dentro de los 16 KiB del chip gráfico.

10 FOR X=16192 TO 16192+63: READ Y: POKE X,Y: NEXT X
20 FOR X=16256 TO 16256+63: READ Y: POKE X,Y: NEXT X
30 POKE 2040,253
40 POKE 2041,254

Para leer los datos del archivo del bitmap es posible utilizar varios métodos. Con el comando LOAD podemos cargar el archivo en la dirección de memoria indicada en los dos primeros bytes del archivo y desde allí copiar los datos a las direcciones donde espera encontrarlos el ordenador. Con el número "1" como tercer parámetro se le indica al comando LOAD que cargue el archivo en la dirección indicada al principio del archivo. El comando LOAD después de cargar el archivo vuelve a ejecutar el programa, por lo que es conveniente cargar el archivo al principio del programa y controlar mediante una variable si ya se ha cargado para no volver a cargarlo nuevamente y entrar en un bucle infinito.

5 IF (L=0) THEN L=1: LOAD "BITMAP",8,1 

6050 LM=24576
6060 FOR X=0 TO 7999: POKE 8192+X,PEEK(LM+X): NEXT X: LM=LM+8000
6070 FOR X=0 TO 999: POKE 1024+X,PEEK(LM+X): NEXT X: LM=LM+1000
6080 FOR X=0 TO 999: POKE 55296+X,PEEK(LM+X): NEXT X: LM=LM+1000
6090 POKE 53281,PEEK(LM)

Si compilamos el programa con MOSpeed es necesario indicarle que deje libre la memoria donde se carga la información del bitmap y los sprites. Con el parámetro runtimestart podemos hacer que el código que añade el compilador para la ejecución del programa se escriba en una dirección fuera de la memoria del chip gráfico. Con el parámetro memhole se le indica al compilador que deje una zona de memoria libre.

java -cp basicv2.jar com.sixtyfour.cbmnative.shell.MoSpeedCL ball-and-paddle-bitmap-load.bas -tolower=true -deadstoreopt=false -target=ball-and-paddle-bitmap-load.prg -runtimestart=16384

Para ejecutar el programa en el emulador VICE es necesario crear una imagen de disco D64 donde incluir el programa y el bitmap. Para acelerar la carga de los programas y bitmaps en VICE se puede desactivar "True drive emulation" y activar "Virtual device" en Preferences -> Settings -> Peripheral devices -> Drive.

c1541 -format ball-and-paddle,01 d64 ball-and-paddle-bitmap-load.d64 -attach ball-and-paddle-bitmap-load.d64 -write ball-and-paddle-bitmap-load.prg ball-and-paddle -write ball-and-paddle-bitmap.koa bitmap

x64sc ball-and-paddle-bitmap-load.d64

Otra forma de leer los datos es abrir el archivo con el comando OPEN y leerlo byte a byte con el comando GET#. Al comando OPEN se le pasan los parámetros S y R para leer el archivo de forma secuencial y en modo solo lectura. Los dos primeros bytes, donde se indica la dirección de carga, se leen pero no se utilizan. Al final se cierra el archivo con el comando CLOSE. Leer el archivo con este método tiene la ventaja de que no se vuelve a ejecutar el programa y podemos escribir los datos directamente en las direcciones de destino. Pero por otro lado tiene el problema de que hay que hacer una serie de operaciones por un problema con el comando GET#.

El comando GET# cuando lee un "0" lo convierte a una cadena vacía. Si utilizamos una variable numérica, cuando el comando le asigne una cadena vacía dará error. Para que no de error es necesario utilizar una variable cadena de texto. En la cadena de texto el byte se guardará como un carácter. Después para poder escribir el byte en una dirección de memoria con el comando POKE hay que convertir el carácter en un número con el comando ASC. Este comando convierte el primer carácter de una cadena en un número. Para que cuando la cadena esté vacía devuelva un "0" se suma a la cadena el carácter "0".

6050 Z$=CHR$(0)
6060 OPEN 2,8,2,"BITMAP,S,R"
6070 GET# 2,Y$: GET# 2,Y$
6080 FOR X=8192 TO 16191: GET# 2,Y$: POKE X,ASC(Y$+Z$): NEXT X
6090 FOR X=1024 TO 2023: GET# 2,Y$: POKE X,ASC(Y$+Z$): NEXT X
6100 FOR X=55296 TO 56295: GET# 2,Y$: POKE X,ASC(Y$+Z$): NEXT X
6110 GET# 2,Y$: POKE 53281,ASC(Y$+Z$)
6120 CLOSE 2

Al crear la imagen D64 el archivo del bitmap se debe escribir como archivo secuencial añadiendo ",s" al nombre.

c1541 -format ball-and-paddle,01 d64 ball-and-paddle-bitmap-open.d64 -attach ball-and-paddle-bitmap-open.d64 -write ball-and-paddle-bitmap-open.prg ball-and-paddle -write ball-and-paddle-bitmap.koa bitmap,s

x64sc ball-and-paddle-bitmap-open.d64

Si solo necesitamos cargar unos pocos bitmaps y nuestro programa deja suficiente memoria libre, podemos incluir los bitmaps en líneas del programa y no necesitar crear un archivo D64. Con el comando od y un script en Bash es posible convertir cualquier archivo binario a líneas numeradas de BASIC con el comando DATA y los bytes codificados como un número entero entre 0 y 255. Con el comando READ podremos leer los datos y copiarlos a sus direcciones de memoria correspondientes. Al igual que con OPEN los dos primeros bytes se leen pero no se utilizan.

#!/bin/bash

number=$1

od -A n -v -t u1 $2 | tr -s " " "," | while read -r line
do
    echo "$number DATA ${line:1}"
    ((number++))

done
# binary2basic.sh 20000 ball-and-paddle-bitmap.koa > bitmap.bas

20000 DATA 0,96,0,0,0,0,85,85,0,0,0,0,0,0,85,85
20001 DATA 0,0,0,0,0,0,85,85,0,0,0,0,0,0,85,85
20002 DATA 0,0,0,0,0,0,85,85,0,0,0,0,0,0,85,85
20003 DATA 0,0,0,0,0,0,85,85,0,0,0,0,0,0,85,85
20004 DATA 0,0,0,0,0,0,85,85,0,0,0,0,0,0,85,85
20005 DATA 0,0,0,0,0,0,85,85,0,0,0,0,0,0,85,85
20006 DATA 0,0,0,0,0,0,85,85,0,0,0,0,0,0,85,85
20007 DATA 0,0,0,0,0,0,85,85,0,0,0,0,0,0,85,85
...
6050 READ Y: READ Y
6060 FOR X=8192 TO 16191: READ Y: POKE X,Y: NEXT X
6070 FOR X=1024 TO 2023: READ Y: POKE X,Y: NEXT X
6080 FOR X=55296 TO 56295: READ Y: POKE X,Y: NEXT X
6090 READ Y: POKE 53281,Y

La copia de la información de la imagen tarda bastante. Si no queremos que se vea el proceso de dibujado podemos desactivar la pantalla antes y activarla cuando esté la imagen completa. Para ello debemos desactivar y activar el bit 4 del registro 53265.

6010 POKE 53265,PEEK(53265) AND NOT 16

6110 POKE 53265,PEEK(53265) OR 16

Como no se pueden utilizar el juego de caracteres del ordenador y el comando PRINT, la escritura de texto es más difícil que en los modos de caracteres. Si queremos escribir algún texto debemos dibujarlo nosotros y hay que tener en cuenta que como en el modo multicolor se agrupan los puntos de dos en dos, las líneas verticales tienen un grosor de dos puntos, lo que no permite caracteres con tanto detalle como el modo monocromo.

Ya que los números de la puntuación del juego deben cambiar según aumentan los puntos, es necesario crear los caracteres de los números para poder imprimir las diferentes combinaciones de números. Los podemos dibujar en el editor y convertirlos a líneas DATA de BASIC. Cada carácter se compone de 8 bytes. Estos bytes se cargan en un array para utilizarlos en una subrutina que se encarga de aumentar el marcador de puntos. Hay que dejar libres una línea vertical a la izquierda y una línea horizontal abajo para que haya separación entre el carácter anterior y el de abajo.

30010 DATA 21,17,17,17,17,17,21,0
30020 DATA 4,4,4,4,4,4,4,0
30030 DATA 21,1,1,21,16,16,21,0
30040 DATA 21,1,1,5,1,1,21,0
30050 DATA 17,17,17,21,1,1,1,0
30060 DATA 21,16,16,21,1,1,21,0
30070 DATA 16,16,16,21,17,17,21,0
30080 DATA 21,1,1,1,1,1,1,0
30090 DATA 21,17,17,21,17,17,21,0
30100 DATA 21,17,17,21,1,1,1,0
6120 FOR X=0 TO 9: FOR Y=O TO 7: READ Z: N(X,Y)=Z: NEXT Y: NEXT X

El comando PRINT se encarga de imprimir los caracteres que forman los números y palabras. Como no podemos utilizar el comando PRINT, es necesario que nosotros hagamos la traducción entre números/palabras y caracteres. El número de puntos hay que descomponerlo en los dígitos que lo forman para después imprimir el carácter correspondiente a cada dígito. Para ello debemos dividir entre 10 sucesivamente y el resto será el último dígito. Mientras el dígito sea mayor que 0 no cambian los siguientes dígitos y no es necesario procesarlos. Los bytes de los caracteres se escriben en memoria a continuación del bloque 48 / byte 8576 (8192 + (48 * 8) = 8576).

7010 PO=PO+1: N1=PO
7020 FOR X=3 TO 0 STEP -1
7030 N2=INT(N1/10)
7040 N3=N1-(N2*10)
7050 NM=8576+(X*8)
7060 FOR Y=0 TO 7: POKE NM+Y,N(N3,Y): NEXT Y
7070 IF N3>0 THEN GOTO 7100
7080 N1=N2
7090 NEXT X
7100 RETURN

El interprete de BASIC del Commodore 64 no permite realizar la operación módulo para calcular el resto, por lo que tendremos que dividir el número entre 10, convertir el resultado en un número entero y restar este resultado multiplicado por 10 al número inicial.

PO=532

532/10=53
532-(53*10)=2

53/10=5
53-(5*10)=3

5/10=0
5-(0*10)=5

Para añadir números, palabras o frases que no cambien podemos utilizar trozos de imagen compuestos por varios bloques. Los podemos dibujar en el editor, convertirlos a líneas DATA de BASIC y escribir los bytes en memoria. Si solo necesitamos que se muestre o no se muestre un texto podemos ocultarlo y mostrarlo cambiando su color. Por ejemplo el mensaje "GAME OVER" se oculta al inicio haciendo que su color sea el mismo que el del fondo. Cuando termina el juego se cambia su color a negro para mostrarlo.

Como las líneas verticales tienen dos puntos de ancho, no caben las tres patas del carácter "M" en un solo bloque y es necesario desplazar a la derecha los siguientes caracteres. Esto hace que los caracteres no coincidan con los bloques de la pantalla. Por esta razón escribir la misma frase usando caracteres individuales sería más complicado.

6100 FOR X=1612 TO 1621: POKE X,3: NEXT X
690 FOR X=1612 TO 1621: POKE X,0: NEXT X

Estas son las ventajas e inconvenientes de usar bitmaps. En general si no necesitamos gráficos con mucho detalle no es necesario usar bitmaps y es mucho más sencillo utilizar los modos de caracteres. También existe la posibilidad de utilizar el modo bitmap en una parte de la pantalla y el modo carácter en otra usando la interrupción "raster".