Raster

El registro “raster” ya se presentó en la entrada dedicada a la animación de sprites.

El “raster” se puede entender como un rayo que va recorriendo la pantalla de izquierda a derecha y de arriba abajo, actualizando la información. En el fondo así funcionaban las televisiones de tubo de rayos catódicos (CRTs) de los 80, mediante un rayo que iba actualizando la imagen. Lo que ocurre en este caso es que ese rayo está controlado por el VIC.

Pues bien, existe un registro en la memoria del C64 que permite trabajar con este rayo. En realidad, es un registro ($d012) y parte de otro (bit 7 de $d011). Más concretamente, estos registros permiten hacer dos cosas:

  • Leer por qué línea de la pantalla va el rayo. Esta información se obtiene leyendo la posición $d012 (bits 0…7) y el bit 7 de $d011 (bit 8). Con 9 bits tenemos 512 líneas.
  • Escribir en el registro. En este caso, lógicamente, no se consigue modificar la secuencia normal de actualización del rayo, que es fija, sino que lo que se consigue es que cuando el rayo alcance la línea escrita, se genere una interrupción.

Las interrupciones del VIC se describirán más en detalle en la entrada siguiente, ya que no sólo el “raster” puede generar interrupciones. Las colisiones de sprites también puede generarlas.

De momento, baste decir que cualquiera de los dos enfoques anteriores (detectar mediante lectura que el “raster” ha alcanzado una línea X o generar y atender una interrupción cuando alcanza una línea X) sirve para sincronizar la ejecución del programa (por ejemplo, las animaciones) y el refresco de la pantalla.

Lo más habitual suele ser actualizar los gráficos cuando el “raster” está fuera de la zona visible de la pantalla (a partir de la línea 250), porque de este modo se consigue una actualización más suave, sin parpadeo.

El “raster” actualiza la pantalla 60 veces por segundo (60 Hz) de modo que, aunque sólo se actualicen los gráficos cuando el “raster” pasa por la línea 255, ya se actualizan con mucha frecuencia. Tanta que el ojo no lo percibe.


Programa de ejemplo: el programa de la entrada dedicada a la animación utiliza el «raster» en modo lectura.

Nomenclatura

Quizás deberíamos haber empezado por aquí, pero no lo hemos hecho así. Los programas de ejemplo que llevamos hasta ahora han sido sencillos y, por ello, no ha sido necesario.

Sin embargo, en cuanto los programas en ensamblador son un poco complejos se hace necesario poner un poco de orden. Y, por ello, se vuelve muy conveniente definir una nomenclatura –una forma de llamar a las cosas– para nombrar las cosas siempre de la misma manera, y para facilitar la identificación de su naturaleza.

En un programa en ensamblador encontramos uno o varios ficheros *.asm con:

  • Constantes
  • Variables
  • Etiquetas
  • Instrucciones
  • Comentarios
  • Rutinas
  • Macros
  • Directivas
  • Etc.

Cada uno puede definir la nomenclatura que más le guste. Lo realmente importante es tener una para facilitar la identificación de qué son las cosas.

Una posible nomenclatura es la que se define en el capítulo 6 del libro “Retro Game Dev: C64 Edition”, es decir:

Elemento Nomenclatura Ejemplo
Ficheros del programa XXX.asm Prog37.asm
Ficheros con librerías de rutinas o macros libXXX.asm libSprites.asm
Constantes Primera letra en mayúscula y resto en minúscula. Negro = $00
Variables Todas las letras en minúscula. Si consta de varias palabras, se separan con mayúscula. numero byte $00

numeroVidas byte $00

Posiciones de memoria y registros del VIC, SID, etc. Todas las letras en mayúscula. CIAPRA, CIAPRB, …
Macros Todas las letras en mayúscula. Si consta de varias palabras, se separan con subrayado (_).

Además, se añadirá una V por cada parámetro de entrada de tipo valor y una D por cada parámetro de entrada de tipo dirección.

CONF_BASICA_VDV
Subrutinas Todas las letras en minúscula. Si consta de varias palabras, se separan con mayúscula. activaSprite
Etiquetas globales Todas las letras en minúscula. Si consta de varias palabras, se separan con mayúscula.

Es probable que la misma etiqueta (ej. bucle, fin, …) se repita en muchas rutinas, por lo que se propone prefijar la etiqueta con algo que indique la rutina (ej. “as” por activaSprite) y evite repeticiones.

asBucle, asFin, …
Etiquetas de macro Igual que las etiquetas globales, pero se sustituye el prefijo de rutina (ej. “as” por activaSprite) por el prefijo @. Este prefijo hace que el ensamblador genere una etiqueta única para cada copia o instancia de la macro. @bucle, @fin, …

De este modo, si el programador se encuentra “Negro” automáticamente sabe que es una constante, si se encuentra “numeroVidas” sabe que es una variable, si se encuentra “CIAPRA” sabe que es un registro o posición de memoria conocida del C64, si se encuentra “acitvaSprite” sabe que es una rutina, etc. Y esto es algo muy práctico…


Programa de ejemplo: el programa de la entrada anterior -colisiones-, al ser un programa algo más complejo, ya se ha hecho conforme a esta nomenclatura.

Colisiones de sprites

Con las colisiones de sprites ocurre algo similar a lo que ocurre con las prioridades, es decir, hay de dos tipos:

  • Colisiones entre sprites.
  • Colisiones entre sprites y el fondo.

Es importante señalar que, en ambos casos, la colisión se produce cuando una parte activa del sprite entra en contacto con la parte activa de otro sprite o del fondo. Es decir, para que se produzca una colisión no es suficiente con que la matriz 24×21 de un sprite se solape con la matriz 24×21 de otro sprite; tienen que solaparse o entrar en contacto partes activas, es decir, pixels que están activados.

Colisiones entre sprites

Las colisiones entre sprites se señalizan mediante el registro del VIC $d01e. Cuando dos sprites colisionan, se activan sus bits correspondientes en este registro. Y estos bits permanecen activos hasta que se lee el registro con una instrucción de carga o lectura de datos (“lda”, “ldx” o “ldy”).

Una cuestión a tener en cuenta es que, si colisionan más de dos sprites, por ejemplo, si colisionan cuatro sprites dos a dos, el registro $d01e nos dirá qué sprites se han visto involucrados en las colisiones, pero no quién ha colisionado con quién. Por tanto, para tener una información completa sobre la situación será necesario tener en cuenta también las posiciones de los sprites.

Colisiones entre sprites y fondo

Las colisiones entre sprites y el fondo se señalizan mediante el registro del VIC $d01f. Cuando un sprite colisiona con el fondo, se activa su bit correspondiente en este registro. Y ese bit permanece activo hasta que se lee el registro con una instrucción de carga o lectura de datos (“lda”, “ldx” o “ldy”).

Como en el caso anterior, una cuestión a tener en cuenta es que, si colisionan con el fondo dos sprites o más, el registro $d01f nos dirá qué sprites se han visto involucrados en las colisiones, pero no quién ha colisionado con qué. Por tanto, para tener una información completa sobre la situación será necesario tener en cuenta también las posiciones de los sprites.

Interrupciones

Los registros descritos anteriormente ($d01e y $d01f) sirven para obtener información sobre las colisiones. Pero además de obtener información, normalmente habrá que gestionar esas colisiones, es decir, el programa tendrá que responder ante ellas ejecutando algún código (ej. descontando vidas, sumando puntos, etc.).

Por supuesto, el programa podría estar validando constantemente (o cada cierto tiempo) los registros $d01e y/o $d01f, pero normalmente será más cómodo usar interrupciones, de modo que, cuando se produzca una colisión, se ejecute instantáneamente una rutina de interrupción.

Los detalles de las interrupciones generadas por el VIC se verán en detalle más adelante.

colisiones


Programa de ejemplo: Prog37

Prioridades de sprites

Las prioridades de los sprites son de dos tipos:

  • Prioridades de los sprites respecto a otros sprites.
  • Prioridades de los sprites respecto al fondo.

Prioridades entre sprites

Las prioridades entre sprites son fijas. El sprite con mayor prioridad es el sprite 0, y el sprite con menor prioridad es el sprite 7.

Esto significa que, si dos sprites se cruzan en pantalla, siempre se verá encima el sprite de mayor prioridad, es decir, el de menor número.

Prioridades de los sprites respecto al fondo

Las prioridades de los sprites respecto al fondo no son fijas, sino que son programables sprite a sprite. Para ello se utiliza el registro $d01b, que tiene un bit por cada sprite. Poniendo ese bit a cero, el sprite tendrá prioridad sobre el fondo; poniendo ese bit a uno, el fondo tendrá prioridad.

Lógicamente, las prioridades respecto al fondo pueden ser diferentes para unos sprites y otros. Para ello, llega con activar/desactivar los bits correspondientes del registro $d01b.

Prioridades.PNG


Programa de ejemplo: Prog35

Animación de sprites

La animación de sprites consiste en cambiar el diseño gráfico de un sprite, por ejemplo, que un sprite con forma humana mueva las piernas al caminar, o que una nube de humo se expanda o cambie de forma cuando estalla una bomba.

Para conseguir este efecto hay que definir cada una de las formas o “frames” del sprite (64 bytes) y almacenarlas en memoria conforme a lo ya explicado (empezando en posiciones múltiplo de 64 dentro del banco de 16 KB direccionado por el VIC). Posteriormente, hay que modificar el puntero del sprite para que vaya apuntando a cada una de las formas deseadas.

Es decir, un sprite no tiene por qué tener una única forma; puede tener varias y se puede cambiar entre ellas.

En este contexto, la temporización es fundamental. Es decir, normalmente no se podrá cambiar de una forma a otra sin controlar nada más. Habrá que controlar el ritmo de esos cambios para que la visualización sea buena.

Un primer intento para que el ritmo no sea demasiado rápido puede consistir en meter un bucle de N iteraciones que no haga nada especial, simplemente que pierda el tiempo. Enseguida se notará que son necesarias varias ejecuciones del bucle de espera, una después de configurar cada forma. Si el bucle sólo se ejecutara una vez, visualmente predominaría aquella forma/frame configurada justo antes del bucle de espera.

Pero incluso ejecutando el bucle de espera varias veces, después de cada forma, la visualización no será buena. Esto es así porque el microprocesador 6510 ejecuta el programa a la vez que el VIC refresca la pantalla (el VIC refresca la pantalla 60 veces por segundo) y, en función de cómo se entrelace la ejecución del programa y el refresco de pantalla, la visualización puede ser buena o no. En general, si no hay ningún tipo de sincronización entre ambas cosas la visualización será de mala calidad.

Por ello, para que la visualización sea buena es aconsejable hacer algún tipo de sincronización entre los refrescos del VIC y la ejecución del programa por parte del 6510. Por ejemplo, se puede detectar cuándo el VIC está actualizando la línea X de la pantalla y, en ese momento, o cada N actualizaciones de esa línea, cambiar de forma.

Para hacer esta detección (que el VIC está actualizando la línea X de la pantalla), se puede utilizar el registro $d012, también llamado “raster”. El “raster” es, precisamente, el “rayo” con el que el VIC va actualizando la pantalla línea por línea, de izquierda a derecha y de arriba abajo.

Otra forma de conseguir esto mismo es mediante interrupciones, puesto que es posible configurar el VIC para que determinados eventos gráficos, por ejemplo, el hecho de que el “raster” actualice la línea X, genere una interrupción, hecho que se puede aprovechar para, mediante una rutina de interrupción, cambiar de forma el sprite o actualizar un contador, de modo que el sprite cambie de forma cada N actualizaciones.

Animación 3.gif


Programa de ejemplo: Prog34
 

Expansión de sprites

Los sprites pueden tener el tamaño normal o se pueden expandir en dirección horizontal, vertical, o ambas. Cuando esto ocurre el tamaño se duplica en la dirección o direcciones elegidas.

Para conseguir esto hay que actuar sobre los registros del VIC:

  • Expansión horizontal: $d01d.
  • Expansión vertical: $d017.

Como todas las posiciones de memoria del C64, estos registros tienen 8 bits, uno por cada sprite. Por tanto, activando el bit X se consigue la expansión del sprite correspondiente, y desactivándolo se anula la expansión.

La expansión horizontal/vertical aplica tanto a sprites monocolor como a sprites multicolor.

Expansion


Programa de ejemplo: Prog33

Sprites multicolor

Los sprites, además de ser monocolor, también pueden ser multicolor.

En este último caso los bits/pixels se agrupan de dos en dos en sentido horizontal y, a cada una de las cuatro combinaciones posibles, le corresponde un color:

  • %00: Color transparente, es decir, el color del fondo de pantalla.
  • %01: Color multicolor número 1 (valor del registro $d025).
  • %10: Color del sprite (valor del registro $d027 – $d02e, según el número de sprite).
  • %11: Color multicolor número 2 (valor del registro $d026).

De lo anterior se deducen varias cosas:

  • Unos sprites pueden ser monocolor y otros multicolor.
  • Los sprites multicolor tienen menos resolución, ya que los pixels se agrupan de dos en dos en sentido horizontal. Y cada pareja tiene un color.
  • Los dos colores extra del sistema multicolor (valores de los registros $d025 y $d026) son compartidos entre todos los sprites multicolor. Aun así, los sprites multicolor conservan un color exclusivo (valores de los registros $d027 – $d02e).

Por último, queda especificar si un sprite es monocolor o multicolor. Esto se hace actuando sobre el registro $d01c, que tiene un bit para cada uno de los 8 sprites. Si el bit está a uno, el sprite es multicolor; si está a cero, el sprite es monocolor.

Las herramientas de diseño de sprites, como “Sprite Editor” de CBM prg Studio, permiten definir sprites monocolor y multicolor:

Multicolor


Programa de ejemplo: Prog32

Posicionamiento y movimiento de sprites

Este es el último paso para llegar a ver el sprite: ubicarlo en pantalla. Para este propósito hay dos registros por sprite (total 16 registros), uno para la coordenada X y otro para la coordenada Y.

Es importante tener en cuenta que las posiciones (X, Y) se refieren a la esquina superior izquierda de los sprites. Pero los sprites no son puntos; ocupan una cierta superficie.

Los registros del VIC para controlar las coordenadas de los sprites son:

$d000 – Coord. X del sprite 0 $d001 – Coord. Y del sprite 0
$d002 – Coord. X del sprite 1 $d003 – Coord. Y del sprite 1
$d004 – Coord. X del sprite 2 $d005 – Coord. Y del sprite 2
$d006 – Coord. X del sprite 3 $d007 – Coord. Y del sprite 3
$d008 – Coord. X del sprite 4 $d009 – Coord. Y del sprite 4
$d00a – Coord. X del sprite 5 $d00b – Coord. Y del sprite 5
$d00c – Coord. X del sprite 6 $d00d – Coord. Y del sprite 6
$d00e – Coord. X del sprite 7 $d00f – Coord. Y del sprite 7
$d010 – Bit más significativo coords.  X No aplica

Cada uno de estos registros, como todas las posiciones de memoria del C64, toma un valor de un byte. Por tanto, podríamos deducir que las posiciones de pantalla posibles para un sprite son desde la posición (0, 0) hasta la posición (255, 255).

En este sentido, hay que hacer dos comentarios importantes:

  • No todas las posiciones son visibles. Hay posiciones que quedan fuera de la zona visible de la pantalla. Por ejemplo, definiendo un sprite que consiste en un único pixel en la esquina superior izquierda de la matriz, he podido comprobar que las posiciones visibles son desde la (24, 50) hasta la (343, 249).
  • Del punto anterior se deriva que un byte no es suficiente para la coordenada X, ya que esta coordenada puede tomar el valor 343 (y otros superiores). En cambio, la coordenada Y no necesita más; con un byte es suficiente para la coordenada Y.

Para complementar lo que falta de la coordenada X, el VIC tiene un registro adicional: la posición $d010. Cada bit de esta posición (bit 0 … bit 7) se añade por la izquierda como bit más significativo a las coordenadas X de los sprites 0 … 7. Por tanto, las coordenadas X tienen 9 bits, los 8 bits menos significativos en las posiciones $d000, $d002, …, $d00e, y el bit más significativo en el bit correspondiente de la posición $d010. No es lo más cómodo, pero es así como funciona.

Por tanto, las coordenadas Y van desde 0 hasta 255 (1 byte), mientras que las coordenadas X van desde 0 hasta 511 (9 bits). Pero no todas esas posiciones (X, Y) son visibles; sólo son visibles desde la (24, 50) hasta la (343, 249). Y siempre teniendo en cuenta que estas posiciones se refieren a la esquina superior izquierda del sprite, y que estos límites pueden variar en función de que el C64 sea del modelo PAL (el modelo de TV analógica que triunfó en Europa) o del modelo NTSC (el modelo de TV analógica que triunfó en América).

Pulga

Por último, para mover sprites obviamente basta con cambiar su posición. El principal cuidado que hay que tener es cuando la coordenada X pase de 255 a 256 o viceversa. En tal caso, hay que acordarse de activar o desactivar el bit correspondiente al sprite en el registro $d010, así como inicializar a 0 o 255 los 8 bits menos significativos de la coordenada X.


Programa de ejemplo: Prog31

Sprites monocolor

Otro paso necesario para llegar a ver un sprite es definir su color. Aunque nos olvidemos de este paso, es probable que veamos el sprite, aunque no tendrá el color deseado; tendrá un color aleatorio en función del contenido de la posición de memoria del VIC que controla su color.

Los registros del VIC que controlan los colores de los sprites son:

$d027 – Color del sprite 0 $d028 – Color del sprite 1
$d029 – Color del sprite 2 $d02a – Color del sprite 3
$d02b – Color del sprite 4 $d02c – Color del sprite 5
$d02d – Color del sprite 6 $d02e – Color del sprite 7

Los valores a almacenar en estas posiciones y los colores correspondientes son los que ya se vieron en la entrada dedicada a la RAM de color.

Activación y desactivación de sprites

Todos los pasos anteriores (diseñar el sprite, obtener sus 64 bytes, ubicarlos en una zona de memoria adecuada, y dar valor al puntero del sprite) son pasos necesarios, pero no son suficientes para ver el sprite.

Además de todo lo anterior, hay que activar el sprite, lo cual se consigue activando el bit correspondiente (desde el bit 0 hasta el bit 7) de la posición $d015. En esta posición hay un registro del VIC que lleva control de qué sprites están activos (bit a 1) y cuáles no (bit a 0).

Este es un ejemplo típico en el que interesa utilizar números en binario y/u operaciones lógicas. Por ejemplo, si cargamos el valor %00000001 en la posición $d015 activaremos el sprite 0. Igualmente, haciendo un “and” del acumulador y la posición $d015 podremos desactivar sprites (aquellos cuyo bit en el acumulador esté a 0), igual que podremos activarlos si lo que hacemos es un “ora” (aquellos cuyo bit en el acumulador esté a 1), o cambiarlos de estado si lo que hacemos es un “eor” (aquellos cuyo bit en el acumulador esté a 1).