Source: https://wiki.speccy.org/cursos/ensamblador/gfx3_sprites_lowres
Timestamp: 2020-08-05 14:34:27+00:00

Document:
cursos:ensamblador:gfx3_sprites_lowres [El wiki de speccy.org]
gfx3_sprites_lowres
cursos:ensamblador:gfx3_sprites_lowres
Gráficos (y III): Sprites y Gráficos en baja resolución (bloques)
Teoría sobre el trazado de sprites
Diseño de una rutina de impresión de sprites
El diseño gráfico del Sprite
La creación de la rutina de impresión
Organización de los sprites en memoria
Conversion de datos graficos a códigos dibujables
Paso de parámetros a las rutinas
Trazado de Sprites de 8x8
Sobreescribiendo el fondo (impresión con LD)
Respetando el fondo (impresión con OR)
Impresión 8x8 usándo máscaras
Trazado de Sprites de 16x16
Impresión 16x16 con Transferencia por LD
Impresión 16x16 usándo operaciones lógicas
Impresión 16x16 usándo máscaras
Técnicas de impresión adecuadas a cada tipo de juego
Rutina genérica trazado de Sprites multicarácter
Borrado de Sprites y Restauración del fondo
Optimizar las rutinas
En este capítulo crearemos rutinas específica de impresión de sprites de 8×8 píxeles en posiciones exáctas de carácter (gráficos de bloques) y la extenderemos a la impresión de sprites de 16×16 píxeles (2×2 bloques). Además de estas rutinas de tamaños específicos, analizaremos una rutina más genérica que permita imprimir sprites de tamaños múltiples del anterior (16×16, 24×16, etc).
El capítulo hará uso intensivo de los algoritmos de cálculo de direcciones de memoria a partir de coordenadas y del movimiento relativo descritos en 2 anteriores capítulos, aunque las rutinas mostradas a continuación podrían ser directamente utilizadas incluso sin conocimientos sobre la organización de la videomemoria del Spectrum.
Estudiaremos también los métodos de impresión sobre pantalla: transferencia directa, operaciones lógicas y uso de máscaras, y la generación de los datos gráficos y de atributos a partir de la imagen formada en un editor de sprites.
Comencemos con las definiciones básicas de la terminología que se usará en este capítulo.
Sprite: Se utiliza el término anglosajón sprite (traducido del inglés: “duendecillo”) para designar en un juego o programa a cualquier gráfico que tenga movimiento. En el caso del Spectrum, que no tiene como otros sistemas hardware dedicado a la impresión de Sprites, aplicamos el término a cualquier mapa de bits (del inglés bitmap) que podamos utilizar en nuestros programas: el gráfico del personaje protagonista, los de los enemigos, los gráficos de cualquier item o incluso las propias fuentes de texto de los marcadores.
Editor de Sprites: Los sprites se diseñan en editores de sprites, que son aplicaciones diseñadas para crear Sprites teniendo en cuenta la máquina destino para la que se crean. Por ejemplo, un editor de Sprites para Spectrum tomará en cuenta, a la hora de aplicar colores, el sistema de tinta/papel en bloques de 8×8 píxeles y no nos permitirá dibujar colores saltándonos dicha limitación que después existirá de forma efectiva en el hardware destino.
Estos programas gráficos tratan los sprites como pequeños rectángulos (con o sin zonas transparentes en ellos) del ancho y alto deseado y se convierten en mapas de bits (una matriz de píxeles activos o no activos agrupados) que se almacenan en los programas como simples ristras de bytes, preparados para ser volcados en la pantalla con rutinas de impresión de sprites.
Sprite en editor de sprites, su bitmap, y su conversión a datos binarios.
Rutinas de impresión de Sprites: son rutinas que reciben como parámetro la dirección en memoria del Sprite y sus Atributos y las coordenadas (x,y) destino, y vuelcan esta información gráfica en la pantalla.
Sprite Set: Normalmente todos los Sprites de un juego se agrupan en un “sprite set” (o “tile set”), que es una imagen rectangular o un array lineal que almacena todos los datos gráficos del juego de forma que la rutina de impresión de Sprites pueda volcar uno de ellos mediante unas coordenadas origen y un ancho y alto (caso del tileset rectangular) o mediante un identificador dentro del array de sprites (caso del tileset lineal).
El sistema de sprites en formato rectangular suele ser utilizado en sistemas más potentes que el Spectrum, permitiendo además sprites de diferentes tamaños en el mismo tileset. Las rutinas que imprimen estos sprites a lo largo del juego requieren como parámetros, además de la posición (x,y) de destino, una posición (x,y) de origen y un ancho y alto para “extraer” cada sprite de su “pantalla origen” y volcarlo a la pantalla destino.
Sprite set de Pacman -© NAMCO- coloreado por Simon Owen
para la versión SAM. Gráficos en formato rectangular.
Las rutinas de impresión requieren
coordenadas (xorg,yorg).
En el caso del Spectrum, nos interesa mucho más el sistema de almacenamiento lineal dentro de un “vector” de datos, ya que normalmente agruparemos todos los sprites de un mismo tamaño en un mismo array. Podremos disponer de diferentes arrays para elementos de diferentes tamaños. Cuando queramos hacer referencia a uno de los sprites de dicho array, lo haremos con un identificador numérico (0-N) que indicará el número de sprite que queremos dibujar comenzando desde arriba y designando al primero como 0.
Parte del sprite set de Sokoban: gráficos en formato
lineal vertical. Las rutinas de impresión
requieren un identificador de sprite (0-NumSprites).
En un juego donde todos los sprites son de 16×16 y los fondos están formados por sprites o tiles de 8×8, se podría tener un “array” para los sprites, otro para los fondos, y otro para las fuentes de texto. Durante el desarrollo del bucle del programa llamaremos a la rutina de impresión de sprites pasando como parámetro el array de sprites, el ancho y alto del sprite, y el identificador del sprite que queremos dibujar.
Frame (fotograma): El “sprite set” no sólo suele alojar los diferentes gráficos de cada personaje o enemigo de un juego, sino que además se suelen alojar todos los frames (fotogramas) de animación de cada personaje. En sistemas más modernos se suele tener un frameset (un array de frames) por cada personaje, y cada objeto del juego tiene asociado su frameset y su estado actual de animación y es capaz de dibujar el frame que le corresponde.
En el Spectrum, por limitaciones de memoria y código, lo normal es tener todo en un mismo spriteset, y tener almacenados los identificadores de animación de un personaje en lugar de su frameset. Así, sabremos que nuestro personaje andando hacia la derecha tiene una animación que consta de los frames (por ejemplo) 10, 11 y 12 dentro del spriteset.
Tiles: Algunos bitmaps, en lugar de ser llamados “sprites”, reciben el nombre de tiles (“bloques”). Normalmente esto sucede con bitmaps que no van a tener movimiento, que se dibujan en posiciones exáctas de carácter, y/o que no tienen transparencia. Un ejemplo de tiles son los “bloques” que forman los escenarios y fondos de las pantallas cuando son utilizados para componer un mapa de juego en base a la repetición de los mismos. Los tiles pueden ser impresos con las mismas rutinas de impresión de Sprites (puesto que son bitmaps), aunque normalmente se diseñan rutinas específicas para trazar este tipo de bitmaps aprovechando sus características (no móviles, posición de carácter, no transparencia), con lo que dichas rutinas se pueden optimizar. Como veremos en el próximo capítulo, los tiles se utilizan normalmente para componer el área de juego mediante un tilemap (mapa de tiles):
Tilemap: componiendo un mapa en pantalla
a partir de tiles de un tileset/spriteset + mapa.
Máscaras de Sprites: Finalmente, cabe hablar de las máscaras de sprites, que son bitmaps que contienen un contorno del sprite de forma que se define qué parte del Sprite original debe sobreescribir el fondo y qué parte del mismo debe de ser transparente.
Un Sprite y su máscara aplicados sobre el fondo.
Las máscaras son necesarias para saber qué partes del sprite son transparentes: sin ellas habría que testear el estado de cada bit para saber si hay que “dibujar” ese pixel del sprite o no. Gracias a la máscara, basta un AND entre la máscara y el fondo y un OR del sprite para dibujar de una sóla vez 8 píxeles sin realizar testeos individuales de bits.
En microordenadores como el Spectrum existe un vínculo especial entre los “programadores” y los “diseñadores gráficos”, ya que estos últimos deben diseñar los sprites teniendo en cuenta las limitaciones del Spectrum y a veces hacerlo tal y como los programadores los necesitan para su rutina de impresión de Sprites o para salvar las limitaciones de color del Spectrum o evitar colisiones de atributos entre personajes y fondos.
El diseñador gráfico y el programador deben decidir el tamaño y características de los Sprites y el “formato para el sprite origen” a la hora de exportar los bitmaps como “datos binarios” para su volcado en pantalla.
A la hora de crear una rutina de impresión de sprites tenemos que tener en cuenta el formato del Sprite de Origen. Casi se podría decir que más bien, la rutina de impresión de sprites debemos escribirla o adaptarla al formato de sprites que vayamos a utilizar en el juego.
Dicho formato puede ser:
Sprite con atributos de color (multicolor) o sin atributos de color (monocolor).
Si el sprite tiene atributos de color, los atributos pueden ir:
En un array de atributos aparte del array de datos gráficos.
En el mismo array de datos gráficos, pero detrás del último de los sprites (linealmente, igual que los sprites), como: sprite0,sprite1,atributos0,atributos1.
En el mismo array de datos gráficos, pero el atributo de un sprite va detrás de dicho sprite en el vector, intercalado: sprite0,atributos0,sprite1,atributos1.
Sprite que altere o no altere el fondo:
Si no debe alterarlo, se tiene que decidir si será mediante impresión por operación lógica o si será mediante máscaras (y dibujar y almacenar estas).
Además, hay que tener las herramientas para el dibujado y la conversión de los bitmaps o spritesets en código, en el formato que hayamos decidido. Más adelante en el capítulo profundizaremos en ambos temas: la organización en memoria del Sprite (o del Spriteset completo) y las herramientas de dibujo y conversión.
Dadas las limitaciones en velocidad de nuestro querido procesador Z80A, el realizar una rutina de impresión de sprites en alta resolución rápida es una tarea de complejidad media/alta que puede marcar la diferencia entre un juego bueno y un juego malo, especialmente conforme aumenta el número de sprites en pantalla y por tanto, el parpadeo de los mismos si la rutina no es suficientemente buena.
La complejidad de las rutinas que veremos concretamente en este capítulo será de un nivel más asequible puesto que vamos a trabajar con posiciones de carácter en baja resolución y además crearemos varias rutinas específicas y una genérica.
Para crear estas rutinas necesitamos conocer la teoría relacionada con:
El cálculo de posición en memoria de las coordenadas (c,f) en las que vamos a dibujar el Sprite.
El dibujado de cada scanline del sprite en pantalla, ya sea con LD/LDIR o con operaciones lógicas tipo OR/XOR.
El avance a través del sprite para acceder a otros scanlines del mismo.
El avance diferencial en pantalla para movernos hacia la derecha (por cada bloque de anchura del sprite), y hacia abajo (por cada scanline de cada bloque y por cada bloque de altura del sprite).
El cálculo de posición en memoria de atributos del bloque (0,0) del sprite.
El avance diferencial en la zona de atributos para imprimir los atributos de los sprites de más de 1×1 bloques.
Gracias a los 2 últimos capítulos del curso y a nuestros conocimientos en ensamblador, ya tenemos los mecanismos para dar forma a la rutina completa.
Diseñaremos rutinas de impresión de sprites en baja resolución de 1×1 bloques (8×8 píxeles), 2×2 bloques (16×16 píxeles) y NxM bloques. Las 2 primeras rutinas, específicas para un tamaño concreto, serán más óptimas y eficientes que la última, que tendrá que adecuarse a cualquier tamaño de sprite y por lo tanto no podrá realizar optimizaciones basadas en el conocimiento previo de ciertos datos.
Por ejemplo, cuando sea necesario multiplicar algún registro por el valor del ancho del sprite, en el caso de la rutina de 1×1 no será necesario multiplicar y en el caso de la rutina de 2×2 podremos hacer uso de 1 desplazamiento a izquierda, pero la rutina de propósito general tendrá que realizar la multiplicación por medio de un bucle de sumas. Así, imprimir un sprite de 2×2 con su rutina específica será mucho más rápido que imprimir el mismo sprite con la genérica.
Aunque trataremos de optimizar las rutinas en la medida de lo posible, se va a intentar no realizar optimizaciones que hagan la rutina ilegible para el lector. Las rutinas genéricas que veremos hoy serán rápidas pero siempre podrán optimizarse más mediante trucos y técnicas al alcance de los programadores con más experiencia. Es labor del programador avanzado el adaptar estas rutinas a cada juego para optimizarlas al máximo en la medida de lo posible.
En este sentido, en alguna de las rutinas utilizaremos variables en memoria para alojar datos de entrada o datos temporales o intermedios. Aunque acceder a la memoria es “lenta” comparada con tener los datos guardados en registros, cuando comenzamos a manejar muchos parámetros de entrada (y de trabajo) en una rutina y además hay que realizar cálculos con ellos, es habitual que agotemos los registros disponibles, más todavía teniendo en cuenta la necesidad de realizar dichos cálculos. En muchas ocasiones se acaba realizando uso de la pila con continuos PUSHes y POPs destinados a guardar valores y recuperarlos posteriormente a realizar los cálculos o en ciertos puntos de la rutina.
Las instrucciones PUSH y POP toman 11 y 10 t-estados respectivamente, mientras que escribir o leer un valor de 8 bits en memoria (LD (NN), A y LD A, (NN)) requiere 13 t-estados y escribir o leer un valor de 16 bits toma 20 t-estados (LD (NN), rr y LD rr, (NN)) con la excepción de LD (NN), HL que cuesta 16 t-estados.
Tiempo en t-estados
PUSH rr 11
PUSH IX o PUSH IY 16
POP rr 10
POP IX o POP IY 14
LD (NN), A 13
LD A, (NN) 13
LD rr, (NN) 20
LD (NN), rr 20
LD (NN), HL 16
Aunque es una diferencia apreciable, no siempre podemos obtener una “linealidad” de uso de la pila que requiera 1 POP por cada PUSH, por lo que en ocasiones se hace realmente cómodo y útil el aprovechar variables de memoria para diseñar las rutinas.
En nuestro caso utilizaremos algunas variables de memoria para facilitar la lectura de las rutinas: serán más sencillas de seguir y más intuitivas a costa de algunos ciclos de reloj. No deja de ser cierto también que los programadores en ocasiones nos obsesionamos por utilizar sólo registros y acabamos realizando combinaciones de intercambios de valores en registros y PUSHes/POPs que acaban teniendo más coste que la utilización de variables de memoria.
El programador profesional tendrá que adaptar cada rutina a cada caso específico de su programa y en este proceso de optimización podrá (o no) sustituir dichas variables de memoria por combinaciones de código que eviten su uso, aunque no siempre es posible dado el reducido juego de registros del Z80A.
Finalmente, recordar que las rutinas que veremos en este capítulo pueden ser ubicadas en memoria y llamadas desde BASIC. Una vez ensambladas y POKEadas en memoria, podemos hacer uso de ellas utilizando POKE para establecer los parámetros de llamada y RANDOMIZE USR DIR_RUTINA para ejecutarlas. A lo largo de la vida de revistas como Microhobby se publicaron varios paquetes de rutinas de gestión de Sprites en ensamblador que utilizan este método y que estaban pensadas para ser utilizadas tanto desde código máquina como desde BASIC.
Como ya hemos visto, una vez diseñados los diferentes sprites de nuestro juego hay que agruparlos en un formato que después, convertidos a datos binarios, pueda interpretar nuestra rutina de impresión.
Hay 4 decisiones principales que tomar al respecto:
Formato de organización del tileset (lineal o en forma de matriz/imagen).
Formato de almacenamiento de cada tile (por bloques, por scanlines).
Formato de almacenamiento de los atributos (después de los sprites, intercalados con ellos).
Formato de almacenamiento de las máscaras de los sprites si las hubiera.
El formato de organización del tileset no debería requerir mucho tiempo de decisión: la organización del tileset en formato lineal es mucho más eficiente para las rutinas de impresión de sprites que el almacenamiento en una “imagen” rectangular. Teniendo todos los sprites (o tiles) en un único vector, podemos hacer referencia a cualquier bloque, tile, sprite o cuadro de animación mediante un identificador numérico.
De esta forma, el “bloque 0” puede ser un bloque vacío, el bloque “1” el primer fotograma de animación de nuestro personaje, etc.
Donde sí debemos tomar decisiones importantes directamente relacionadas con el diseño de la rutina de impresión de Sprites es en la organización en memoria de los datos gráficos del sprite y sus atributos (y la máscara si la hubiera). El formato de almacenamiento de los tiles, los atributos y los datos de máscara definen la exportación de los datos desde el editor de Sprites y cómo debe de trabajar la rutina de impresión.
Veamos con un ejemplo práctico las distintas opciones de que disponemos. Para ello vamos a definir un ejemplo basado en 2 sprites de 16×8 pixeles (2 bloques de ancho y 1 de alto, para simplificar). Marcaremos cada scanline de cada bloque con una letra que representa el valor de 8 bits con el estado de los 8 píxeles, de forma que podamos estudiar las posibilidades existentes.
Así, los 16 píxeles de la línea superior del sprite (2 bytes), los vamos a identificar como “A1” y “B1”. Los siguientes 16 píxeles (scanline 2 del sprite y de cada uno de los 2 bloques), serán los bytes “C1” y “D1”, y así sucesivamente.
Datos gráficos Sprite 1:
| C1 | D1 |
| E1 | F1 |
| G1 | H1 |
| I1 | J1 |
| K1 | L1 |
| M1 | N1 |
| O1 | P1 |
Atributos Sprite 1:
| S1_Attr1 | S1_Attr2 |
Datos gráficos Sprite 2:
| C2 | D2 |
| E2 | F2 |
| G2 | H2 |
| I2 | J2 |
| K2 | L2 |
| M2 | N2 |
| O2 | P2 |
| S2_Attr1 | S2_Attr2 |
Al organizar los datos gráficos y de atributos en disco, podemos hacerlo de 2 formas:
Utilizando 2 arrays: uno con los datos gráficos y otro con los atributos, organizando la información horizontal por scanlines del sprite. Todos los datos gráficos o de atributo de un mismo sprite son consecutivos en memoria y el “salto” se hace al acabar cada scanline completo del sprite (no de cada bloque). La rutina de impresión recibe como parámetro la dirección de inicio de ambas tablas y traza primero los gráficos y después los atributos.
Tabla_Sprites:
DB A1, B1, C1, D1, E1, F1, G1, H1, I1, J1, K1
DB L1, M1, N1, O1, P1, A2, B2, C2, D2, E2, F2
DB G2, H2, I2, J2, K2, L2, M2, N2, O2, P2
Tabla_Atributos:
DB S1_Attr1, S1_Attr2, S2_Attr1, S2_Attr2
Utilizando un único array: Se intercalan los atributos dentro del array de gráficos, detrás de cada Sprite. La rutina de impresión calculará en el array el inicio del sprite a dibujar y encontrará todos los datos gráficos de dicho sprite seguidos a partir de este punto. Al acabar de trazar los datos gráficos, nos encontramos directamente en el vector con los datos de atributo del sprite que estamos tratando.
DB L1, M1, N1, O1, P1, S1_Attr1, S1_Attr2, A2
DB B2, C2, D2, E2, F2, G2, H2, I2, J2, K2, L2
DB M2, N2, O2, P2, S2_Attr1, S2_Attr2
Finalmente, no debemos olvidarnos de que si utilizamos máscaras de sprite también deberemos incluirlas en nuestro “array de datos” (o sprite set). El dónde ubicar cada scanline de la máscara depende, de nuevo, de nuestra rutina de impresión. Una primera aproximación sería ubicar cada byte de máscara antes o después de cada dato del sprite, para que podamos realizar las pertinentes operaciones lógicas entre la máscara, el fondo y el sprite.
Si denominamos “XX” a los datos de la máscara del sprite 1 y “YY” a los datos de máscara del sprite 2, nuestra tabla de datos en memoria quedaría de la siguiente forma:
; Formato: Una única tabla:
DB XX, A1, XX, B1, XX, C1, XX, D1, XX, E1, XX, F1, XX, G1
DB XX, H1, XX, I1, XX, J1, XX, K1, XX, L1, XX, M1, XX, N1
DB XX, O1, XX, P1, S1_Attr1, S1_Attr2
DB YY, A2, YY, B2, YY, C2, YY, D2, YY, E2, YY, F2, YY, G2
DB YY, H2, YY, I2, YY, J2, YY, K2, YY, L2, YY, M2, YY, N2
DB YY, O2, YY, P2, S2_Attr1, S2_Attr2
; Formato: Dos tablas:
DB XX, O1, XX, P1, YY, A2, YY, B2, YY, C2, YY, D2, YY, E2
DB YY, F2, YY, G2, YY, H2, YY, I2, YY, J2, YY, K2, YY, L2
DB YY, M2, YY, N2, YY, O2, YY, P2
Para las rutinas que crearemos como ejemplo utilizaremos el formato lineal horizontal mediante 2 tablas, una con los gráficos y otra con los atributos de dichos gráficos. En las rutinas con máscara, intercalaremos los datos de máscara antes de cada dato del sprite, como acabamos de ver. Es el formato más sencillo para la generación de los gráficos y para los cálculos en las rutinas, y por tanto el elegido para mostrar rutinas comprensibles por el lector.
Sería posible también almacenar la información del sprite por columnas (formato lineal vertical), lo cual requeriría rutinas diferentes de las que vamos a ver en este capítulo.
A continuación hablaremos sobre el editor de Sprites SevenuP y veremos de una forma gráfica el formato de organización lineal-horizontal de datos en memoria, y cómo un gráfico de ejemplo se traduce de forma efectiva en un array de datos con el formato deseado.
Para diseñar los sprites de nuestro juego necesitaremos utilizar un editor de Sprites. Existen editores de sprites nativos en el Spectrum, pero esa opción nos podría resultar realmente incómoda por usabilidad y gestión de los datos (se tendría que trabajar en un emulador o la máquina real y los datos sólo se podrían exportar a cinta o a TAP/TZX).
Lo ideal es utilizar un Editor de Sprites nativo de nuestra plataforma cruzada de desarrollo que permita el dibujado en un entorno cómodo y la exportación de los datos a código “.asm” (ristras de DBs) que incluir directamente en nuestro ensamblador.
Nuestra elección principal para esta tarea es SevenuP, de metalbrain. Nos decantamos por SevenuP por su clara orientación al dibujo “al pixel” y sus opciones para el programador, especialmente el sistema de exportación de datos a C y ASM y la gestión de máscaras y frames de animación. Además, SevenuP funciona bajo múltiples Sistemas Operativos, siendo la versión para Microsoft Windows emulable también en implementaciones como WINE de GNU/Linux.
Para el propósito de este capítulo (y, en general durante el proceso de creación de un juego), dibujaremos en SevenuP nuestro spriteset con los sprites distribuídos verticalmente (cada sprite debajo del anterior). Crearemos un nuevo “sprite” con File → New e indicaremos el ancho de nuestro sprite en pixels y un valor para la altura que nos permita alojar suficientes sprites en nuestro tileset.
Por ejemplo, para guardar la información de 10 sprites de 16×16 crearíamos un nuevo sprite de 16×160 píxeles. Si nos vemos en la necesidad de ampliar el sprite para alojar más sprites podremos “cortar” los datos gráficos, crear una imagen nueva con un tamaño superior y posteriormente pegar los datos gráficos cortados. La documentación de SevenUp explica cómo copiar y pegar:
Modo de Selección 1:
Set Pixel/Reset Pixel
El botón izquierdo pone los pixels a 1.
El botón derecho pone los pixels a 0.
Atajo de teclado: 1
Modo de Selección 2:
Toggle Pixel/Select Zone
El botón izquierdo cambia el valor de los pixels entre 0 y 1.
El botón derecho controla la selección. Para seleccionar una zona,
se hace click-derecho en una esquina, click-derecho en la opuesta y ya
tenemos una porción seleccionada. La zona seleccionada será algo mas
brillante que la no seleccionada y las rejillas (si están presentes)
se verán azules. Ahora los efectos solo afectarán a la zona seleccionada,
y se puede copiar esta zona para pegarla donde sea o para usarla como
patrón en el relleno con textura. Un tercer click-derecho quita la
selección. Atajo de teclado: 2
Copia la zona seleccionada (o el gráfico completo si no hay zona seleccionada)
a la memoria intermedia para ser pegada (en el mismo gráfico o en otro) o para
ser usada como textura de relleno. Atajo de teclado: CTRL+C.
Activa/desactiva el modo de pegado, que pega el gráico de la memoria intermedia
a la posición actual del ratón al pulsar el botón izquierdo. Los atributos solo
se pegan si el pixel de destino tiene la misma posición dentro del carácter que
la fuente de la copia. Con el botón derecho se cancela el modo de pegado. Atajo
de teclado: CTRL+V.
Otra opción es trabajar con un fichero .sev por cada sprite del juego, aprovechando así el soporte para “fotogramas” de SevenuP. No obstante, suele resultar más cómodo mantener todos los sprites en un único fichero con el que trabajar ya que podemos exportar todo con una única operación y nos evita tener que “mezclar” las múltiples exportaciones de cada fichero individual.
Mediante el ratón (estando en modo 1) podemos activar y desactivar píxeles y cambiar el valor de tinta y papel de cada recuadro del Sprite. El menú de máscara nos permite definir la máscara de nuestros sprites, alternando entre la visualización del sprite y la de la máscara.
El menú de efectos nos permite ciertas operaciones básicas con el sprite como la inversión, rotación, efecto espejo horizontal o vertical, rellenado, etc.
Es importante que guardemos el fichero en formato .SEV pues es el que nos permitirá realizar modificaciones en los gráficos del programa y una re-exportación a ASM si fuera necesario.
Antes de exportar los datos a ASM, debemos definir las opciones de exportación en File → Output Options:
Este menú permite especificar diferentes opciones de exportación:
Data outputted: Permite seleccionar si queremos exportar sólo los gráficos, sólo los atributos, o los dos, primero gráficos y luego atributos o primero atributos y luego gráficos.
Mask Before Graph: Si activamos esta opción, cada byte del sprite irá precedido en el array por su byte de máscara correspondiente.
Sort priority: Esta importantísima opción determina el orden de la exportación, indicando a SevenuP qué orden / prioridad debe de seguir al recorrer el gráfico para exportar los valores. Las diferentes opciones para priorizar son:
X Char: Coordenada X de bloque. Prioriza el recorrer el sprite horizontalmente aunque pasemos a otro bloque del mismo.
Char line: Scanline horizontal de bloque. Prioriza acabar los datos del bloque actual horizontalmente antes de pasar al siguiente elemento.
Y Char: Coordenada Y de bloque. Prioriza el recorrer el bloque actual verticalmente antes de bajar al siguiente.
Mask: Prioriza el valor de máscara del byte actual sobre el resto de elementos.
Interleave: Permite definir la forma en que se intercalan gráficos y atributos en el sprite.
Veamos las opciones que debemos especificar de forma predeterminada para exportar los datos de nuestro set de sprites en el formato adecuado para las rutinas utilizadas en este capítulo:
Múltiples sprites en formato vertical sin máscara y sin atributos en 1 array:
Sort Priorities: X char, Char line, Y char
Data Outputted: Gfx
Múltiples sprites en formato vertical sin máscara y con atributos en 1 array:
Data Outputted: Gfx+Attr
Múltiples sprites en formato vertical sin máscara y con atributos en 2 arrays:
Data Outputted: Primero exportamos Gfx y luego Attr
Múltiples sprites en formato vertical con máscara intercalada y con atributos:
Sort Priorities: Mask, X char, Char line, Y char
Mask: Yes, before graphic
Múltiples sprites en formato vertical con máscara intercalada y con atributos en 2 arrays:
Tras establecer las opciones adecuadas para el gráfico en cuestión, seleccionamos File → Export Data: para generar un fichero de texto de extensión .asm con los datos en el formato elegido.
Veamos un ejemplo bastante claro de las posibilidades de exportación utilizando dos sprites de 2×2 bloques (16×16) con valores binarios fácilmente identificables para cada uno de los 8 bloques que forman estos 2 sprites de 16×16:
El spriteset es, pues, de 16×32 píxeles, o lo que es lo mismo, 2 sprites de 2×2 bloques colocados verticalmente.
Hemos rellenado cada “bloque” del spriteset con un patrón de píxeles diferente que sea claramente identificable en su conversión a valor numérico, de forma que los 8 scanlines de cada bloque tienen el mismo valor, pero que a su vez es diferente de los valores de los demás bloques:
| 1  | 128 |  <-- Bloques 1 y 2 de Sprite 1
| 2  |  64 |  <-- Bloques 3 y 4 de Sprite 1
| 4  |  32 |  <-- Bloques 1 y 2 de Sprite 2
| 8  |  16 |  <-- Bloques 3 y 4 de Sprite 2
Veamos el resultado de la exportación del Sprite con diferentes opciones:
Multiples sprites en formato vertical sin máscara y sin atributos:
Marcamos en “Data Outputted” la opción “Gfx”, de forma que no se exporten los atributos. Asímismo, definimos como “Sort Priority” el orden “X char, Char line, Y Char”. Esto da prioridad a los scanlines completos (Char Line) sobre la coordenada Y de cada carácter (Y Char), por lo que tendremos un scanline de cada carácter en nuestro export, avanzando hacia abajo en nuestros 2 sprites:
;Sort Priorities: X char, Char line, Y char
;Mask:            No
Sprite_Sin_Atributos:
DEFB	  1,128,  1,128,  1,128,  1,128
DEFB	  2, 64,  2, 64,  2, 64,  2, 64
DEFB	  4, 32,  4, 32,  4, 32,  4, 32
DEFB	  8, 16,  8, 16,  8, 16,  8, 16
Multiples sprites en formato vertical sin máscara y con atributos al final:
Si marcamos la opción “Data Outputted” = “Gfx + Attr”, obtendremos el siguiente export:
;Data Outputted:  Gfx+Attr
Sprite_con_atributos:
DEFB	 57, 58, 59, 60, 61, 62, 57, 56
El array de datos resultante es esencialmente igual al anterior, salvo que se añaden los 8 bytes de atributos (2 sprites de 16×16 = 2×2 bytes por cada sprite = 8 bytes de atributo).
Multiples sprites en formato vertical con máscara y con atributos al final:
Con las mismas opciones que en el caso anterior, pero activando la opción “Mask Before Graph” y subiendo la prioridad de la máscara al máximo obtenemos el siguiente array de datos:
;Sort Priorities: Mask, X char, Char line, Y char
;Mask:            Yes, before graphic
Sprite_con_mascara_y_atributos:
DEFB	254,  1,127,128,254,  1,127,128
DEFB	253,  2,191, 64,253,  2,191, 64
DEFB	251,  4,223, 32,251,  4,223, 32
DEFB	247,  8,239, 16,247,  8,239, 16
En este export, hemos habilitado el uso de máscaras y el byte de máscara de cada scanline aparece justo antes del byte de datos del mismo. Los atributos permanecen al final del array.
Multiples sprites en formato vertical separando GFX y ATTR en 2 tablas:
Podemos obtener la información del Sprite en 2 tablas separadas realizando 2 exportaciones con las anteriores configuraciones que hemos visto: bastará con realizar la primera como Outputted Data = Gfx y la segunda como Outputted Data = Attr, marcando la opción de Append Data para no sobreescribir el fichero destino en el segundo export.
De esta forma, utilizando como ejemplo una exportación de datos sin máscara, el fichero resultante tendrá el siguiente contenido:
;Data Outputted:  Attr
Como puede verse, SevenuP nos permite realizar la exportación tal y como la necesitemos en nuestras rutinas de impresión. Nosotros utilizaremos principalmente los 3 formatos que acabamos de ver, pero otras rutinas pueden necesitar de otra organización diferente, la cual podemos lograr alterando estos parámetros.
Aconsejamos al lector la lectura del fichero README de SevenuP para conocer todas sus funciones y la forma de utilizarlas.
Por otra parte, si no nos resultase cómodo trabajar con SevenuP como editor, podemos utilizarlo simplemente como exportador de datos. Basta con utilizar un programa de dibujo clásico (xpaint, kpaint, MSPaint, PaintShop Pro, o incluso The GIMP o Photoshop) y dibujar nuestros sprites en formato tileset vertical. Debemos utilizar los colores de la paleta del Spectrum y las mismas restricciones que nos encontraríamos con el sistema de atributos del Spectrum (celdillas de 8×8, 16 colores, etc.). El fichero final debe guardarse preferentemente como PNG sin compresión.
Después, utilizaremos la opción de importación de SevenuP (File → Import) para convertir la imagen de color “real” a un mapa de bits ya editable en SevenuP. Con la imagen importada en SevenuP, y tras realizar los retoques que consideremos oportunos, realizamos una exportación a código ASM con los parámetros de configuración de exportación correctos para nuestra rutina de impresión de Sprites.
Al programar esta rutina, y cualquiera de las que veremos en este capítulo, debemos decidir cómo pasar los parámetros de entrada a la misma, ya que en estas rutinas manejaremos bastantes parámetros y en alguno de los casos no tendremos suficientes registros libres para establecerlos antes del CALL.
En este sentido, podemos utilizar para el paso de los parámetros, las siguientes posibilidades:
Registros de 8 y 16 bits, allá donde sea posible, especialmente en rutinas con pocos parámetros de entrada y que sean llamadas en el programa en momentos críticos o gran cantidad de veces.
La Pila: realizando PUSH de los parámetros de entrada en un orden concreto. La rutina, en su punto inicial, hará POP de dichos valores en los registros adecuados. Tiene la ventaja de que podemos ir recuperando los valores conforme los vayamos necesitando y tras haber realizado cálculos con los parámetros anteriores que nos hayan dejado libres registros para los siguientes cálculos.
El Stack del Calculador: Este método permite que programa BASIC puedan llamar a nuestras subrutinas en ensamblador mediante DEF FN. Su principal desventaja es la lentitud y la no portabilidad a otros Sistemas.
Variables de memoria: podemos establecer los valores de entrada en variables de memoria con LD y recuperarlos dentro de la rutina también con instrucciones de carga LD. Esta técnica tiene una desventaja: la “lentitud” del acceso a memoria para lectura y escritura, pero también tiene sustanciales ventajas:
Los registros quedan libres para realizar todo tipo de operaciones.
No tenemos que preservar los valores de los parámetros de entrada al realizar operaciones con los registros, y podemos recuperarlos en cualquier otro punto de la rutina para realizar nuevos cálculos.
Nos permite llamar a las rutinas desde BASIC, estableciendo los parámetros con POKE y después realizando el RANDOMIZE USR direccion_rutina.
Lo normal en rutinas de este tipo sería utilizar en la medida de lo posible el paso mediante registros (programa en ASM puro) o mediante la pila (programas en C y ASM), pero en nuestros ejemplos utilizaremos el último método: paso de variables en direcciones de memoria, con el objetivo de que las rutinas puedan llamarse desde BASIC y para que sean lo suficientemente sencillas de leer para que cada programador pueda adaptar la entrada de una rutina concreta a las necesidades de su programa utilizando otro de los métodos de paso de parámetros descrito.
Es habitual incluso que en los programas ni siquiera se le pase a las rutinas las direcciones origen de Sprites y Atributos sino que dichas direcciones estén “hardcodeadas” dentro del código de la rutina, utilizando un único array de tiles y otro de atributos con unas direcciones concretas y exactas. Este podría ser un paso básico de optimización que evitaría carga y paso de parámetros y nos permitiría recuperar direcciones de origen de los arrays en cualquier punto de la rutina.
Por otra parte, el paso de parámetros es sólo un pequeño porcentaje del tiempo total de la rutina: el interés de optimización residirá en la impresión del sprite en sí, ya que esta recogida de parámetros se realiza una sola vez por sprite independientemente del número de bloques que lo compongan (en cuyo dibujado será donde realicemos el mayor gasto de tiempo).
Comencemos con la primera rutina de impresión de sprites: trazaremos un sprite de 8×8 píxeles (1 bloque) en una dirección de baja resolución (c,f) de la pantalla. Este tipo de Sprite es parecido a lo que en BASIC se denomina “UDG” (User Defined Graphic), pero sin las limitaciones en el número de UDGs posibles a definir.
El sprite a utilizar como “modelo” para desarrollar la rutina y para un posterior ejemplo de aplicación de la misma será el siguiente:
El bitmap con el que se corresponde este sprite y los valores binarios de los scanlines son los siguientes:
Pixeles     Binario    Decimal
---***--  =  00011100  =  28
--*****-  =  00111110  =  62
-**-*-**  =  01101011  =  107
-*******  =  01111111  =  127
-*-***-*  =  01011101  =  93
-**---**  =  01100011  =  99
Efectivamente, si exportamos el sprite en SevenuP con los parámetros “X Char, Char line, Y Char, Mask”, y exportando por un lado el gráfico y por otro los atributos, obtenemos el siguiente fichero fuente:
;Pixel Size:      (  8,   8) - (1,   1)
;Sort Priorities: Char line
cara_gfx:
DEFB  28, 62,107,127, 93, 99, 62, 28
cara_attrib:
DEFB  56
La primera de las rutinas que veremos realizará transferencias directas de datos entre el origen (el sprite) y el destino (la pantalla).
Para nuestra rutina utilizaremos el siguiente esquema de paso de parámetros:
50000 Dirección de la tabla de Sprites
50002 Dirección de la tabla de Atributos
50004 Coordenada X en baja resolución
50005 Coordenada Y en baja resolución
50006 Numero de sprite a dibujar (0-N)
El pseudocódigo de la rutina es el siguiente:
; Recoger parametros de entrada
; Calcular posicion origen (array sprites) en DE como
;     direccion = base_sprites + (NUM_SPRITE*8)
; Calcular posicion destino (pantalla) en DE con X e Y
; Repetir 8 veces:
;    Dibujar scanline (DE) -> (HL)
;    Incrementar scanline del sprite (DE).
;    Bajar a siguiente scanline en pantalla (HL).
; Si base_atributos == 0 -> RET
; Calcular posicion origen de los atributos array_attr+NUM_SPRITE en HL.
; Calcular posicion destino en area de atributos en DE.
; Copiar (HL) en (DE)
La rutina debe de comenzar calculando la direcciones origen y destino para las transferencias de datos.
La dirección origen es el primer scanline del sprite a dibujar. Dada una tabla de sprites 8×8 en formato vertical, y teniendo en cuenta que cada sprite 8×8 ocupa 8 bytes (1 byte por cada scanline, 8 scanlines), nos posicionaremos en el sprite correcto “saltando” 8 bytes por cada sprite que haya antes del sprite que buscamos:
DIR_MEMORIA_GFX = DIR_BASE_SPRITES + (NUM_SPRITE_A_DIBUJAR * 8)
La dirección destino en pantalla la calculamos a partir de las coordenadas C, F (x/8,y/8) con las técnicas que vimos en el capítulo anterior; en este caso, mediante coordenación en baja resolución.
Una vez tenemos la dirección origen y destino para los gráficos, realizamos un bucle de 8 iteraciones (por haber 8 scanlines) que haga lo siguiente:
Copiamos el byte apuntado por DE (scanline de sprite) a la dirección apuntada por HL (pantalla).
Incrementamos DE (siguiente scanline del sprite).
Cambiamos HL para que apunte en pantalla al scanline de debajo del actual (sumando 1 a la parte alta).
Tras las 8 iteraciones tendremos el gráfico completo dibujado en pantalla, y faltará el procesado de los atributos.
La misma rutina que vamos a crear servirá para dibujar gráficos con atributos o sin atributos. Nos puede interesar el dibujado de gráficos sin atributos en juegos monocolor donde los atributos de fondo ya están establecidos en pantalla y no sea necesario re-escribirlos, ahorrando ciclos de reloj al no realizar esta tarea sin efectos en pantalla. Nótese que aunque no dibujemos los atributos de un sprite, esto no quiere decir que no tendrá color en pantalla: si no modificamos los atributos de una posición (c,f), el sprite que dibujemos en esas coordenadas adoptará los colores de tinta y papel que ya tenía esa posición de pantalla.
En lugar de crear 2 rutinas diferentes, una que imprima un sprite con atributos y otra que lo haga sin ellos, vamos a utilizar en este capítulo una única rutina indicando en el parámetro de la dirección de atributos si queremos dibujarlos o no. La rutina comprobará si la dirección de atributos es 0 (basta con comprobar su parte alta) y si es así, saldrá con un RET sin realizar los cálculos de atributos (dirección origen, destino, y dibujado).
Utilizaremos este sistema de comprobación (DIR_ATTR==0) para evitar la necesidad de crear 2 rutinas diferentes, aunque en un juego lo normal será que adaptemos la rutina al caso concreto y exacto (SIN o CON atributos) para evitar la comprobación de DIR_ATTR=0 y cualquier otro código no necesario.
Continuemos: Si DIR_ATRIBUTOS (50002) no es cero, se utilizará dicha dirección y el número del sprite para calcular la posición del único atributo que tenemos que trazar en pantalla (sprite 8×8 = 1 único atributo). Como cada sprite tiene un atributo de 1 byte, esta dirección origen será:
DIR_MEMORIA_ATTR = DIR_BASE_ATTRIBS + NUM_SPRITE_A_DIBUJAR
Es decir, saltamos 1 byte (un atributo) por cada sprite 8×8 (1 bloque=1 atributo) para posicionarnos en el atributo del sprite DS_NUMSPR.
La dirección destino en el área de atributos se calcula con las rutinas que vimos en el capítulo anterior, y la transferencia destino se realiza con instrucciones de carga.
Los cálculos y la transferencia de atributos los haremos usando HL como puntero origen y DE como puntero destino, ya que necesitamos realizar ADDs de 16 bits para calcular la dirección origen del atributo y el único registo que soporta esta operación como destino es HL.
Hemos asumido que nuestra rutina trabaja con una tabla de sprites en formato vertical, pero en nuestro ejemplo tenemos un único sprite. Esto no importa porque tener un único sprite es un subcaso de la tabla de sprites donde NUM_SPRITE=0, por lo que las mismas rutinas nos sirven si les solicitamos imprimir el sprite número “0”.
; DrawSprite_8x8_LD:
; Imprime un sprite de 8x8 pixeles con o sin atributos.
; Direccion   Parametro
; 50000       Direccion de la tabla de Sprites
; 50002       Direccion de la tabla de Atribs  (0=no atributos)
; 50004       Coordenada X en baja resolucion
; 50005       Coordenada Y en baja resolucion
; 50006       Numero de sprite a dibujar (0-N)
DrawSprite_8x8_LD:
; Guardamos en BC la pareja (x,y) -> B=COORD_Y y C=COORD_X
LD BC, (DS_COORD_X)
LD D, A           ; Ya tenemos la parte alta calculada (010TT000)
LD A, B           ; Ahora calculamos la parte baja
RRCA              ; A = NNN00000b
ADD A, C          ; Sumamos COLUMNA -> A = NNNCCCCCb
LD E, A           ; Lo cargamos en la parte baja de la direccion
; DE contiene ahora la direccion destino.
LD BC, (DS_SPRITES)
LD A, (DS_NUMSPR)
LD L, A           ; HL = DS_NUMSPR
ADD HL, HL        ; HL = HL * 2
ADD HL, HL        ; HL = HL * 4
ADD HL, HL        ; HL = HL * 8 = DS_NUMSPR * 8
ADD HL, BC        ; HL = BC + HL = DS_SPRITES + (DS_NUMSPR * 8)
; HL contiene la direccion de inicio en el sprite
EX DE, HL         ; Intercambiamos DE y HL (DE=origen, HL=destino)
;;; Dibujar 8 scanlines (DE) -> (HL) y bajar scanline
;;; Incrementar scanline del sprite (DE)
LD B, 8          ; 8 scanlines -> 8 iteraciones
drawsp8x8_loopLD:
LD A, (DE)       ; Tomamos el dato del sprite
INC DE           ; Incrementamos puntero en sprite
INC H            ; Incrementamos puntero en pantalla (scanline+=1)
DJNZ drawsp8x8_loopLD
;;; En este punto, los 8 scanlines del sprite estan dibujados.
SUB 8              ; Recuperamos la posicion de memoria del
LD B, A            ; scanline inicial donde empezamos a dibujar
LD C, L            ; BC = HL - 8
;;; Considerar el dibujado de los atributos (Si DS_ATTRIBS=0 -> RET)
LD HL, (DS_ATTRIBS)
XOR A              ; A = 0
ADD A, H           ; A = 0 + H = H
RET Z              ; Si H = 0, volver (no dibujar atributos)
LD A, B            ; Codigo de Get_Attr_Offset_From_Image
RRCA               ; Obtenemos dir de atributo a partir de
RRCA               ; dir de zona de imagen.
RRCA               ; Nos evita volver a obtener X e Y
AND 3              ; y hacer el calculo completo de la
OR $58             ; direccion en zona de atributos
LD E, C            ; DE tiene el offset del attr de HL
LD A, (DS_NUMSPR)  ; Cogemos el numero de sprite a dibujar
ADD HL, BC         ; HL = HL+DS_NUMSPR = Origen de atributo
;;; Copiar (HL) en (DE) -> Copiar atributo de sprite a pantalla
LD (DE), A         ; Mas rapido que LDI (7+7 vs 16 t-estados)
RET                ; porque no necesitamos incrementar HL y DE
Al respecto del código de la rutina, caben destacar las siguientes consideraciones:
Nótese que en la rutina se emplean las subrutinas Get_Char_Offset_LR y Attr_Offset_From_Image con el código de las mismas embebido dentro de la rutina principal. Esto se hace con el objetivo de evitar los correspondientes CALLs y RET y para poder personalizarlas (en este caso, están modificadas para devolver la dirección calculada en DE en lugar de en HL, por requerimientos del código de DrawSprite_8x8).
Para realizar la transferencia de datos entre el sprite (apuntado por DE) y la pantalla (apuntada por HL) hemos utilizado 2 instrucciones de transferencia LD usando A como registro intermedio en lugar de utilizar una instrucción LDI. Más adelante veremos el por qué de esta elección.
Como ya vimos en el capítulo anterior, para avanzar o retroceder el puntero HL en pantalla, en lugar de utilizar DEC HL o INC HL (6 t-estados), realizamos un DEC L o INC L (4 t-estados). Esto es posible porque dentro de un mismo scanline de pantalla no varía el valor del byte alto de la dirección. Esta pequeña optimización no podemos realizarla con el puntero de datos del Sprite porque no tenemos la certeza de que esté dentro de una página de 256 bytes y que, por lo tanto, alguno de los incrementos del puntero deba modificar la parte alta del mismo.
La rutina que hemos visto, por simplicar el código, utiliza un bucle de 8 iteraciones para dibujar los 8 scanlines. Esto ahorra espacio (ocupación de la rutina) pero implica un testeo del contador y salto por cada iteración (excepto en la última). En una rutina crítica, si tenemos suficiente espacio libre, y si conocemos de antemano el número de iteraciones exacto de un bucle, lo óptimo sería desenrollar el bucle, es decir, repetir 8 veces el código de impresión. De este modo evitamos el LD B, 8 y el DJNZ bucle.
En el caso de nuestra rutina de ejemplo, cambiaríamos…
LD B, 8          ; 8 scanlines
… por:
LD A, (DE)       ; Scanline 0
LD A, (DE)       ; Scanline 1
LD A, (DE)       ; Scanline 2
LD A, (DE)       ; Scanline 3
LD A, (DE)       ; Scanline 4
LD A, (DE)       ; Scanline 5
LD A, (DE)       ; Scanline 6
LD A, (DE)       ; Scanline 7
;;;INC H         ; no es necesario el ultimo INC H
Nótese cómo al desenrollar el bucle ya no es necesario el último INC H para avanzar al siguiente scanline de pantalla. El INC DE sí que es necesario ya que tenemos que avanzar en el sprite al primero de los atributos (aunque este INC también podría realizarse después del código de comprobación de la dirección de atributo, evitando hacerlo si no queremos imprimirlos).
Al no ser necesario el INC H, en la versión desenrollada del bucle tenemos que cambiar la resta de HL - 8 por HL - 7:
SUB 7              ; Recuperamos la posicion de memoria del
LD C, L            ; BC = HL - 7
Lo normal es desenrollar sólo aquellas rutinas lo suficiente críticas e importantes como para compensar el mayor espacio en memoria con un menor tiempo de ejecución. En esta rutina evitaríamos la pérdida de ciclos de reloj en el establecimiento del contador, en el testeo de condición de salida y en el salto, a cambio de una mayor ocupación de espacio tras el ensamblado.
Una rutina de impresión de sprites de tamaños fijos (8×8, 16×16, 8×16, etc) en la que conocemos el número de iteraciones verticales y horizontales para la impresión es uno de los casos típicos en los que usaremos esta técnica. Si la rutina fuera para sprites de tamaño variable (NxM), no podríamos aplicarla porque necesitamos los bucles de N y M iteraciones que no podemos sustituir de antemano.
El programa de ejemplo que veremos a continuación utiliza la rutina anterior (no incluída en el listado) para imprimir el sprite de ejemplo 8×8 que hemos visto:
; Ejemplo impresion sprites 8x8
DS_SPRITES  EQU  50000
DS_ATTRIBS  EQU  50002
DS_COORD_X  EQU  50004
DS_COORD_Y  EQU  50005
DS_NUMSPR   EQU  50006
CALL ClearScreen_Pattern
; Establecemos los parametros de entrada a la rutina
; Los 2 primeros se pueden establecer una unica vez
LD HL, cara_gfx
LD (DS_SPRITES), HL
LD HL, cara_attrib
LD (DS_ATTRIBS), HL
LD (DS_COORD_X), A
LD (DS_COORD_Y), A
LD (DS_NUMSPR), A
CALL DrawSprite_8x8_LD
; ClearScreen_Pattern
; Limpia la pantalla con patrones de pixeles alternados
ClearScreen_Pattern:
LD B, 191                   ; Numero de lineas a rellenar
cs_line_loop:
CALL $22B1                  ; ROM (Pixel-Address)
JR Z, cs_es_par
JR cs_pintar
cs_es_par:
LD A, 85
cs_pintar:
LD D, B                     ; Salvar el contador del bucle
LD B, 32                    ; Imprimir 32 bytes
cs_x_loop:
DJNZ cs_x_loop
LD B, D                     ; Recuperamos el contador externo
DJNZ cs_line_loop           ; Repetimos 192 veces
;SevenuP (C) Copyright 2002-2006 by Jaime Tejedor Gomez, aka Metalbrain
;;; codigo de la rutina...
El resultado de la ejecución del anterior programa es el siguiente:
Transferencia por LDI vs LD+INC
En nuestra rutina de impresión hemos utilizado instrucciones de carga entre memoria para transferir bytes desde la dirección apuntada por DE (el origen; el Sprite) a la dirección apuntada por HL (el destino; la pantalla).
Para trazar los píxeles en pantalla podríamos haber utilizado la instrucción LDI, que con 16 t-estados realiza una transferencia de 1 byte entre la dirección de memoria apuntada por HL (origen) y la apuntada por DE (destino), y además incrementa HL y DE.
El bucle principal de impresión de nuestro programa es el siguiente:
drawsp8x8_loop:
LD A, (DE)         ; A = (DE) = leer dato del sprite
LD (HL), A         ; (HL) = A = escribir dato a la pantalla
INC DE             ; Incrementamos DE (puntero sprite)
INC H              ; Incrementamos scanline HL (HL+=256)
DJNZ drawsp8x8_loop
Las instrucciones antes del DJNZ tienen un coste de ejecución de 7, 7, 6 y 4 t-estados respectivamente (empezando por el LD A, (DE) y acabando por el INC H). Esto suma un total de 24 t-estados por cada byte transferido.
Si invertimos el uso de los punteros y utilizamos HL como puntero al Sprite (origen) y DE como puntero a pantalla (destino), el bucle anterior podría haberse reescrito de la siguiente forma:
LDI                ; Copia (HL) en (DE) y HL++ DE++
INC D              ; Sumamos 256 (+1=257)
DEC E              ; Restamos 1 (+=256)
Aunque es un formato más compacto, el coste de ejecución es el mismo (16+4+4 = 24 t-estados).
Entre las 2 posibles formas de realizar la impresión (LD+INC vs LDI), utilizaremos la primera porque, como veremos a continuación, la impresión de sprites mediante operaciones lógicas o mediante máscaras no permite el uso de LDI y utilizar la primera técnica hace todas las rutinas muy similares entre sí y por lo tanto podremos aplicar en todas cualquier mejora u optimización de una forma más rápida y sencilla.
Además, LDI decrementa el registro BC tras las transferencia, por lo que si lo utilizamos en un bucle tenemos que tener en cuenta que cada LDI puede alterar el valor de BC y por tanto del contador de iteraciones del bucle, lo cual es otro motivo para elegir LD+INC vs LDI.
Veamos una ampliación del Sprite de 8×8 del ejemplo anterior impreso sobre un fondo no plano:
Nótese cómo la impresión del sprite no ha respetado el fondo en los píxeles a cero del mismo: al establecer el valor del byte en pantalla con un LD, hemos establecido a cero en pantalla los bits que estaban a cero en el sprite sin respetar el valor que hubiera en videoram para dichos bits.
Valor en Videomemoria (HL):     10101010
Valor en el sprite - reg. A:    00111110
Operación:                      LD (HL), A
Resultado en VRAM:              00111110
La primera de las soluciones a este problema es la de escribir los píxeles con una operación lógica OR entre el scanline y el valor actual en memoria. Con la operación OR mezclaremos los bits de ambos elementos:
Operación:                      LD + OR/XOR
Resultado en VRAM:              10111110
La operación OR nos permitirá respetar el fondo en aquellos juegos en que los sprites no tengan zonas a 0 dentro del contorno del mismo (que, como veremos más adelante, no es el caso de nuestro pequeño sprite).
Para modificar la rutina de impresión de Sprites de forma que utilice OR (u otra operación lógica con otras aplicaciones como XOR), sólo necesitamos añadir la correspondiente instrucción lógica entre A y el contenido de la memoria:
Cambiamos el bucle de impresión…
LD B, 8             ; 8 scanlines -> 8 iteraciones
INC H            ; Incrementamos puntero en pantalla
drawsp8x8_loop_or:
LD A, (DE)          ; Tomamos el dato del sprite
OR (HL)             ; NUEVO: Hacemos un OR del scanline con el fondo
LD (HL), A          ; Establecemos el valor del OR en videomemoria
INC DE              ; Incrementamos puntero en sprite (DE+=1)
INC H               ; Incrementamos puntero en pantalla (HL+=256)
DJNZ drawsp8x8_loop_or
A continuación, el código fuente completo de la rutina de impresión de 8×8 con operación lógica OR:
; DrawSprite_8x8_OR:
DrawSprite_8x8_OR:
OR (HL)          ; NUEVO: Hacemos un OR del scanline con el fondo
(Nótese que el bucle de impresión de scanlines también puede, y debería, ser desenrollado).
Veamos qué ha ocurrido con los píxeles del fondo y la operación lógica OR:
Ampliando el sprite…
¿Qué ha ocurrido con el sprite? ¿Por qué le faltan los “ojos” y hay un pixel activo en medio de la “boca”? Sencillamente, porque mediante el OR hemos impreso el sprite respetando el valor a 1 de los píxeles del fondo cuando el mismo pixel estaba a 0 en nuestro sprite. Eso ha hecho que alrededor de nuestro “personaje” no se haya borrado el fondo, ya que los píxeles a cero de nuestro sprite se convierten en “transparentes”. Por desgracia, eso también hace que los ojos del personaje sean transparentes en lugar de estar a cero. En el caso del ejemplo anterior, los “ojos” del personaje coinciden con 2 píxeles de pantalla activos por lo que la operación OR los deja a 1. Lo mismo ocurre con el pixel en el centro de la boca, que se corresponde con un pixel activo en la pantalla.
En tal caso, ¿qué hacemos para imprimir nuestro sprite respetando el fondo pero que a su vez podamos disponer de zonas que no sean transparentes?
La respuesta es: mediante máscaras.
Como hemos visto, las operaciones con OR nos permiten respetar el fondo pero a su vez provocan zonas transparentes en nuestro sprite. En sistemas más modernos se utiliza un “color transparente” (que suele ser unas componentes concretas de color con un tono específico de rosa fucsia). En el caso del Spectrum, el color reside en los atributos y no en los sprites en sí, por lo que no podemos utilizar un “color transparencia”.
La solución para respetar el fondo y sólo tener transparencias en los sprites en las zonas que nosotros deseemos es la utilización de máscaras.
La máscara es un bitmap del mismo tamaño que el sprite al que está asociada. Tiene un contorno similar al del sprite, donde colocamos a 1 todos los pixeles del fondo que necesitamos respetar (zonas transparentes) y a 0 todos los píxeles que se transferirán desde el sprite (píxeles opacos o píxeles del sprite) y que deben de ser borrados del fondo.
A la hora de dibujar el sprite en pantalla, se realiza un AND entre el valor del pixel y el valor del fondo (de los 8 píxeles, en el caso del Spectrum), con lo cual “borramos” del fondo todos aquellos píxeles a cero en la máscara. Después se realiza un OR del Sprite en el resultado del anterior AND, activando los píxeles del Sprite.
De esta forma, podemos respetar todos los píxeles del fondo alrededor de la figura del personaje, así como algunas zonas de mismo que podamos querer que sean transparentes, mientras que borramos todos aquellos píxeles de pantalla que deben de ser reemplazados por el sprite.
Apliquemos una máscara a nuestro ejemplo anterior. Al hacer el AND entre la máscara y el fondo (paso 1.-), eliminamos el entramado de pixeles de pantalla, con lo que al imprimir nuestro sprite con el OR (paso 2.-), los ojos y el centro de la boca serán de nuevo píxeles no activos:
El resultado es que los ojos y la boca del sprite, que en la máscara están a 0, son borrados del fondo y por lo tanto no se produce el efecto de “transparencia” que presentaba el dibujado con OR.
No obstante, nuestro sprite sigue teniendo un problema relacionado con el sistema de atributos del Spectrum, y es que los píxeles de nuestro personaje se “confunden” con los del fondo en los contornos del sprite, ya que todos tendrán idéntico color si están dentro de una misma celda 8×8. Para evitar esto, lo ideal sería disponer de un “borde” alrededor del mismo.
Podemos aprovechar la máscara (no en nuestro sprite de 8×8, pero sí en sprites de mayores dimensiones), para dotar de un “reborde” a nuestra figura y que los píxeles del sprite no se confundan con los del fondo. Basta con hacer el contorno en la máscara más grande que el contorno del sprite, de esta forma, los bytes de contorno “extra” de la máscara borrarán fondo alrededor del Sprite, con lo que no se producirá la “confusión” con el fondo.
La siguiente imagen ilustra lo que acabamos de comentar:
Así pues, con la máscara podemos conseguir 2 cosas:
Evitar transparencias en el interior de nuestro Sprite: bastará con que la máscara sea una copia del contorno del sprite, pero “relleno”, por lo que todos los pixeles del sprite serán transferidos dentro de dicho contorno.
Conseguir un contorno de pixeles “a cero” alrededor de nuestro Sprite, un borde que lo haga visualmente más identificable y no mezcle sus píxeles con los de la pantalla. Para eso, basta con que el contorno del sprite en la máscara sea ligeramente más grande que el del sprite.
Por contra, el uso de máscaras tiene también desventajas:
El uso de máscaras requiere utilizar el doble de memoria para el spriteset gráfico (no para el de atributos), ya que por cada byte del sprite necesitamos el correspondiente byte de máscara.
Las rutinas de impresión con máscaras son más lentas que sin ellas, porque tienen que recoger datos de la máscara, realizar operaciones lógicas entre los datos de la misma y el fondo, y realizar incrementos adicionales del puntero al sprite (DE).
La rutina de impresión de máscaras tiene ahora que recoger un dato extra por cada byte del sprite: la máscara que se corresponde con ese byte. Para no utilizar un puntero de memoria adicional en un array de máscara, lo ideal es exportar cada byte de máscara junto al byte del sprite correspondiente, de forma que podamos recoger ambos valores usando el mismo puntero (DE en nuestro caso). Para eso habría que exportar el Sprite como Mask, X Char, Char line, Y Char y activando “Mask before graphic”.
El pseudocódigo de la nueva rutina DrawSprite_8x8_MASK sería el siguiente:
; Calcular posicion destino en HL con X e Y
; Calcular posicion origen en DE base_sprites + (frame*8*2) (*2 -> por la mascara)
;    Coger dato de mascara
;    AND del byte de mascara con el byte actual de fondo.
;    Coger dato de scanline del sprite (DE++)
;    Dibujar scanline en pantalla como un OR sobre el resultado del AND.
;    Incrementar scanline sprite/mascara en DE.
;    Bajar a siguiente scanline en pantalla.
La rutina completa sería:
; DrawSprite_8x8_MASK:
; Imprime un sprite de 8x8 pixeles + mascara con o sin atributos.
DrawSprite_8x8_MASK:
;;;     direccion = base_sprites + (NUM_SPRITE*16)
ADD HL, HL        ; HL = HL * 8
ADD HL, HL        ; HL = HL * 16 = DS_NUMSPR * 16
ADD HL, BC        ; HL = BC + HL = DS_SPRITES + (DS_NUMSPR * 16)
EX DE, HL         ; Intercambiamos DE y HL para el OR
;;; Dibujar 8 scanlines (DE) -> (HL) + bajar scanline y avanzar en SPR
drawspr8x8m_loop:
LD A, (DE)       ; Obtenemos un byte del sprite (el byte de mascara)
AND (HL)         ; A = A AND (HL)
LD C, A          ; Nos guardamos el valor del AND
INC DE           ; Avanzamos al siguiente byte (el dato grafico)
LD A, (DE)       ; Obtenemos el byte grafico
OR C             ; A = A OR C = A OR (MASK AND FONDO)
LD (HL), A       ; Imprimimos el dato tras aplicar operaciones logicas
INC DE           ; Avanzamos al siguiente dato del sprite
INC H            ; Incrementamos puntero en pantalla (siguiente scanline)
DJNZ drawspr8x8m_loop
La rutina que acabamos de ver presenta los siguientes cambios respecto a las rutinas con LD y OR:
La primera modificación es el cálculo de la dirección de origen. Antes cada sprite ocupaba 8 bytes por lo que teníamos que calcular la dirección origen en el array de Sprites así:
Pero ahora cada sprite tiene por cada byte gráfico un byte de máscara, ocupando 16 bytes en lugar de 8. El cálculo debe adaptarse pues a:
DIR_MEMORIA_GFX = DIR_BASE_SPRITES + (NUM_SPRITE_A_DIBUJAR * 16)
El código para el cálculo agrega un “ADD HL, HL” adicional para esta tarea:
La segunda modificación principal en la rutina es la impresión de cada dato del sprite en sí. En nuestros sprites con máscara cada bloque de 8 píxeles (1 byte) viene precedido del byte de la máscara, por lo que debemos recoger ésta, hacer el AND de la máscara con el fondo, incrementar DE, recoger el dato gráfico y hacer el OR de éste con el resultado anterior:
No olvidemos que el bucle de operación lógica de la máscara e impresión del scanline puede, como en los anteriores casos, ser desenrollado.
Veamos su aplicación en el ejemplo de nuestro pequeño sprite sonriente. En SevenuP, activamos la máscara en Mask → Use Mask y cambiamos al modo “Ver máscara” con Mask → View Mask. Podemos volver al modo de visualización de sprite de nuevo con Mask → View Mask.
En el módo de visualización de máscara, podemos generar la máscara de nuestro sprite produciendo una versión invertida del mismo. Para ayudarnos en esa tarea, SevenuP nos muestra con diferentes colores el estado de los píxeles del sprite, e incluso tiene una opción de “AutoMask” que generará una máscara básica como inversión del sprite para comenzar a trabajar con ella:
La máscara de nuestro pequeño Sprite en SevenuP
En la anterior imagen, tenemos en “negro” los píxeles a respetar en el fondo, y en blanco y amarillo (dentro de la imagen) los píxeles a borrar. SevenuP nos marca en diferente color los píxeles a cero de la máscara que coinciden con los del sprite.
Exportando a ASM este sprite con los parámetros de Prioridad: Mask, X Char, Char line, Y Char y activando “Mask before graphic” obtenemos los siguientes array de datos (con 2 exportaciones por separado de Gfx y Attr):
;Sort Priorities:      Mask, X Char, Char line, Y Char
;Con ancho=1, aparece: Mask, Char line
DEFB   227, 28, 193, 62, 128, 107, 128, 127
DEFB   128, 93, 128, 99, 193,  62, 227, 28
DEFB   56
Veamos el resultado del mismo ejemplo que hemos usado hasta ahora (fondo con patrón de píxeles alternados), pero con la rutina de impresión con máscaras. El código es igual a los 2 ejemplos anteriores, pero llamando a DrawSprite_8x8_MASK:
Ampliando la anterior captura de pantalla en la zona del sprite, podemos apreciar que los ojos y la boca de nuestro personaje ya se visualizan correctamente:
Nuestro pequeño sprite se vería mucho mejor con un reborde vacío alrededor, ya que hay píxeles del fondo pegados a nuestro personaje. Por desgracia, en un sprite de 8×8 como el nuestro apenas nos queda espacio para este reborde en la máscara, pero se podría haber aplicado si el sprite fuera de mayores dimensiones.
Un apunte final sobre los ejemplos que hemos visto: nótese que estamos llamando a todas las rutinas asignando DS_NUMSPR=0, para imprimir el “primer” (y único sprite en nuestro caso) del spriteset. El Spriteset podría tener hasta 255 sprites dispuestos verticalmente (con sus máscaras intercaladas) y esta misma rutina, sin modificaciones, nos serviría para dibujar cualquiera de ellos variando DS_NUMSPR.
La impresión de sprites de 16×16 sin máscaras es esencialmente idéntica a la de 8×8, ajustando las funciones que hemos visto hasta ahora a las nuevas dimensiones del sprite:
El cálculo de la dirección origen en el sprite cambia, ya que ahora cada scanline ocupa 2 bytes y no 1, y tenemos 2 bloques de altura en el sprite y no uno. Antes calculábamos la dirección origen como BASE+(DS_NUMSPR*8), pero ahora tendremos que avanzar 8*2*2=32 bytes por cada sprite en los sprites sin máscara. El cálculo quedaría como BASE+(DS_NUMSPR*32).
Para multiplicar DS_NUMSPR por 32 vamos a utilizar desplazamientos a la derecha de un pseudo-registro de 16 bits formado por A y L en lugar de utilizar sumas sucesivas ADD HL, HL. Esta técnica requiere menos ciclos de reloj para su ejecución.
La impresión de datos debe imprimir todo un scanline horizontal del Sprite (2 bytes) antes de avanzar al siguiente scanline de pantalla.
El avance al siguiente scanline cambia ligeramente, ya que necesitamos incrementar HL (concretamente, L) para poder imprimir el segundo byte horizontal.
El bucle vertical es de 16 iteraciones en lugar de 8, ya que el sprite tiene 2 caracteres verticales.
El cálculo de la dirección origen en los atributos cambia, ya que ahora tenemos 4 bytes de atributos por cada sprite. Así, la posición ya no se calcula como BASE+(DS_NUMSPR*1) sino como BASE+(DS_NUMSPR*4).
La impresión de los atributos también cambia, ya que ahora hay que imprimir 4 atributos (2×2 bloques) en lugar de sólo 1.
El pseudocódigo de la rutina que tenemos que programar sería el siguiente:
;     direccion = base_sprites + (NUM_SPRITE*32)
; Calcular posicion destino (pantalla) en HL con X e Y
;    Dibujar byte (DE) -> (HL), trazando el scanline del 1er bloque
;    Incrementar HL y DE
;    Dibujar byte (DE) -> (HL), trazando el scanline del 2o bloque
;    Incrementar DE
;    Bajar a siguiente scanline en pantalla (HL), sumando 256 (INC H/DEC L).
; Avanzar puntero de pantalla (HL) a la posicion de la segunda
; fila de bloques a trazar
; Calcular posicion origen de los atributos array_attr+(NUM_SPRITE*4) en HL.
; Dibujar los atributos (2 filas de 2 atributos, 4 bytes en total):
; Incrementar HL y DE
; Incrementar HL
; Avanzar a la siguiente línea de atributos en pantalla (DE+=32)
El código completo de la rutina de impresión en 2×2 quedaría, pues, como sigue:
; DrawSprite_16x16_LD:
; Imprime un sprite de 16x16 pixeles con o sin atributos.
DrawSprite_16x16_LD:
PUSH DE           ; Lo guardamos para luego, lo usaremos para
; calcular la direccion del atributo
;;;     direccion = base_sprites + (NUM_SPRITE*32)
;;; Multiplicamos con desplazamientos, ver los comentarios.
LD L, 0           ; AL = DS_NUMSPR*256
SRL A             ; Desplazamos a la derecha para dividir por dos
RR L              ; AL = DS_NUMSPR*128
RRA               ; Rotamos, ya que el bit que salio de L al CF fue 0
RR L              ; AL = DS_NUMSPR*64
RR L              ; AL = DS_NUMSPR*32
LD H, A           ; HL = DS_NUMSPR*32
ADD HL, BC        ; HL = BC + HL = DS_SPRITES + (DS_NUMSPR * 32)
;;; Repetir 8 veces (primeros 2 bloques horizontales):
drawsp16x16_loop1:
LD A, (DE)         ; Bloque 1: Leemos dato del sprite
LD (HL), A         ; Copiamos dato a pantalla
INC DE             ; Incrementar puntero en sprite
INC L              ; Incrementar puntero en pantalla
LD A, (DE)         ; Bloque 2: Leemos dato del sprite
INC H              ; Hay que sumar 256 para ir al siguiente scanline
DEC L              ; pero hay que restar el INC L que hicimos.
DJNZ drawsp16x16_loop1
; Avanzamos HL 1 scanline (codigo de incremento de HL en 1 scanline)
; desde el septimo scanline de la fila Y+1 al primero de la Y+2
;;;INC H           ; No hay que hacer INC H, lo hizo en el bucle
;;;LD A, H         ; No hay que hacer esta prueba, sabemos que
;;;AND 7           ; no hay salto (es un cambio de bloque)
;;;JR NZ, drawsp16_nofix_abajop
JR C, drawsp16_nofix_abajop
drawsp16_nofix_abajop:
;;; Repetir 8 veces (segundos 2 bloques horizontales):
drawsp16x16_loop2:
DJNZ drawsp16x16_loop2
;;; En este punto, los 16 scanlines del sprite estan dibujados.
POP BC             ; Recuperamos el offset del primer scanline
ADD HL, BC         ; HL = HL+DS_NUMSPR
ADD HL, BC         ; HL = HL+DS_NUMSPR*2
ADD HL, BC         ; HL = HL+DS_NUMSPR*3
ADD HL, BC         ; HL = HL+HL=(DS_NUMSPR*4) = Origen de atributo
LDI                ; Imprimimos las 2 primeras filas de atributo
;;; Avance diferencial a la siguiente linea de atributos
LD A, E            ; A = L
ADD A, 30          ; Sumamos A = A + 30 mas los 2 INCs de LDI.
LD E, A            ; Guardamos en L (L = L+30 + 2 por LDI=L+32)
JR NC, drawsp16x16_attrab_noinc
drawsp16x16_attrab_noinc:
Lo primero que nos llama la atención de la rutina es la forma de multiplicar por 32 el valor de DS_NUMSPR. Una primera aproximación de multiplicación de HL = NUM_SPR * 32 podría ser aumentar el número de sumas ADD HL, HL tal y como se realizan en las rutinas de 8×8:
;;; Multiplicar DS_SPRITES por 32 con sumas
LD H, 0           ; H = 0
ADD HL, HL        ; HL = HL * 16
ADD HL, HL        ; HL = HL * 32
ADD HL, BC        ; HL = DS_SPRITES + (DS_NUMSPR * 32)
Esta porción de código tarda 11 t-estados por cada ADD de 16 bits, más 7 t-estados de LD H, 0, más 4 de LD L, A, lo que da un total de 77 t-estados para realizar la multiplicación.
La técnica empleada en el listado, proporcionada por metalbrain, implica cargar el valor de DS_NUMSPR en la parte alta de un registro de 16 bits (con lo que el registro tendría el valor de DS_NUMSPR*256, como si lo hubieramos desplazado 8 veces a la izquierda), y después realizar desplazamientos de 16 bits a la derecha, dividiendo este valor por 2 en cada desplazamiento. Con un desplazamiento, obtenemos en el registro el valor DS_NUMSPR * 128, con otro desplazamiento DS_NUMSPR * 64, y con otro más DS_NUMSPR * 32.
Los desplazamientos los tenemos que realizar con el registro A (mas rápidos), por lo que utilizaremos el par de registros A + L para realizar la operación y finalmente cargar el resultado en HL:
;;; Multiplicar DS_SPRITES por 32 con desplazamientos >>
Esta porción de código tiene un coste de 11 (ADD) + 8 + 8 + 8 (RR) + 4 + 4 + 4 (RRA) + 7 + 4 (LD) = 58 t-estados, 29 ciclos de reloj menos que la rutina con ADD.
Si tuvieramos que multiplicar por 64 (realizar un RRA/RL menos), el coste sería todavía menor: 12 t-estados menos con un total de 46 t-estados. En el caso de las rutinas de 8×8, resultaba más rápido realizar la multiplicación por medio de sumas que por desplazamientos, con un coste total de 55 t-estados.
Otra parte interesante de la rutina de dibujado de sprites está en la impresión de los datos gráficos. En esta ocasión hay que imprimir 2 bytes horizontales en cada scanline, y sumar 256 para avanzar a la siguiente línea de pantalla. Esto nos obliga a decrementar HL en 1 unidad (con DEC L) para compensar el avance horizontal utilizado para posicionarnos en el lugar de dibujado del segundo bloque del sprite. Tras esto, ya podemos hacer el avance de scanline con un simple INC H (HL=HL+256).
Una vez finalizado el bucle de 8 iteraciones que imprime los datos de los 2 bloques de la fila 1 del sprite, debemos avanzar al siguiente scanline de pantalla (8 más abajo de la posicion Y inicial) para trazar los 2 bloques restantes (los bloques “de abajo”). Para ello se ha insertado el código de “Siguiente_Scanline_HL” dentro de la rutina (evitando el CALL y el RET). La instrucción inicial INC H de la rutina que vimos en el capítulo anterior no es necesaria porque la ejecuta la última iteración del bucle anterior.
Tras ajustar HL tenemos que dibujar los 2 últimos bloques del sprite, con un bucle similar al que dibujó los 2 primeros.
Nótese que hemos dividido la impresión de los 16 scanlines en 2 bucles (1 para cada fila de caracteres). Cada una de las 2 filas a dibujar está dentro de una posición de carácter y para avanzar un scanline basta con incrementar la parte alta de la dirección, pero para pasar de un bloque al siguiente el código es diferente.
El bucle podría haber sido de 16 iteraciones utilizando la rutina genérica de “Avanzar HL en 1 scanline” (que funciona tanto para avanzar dentro de un carácter como para avanzar del fin de un carácter al siguiente), y habría tenido el siguiente aspecto:
LD B, 16           ; 16 iteraciones
drawsp16x16_loop:
DEC L              ; Decrementamos el avance realizado
DJNZ drawsp16x16_loop
Este código es más pequeño en tamaño que el uso de 2 bucles, pero estamos efectuando un JR innecesario en 14 de las 16 iteraciones, ya que sólo se debe chequear el caracter/tercio en el salto de un bloque de pantalla al siguiente, que sólo ocurre 1 vez en el caso de un sprite de 2×2 bloques impreso en posiciones de carácter. Además, en la última iteración es innecesario incrementar y ajustar HL, por lo que son ciclos de reloj que se malgastan.
Finalmente, a la hora de escribir los atributos cambia el cálculo de la posición origen (ahora es DS_NUMSPR*4, cuya multiplicación realizamos en base a 4 sumas), así como la copia de los 4 bytes, ya que hay que imprimir los 2 primeros (LDI + LDI), avanzar DE hasta la siguiente “fila de atributos”, y copiar los 2 siguientes (con otras 2 instrucciones LDI).
Veamos la ejecución de la rutina con un sencillo ejemplo… Primero dibujamos en SevenuP el pequeño personaje de 2×2 bloques con el que abríamos este apartado y lo exportamos a ASM:
;ASM source file created by SevenuP v1.20
;Pixel Size:      ( 16,  64)
;Char Size:       (  2,   8)
bicho_gfx:
DEFB    8,128,  4, 64,  0,  0,  7,224
DEFB   15, 80, 15, 80, 15,240,  7,224
DEFB    0,  0,  1, 64,  0, 32,  2,  0
DEFB  104, 22,112, 14, 56, 28,  0,  0
bicho_attrib:
DEFB   70, 71, 67,  3
A continuación, imprimimos el sprite mediante la siguiente porción de código:
LD HL, bicho_gfx
LD HL, bicho_attrib
LD A, 13
CALL DrawSprite_16x16_LD
Este es el aspecto del sprite impreso en pantalla sobre la trama de píxeles alternos que estamos utilizando hasta el momento:
Ampliando la zona de pantalla con el sprite:
Podemos convertir la anterior rutina fácilmente en una rutina de impresión con operaciones lógicas añadiendo el OR (HL) (o el XOR) antes de la escritura del dato en pantalla.
Simplemente, reemplazaríamos las diferentes operaciones de transferencia cambiando:
OR (HL)            ; Realizamos operación lógica
El resto de la rutina es esencialmente idéntico a la versión con transferencias de datos LD.
La rutina para dibujar sprites de 16×16 (2×2) con transparencia usando máscaras también sería una mezcla entre la rutina de 8×8 con máscara y a la de 16×16 sin máscara, con el siguiente cambio:
El cálculo de la dirección origen en el sprite cambia, ya que ahora cada scanline ocupa 4 bytes (2 del sprite y 2 de la máscara) y no 2, y tenemos 2 bloques de altura en el sprite y no uno. Como tenemos el doble de datos gráficos por cada sprite, si antes habíamos calculado la dirección origen como BASE+(DS_NUMSPR*32), ahora tendremos que calcularla como BASE+(DS_NUMSPR*64). Esta circunstancia nos ahorra un desplazamiento del pseudoregistro “AL” hacia la derecha (evitamos un RRA + RR L), lo que nos permite hacer la operación de multiplicación con sólo 46 t-estados:
; DrawSprite_16x16_MASK:
; Imprime un sprite de 16x16 pixeles + mascara con o sin atributos.
DrawSprite_16x16_MASK:
;;;     direccion = base_sprites + (NUM_SPRITE*64)
LD H, A           ; HL = DS_NUMSPR*64
ADD HL, BC        ; HL = BC + HL = DS_SPRITES + (DS_NUMSPR * 64)
EX DE, HL         ; Intercambiamos DE y HL para las OP LOGICAS
drawspr16m_loop1:
INC L            ; Avanzamos al segundo bloque en pantalla
DEC L            ; Volvemos atras del valor que incrementamos
DJNZ drawspr16m_loop1
JR NZ, drawsp16m_nofix_abajop
JR C, drawsp16m_nofix_abajop
drawsp16m_nofix_abajop:
drawspr16m_loop2:
DJNZ drawspr16m_loop2
JR NC, drawsp16m_attrab_noinc
drawsp16m_attrab_noinc:
Datos a destacar sobre el código:
Al igual que en el caso de la rutina con LD y con OR, el bucle de 16 iteraciones de la rutina de impresión con máscaras, se debería desenrollar si el trazado de sprites es una rutina prioritaria en nuestro programa (que suele ser el caso, especialmente en juegos). No obstante, hay que tener en cuenta que desenrollar estos 2 bucles supone añadir a la rutina 14 veces el tamaño de cada iteración (hay 16 iteraciones, en el código hay 2 de ellas repetidas 8 veces, por lo que en desenrollamiento añadiría 14 veces el código de impresión). Esto puede suponer un problema cuando los programas son grandes y nos acercamos a los límites de la memoria del Spectrum.
Probemos la rutina con un spriteset de múltiples sprites de 2×2 bloques con su máscara:
Exportados con los parámetros adecuados, nos queda el siguiente resultado en forma de array:
DEFB	247,  8,127,128,251,  4,191, 64, 255,  0,255,  0,248,  7, 31,224
DEFB	240, 15, 15, 80,240, 15, 15, 80, 240, 15, 15,240,248,  7, 31,224
DEFB	255,  0,255,  0,254,  1,191, 64, 255,  0,223, 32,253,  2,255,  0
DEFB	151,104,233, 22,143,112,241, 14, 199, 56,227, 28,255,  0,255,  0
DEFB	251,  4,191, 64,255,  0,255,  0, 248,  7, 31,224,240, 15, 15, 80
DEFB	240, 15, 15, 80,240, 15, 15,240, 248,  7, 31,224,255,  0,255,  0
DEFB	255,  0,255,  0,254,  1,191, 64, 255,  0,255,  0,253,  2,223, 32
DEFB	247,  8,255,  0,241, 14,143,112, 248,  7,135,120,255,  0,255,  0
DEFB	253,  2,223, 32,255,  0,255,  0, 252,  3, 15,240,248,  7,  7,168
DEFB	248,  7,  7,168,248,  7,  7,248, 252,  3, 15,240,255,  0,255,  0
DEFB	255,  0,255,  0,254,  1,191, 64, 255,  0,255,  0,254,  1,191, 64
DEFB	255,  0,255,  0,253,  2, 31,224, 250,  5, 15,240,255,  0,255,  0
DEFB	254,  1,239, 16,253,  2,223, 32, 255,  0,255,  0,252,  3, 15,240
DEFB	248,  7,  7,168,248,  7,  7,168, 248,  7,  7,248,252,  3, 15,240
DEFB 70, 71, 67,  3, 70, 71, 67,  3, 70, 71,  3, 67, 70, 71,  3, 67
La siguiente captura muestra la impresión de uno de los sprites de 2×2 bloques del tileset usando su correspondiente máscara sobre nuestro fondo de píxeles alternos:
En ella, resulta evidente el problema del colour clash o colisión de atributos.
Por desgracia, debido a las características de color en baja resolución del Spectrum, el uso de máscaras con sprites multicolor sobre fondos con patrones provoca este tipo de resultados. Los sprites multicolor con máscara deben utilizarse en otras circunstancias / formatos de juego.
Por ejemplo, si el juego fuera monocolor y no imprimieramos los atributos del sprite, el resultado sería mucho más agradable a la vista:
El resultado en esta ocasión es mucho mejor, aunque este sprite sigue necesitando claramente un reborde generado vía máscara para una impresión mucho más agradable a la vista, aún en un fondo tan “desfavorable” como es este patrón de píxeles alternos.
Y es que, como vamos a ver a continuación, cada técnica de impresión tiene una situación de aplicación válida.
Hemos visto en este capítulo 3 técnicas diferentes de impresión de Sprites, las cuales nos dan 4 posibilidades:
Impresión con transferencia (LD/LDI):
La impresión por transferencia directa de datos (sprite → pantalla) es adecuada para juegos con movimiento “bloque a bloque” (en baja resolución), donde todos los sprites (y el mapa de juego) se mueven en la rejilla de 32×24 bloques del Spectrum (juegos de puzzle, rogue-likes, juegos basados en mapas de bloques, etc).
Tetris, Plotting, Maziacs y Boulder Dash
En estos juegos no suele ser necesario tener transparecia con el fondo (al ser plano) y no coinciden 2 sprites en la misma cuadrícula (si lo hacen, es, por ejemplo, para recoger un objeto).
Impresión por operación lógica OR:
La impresión por OR es adecuada en juegos donde el personaje no tenga pixeles a cero dentro de su contorno, de forma que no se vean píxeles del fondo en el interior de nuestro personaje, alterando el sprite.
La segunda opción (y la más habitual) es su uso en juegos donde (sea como sea el personaje del jugador) el fondo sea plano (sin tramas) y el color del mismo sea idéntico al color de PAPER de los sprites del juego. De esta forma nos podemos mover en el juego sin causar colisión de atributos con el fondo (ya que no tiene pixeles activos y el PAPER del fondo es igual al PAPER de nuestro Sprite) y podemos ubicar en la pantalla objetos multicolor en posiciones de carácter que no provoquen colisión de atributos con nuestro personaje o si la provocan sea imperceptible (elemento muy ajustado a tamaño de carácter) o durante un período de tiempo mínimo (recogida de un objeto).
Manic Miner, Dizzy, Dynamite Dan y Fred
Impresión por operación lógica XOR:
Más que juegos adecuados para esta técnica, podemos hablar más bien de sprites adecuados para ella. Nos referimos a los cursores, que suelen ser dibujados con XOR para no confundirse con el fondo (permitiendo una visión “inversa” del mismo sobre el sprite) y para poder ser borrados con otra operación XOR.
Impresión con máscaras:
Por norma general, las rutinas de impresión de sprites con máscara se utilizan principalmente en juegos monocolor donde todo el área de juego (fondo incluído) y los personajes comparten el mismo atributo de pantalla. De esta forma, la impresión del sprite no cambia el color de los píxeles del fondo que caen dentro de su recuadro y además nos ahorramos la impresión de los atributos, puesto que se realiza rellenando los atributos del área de juego una única vez durante el dibujado inicial de la pantalla.
Una segunda opción es que el fondo sea multicolor y nuestro personaje se imprima sin atributos, adaptandose el personaje al color del fondo sin cambiar éste. En la captura de abajo tenemos al personaje principal de Target: Renegade adoptando los atributos de las posiciones de pantalla donde es dibujado.
H.A.T.E., Arkanoid, Rick Dangerous y Target: Renegade
Así pues, a la hora de realizar un juego, deberemos elegir la técnica más adecuada al tipo de juego que vamos a programar.
Nuestro siguiente paso sería el de crear una rutina de sprites que sirva para cualquier resolución de bloques de un sprite, a la que además de los parámetros anteriores se le tendría que añadir el ancho y alto del sprite.
Como no conocemos de antemano las dimensiones del sprite, todos los cálculos de dirección de origen de datos gráficos y de atributos se deben de basar en multiplicaciones realizadas con bucles. El bucle basado en el número de scanlines (ALTO_en_pixeles iteraciones) no se puede desenrollar y cada una de las transferencias de datos tiene que realizarse también en un bucle de ANCHO_en_caracteres iteraciones. Además, como necesitamos realizar varios bucles, utilizaremos más registros y por lo tanto nos veremos en la necesidad de alojar valores en la pila o en variables temporales o incluso de recuperar parámetros más de una vez dentro de la rutina.
La rutina resultante es mucho menos óptima y rápida que cualquier rutina específica de 8×8, 8×16, 16×8, 16×16, etc. Si tenemos predefinidos todos los tamaños de los sprites de nuestro juego es recomendable utilizar estas rutinas específicas en lugar de una rutina genérica.
La rutina que crearemos permitirá la impresión de sprites de cualquier tamaño con la única restricción que Ancho * Alto en caracteres no supere el valor de 255, por ejemplo, se podría imprimir un sprite de 15×16 o 16×15 bloques.
La rutina, al ser genérica, y para que pueda ser legible por el lector, no es especialmente eficiente: debido a la necesidad de realizar multiplicaciones de 16 bits, se utilizan los registros HL, DE y BC y nos vemos obligados a hacer continuos PUSHes y POPs de estos registros, así como a apoyarnos en variables de memoria. La rutina podría ser optimizada en cuanto a algoritmo y/o en cuanto a reordenación de código y uso de shadow-registers para tratar de ganar todos los t-estados posibles.
Incluso una opción mucho más recomendable sería crear tantas rutinas específicas como tamaños de sprites tengamos en nuestro juego y hacer una rutina “maestra” (un wrapper) que llame a una u otra según el tamaño del Sprite que estemos solicitando imprimir. Un par de comprobaciones sobre el ancho y el alto del Sprite y saltos condicionales con un CALL final a la rutina adecuada resultará muchísimo más óptimo que la rutina que vamos a examinar:
; DrawSprite_MxN_LD:
; Imprime un sprite de MxN pixeles con o sin atributos.
; Maximo, 16x15 / 15x16 bloques de ancho x alto caracteres.
; 50010       Ancho del sprite en caracteres
; 50011       Alto del sprite en caracteres
DrawSprite_MxN_LD:
;;;     direccion = base_sprites + (NUM_SPRITE*ANCHO*ALTO)
;;;; Multiplicamos ancho por alto (en bloques)
LD A, (DS_WIDTH)
LD A, (DS_HEIGHT)
RLCA               ; Multiplicamos por 8, necesitamos
RLCA               ; la altura en pixeles (FILAS*8)
RLCA               ; Y la guardamos porque la necesitaremos:
LD (drawsp_height), A
;;; Multiplicamos Ancho_bloques * Alto_pixeles:
drawsp_mul1:
ADD A, C           ; A = A + C   (B veces)  = B*C
DJNZ drawsp_mul1   ; B veces -> A = A*C = Ancho * Alto
; Ahora A = Ancho*Alto (maximo 255!!!)
;;; Multiplicamos DS_NUMSPR por (Ancho_bloques*Alto_pixeles)
LD B, A            ; Repetimos Ancho * Alto veces
LD D, H            ; HL = 0
LD E, A            ; DE = DS_NUMSPR
drawsp_mul2:
ADD HL, DE         ; HL = HL+DS_NUMSPR
DJNZ drawsp_mul2   ; Sumamos HL+DE B veces = DS_NUMSPR*B
; guardamos el valor de ancho*alto_pixeles*NUMSPR
LD (drawsp_width_by_height), HL
;;; Calculamos direccion origen copia en el sprite
ADD HL, BC         ; HL = BC + HL = DS_SPRITES + (DS_NUMSPR*ANCHO*ALTO)
LD BC, (DS_COORD_X)     ; B = Y,  C = X
PUSH DE            ; Lo guardamos para luego, lo usaremos para
;;; Bucle de impresión vertical
; Recogemos de nuevo la altura en pixeles
LD A, (drawsp_height)
LD B, A            ; Contador del bucle exterior del bucle
drawsp_yloop:
LD C, B            ; Nos guardamos el contador
;;; Bucle de impresion horizontal
PUSH HL            ; Guardamos en pila inicio de scanline
; para poder volver a el luego
drawsp_xloop:
LD A, (DE)         ; Leemos dato del sprite
DJNZ drawsp_xloop
POP HL             ; Recuperamos de pila inicio de scanline
;;; Avanzamos al siguiente scanline de pantalla
JR NZ, drawspNM_nofix
JR C, drawspNM_nofix
drawspNM_nofix:
DJNZ drawsp_yloop  ; Repetimos "alto_en_pixeles" veces
;;; Aqui hemos dibujado todo el sprite, vamos a los attributos
;;; Considerar el dibujado de atributos (Si DS_ATTRIBS=0 -> RET)
LD  A,[DS_ATTRIBS+1]     ; para obtener la parte alta de la direccion
OR  A                    ; para comprobar si es 0
RET Z                    ; Si H = 0, volver (no dibujar atributos)
PUSH DE            ; Guardamos una copia
; Recuperamos el valor de ancho_caracteres * alto_en_pixeles * NUMSPR
; para ahorrarnos repetir otra vez dos multiplicaciones:
LD HL, (drawsp_width_by_height)
;;; HL = ANCHO_BLOQUES*ALTO_PIXELES*NUMSPR
;;; El Alto lo necesitamos en BLOQUES, no en píxeles-> dividir /8
SRL H     ; Desplazamos H a la derecha
RR L      ; Rotamos L a la derecha introduciendo CF
SRL H     ;
RR L      ;
RR L      ; Resultado : HL = HL >> 3 = HL / 8
;;;; HL = ANCHO_BLOQUES*ALTO_BLOQUES*NUMSPR
ADD HL, BC         ; HL = Base_Atributos + (DS_NUMSPR*ALTO*ANCHO)
POP DE             ; Recuperamos direccion destino
;;; Bucle impresion vertical de atributos
drawsp_attyloop:
PUSH DE            ; Guardamos inicio de linea de atributos
;;; Bucle impresion horizontal de atributos
drawsp_attxloop:
LD A, (HL)         ; Leer atributo del sprite
LD (DE), A         ; Escribir atributo
DJNZ  drawsp_attxloop
POP DE             ; Recuperamos inicio de linea de atributos
JR NC, drawsp_attrab_noinc
drawsp_attrab_noinc:
DJNZ drawsp_attyloop
drawsp_height          DB 0
drawsp_width_by_height DW 0
Nótese que pese al uso extensivo de registros y memoria, todavía podemos realizar algunas optimizaciones, como la de “reaprovechar” el cálculo de Ancho*Alto*NUMSPR realizado al principio de la rutina (gráficos) para los cálculos del final (atributos). Como la multiplicación inicial tiene Alto en píxeles y lo necesitamos en Caracteres, dividimos el registro HL por 8 mediante instrucciones de desplazamiento que afecten a su parte baja y su parte alta.
La anterior rutina sería llamada de la siguiente forma para imprimir nuestro ya conocido sprite de 2×2 bloques:
DS_WIDTH    EQU  50010
DS_HEIGHT   EQU  50011
LD (DS_WIDTH), A
LD (DS_HEIGHT), A
CALL DrawSprite_MxN_LD
La rutina anterior trabaja mediante transferencia de datos sprite → pantalla. Para realizar una operación lógica, bastará modificar el bucle horizontal de impresión y añadir el correspondiente OR / XOR contra (HL).
La rutina para trabajar con máscaras implicar modificar el bucle principal para gestionar los datos de la máscara y modificar también las multiplicaciones iniciales de parámetros (*2 en el caso de los datos gráficos, sin modificación en los atributos).
Una vez vistas las rutinas de impresión, hay que tratar el tema del borrado de los sprites de pantalla. El proceso de movimiento y animación implica el borrado del Sprite de su anterior posición (o en la actual si sólo cambia el fotograma) y el redibujado del mismo.
Hay diferentes técnicas de borrado de sprites, en las que profundizaremos cuando tratemos las Animaciones, pero podemos distinguir inicialmente las 4 siguientes:
Borrado de sprite por sobreimpresión:
Se utiliza con rutinas de impresión sin transparencia y se basa en imprimir otro sprite encima del que queremos borrar. Ese otro sprite puede ser, en juegos de fondo plano, un tile totalmente vacío con los colores o patrones del fondo (un “blanco”) de forma que eliminemos el sprite y recuperemos el fondo que había en esa posición antes de imprimirlo.
En juegos de fondo monocolor (típicamente fondo negro) se suele reservar el bloque 0 del SpriteSet para alojar un “bloque vacío” de fondo que sirva de tile con el que sobreescribir con LDs una posición de sprite para borrarlo.
Borrado de sprite por borrado de atributos:
En juegos con fondo plano (ejemplo: fondo totalmente negro) y movimiento a bloques, podemos borrar un sprite que está sobre el fondo simplemente cambiando los atributos de las celdillas de pantalla a borrar a tinta y papel del color del fondo. Por ejemplo, en un fondo negro, se podría borrar un sprite de 16×16 pixeles con 4 simples operaciones de escritura de atributo de color tinta=papel=negros. Cuando otro sprite sea dibujado sobre nuestro “sprite borrado”, si se hace con una operación de transferencia eliminará todo rastro gráfico del Sprite anterior.
Si el juego no tiene movimiento por bloques, aún podemos aprovechar esta técnica mediante el sistema descrito por TrueVideo en los foros de Speccy.org: utilizando una tabla de 32×24 bytes que represente el estado de ocupación de las celdillas de pantalla.
Citando a TrueVideo:
El buffer al que me refiero puedes verlo como una segunda capa de atributos,
que es la forma más fácil de implementar. Es una zona de memoria de
32x24 = 768 bytes (como el area de atributos) en la que se marcan los caracteres
ocupados en cada momento.
Algunas pistas para implementarlo:
- cuando vayas a imprimir un caracter consulta primero su entrada en el buffer.
Si está libre puedes volcar el gráfico directamente con LD (HL),A... porque es
más rápido y sabes *seguro* que puedes machacar lo que haya en pantalla. Si por
el contrario está ocupado (o sea, hay otro sprite en esa posición) sabes que tienes
que imprimir mezclando para preservar el fondo (p.e. con XOR). De esta forma tu
rutina de impresión imprimirá siempre de la forma más rápida posible, excepto en
los caracteres que se superpongan.
- con este método puedes borrar pintando atributos en lugar de volcar blancos con
toda seguridad, sin riesgo a que se vea basura, porque la rutina de impresión de
caracteres sabe en todo momento cuándo mezclar y cuándo no.
- es importante elegir con cabeza la dirección en memoria del buffer. Teniendo en
cuenta que el área de atributos de la pantalla empieza en $5800, si el buffer lo
sitúas p.e. en $xx00 (donde xx es la dirección que tu quieras de la parte alta)
entonces puedes moverte de una zona a otra cambiando sólo la parte alta del
registro. En general es buena idea usar siempre la dirección de atributos como
base para todos tus cálculos: a partir de una dirección de atributos puedes calcular
fácilmente la dirección del archivo de pantalla.. y también enlazar con otros bufferes
propios de forma rápida. Por el contrario al trabajar con coordenadas (por decir algo)
los cálculos para referenciar los buffers (de atributos o propios) son más costosos.
La ventaja de todo esto es que, a cambio de mantener ese buffer, nos aseguramos de
que borramos siempre muy rápido e imprimimos "lento" sólo cuando es estrictamente
Borrado (e impresión) con XOR
Cuando un sprite ha sido dibujado con la operación XOR, podemos borrarlo realizando otra impresión en la misma posición mediante la misma operación. Esto es así porque la operación XOR es reversible.
Veamos la tabla de verdad de la operación XOR:
Bit Pantalla
Bit Sprite
A continuación tenemos un ejemplo de cómo restaurar el fondo gracias a XOR:
Impresión del sprite con XOR:
Fondo:    01010101
Sprite:   00111000
Tras XOR: 01101101
Recuperación del fondo con XOR:
Fondo:    01101101
Tras XOR: 01010101   <-- Valor original de la pantalla
La impresión de un sprite con XOR produce una especie de “negativo” del Sprite en la pantalla allá donde haya píxeles de fondo activos, por lo que esto, unido a la facilidad de borrado, lo hace ideal para la impresión de cursores y punteros.
Almacenamiento en buffer temporal
La última solución es la más costosa en términos de memoria y de instrucciones, pero nos permite restaurar el estado del fondo sea cual sea el número de sprites impresos y las características de este.
La técnica consiste en almacenar el contenido de los datos gráficos y de atributo del fondo en un array de memoria temporal antes de volcar el sprite sobre la pantalla.
Podríamos realizar una función específica para esta tarea (recuperar una porción de fondo, o lo que es lo mismo, crear un sprite tomando los datos de la pantalla); sería una modificación de las rutinas de impresión donde el origen sería la pantalla y el destino el array. No obstante, esto no es eficiente porque estaríamos realizando las operaciones de cálculo de posiciones 2 veces: una al salvaguardar el contenido del fondio y otra, más tarde, al llamar a la función de dibujado del Sprite.
Para evitar esto, podemos adaptar las rutinas de impresión para que salvaguarden el fondo en nuestro array temporal antes de escribir los datos en pantalla.
Como tenemos ya utilizados DE y HL para la transferencia del Sprite, necesitamos un registro de 16 bits adicional. En este caso utilizaremos el registro IX para apuntar al array temporal.
Podemos modificar cualquiera de las rutinas para recuperar el fondo antes de escribir el sprite. Tomemos como ejemplo el esqueleto de la rutina de impresión de 8×8 y veamos las instrucciones que habría que añadir para salvaguardar el fondo en un array temporal externo. No reproduciremos la totalidad de la rutina reducir la extensión del listado:
; DrawSprite_8x8_Restore:
; Imprime un sprite de 8x8 pixeles salvaguardando el fondo
; en la direccion de memoria indicada por (50008).
; 50008       Direccion del array temporal para el fondo
DS_TEMPBUF EQU 50008
;;; Recogida de datos y parametros
LD IX, (DS_TEMPBUF)
;;; Bucle de impresion del Sprite:
LD A, (HL)       ; NUEVO: Leemos el valor actual del fondo
LD (IX), A       ; NUEVO: Lo almacenamos en el array temporal
INC IX           ; NUEVO: Incrementamos IX (puntero array fondo)
; Ya podemos imprimir el sprite
LD A, (DE)       ; NUEVO: Leemos el atributo actual
LD (IX), A       ; NUEVO: Lo guardamos en el array temporal
LD A, (HL)       ; Ya podemos imprimir el atributo
El registro IX no es especialmente rápido (10 t-estados el INC IX y 19 t-estados el LD (IX), A o LD (IX+0), A, pero es más rápido utilizarlo que escribir y ejecutar una rutina adicional para almacenar el fondo.
Cuando necesitemos borrar el sprite, bastará con recuperar el fondo que había antes de imprimirlo, es decir, volcar el contenido del buffer temporal en la posición del sprite a borrar. Para realizar este volcado podemos llamar a una de las funciones de volcado de Sprites basada en transferencias ya que el buffer del fondo en memoria se corresponde con un spriteset de 1 sólo elemento (DS_NUMSPR=0), sin máscara. Las rutinas impresión de 8×8, 16×16 o genéricas basadas en LD servirían para recuperar el contenido del fondo usando como origen el buffer temporal donde lo hemos almacenado.
Si tenemos más de un Sprite y hemos almacenado el fondo de cada uno de ellos en un array diferente, debemos restaurar todos los fondos en orden inverso al que se haya realizado la impresión. Veamos por qué:
Supongamos que imprimimos un sprite (y almacenamos el fondo). Al imprimir el segundo sprite en la misma posición que el anterior o en una posición cercana, almacenaríamos como fondo de este segundo sprite parte del anterior sprite impreso. Si recuperamos los fondos en orden inverso al impreso, nos aseguramos de que el último fondo recuperado será el del sprite 1, que es el correcto.
No olvidemos que las operaciones de borrado y reimpresión deben de ser lo más rápidas posibles para evitar parpadeos debidos a que el ojo humano pueda ver el momento del borrado si la ULA refresca la pantalla tras el “restore” del fondo y antes del dibujado del nuevo sprite.
Adaptación de las rutinas al juego
Las rutinas que hemos visto en este apartado y en los siguientes no son, con diferencia, las versiones más óptimas posibles que un programador avanzado podría crear. Es imprescindible que adaptemos las rutinas al juego que vamos a crear eliminando todas aquellas partes de la misma que no sean necesarias.
Por ejemplo, si nuestro juego va a imprimir sprites sin atributos sobre un fondo monocolor con los atributos ya establecidos, lo más normal sería modificar las subrutinas y eliminar todo rastro de la gestión y cálculo de los atributos, no requiriendo siquiera establecer la dirección de atributo a cero y el código que hace esta comprobación. Con esto ahorramos espacio en memoria (de instrucciones eliminadas) y tiempo de ejecución (de comprobaciones innecesarias).
Adaptación de los parámetros de entrada
Por otra parte, si no tenemos intención de llamar a estas funciones desde BASIC, lo más óptimo sería eliminar el paso de parámetros por variables de memoria y utilizar registros y/o pila allá donde sea posible, puesto que estas operaciones son más rápidas que los acceso a memoria.
No obstante, nótese que las 2 variables que más tiempo cuesta de establecer y de leer (DS_SPRITES y DS_ATTRIBS) se pueden establecer como constantes (y no como variables) directamente dentro de la rutina de impresión. Eso quiere decir que si tenemos un único tileset, podríamos cambiar partes de la rutina, como:
LD BC, Tabla_Sprites
Establecer BC a un valor inmediato (LD BC, NN) tiene un coste de 10 t-estados, frente a los 20 t-estados que cuesta establecerlo desde una posición de memoria (LD BC, (NN)). De esta forma evitamos cargar desde memoria las direcciones de los tilesets y también tener que establecerlos al inicio del programa.
Deshabilitar las interrupciones
Si nuestra rutina de dibujado de interrupciones es crítica, puede que nos resulte necesario en algunas circunstancias realizar un DI (Disable Interrupts) al principio de la misma y un EI (Enable Interrupts) al acabar, para evitar que una interrupción del Z80 sea lanzada durante la ejecución de la misma.
Normalmente no deberíamos requerir de esta operación porque lo normal es estar sincronizado con las interrupciones y realizar el redibujado de los sprites en la ISR o al volver de la ISR de atención a la ULA.
No obstante, si no estamos sincronizados con las interrupciones o si estamos usando IM1, es un dato interesante que nos puede evitar la ejecución de código ISR en medio de la ejecución de la rutina.
Trataremos este tema con más detalle en el capítulo dedicado a Animaciones, en el apartado sobre Evitar Flickering o Parpadeos.
Uso de la pila para acceso a datos
Una opción realmente ingeniosa para optimizar el acceso al sprite sería la de utilizar la pila para realizar operaciones de lectura de 2 bytes del sprite con una única instrucción (POP).
Apuntando SP a nuestro sprite, un simple POP DE carga en E y D (en ese orden) los 2 bytes apuntados por SP e incrementa la dirección contenida en DE en 2 unidades, por lo que realizamos 2 operaciones de lectura y 2 incrementos (de 16 bits) en 1 byte de código y 10 t-estados, frente a los 7+7+6+6 = 26 t-estados que cuestan las operaciones LD+INC por separado. Eso implica leer en E y D dos bytes de datos del sprite o el byte de máscara y un byte de datos.
Como desventaja, debemos deshabilitar las interrupciones con DI al principio del programa y habilitarlas de nuevo antes del RET con EI ya que no podemos permitir la ejecución de una ISR estando SP apuntando al Sprite. Un “RST” o “CALL” (así como un PUSH) ejecutado con SP apuntando al Sprite provocaría la corrupción del mismo al introducirse en la pila la dirección de memoria del RET o RETI para la ISR.
Veamos el pseudocódigo de una rutina que use la pila de esta forma:
; Calcular dirección destino en pantalla
; Calcular dirección origen de sprite
; Guardar estado anterior pila
; Establecer direccion Sprite en SP en dir sprite
; Bucle de N scanlines:
;   Recuperar datos del sprite con POP
; Calcular e imprimir atributos
; Recuperar puntero de pila
A continuación vamos a ver una versión “reducida” (sin impresión de atributos, y con un bucle de 16 iteraciones sin desenrollar) de DrawSprite_16x16 que utiliza la pila para recuperar los datos del sprite mediante POP DE. Esto implica la lectura de 2 bytes en una sóla instrucción y el doble incremento de DE para apuntar a los 2 siguientes elementos.
En la rutina guardaremos el valor de SP en una variable temporal, antes de modificarlo, para poder recuperarlo antes del RET. Una vez calculada la dirección de origen del sprite a dibujar, realizaremos la copia de SP en DS_TEMP_SP (variable de memoria) y cargaremos el valor de HL en SP:
; DrawSprite_16x16_LD_STACK_no_attr:
; Imprime un sprite de 16x16 pixeles sin atributos
; usando la pila para obtener datos del sprite.
DS_TEMP_SP  DW 0           ; Variable temporal donde alojar SP
DrawSprite_16x16_LD_STACK_no_attr:
;;; Deshabilitar interrupciones (vamos a cambiar SP).
;;; Guardamos el valor actual y correcto de SP
LD (DS_TEMP_SP), SP
;;; Calcular posicion origen (array sprites) en HL:
;;; Establecemos el valor de SP apuntando al sprite
LD B, 16
drawsp16x16_stack_loop:
POP DE             ; Recuperamos 2 bytes del sprite
; Y ademas no hace falta sumar 2 a DE
LD (HL), E         ; Ahora imprimimos los 2 bytes en pantalla
INC H              ; Siguiente scanline
LD A, H            ; Ajustar tercio/bloque si necesario
JR NZ, drawsp16_nofix_abajop
DJNZ drawsp16x16_stack_loop
;;; Recuperamos el valor de SP
LD HL, (DS_TEMP_SP)
EI                  ; Habilitar de nuevo interrupciones
Al igual que en el caso de las rutinas anteriores, si desenrollamos el bucle de 16 iteraciones en 2 bucles de 8, podemos utilizar INC H dentro de los bucles y el código de Scanline_Abajo_HL entre ambos. El bucle convertido en dos quedaría de la siguiente forma:
drawsp16x16_stack_loop1:
INC L              ; de la parte superior del Sprite
DJNZ drawsp16x16_stack_loop1
; No hay que comprobar si (H & $07) == 0 porque sabemos que lo es
drawsp16x16_stack_loop2:
INC L              ; de la parte inferior del Sprite
DJNZ drawsp16x16_stack_loop2
Impresión de Sprite 8x8 sin máscara y sin OR
Impresión de Sprite 8x8 sin máscara y con OR
Impresión de Sprite 8x8 con máscara
Impresión de Sprite 16x16 sin máscara
Impresión de Sprite 16x16 con máscara
cursos/ensamblador/gfx3_sprites_lowres.txt
Última modificación: 20-11-2010 17:36

References: resolución 
 resolución 
 resolución 
 resolución 
 resolución 
 resolución

 resolución

 resolución 
 resolución