A lo largo de este blog ya hemos mencionado, al menos en un par de ocasiones, la página web https://8bitworkshop.com/, relativa a la programación retro de diferentes máquinas de 8 bits. Pues bien, vinculados a esa página web hay varios libros que se puede ver en la dirección:
De todos ellos, el que me parece más interesante es el titulado “Making 8-bit arcade games in C”, de Steven Hugg:
Como indica su título, está dedicado a la programación en C de juegos para máquinas de 8 bits.
Es importante destacar que el libro no trata específicamente el C64. Sí trata otras plataformas de los 80 como Midway 8080, VIC Dual, Galaxian / Scramble, Atari y Williams. Se trata de plataformas hardware específicas para juegos de tipo arcade.
De hecho, el libro toca un poco de todo:
Conceptos básicos de microprocesadores.
Conceptos y ejemplos de programación en C.
Plataformas de hardware como las mencionadas.
Y la programación de diferentes proyectos o juegos.
El libro va saltando de una temática a otra a lo largo de sus 31 capítulos y 220 páginas, pero yo diría que el hilo conductor principal son las diferentes plataformas y cómo fueron evolucionando con el paso del tiempo.
En definitiva, un libro interesante y que guarda relación con lo que venimos tratando aquí últimamente.
Igual que hemos hecho con los sprites, se podrían desarrollar librerías en C para trabajar en modo bitmap, modo bitmap multicolor, definir y activar juegos de caracteres personalizados, hacer scroll, etc. Algunas de estas librerías ya estarían parcialmente cubiertas por otras propias de cc65, como TGI; otras no.
Sea como fuere, la otra gran librería que podemos hacer es una librería de sonido en C, y a esto es a lo que vamos a dedicar esta entrada.
Nuevamente, si nos inspiramos en la librería en ensamblador del Volumen II:
y, más concretamente, en el fichero “LibSonido.asm”, vemos que tiene rutinas para:
Inicializar una imagen del SID.
Transferir la imagen del SID (al SID, claro).
Fijar el volumen.
Fijar la frecuencia de una voz.
Fijar la forma de onda de una voz.
Fijar el ADSR de una voz (attack – decay – sustain – release).
Fijar el ancho de pulso de una voz, en caso de que la forma de onda sea cuadrada.
Configurar un filtro sobre una voz.
Activar una voz.
Desactivar una voz.
Pasar de octava y nota a frecuencia.
Introducir un retardo.
Pues bien, todo esto perfectamente puede hacerse en C. Es más, es mucho más cómodo usarlo mediante una librería (pareja de ficheros sonido.h y sonido.c) que conociendo y manejando las estructuras de datos que cc65 define en _sid.h:
Vamos a ello:
Fichero de cabecera sonido.h:
El fichero de cabecera tiene una primera parte de definición de constantes que es así:
Es decir, aparte de controlar si el fichero ya está incluido con __SONIDO_H, define constantes para el tamaño del SID (25 registros de sólo escritura; 29 en total), para las tres voces (que ahora vamos a numerar 0, 1 y 2), para las formas de onda y para los tipos de filtro. También define el tipo byte.
A partir de ahí, incluye prototipos para las funciones de interés:
No vamos a repetir aquí las funciones, porque en el fondo son las mismas que las rutinas que ya se han enumerado para la librería en ensamblador.
Sí interesa recordar que, al ser el SID en su mayoría registros de sólo escritura, salvo los cuatro últimos ($d419 – $d41c), que son de sólo lectura, aplicaremos la técnica de trabajar sobre una imagen del SID (un array de 25 posiciones, uno por cada registro de sólo escritura) y copiar o transferir esa imagen al SID cuando se quiera configurar éste. De este modo será posible conocer y modificar el estado del SID a partir de su imagen.
Fichero de implementación sonido.c:
El fichero que implementa la librería empieza con esta apariencia:
Es decir, primero incluye el header file sonido.h (con “…” para que no se busque en cc65\include, sino en el mismo directorio que sonido.c) y algunas librerías estándar. Después recoge tres tablas de interés:
La tabla sonido_imagen_sid[] es la imagen del SID. Esta es la tabla que vamos a configurar con las diferentes funciones y, llegado el momento, vamos a transferir o copiar al SID.
La tabla sonido_offset_voces[] nos da el offset de los registros de cada voz ($00, $07 y $0e respectivamente) dentro del mapa de memoria del SID.
La tabla sonido_frecuencias_oct7[] nos da las frecuencias correspondientes a las 12 notas de la octava 7, y que nos permite calcular las frecuencias de esas mismas notas en otras octavas (0…6).
A partir de ahí, vienen las implementaciones de las diferentes funciones. E, igual que en el caso de la librería de sprites, no vamos a revisarlas todas, sino alguna seleccionada:
Por ejemplo, para la función sonido_fija_volumen():
Recibe el volumen en un byte, aunque sólo ocupa un nibble.
Se queda con el nibble bajo mediante un AND (volumen & 0x0F).
Toma la posición 0x18 = 24 de la imagen del SID, le hace un OR con el volumen (sonido_imagen_sid[0x18] | volumen), y lo vuelve a guardar en la posición 0x18.
Esto último, es decir, el tomar la posición y hacerle un OR, lo podemos hacer porque estamos trabajando con una imagen del SID. Directamente contra el SID no sería posible hacerlo, porque esos registros son de sólo escritura.
Ya sólo nos quedaría transferir la imagen al SID para que el cambio de volumen fuera efectivo.
Por otro lado, la función sonido_fija_frecuencia() hace cosas parecidas, por ejemplo un AND (frecuencia & 0x00FF) para quedarse con el byte low o menos significativo de la frecuencia, y un desplazamiento de 8 bits a la derecha (frecuencia >> 8) para quedarse con el byte high o más significativo.
Todo esto demuestra que C es un lenguaje de “alto nivel”, pero no mucho, ya que tiene operadores como &, |, >> y muchos otros que permiten operar con bits de forma similar al ensamblador.
Programa de ejemplo:
Para el programa de ejemplo nuevamente nos vamos a basar en uno ya conocido, concretamente el programa 53 del Volumen I:
Este programa, tanto la versión en ensamblador como la versión en C, tiene una tabla de cuatro entradas, siendo cada entrada:
La octava y la nota de la voz 0.
La octava y la nota de la voz 1.
La octava y la nota de la voz 2.
La duración y el volumen, que son comunes a las tres voces.
Y para que la tabla con la melodía ocupe lo menos posible, octavas y notas se codifican en el mismo byte, a razón de un nibble cada una, al igual que la duración y el volumen.
Por tanto, lo que tiene que hacer el programa es inicializar las voces con su forma de onda y ADSR y, luego, reproducir la melodía. Para esto último:
Lee un byte de la tabla, separa octava y nota, calcula la frecuencia, y configura la voz 0 con esa frecuencia.
Idem para la voz 1.
Idem para la voz 2.
Lee un byte de la tabla, separa duración y volumen, y configura el volumen para las tres voces.
Activa las tres voces y transfiere la imagen al SID. Hasta este punto el SID no ha cambiado.
Espera la duración de la nota.
Y repite todo lo anterior hasta que termine la melodía, lo cual se señaliza con el byte $ff, que no es un byte válido pues el C64 no soporta ni $0f = 15 octavas ni 15 notas por octava.
Para terminar, desactiva las tres voces y vuelve a transferir la imagen al SID. Esto último –desactivar las voces– se puede hacer en cada iteración del bucle, es decir, de una nota a la siguiente, pero si se hace sólo al final del mismo la reproducción queda como más fluida.
Y como el programa es largo, aunque esencialmente es un bucle, sólo vamos a ver en detalle algunas de sus funciones más representativas. Por ejemplo, la función configura_frecuencia() es así:
Es decir, recibe como parámetros la pareja (octava, nota) y la voz, que salen de la tabla con la melodía, y separa el nibble alto (octava) del nibble bajo (nota), imprime ambos con printf(), obtiene la frecuencia asociada con sonido_obten_frecuencia(), también la imprime, y fija esa frecuencia para la voz en cuestión.
Por su parte, configura_volumen() es parecida:
Es decir, recibe la pareja (duración, volumen), separa el nibble alto (duración) del nibble bajo (volumen), imprime ambos con printf(), y fija el volumen con sonido_fija_volumen(). Aquí no interviene la voz porque el volumen es común para todas.
Si compilamos y ejecutamos el programa el resultado es así, aparte de escuchar una bonita canción:
Los datos que se muestran en cada fila son la voz, la octava, la nota y la frecuencia. Esto para las tres voces (numeradas ahora como 0, 1 y 2), y luego la duración y el volumen.
En la entrada anterior hemos visto que el manejo de sprites (y del VIC en general) con cc65 es sencillo. Llega con conocer las estructuras de datos de _vic2.h. y hacer asignaciones de valores a los registros.
Ahora bien, se puede hacer todavía más sencillo. Si recordamos, cc65 aporta header files, es decir, constantes, funciones y otras cosas para el manejo de joystick, ratón, consola, disco, gráficos, etc. ¿Por qué no hacer lo mismo con los sprites? ¿Por qué limitarnos a las estructuras de datos de _vic2.h y similares?
Y esto es precisamente lo que me planteo en esta entrada. Inspirándonos en la librería en ensamblador para manejar sprites del Volumen II, hacer una librería equivalente en C. Y la implementación de esa librería se apoyaría en _vic2.h.
Vamos a ello. Partimos del fichero lib-v2.zip disponible aquí:
El contenido de ese ZIP es así (hay muchas librerías para diferentes propósitos):
Y si analizamos el fichero “LibSprites.asm” veremos que contiene estas rutinas:
Una rutina para copiar los datos de un sprite de un origen a un destino.
Una rutina para configurar los colores multicolor.
Una rutina para hacer la configuración básica de un sprite, es decir, para configurar su bloque, su color y activarlo.
Una rutina para posicionar un sprite.
Una rutina para hacer la configuración avanzada de un sprite, es decir, para configurar el multicolor, la expansión horizontal y/o vertical, y la prioridad sobre el fondo.
Una rutina para decidir si dos sprites están o no en colisión.
Todo esto lo vamos a convertir en dos ficheros en C:
sprites.h, que será el header file con los prototipos, constantes y demás.
Y sprites.c, que será la implementación de las funciones.
Por claridad, los nombres de las funciones serán similares a los de las rutinas, y lo de los parámetros también, salvando las diferencias lógicas por las nomenclaturas de nombrado en ensamblador y en C.
Fichero de cabecera sprites.h:
El fichero sprites.h quedaría así. Tendría una primera parte con constantes:
Una primera constante (__SPRITES_H) serviría para controlar si el header file ya está incluido en un programa y, en tal caso, no volver a incluirlo. A partir de ahí, habría constantes para el tamaño de los sprites (64 bytes), el número de sprites (8), controlar el multicolor, la expansión X e Y, la prioridad del fondo y las colisiones.
También aprovechamos para definir el tipo byte, que equivale a un unsigned char de C. Este tipo es muy útil para programar el C64, ya que es un ordenador de 8 bits.
Y la segunda parte sería así:
Es decir, primero define una estructura (el tipo de dato) para tener acceso a los punteros de los sprites, y luego define SPRITES_PUNTEROS como un puntero a ese tipo de estructura y lo vincula la posición $07f8, que es donde empiezan los punteros de los sprites ($07f8 – $07ff).
Por último, define los prototipos de las funciones que permiten copiar los datos de un sprite, configurar los multicolores, hacer la configuración básica de un sprite (puntero / bloque, color y activación), posicionar un sprite, hacer la configuración avanzada (multicolor, expansión y prioridad del fondo) y, por último, la función que permite detectar si dos sprites han colisionado.
Por supuesto, se pueden definir más funciones con otros propósitos, por ejemplo, para animar sprites, pero lo anterior es lo básico.
Fichero de implementación sprites.c:
La implementación de la librería de sprites es el fichero sprites.c. Este fichero incluye el sprites.h (#include <sprites.h>) y, a partir de ahí, aporta implementaciones para sus funciones. Estas implementaciones lógicamente se apoyan en las estructuras de datos de _vic2.h.
A modo de ejemplo, la implementación de la función sprites_conf_basica() sería así:
Es decir:
Recibe tres parámetros, el número de sprite, el bloque donde está almacenada la definición y el color deseado.
Guarda el bloque recibido en el puntero del sprite.
Guarda el color deseado en el registro del VIC encargado del color.
Y, mediante un OR, es decir, mediante el operador | de C, activa el bit correspondiente al sprite en el registro encargado de habilitar los sprites.
Las otras funciones tienen implementaciones similares. Su revisión queda como ejercicio para el lector interesado.
Programa de ejemplo:
Lo último sería hacer un programa de ejemplo que, usando la nueva librería, maneje sprites. Para ello nos inspiramos en el ejemplo de la entrada:
Limpia la pantalla con la función clrscr() de conio.
Instala el driver de los joysticks.
Configura los sprites.
Y, a partir de ahí, entra en un bucle que mueve un sprite, analiza las posibles colisiones y espera un tiempo. Y lo mismo con el otro sprite.
Las funciones para configurar los sprites, moverlos y analizar las colisiones se apoyan en la nueva librería.
Para configurar los sprites, la función conf_sprites() hace así:
A saber, configura los multicolores, que son compartidos entre todos los sprites, copia la definición del sprite 0 en el bloque 0 (254), hace su configuración básica (pone el puntero apuntando al bloque 254, configura el color verde y activa el sprite), hace su configuración avanzada (activa el multicolor, desactiva las expansiones y da prioridad al fondo), y termina con su posición. Y algo análogo con el sprite 1.
Por otro lado, para mover los sprites las funciones mueve_sprite_0() y mueve_sprite_1() hacen así:
Es decir, leen el joystick 1 y 2 respectivamente, y, en función del movimiento leído incrementan o decrementan la coordenada X o la Y. Finalmente, vuelven a posicionar el sprite.
Por último, para detectar las colisiones la función analiza_colision():
Es decir, se apoya directamente en la función sprites_colision() de la nueva librería. Y, en caso de detectar la colisión, termina la ejecución con exit().
El resultado es un viejo conocido:
En conclusión, que no hay por qué limitarse al uso de las estructuras de datos de _vic2.h. Sobre éstas es posible programar librerías que simplifiquen todavía más la programación con sprites o del VIC en general.
Como hemos comentado varias veces, cc65, a través de su header file c64.h, tiene estructuras de datos que permiten manejar fácilmente el VIC, el SID, la RAM de color y las CIAs. Por ejemplo:
Y en el caso particular del VIC (ver header file cc65\include\_vic2.h):
Esto quiere decir que mediante una sintaxis de C tan sencilla como ésta (asignaciones):
VIC.spr0_x = 100
VIC.spr0_y = 100
es posible modificar la posición del sprite 0, por poner un ejemplo.
Veamos entonces cómo podemos hacer un ejemplo sencillo en C para manejar sprites. Si recordamos de alguna entrada antigua de este blog, para definir un sprite necesitamos:
Diseñar el aspecto gráfico del sprite y guardar los 64 bytes que lo definen (en realidad 63) en alguna zona de memoria.
Copiar esos 64 bytes a uno de los 256 “bloques” a los que tiene acceso el VIC.
Poner el puntero del sprite (posiciones $07f8 –$07ff, según el número de sprite) apuntando al bloque elegido.
Habilitar el sprite.
Elegir el color del sprite.
Posicionar el sprite.
Pues bien, veamos cada uno de estos pasos:
Diseñar el aspecto gráfico del sprite:
En este caso vamos a reutilizar algún sprite ya diseñado. Se trata de nuestra vieja amiga la pulga:
Guardar la definición del sprite en una zona de memoria:
Como sabemos, ese diseño de 24 x 21 pixels se traduce en (yendo de izquierda a derecha y de arriba abajo):
3 bytes por fila (24 pixels).
21 filas (21 pixels).
En total, 63 bytes.
En ocasiones, en vez de 63 bytes se manejan 64, aprovechando el byte 64 para guardar la información del color (o colores, en el caso multicolor). También puede ser mero relleno o, simplemente, manejar 63 bytes.
En nuestro ejemplo en C esos 64 bytes se traducen en este array de 64 bytes (o char, en C vienen a ser lo mismo):
Usamos notación hexadecimal (0x…) porque resulta lo más directo en este caso.
Copiar la definición del sprite a uno de los 256 bloques:
Se podría intentar que al cargar el programa PRG en el C64, esos 64 bytes se cargaran ya directamente en el bloque en el que tienen que estar. Sin embargo, no es lo más práctico.
Por ello, lo que vamos a hacer es copiar esos 64 bytes desde la posición que les haya tocado “en suerte” al compilar, hasta el bloque en que queremos que se almacenen. Esto lo hacemos con la función copia_def_sprite(), de la que lógicamente necesitamos prototipo e implementación.
La implementación es así:
Como se puede ver, el código es sencillo:
A partir del número de bloque determinamos la dirección destino multiplicando por 64.
Utilizamos la función estándar memcpy() para copiar los 64 bytes del sprite desde el origen (el array de bytes) hasta el destino (el bloque elegido).
Ya tenemos la definición del sprite en su sitio.
Configurar el puntero del sprite:
Para configurar el puntero del sprite tenemos que guardar el número de bloque elegido en la posición correspondiente al sprite, que es la $07f8 en el caso del sprite 0 y así sucesivamente hasta la $07ff en el caso del sprite 7 (en total, 8 sprites).
Esto lo hacemos con la función conf_ptr_sprite() que, en una primera aproximación, tiene una implementación muy similar a la de copia_def_sprite(), es decir, basada en memcpy():
Posteriormente, veremos otras implementaciones mejores y más claras.
Habilitar el sprite:
Lo siguiente es habilitar el sprite, para lo que ni siquiera necesitamos una función, ya que se trata de asignar el valor 1 (o el valor previo OR 1) a la posición SPENA = $d015, que está accesible mediante VIC.spr_ena.
Esto es tan sencillo que no merece un pantallazo propio, así que aprovechamos para presentar el programa principal en su conjunto. Véase la línea 29 en particular:
Como se puede ver en las primeras líneas del programa principal main(), estamos usando el sprite 0 y el bloque 254. Pero cambiar esto se ha hecho fácilmente configurable mediante variables (sprite y bloque).
Elegir el color y posicionar del sprite:
Con estas dos últimas operaciones ocurre lo mismo que con la habilitación del sprite, son tan sencillas que no requieren de funciones propias. En el caso del color se trata de dar valor a la variable VIC.spr0_color y en el caso de la posición a las variables VIC.spr0_x y VIC.spr0_y.
Una cosa chula del header file _vic2.h es que hay dos formas de usar los registros, bien mediante un nombre específico para cada sprite, o bien mediante un array con un índice que indica el número de sprite. Por ejemplo, en el caso de las posiciones (ídem colores, etc.):
Es decir, la posición del sprite 0 se puede cambiar:
Bien con VIC.spr0_x = X y VIC.spr0_y = Y.
O con VIC.spr_pos[0].x = X y VIC.spr_pos[0].y = Y.
La segunda forma me parece mejor, ya que permite trabajar con una variable “sprite” o “num_sprite” que en ocasiones valdrá 0, en otras 1, en otras 2, … Es más general.
Resultado:
Bueno, pues el resultado de compilar y ejecutar el programa es el que cabría esperar:
En realidad, el resultado no es tan espectacular. El tema sprites lo tenemos controlado desde hace tiempo. Lo que me resulta más espectacular es lo sencillo y directo que resulta hacerlo en C.
Y más sencillo que se puede hacer, lo que me dará pie a futuras entradas…
Mejoras:
En el ZIP adjunto encontraréis la versión original de este programa (sprite1.c), así como varias mejoras:
sprite2.c: En vez de usar el sprite 0 de forma fija usamos el sprite indicado por la variable “sprite”. Es decir, usamos las variables de _vic2.h en su variante por índice.
sprite3.c: Definimos un header file (sprite3.h) con variables para tener acceso de forma fácil a los punteros de los sprites (posiciones $07f8 – $07ff), de forma que no tengamos que usar memcpy() para algo tan sencillo como darles valor.
sprite4.c: Mejoramos el header file (ahora sprite4.h) para poder usar los punteros de los sprites mediante un índice, no con nombres fijos.
Mediante el uso de uniones de C se puede hacer un nuevo header file (sprite5.h) que permita tanto nombres fijos (SPR_PTR.spr0_ptr, SPR_PTR.spr1_ptr, …) como nombres con índice (SPR_PTR.spr_ptr[sprite]). Esto se deja como ejercicio para el lector.
Como ya comentamos algunas entradas más atrás, el resultado de compilar y enlazar un programa en C para el C64 con cc65 es un programa en código máquina con un cachito de BASIC que llama al código máquina con un comando SYS. Por tanto, los programas preparados con cc65 se cargan y ejecutan como si fueran programas en BASIC, pero su grueso es código máquina. Esta es la configuración por defecto, aunque se puede cambiar.
Respecto al mapa de memoria de los programas, estos empiezan en $0801, puesto que tienen ese trocito de BASIC, y pueden llegar hasta $cfff. Esto es así porque cc65 desactiva el intérprete de BASIC ($a000 – $bfff), pero mantiene los chips de entrada / salida (VIC, SID, RAM de color y CIAs) y el kernal.
La RAM de pantalla, salvo que se utilice el driver de Conio para 80 columnas, sigue en su sitio habitual: $0400 – $07e7. La pila de C empieza en $cfff y crece hacia abajo; no hay que confundir esta pila con la pila del C64 (página 1; $0100 – $01ff). Por último, el “heap”, que es de donde C saca la memoria para las estructuras de datos dinámicas que pudiera usar el programa (ej. listas enlazadas, árboles, etc.), se ubica al final del programa y crece hacia la pila (la pila de C).
Por tanto, el mapa de memoria sería algo así:
Ya comentamos anteriormente que cc65 tiene header files específicos para el C64 (cbm.h y c64.h), así como estructuras (structs) vinculadas a las direcciones de memoria del VIC, SID, RAM de color y CIAs, lo que permite manipular cómodamente desde C los registros de estos chips, básicamente haciendo asignaciones de valores a los campos de esas estructuras.
cc65 también tiene drivers para que el C64 pueda manejar gráficos TGI, la consola con Conio, el ratón, el joystick, etc., como hemos ido comentando en entradas anteriores.
También es interesante comentar que, para que el programa en C pueda recibir parámetros del entorno, se puede usar esta sintaxis al ejecutar el programa desde BASIC:
RUN : REM ARG1 “ARG 2” ARG3 “ARG 4” …
Igualmente, el programa en C puede devolver un valor de retorno al BASIC en la posición de memoria STATUS = $90 = 144.
Por último, es interesante comentar que es posible programar en C sacando provecho de las interrupciones del C64, si bien para ello las rutinas de interrupción tienen que estar en ensamblador.
Todo esto y mucho más se puede ver en detalle en la página:
Es increíble, pero el C64 tuvo un sistema operativo con interfaz de usuario gráfica (GUI) ya en el año 1986. Se llamaba GEOS y podéis ver una descripción aquí:
El GEOS se distribuyó sobre todo con el modelo C64C, y como mi modelo era la “panera” original, he de reconocer que en los 80 no caté mucho GEOS. Pero sí lo vi en funcionamiento en alguna ocasión.
Algunos aspectos increíbles de GEOS son:
Residía en disco. Había que cargarlo desde ahí.
Tenía interfaz de usuario gráfica.
Permitía el uso de ratón.
Incluía aplicaciones de edición de textos, hojas de cálculo, para pintar, etc. También había aplicaciones hechas por terceros.
Tenía un cargador turbo para disco.
Permitía el uso de múltiples impresoras.
Etc.
Un sitio interesante para aprender mucho sobre GEOS y descargarlo es éste:
En realidad, lo que te descargas es un fichero ZIP de unos 380 KB con varios discos en formato D64:
El disco / fichero que más nos interesa es el “GEOS64.D64”. Si lo arrastras a VICE…
Ahora sólo queda configurar un ratón o un joystick en VICE (puerto de control 1) y empezar a jugar con GEOS. ¿No es increíble?
En particular, con el menú de VICE File > Attach disk image > Drive 8 se puede conectar cualquiera de los otros discos, por ejemplo, el “APPS64.D64”, y explorar su contenido:
En cualquier caso, aquí estamos para hablar de cc65. Y ocurre que cc65 también soporta la programación en C para un C64 con GEOS mediante el header file geos.h. De hecho, si nos vamos a cc65\include veremos que no sólo tenemos el fichero geos.h, sino toda una carpeta geos con este contenido:
Es decir, que la programación en C para GEOS tiene su miga, hasta el punto de que no hay un único header file, sino 13, cada uno de ellos especializado en funciones concretas de este sistema operativo (constantes, disco, memoria, procesos, sprites, etc.).
La programación para GEOS, incluso en C que será más fácil que en ensamblador, queda fuera del objetivo de este blog, al menos de momento. Pero el que tenga interés puede profundizar en esta página de cc65:
TGI significa “Tiny Graphics Interface”, es decir, algo así como “interfaz para gráficos pequeña”. Por tanto, se trata de una librería para hacer gráficos con máquinas basadas en el 6502, en nuestro caso el C64.
Lo primero sería repasar los modos gráficos del C64:
Todos estos modos pueden combinarse con sprites. Es más, usando interrupciones raster, también pueden combinarse varios modos gráficos entre sí, cambiando el modo cuando el raster llega a la línea X.
Y por recordar un poco más, el modo bitmap estándar tiene una resolución de 200 x 320 pixels y cada uno de esos pixels se puede activar / desactivar independientemente de todos los demás. El estado activado / desactivado de esos 64.000 pixels sale de 8.000 bytes de la RAM (no de la RAM de pantalla), y su color sale de la RAM de pantalla (no de la RAM de color). El color de los pixels activos y el de los pixels inactivos de un carácter 8 x 8, pueden ser diferentes de los colores de los pixels activos e inactivos del carácter 8 x 8 de al lado, pero esto no suele ser lo habitual. A final, en el modo bitmap estándar o “HIRES” lo habitual es tener un color uniforme para pintar formas (pixels activos) y otro color uniforme para el fondo (pixels inactivos). Por ejemplo, líneas blancas sobre fondo negro. En definitiva, es útil para líneas, polígonos, etc.
Por su parte, el modo bitmap multicolor es parecido al modo bitmap estándar, pero los pixels se agrupan de dos en dos en sentido horizontal. Por tanto, la resolución es de 200 x 160 pixels y, a cambio, en vez de dos fuentes de color se utilizan cuatro (por cada carácter 8 x 8). Es útil para gráficos tipo foto, dibujo, etc.
Pues bien, lo que nos ofrece tgi.h es una interfaz de programación, es decir, un conjunto de funciones y constantes, para programar en modo bitmap estándar con el C64.
Inspección de tgi.h:
Lo mejor es que le echemos un vistazo al header file tgi.h:
Como podemos ver, hay funciones para:
Pintar puntos o pixels.
Pintar una línea entre dos posiciones.
Continuar una línea desde la posición actual hasta otra.
Pintar círculos.
Pintar elipses.
Pintar arcos.
Pintar sectores.
Pintar rectángulos.
Pintar textos.
Etc.
Esta es la parte más representativa u obvia, pero también hay funciones y constantes para:
Cargar e instalar un driver.
Inicializar el modo gráfico o salir de él.
Cargar e instalar fuentes (tipos de letras).
Consultar códigos y mensajes de error.
Borrar la pantalla gráfica.
Contar el número de páginas disponibles.
Elegir la página que se ve y la que se pinta.
Consultar los colores disponibles.
Consultar y fijar el color con que se pinta.
Consultar la resolución.
Cambiar la posición actual.
Como siempre, lo mejor es verlo con un ejemplo o, mejor todavía, con un par.
Primer programa de ejemplo con tgi.h:
El primer programa va a ser muy sencillo. Básicamente vamos a instalar el driver y luego vamos a preguntarle cosas. Algo así:
De este modo, si compilamos y ejecutamos:
averiguamos que el driver de TGI para el C64:
Soporta una única página.
Soporta dos colores, que son el blanco y el negro.
Que la resolución es 320 x 200, es decir, bitmap estándar.
Y que la ratio de aspecto es 212.
Bueno, no está mal para empezar, vamos con el segundo.
Segundo programa de ejemplo con tgi.h:
El segundo programa tiene una primera parte que instala el driver, inicializa el modo gráfico y llama a una función pinta_tgi() (también tiene una última llamada que sale del modo gráfico, pero actualmente está comentada para ver el resultado):
Por su parte, la función pinta_tgi() es así:
Es decir:
Borra la pantalla gráfica.
Fija el color blanco como color de pintado.
Activa el pixel (10, 10).
Pinta una línea entre un origen y un final.
Continúa la línea hasta otro nuevo final.
Pinta un círculo.
Pinta una elipse.
Y pinta una barra o rectángulo.
Adicionalmente hay unas líneas finales relativas al pintado de algún texto, pero yo no he conseguido que me funcionen. Quizás algún lector se anime a dar alguna pista extra.
En la documentación de cc65 (https://cc65.github.io/doc/dio.html) se describe dio.h como una librería de entrada/salida de bajo nivel (bajo nivel de abstracción) o de entrada/salida de sectores.
Recordemos que los discos, tanto los actuales de PC como los antiguos del C64, se organizan en platos, pistas y sectores. Los sectores son la unidad mínima de información que se puede leer o escribir.
Cuando uno maneja un API o librería de alto nivel, la programación para acceder a disco la hace en términos de “ficheros” (ej. abre el fichero, cierra el fichero, escribe X en el fichero, etc.). Cuando uno maneja un API o librería de bajo nivel, la programación la hace en términos de sectores, lo cual es bastante más duro, porque de algún modo el programador tiene que saber qué sectores corresponden a qué ficheros, si es que quiere programar algo con sentido. Otra opción es limitarse a hacer programas de gestión de discos, del tipo copiadores de disco, reparadores de disco, etc.
Pues bien, en teoría, con la librería dio.h deberíamos ser capaces de hacer entrada/salida a disco de bajo nivel desde un C64. Sin embargo, me da la sensación de que, al menos la distribución de cc65 que yo tengo, no implementa dio para el C64.
Inspección de dio.h:
Si le echamos un vistazo a dio.h vemos que hay funciones para:
Obtener el tamaño (en bytes) de los sectores de un disco.
Obtener el número de sectores de un disco.
Abrir un disco y obtener un manejador (“handle”).
Cerrar un disco.
Leer desde un disco unos cuantos sectores.
Escribir a disco unos cuantos sectores.
Escribir a disco unos cuantos sectores y verificarlos.
Etc.
Aquí ponemos un extracto:
Como vemos, efectivamente se trata de un API de “bajo nivel”, puesto que la abstracción más compleja que se maneja es el “sector”. En ningún momento el API maneja el concepto de “fichero”.
En todo caso, serían funciones muy útiles para un C64.
Imágenes T64 y D64:
En este blog ya hemos analizado las imágenes T64 y D64 con anterioridad. Se trata de ficheros, típicamente de PC, que son una copia bit a bit de un dispositivo. En el caso T64 se trata de la copia de una cinta (“T” por tape); en el caso D64 se trata de la copia de un disco (“D” por disk).
En VICE es posible crear y conectar imágenes T64 y D64. Por ejemplo, con la opción File > Attach disk image > Drive 8 podemos:
Crear una nueva imagen D64 con “Create image”.
O seleccionar una imagen D64, es decir, un fichero del PC como “DISCO.D64”, y conectarlo a VICE / al C64 como si fuera la unidad 8.
El contenido del disco, es decir, de la imagen D64, se puede ver en la sección “Image Contents”. En este caso el disco llamado “DIO” tiene 664 bloques o sectores, de los que uno contiene un programa PRG llamado “MIPROGRAMA” y otros 663 están disponibles.
De hecho, el programa PRG lo he grabado en la imagen D64 tecleando un programa sencillo en BASIC (10 PRINT “HOLA”; 20 GOTO 10) y luego usando SAVE “MIPROGRAMA”, 8. Todo ello desde VICE, claro.
Sea como fuere, lo que tenemos es un disco de 664 bloques o sectores a 256 bytes el sector, es decir, de casi 170 KB. Bueno, más que un disco es una imagen D64, pero a todos nuestros efectos es lo mismo.
Pues bien, usando dio.h debería ser posible contar los sectores del disco, consultar el tamaño del sector (256 bytes), leer sectores, escribir sectores, etc. Vamos a intentarlo.
Programa de ejemplo con dio.h:
Veamos el siguiente programa en C:
Es bien sencillo:
Importa dio.h.
Y la función main(), define un “manejador” e intenta abrir el disco 8.
Pues bien, si compilamos con cc65 vemos que nos da el error:
Es decir, el programa se compila bien, porque el tipo dhandle_t y el prototipo de la función dio_open() están en el header file dio.h. Es decir, es un programa correcto. Sin embargo, el fichero objeto que se genera (“dio.o”) no se consigue enlazar con alguna librería que implemente dio_open().
En definitiva, la distribución de cc65, al menos la que tengo yo, no trae soporte para dio en el C64. Otra forma de confirmar esto mismo es revisando el fichero “readme.txt” del directorio cc65\samples:
En este fichero se afirma que el programa de ejemplo “diodemo.c”, que se apoya en conio y dio, sólo está disponible para Atari y Apple II.
Los ratones son dispositivos apuntadores que se suelen usar en ordenadores con interfaz de usuario gráfica, por ejemplo, un PC con Windows o un MAC. Sin embargo, la interfaz de usuario normal del C64 era en modo texto (pantalla de 40×25), aunque, por supuesto, también estaba GEOS.
A pesar de eso, el C64 y otros ordenadores Commodore sí soportaron el uso de ratón, aunque no era tan habitual encontrarlos como el joystick. De hecho, en el C64 había dos tipos de ratones:
Ratones en modo joystick o digital: En este modo, la lectura del ratón arrojaba una dirección análoga a la de un joystick (norte, sur, este, oeste, noroeste, noreste, suroeste o sureste), así como la pulsación de un botón. Esencialmente, era lo mismo que usar un joystick, pero manejando un ratón.
Ratones en modo proporcional o analógico: En este modo, la lectura del ratón arroja una posición (X, Y) que, además, puede cambiar más rápido o más despacio en función de la velocidad con que se mueva el ratón. Adicionalmente, permite la lectura de dos o más botones.
Un ejemplo del primer tipo de ratón era el Commodore 1350, y del segundo el Commodore 1351:
Inspección de mouse.h:
La librería mouse.h está en cc65\include, como todos los header files de cc65. Si le echamos un vistazo vemos cosas parecidas a las que vimos en joystick.h:
Constantes y funciones para manejar drivers de ratones.
Constantes para identificar el botón izquierdo y derecho.
Tipos de datos (structs de C) para manejar la posición del ratón, así como el estado de los botones.
Funciones para mover el ratón y leer su posición.
Etc.
Por ejemplo, aquí tenemos un extracto:
De especial interés son las estructuras “mouse_pos” y “mouse_info”, porque nos van a permitir leer la posición y los botones del ratón. Recordemos que los structs de C sólo son tipos de datos; luego hay que declarar variables de esos tipos.
Vamos a hacer un programa de ejemplo muy parecido al de joystick.h. Primero instalaremos el driver para el ratón, y luego leeremos y pintaremos su posición.
Instalación del driver:
El programa principal, es decir, la función main(), es como sigue:
Este programa:
Borra la pantalla con la función clrscr().
Instala el driver con la función mouse_install().
En caso de error en la instalación del driver, llama a error_driver() y termina.
Entra en un bucle infinito que lee el ratón con mouse_pos() y pinta su posición con pinta_mouse().
Lectura del ratón:
La lectura del ratón se hace con la función mouse_pos(). Esta función es equivalente a joy_read() en el caso del joystick.
La principal diferencia entre ambas funciones es que leer un joystick se resuelve con un byte (de hecho, con 5 bits, uno por cada dirección y otro para el disparo), mientras que leer un ratón requiere una estructura de datos más compleja (una coordenada X, una coordenada Y, y el estado de los botones).
Por este motivo, la función joy_read() simplemente devuelve un char (un byte), mientras que la función mouse_pos() no puede devolver un valor, que tendría que ser de un tipo de dato simple en C (ej. un char), sino que recibe la dirección de (un puntero a) una estructura de tipo “mouse_pos”. Con este puntero, la función puede acceder al contenido de la variable de tipo “mouse_pos” y cambiar su contenido. Esto es lo que en C y otros lenguajes se llama “pasar variables por referencia”, en oposición a “pasarlas por valor”.
Bueno, al final en la variable “pos”, que es de tipo “mouse_pos”, lo que tenemos es la lectura del ratón. “pos” es una estructura con dos campos:
pos.x: La coordenada X del ratón.
pos.y: La coordenada Y del ratón.
La forma de acceder a los campos de una estructura en C (struct) es mediante el punto (.).
Por último, hay que pintar la posición del ratón. Esto se hace en la función pinta_mouse():
Lo que hace esta función es:
Coloca el cursor en la posición (0, 0) con la función gotoxy() de conio.h.
Pinta la coordenada X del ratón con printf().
Pinta la coordenada Y del ratón con printf().
No hay que confundir el parámetro “pos”, que es local a la función pinta_mouse(), con la variable “pos”, que es de la función main(). El parámetro “pos” es un puntero a la variable “pos”. Por eso al llamar a pinta_mouse() se usa &pos, siendo & el operador que obtiene la dirección de la variable “pos”.
Siendo “pos” un puntero a la variable “pos” (jaja, parece un trabalenguas), la forma de acceder a sus campos x e y es mediante el operador -> . Este operador de C es muy gráfico, porque parece una flecha, recordándonos que el parámetro “pos” es un puntero.
Quizás podíamos haber llamado “pos” a la variable y “ppos” al parámetro, significando la primera “p” que es un puntero a “pos”.
Ratón y VICE:
Como sabemos, en VICE es posible simular el joystick con el teclado. Pues bien, también es posible simular el ratón del C64 con el ratón del PC en que se ejecuta VICE.
Para ello, hay que ir a la opción Settings > Control port settings… y poner en el puerto de control 1 un ratón 1351:
A partir de ese momento, se le puede indicar a VICE que capture los eventos del ratón del PC como si fueran del ratón del C64, lo cual se hace con Alt + Q o Settings > Mouse settings… y Grab mouse events:
El problema de hacerlo así es que veréis que el ratón del PC deja de responder, es decir, deja de moverse el puntero del ratón del PC. Esto es así porque esos movimientos ya los está capturando VICE. Pero tampoco se aprecia nada en VICE porque el programa que corre en el C64 (de momento ninguno, el intérprete de BASIC) tampoco está preparado para ello.
Por tanto, lo que hay que hacer es compilar el programa en C (esto damos por conocido cómo se hace), cargar el programa arrastrando el fichero PRG hasta VICE, y luego configurar VICE con un mouse 1351 en el puerto de control 1 y empezar a capturar eventos del ratón con Alt + Q. Veréis algo así:
De hecho, si movéis el ratón del PC veréis que las coordenadas X e Y van cambiando. Éstas oscilan desde (0, 0) hasta (319, 199). ¿De dónde salen estos valores? Pues muy fácil: 40 caracteres de ancho * 8 pixels = 320 pixels; 25 caracteres de alto * 8 pixels = 200. Y como empezamos en el (0, 0), que es la esquina superior izquierda, la esquina inferior derecha es la (319, 199). ¡¡Todo encaja!!
Ah, y una cosa importante: para salir del modo captura de eventos del ratón hay que volver a pulsar Alt + Q. Por tanto, Alt + Q sirve tanto para entrar como para salir del modo captura de eventos.
El lector interesado puede completar este ejemplo definiendo un sprite con la forma de un puntero de ratón, y moviéndolo por la pantalla del C64 en función de la lectura (X, Y) del ratón.
Joystick, es decir, el fichero joystick.h y la implementación correspondiente, es la librería de cc65 para el manejo de joysticks. Recordemos que al C64 se le pueden conectar uno o dos joysticks.
El fichero joystick.h, al igual que todos los header files de cc65, está en cc65\include. Si le echamos un vistazo vemos que define:
Constantes y funciones para manejar drivers de joysticks.
Constantes para identificar el joystick 1 y el joystick 2.
Macros para identificar si un joystick se está moviendo hacia arriba, abajo, izquierda o derecha, o pulsando un botón de disparo. Una macro del preprocesador de C es parecida a una constante, es decir, una instrucción #define, pero con parámetros.
Etc.
Aquí vemos un extracto del fichero joystick.h:
Como en el caso de conio.h, podemos hacer de programa de ejemplo en C para ver si nos detecta los joysticks en VICE, que en el fondo se simulan mediante teclas del PC donde se ejecuta VICE:
Instalación del driver:
En informática un “driver” suele referirse a un software que permite que un ordenador se comunique con un dispositivo. Pues bien, cc65 maneja el concepto de “driver” para tener acceso a los joysticks.
Esto me resulta un tanto confuso porque, como ya vimos hace tiempo en este blog, el joystick 1 viene a equivaler al registro CIAPRB = $dc01 y el joystick 2 viene a equivaler al registro CIAPRA = $dc00. Vamos, que no veo mucha necesidad de que ningún software o driver intermedie entre esos registros y la aplicación. Bastaría con leer esas posiciones de memoria y actuar en consecuencia. Esto con cc65 debería ser fácil, porque cc65 permite definir variables y vincularlas a determinadas posiciones de memoria (ej. $dc00 o $dc01).
Quizás el motivo radique en que cc65 no sólo está pensando para el C64, sino también para otros ordenadores basados en el 6502, que podrían ser más complejos en cuanto al manejo del joystick. En este contexto, tener una interfaz de programación unificada para manejar los joysticks podría ser de utilidad.
Sea como fuere, para conseguir leer los joysticks en VICE, que se simulan con el teclado del PC donde corre VICE, he tenido que instalar un driver:
Como se puede ver, el programa en C anterior:
Borra la pantalla con la función clrscr().
Instala el driver con la función joy_install().
En caso de error en la instalación del driver, llama a la función error_driver(), que básicamente saca un mensaje por pantalla.
Entra un bucle infinito que lee el joystick 2 con la función joy_read() y pinta un mensaje significativo con la función pinta_joy().
Lectura del joystick:
Estrictamente hablando, la lectura del joystick se hace con la función joy_read(). Ahora bien, tras leer el joystick, que en este caso es el JOY_2, hay que actuar en consecuencia, en función de que el joystick apunte arriba, abajo, a la izquierda, a la derecha, o a una combinación de estas direcciones. Además, tenemos el disparo.
Este “actuar en consecuencia” en este ejemplo consiste simplemente en mostrar un mensaje por pantalla. En un ejemplo más realista se trataría de mover un sprite, por ejemplo.
Pues bien, esto se hace con la función pinta_joy():
La función pinta_joy() hace uso de las macros JOY_UP(), JOY_DOWN(), JOY_LEFT(), JOY_RIGHT() y JOY_BTN_1(). Las macros son instrucciones del preprocesador de C que se definen con #define. Por tanto, son muy parecidas a las constantes, pero tienen algún parámetro, en este caso el byte (o char) que se ha leído previamente del joystick.
Al final, lo que hace el preprocesador antes de compilar es sustituir la macro por su definición. Es decir, JOY_UP(joy) se sustituiría por (joy & JOY_UP_MASK), donde & es la operación AND y JOY_UP_MASK es una máscara (una constante) que representa el movimiento hacia arriba. De este modo, si ese AND es distinto de cero, o “cierto” en C, eso significa que el joystick efectivamente se está empujando hacia arriba.
Al final, en función de la dirección en que se empuje el joystick, lo que hace pinta_joy() es pintar con Conio “Arriba”, “Abajo”, “Izquierda”, “Derecha” o “Disparo” en la esquina superior izquierda de la pantalla (posición (0, 0)). A las cadenas les ponemos algunos espacios extra al final porque, al ser “Izquierda” la cadena más larga (9 caracteres), conviene borrar los posibles caracteres sobrantes si después de mover a la izquierda movemos en otra dirección cuya cadena representativa tiene menos caracteres.
Joystick y VICE:
A estas alturas del blog, con varios libros sobre C64 a nuestras espaldas, yo creo que todos sabemos que VICE simula los joysticks mediante el teclado del PC donde se ejecuta, y que hay que ir al menú Settings > Joystick settings > Joystick settings… para configurar los joysticks:
Sobre el joystick 2, que es el que usa nuestro programa, seleccionamos “Keyset A” (o B, el que más rabia nos dé) y luego “Configure Keyset A”. Y en la pantalla emergente, configuramos las teclas del PC que queremos asociar con cada movimiento del joystick 2, por ejemplo, los cursores y Control:
A partir de aquí, compilamos el programa en C con cl65.exe (damos por sabido cómo se hace), arrastramos el fichero PRG hasta VICE, y observamos algo así, que va cambiando según movemos el joystick 2 / pinchamos las teclas:
Total, ¡¡ya sabemos usar los joysticks desde C!! Fácil, ¿no?