Gráficos (y V): Técnicas de mapeado por bloques

En el amplio catálogo de software del Spectrum existen juegos con decenas y hasta cientos de pantallas. ¿Cómo es posible que, a 7KB por pantalla, quepan tantas “localidades” de juego en la memoria del Spectrum?

Lo que posibilita esta variedad de pantallas de juego es la técnica de mapeado por bloques.

En esta técnica se utiliza un spriteset de bitmaps denominados tiles y se definen en memoria mapas basados en identificadores numéricos donde cada valor representa un tile concreto. Así, se compone cada pantalla de juego como una matriz de “tiles” a dibujar.

Un sólo spriteset (tileset) lo suficientemente variado y la habilidad del diseñador y del programador pueden dar lugar a gran cantidad de pantallas y un amplio mapeado con un mínimo uso de memoria, todo basado en la composición de la pantalla como repetición de tiles.

En este capítulo veremos cómo definir las pantallas del mapeado y rutinas para imprimir estas pantallas en la videomemoria.


Como ya vimos en el capítulo dedicado a Sprites en baja resolución, podemos generar las pantallas de nuestros juegos como repetición de bitmaps tomados de un set gráfico a partir del cual se puedan construir todo el mapeado.

Cada uno de estos “bloques” a partir del cual componemos la pantalla recibe el nombre de tile.

Con los tiles podemos componer tanto pantallas individuales de juego como mapas de gran extensión en los que podemos cambiar de una pantalla a otra constantemente (redibujando una pantalla entera cada vez) o incluso scrollear porciones de la misma.

La pantalla se codifica como una matriz o vector de datos numéricos donde cada valor representa un índice en el tileset (un bitmap concreto). La rutina de impresión recorre este vector/matriz y traza cada bitmap en su posición correspondiente para generar la imagen que ve el jugador.

Pueden existir incluso identificadores numéricos de bloque que nuestra rutina trate de forma especial, para definir por ejemplo bloques que no deben de ser dibujados y que permitan ver el fondo que hay en la pantalla, como sucede en el caso de Sokoban:


 Tilemaps

Tilemap: componiendo un mapa en pantalla
a partir de tiles de un tileset/spriteset + mapa.


Crear los mapeados mediante tiles nos permite un enorme ahorro de memoria ya que en lugar de necesitar 6912 bytes por cada pantalla de juego nos basta con un tileset y pantallas formadas por repetición de los mismos. Una pantalla completa (256×192) formada por tiles de 16×16 ocupará apenas (256/16)x (192/16) = 16×12 = 192 bytes. En los 7 KB que ocupa una pantalla gráfica entera podemos definir 36 pantallas de juego en base a tiles.

Una gran parte de los juegos de Spectrum tienen sus pantallas formadas por mapas de tiles, debido al gran ahorro de memoria que suponen. El diseñador de los gráficos y de las pantallas será el principal responsable de que existan suficientes bloques diferentes para representar todos los elementos del mapeado con suficiente diversidad gráfica.


 Juegos basados en tiles

Y no sólo podemos realizar juego de puzzle tipo Puzznic, Plotting o Sokoban: en base a tiles podemos crear videoaventuras, los mapeados de un juego de disparos, juegos de laberintos, plataformas, o generar todas las pantallas en que se desenvuelva un arcade. Una vez impresa la pantalla en base a bloques, el juego puede desarrollarse pixel a pixel en cuanto al movimiento del personaje principal y los enemigos y objetos.


Los mapas están compuestos por pantallas, que representan la porción del mismo que resulta visible por el usuario.

Cada nivel puede estar formado por una única pantalla (caso de juegos como Sokoban, Bubble Bobble, Manic Miner, etc.), o puede estar formado por múltiples pantallas (R-Type, Rick Dangerous, Dynamite Dan, Sabre Wulf, Into the eagle's Nest, etc.).

Comencemos examinando la forma de definir y trazar una única pantalla. Posteriormente veremos las estructuras de datos necesarias para definir un mapa completo como una matriz de pantallas.


Las pantallas de un mapa son vectores de datos donde cada elemento representa un identificador de tile (valor numérico 0-N) que utilizaremos para obtener desde el tileset el gráfico concreto a dibujar en esa posición.

Podemos organizar los tiles linealmente en memoria en formato horizontal o en formato vertical.

Una pantalla en formato horizontal contiene los identificadores de tiles de la pantalla en scanlines horizontales de tiles, comenzando con la fila 0, a la cual le sigue la fila 1, la 2, la 3, etc.


El tileset de Sokoban (el primero de los sets gráficos disponibles) es el siguiente:


 Tileset de Sokoban

Veamos ampliados los bloques de 16×16:


 (Ampliacion del tileset)

Como puede apreciarse, hay 8 tiles gráficos que numeramos desde el 0 al 7 siendo el 0 un bloque vacío de fondo negro.


Ahora veamos cómo codificar una pantalla utilizando el tileset que acabamos de ver. Utilizaremos para eso la primera pantalla del juego, que es la siguiente:


 Pantalla 1 de Sokoban

Es una pantalla de 16×12 tiles formados por 2×2 bloques cada uno (16×16=256 y 12×16=192 → 256×192 pixeles). Hay 7 tiles gráficos, uno “en blanco” (el 0), y un tile que es transparente (el que permite que se vea el fondo). Los tiles están impresos sobre un fondo negro decorado con diferentes logotipos y gráficos del robot protagonista (de ahí la importancia de los tiles transparentes, que permiten que no se sobreescriba el fondo).

Veamos el proceso de construcción de la pantalla: Codificaremos cada “bloque” de pantalla con el identificador numérico que lo representa en el tileset, siendo 0 el tile inicial de contenido vacío, y siendo 8 el código especial que indique que no se debe dibujar el tile (de forma que sea transparente y deje ver el fondo).



Organización horizontal

Empecemos con la primera fila horizonzal de datos. Nótese que esta primera fila (fila superior de la pantalla) es totalmente transparente (no se debe escribir ningún tile y debe verse el fondo original con el patrón de relleno), por lo que se codifica como:

  DEFB 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8

La siguiente fila (segunda fila horizontal de tiles) ya tiene 4 bloques gráficos (tiles 2, 3, 1, 4), en el centro de la pantalla. Todos los demás tiles son transparentes (valor 8):

  DEFB 8,8,8,8,8,8,2,3,1,4,8,8,8,8,8,8

Continuemos observando la pantalla del nivel 1 de Sokoban y el vector de datos que vamos generando: La tercera fila tiene 7 tiles: 3 consecutivos con formas de paredes (tiles nº 1, 2 y 3), luego 2 tiles “vacíos” de fondo (valor 0) y 2 tiles más de “paredes” (tiles nº 5 y 4). El resto de tiles son transparentes (valor 8):

  DEFB 8,8,8,8,1,2,3,0,0,5,4,8,8,8,8,8

Si continuamos codificando cada línea horizontal de pantalla en nuestro vector de datos, obtenemos la pantalla completa:

;;; Pantalla 1 de Sokoban codificada horizontalmente.
;;;
;;; El tile de valor 8 es un tile transparente (no esta en el tileset).
 
sokoban_LEVEL1_h:
  DEFB 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8
  DEFB 8,8,8,8,8,8,2,3,1,4,8,8,8,8,8,8
  DEFB 8,8,8,8,1,2,3,0,0,5,4,8,8,8,8,8
  DEFB 8,8,8,8,4,0,6,6,0,0,5,8,8,8,8,8
  DEFB 8,8,8,8,5,0,0,6,0,0,4,8,8,8,8,8
  DEFB 8,8,8,8,4,0,0,0,0,0,5,8,8,8,8,8
  DEFB 8,8,8,8,5,2,3,0,0,2,3,8,8,8,8,8
  DEFB 8,8,8,8,8,8,1,0,0,0,4,8,8,8,8,8
  DEFB 8,8,8,8,8,8,4,7,7,7,5,8,8,8,8,8
  DEFB 8,8,8,8,8,8,5,2,3,2,3,8,8,8,8,8
  DEFB 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8
  DEFB 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8

Hemos ubicado los datos en una representación visual clara mediante un DEFB por cada fila horizontal, pero el aspecto real de los datos en memoria es totalmente lineal:

sokoban_LEVEL1_h:
 DEFB 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,2,3,1,4,(...),8,8,8,8

Así pues, cada “fila” de 16 tiles de la pantalla se codifica con 16 identificadores de tile. Hay un total de 12 filas, por lo que acabamos obteniendo un vector de 16*12 = 192 bytes de datos para la pantalla.

Normalmente, la conversión de datos gráficos a identificadores de tile no se realiza manualmente sino que se emplea un “programa de dibujo de mapeados” (editor de mapas) con el que “dibujamos tiles” utilizando el tileset como paleta y que permite exportar el mapa directamente como datos.



Organización vertical

En el anterior ejemplo hemos organizado el mapa en formato horizontal. También habría cabido la posibilidad de organizarlo verticalmente, es decir, creando un vector que almacenara los identificadores de tile de cada columna de la pantalla.

Veamos de nuevo la pantalla inicial de Sokoban para codificarla verticalmente:


 Pantalla 1 de Sokoban

En este caso, la pantalla comenzaría con 4 columnas de datos “transparentes”.

  DEFB 8,8,8,8,8,8,8,8,8,8,8,8
  DEFB 8,8,8,8,8,8,8,8,8,8,8,8
  DEFB 8,8,8,8,8,8,8,8,8,8,8,8
  DEFB 8,8,8,8,8,8,8,8,8,8,8,8

La quinta columna ya tiene bloques gráficos :

  DEFB 8,8,1,4,5,4,5,8,8,8,8,8

La sexta columna bloques gráficos y bloques “vacíos” (0):

  DEFB 8,8,2,0,0,0,2,8,8,8,8,8

La pantalla completa codificada verticalmente sería la siguiente:

;;; Pantalla 1 de Sokoban codificada verticalmente.
;;;
;;; El tile de valor 8 es un tile transparente (no esta en el tileset).
 
sokoban_LEVEL1_v:
  DEFB 8,8,8,8,8,8,8,8,8,8,8,8
  DEFB 8,8,8,8,8,8,8,8,8,8,8,8
  DEFB 8,8,8,8,8,8,8,8,8,8,8,8
  DEFB 8,8,8,8,8,8,8,8,8,8,8,8
  DEFB 8,8,1,4,5,4,5,8,8,8,8,8
  DEFB 8,8,2,0,0,0,2,8,8,8,8,8
  DEFB 8,2,3,6,0,0,3,1,4,5,8,8
  DEFB 8,3,0,6,6,0,0,0,7,2,8,8
  DEFB 8,1,0,0,0,0,0,0,7,3,8,8
  DEFB 8,4,5,0,0,0,2,0,7,2,8,8
  DEFB 8,8,4,5,4,5,3,4,5,3,8,8
  DEFB 8,8,8,8,8,8,8,8,8,8,8,8
  DEFB 8,8,8,8,8,8,8,8,8,8,8,8
  DEFB 8,8,8,8,8,8,8,8,8,8,8,8
  DEFB 8,8,8,8,8,8,8,8,8,8,8,8
  DEFB 8,8,8,8,8,8,8,8,8,8,8,8

En este caso nuestros “DEFBs” son de 12 bytes cada uno, teniendo un total de 16 columnas de 12 bytes = los mismos 192 bytes de datos de pantalla.

Según codifiquemos el mapa en formato horizontal o vertical tendremos que programar una rutina de impresión de pantalla u otra, ya que el orden de obtención de los datos desde el “vector de pantalla” es diferente.

La rutina de impresión vertical suele ser ligeramente más óptima que la horizontal dada la estructura de la pantalla del Spectrum. Por contra, existe una tendencia generalizada a utilizar la codificación e impresión en formato horizontal por similitud con nuestro sistema de escritura (de izquierda a derecha y de arriba a abajo) en cuanto a la hora de “leer” los datos gráficos por parte del programador (visualmente hablando).

En nuestros ejemplos utilizaremos codificación horizontal y rutinas de impresión de filas (impresión horizontal) siendo las rutinas de impresión vertical fácilmente deducibles a partir de estas.

Por otra parte, cabe destacar que podemos crear diferentes tilesets y una misma pantalla puede trazarse con cualquiera de ellos permitiéndonos tener “sets gráficos” diferentes para que el usuario pueda elegir el más acorde a sus gustos. El juego Sokoban, por ejemplo, incluye diferentes sets gráficos seleccionables por el usuario; las pantallas de mapa son las mismas, y sólo cambia la dirección de origen de la que se leen los tiles (el tileset) entre los varios disponibles (bastará con cambiar DM_SPRITES, la variable de memoria que utilizarán nuestras rutinas, para apuntar a un tileset diferente).


Rutinas de impresión de pantallas

En este apartado vamos a ver rutinas diseñadas para dibujar una pantalla estática formada a base de tiles. La rutina debe imprimir un mapa de ANCHOxALTO tiles a partir de una posición inicial (X_INICIO,Y_INICIO), y está pensada para el trazado de una pantalla que no tiene scroll de ningún tipo y sobre la que después trabajaremos.

Ejemplos de juegos con este “formato” de pantalla son Manic Miner, Bubble Bobble, Sabre Wulf, Dynamite Dan, Sir Fred, etc…. La pantalla del mapa se dibuja una sóla vez y no se redibuja se o cambia de pantalla a menos que el personaje cruce los límites de la misma.

Para juegos con scroll basados en tiles es necesario scrollear la pantalla y diseñar rutinas que impriman porciones del mapeado (tiras verticales u horizontales de la pantalla entrante que aparezcan por alguna de las 4 direcciones), además de requerir una estructura de mapeado concreta, como veremos más adelante.

Aunque nos vamos a centrar ahora en la impresión de una sóla pantalla, no debemos olvidar que el mapa completo del juego está formado por múltiples pantallas. El mapa en sí no será más que un array de direcciones de las diferentes pantallas, del que obtendremos la dirección de la pantalla actual para proporcionársela a la rutina de impresión.

Una primera aproximación a la impresión del mapa se basaría en la utilización de las rutinas de impresión de Sprites ya vistas usando dos bucles (uno vertical y el otro, anidado, horizontal). La rutina recorrería toda la pantalla del mapa e imprimiría cada tile utilizando DrawSprite:

;;; Aproximacion 1:
FOR Y=0 TO ALTO_PANTALLA_EN_TILES:
  FOR X=0 TO ANCHO_PANTALLA_EN_TILES:
     TILE = PANTALLA[x][y]
     XTILE = X_INICIAL + X*ANCHO_TILE
     YTILE = Y_INICIAL + Y*ALTO_TILE
     CALL Draw_Sprite

La implementación de esta rutina no sería óptima porque implica gran cantidad de cálculos tanto en el bucle interior como dentro de la rutina Draw_Sprite. Para cada tile de pantalla se realizaría un cálculo de dirección de memoria a partir de las coordenadas X,Y.

Además, la obtención de cada TILE implica un acceso a una matriz de 2 dimensiones X,Y, que en nuestro array de datos se corresponde con:

TILE = [ PANTALLA + (Y*ANCHO_PANTALLA_EN_TILES) + X ]

Veamos una aproximación mucho más óptima:

1.- Teniendo en cuenta que los datos de la pantalla son consecutivos en memoria, vamos a utilizar un puntero para obtener los datos de los tiles linealmente sin tener que calcular la posición dentro del array de datos una y otra vez. Bastará con incrementar nuestro puntero (DIR_PANTALLA) para apuntar al siguiente dato.

2.- En lugar de calcular la posición en videomemoria (DIR_MEM) de cada tile, calcularemos una sóla vez esta posición para el primer tile de cada fila y avanzaremos diferencialmente a lo largo de la misma. De esta forma sólo realizamos un cálculo de dirección de videomemoria por fila, y no por tile.

;;; Aproximacion 2b - Impresion horizontal con mapa horizontal.
;;; Calculamos la posicion en memoria del primer bloque de linea
;;; y despues nos movemos diferencialmente a los siguientes bloques
;;; El mapa se accede linealmente (no se indexa por x,y):

DIR_PANTALLA = Direccion de memoria de los datos de la PANTALLA actual
FOR Y=0 TO ALTO:
  DIR_MEM = Posicion_Memoria( X_INICIAL, Y_INICIAL + Y*ALTO_BLOQUE )
  FOR X=0 TO ANCHO_PANTALLA:
     TILE = [DIR_PANTALLA]
     DIR_PANTALLA = DIR_PANTALLA + 1
     PUSH DIR_MEM
     DIR_SPRITE = BASE_TILESET + (TILE*ANCHO_TILE*ALTO_TILE)
     Dibujar Sprite desde DIR_SPRITE a DIR_MEM
     POP DIR_MEM
     DIR_MEM = DIR_MEM + ANCHO_TILE

Esta rutina sólo realiza un cálculo de posición de videomemoria por fila y además accede a nuestra pantalla de mapa linealmente. Antes de dibujar el tile en pantalla hacemos un PUSH de la dirección actual de memoria ya que la rutina de impresión la modifica. El posterior POP nos recupera la posición de impresión inicial de forma que baste un simple incremento de la misma para apuntar en videomemoria a la posición del siguiente tile que tenemos que dibujar.

Vamos a desarrollar un poco más la rutina en un pseudocódigo más parecido a ASM y con más detalles. Para ello establecemos las siguientes premisas:


  • Utilizaremos IX como puntero de la pantalla del mapa (DIR_PANTALLA).
  • Utilizaremos el valor de tile 255 como un tile “especial” que la rutina no dibujará. Este tile será pues un tile transparente que dejará ver el fondo en contraposición al típico bloque “0” vacío que borra un tile de pantalla.
DrawMap:

  IX = MAPA
  B = ALTURA_MAPA_EN_TILES

bucle_altura:
  Y = ALTURA_MAPA_EN_TILES - B
  DE = DIR_MEM( X_INICIO, Y_INICIO + (Y*2) )

       B = ANCHURA_MAPA_EN_TILES
       bucle_anchura:
          A = (IX)
          INC IX

          Si A == 255 : 
             JR saltar_bloque

          PUSH HL
          DIR_SPRITE = BASE_TILESET + (A*ANCHO_TILE*ALTO_TILE)
          HL = BASE_TILESET + (A*8*TAMAÑO_BLOQUES_EN_CADA_TILE)
          PUSH HL
          Imprimir_Sprite_de_HL_a_DE
          Convertir DIR HL imagen en DIR HL atributos
          Imprimir_Atributos_Sprite
          POP HL

       saltar_bloque:
          HL = HL + ANCHO_TILE
          DJNZ bucle_anchura

  DJNZ bucle_altura

Este algoritmo es genérico y puede ser optimizado personalizándolo a cada situación / tipo de juego que estemos realizando.


Rutina para bloques de 16x16 con mapeados horizontales

Veamos una rutina de impresión de pantallas de mapa basadas en tiles de 16×16 píxeles, el tamaño más habitual de tile para la resolución del Spectrum.

Con tiles de 16×16 píxeles podemos generar una pantalla de hasta 16×12 tiles utilizando 192 tiles por pantalla. Con tiles referenciadas por variables de 1 byte, cada pantalla ocuparía, pues, 192 bytes.

Tamaños de 8×8 requerirían 768 bytes por pantalla y un tileset con gran cantidad de elementos para poder componer las pantallas.

Tiles de mayores tamaños darían poca “resolución” para generar la pantalla, ya que serían demasiado grandes. Por ejemplo, tiles de 32×32 pixeles generarían pantallas de 8×6 tiles (poca “resolución” de mapa).

La rutina que veremos ahora es genérica: permite especificar un ancho y alto de cada pantalla en tiles para poder dibujar pantallas desde 1×1 a 16×12 tiles. Además podemos especificar una dirección de inicio de impresión (inicio_x, inicio_y), de forma que podamos dibujar nuestro mapa en una posición concreta de pantalla (por ejemplo, dentro de un marco, centrado, etc).

El hecho de ser una rutina genérica también le resta algo de velocidad en ciertos cálculos que podríamos evitar o desenrollar para rutinas específicas. Si en nuestro juego todas las pantallas tienen el mismo tamaño y las imprimimos siempre en la misma posición, podemos (y debemos) alterar la rutina para utilizar los valores adecuados dentro de ella, modificar los cálculos y desenrollar los bucles utilizando estos datos constantes.

La rutina se basa en el pseudocódigo que vimos al inicio del capítulo, pero adaptado a tamaños de 16×16. Veamos la rutina comentada:

;---------------------------------------------------------------
; DrawMap_16x16:
; Imprime una pantalla de tiles de 16x16 pixeles.
;
; Entrada (paso por parametros en memoria):
; Direccion             Parametro
; --------------------------------------------------------------
; DM_SPRITES (2 bytes)  Direccion de la tabla de tiles.
; DM_ATTRIBS (2 bytes)  Direccion de la tabla de atributos.
; DM_MAP     (2 bytes)  Direccion de la pantalla en memoria.
; DM_COORD_X (1 byte)   Coordenada X-Inicial en baja resolucion.
; DM_COORD_Y (1 byte)   Coordenada Y-Inicial en baja resolucion.
; DM_WIDTH   (1 byte)   Ancho del mapa en tiles
; DM_HEIGHT  (1 byte)   Alto del mapa en tiles
;---------------------------------------------------------------
DrawMap_16x16:
 
   ;;;;;; Impresion de la parte grafica de los tiles ;;;;;;
   LD IX, (DM_MAP)           ; IX apunta al mapa 
   LD A, (DM_HEIGHT)
   LD B, A                   ; B = ALTO_EN_TILES (para bucle altura)
 
drawm16_yloop:
   PUSH BC                   ; Guardamos el valor de B
 
   LD A, (DM_HEIGHT)         ; A = ALTO_EN_TILES
   SUB B                     ; A = ALTO - iteracion_bucle = Y actual
   RLCA                      ; A = Y * 2
 
   ;;; Calculamos la direccion destino en pantalla como
   ;;; DIR_PANT = DIRECCION(X_INICIAL, Y_INICIAL + Y*2)
   LD BC, (DM_COORD_X)       ; B = DB_COORD_Y y C = DB_COORD_X
   ADD A, B
   LD B, A
   LD A, B
   AND $18
   ADD A, $40
   LD H, A
   LD A, B
   AND 7
   RRCA
   RRCA
   RRCA
   ADD A, C
   LD L, A                   ; HL = DIR_PANTALLA(X_INICIAL,Y_INICIAL+Y*2)
 
   LD A, (DM_WIDTH)
   LD B, A                   ; B = ANCHO_EN_TILES
 
drawm16_xloop:
   PUSH BC                   ; Nos guardamos el contador del bucle
 
   LD A, (IX+0)              ; Leemos un byte del mapa   
   INC IX                    ; Apuntamos al siguiente byte del mapa
 
   CP 255                    ; Bloque especial a saltar: no se dibuja
   JP Z, drawm16_next
 
   LD B, A
   EX AF, AF'                ; Nos guardamos una copia del bloque en A'
   LD A, B
 
   ;;; Calcular posicion origen (array sprites) en HL como:
   ;;;     direccion = base_sprites + (NUM_SPRITE*32)
   EX DE, HL                 ; Intercambiamos DE y HL (DE=destino)
   LD BC, (DM_SPRITES)
   LD L, 0
   SRL A
   RR L
   RRA
   RR L
   RRA
   RR L
   LD H, A
   ADD HL, BC                ; HL = BC + HL = DS_SPRITES + (DS_NUMSPR * 32)
   EX DE, HL                 ; Intercambiamos DE y HL (DE=origen, HL=destino)
 
   PUSH HL                   ; Guardamos el puntero a pantalla recien calculado
   PUSH HL
 
   ;;; Impresion de los primeros 2 bloques horizontales del tile
 
   LD B, 8
drawm16_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
   LD (HL), A                ; Copiamos dato a pantalla
   INC DE                    ; Incrementar puntero en sprite
   INC H                     ; Hay que sumar 256 para ir al siguiente scanline
   DEC L                     ; pero hay que restar el INC L que hicimos.
   DJNZ drawm16_loop1
   INC L                     ; Decrementar el ultimo incrementado en el bucle
 
   ; 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
   LD A, L
   ADD A, 31
   LD L, A
   JR C, drawm16_nofix_abajop
   LD A, H
   SUB 8
   LD H, A
drawm16_nofix_abajop:
 
   ;;; Impresion de los segundos 2 bloques horizontales:
   LD B, 8
drawm16_loop2:
   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
   LD (HL), A                ; Copiamos dato a pantalla
   INC DE                    ; Incrementar puntero en sprite
   INC H                     ; Hay que sumar 256 para ir al siguiente scanline
   DEC L                     ; pero hay que restar el INC L que hicimos.
   DJNZ drawm16_loop2
 
 
   ;;; En este punto, los 16 scanlines del tile estan dibujados.
 
   ;;;;;; Impresion de la parte de atributos del tile ;;;;;;
 
   POP HL                    ; Recuperar puntero a inicio de tile
 
   ;;; Calcular posicion destino en area de atributos en DE.
   LD A, H                   ; Codigo de Get_Attr_Offset_From_Image
   RRCA
   RRCA
   RRCA
   AND 3
   OR $58
   LD D, A
   LD E, L                   ; DE tiene el offset del attr de HL
 
   LD HL, (DM_ATTRIBS)
   EX AF, AF'                ; Recuperamos el bloque del mapa desde A'
   LD C, A
   LD B, 0
   ADD HL, BC
   ADD HL, BC
   ADD HL, BC
   ADD HL, BC                ; HL = HL+HL=(DS_NUMSPR*4) = Origen de atributo
 
   LDI
   LDI                       ; Imprimimos la primeras fila de atributos
 
   ;;; Avance diferencial a la siguiente linea de atributos
   LD A, E                   ; A = E
   ADD A, 30                 ; Sumamos A = A + 30 mas los 2 INCs de LDI.
   LD E, A                   ; Guardamos en E (E = E+30 + 2 por LDI=E+32)
   JR NC, drawm16_att_noinc
   INC D
drawm16_att_noinc:
   LDI
   LDI                       ; Imprimimos la segunda fila de atributos
 
   POP HL                    ; Recuperamos el puntero al inicio
 
drawm16_next:
   INC L                     ; Avanzamos al siguiente tile en pantalla
   INC L                     ; horizontalmente
 
   POP BC                    ; Recuperamos el contador para el bucle
   DEC B                     ; DJNZ se sale de rango, hay que usar DEC+JP
   JP NZ, drawm16_xloop
 
   ;;; En este punto, hemos dibujado ANCHO tiles en pantalla (1 fila)
   POP BC
   DEC B                     ; Bucle vertical
   JP NZ, drawm16_yloop
 
   RET

Nótese que hemos integrado las rutinas de impresión de sprites 16×16 con sus correspondientes cálculos dentro del cuerpo de nuestra rutina de “mapeado”.

Por motivos de espacio (gran extensión de la rutina), la impresión de los 4 bloques gráficos se realiza en dos bucles aunque la versión definitiva de nuestro programa debería desenrollarlos con los siguientes cambios:


1.- Eliminar el INC L tras el DJNZ drawm16_loop1.

2.- Eliminar los INC DE, INC H y DEC L antes del DJNZ drawm16_loop2.

Por supuesto, si se conocen de antemano otros parámetros fijos de la rutina (ancho, alto, posición inicial, etc) podemos utilizar estos valores directamente (como constantes o incluso anticipar el cálculo de la dirección de pantalla inicial o usar una tabla de direcciones iniciales de tile precalculadas) para acelerar la ejecución de la misma.

No obstante, si estamos ante un juego de pantallas “fijas” y “estáticas”, el tiempo de dibujado de la pantalla será prácticamente inapreciable para el jugador, por lo que no suele ser necesario realizar optimizaciones extremas.


Ejemplo: Impresión de una pantalla 16x16

Juntemos en un mismo programa la rutina de impresión de pantallas de mapa en 16×16, la pantalla del nivel 1 de Sokoban y su tileset, y el siguiente código de test:

  ; Ejemplo impresion mapa de 16x16
  ORG 32768
 
  CALL ClearScreen_Pattern        ; Imprimimos patron de fondo
 
  LD HL, sokoban1_gfx
  LD (DM_SPRITES), HL
  LD HL, sokoban1_attr
  LD (DM_ATTRIBS), HL
  LD HL, sokoban_LEVEL1
  LD (DM_MAP), HL
  LD A, 16
  LD (DM_WIDTH), A                ; ANCHO
  LD A, 12
  LD (DM_HEIGHT), A               ; ALTO
  XOR A
  LD (DM_COORD_X), A              ; X = Y = 0
  LD (DM_COORD_Y), A              ; Establecemos valores llamada
 
  CALL DrawMap_16x16              ; Imprimir pantalla de mapa
 
loop:
  JR loop
 
 
;-----------------------------------------------------------------------
ClearScreen_Pattern:              ; Rutina para incluir:
   (...)                          ; Rellenado de fondo con un patron
   RET                            ; (del capitulo de Sprites Lowres)
 
 
;-----------------------------------------------------------------------
DM_SPRITES  EQU  50020
DM_ATTRIBS  EQU  50022
DM_MAP      EQU  50024
DM_COORD_X  EQU  50026
DM_COORD_Y  EQU  50027
DM_WIDTH    EQU  50028
DM_HEIGHT   EQU  50029
 
 
;-----------------------------------------------------------------------
; Level 1 from Sokoban:
;-----------------------------------------------------------------------
sokoban_LEVEL1: 
  DEFB 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255
  DEFB 255,255,255,255,255,255,2,3,1,4,255,255,255,255,255,255
  DEFB 255,255,255,255,1,2,3,0,0,5,4,255,255,255,255,255
  DEFB 255,255,255,255,4,0,6,6,0,0,5,255,255,255,255,255
  DEFB 255,255,255,255,5,0,0,6,0,0,4,255,255,255,255,255
  DEFB 255,255,255,255,4,0,0,0,0,0,5,255,255,255,255,255
  DEFB 255,255,255,255,5,2,3,0,0,2,3,255,255,255,255,255
  DEFB 255,255,255,255,255,255,1,0,0,0,4,255,255,255,255,255
  DEFB 255,255,255,255,255,255,4,7,7,7,5,255,255,255,255,255
  DEFB 255,255,255,255,255,255,5,2,3,2,3,255,255,255,255,255
  DEFB 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255
  DEFB 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255
 
 
;-----------------------------------------------------------------------
; ASM source file created by SevenuP v1.20 
; SevenuP (C) Copyright 2002-2006 by Jaime Tejedor Gomez, aka Metalbrain
; Pixel Size:      ( 16, 128) -   Char Size:       (  2,  16)
; Sort Priorities: X char, Char line, Y char
; Data Outputted:  Gfx / Attr
;-----------------------------------------------------------------------
 
sokoban1_gfx:
  DEFB   0,  0,  0,  0,  0,  0,  0,  0,   0,  0,  0,  0,  0,  0,  0,  0
  DEFB   0,  0,  0,  0,  0,  0,  0,  0,   0,  0,  0,  0,  0,  0,  0,  0
  DEFB 127,252,193, 86,152,  2,180,170, 173, 86,153,254,194,170,255,254
  DEFB   0,  0,102,102, 51, 50,153,152, 204,204,102,102, 51, 50,  0,  0
  DEFB 127,102,205, 76,151, 24,205, 50, 151,102,205, 76,151, 24,205, 50
  DEFB 131,102,153, 76,173, 24,181, 50, 153,102,195, 76,127, 24,  0,  0
  DEFB 255,252,255,134,255, 50,255, 90, 255,106,255, 50,255,134,255,254
  DEFB 255,254,255,254,255,254,255,250, 255,242,253,166,255,252,  0,  0
  DEFB 127,252,205,134,151, 50,205,106, 151, 90,205, 50,151,134,205,254
  DEFB 195,254,153,254,173,254,181,250, 153,242,195,166,127,252,  0,  0
  DEFB 255,254,255,254,255,254,255,254, 255,254,255,254,191,254,255,254
  DEFB 255,134,191, 50,255,106,191, 90, 159, 50,207,134,127,252,  0,  0
  DEFB   0,  0,127,254, 95,250, 96,  6, 111,182,111,118, 96,230,109,214
  DEFB 107,182,103,  6,110,246,109,246,  96,  6, 95,250,127,254,  0,  0
  DEFB   0,  0,123,222,123,222, 96,  6,  96,  6,  0,  0, 96,  6, 96,  6
  DEFB  96,  6, 96,  6,  0,  0, 96,  6,  96,  6,123,222,123,222,  0,  0
 
sokoban1_attr:
  DEFB   0,  0,  0,  0,  5,  5, 70, 70, 5, 70,  5, 70, 69, 71, 69, 71
  DEFB   5, 69, 69, 71, 69, 69, 71, 71, 2, 66, 66, 67,  6, 70, 70, 71

El resultado de la ejecución del anterior ejemplo es la siguiente pantalla:


 Programa de ejemplo impresion pantalla 16x16

Nótese que las pantallas de mapa no tienen por qué tener el tamaño exacto de 256×192 de la pantalla de TV. Nuestra pantalla anterior ocupa más memoria de la estrictamente necesaria, ya que gran parte de la información alrededor del “área de juego real” son bloques transparentes.

La misma pantalla codificada con un tamaño de 7×9 e impresa a partir de las coordenadas de pantalla (8,3) ocuparía 63 bytes en lugar de 192 y produciría el mismo resultado visual:

;-----------------------------------------------------------------------
; Level 1 from Sokoban (7x9):
;-----------------------------------------------------------------------
sokoban_LEVEL1: 
  DEFB 255,255,  2,  3,  4,  1,255
  DEFB   4,  2,  3,  0,  0,  5,  1
  DEFB   1,  0,  6,  6,  0,  0,  5
  DEFB   5,  0,  0,  6,  0,  0,  1
  DEFB   1,  0,  0,  0,  0,  0,  5
  DEFB   5,  2,  3,  0,  0,  2,  3
  DEFB 255,255,  4,  0,  0,  0,  1
  DEFB 255,255,  1,  7,  7,  7,  5
  DEFB 255,255,  5,  2,  3,  2,  3

La pantalla de 7×9 se imprimiría así:

  LD HL, sokoban1_gfx
  LD (DM_SPRITES), HL
  LD HL, sokoban1_attr
  LD (DM_ATTRIBS), HL
  LD HL, sokoban_LEVEL1
  LD (DM_MAP), HL
  LD A, 7
  LD (DM_WIDTH), A                ; ANCHO
  LD A, 9
  LD (DM_HEIGHT), A               ; ALTO
  LD A, 8
  LD (DM_COORD_X), A              ; X_INICIAL
  LD A, 3
  LD (DM_COORD_Y), A              ; Y_INICIAL
  CALL DrawMap_16x16              ; Imprimir pantalla de mapa

Si tenemos pantallas de diferentes tamaños podemos almacenarlas en memoria utilizando una estructura de datos con 4 bytes de información por pantalla: ancho, alto, x_inicial e y_inicial. De esta forma cada pantalla ocuparía en memoria sólo el espacio necesario para definirla, sin necesidad de que todas las pantallas se adapten a un tamaño común. Nuestra rutina de cambio de pantalla recogería estos 4 bytes de datos de la tabla de “tamaños y posiciones” para establecer los parámetros de entrada a DrawMap_16x16 y dibujar la pantalla en su posición correcta.

Esto puede valer para juegos como Sokoban (donde cada pantalla puede tener un tamaño diferente) pero no para juegos tipo plataformas/aventuras/arcade donde todas las pantallas tienen un tamaño fijo. En ese caso basta con usar nuestra rutina con unos valores fijos para todas ellas.


Rutina para bloques de 8x8 con mapeados horizontales

La rutina para tiles de 8×8 es fácilmente adaptable a partir de la rutina de 16×16 modificando los bucles de impresión y los cálculos (multiplicaciones y posicionamientos) para el nuevo tamaño de tile y atributo:

;---------------------------------------------------------------
; DrawMap_8x8:
; Imprime una pantalla de tiles de 8x8 pixeles.
;---------------------------------------------------------------
DrawMap_8x8:
 
   ;;;;;; Impresion de la parte grafica de los tiles ;;;;;;
   LD IX, (DM_MAP)           ; IX apunta al mapa 
   LD A, (DM_HEIGHT)
   LD B, A                   ; B = ALTO_EN_TILES (para bucle altura)
 
drawm8_yloop:
   PUSH BC                   ; Guardamos el valor de B
 
   LD A, (DM_HEIGHT)         ; A = ALTO_EN_TILES
   SUB B                     ; A = ALTO - iteracion_bucle = Y actual
                             ;;; NUEVO: Eliminamos RLCA (no multiplicar Y*2)
 
   ;;; Calculamos la direccion destino en pantalla como
   ;;; DIR_PANT = DIRECCION(X_INICIAL, Y_INICIAL + Y)
   LD BC, (DM_COORD_X)       ; B = DB_COORD_Y y C = DB_COORD_X
   ADD A, B
   LD B, A
   LD A, B
   AND $18
   ADD A, $40
   LD H, A
   LD A, B
   AND 7
   RRCA
   RRCA
   RRCA
   ADD A, C
   LD L, A                   ; HL = DIR_PANTALLA(X_INICIAL,Y_INICIAL+Y*2)
 
   LD A, (DM_WIDTH)
   LD B, A                   ; B = ANCHO_EN_TILES
 
drawm8_xloop:
   PUSH BC                   ; Nos guardamos el contador del bucle
 
   LD A, (IX+0)              ; Leemos un byte del mapa   
   INC IX                    ; Apuntamos al siguiente byte del mapa
 
   CP 255                    ; Bloque especial a saltar: no se dibuja
   JP Z, drawm8_next
 
   LD B, A
   EX AF, AF'                ; Nos guardamos una copia del bloque en A'
   LD A, B
 
   ;;; Calcular posicion origen (array sprites) en HL como:
   ;;;     direccion = base_sprites + (NUM_SPRITE*8)
   EX DE, HL                 ; Intercambiamos DE y HL (DE=destino)
   LD BC, (DM_SPRITES)
   LD L, A
   LD H, 0
   ADD HL, HL
   ADD HL, HL
   ADD HL, HL                ;;; NUEVO: NUM_SPRITE*8 en lugar de *32
   ADD HL, BC                ; HL = BC + HL = DM_SPRITES + (DM_NUMSPR * 8)
   EX DE, HL                 ; Intercambiamos DE y HL (DE=origen, HL=destino)
 
   PUSH HL                   ; Guardamos el puntero a pantalla recien calculado
   PUSH HL
 
   ;;; Impresion de los primeros 2 bloques horizontales del tile
   LD B, 8
 
drawm8_loop:                 ;;; NUEVO: Bucle de impresion de 1 solo bloque
   LD A, (DE)                ; Bloque 1: Leemos dato del sprite
   LD (HL), A                ; Copiamos dato a pantalla
   INC DE                    ; Incrementar puntero en sprite
   INC H                     ; Hay que sumar 256 para ir al siguiente scanline
   DJNZ drawm8_loop          
 
   ;;; En este punto, los 8 scanlines del tile estan dibujados.
 
   ;;;;;; Impresion de la parte de atributos del tile ;;;;;;
   POP HL                    ; Recuperar puntero a inicio de tile
 
   ;;; Calcular posicion destino en area de atributos en DE.
   LD A, H                   ; Codigo de Get_Attr_Offset_From_Image
   RRCA
   RRCA
   RRCA
   AND 3
   OR $58
   LD D, A
   LD E, L                   ; DE tiene el offset del attr de HL
 
   LD HL, (DM_ATTRIBS)
   EX AF, AF'                ; Recuperamos el bloque del mapa desde A'
   LD C, A
   LD B, 0                   ;;; NUEVO: HL = HL+DM_NUMSPR (NO *4)
   ADD HL, BC                ; HL = HL+DM_NUMSPR = Origen de atributo
 
   LD A, (HL)
   LD (DE), A                ;;; NUEVO: Impresion de un unico atributo.
   POP HL                    ; Recuperamos el puntero al inicio
 
drawm8_next:
   INC L                     ; Avanzamos al siguiente tile en pantalla
 
   POP BC                    ; Recuperamos el contador para el bucle
   DEC B                     ; DJNZ se sale de rango, hay que usar DEC+JP
   JP NZ, drawm8_xloop
 
   ;;; En este punto, hemos dibujado ANCHO tiles en pantalla (1 fila)
   POP BC
   DEC B                     ; Bucle vertical
   JP NZ, drawm8_yloop
 
   RET


Ya conocemos la forma de diseñar e imprimir pantallas individuales de mapeado, pero seguimos necesitando agrupar estas pantallas para componer un mapa de tamaño superior al del área de visión del jugador. En esta sección veremos las estructuras de datos necesarias para definir el mapeado total del juego así como la interconexión entre las diferentes pantallas.


Estructura de datos del mapa

Un mapa de tiles es la representación de un mapeado mediante la utilización de tiles, donde el ancho y alto del mapa es mayor que el tamaño de una pantalla individual.

Las pantallas se interconectan formando un mapa. Esta conexión puede ser:


  • Estática: El jugador cambia de una pantalla a otra al acabar el nivel, sin que las pantallas estén “conectadas” realmente entre sí salvo por el número de nivel actual (Ejemplo: Manic Miner, Sokoban, Bubble Bobble, Puzznic, Plotting…).
  • Lineal en un sólo eje: Las pantallas se agrupan una a continuación de la otra con desarrollo del juego en una dirección, ya sea izquierda-derecha (R-Type, Game Over, Target Renegade…) o arriba-abajo (Flying Shark, Commando…).
  • Lineal en dos ejes: Las pantallas se agrupan en forma de mapa bidimensional, permitiendo al jugador moverse en el mapeado hacia arriba, abajo, izquierda o derecha (Sabre Wulf, Las Tres Luces de Glaurung, Atic Atac, Sir Fred…).


La pantalla es una ventana dentro del mapeado, por lo que resulta necesario disponer de una estructura de datos que nos permita representar los 3 modelos de mapa que acabamos de describir.

Hay 2 posibilidades de agrupación de las pantallas: como un array de pantallas, o como una matriz global de mapeado.


Mapa como array de pantallas

El mapa como array de pantallas consiste en la creación de un array con las direcciones de datos de cada pantalla de forma que podamos direccionarlo con la variable Pantalla_Actual para obtener la dirección donde están los datos a imprimir.

Para hacer uso de este sistema debemos almacenar cada pantalla en memoria con una etiqueta identificativa para nuestro programa ensamblador:

Pantalla_Inicio:
  DB 0, 0, 0, 3, (...)
 
Pantalla_Salon:
  DB 1, 2, 3, 4, (...)
 
Pantalla_Pasillo:
  DB 2, 2, 2, 2, (...)
 
Pantalla_Escalera:
  DB 4, 4, 5, 1, (...)
 
(...)

A continuación, definimos una tabla “Mapa” que contenga las direcciones de inicio de los datos de cada pantalla:

Mapa:
  DW Pantalla_Inicio         ; Pantalla 0
  DW Pantalla_Salon          ; Pantalla 1
  DW Pantalla_Pasillo        ; Pantalla 2
  DW Pantalla_Escalera       ; Pantalla 3
  (...)                      ; (etc...)
  DW 0000                    ; Fin de pantalla

Nuestro juego almacenará el identificador de la pantalla actual en una variable de programa (por ejemplo, ID_PANTALLA). De esta forma podemos acceder a los datos de cada pantalla utilizando ID_PANTALLA como índice en esta tabla:

DIR_DATOS_PANTALLA = Mapa[ ID_PANTALLA ]

O lo que es lo mismo, desplazándonos 2 bytes desde el inicio de nuestro mapa (ya que cada dirección ocupa 2 bytes) y leyendo el contenido de la dirección resultante:

DIR_DATOS_PANTALLA = [ Mapa + (ID_PANTALLA * 2) ]

Traducido a código, para obtener la dirección donde se aloja la pantalla actual de juego, asumiendo que su identificador 0-N estuviera en la variable de memoria de 8 bits pantalla_actual:

  ;;; Calculamos la posicion de "pantalla_actual" en al tabla
  LD BC, Mapa                     ; BC = Inicio de la tabla Mapa
  LD A, (pantalla_actual)         ; A = Pantalla actual
  LD L, A
  LD H, 0                         ; HL = Pantalla actual
  SLA L
  RL  H                           ; HL = Pantalla actual * 2
  ADD HL, BC                      ; HL = Mapa + (Pantalla actual * 2)
 
  ;;; Ahora leemos de (HL) la dirección de dibujado en el mismo HL
  LD A, (HL)                      ; Leemos la parte baja de la direccion en A
  INC HL                          ; ... para no corromper HL y poder leer ...
  PUSH HL
  LD H, (HL)                      ; ... la parte alta sobre H ...
  LD L, A
  LD (DM_MAP), HL                 ; Almacenamos el mapa a imprimir
  CALL DrawMap_16x16              ; Imprimimos el mapa

Para un juego de pantallas no conectadas (tipo Manic Miner o Sokoban), el movimiento de una pantalla a otra se basaría en incrementar el valor de ID_PANTALLA cada vez que el jugador progrese un nivel en el juego, o poner ID_PANTALLA = 0 cuando se finalice el juego o se inicie una nueva partida.

En juegos con desplazamiento en una única dirección (R-Type, Flying Shark, Game Over…), el desplazamiento a la pantalla anterior o siguiente se realizará decrementando ID_PANTALLA si ID_PANTALLA no es cero (límite izquierdo/inferior), o incrementando ID_PANTALLA si el valor Mapa[ID_PANTALLA] es menor que el número máximo de pantallas (límite derecho/superior).

Para juegos con mapeados bidimensionales que requieran especificar una conexión concreta entre pantallas, podemos agregar a nuestro vector de mapeado bytes adicionales que indiquen los identificadores de las pantallas a las que deberíamos movernos si vamos en una determinada dirección.

Por ejemplo, en un hipotético juego que se desarrolle en una sóla dirección (ej: izquierda-derecha) necesitaremos almacenar los IDs de las pantallas que tenemos a izquierda y a derecha de la pantalla actual:

;;; Vector de direcciones de pantalla.
;;; Contiene la direccion de cada pantalla en orden de ID,
;;; seguido de los IDs de las pantallas de su izquierda y
;;; su derecha. Se utiliza -1 para definir que no hay
;;; conexion con otras pantallas:
;;;
;;; Formato de cada pantalla + conexiones: 
;;;
;;;  DW DIR_DATOSPANTALLA
;;;  DB ID_IZQUIERDA, ID_DERECHA
 
Mapa:
  DW Pantalla_Inicio            ; ID = 0
  DB -1, 1                      ; Conexiones izq y derecha ID 0
  DW Pantalla_Salon             ; ID = 1 
  DB 0, 2                       ; Conexiones izq y derecha ID 1
  DW Pantalla_Pasillo           ; ID = 2
  DB 1, 3                       ; Conexiones izq y derecha ID 2
  DW Pantalla_Escalera          ; ID = 3
  DB 3, -1                      ; Conexiones izq y derecha ID 3
  (...)

Para acceder ahora a los datos de una pantalla debemos desplazarnos 2 bytes por la dirección y 2 bytes por los 2 identificadores, es decir, un total de 4 bytes:

DIR_DATOS_PANTALLA    = [ Mapa + (ID_PANTALLA * 4) ]
ID_PANTALLA_IZQUIERDA = [ Mapa + (ID_PANTALLA * 4) + 2 ]
ID_PANTALLA_DERECHA   = [ Mapa + (ID_PANTALLA * 4) + 3 ]

Traducido a código, basta con multiplicar ID_PANTALLA por 2, 4 ó 6 (según la cantidad de IDs de conexión que tengamos definidos en el mapa), apuntar HL, DE o IX a “Mapa”, sumarle el valor de la multiplicación y leer los 6 bytes consecutivos incrementando este puntero.

BYTES_POR_PANTALLA     EQU   4
RUTINA_ROM_HL_POR_DE   EQU   $30A9
 
;------------------------------------------------------------
; Obtener direccion donde se alojan los datos de la pantalla
; Entrada: 
;    L = pantalla
;   BC = Mapa (direccion base)
; Salida:
;   HL = Direccion de datos de la pantalla
;------------------------------------------------------------
Get_Screen_Pointer:
  LD H, 0
  LD D, H                         ; HL = PANTALLA
  LD E, BYTES_POR_PANTALLA        ; DE = BYTES POR PANTALLA
  CALL RUTINA_ROM_HL_POR_DE       ; HL = HL * DE
  ADD HL, BC                      ; Lo sumamos al inicio del MAPA
  RET                             ; HL = MAPA + (PANTALLA*BYTES)

Para realizar la multiplicación hemos utilizado la rutina HL=HL*DE de la ROM del Spectrum. Si no estamos en un Spectrum sino que estamos programando para otro sistema Z80, bastará con llamar a la rutina de multiplicación adecuada.

Podríamos haber realizado la multiplicación por 4 mediante desplazamientos, pero utilizando una rutina de multiplicación nos aseguramos que Get_Screen_Pointer pueda ser utilizado para mapas que definan más conexiones.

Ahora ya podemos acceder a los datos de una pantalla concreta:

  ;;; En el inicio del programa...
  LD HL, sokoban1_gfx
  LD (DM_SPRITES), HL
  LD HL, sokoban1_attr
  LD (DM_ATTRIBS), HL
  LD A, 16
  LD (DM_WIDTH), A                ; ANCHO
  LD A, 12
  LD (DM_HEIGHT), A               ; ALTO
  XOR A
  LD (DM_COORD_X), A              ; X = Y = 0
  LD (DM_COORD_Y), A              ; Establecemos valores llamada
 
  (...)
 
  ;;; En el bucle principal de nuestro programa:
DibujarPantalla:
  LD BC, Mapa
  LD A, (pantalla_actual)
  LD L, A
  CALL Get_Screen_Pointer         ; HL = Datos de la pantalla
  LD A, (HL)                      ; Leemos la parte baja de la direccion en A
  INC HL                          ; ... para no corromper HL y poder leer ...
  PUSH HL
  LD H, (HL)                      ; ... la parte alta sobre H ...
  LD L, A
  LD (DM_MAP), HL                 ; Almacenamos el mapa a imprimir
  CALL DrawMap_16x16              ; Imprimimos el mapa
 
  POP HL                          ; Recuperamos el puntero a datos de pantalla
  INC HL                          ; Avanzamos hasta el primer ID de conexion
  LD A, (HL)                      ; Leemos conexion izquierda
  LD (con_izquierda), A           ; la almacenamos
  INC HL                          ; Avanzamos hasta el segundo ID de conexion
  LD A, (HL)                      ; Leemos conexion a derecha
  LD (con_derecha), A             ; la almacenamos

Con los datos en las variables con_izquierda y con_derecha podemos movernos a una de las 2 pantallas cambiando el valor de pantalla_actual al de la pantalla correspondiente.

Nótese que la “costosa” multiplicación genérica se puede sustituir por desplazamientos (*2, *4…) si separamos la tabla de pantallas en una tabla de direcciones y otra de conexiones:

Mapa:
  DW Pantalla_Inicio            ; ID = 0
  DW Pantalla_Salon             ; ID = 1 
  DW Pantalla_Pasillo           ; ID = 2
  DW Pantalla_Escalera          ; ID = 3
  (...)
 
Conexiones:
  DB -1, 1                      ; Conexiones izq y derecha ID 0
  DB 0, 2                       ; Conexiones izq y derecha ID 1
  DB 1, 3                       ; Conexiones izq y derecha ID 2
  DB 3, -1                      ; Conexiones izq y derecha ID 3
  (...)

De esta forma podemos calcular las posiciones de los datos que necesitamos con simples operaciones de desplazamiento. No obstante, tener los datos de las pantallas separados en 2 o más tablas es más “complicado” de mantener manualmente a menos que estemos generado estas estructuras de mapa con algún programa propio de diseño y exportación de mapeados que nos permita su exportación a este formado. Teniendo los datos por separado es más “complicado” (o, al menos, no tan intuitivo) hacer cambios manuales en el código.

Si estuvieramos hablando de un juego con “scroll” de pantallas en las 4 direcciones, bastaría con definir 4 identificadores de conexión tras cada dirección de pantalla, y obtener los datos de cada pantalla saltando 2+1+1+1+1 = 6 bytes por cada pantalla:

DIR_DATOS_PANTALLA    = [ Mapa + (ID_PANTALLA * 6) ]
ID_PANTALLA_IZQUIERDA = [ Mapa + (ID_PANTALLA * 4) + 2 ]
ID_PANTALLA_DERECHA   = [ Mapa + (ID_PANTALLA * 4) + 3 ]
ID_PANTALLA_ARRIBA    = [ Mapa + (ID_PANTALLA * 4) + 4 ]
ID_PANTALLA_ABAJO     = [ Mapa + (ID_PANTALLA * 4) + 5 ]

(O, en caso de usar tablas separadas, se realizaría un desplazamiento a la izquierda para multiplicar por 2 en la tabla de direcciones, y 2 desplazamientos para multiplicar por 4 en la de conexiones, como ya hemos visto en un ejemplo anterior).

El movimiento por el mapa se basaría en establecer ID_PANTALLA a cualquiera de los cuatro valores siempre que estos sean distintos de 255 (-1).

Nótese que estamos asumiendo que no hay más de 254 pantallas. En caso de requerir un mayor número de pantallas, el identificador de tile deberá ser de 16 bits por lo que la definición de los identificadores de conexión sería de tipo DW en lugar de DB y cambiarían los valores de las multiplicaciones:

Mapa:
  DW Pantalla_Inicio            ; ID = 0
  DW -1, 1                      ; Conexiones izq y derecha ID 0

Multiplicamos por 6 ya que ahora cada pantalla ocupa 6 bytes (2 de la dirección y 4 de los identificadores de conexión).

Si necesitaramos definir más datos de la pantalla (como por ejemplo, el “título” de la misma, al estilo Manic Miner), podríamos añadir a nuestra estructura 2 bytes con la dirección en memoria de una cadena de texto (una dirección para cada pantalla individual del mapa.) Esto implicaría modificar la cantidad de bytes por los que se multiplica para obtener la dirección que contiene los datos de una pantalla concreta:

;;; Vector de direcciones de pantalla.
Mapa:
  DW Pantalla_Inicio            ; ID = 0
  DB -1, 1                      ; Conexiones izq y derecha ID 0
  DW titulo_inicio              ; Direccion de la cadena de titulo
  DW Pantalla_Salon             ; ID = 1 
  DB 0, 2                       ; Conexiones izq y derecha ID 1
  DW titulo_salon               ; Direccion de la cadena de titulo
  DW Pantalla_Pasillo           ; ID = 2
  DB 1, 3                       ; Conexiones izq y derecha ID 2
  DW titulo_pasillo
  DW Pantalla_Escalera          ; ID = 3
  DW titulo_escalera
  DB 3, -1                      ; Conexiones izq y derecha ID 3
  (...)
 
titulo_inicio   DB "La pantalla de inicio", 0
titulo_salon    DB "El salon", 0
titulo_pasillo  DB "El pasillo", 0
titulo_escalera DB "La escalera", 0

Con estos 2 bytes adicionales de nombre por cada pantalla a los 4 que ya se utilizaban, el anterior ejemplo requeriría el siguiente cálculo para acceder a los datos de una pantalla concreta:

DIR_DATOS_PANTALLA    = [ Mapa + (ID_PANTALLA * 6) ]
ID_PANTALLA_IZQUIERDA = [ Mapa + (ID_PANTALLA * 6) + 2 ]
ID_PANTALLA_DERECHA   = [ Mapa + (ID_PANTALLA * 6) + 3 ]
TITULO_PANTALLA       = [ Mapa + (ID_PANTALLA * 6) + 4 ]

De nuevo, como vimos antes, es posible mantener los datos las pantallas en diferentes tablas (tabla de direccion de datos, tabla de conexiones, tabla de títulos) para facilitar el acceso a los mismos vía operaciones de desplazamiento, aunque si la obtención de estos datos no es prioritaria, el poder acceder a ellos mediante una única operación y un único puntero puede acabar resultando más rápido (o simplemente, cómodo) que en múltiples tablas.



Repetición de pantallas

Gracias a nuestro mapa definido como “vector” de pantallas podemos repetir pantallas en nuestro mapeado sin duplicar los datos gráficos.

Por ejemplo, si en nuestro hipotético juego de desarrollo lineal izquierda-derecha tenemos 3 pasillos iguales, podemos duplicar las entradas de pantalla con diferentes identificadores de conexión:

Mapa:
  DW Pantalla_Inicio            ; ID = 0
  DB -1, 1                      ; Conexiones izq y derecha ID 0
  DW Pantalla_Salon             ; ID = 1 
  DB 0, 2                       ; Conexiones izq y derecha ID 1
  DW Pantalla_Pasillo           ; ID = 2
  DB 1, 3                       ; Conexiones izq y derecha ID 2
  DW Pantalla_Pasillo           ; ID = 3
  DB 2, 4                       ; Conexiones izq y derecha ID 3
  DW Pantalla_Pasillo           ; ID = 4
  DB 3, 5                       ; Conexiones izq y derecha ID 4
  DW Pantalla_Escalera          ; ID = 5
  DB 4, -1                      ; Conexiones izq y derecha ID 5
  (...)

De esta forma hemos definido 3 pantallas de nuestro juego mediante un mismo bloque de datos de tiles. Hemos creado 3 “pasillos” consecutivos, y nada nos impide volver a utilizar este mismo bloque en otros áreas del mapeado que también tengan pasillos.

Con este tipo de trucos podemos exprimir la escasa memoria de nuestro Spectrum y crear mapeados de gran tamaño.



Transiciones entre 2 pantallas de mapa

Cuando el jugador pasa de una pantalla a otra debemos realizar el borrado de la pantalla en curso y la impresión de la nueva. Para realizar esto tenemos diferentes opciones:


  • Transición abrupta: Simplemente realizamos un borrado del área de pantalla donde se dibuja el mapa y trazamos sobre ese área los datos de la siguiente pantalla. Es el método más común (y el más rápido), utilizado en la mayoría de juegos.
  • Fundido de pantalla: Realizamos un fundido de la pantalla actual a negro, dibujamos la nueva pantalla con atributos a negro, y realizamos un fundido desde negro a los nuevos atributos.
  • Scroll de pantallas: Realizamos un scroll entre la pantalla que sale y la que entra. Consiste en realizar una “salida” de la pantalla actual en la dirección contraria de los límites cruzados por el personaje (por ejemplo: si éste sale por la derecha, scrolleamos la pantalla actual hacia la izquierda), de forma que para cada línea de la pantalla saliente aparezca por la derecha una línea del mapa entrante (la nueva pantalla actual). Este scroll, que debe de ser rápido para no ralentizar el cambio de pantallas en el juego, requiere la realización de 4 rutinas específicas que realicen scroll de una porción de la pantalla y que permitan dibujar N filas o N columnas de la nueva pantalla entrante a partir de una posición (x,y) dada.



El mapa como array global de mapeado

Existe una alternativa a la definir cada pantalla de juego por separado, y es la de disponer de una matriz global que comprenda todo el mapeado. Esta matriz contiene todos los datos de las pantallas del mapa, linealmente:

;;; Sea un juego de 8x8 bloques por pantalla
;;; formado por un mapa de 2x2 pantallas.
;;;
;;; Sean "A" los datos de la primera pantalla.
;;; Sean "B" los datos de la segunda pantalla.
;;; Sean "C" los datos de la tercera pantalla.
;;; Sean "D" los datos de la tercera pantalla.
 
Mapa
 DB A,A,A,A,A,A,A,A,B,B,B,B,B,B,B,B
 DB A,A,A,A,A,A,A,A,B,B,B,B,B,B,B,B
 DB A,A,A,A,A,A,A,A,B,B,B,B,B,B,B,B
 DB A,A,A,A,A,A,A,A,B,B,B,B,B,B,B,B
 DB A,A,A,A,A,A,A,A,B,B,B,B,B,B,B,B
 DB A,A,A,A,A,A,A,A,B,B,B,B,B,B,B,B
 DB A,A,A,A,A,A,A,A,B,B,B,B,B,B,B,B
 DB A,A,A,A,A,A,A,A,B,B,B,B,B,B,B,B
 DB C,C,C,C,C,C,C,C,D,D,D,D,D,D,D,D
 DB C,C,C,C,C,C,C,C,D,D,D,D,D,D,D,D
 DB C,C,C,C,C,C,C,C,D,D,D,D,D,D,D,D
 DB C,C,C,C,C,C,C,C,D,D,D,D,D,D,D,D
 DB C,C,C,C,C,C,C,C,D,D,D,D,D,D,D,D
 DB C,C,C,C,C,C,C,C,D,D,D,D,D,D,D,D
 DB C,C,C,C,C,C,C,C,D,D,D,D,D,D,D,D
 DB C,C,C,C,C,C,C,C,D,D,D,D,D,D,D,D

Nosotros sólo vemos un área de 8×8 bloques del anterior mapeado, que inicialmente podría ser, por ejemplo, la que comprende todos los bloques “A”. Se utiliza un puntero 2D “xmapa,ymapa” o uno lineal (mapa_pos) para conocer la posición de la “ventana de visión” de 8×8 bloques (en nuestro ejemplo) dentro del mapa.

Podemos “movernos” en el mapa simplemente modificando las coordenadas del puntero de la ventana de visión y redibujando en pantalla el área de 8×8 bloques que comienza en dicho puntero.

Por ejemplo, supongamos que la posición inicial del área de visión es (0,0), con lo que la primera pantalla impresa serán los 8×8 bloques “A” del ejemplo. Para avanzar hacia la derecha basta con incrementar el puntero “xmapa” con lo que la siguiente impresión de la pantalla mostrará 7 columnas “A” y una columna “B” en el extremo derecho de la pantalla.

La impresión de este tipo de pantallas requiere una rutina similar a las rutinas de impresión sin agrupación que ya hemos visto (DrawMap_16x16), pero modificada en los siguientes términos:


  • Cambio 1: Debe de calcular la posición inicial de lectura de datos para la impresión como Mapa + (ymapa*ANCHO_MAPA) + xmapa.
  • Cambio 2: Una vez impreso un scanline horizontal de ANCHO_PANTALLA datos, debe de avanzar el registro usado como puntero de datos en el mapa un total de ANCHO_MAPA-ANCHO_PANTALLA bytes (para posicionarse en el siguiente scanline de datos del mapa).


La rutina de impresión de este tipo de mapas tiene el primero de los cambios descritos al principio de la misma:

DrawMap_16x16_Map:
 
   LD IX, (DM_MAP)           ; IX apunta al mapa 
 
   ;;; NUEVO: Posicionamos el puntero de mapa en posicion inicial.
   LD HL, (DM_MAPY)
   LD DE, ANCHO_MAPA_TILES
   CALL MULT_HL_POR_DE       ; HL = (ANCHO_MAPA * MAPA_Y)
   LD BC, (DM_MAPX)
   ADD HL, BC                ; HL = MAPA_X + (ANCHO_MAPA * MAPA_Y)
   EX DE, HL
   ADD IX, DE                ; IX = Inicio_Mapa + HL
   ;;; FIN NUEVO

El segundo de los cambios está localizado al final de la rutina, al final de cada iteración de scanline horizontal:

   ;;; NUEVO: Incrementar puntero de mapa a siguiente linea
   LD BC, ANCHO_MAPA_TILES - ANCHO_PANTALLA
   ADD IX, BC
   ;;; FIN NUEVO
 
   ;;; En este punto, hemos dibujado ANCHO tiles en pantalla (1 fila)
   POP BC
   DEC B                     ; Bucle vertical
   JP NZ, drawmg16_yloop
 
   RET

La rutina completa es la siguiente:

;-------------------------------------------------------------
DM_SPRITES  EQU  50020
DM_ATTRIBS  EQU  50022
DM_MAP      EQU  50024
DM_COORD_X  EQU  50026
DM_COORD_Y  EQU  50027
DM_WIDTH    EQU  50028
DM_HEIGHT   EQU  50029
DM_MAPX     EQU  50030
DM_MAPY     EQU  50032
 
;-------------------------------------------------------------
; Algunos valores hardcodeados para el ejemplo, en la rutina
; final se puede utilizar DM_WIDTH y DM_HEIGHT.
;-------------------------------------------------------------
ANCHO_MAPA_TILES       EQU   32
ALTO_MAPA_TILES        EQU   24
ANCHO_PANTALLA         EQU   14
ALTO_PANTALLA          EQU   11
 
;;; Rutina de la ROM del Spectrum, en otros sistemas 
;;; sustituir por una rutina especifica de multiplicacion
MULT_HL_POR_DE         EQU   $30A9
 
 
;---------------------------------------------------------------
; DrawMap_16x16_Map:
; Imprime una pantalla de tiles de 16x16 pixeles.
;
; Entrada (paso por parametros en memoria):
; Direccion             Parametro
; --------------------------------------------------------------
; DM_SPRITES (2 bytes)  Direccion de la tabla de tiles.
; DM_ATTRIBS (2 bytes)  Direccion de la tabla de atributos.
; DM_MAP     (2 bytes)  Direccion de la pantalla en memoria.
; DM_COORD_X (1 byte)   Coordenada X-Inicial en baja resolucion.
; DM_COORD_Y (1 byte)   Coordenada Y-Inicial en baja resolucion.
; DM_WIDTH   (1 byte)   Ancho del mapa en tiles
; DM_HEIGHT  (1 byte)   Alto del mapa en tiles
; DM_MAPX    (2 bytes)  Coordenada X en mapa.
; DM_MAPY    (2 bytes)  Coordenada Y en mapa.
;---------------------------------------------------------------
DrawMap_16x16_Map:
 
   LD IX, (DM_MAP)           ; IX apunta al mapa 
 
   ;;; NUEVO: Posicionamos el puntero de mapa en posicion inicial.
   LD HL, (DM_MAPY)
   LD DE, ANCHO_MAPA_TILES
   CALL MULT_HL_POR_DE       ; HL = (ANCHO_MAPA * MAPA_Y)
   LD BC, (DM_MAPX)
   ADD HL, BC                ; HL = MAPA_X + (ANCHO_MAPA * MAPA_Y)
   EX DE, HL
   ADD IX, DE                ; IX = Inicio_Mapa + HL
   ;;; FIN NUEVO
 
   LD A, (DM_HEIGHT)
   LD B, A                   ; B = ALTO_EN_TILES (para bucle altura)
 
drawmg16_yloop:
   PUSH BC                   ; Guardamos el valor de B
 
   LD A, (DM_HEIGHT)         ; A = ALTO_EN_TILES
   SUB B                     ; A = ALTO - iteracion_bucle = Y actual
   RLCA                      ; A = Y * 2
 
   ;;; Calculamos la direccion destino en pantalla como
   ;;; DIR_PANT = DIRECCION(X_INICIAL, Y_INICIAL + Y*2)
   LD BC, (DM_COORD_X)       ; B = DB_COORD_Y y C = DB_COORD_X
   ADD A, B
   LD B, A
   LD A, B
   AND $18
   ADD A, $40
   LD H, A
   LD A, B
   AND 7
   RRCA
   RRCA
   RRCA
   ADD A, C
   LD L, A                   ; HL = DIR_PANTALLA(X_INICIAL,Y_INICIAL+Y*2)
 
   LD A, (DM_WIDTH)
   LD B, A                   ; B = ANCHO_EN_TILES
 
drawmg16_xloop:
   PUSH BC                   ; Nos guardamos el contador del bucle
 
   LD A, (IX+0)              ; Leemos un byte del mapa   
   INC IX                    ; Apuntamos al siguiente byte del mapa
 
   CP 255                    ; Bloque especial a saltar: no se dibuja
   JP Z, drawmg16_next
 
   LD B, A
   EX AF, AF'                ; Nos guardamos una copia del bloque en A'
   LD A, B
 
   ;;; Calcular posicion origen (array sprites) en HL como:
   ;;;     direccion = base_sprites + (NUM_SPRITE*32)
   EX DE, HL                 ; Intercambiamos DE y HL (DE=destino)
   LD BC, (DM_SPRITES)
   LD L, 0
   SRL A
   RR L
   RRA
   RR L
   RRA
   RR L
   LD H, A
   ADD HL, BC                ; HL = BC + HL = DM_SPRITES + (DM_NUMSPR * 32)
   EX DE, HL                 ; Intercambiamos DE y HL (DE=origen, HL=destino)
 
   PUSH HL                   ; Guardamos el puntero a pantalla recien calculado
   PUSH HL
 
   ;;; Impresion de los primeros 2 bloques horizontales del tile
 
   LD B, 8
drawmg16_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
   LD (HL), A                ; Copiamos dato a pantalla
   INC DE                    ; Incrementar puntero en sprite
   INC H                     ; Hay que sumar 256 para ir al siguiente scanline
   DEC L                     ; pero hay que restar el INC L que hicimos.
   DJNZ drawmg16_loop1
   INC L                     ; Decrementar el ultimo incrementado en el bucle
 
   ; 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
   LD A, L
   ADD A, 31
   LD L, A
   JR C, drawmg16_nofix_abajop
   LD A, H
   SUB 8
   LD H, A
drawmg16_nofix_abajop:
 
   ;;; Impresion de los segundos 2 bloques horizontales:
   LD B, 8
drawmg16_loop2:
   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
   LD (HL), A                ; Copiamos dato a pantalla
   INC DE                    ; Incrementar puntero en sprite
   INC H                     ; Hay que sumar 256 para ir al siguiente scanline
   DEC L                     ; pero hay que restar el INC L que hicimos.
   DJNZ drawmg16_loop2
 
   ;;; En este punto, los 16 scanlines del tile estan dibujados.
 
   ;;;;;; Impresion de la parte de atributos del tile ;;;;;;
 
   POP HL                    ; Recuperar puntero a inicio de tile
 
   ;;; Calcular posicion destino en area de atributos en DE.
   LD A, H                   ; Codigo de Get_Attr_Offset_From_Image
   RRCA
   RRCA
   RRCA
   AND 3
   OR $58
   LD D, A
   LD E, L                   ; DE tiene el offset del attr de HL
 
   LD HL, (DM_ATTRIBS)
   EX AF, AF'                ; Recuperamos el bloque del mapa desde A'
   LD C, A
   LD B, 0
   ADD HL, BC
   ADD HL, BC
   ADD HL, BC
   ADD HL, BC                ; HL = HL+HL=(DM_NUMSPR*4) = Origen de atributo
 
   LDI
   LDI                       ; Imprimimos la primeras fila de atributos
 
   ;;; Avance diferencial a la siguiente linea de atributos
   LD A, E                   ; A = E
   ADD A, 30                 ; Sumamos A = A + 30 mas los 2 INCs de LDI.
   LD E, A                   ; Guardamos en E (E = E+30 + 2 por LDI=E+32)
   JR NC, drawmg16_att_noinc
   INC D
drawmg16_att_noinc:
   LDI
   LDI                       ; Imprimimos la segunda fila de atributos
 
   POP HL                    ; Recuperamos el puntero al inicio
 
drawmg16_next:
   INC L                     ; Avanzamos al siguiente tile en pantalla
   INC L                     ; horizontalmente
 
   POP BC                    ; Recuperamos el contador para el bucle
   DEC B                     ; DJNZ se sale de rango, hay que usar DEC+JP
   JP NZ, drawmg16_xloop
 
   ;;; NUEVO: Incrementar puntero de mapa a siguiente linea
   LD BC, ANCHO_MAPA_TILES - ANCHO_PANTALLA
   ADD IX, BC
   ;;; FIN NUEVO
 
   ;;; En este punto, hemos dibujado ANCHO tiles en pantalla (1 fila)
   POP BC
   DEC B                     ; Bucle vertical
   JP NZ, drawmg16_yloop
 
   RET

Además es necesario realizar rutinas adicionales que gestionen el movimiento por pantalla alterando DM_MAPX y DM_MAPY sin permitir incrementarlos más allá de (Ancho_Mapa-Ancho_Pantalla) y (Alto_Mapa-Alto_Pantalla) o decrementarlos por debajo de cero:

;-------------------------------------------------------------
; Incrementar la variable DM_MAPX para scrollear a la derecha.
;-------------------------------------------------------------
Map_Inc_X:
  LD HL, (DM_MAPX)
 
  ;;; Comparacion 16 bits de HL y (ANCHO_MAPA-ANCHO_PANTALLA)
  LD A, H
  CP (ANCHO_MAPA_TILES-ANCHO_PANTALLA) / 256
  RET NZ
  LD A, L
  CP (ANCHO_MAPA_TILES-ANCHO_PANTALLA) % 256
  RET Z
 
  INC HL                     ; No eran iguales, podemos incrementar.
  LD (DM_MAPX), HL
  RET
 
;-------------------------------------------------------------
; Incrementar la variable DM_MAPY para scrollear hacia abajo.
;-------------------------------------------------------------
Map_Inc_Y:
  LD HL, (DM_MAPY)
 
  ;;; Comparacion 16 bits de HL y (ALTO_MAPA-ALTO_PANTALLA)
  LD A, H
  CP (ALTO_MAPA_TILES-ALTO_PANTALLA) / 256
  RET NZ
  LD A, L
  CP (ALTO_MAPA_TILES-ALTO_PANTALLA) % 256
  RET Z
 
  INC HL                     ; No eran iguales, podemos incrementar.
  LD (DM_MAPY), HL
  RET
 
;-------------------------------------------------------------
; Decrementar la variable DM_MAPX para scrollear a la izq.
;-------------------------------------------------------------
Map_Dec_X:
  LD HL, (DM_MAPX)
  LD A, H
  AND A
  JR NZ, mapdecx_doit        ; Verificamos que DM_MAPX no sea 0
  LD A, L
  AND A
  RET Z
mapdecx_doit:
  DEC HL
  LD (DM_MAPX), HL           ; No es cero, podemos decrementar
  RET
 
;-------------------------------------------------------------
; Decrementar la variable DM_MAPY para scrollear hacia arriba.
;-------------------------------------------------------------
Map_Dec_Y:
  LD HL, (DM_MAPY)
  LD A, H
  AND A
  JR NZ, mapdecy_doit        ; Verificamos que DM_MAPX no sea 0
  LD A, L
  AND A
  RET Z
mapdecy_doit:
  DEC HL
  LD (DM_MAPY), HL           ; No es cero, podemos decrementar
  RET

El incremento de DM_MAPX y DM_MAPY requiere verificar que ninguna de las 2 variables excede ANCHO_MAPA-ANCHO_PANTALLA y ALTO_MAPA-ALTO_PANTALLA respectivamente. Sus decrementos requieren comprobar que el valor actual de estas variables no es cero.

Utilicemos las anteriores rutinas en un programa de ejemplo en el que podemos mover una ventana de 14×11 bloques a través de un mapa de 32×24 tiles usando las teclas O, P, Q y A:

  ; Ejemplo impresion mapa de 16x16 desde array global
  ORG 32768
 
  LD HL, sokoban1_gfx
  LD (DM_SPRITES), HL
  LD HL, sokoban1_attr
  LD (DM_ATTRIBS), HL
  LD HL, mapa_ejemplo
  LD (DM_MAP), HL
  LD A, ANCHO_PANTALLA
  LD (DM_WIDTH), A
  LD A, ALTO_PANTALLA
  LD (DM_HEIGHT), A
  XOR A
  LD (DM_COORD_X), A
  LD (DM_COORD_Y), A   
  LD (DM_MAPX), A            ; Establecemos MAPX, MAPY iniciales = 0
  LD (DM_MAPY), A
 
redraw:
  CALL DrawMap_16x16_Map     ; Imprimir pantalla de mapa
 
bucle:
  CALL LEER_TECLADO          ; Leemos el estado de O, P, Q, A
 
  BIT 0, A                   ; Modificamos MAPX y MAPY segun OPQA
  JR Z, nopulsada_q
  CALL Map_Dec_Y
  JR redraw
nopulsada_q:
  BIT 1, A
  JR Z, nopulsada_a
  CALL Map_Inc_Y
  JR redraw
nopulsada_a:
  BIT 2, A
  JR Z, nopulsada_p
  CALL Map_Inc_X
  JR redraw
nopulsada_p:
  BIT 3, A
  JR Z, nopulsada_o
  CALL Map_Dec_X
  JR redraw
nopulsada_o:
  JR bucle
 
loop:
  JR loop
 
;-------------------------------------------------------------
; LEER_TECLADO: Lee el estado de O, P, Q, A, y devuelve
; en A el estado de las teclas (1=pulsada, 0=no pulsada).
; El byte está codificado tal que:
;
; BITS            3    2     1   0
; SIGNIFICADO   LEFT RIGHT DOWN  UP
;-------------------------------------------------------------
LEER_TECLADO:
  LD D, 0
  LD BC, $FBFE
  IN A, (C)
  BIT 0, A                   ; Leemos la tecla Q
  JR NZ, Control_no_up       ; No pulsada, no cambiamos nada en D
  SET 0, D                   ; Pulsada, ponemos a 1 el bit 0
Control_no_up:
 
  LD BC, $FDFE
  IN A, (C)
  BIT 0, A                   ; Leemos la tecla A
  JR NZ, Control_no_down     ; No pulsada, no cambianos nada en D
  SET 1, D                   ; Pulsada, ponemos a 1 el bit 1
Control_no_down:
 
  LD BC, $DFFE
  IN A, (C)
  BIT 0, A                   ; Leemos la tecla P
  JR NZ, Control_no_right    ; No pulsada
  SET 2, D                   ; Pulsada, ponemos a 1 el bit 2
Control_no_right:
                             ; BC ya vale $DFFE, (O y P en misma fila)
  BIT 1, A                   ; Tecla O
  JR NZ, Control_no_left
  SET 3, D
Control_no_left:
 
  LD A, D                    ; Devolvemos en A el estado de las teclas
  RET
 
;-------------------------------------------------------------
DM_SPRITES  EQU  50020
DM_ATTRIBS  EQU  50022
DM_MAP      EQU  50024
DM_COORD_X  EQU  50026
DM_COORD_Y  EQU  50027
DM_WIDTH    EQU  50028
DM_HEIGHT   EQU  50029
DM_MAPX     EQU  50030
DM_MAPY     EQU  50032
 
;-------------------------------------------------------------
; Algunos valores hardcodeados para el ejemplo, en la rutina
; final se puede utilizar DM_WIDTH y DM_HEIGHT.
;-------------------------------------------------------------
ANCHO_MAPA_TILES       EQU   32
ALTO_MAPA_TILES        EQU   24
ANCHO_PANTALLA         EQU   14
ALTO_PANTALLA          EQU   11
 
;;; Rutina de la ROM del Spectrum, en otros sistemas 
;;; sustituir por una rutina especifica de multiplicacion
MULT_HL_POR_DE         EQU   $30A9
 
;-----------------------------------------------------------------------
;;; Nuestra pantalla de ejemplo de 32x24 bloques:
;-----------------------------------------------------------------------
mapa_ejemplo: 
  DEFB 1,2,1,1,2,1,2,1,2,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
  DEFB 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1
  DEFB 1,0,0,0,0,0,0,0,0,0,7,7,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1
  DEFB 1,0,0,0,0,0,0,0,0,2,3,2,3,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,1
  DEFB 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,1
  DEFB 1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,7,7,0,0,0,1
  DEFB 1,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,7,7,7,0,0,1
  DEFB 1,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,5,0,0,0,0,0,0,2,3,2,3,2,0,1
  DEFB 1,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,6,4,0,0,0,0,0,0,0,0,0,0,0,0,1
  DEFB 1,5,0,0,0,0,0,0,0,0,0,0,0,0,0,6,6,6,5,0,0,0,0,0,0,0,0,0,0,0,0,1
  DEFB 1,4,2,3,2,3,0,0,0,0,2,3,2,3,2,3,2,3,4,2,3,2,0,0,0,0,0,0,0,0,0,1
  DEFB 1,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1
  DEFB 1,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1
  DEFB 1,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1
  DEFB 1,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,2,3,2,3,0,0,0,0,0,0,0,0,0,0,1
  DEFB 1,0,0,0,6,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1
  DEFB 1,0,0,6,6,6,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1
  DEFB 1,0,2,3,2,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,1
  DEFB 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,6,6,0,0,0,0,0,1
  DEFB 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,6,6,6,6,0,0,0,0,1
  DEFB 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,6,6,6,6,6,6,0,0,0,1
  DEFB 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,2,3,2,3,2,3,2,3,0,0,1
  DEFB 1,0,2,3,2,2,2,3,2,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1
  DEFB 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1

Veamos una captura del resultado del anterior ejemplo después de moverse a través del mapeado:


 Moviendose por un mapa en array global

El anterior ejemplo es meramente ilustrativo del uso de la rutina de impresión: para un juego basado en scroll (como simula el ejemplo con la pulsación de teclas) resultará mucho más rápido y eficiente realizar un scroll del contenido del área de juego y trazar sólo la fila o columna de datos que “entra” en el área de visión del jugador.

Para esta implementación serían necesarias 4 rutinas de scroll de una porción de videomemoria, según el tipo de movimiento, y la impresión en la primera/última fila/columna de visión del dato de la pantalla entrante. Sigue siendo necesario modificar DM_MAPX y DM_MAPY.

El scroll mediante instrucciones de transferencia de N-1 FILAS o N-1 COLUMNAS de pantalla gráfica y de atributos resultará bastante más rápido y eficiente que la rutina de impresión de sprites integrada en DrawMap, al no tener que realizar apenas cálculos.

Como desventaja principal de los mapeados basados en arrays globales de tiles, en este tipo de mapeados no podemos utilizar de una forma inmediata las técnicas de “agrupación” o “compresión” que veremos a continuación.


Las pantallas de tamaño fijo almacenan la información tanto de los bloques “no dibujables” (fondos transparentes o sólidos) como de los dibujables (los gráficos que forman la pantalla en sí misma). Esto supone un pequeño desperdicio de memoria ya que almacenamos en el array de la pantalla datos que finalmente no vamos a utilizar y que no aparecerán en pantalla.

Una pantalla de 16×12 tiles que ocupe todo el área visible ocupa 192 bytes, lo que nos permite un total de 85 pantallas en 16 KB de memoria. Si tenemos en cuenta que necesitamos espacio para los gráficos de personajes y enemigos, el tileset, fuentes de texto, código del programa, textos, sonido, variables, nos encontramos con que se establece un límite de cantidad de pantallas que podemos incorporar en nuestro programa en función de la memoria libre que nos queda tras incorporar todos los elementos del mismo.

Para reducir el espacio que ocupan nuestras pantallas y por tanto poder incluir más pantallas en la misma cantidad de memoria podemos utilizar diferentes métodos de codificación.

Uno de ellos podría basarse en la compresión por diferentes algoritmos del mapeado considerado globalmente: si tomamos todo el bloque de datos con información sobre las pantallas y lo comprimimos antes de salvarlo a cintar y lo descomprimimos al vuelo durante su carga reducimos la ocupación del binario resultante en cinta pero no de la ocupación de datos en memoria.

Por esto, lo mejor es codificar o “comprimir” cada pantalla en sí misma y que la rutina de impresión la desempaquete al vuelo.

La técnica que vamos a ver no es una compresión en sí misma sino que se basa en no almacenar en el vector de datos de la pantalla los datos en blanco/transparentes. Los datos de la pantalla incluirán sólo los tiles que deben de ser dibujados.

Si tenemos una fila de 16 tiles pero sólo 5 de ellos deben de ser dibujados, es absurdo almacenar la información de los 11 tiles “en blanco”. A continuación veremos diferentes formas de codificar los datos de los tiles “reales” y descartar los tiles “vacíos”.

Aunque los mapeados diferenciales consiguen su mayor compresión incluyendo técnicas de repetición de tiles y de patrones, nosotros vamos a considerar las técnicas de compresión básica basadas simplemente en descartado de tiles fondo/transparentes y en agrupación de scanlines.


Mapeados diferenciales con un único tileset

En las técnicas de mapeados diferenciales, el mapa no cambia: incluye (como mínimo) las direcciones de las pantallas y las conexiones entre las mismas. Lo que sí que se ve modificada es la estructura de la pantalla, que ahora no tiene un tamaño fijo (al no ser ya una matriz de Ancho*Alto tiles).

Este tamaño variable requiere finalizar los datos de la pantalla con un identificador para informar a las rutinas de impresión de cuándo deben terminar su ejecución. En nuestro caso finalizaremos las pantallas con un valor 255 (-1).

A la hora de codificar las pantallas, podemos optar por diferentes “algoritmos”:


  • Codificación básica: se almacenan en el vector “pantalla” los datos de cada tile que realmente deba de ser impreso. Los datos mínimos necesarios son la coordenada X, la coordenada Y y el identificador de tile.
  • Codificación por agrupación horizontal: Para evitar incluir la coordenada X e Y en cada tile, podemos agrupar tiles consecutivos horizontalmente y marcar sólo la posición del primero, ahorrando 2 bytes por cada tile que le sigue.
  • Codificación por agrupación vertical: Para evitar incluir la coordenada X e Y en cada tile, podemos agrupar tiles consecutivos verticalmente y marcar sólo la posición del primero, ahorrando 2 bytes por cada tile bajo él.
  • Codificación por agrupación mixta: Cada tile se codifica por agrupación horizontal o vertical según produzca un mayor o menor ahorro de tamaño. La pantalla contiene primero los scanlines horizontales, seguidos de un byte de valor 254, y después los scanlines verticales.


Veamos en detalle todos estos tipos de codificación.


Mapeado diferencial básico (sin agrupación)

Implementaremos codificación básica por scanlines horizontales, verticales, y mixtos utilizando un pequeño programa en python creado específicamente para este capítulo. Con él codificaremos la pantalla 1 de Sokoban.

A esta pantalla tendremos que realizarle una pequeña modificación para mostrar las bondades de la codificación: eliminar los tiles transparentes para que podamos codificarla ignorando los tiles vacíos.

Si nos fijamos en cualquier juego de plataformas, shooter, aventura, etc, veremos que, al contrario que en las pantallas de ejemplo de Sokoban, no existen bloques transparentes y que la gran mayoría de los bloques en pantalla son o bien “vacíos” (fondo sólido) o bien “transparentes” (fondo no sólido), siendo esos bloques el área por donde se mueve el personaje y los enemigos. En la mayoría de juegos, pues, hay bloques vacíos o transparentes pero no de ambos tipos.

Para mostrar el nivel de compresión que se conseguiría en un juego basado en tiles, supongamos que nuestro juego de ejemplo (Sokoban) no tuviera transparencias y el fondo del área de juego fuera totalmente plana (un mismo color) con lo que pudieramos ignorar la impresión de bloques 0 en lugar de la de los bloques 255:

Introducimos los datos del nivel 1 (cambiando los tiles transparentes por ceros, tal y como sería la pantalla de cualquier otro juego) en un fichero pantalla.dat con el siguiente formato:

sokoban_LEVEL1:
  DEFB 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
  DEFB 0,0,0,0,0,0,2,3,1,4,0,0,0,0,0,0
  DEFB 0,0,0,0,1,2,3,0,0,5,4,0,0,0,0,0
  DEFB 0,0,0,0,4,0,6,6,0,0,5,0,0,0,0,0
  DEFB 0,0,0,0,5,0,0,6,0,0,4,0,0,0,0,0
  DEFB 0,0,0,0,4,0,0,0,0,0,5,0,0,0,0,0
  DEFB 0,0,0,0,5,2,3,0,0,2,3,0,0,0,0,0
  DEFB 0,0,0,0,0,0,1,0,0,0,4,0,0,0,0,0
  DEFB 0,0,0,0,0,0,4,7,7,7,5,0,0,0,0,0
  DEFB 0,0,0,0,0,0,5,2,3,2,3,0,0,0,0,0
  DEFB 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
  DEFB 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

Veamos el código de nuestro sencillo programa de “compresión/codificación” realizado en lenguaje python:

#!/usr/bin/python
#
# Convierte una pantalla de mapa en pantalla codificada por
# codif. basica, scanlines horizontales, verticales o mixtos.
# Permite agrupacion de coordenadas XY en un mismo byte con
# el flag -a.
#
 
import os, sys
 
# Variables de configuracion del script
ANCHO_MAPA = 16
ALTO_MAPA = 12
IGNORE_VALUES = 0
BLANK = 0
agrupar_xy = 0
 
MIXTA_CODIF_HORIZ  = 0
MIXTA_CODIF_VERT   = 1
 
#-----------------------------------------------------------------------
def Uso():
   print "\nUso:", sys.argv[0], "TIPO_CODIFICACION [-a] nombre_fichero"
   print "\n  Flag TIPO_CODIFICACION:\n"
   print "    Basica = -b"
   print "    Scanlines horizontales = -h"
   print "    Scanlines verticales= -v"
   print "    Scanlines horizontales = -h"
   print "    Mixta horizontales/verticales = -m\n"
   print "    Flag opcional -a: (por defecto = off)"
   print "    Agrupar coordenadas X e Y en un mismo byte = -a\n"
 
   sys.exit(1)
 
#-----------------------------------------------------------------------
def Consecutivos_Horiz( x, y, pantalla, ancho ):
   cuales = []
   tiles = []
   while (x < ancho) and ( pantalla[y][x] != IGNORE_VALUES ):
      cuales.append( x )
      cuales.append( y )
      tiles.append(pantalla[int(y)][int(x)])
      x += 1
   return cuales, tiles
 
#-----------------------------------------------------------------------
def Consecutivos_Vert( x, y, pantalla, alto ):
   cuales = []
   tiles = []
   while (y < alto) and ( pantalla[y][x] != IGNORE_VALUES ):
      cuales.append( x )
      cuales.append( y )
      tiles.append(pantalla[y][x])
      y += 1
   return cuales, tiles
 
#-----------------------------------------------------------------------
def Borrar_Consecutivos( pantalla, lista ):
   for i in range(0,len(lista),2):
      x = lista[i]
      y = lista[i+1]
      pantalla[y][x] = BLANK
   return pantalla
 
#-----------------------------------------------------------------------
def Codificacion_Horiz_o_Vert( pantalla ):
 
   COORD_Y = 0
   mapa_codificado = []
 
   # Procesamos la matriz de datos (la pantalla) segun la codificacion:
   for y in range(0,ALTO_MAPA):
      COORD_X = 0
 
      # Procesar los valores separados por coma:
      for valor in pantalla[y]:
         if codificacion == "-b":
            if valor != IGNORE_VALUES:
               if agrupar_xy == 0:
                  mapa_codificado.append( COORD_X )
                  mapa_codificado.append( COORD_Y )
               else:
                  mapa_codificado.append( (COORD_X*16) + COORD_Y )
               mapa_codificado.append( valor )
         elif codificacion == "-h" or codificacion == "-v":
            if valor != IGNORE_VALUES:
               if agrupar_xy == 0:
                  mapa_codificado.append( COORD_X )
                  mapa_codificado.append( COORD_Y )
               else:
                  mapa_codificado.append( (COORD_X*16) + COORD_Y )
               if codificacion == "-h":
                  a, b = Consecutivos_Horiz(COORD_X, COORD_Y, pantalla, ANCHO_MAPA)
               else:
                  a, b = Consecutivos_Vert(COORD_X, COORD_Y, pantalla, ALTO_MAPA)
               pantalla = Borrar_Consecutivos( pantalla, a )
               mapa_codificado.extend( b )
               mapa_codificado.append( 255 );
         COORD_X += 1
      COORD_Y += 1
 
   return mapa_codificado
 
#-----------------------------------------------------------------------
def Tiles_Pendientes( pantalla ):
   cuantos = 0
   for y in range(0,len(pantalla)):
      for value in pantalla[y]:
         if value != IGNORE_VALUES:
            cuantos += 1
   return cuantos
 
#-----------------------------------------------------------------------
def Coordenadas_Mayor_Valor( matriz ):
   maxx, maxy, maxv = 0, 0, 0
 
   for y in range(0,len(matriz)):
      for x in range(len(matriz[y])):
         valor = matriz[y][x]
         if valor != IGNORE_VALUES and valor > maxv:
            maxv = valor
            maxx = x
            maxy = y
 
   return maxx, maxy, maxv
 
#-----------------------------------------------------------------------
def Codificacion_Mixta( pantalla ):
 
   mapa_codificado_h = []
   mapa_codificado_v = []
   mapa_codificado = []
 
   # Repetir hasta que no queden tiles que codificar:
   while Tiles_Pendientes(pantalla) != 0:
 
      # Construir 2 tablas con la cantidad de tiles horizontales y
      # verticales que salen de codificar cada posicion:
      horizontales = [ [ 0 for i in range(0,ANCHO_MAPA) ] for j in range(0,ALTO_MAPA) ]
      verticales = [ [ 0 for i in range(0,ANCHO_MAPA) ] for j in range(0,ALTO_MAPA) ]
 
      COORD_Y = 0
      for y in range(0,ALTO_MAPA):
         COORD_X = 0
         for valor in pantalla[y]:
            if valor != IGNORE_VALUES:
               a, b = Consecutivos_Horiz(COORD_X, COORD_Y, pantalla, ANCHO_MAPA)
               c, d = Consecutivos_Vert(COORD_X, COORD_Y, pantalla, ALTO_MAPA)
               horizontales[COORD_Y][COORD_X] = len(b)
               verticales[COORD_Y][COORD_X] = len(d)
            COORD_X += 1
         COORD_Y += 1
 
      # Una vez construida la tabla, buscar la posicion X,Y que
      # tiene el valor mas alto y codificarla
      max_hx, max_hy, maxh_v = Coordenadas_Mayor_Valor( horizontales )
      max_vx, max_vy, maxv_v = Coordenadas_Mayor_Valor( verticales )
 
      # Codificar con horizontal o vertical segun cual sea el mayor
      if maxh_v >= maxv_v:
         if agrupar_xy == 0:
            mapa_codificado_h.append( max_hx )
            mapa_codificado_h.append( max_hy )
         else:
            mapa_codificado_h.append( (max_hx*16) + max_hy )
         a, b = Consecutivos_Horiz(max_hx, max_hy, pantalla, ANCHO_MAPA)
         pantalla = Borrar_Consecutivos( pantalla, a )
         mapa_codificado_h.extend( b )
         mapa_codificado_h.append( 255 )
      else:
         if agrupar_xy == 0:
            mapa_codificado_v.append( max_vx )
            mapa_codificado_v.append( max_vy )
         else:
            mapa_codificado_h.append( (max_vx*16) + max_vy )
         c, d = Consecutivos_Vert(max_vx, max_vy, pantalla, ALTO_MAPA)
         pantalla = Borrar_Consecutivos( pantalla, c )
         mapa_codificado_v.extend( d )
         mapa_codificado_v.append( 255 )
 
   # Sacamos las codificaciones en orden: primero horizontales, luego 254
   # tras eliminar el 255 final de las horizontales, y luego verticales.
   if mapa_codificado_h != []:
      mapa_codificado.extend(mapa_codificado_h[:-1])
 
   mapa_codificado.append( 254 )
 
   if mapa_codificado_v != []:
      mapa_codificado.extend(mapa_codificado_v)
 
   return mapa_codificado
 
 
#-----------------------------------------------------------------------
def Imprimir_Resultados( codificacion, mapa_codificado ):
   print " ; Flag codificacion:", codificacion, "-a" * agrupar_xy
   print " ; Resultado:", len(mapa_codificado), "Bytes"
   # Imprimimos los resultados de la codificacion
   CONTADOR_DB = -1
   for valor in mapa_codificado:
      CONTADOR_DB += 1
      if CONTADOR_DB == 0:     
         print " DB", str(valor),
      elif CONTADOR_DB == 11: 
         print ",", str(valor)
         CONTADOR_DB = -1
      else:
         print ",", str(valor),
 
 
#-----------------------------------------------------------------------
if __name__ == '__main__':
 
   # Variables que utilizaremos 
   pantalla = []
   COORD_X = 0
   COORD_Y = 0
 
   # Comprobar numero de argumentos + recoger y validar parametros:
   if len( sys.argv ) < 3:
      Uso()
 
   if "-a" in sys.argv:
      agrupar_xy = 1
      args = [ arg for arg in sys.argv if arg != '-a' ]
   else:
      args = sys.argv[:]
 
   if args[1][0] == '-':
      codificacion=args[1]
      fichero=args[2]
   elif args[2][0] == '-':
      codificacion=args[2]
      fichero=args[1]
   else:
      Uso()
 
   if codificacion[1] not in "bhvm":
      Uso()
 
   # Abrir el fichero de pantalla:
   try:
      fich = open( fichero )
   except:
      print "No se pudo abrir ", sys.argv[1]
      sys.exit(0)
 
   # Procesar el fichero "linea a linea"
   for linea in fich.readlines():
 
     pantalla.append( [] )
 
     # Eliminamos todo lo que no sean numeros, coma, y retorno de carro
     linea = filter(lambda c: c in "0123456789," + chr(10) + chr(13), linea)
 
     # Si encontramos una coma en la linea, procesarla:
     if ',' in linea:
 
        # Partimos la linea en valores separados por comas:
        linea = linea.strip()
        rows = linea.split(',')
        if len(rows) != ANCHO_MAPA:
          print "ERROR: Ancho =", len(rows), "valores. Esperados =", ANCHO_MAPA
          print "Finalizando programa: Por fvor corrija el fichero de datos."
          sys.exit(2)
 
        rows = [int(value) for value in rows]
        pantalla[ COORD_Y ].extend( rows )
 
        COORD_Y += 1
   # Fin lectura datos fichero
 
   if COORD_Y != ALTO_MAPA:
      print "ERROR: Alto =", COORD_Y+1, "lineas. Esperados =", ALTO_MAPA
      print "Finalizando programa: Por fvor corrija el fichero de datos."
      sys.exit(2)
 
   if codificacion in [ "-h", "-v", "-b" ]:
      mapa_codificado = Codificacion_Horiz_o_Vert( pantalla )
   elif codificacion == '-m':
      mapa_codificado = Codificacion_Mixta( pantalla )
 
   # Acabamos el mapa con el valor de fin de mapa
   mapa_codificado.append( 255 )
 
   # Imprimimos los resultados
   Imprimir_Resultados( codificacion, mapa_codificado)

Ejecutamos el script de conversión mediante codificación básica (flag -b):

$ ./codificar_pantalla.py -b pantalla.dat
 ; Flag codificacion: -b
 ; Resultado: 106 Bytes
 DB 6 , 1 , 2 , 7 , 1 , 3 , 8 , 1 , 1 , 9 , 1 , 4
 DB 4 , 2 , 1 , 5 , 2 , 2 , 6 , 2 , 3 , 9 , 2 , 5
 DB 10 , 2 , 4 , 4 , 3 , 4 , 6 , 3 , 6 , 7 , 3 , 6
 DB 10 , 3 , 5 , 4 , 4 , 5 , 7 , 4 , 6 , 10 , 4 , 4
 DB 4 , 5 , 4 , 10 , 5 , 5 , 4 , 6 , 5 , 5 , 6 , 2
 DB 6 , 6 , 3 , 9 , 6 , 2 , 10 , 6 , 3 , 6 , 7 , 1
 DB 10 , 7 , 4 , 6 , 8 , 4 , 7 , 8 , 7 , 8 , 8 , 7
 DB 9 , 8 , 7 , 10 , 8 , 5 , 6 , 9 , 5 , 7 , 9 , 2
 DB 8 , 9 , 3 , 9 , 9 , 2 , 10 , 9 , 3, 255

La codificación muestra, por ejemplo, cómo en la columna 6, fila 1, hay un tile 2 (lo cual es correcto). Le sigue un tile 3 en (7,1), etc. Las filas y columnas se numeran desde cero y los valores 0 se ignoran (no se codifican).

El mapa acaba con un valor 255. Dado que las diferentes pantallas de juego no van a tener un tamaño fijo y común, es necesario este byte de fin de pantalla para que nuestra rutina pueda determinar cuándo se ha finalizado la impresión.

El script codifica esta pantalla con 106 bytes cuando el tamaño original era de 192, consiguiendo una compresión de aprox. el 46% (casi la mitad de tamaño). Esto quiere decir que el mapeado de nuestro programa podría ser (manteniendo este ratio de compresión en todas las pantallas), hasta casi el doble de grande que el mapa máximo actual.

Veamos a continuación una sencilla rutina que permitiría imprimir un mapa con este tipo de codificación. La rutina se basa en recorrer el array de datos de pantalla obtenido el valor X, Y y de TILE y llamando a DrawSprite16x16 para la impresión de cada tile:

;---------------------------------------------------------------
; DrawMap_16x16_Cod_Basica:
; Imprime una pantalla de tiles de 16x16 pixeles.
;
; Entrada (paso por parametros en memoria):
; Direccion             Parametro
; --------------------------------------------------------------
; DM_SPRITES (2 bytes)  Direccion de la tabla de tiles.
; DM_ATTRIBS (2 bytes)  Direccion de la tabla de atributos.
; DM_MAP     (2 bytes)  Direccion de la pantalla en memoria.
; DM_COORD_X (1 byte)   Coordenada X-Inicial en baja resolucion.
; DM_COORD_Y (1 byte)   Coordenada Y-Inicial en baja resolucion.
; DM_WIDTH   (1 byte)   Ancho del mapa en tiles
; DM_HEIGHT  (1 byte)   Alto del mapa en tiles
;---------------------------------------------------------------
DrawMap_16x16_Cod_Basica:
 
   LD HL, (DM_SPRITES)
   LD (DS_SPRITES), HL       ; Establecer tileset (graficos)
   LD HL, (DM_ATTRIBS)
   LD (DS_ATTRIBS), HL       ; Establecer tileset (atributos)
   LD BC, (DM_COORD_X)       ; B = Y_INICIO, C = X_INICIO
   LD DE, (DM_MAP)           ; DE apunta al mapa 
 
drawm16cb_loop:
   LD A, (DE)                ; Leemos el valor de COORD_X_TILE
   INC DE                    ; Apuntamos al siguiente byte del mapa
 
   CP 255                    ; Bloque especial fin de pantalla
   RET Z                     ; En ese caso, salir
 
   ADD A, C                  ; A = (X_INICIO + COORD_X_TILE)
   RLCA                      ; A = (X_INICIO + COORD_X_TILE) * 2
   LD (DS_COORD_X), A        ; Establecemos COORD_X a imprimir tile
 
   LD A, (DE)                ; Leemos el valor de COORD_Y_TILE
   INC DE
   ADD A, B                  ; A = (X_INICIO + COORD_X_TILE)
   RLCA                      ; A = (Y_INICIO + COORD_Y_TILE)
   LD (DS_COORD_Y), A        ; Establecemos COORD_Y a imprimir tile
 
   LD A, (DE)                ; Leemos el valor del TILE
   INC DE
   LD (DS_NUMSPR), A         ; Establecemos el TILE
 
   EXX                       ; Preservar todos los registros en shadows
   CALL DrawSprite_16x16_LD  ; Imprimir el tile con los parametros
   EXX                       ; Recuperar valores de los registros
 
   JR drawm16cb_loop         ; Repetimos hasta encontrar el 255

Este tipo de rutina de impresión puede no ser tan rápida como una específica con el mapa en formato crudo pero supone un gran ahorro de memoria y es posible que la diferencia de velocidad no sea importante o perceptible. Hablamos de unas diferencias de tiempos de impresión que siempre que no tratemos con un juego basado en scroll serán imperceptibles por el usuario: alguien que juegue al Manic Miner no podrá determinar si la pantalla en la que va a jugar durante varios minutos ha sido dibujada en 0.03 o en 0.10 segundos. Aunque una rutina tardara el triple que la otra, 1 décima de segundo de tiempo total de impresión es inapreciable para el usuario.

Por otra parte, como ya no estamos dibujando los bloques 0, antes de llamar a la rutina de dibujado diferencial es necesario borrar el contenido del área donde vamos a dibujar con el color plano del bloque 0 para que el resultado de la impresión incluya los bloques vacíos en aquellas áreas en que no dibujamos. Es decir, vaciaremos los “bloques vacíos” borrando inicialmente la pantalla antes de realizar la impresión de los “bloques con datos”.

Para borrar este área de pantalla podemos utilizar un simple borrado de atributos (establecer todos los atributos a cero) siempre y cuando los personajes del juego que se moverán sobre las áreas “vacías” se dibujen mediante transferencia y no mediante operaciones lógicas. Recordemos que si hemos borrado mediante atributos en negro estas áreas están vacías de color pero no de contenido gráfico y la impresión con OR haría aparecer el antiguo contenido gráfico de la pantalla. En el caso de impresión de sprites con OR sobre el área “vacía” del mapa sería necesario realizar el borrado previo a la impresión del mapa no como borrado de atributos sino como borrado de zona gráfica y de atributos.

El siguiente programa ejemplo hace uso de la anterior rutina:

  ; Ejemplo impresion mapa de 16x16 codificacion basica
  ORG 32768
 
  ;;; Borramos la pantalla (graficos y atributos)
  XOR A
  CALL ClearScreen
  XOR A
  CALL ClearAttributes
 
  ;;; Establecer valores de llamada:
  LD HL, sokoban1_gfx
  LD (DM_SPRITES), HL
  LD HL, sokoban1_attr
  LD (DM_ATTRIBS), HL
  LD HL, sokoban_LEVEL1_codif_basica
  LD (DM_MAP), HL
  LD A, 16
  LD (DM_WIDTH), A
  LD A, 12
  LD (DM_HEIGHT), A
  XOR A
  LD (DM_COORD_X), A
  LD (DM_COORD_Y), A
 
  ;;; Impresion de pantalla por codificacion basica
  CALL DrawMap_16x16_Cod_Basica
 
loop:
  JR loop
 
DM_SPRITES  EQU  50020
DM_ATTRIBS  EQU  50022
DM_MAP      EQU  50024
DM_COORD_X  EQU  50026
DM_COORD_Y  EQU  50027
DM_WIDTH    EQU  50028
DM_HEIGHT   EQU  50029
 
DS_SPRITES  EQU  50000
DS_ATTRIBS  EQU  50002
DS_COORD_X  EQU  50004
DS_COORD_Y  EQU  50005
DS_NUMSPR   EQU  50006

El resultado de la ejecución es el siguiente:


 Codificación básica



Mapeado diferencial con agrupación por scanlines

La pega de la técnica de codificación básica es que por cada bloque a imprimir estamos añadiendo 2 bytes de datos (coordenada X y coordenada Y) por lo que si más de 1/3 de los bloques totales de la pantalla son tiles a imprimir obtenemos una pantalla con más tamaño que la original.

La solución es codificar tiles “consecutivos” de forma que sólo haya que indicar una coordenada X e Y iniciales para cada “fila” o “columna” de tiles gráficos. De esta forma, 4 tiles gráficos consecutivos se codificarían como:

  DB coordenada_x_primer_tile, coordenada_y_primer_tile,
  DB tile1, tile2, tile3, tile4, 255
  ; (255 = byte de fin de "scanline")

Codificar estos 4 tiles con codificación básica hubiera requerido 4*3 = 12 bytes, pero mediante este sistema se requieren sólo 7.

La agrupación de tiles la podemos hacer buscando “conjuntos de tiles horizontales” o “verticales”. Según el tipo de “scanline de tiles” que generemos, necesitaremos codificar la pantalla de una forma o de otra y utilizar una rutina de impresión u otra.

Al final de nuestra pantalla necesitaremos un valor 255 adicional para que la rutina de impresión, al recogerlo como coordenada X, detecta la finalización de la misma.



Mapeado diferencial con agrupación por scanlines horizontales

El script codificador en python que hemos visto en el apartado de codificación básica permite, mediante el flag “-h” codificar la pantalla buscando ristras de tiles horizontales consecutivos y codificándolos de esta forma. Nótese que un sólo tile sería codificado como una ristra de tamaño 1 (x, y, tile, 255).

Para empezar, se certifica que una codificación basada en scanlines horizontales de tiles da una pantalla codificada resultante más reducida:

$ ./codificar_pantalla.py -h pantalla.dat 
 ; Flag codificacion: -h
 ; Resultado: 87 Bytes
 DB 6 , 1 , 2 , 3 , 1 , 4 , 255 , 4 , 2 , 1 , 2 , 3
 DB 255 , 9 , 2 , 5 , 4 , 255 , 4 , 3 , 4 , 255 , 6 , 3
 DB 6 , 6 , 255 , 10 , 3 , 5 , 255 , 4 , 4 , 5 , 255 , 7
 DB 4 , 6 , 255 , 10 , 4 , 4 , 255 , 4 , 5 , 4 , 255 , 10
 DB 5 , 5 , 255 , 4 , 6 , 5 , 2 , 3 , 255 , 9 , 6 , 2
 DB 3 , 255 , 6 , 7 , 1 , 255 , 10 , 7 , 4 , 255 , 6 , 8
 DB 4 , 7 , 7 , 7 , 5 , 255 , 6 , 9 , 5 , 2 , 3 , 2
 DB 3 , 255 , 255

Con esta codificación la pantalla original de 192 bytes que ocupaba 106 bytes con codificación básica pasa a requerir sólo 87 bytes mediante ristras de scanlines horizontales de tiles, lo que representa un 55% de compresión.

Veamos un ejemplo de rutina para imprimir este tipo de pantallas codificadas:

;---------------------------------------------------------------
; DrawMap_16x16_Cod_Horiz:
; Imprime una pantalla de tiles de 16x16 pixeles.
;
; Entrada (paso por parametros en memoria):
; Direccion             Parametro
; --------------------------------------------------------------
; DM_SPRITES (2 bytes)  Direccion de la tabla de tiles.
; DM_ATTRIBS (2 bytes)  Direccion de la tabla de atributos.
; DM_MAP     (2 bytes)  Direccion de la pantalla en memoria.
; DM_COORD_X (1 byte)   Coordenada X-Inicial en baja resolucion.
; DM_COORD_Y (1 byte)   Coordenada Y-Inicial en baja resolucion.
; DM_WIDTH   (1 byte)   Ancho del mapa en tiles
; DM_HEIGHT  (1 byte)   Alto del mapa en tiles
;---------------------------------------------------------------
DrawMap_16x16_Cod_Horiz:
 
   LD HL, (DM_SPRITES)
   LD (DS_SPRITES), HL       ; Establecer tileset (graficos)
   LD HL, (DM_ATTRIBS)
   LD (DS_ATTRIBS), HL       ; Establecer tileset (atributos)
   LD BC, (DM_COORD_X)       ; B = Y_INICIO, C = X_INICIO
   LD DE, (DM_MAP)           ; DE apunta al mapa 
 
   LD HL, DS_COORD_X
 
drawm16ch_read:
   LD A, (DE)                ; Leemos el valor de COORD_X_TILE
   INC DE                    ; Apuntamos al siguiente byte del mapa
 
drawm16ch_loop:
   CP 255                    ; Bloque especial fin de pantalla
   RET Z                     ; En ese caso, salir
 
   ADD A, C                  ; A = (X_INICIO + COORD_X_TILE)
   RLCA                      ; A = (X_INICIO + COORD_X_TILE) * 2
   LD (HL), A                ; Establecemos COORD_X a imprimir tile
 
   LD A, (DE)                ; Leemos el valor de COORD_Y_TILE
   INC DE
   ADD A, B                  ; A = (Y_INICIO + COORD_Y_TILE)
   RLCA                      ; A = (Y_INICIO + COORD_Y_TILE)
   LD (DS_COORD_Y), A        ; Establecemos COORD_Y a imprimir tile
 
   ;;; Bucle impresion de todos los tiles del scanline (aunque sea 1 solo)
 
drawm16ch_tileloop:
   LD A, (DE)                ; Leemos el valor del TILE de pantalla
   INC DE                    ; Incrementamos puntero
 
   CP 255
   JR Z, drawm16ch_read      ; Si es fin de tile codificado, fin bucle
 
   LD (DS_NUMSPR), A         ; Establecemos el TILE
 
   EXX                       ; Preservar todos los registros en shadows
   CALL DrawSprite_16x16_LD  ; Imprimir el tile con los parametros
   EXX                       ; Recuperar valores de los registros
 
   INC (HL)                  ; Avanzamos al siguiente tile
   INC (HL)                  ; COORD_X = COORD_X + 2
 
   JR drawm16ch_tileloop     ; Repetimos hasta encontrar el 255



Mapeado diferencial con agrupación por scanlines verticales

Según el tipo de pantalla que estemos codificando, es posible que existan más agrupaciones de bloques en “scanlines verticales” (columnas de bloques) que horizontales (filas de bloques). En ese caso, puede convenirmos codificar los bloques por scanlines verticales.

Si utilizamos el script en python con el flag -v sobre la pantalla de ejemplo, obtenemos la siguiente pantalla codificada:

$ ./codificar_pantalla.py -v pantalla.dat 
 ; Flag codificacion: -v  :
 ; Resultado: 78 Bytes
 DB 6 , 1 , 2 , 3 , 6 , 255 , 7 , 1 , 3 , 255 , 8 , 1
 DB 1 , 255 , 9 , 1 , 4 , 5 , 255 , 4 , 2 , 1 , 4 , 5
 DB 4 , 5 , 255 , 5 , 2 , 2 , 255 , 10 , 2 , 4 , 5 , 4
 DB 5 , 3 , 4 , 5 , 3 , 255 , 7 , 3 , 6 , 6 , 255 , 5
 DB 6 , 2 , 255 , 6 , 6 , 3 , 1 , 4 , 5 , 255 , 9 , 6
 DB 2 , 255 , 7 , 8 , 7 , 2 , 255 , 8 , 8 , 7 , 3 , 255
 DB 9 , 8 , 7 , 2 , 255 , 255

En este caso tenemos un tamaño de pantalla de 78 bytes, todavía menor que la codificación por scanlines horizontales. Para otras pantallas y casos el resultado de la codificación podría ser mejor con scanlines horizontales.

A continuación se transcribe la rutina de impresión de una pantalla codificada en scanlines verticales:

;---------------------------------------------------------------
; DrawMap_16x16_Cod_Vert:
; Imprime una pantalla de tiles de 16x16 pixeles.
;
; Entrada (paso por parametros en memoria):
; Direccion             Parametro
; --------------------------------------------------------------
; DM_SPRITES (2 bytes)  Direccion de la tabla de tiles.
; DM_ATTRIBS (2 bytes)  Direccion de la tabla de atributos.
; DM_MAP     (2 bytes)  Direccion de la pantalla en memoria.
; DM_COORD_X (1 byte)   Coordenada X-Inicial en baja resolucion.
; DM_COORD_Y (1 byte)   Coordenada Y-Inicial en baja resolucion.
; DM_WIDTH   (1 byte)   Ancho del mapa en tiles
; DM_HEIGHT  (1 byte)   Alto del mapa en tiles
;---------------------------------------------------------------
DrawMap_16x16_Cod_Vert:
 
   LD HL, (DM_SPRITES)
   LD (DS_SPRITES), HL       ; Establecer tileset (graficos)
   LD HL, (DM_ATTRIBS)
   LD (DS_ATTRIBS), HL       ; Establecer tileset (atributos)
   LD BC, (DM_COORD_X)       ; B = Y_INICIO, C = X_INICIO
   LD DE, (DM_MAP)           ; DE apunta al mapa 
 
   LD HL, DS_COORD_Y         ; CAMBIO: Ahora HL apunta a la variable Y
 
drawm16cv_read:
   LD A, (DE)                ; Leemos el valor de COORD_X_TILE
   INC DE                    ; Apuntamos al siguiente byte del mapa
 
drawm16cv_loop:
   CP 255                    ; Bloque especial fin de pantalla
   RET Z                     ; En ese caso, salir
 
   ADD A, C                  ; A = (X_INICIO + COORD_X_TILE)
   RLCA                      ; A = (X_INICIO + COORD_X_TILE) * 2
   LD (DS_COORD_X), A        ; CAMBIO: Establecemos COORD_X a imprimir tile
 
   LD A, (DE)                ; Leemos el valor de COORD_Y_TILE
   INC DE
   ADD A, B                  ; A = (Y_INICIO + COORD_Y_TILE)
   RLCA                      ; A = (Y_INICIO + COORD_Y_TILE)
   LD (HL), A                ; CAMBIO: Establecemos COORD_Y a imprimir tile
 
   ;;; Bucle impresion de todos los tiles del scanline (aunque sea 1 solo)
 
drawm16cv_tileloop:
 
   LD A, (DE)                ; Leemos el valor del TILE de pantalla
   INC DE                    ; Incrementamos puntero
 
   CP 255
   JR Z, drawm16cv_read      ; Si es fin de tile codificado, fin bucle
 
   LD (DS_NUMSPR), A         ; Establecemos el TILE
 
   EXX                       ; Preservar todos los registros en shadows
   CALL DrawSprite_16x16_LD  ; Imprimir el tile con los parametros
   EXX                       ; Recuperar valores de los registros
 
   INC (HL)
   INC (HL)                  ; COORD_Y = COORD_Y + 2
 
   JR drawm16cv_tileloop     ; Repetimos hasta encontrar el 255



Mapeado diferencial con agrupación mixta

Finalmente, cabe la posibilidad de que en una misma pantalla nos encontremos “filas” de tiles y “columnas” de tiles que convenga codificar con un método u otro.

Por ejemplo, cabe la posibilidad de que en 3 tiles horizontales consecutivos nos encontremos con el que el de el medio forme parte de una fila vertical de tiles de gran tamaño, por lo que nos interesará codificar ese tile como scanline vertical antes que los 3 como horizontal.

El formato de salida que vamos a utilizar en las pantallas será el siguiente:

El codificador debe codificar todos los scanlines horizontales de tiles acabados en el identificador de fin de scanline 255 menos el último, el cual acabará con un valor 254. A continuación almacenará todos los scanlines verticales de tiles acabados en 255. Un valor 255 final indicará el fin de pantalla.

El valor 254 permite a la rutina de impresión saber cuándo hemos acabado de imprimir los scanlines horizontales y le indica que debe interpretar todos los scanlines restantes como verticales. De esta forma nos ahorramos el tener que incluir un byte con el tipo de codificación precediendo a cada scanline, y ahorrando así 1 byte adicional por “agrupación”.

Como desventaja, nuestro tileset sólo puede contener ahora 254 tiles (0-253).

Nuestro script de conversión en Python, con el flag “-m”, calcula todas las posibilidades de codificación de cada tile y los procesa en orden de mayor a menor cantidad de tiles agrupados para codificar toda la pantalla en un formato mixto donde cada ristra de datos de la pantalla tendría el siguiente aspecto:

Pantalla:
  DB coordenada_x_primer_tile_horiz, coordenada_y_primer_tile_horiz,
  DB tile1, tile2, (...), tileN, 255
  DB coordenada_x_primer_tile_horiz, coordenada_y_primer_tile_horiz,
  DB tile1, tile2, (...), tileN, 254
  DB coordenada_x_primer_tile_vert, coordenada_y_primer_tile_vert,
  DB tile1, tile2, (...), tileN, 255
  DB coordenada_x_primer_tile_vert, coordenada_y_primer_tile_vert,
  DB tile1, tile2, (...), tileN, 255
  DB 255
 
  ; Donde:
  ; 255 como coordenada_x = fin de pantalla.
  ; 255 a final de scanline = byte de fin de "scanline"
  ; 254 a final de scanline = cambio de codificacion de horizontal a vertical

Veamos la compresión de la pantalla de Sokoban que hemos venido utilizando como ejemplo:

$ ./codificar_pantalla.py -m pantalla.dat 
 ; Flag codificacion: -m
 ; Resultado: 72 Bytes
 DB 6 , 1 , 2 , 3 , 1 , 4 , 255 , 6 , 8 , 4 , 7 , 7
 DB 7 , 255 , 6 , 9 , 5 , 2 , 3 , 2 , 255 , 5 , 2 , 2
 DB 3 , 255 , 6 , 3 , 6 , 6 , 255 , 5 , 6 , 2 , 3 , 255
 DB 9 , 2 , 5 , 255 , 7 , 4 , 6 , 255 , 9 , 6 , 2 , 255
 DB 6 , 7 , 1 , 254 , 10 , 2 , 4 , 5 , 4 , 5 , 3 , 4
 DB 5 , 3 , 255 , 4 , 2 , 1 , 4 , 5 , 4 , 5 , 255 , 255

Hemos reducido el tamaño de la pantalla codificada de 192 bytes a 72 bytes (6 bytes menos que la mejor de las anteriores codificaciones, la vertical).

Aunque 6 bytes pueda parecer un valor insignificante, 100 pantallas de juego con el mismo ahorro supone una reducción de 600 bytes lo que permitiría añadir 8 pantallas de juego más, o tal vez más código, sonido o gráficos.

Esta rutina producirá generalmente codificaciones mejores que las únicamente horizontales o verticales en pantallas con “formas” variadas. Lo ideal es codificar cada pantalla con el tipo de codificación que mejores resultados obtenga y utilizar una rutina de impresión que llame a una de las 3 rutinas (horizontal, vertical o mixta) según cómo se haya codificado la pantalla.

La rutina de impresión de este tipo de codificación es una fusión entre las 2 rutinas anteriores, donde usaremos en esta ocasión IX para acceder al mapeado y HL y DE apuntarán a las coordenadas X e Y para los 2 posibles bucles de impresión que se utilizarán en función de si estamos imprimiendo scanlines horizontales o verticales.

Concretamente, tomamos como base la rutina de impresión de scanlines horizontales y utilizamos un sencillo truco para convertirla en la rutina de impresión de scanlines verticales. Lo haremos mediante código automodificable (self-modifying code).

Primero, cargamos en HL la dirección de la variable COORD_X y en DE la dirección de la variable COORD_Y:

   LD DE, DS_COORD_Y         ; DE apunta a la coordenada Y
   LD HL, DS_COORD_X         ; HL apunta a la coordenada X

Despues, incluímos 2 instrucciones NOP (de 1 byte, con opcode 0), antes y después de los INC (HL) que producen el incremento de la coordenada apuntada por HL (COORD_X). Además, establecemos 2 etiquetas del programa ensamblador en las posiciones de los 2 NOPs para poder referenciar la dirección de memoria donde se han ensamblado estos NOPs en el código:

drawm16cm_dir1:
   NOP
   INC (HL)
   INC (HL)                  ; COORD = COORD + 2
drawm16cm_dir2:
   NOP

Cuando entramos por primera vez en la rutina, y comenzamos a procesar “scanlines”, sabemos que son todos horizontales hasta que encontremos el valor “254” (cambio de horizontales a verticales), por lo que nuestra rutina de impresión de tiles en bucle horizontal funciona adecuadamente: se ejecuta un NOP (que no tiene ningún efecto salvo el consumo de 4 ciclos de reloj), después los 2 INC (HL) y luego otro NOP, lo que produce el incremento de COORD_X en 2 unidades (HL apunta a COORD_X).

Cuando la rutina encuentra un valor 254 como “fin de scanline” debe de cambiar al modo de impresión vertical, por lo que dentro del bucle de procesado del scanline añadimos el siguiente código:

drawm16cm_tileloop:
 
   (...)
 
   CP 254
   JR Z, drawm16cm_switch    ; Codigo 254 -> cambiar a codif. vertical
 
   (...)
 
drawm16cm_switch:
   ;;; Cambio de codificacion de horizontal a vertical:
   LD A, $EB                 ; Opcode de EX DE, HL
   LD (drawm16cm_dir1), A    ; Lo escribimos sobre los NOPs
   LD (drawm16cm_dir2), A
   JR drawm16cm_read

Es decir, cuando se encuentra un valor 254 como fin de scanline saltamos a drawm16cm_switch, la cual escribe un valor $EB (EX DE, HL) en las posiciones de memoria donde antes había un NOP, cambiando la porción de código que habíamos visto antes por:

drawm16cm_dir1:
   EX DE, HL
   INC (HL)
   INC (HL)                  ; COORD = COORD + 2
drawm16cm_dir2:
   EX DE, HL

Esto provoca que, a partir de haber encontrado el 254 y hasta que finalice la rutina (código 255 de fin de pantalla), los INC (HL) incrementen COORD_Y (debido al EX DE, HL) en lugar de COORD_X, convirtiendo la rutina en un sistema de impresión de scanlines horizontales.

Cuando salimos de la rutina, esta se queda con los valores de “EX DE, HL” en memoria, por lo que la siguiente vez que sea llamada tenemos que asegurarnos de que vuelven a estar los NOPs en su lugar, porque las pantallas siempre empiezan por scanlines horizontales. Para lograr esto, nuestra rutina debe empezar por la colocación del “NOP” en las direcciones apuntadas por las etiquetas:

DrawMap_16x16_Cod_Mixta:
 
   XOR A                     ; Opcode de "NOP"
   LD (drawm16cm_dir1), A    ; Almacenar en la posicion de las labels
   LD (drawm16cm_dir2), A

Veamos el código completo de la rutina de impresión mixta:

;---------------------------------------------------------------
; DrawMap_16x16_Cod_Mixta:
; Imprime una pantalla de tiles de 16x16 pixeles.
;
; Entrada (paso por parametros en memoria):
; Direccion             Parametro
; --------------------------------------------------------------
; DM_SPRITES (2 bytes)  Direccion de la tabla de tiles.
; DM_ATTRIBS (2 bytes)  Direccion de la tabla de atributos.
; DM_MAP     (2 bytes)  Direccion de la pantalla en memoria.
; DM_COORD_X (1 byte)   Coordenada X-Inicial en baja resolucion.
; DM_COORD_Y (1 byte)   Coordenada Y-Inicial en baja resolucion.
; DM_WIDTH   (1 byte)   Ancho del mapa en tiles
; DM_HEIGHT  (1 byte)   Alto del mapa en tiles
;---------------------------------------------------------------
DrawMap_16x16_Cod_Mixta:
 
   XOR A                     ; Opcode de "NOP"
   LD (drawm16cm_dir1), A    ; Almacenar en la posicion de labels
   LD (drawm16cm_dir2), A
 
   LD HL, (DM_SPRITES)
   LD (DS_SPRITES), HL       ; Establecer tileset (graficos)
   LD HL, (DM_ATTRIBS)
   LD (DS_ATTRIBS), HL       ; Establecer tileset (atributos)
   LD BC, (DM_COORD_X)       ; B = Y_INICIO, C = X_INICIO
   LD IX, (DM_MAP)           ; DE apunta al mapa 
 
   LD DE, DS_COORD_Y         ; DE apunta a la coordenada Y
   LD HL, DS_COORD_X         ; HL apunta a la coordenada X
 
drawm16cm_read:
   LD A, (IX+0)              ; Leemos el valor de COORD_X_TILE
   INC IX                    ; Apuntamos al siguiente byte del mapa
 
drawm16cm_loop:
   CP 255                    ; Bloque especial fin de pantalla
   RET Z                     ; En ese caso, salir
 
   ADD A, C                  ; A = (X_INICIO + COORD_X_TILE)
   RLCA                      ; A = (X_INICIO + COORD_X_TILE) * 2
   LD (HL), A                ; Establecemos COORD_X a imprimir tile
 
   LD A, (IX+0)              ; Leemos el valor de COORD_Y_TILE
   INC IX
   ADD A, B                  ; A = (Y_INICIO + COORD_Y_TILE)
   RLCA                      ; A = (Y_INICIO + COORD_Y_TILE)
   LD (DE), A                ; Establecemos COORD_Y a imprimir tile
 
   ;;; Bucle impresion vertical de los N tiles del scanline
drawm16cm_tileloop:
   LD A, (IX+0)              ; Leemos el valor del TILE de pantalla
   INC IX                    ; Incrementamos puntero
 
   CP 255
   JR Z, drawm16cm_read      ; Si es fin de tile codificado, fin bucle
 
   CP 254
   JR Z, drawm16cm_switch    ; Codigo 254 -> cambiar a codif. vertical
 
   LD (DS_NUMSPR), A         ; Establecemos el TILE
   EXX                       ; Preservar todos los registros en shadows
   CALL DrawSprite_16x16_LD  ; Imprimir el tile con los parametros
   EXX                       ; Recuperar valores de los registros
 
drawm16cm_dir1:              ; Etiqueta con la direccion del NOP
   NOP                       ; NOP->INC COORD_X,  EX DE,HL->INC COORD_Y
 
   INC (HL)
   INC (HL)                  ; COORD = COORD + 2
 
drawm16cm_dir2:              ; Etiqueta con la direccion del NOP
   NOP                       ; NOP->INC COORD_X,  EX DE,HL->INC COORD_Y
 
   JR drawm16cm_tileloop     ; Repetimos hasta encontrar el 255
 
drawm16cm_switch:
   ;;; Cambio de codificacion de horizontal a vertical:
   LD A, $EB                 ; Opcode de EX DE, HL
   LD (drawm16cm_dir1), A    ; Ahora se hace el EX DE, HL y por lo tanto
   LD (drawm16cm_dir2), A    ; INC (HL) incrementa COORD_Y en vez de X
   JR drawm16cm_read         ; Volvemos al bucle de lectura

Con el sistema de “automodificación de código” nos ahorramos el disponer de 2 porciones de código para realizar una misma tarea en la que cambia algún salto o alguna instrucción específica.

Esta técnica sirve para gran cantidad de optimizaciones en rutinas de este tipo: podemos por ejemplo en algunas rutinas modificar el código en memoria para evitar saltos (reemplazar la instruccion de comparacion o de salto por instrucciones que no lo provoquen o por NOPs) o evitar la duplicación de código.



Codificar X e Y en un único byte

Veamos una modificación del script/programa de codificación y de la rutinas de impresión que nos van a resultar realmente útiles siempre que nuestro mapa no esté formado por tiles de 8×8 píxeles.

Si el tamaño en tiles de nuestra pantalla de juego es menor que 16×16 bloques podemos codificar las coordenadas X e Y en un mismo byte.

Al ser el mapa de tamaño menor que 16×16, ambas coordenadas pueden ir en el rango 0 a 15, por lo que cada una de las 2 coordenadas puede ser codificada en 4 bits. De esta forma, nuestro script codificador puede componer un byte de posición con la coordenada X en el nibble alto de un byte y la coordenada Y en el nibble bajo del mismo (o a la inversa).

De esta forma ahorramos 1 byte por cada scanline codificado, un ahorro que puede ser bastante significativo.

La modificación realizada en nuestro script codificador ha sido el cambiar en todos los métodos de codificación las 2 líneas siguientes:

  mapa_codificado.append( X )
  mapa_codificado.append( Y )

por:

  mapa_codificado.append( (X * 16) + Y )

Aunque hemos dicho que el tamaño máximo del mapa será de 16×16, la realidad es que puede ser como máximo de 16×14, ya que si ambas coordenadas X e Y valen 15 ($F), el byte resultante compuesto sería $FF que es el código de final de pantalla. De la misma forma, si X vale 15 e Y vale 14, el valor resultante, $FE, sería confundido por la rutina mixta con el código especial de cambio de scanlines horizontales a verticales.

Así, con un mapa de 16×14, el máximo tamaño de pantalla que podemos ocupar según las dimensiones de cada tile serían:

Tamaño de mapa Tamaño de tile Ancho de pantalla Alto de pantalla
16×14 8×8 128 píxeles 112 píxeles
16×12 16×16 256 píxeles 192 píxeles
8×6 32×32 256 píxeles 192 píxeles

Las dimensiones que podemos ver en la tabla hacen esta optimización inusable para juegos con tiles de 8×8 pixeles.

Por otra parte, recordemos que debemos modificar la rutina para que separe los bytes de COORD_X y COORD_Y en 2 valores diferentes, lo que supone un pequeño tiempo adicional de procesado por cada scanline.

Nuestra pantalla de pruebas es de 16×12, por lo que podemos perfectamente codificarla con esta técnica, utilizando el flag -a, además del tipo de codificación:

$ ./codificar_pantalla.py -m -a pantalla.dat 
 ; Flag codificacion: -m -a
 ; Resultado: 60 Bytes
 DB 97 , 2 , 3 , 1 , 4 , 255 , 104 , 4 , 7 , 7 , 7 , 255
 DB 105 , 5 , 2 , 3 , 2 , 255 , 82 , 2 , 3 , 255 , 99 , 6
 DB 6 , 255 , 86 , 2 , 3 , 255 , 146 , 5 , 255 , 116 , 6 , 255
 DB 150 , 2 , 255 , 103 , 1 , 254 , 162 , 4 , 5 , 4 , 5 , 3
 DB 4 , 5 , 3 , 255 , 66 , 1 , 4 , 5 , 4 , 5 , 255 , 255

En esta ocasión hemos obtenido la pantalla codificada con un total de 60 bytes, ya que nos hemos ahorrado 1 byte por cada scanline codificado.

Podemos modificar cualquiera de las rutinas de impresión que hemos visto para adaptarlas al uso de coordenadas X e Y en un mismo byte, simplemente cambiando el código que recoge desde el mapa ambas coordenadas.

Cambiamos:

   ;;; (venimos del LD A, (IX+0) / INC IX de la coordenada X)
 
   ;;; Sumamos la coordenada X recogida y obtenemos desde el mapa la Y:
   ADD A, C                  ; A = (X_INICIO + COORD_X_TILE)
   RLCA                      ; A = (X_INICIO + COORD_X_TILE) * 2
   LD (HL), A                ; Establecemos COORD_X a imprimir tile
 
   LD A, (IX+0)              ; Leemos el valor de COORD_Y_TILE
   INC IX
   ADD A, B                  ; A = (Y_INICIO + COORD_Y_TILE)
   RLCA                      ; A = (Y_INICIO + COORD_Y_TILE)
   LD (DE), A                ; Establecemos COORD_Y a imprimir tile

por:

   PUSH AF
   AND %11110000             ; Nos quedamos con la parte alta (COORD_X)
   RRCA                      ; Pasamos parte alta a parte baja
   RRCA                      ; con 4 desplazamientos
   RRCA
   RRCA                      ; Ya podemos sumar:
   ADD A, C                  ; A = (X_INICIO + COORD_X_TILE)
   RLCA                      ; A = (X_INICIO + COORD_X_TILE) * 2
   LD (HL), A                ; Establecemos COORD_X a imprimir tile
 
   POP AF
   AND %00001111             ; Nos quedamos con la parte baja (COORD_Y)
 
   ADD A, B                  ; A = (Y_INICIO + COORD_Y_TILE)
   RLCA                      ; A = (Y_INICIO + COORD_Y_TILE)
   LD (DE), A                ; Establecemos COORD_Y a imprimir tile

De la rutina original hemos eliminado las instrucciones “LD A, (IX+0)” e “INC IX” (29 ciclos de reloj menos) y hemos añadido PUSH/POP AF e instrucciones AND y RLCA (51 ciclos de reloj más) resultando en una rutina que es 22 ciclos de reloj más lenta por scanline. Pero a cambio de estos 22 ciclos de reloj se pueden producir grandes ahorros en las pantallas resultantes.

La rutina de impresión de 16×16 Mixta con coordenadas X e Y codificadas en un mismo byte quedaría como el código que sigue:

;---------------------------------------------------------------
; DrawMap_16x16_Cod_Mixta_XY: (X e Y codificados en mismo byte)
; Imprime una pantalla de tiles de 16x16 pixeles.
;
; Entrada (paso por parametros en memoria):
; Direccion             Parametro
; --------------------------------------------------------------
; DM_*  = Variables de MAPA
; DS_*  = Variables de SPRITE
;---------------------------------------------------------------
DrawMap_16x16_Cod_Mixta_XY:
 
   XOR A                     ; Opcode de "NOP"
   LD (drawm16cmxy_dir1), A  ; Almacenar en la posicion de las labels
   LD (drawm16cmxy_dir2), A
   LD HL, (DM_SPRITES)
   LD (DS_SPRITES), HL       ; Establecer tileset (graficos)
   LD HL, (DM_ATTRIBS)
   LD (DS_ATTRIBS), HL       ; Establecer tileset (atributos)
   LD BC, (DM_COORD_X)       ; B = Y_INICIO, C = X_INICIO
   LD IX, (DM_MAP)           ; DE apunta al mapa 
 
   LD DE, DS_COORD_Y         ; DE apunta a la coordenada Y
   LD HL, DS_COORD_X         ; HL apunta a la coordenada X
 
drawm16cmxy_read:
   LD A, (IX+0)              ; Leemos el valor de COORDENADAS
   INC IX                    ; Apuntamos al siguiente byte del mapa
 
drawm16cmxy_loop:
   CP 255                    ; Bloque especial fin de pantalla
   RET Z                     ; En ese caso, salir
 
   PUSH AF                   ; Extraccion de coordenadas XY en X e Y
   AND %11110000             ; Nos quedamos con la parte alta (COORD_X)
   RRCA                      ; Pasamos parte alta a parte baja
   RRCA                      ; con 4 desplazamientos
   RRCA
   RRCA                      ; Ya podemos sumar
   ADD A, C                  ; A = (X_INICIO + COORD_X_TILE)
   RLCA                      ; A = (X_INICIO + COORD_X_TILE) * 2
   LD (HL), A                ; Establecemos COORD_X a imprimir tile
   POP AF
   AND %00001111             ; Nos quedamos con la parte baja (COORD_Y)
   ADD A, B                  ; A = (Y_INICIO + COORD_Y_TILE)
   RLCA                      ; A = (Y_INICIO + COORD_Y_TILE)
   LD (DE), A                ; Establecemos COORD_Y a imprimir tile
 
   ;;; Bucle impresion vertical de los N tiles del scanline
drawm16cmxy_tileloop:
   LD A, (IX+0)              ; Leemos el valor del TILE de pantalla
   INC IX                    ; Incrementamos puntero
 
   CP 255
   JR Z, drawm16cmxy_read    ; Si es fin de tile codificado, fin bucle
   CP 254
   JR Z, drawm16cmxy_switch  ; Codigo 254 -> cambiar a codif. vertical
 
   LD (DS_NUMSPR), A         ; Establecemos el TILE
   EXX                       ; Preservar todos los registros en shadows
   CALL DrawSprite_16x16_LD  ; Imprimir el tile con los parametros
   EXX                       ; Recuperar valores de los registros
 
drawm16cmxy_dir1:            ; Etiqueta con la direccion del NOP
   NOP                       ; NOP->INC COORD_X,  EX DE,HL->INC COORD_Y
   INC (HL)
   INC (HL)                  ; COORD = COORD + 2
drawm16cmxy_dir2:            ; Etiqueta con la direccion del NOP
   NOP                       ; NOP->INC COORD_X,  EX DE,HL->INC COORD_Y
   JR drawm16cmxy_tileloop   ; Repetimos hasta encontrar el 255
 
drawm16cmxy_switch:
   ;;; Cambio de codificacion de horizontal a vertical:
   LD A, $EB                 ; Opcode de EX DE, HL
   LD (drawm16cmxy_dir1), A    ; Ahora se hace el EX DE, HL y por lo tanto
   LD (drawm16cmxy_dir2), A    ; INC (HL) incrementa COORD_Y en vez de X
   JR drawm16cmxy_read



Codificar 2 tiles en un mismo byte

Si tenemos menos de 16 tiles podemos codificar 2 tiles en un mismo byte, suponiendo un ahorro de memoria de un 50%. Esto limita mucho la riqueza gráfica del juego resultante a menos que dispongamos de diferentes tilesets gráficos y que cada pantalla pueda tener asociado un set diferente, lo que nos limitaría de forma efectiva a 16 tiles diferentes por pantalla.

La estructura de pantalla (o la de mapa) debería contener un “identificador de tileset” con el que referenciar al conjunto de tiles que se usará para imprimirla.



Mapeado diferencial con diferentes codificaciones

Como para cada pantalla puede ser más apropiado un tipo de codificación que otro, podemos codificar cada una de ellas con el método que resulte más adecuado y alterar nuestro el mapa o la propia pantalla para que almacene también la información de tipo de codificación.

La forma más sencilla es que el primer byte de la pantalla contenga el tipo de codificación utilizado.

Una rutina “wrapper” (o “envoltorio”) de impresión de pantalla obtendría del mapa la dirección de la pantalla en HL o DE, leería el tipo de codificación utilizado (primer byte de la pantalla), incrementaría HL o DE, almacenaría el valor resultante en (DM_MAP) y llamaría a la función de impresión adecuada según el tipo de codificación que acabamos de leer.

Como ID de codificación se podría utilizar, por ejemplo:

MAP_CODIF_NONE     EQU 0
MAP_CODIF_HORIZ    EQU 1
MAP_CODIF_VERT     EQU 2
MAP_CODIF_MIXTA    EQU 3
MAP_CODIF_BASICA   EQU 4

Es difícil que la codificación básica sea más óptima que ninguna de las anteriores a menos que apenas haya bloques “transparentes” o “vacíos”, pero aún así se ha contemplado su uso en la rutina. Hemos incluído también la posibilidad de utilizar una pantalla sin agrupación indicando MAP_CODIF_NONE al inicio de la misma.

;---------------------------------------------------------------
; Llama a la rutina Draw_Map adecuada segun el tipo de
; codificacion de la pantalla apuntada en (DM_MAP).
;---------------------------------------------------------------
DrawMap_16x16_Codificada:
   LD HL, (DM_MAP)            ; HL apunta al mapa 
   LD A, (HL)                 ; Leemos tipo de codificacion
   INC HL                     ; Incrementamos puntero (a datos)
   LD (DM_MAP), HL            ; Guardamos el valor en DM_MAP
 
   AND A                      ; Es A == 0? (MAP_CODIF_NONE)
   JR NZ, dm16c_nocero        ; No
   CALL DrawMap_16x16
   RET
 
dm16c_nocero:
   CP MAP_CODIF_HORIZ         ; Es A == MAP_CODIF_HORIZ?
   JR NZ, dm16c_nohoriz       ; No, saltar
   CALL DrawMap_16x16_Cod_Horiz
   RET
 
dm16c_nohoriz:
   CP MAP_CODIF_VERT          ; Es A == MAP_CODIF_VERT?
   JR NZ, dm16c_novert        ; No, saltar
   CALL DrawMap_16x16_Cod_Vert
   RET
 
dm16c_novert:
   CP MAP_CODIF_MIXTA         ; Es A == MAP_CODIF_MIXTA?
   JR NZ, dm16c_nomixta       ; No, saltar
   CALL DrawMap_16x16_Cod_Mixta
   RET
 
dm16c_nomixta:                ; Entonces es basica.
   CALL DrawMap_16x16_Cod_Basica
   RET

(También habríamos podido basar el salto en una tabla de salto, tal y como vimos en el capítulo dedicado a las Fuentes de texto, sección de Impresión de cadenas con códigos de control).

Quedaría como responsabilidad del programador (o de un sencillo script) el codificar cada pantalla de las diferentes formas posibles y grabar en el fichero de datos de pantallas final el resultado más óptimo precedido del byte de tipo de codificación.

En cualquier caso, si detectamos que la técnica de compresión mixta obtiene mejores resultados para la gran mayoría de las pantallas, podemos optar por codificar todo con el algoritmo mixto (aunque algunas pantallas pudieran ocupar más con este método que con otro, serían una minoría) y así evitar la inclusión de las otras rutinas de impresión y la rutina DrawMap_16x16_Codificada que acabamos de ver. Esta será, probablemente, la mejor de las opciones si el espacio ahorrado por codificar cada pantalla con un sistema diferente es menor que la inclusión en nuestro programa de las 4 rutinas (básica, horizontal, vertical, mixta) y la rutina wrapper que acabamos de ver. Además, también ganaremos algo de tiempo en la impresión de las pantallas al saltarnos la ejecución de la rutina wrapper.



Pantallas con blancos y transparencias

En el caso de la compresión básica no conseguíamos mejoras de tamaño en las pantallas si existían bloques transparentes (255) además de bloques vacíos (0), ya que nosotros sólo podíamos considerar a nivel de codificación uno de los 2 como “bloque no dibujable”.

Lo bueno de las técnicas de codificación por scanlines horizontales, verticales o mixtos es que los bloques vacíos o los transparentes (según nos interese en el juego) se pueden codificar junto a los bloques normales, modificando las variables IGNORE_VALUES y BLANK del script de codificación en python.

Concretamente, tenemos que establecer ambas variables con el identificador de tile que queremos “ignorar” (y por lo tanto no codificar).

Aunque esto pueda implicar algunos bytes extra en la pantalla resultante, nos permite volver a tener transparencia en nuestro juego. En algunos casos, los “bloques vacíos” permiten unir 2 o más de los antiguos scanlines (antes separados por los blancos) con lo que se incrementa el ratio de compresión todavía más.

Por ejemplo, codificando la primera pantalla de Sokoban con sus transparencias obtenemos los siguientes valores para, por ejemplo, la codificación horizontal:

$ grep -E "^(IGNORE_VALUES|BLANK)" codificar_pantalla.py 
IGNORE_VALUES = 255
BLANK = 255
 
$ ./codificar_pantalla.py -m -a pantalla2.dat
 ; Flag codificacion: -m -a
 ; Resultado: 69 Bytes
 DB 97 , 113 , 129 , 145 , 162 , 66 , 254 , 2 , 3 , 6 , 0 , 0
 DB 3 , 1 , 4 , 5 , 255 , 3 , 0 , 6 , 6 , 0 , 0 , 0
 DB 7 , 2 , 255 , 1 , 0 , 0 , 0 , 0 , 0 , 0 , 7 , 3
 DB 255 , 4 , 5 , 0 , 0 , 0 , 2 , 0 , 7 , 2 , 255 , 4
 DB 5 , 4 , 5 , 3 , 4 , 5 , 3 , 255 , 1 , 4 , 5 , 4
 DB 5 , 255 , 2 , 0 , 0 , 0 , 2 , 255 , 255

En este caso, los bloques “0” (fondo negro) se han codificado junto a los demás bloques normales (mejorando la “compresión”) y los bloques 255 (transparentes) no, por lo que si en los ejemplos anteriores eliminamos el ClearScreen() y lanzamos el DrawMap correspondiente, obtenemos la siguiente pantalla:


 Codificando los blancos pero no las transparencias

No hay colisión entre el 255 “transparente” y el 255 “fin de scanline” porque el codificador lo que ha hecho es, precisamente, no codificar los bloques 255 con lo que estos no aparecen en la pantalla resultante. Por contra, sí que es necesario incluir los bloques 0 ya que pretendemos que sean dibujados para “tapar” el fondo (por eso se ha cambiando BLANK e IGNORE_VALUES en el script codificador, cambiando 0 por 255).



Efectos sobre los tiles adyacentes al fondo

Otro efecto interesante para mejorar la riqueza gráfica de un juego es generar una “sombra” para aquellos tiles que estén cercanos a otros tiles definidos como “fondo”.

Por ejemplo, la siguiente captura de pantalla de un juego de los Mojon Twins muestra cómo los muros verticales rojos cercanos a la “bellota” proyectan hacia la derecha, sobre el suelo “morado”, una sombra generada durante el proceso de trazado de la pantalla. Esta sombra hace que los tiles de suelo cercanos al muro sean diferentes del resto de tiles de suelo, sin necesidad de haber definido un tile específico para ello.


 Sombreados sobre tiles

Podemos utilizar esta técnica en caso de necesitar ahorrar memoria, aunque lo más rápido en términos de trazado sería el disponer de tiles específicos “con sombras” y que el mapa los tenga definidos en las posiciones necesarias.

El propio na_th_an nos comenta cómo utilizan esta técnica en los juego de su Colección Pretujao:

 El sombreado se hace en tiempo real. Como sombreamos hacia abajo y hacia
la derecha, a la hora de pintar un tile de fondo se hace así:

Considérese un tile de 16x16 formado por 4 carácteres:

1 2
3 4


 Los carácteres 1, 2, 3, y 4 están en el tileset. Además, tenemos unos
carácteres alternativos sombreados que llamaremos 1', 2' y 3' y 4' (aunque
este no se usa, lo dejamos por temas de velocidad).

 Siendo (x, y) la posición de nuestro tile de fondo (a nivel de tiles):

- Pintamos el carácter 1 si el tile en (x-1, y-1) es fondo o el tile 1' si es obstáculo. 
- Pintamos el carácter 2 si el tile en (x, y-1) es fondo o el tile 2' si es obstáculo.
- Pintamos el carácter 3 si el tile en (x-1, y) es fondo o el tile 3' si es obstáculo.
- Pintamos el carácter 4.

 Eso cubre todas las combinaciones y es realmente rápido.



Resumen de resultados de codificaciones empleadas

Veamos una tabla resumen de los resultados de codificar nuestra pantalla de ejemplo (el nivel 1 de Sokoban) con diferentes técnicas. Inclúimos además una estimación de cuánto ocuparían 100 pantallas de juego y cuántas pantallas cabrían en 16KB de memoria asumiendo que, de media, todas ocuparan tras su codificación tamaños parecidos del primer nivel:


Codificación Tamaño (bytes) Ocupación 100 pantallas Pantallas en 16KB
Datos en crudo 192 18.7 KB 85 pantallas
Codificación básica 106 10.3 KB 154 pantallas
Scanlines horizontales 87 8.4 KB 188 pantallas
Scanlines verticales 78 7.6 KB 210 pantallas
Scanlines mixtos 72 7 KB 227 pantallas
Scanlines mixtos + XY agrupados 60 5.8 KB 273 pantallas


Estos datos son una estimación muy general porque no todas las pantallas de Sokoban ocuparán lo mismo una vez codificadas, pero si utilizamos la técnica de codificación más adecuada a cada pantalla podemos acercarnos mucho a esas 273 pantallas totales en 16 KB de memoria, muy alejadas de las 85 que caben con los datos “en crudo”.



Posibles mejoras en el codificador y las rutinas de impresión



Detección de situaciones especiales de codificación mixta

Una posible mejora sería la de modificar el script de codificación mixta para que detecte ciertas situaciones en la que no es óptimo actualmente. El algoritmo que se ha aplicado en el codificador es un algoritmo genérico basado en buscar la mayor cantidad de tiles consecutivos para una posterior codificación horizontal o vertical por orden de cantidad de tiles. Este algoritmo no detecta determinadas situaciones donde la mejor codificación no es la que más cantidad de tiles consecutivos consigue.

Por ejemplo, supongamos esta situación:

0001234000
0000010000
0000010000
0000010000
0000010000
0001234000

Tenemos dos filas horizontales de 4 bloques y una columna vertical de 5. El algoritmo utilizado determinaría que 5 es mayor que 4 por lo que codificaría primero la columna vertical, quedando para el siguiente ciclo de codificación esta pantalla:

0001204000
0000000000
0000000000
0000000000
0000000000
0001204000

Es decir, para codificar las 3 líneas originales hemos generado 1 ristra de bytes verticales, y ahora 4 horizontales, con sus bytes de X, Y y FIN DE CODIFICACION (255) para los 5 scanlines.

Sin embargo, hubiera sido mejor codificar primero los scanlines horizontales, aunque fueran de menor longitud de que el vertical:

0001234000   --> 0000000000
0000010000   --> 0000010000
0000010000   --> 0000010000 
0000010000   --> 0000010000
0000010000   --> 0000010000
0001234000   --> 0000000000

De esta forma codificamos las 3 líneas en 3 “ristras” (2 horizontales de 4 bloques y una vertical de 4 bloques), utilizando 3*3 = 9 bytes de codificación de coordenadas y fin de scanline en lugar de los 15 del ejemplo anterior.

Estos son los resultados actuales que obtiene el codificador para la anterior pantalla:

$ ./codificar_pantalla.py -h pantalla3.dat 
 ; Flag codificacion: -h
 ; Resultado: 31 Bytes
 DB 3 , 0 , 1 , 2 , 3 , 4 , 255 , 5 , 1 , 1 , 255 , 5
 DB 2 , 1 , 255 , 5 , 3 , 1 , 255 , 5 , 4 , 1 , 255 , 3
 DB 5 , 1 , 2 , 3 , 4 , 255 , 255
 
$ ./codificar_pantalla.py -v pantalla3.dat 
 ; Flag codificacion: -v
 ; Resultado: 34 Bytes
 DB 3 , 0 , 1 , 255 , 4 , 0 , 2 , 255 , 5 , 0 , 3 , 1
 DB 1 , 1 , 1 , 3 , 255 , 6 , 0 , 4 , 255 , 3 , 5 , 1
 DB 255 , 4 , 5 , 2 , 255 , 6 , 5 , 4 , 255 , 255
 
$ ./codificar_pantalla.py -m pantalla3.dat 
 ; Flag codificacion: -m
 ; Resultado: 28 Bytes
 DB 3 , 0 , 1 , 2 , 255 , 3 , 5 , 1 , 2 , 255 , 6 , 0
 DB 4 , 255 , 6 , 5 , 4 , 254 , 5 , 0 , 3 , 1 , 1 , 1
 DB 1 , 3 , 255 , 255

Y este es el coste que tendría la codificación con la detección de la situación que hemos comentado:

 ; Codificacion mixta con deteccion de situaciones T e I:
 ; Resultado: 22 Bytes
 DB 3 , 0 , 1 , 2 , 3 , 4 , 255 , 3 , 5 , 1 , 2 , 3 , 4, 254
 DB 5 , 1 , 1 , 1 , 1 , 1, 255 , 255

Estaríamos ganando unos pocos bytes adicionales sin cambios en nuestro programa: tan sólo mejorando el codificador.



Agrupar 2 scanlines separados por 2 o menos huecos

Finalmente, podríamos modificar el script codificador para que detecte las situaciones en que tengamos 2 scanlines (horizontales o verticales) separados por 1 ó 2 bloques “vacíos” (transparentes).

 00011101110

Codificar la anterior secuencia produciría 2 scanlines con sus bytes de COORD_X, COORD_Y y FIN_SCANLINE por cada fila de tiles, con un total de 6 bytes de “datos de posicionamiento”.

 ; 12 bytes
 DB 3, 0, 1, 1, 1, 255, 8, 0, 1, 1, 1, 255

Si codificamos el anterior scanline usando un código especial (ej: 252) como “tile transparente” a ser ignorado por la rutina de impresión, nos ahorramos los bytes de posicionamiento del segundo scanline:

 ; 10 bytes
 DB 3, 0, 1, 1, 1, 252, 1, 1, 1, 255

Si la separación entre 2 scanlines la forma 1 bloque “transparente” , añadimos 1 byte “252” pero eliminamos el “255” del primer scanline y el COORD_X y COORD_Y del segundo, por lo que sumamos 1 bytes pero restamos 3, ahorrando 2 bytes.

Si la separación entre 2 scanlines la forman 2 bloques “transparentes” , añadimos 2 bytes “252” pero eliminamos el “255” del primer scanline y el COORD_X y COORD_Y del segundo, por lo que sumamos 2 bytes pero restamos 3, ahorrando 1 byte.

Para más de 3 bloques transparentes no se produce ningún ahorro, sino todo lo contrario.

La pega de esta técnica es que a cambio de 1 ó 2 bytes de ahorro en estas situaciones pasamos a poder utilizar 252 tiles (0-251) y se requiere un pequeño tiempo de procesado extra en la rutina para detectar y saltar los tiles “252”.


Mapeados de diferentes tilesets

Cabe también la posibilidad de codificar las pantallas con datos de diferentes tilesets gráficos.

En ese caso, podemos añadir a todas las posibilidades vistas hasta ahora un “identificador de tileset” que referencie a una tabla con los datos de los diferentes tilesets (datos gráficos, datos de atributo, ancho y alto):

Tabla_IDs_Tilesets:
  DW dir_tileset_gfx_1
  DW dir_tileset_attrib_1
  DB ancho_tiles_tileset1, alto_tiles_tileset1
  DW dir_tileset_gfx_2
  DW dir_tileset_attrib_2
  DB ancho_tiles_tileset2, alto_tiles_tileset2
  DW dir_tileset_gfx_3
  DW dir_tileset_attrib_3
  DB ancho_tiles_tileset3, alto_tiles_tileset3

De esta forma podemos tener pantallas codificadas por codificación básica con tiles de diferentes tamaños:

Pantalla1:
   DB ID_TILESET, X, Y, ID_TILE, ID_TILESET, X, Y, ID_TILE
   DB ID_TILESET, X, Y, ID_TILE, ID_TILESET, X, Y, ID_TILE
   DB (...) 
   DB 255 (fin de pantalla)

Podemos combinar este tipo de codificación con las técnicas de agrupación que ya hemos visto agrupando siempre tiles de un mismo tipo.

En este caso las coordenadas X e Y de impresión deben de ser coordenadas de pantalla y no de “mapa” ya que no todos los tiles tienen el mismo tamaño, por lo que resulta más óptimo que el mapa indica la posición exacta de pantalla donde va cada tile.

Por otra parte, esta técnica añade 1 byte extra de ocupación a cada tile, por lo que las pantallas ocuparán más que con tileset único.

La codificación e impresión de este tipo de mapas es mucho más compleja que utilizar un único tileset con tiles de tamaños idénticos. No obstante, existirán situaciones donde resulta necesario disponer de 2 tilesets o spritesets diferentes.


Mapeados de tiles de cualquier tamaño de bloque

Además de la posibilidad de disponer de mapeados basados en diferentes tilesets, podemos componer las pantallas de nuestro juego a partir de todo tipo de gráficos y tiles de tamaños diversos, pertenezcan o no a tilesets.

Para eso, debemos dejar de pensar en las pantallas como matrices de tiles y visualizarlas y definirlas como “listas de sprites a dibujar” (sean tiles o no).

Primero debemos definir una tabla que almacene la información de todos los elementos gráficos que pueden formar parte de una pantalla:

Tabla_Tiles:
   DW tileset_1_gfx+(0*32)             ; Cada tile ocupa 8*4=32 bytes en el tileset 16x16
   DW tileset_1_attr+(0*4)             ; Cada tile ocupa 4 atributos en el tileset 16x16
   DB 16, 16
   DW tileset_1_gfx+(1*32)             ; Apuntamos a datos del tile 1 del tileset
   DW tileset_1_attr+(1*4)
   DB 16, 16
   DW tileset_1_gfx+(2*32)
   DW tileset_1_attr+(2*4)
   DB 16, 16
   DW tileset_2_gfx+(0*8)              ; Cada tile ocupa 8 bytes en el tileset 8x8
   DW tileset_2_attr+(0*1)             ; Cada tile ocupa 1 atributo en el tileset 8x8
   DB 8, 8
   DW tileset_2_gfx+(1*8)
   DW tileset_2_attr+(1*0)
   DB 8, 8
   DW logotipo_gfx
   DW logotipo_attr
   DB 32, 16
   DW piedra_gfx
   DW piedra_attr
   DB 32, 32
   DW muro_grande_gfx
   DW muro_grande_attr
   DB 64, 64
   DW grafico_escalera_gfx
   DW grafico_escalera_attr
   DB 16, 32

A continuación definimos la pantalla en formato “codificación básica” utilizando identificadores de tile que serán índices en la anterior tabla:

Pantalla:
   DB X, Y, ID_EN_TABLA_TILE, X, Y, ID_EN_TABLA_TILE, (...), 255

La pantalla acabará en un valor 255 y los valores de X e Y deberán ser coordenadas exactas de pantalla. La rutina de impresión recorrerá cada byte de la pantalla (hasta encontrar el fin de pantalla o 255) y trazará todos los sprites llamando a la rutina genérica de DrawSprite_MxN.

Nuestra rutina de impresión puede acceder a los datos del tile mediante multiplicación de ID_EN_TABLA_TILE por 2+2+2 e imprimir el sprite gfx/attr/ancho/alto leído desde la tabla.

Con este sistema se debe de diseñar y codificar manualmente la pantalla, pero nos permite tener una riqueza gráfica que no siempre se puede conseguir sólo con tiles de tamaños fijos.

Por contra, nos obliga a utilizar la rutina de impresión genérica DrawSprite_MxN, que no es tan rápida como las rutinas específicas. Para evitar esto, lo que podemos hacer es modificar la rutina genérica para que en caso de que el ANCHO y el ALTO del sprite coincidan con el de alguna de las rutinas específicas disponibles hagan la llamada a dicha rutina, y en caso contrario, utilicen el código genérico.

El código a añadir a la rutina genérica podría ser similar al siguiente:

DrawSprite_MxN_LD_extendida:
 
   LD A, (DS_HEIGHT)
   LD B, A
   LD A, (DS_WIDTH)
   LD C, A                   ; Obtenemos datos del sprite
 
   ;;; B = ALTO de Sprite
   ;;; C = ANCHO de Sprite
 
   LD A, C                   ; A = ANCHO
   CP 16                     ; Comparar ancho
   JR NZ, dspMN_no16         ; ¿es 16?
 
   SUB B                     ; A = Ancho - alto
   JR NZ, dspMN_generica     ; Si no es cero, rutina generica
                             ; Es cero, imprimir 16x16:                 
   CALL DrawSprite16x16      ; Imprimir via especifica 16x16
   RET
 
dspMN_no16:
   LD A, C                   ; Recuperamos ancho
   CP 8                      ; ¿es 8?
   JR NZ, dspMN_generica     ; Si no es 8, ni 16, generica
   SUB B                     ; A = Ancho - alto
   JR NZ, dspMN_generica     ; Si no es cero, rutina generica
 
   CALL DrawSprite_8x8       ; Imprimir via especifica 8x8
   RET
 
dspMN_generica:
   ;;; (resto rutina generica)
   RET

De esta forma, utilizaremos la rutina genérica sólo para los tamaños de sprite de los que no dispongamos rutinas de impresión específicas.



Tabla sólo de los tiles no estándar

Lo más normal es que el mayor porcentaje de gráficos de la pantalla se tomen desde un tileset y unos pocos desde bitmaps de diferentes tamaños ajenos a éste. En ese caso nos podemos ahorrar la tabla de direcciones de tiles si, por ejemplo, definimos estos gráficos especiales a partir de un determinado valor numérico.

Supongamos que tenemos 200 tiles y 20 gráficos de tamaño no estándar que queremos ubicar en nuestros mapeados. En ese caso utilizamos los valores numéricos del 200 al 220 como identificadores de estos tiles y construímos la tabla de direcciones sólo con los datos de estos gráficos:

Tiles_Extendidos:
   DW logotipo_gfx
   DW logotipo_attr
   DB 32, 16
   DW piedra_gfx
   DW piedra_attr
   DB 32, 32
   DW muro_grande_gfx
   DW muro_grande_attr
   DB 64, 64
   DW grafico_escalera_gfx
   DW grafico_escalera_attr
   DB 16, 32

Nuestra rutina de impresión de mapeado sería idéntica a las ya vistas con un pequeño cambio: Cuando la rutina encontrara un identificador de tile menor de 200 (0-199), utilizaría la rutina de impresión basada en tileset, y cuando encontrara un tile mayor o igual a 200, utilizaría los datos de esta tabla usando como índice el valor (TILE-200). De esta forma se podría utilizar la rutina de impresión de sprites específica para nuestros tiles y la genérica sólo para estos tiles “no estándar”. Además nos ahorramos 200*6 bytes (2 de la dirección de los gráficos, 2 de la dirección de atributos y 2 de ancho y alto) en la tabla de “tiles extendidos”.

Este sistema permite extender el funcionamiento “estándar” basado en un tileset añadiendo la posibilidad de incluir determinados gráficos “no estándar” en la pantalla, sin complicar las rutinas de impresión y añadiendo sólo la necesidad de una sencilla tabla que ocuparía 6 bytes de datos por cada “tile no estándar” que queramos incluir en el mapa del juego.


Todas las rutinas que hemos visto se basan en la impresión de tiles con contenido y la no impresión de tiles considerados fondo o transparencia.

En ambos caso, el tile que se utiliza como fondo es un tile único (usualmente un tile sólido con el color del fondo).

Si nuestro personaje es monocolor y no se va a producir attribute clash es posible que queramos un fondo no sólido basado en un patrón repetitivo sobre el que nuestro personaje se pueda desplazar (con el mismo color de tinta y papel que los sprites que se vayan a mover sobre él).

Lo que estabamos haciendo hasta ahora era borrar el área de pantalla donde íbamos a dibujar el tileset, y trazar sólo aquellos tiles codificados en la pantalla y que eran contenido real (diferente del fondo).

Si incluímos ahora en el tileset uno o varios tiles específicos para el fondo y los codificamos dentro de las pantallas, entonces dejará de ser posible la compresión ya que todos los tiles de la pantalla tendrían que ser dibujados (ya no “ignoramos” los bloques de fondo sino que ahora hay que dibujarlos para trazar ese nuevo fondo gráfico). Con esto, se esfuma nuestro ahorro de memoria.

Podemos utilizar una sencilla solución basada en tabla para disponer de fondos en las pantallas y continuar ahorrando memoria con la codificación. La tabla alojará la dirección

FONDO_PIEDRA    EQU   0
FONDO_BALDOSAS  EQU   1
FONDO_HIERBA    EQU   2
 
Fondos:
   DW fondo_piedra_gfx
   DW fondo_piedra_attr
   DB 16, 16
   DW fondo_baldosas_gfx
   DW fondo_baldosas_attr
   DB 64, 64
   DW fondo_hierba_gfx
   DW fondo_hierba_attr
   DB 32, 32

De nuevo, para acceder a los datos de un fondo específico lo haremos mediante:

DIR_DATOS_FONDO    = [ Fondos + (ID_FONDO * 6) ]

A continuación, definimos el mapa de forma que además de la dirección de memoria de la pantalla y las conexiones con otras localidades incluya un identificador dentro de los nuestros fondos:

Mapa:
  DW Pantalla_Inicio            ; ID = 0
  DB -1, 1                      ; Conexiones izq y derecha ID 0
  DB FONDO_BALDOSAS
  DW Pantalla_Salon             ; ID = 1 
  DB 0, 2                       ; Conexiones izq y derecha ID 1
  DB FONDO_PIEDRA
  DW Pantalla_Pasillo           ; ID = 2
  DB 1, 3                       ; Conexiones izq y derecha ID 2
  DB FONDO_PIEDRA
  (...)

(NOTA: Esto modificará la cantidad por la que hay que multiplicar para acceder a los datos de una pantalla ya que ahora incluyen 1 byte adicional).

De esta forma, cada pantalla tiene un bitmap de fondo asociado que puede ir desde el tamaño de un simple tile 8×8 hasta 256×192 píxeles si así lo deseamos.

Finalmente, en lugar de realizar un borrado del área de pantalla donde vamos a imprimir el mapa sería necesario programar una rutina que utilice el sprite Fondo (mediante su ID y la tabla de Fondos). La rutina deberá “rellenar” vía impresión por repetición el área de juego con el fondo seleccionado. Para simplificar la rutina es recomendable que todos los tamaños de los tiles de fondo sean dividores exactos del tamaño de la pantalla, de forma que la rutina no tenga que calcular si la impresión del último tile que quepa en la misma ha de ser dibujado total o parcialmente. Si el área de juego es de 256 píxeles, podremos utilizar tiles de ancho 8 (32 repeticiones), 16 (16 repeticiones), 32 (8 repeticiones), 64 (4 repeticiones), 128 (2 repeticiones) o incluso de 256 (1 repetición). Lo mismo ocurriría para la altura.

La rutina tendría que utilizar DrawSpriteMxN (ya que los fondos pueden tener cualquier tamaño) pero se recomienda que emplee rutinas específicas en los tamaños de fondo para los que tengamos rutina de impresión disponible.

De esta forma, rellenamos el área de juego con el fondo asociado a dicha pantalla y después llamamos a DrawMap para dibujar sobre este “fondo” los tiles reales.

¿Qué ahorramos con este sistema? El ahorro consiste en que hemos definido el fondo de una pantalla con 1 sólo byte (el Identificador de Fondo que asociado a cada pantalla en la estructura de mapa) y ya no es necesario codificar los tiles de fondo en las pantallas. Los tiles del mapa que antes eran tiles con valores numéricos de fondo ahora serán “vacíos” o “transparentes” con lo que la codificación los ignorará y las pantallas sólo incluirán los datos de los tiles “reales”. Los tiles “transparentes”, al no dibujarse, permitirán ver en sus posiciones el fondo predibujado.

Además esto nos permite cambiar el fondo asociado a una pantalla rápidamente (en la estructura de mapa) sin tener que modificar todos los valores de tiles de fondo en una o más pantallas (y/o recodificar las pantallas). Y por si fuera poco, nada nos obliga a que el fondo sea un tile de tamaño igual al de los bloques del tileset, lo que puede dotar de mayor riqueza gráfica a nuestro juego.

Una versión más óptima de este sistema pero que requiere modificar las rutinas de impresión de mapeado podría ser que la rutina de impresión, cuando encontrara un bloque vacío/transparente, utilizara las coordenadas X e Y de impresión actuales como un “índice circular” en el gráfico de fondo para dibujar una porción del mismo con tamaño de un tile y así rellenar el “tile transparente” que estamos considerando. Sería algo parecido a un rellenado con patrón pero sólo en el espacio de un tile. Esta aproximación evitaría el dibujado de toda la pantalla con el fondo transparente, incluídas porciones de pantalla sobre los que después dibujaremos tiles gráficos.


Todavía podríamos arañar algunos bytes adicionales a las pantallas utilizando técnicas de compresión.



Compresión por repetición

La primera de las posibilidades de compresión se basa en repetición de tiles consecutivos en las técnicas de agrupación, utilizando un tipo de compresión que utilice un byte de control “253”, de tal modo que si se encuentra un valor de tile 253, a éste valor le siga el tile a repetir y el número de repeticiones (o al revés).

  DB 253, NUMERO_REPETICIONES, TILE_A_REPETIR

Así, una secuencia:

  ; 9 bytes
  DB 1, 2, 2, 2, 2, 2, 2, 3, 4

Se codificaría como:

  ; 6 bytes
  DB 1, 253, 6, 2, 3, 4

En este caso, la rutina de impresión deberá expandir “253, 6, 2” como “6 veces 2” = 2, 2, 2, 2, 2, 2.

Por desgracia, este tipo de compresión no suele resultar muy efectiva porque requiere que haya un mínimo de 3 tiles consecutivos iguales (lo que permitiría ahorrar el mínimo de 1 byte). En los mapeados con riqueza gráfica, las filas y columnas están formadas por diferentes tiles alternados y no se suele repetir el mismo tile de forma consecutiva una y otra vez, ya que evidenciaría la repetición de un mismo gráfico.

En cualquier caso, para aplicar este tipo de técnica tendríamos que modificar el script codificador (en busca de valores idénticos consecutivos) y la rutina de impresión (en busca del código de control 253 y con un bucle para repetir N veces el valor del tile comprimido).



Compresión por patrones

El sistema de compresión por patrones es el método que, probablemente, producirá los mejores resultados de reducción de tamaño de las pantallas en la mayoría de juegos.

Se basa en identificar “conjuntos de tiles” (no tienen por qué tener todos ellos el mismo valor) que se repitan a lo largo de la pantalla y de otras pantallas. Estos patrones se almacenan en memoria y se referencian por medio de una tabla que relaciona un “identificador de patrón” con la dirección donde está almacenado el patrón, finalizado en 255.

Por ejemplo, supongamos un juego de plataformas donde hay gran cantidad de “suelos”, “plataformas en el aire”, construcciones hechas con bloques, etc, que se repiten entre las diferentes pantallas. Podemos asociar cada “patrón” con un identificador y después codificar la pantalla utilizando algún código de control y la referencia al patrón.

Utilizaremos un código de control especial (253, por ejemplo) para codificar una ristra de datos “patrón” en lugar de tiles reales. Para estos tiles basados en patrones usaremos el siguiente formato (independiente de que la codificación del resto de tiles sea básica, horizontal, vertical o mixta):

Pantalla:
  DB X_TILE, Y_TILE, 253, ID_PATRON

(También podemos agrupar X_TILE e Y_TILE en un mismo byte como hicimos en el script codificador).

Supongamos la siguiente pantalla ilustrativa:

000000000000000878787
001232400000000878787
000000001232400878787
000000000000000878787
012324000000000878787
000000000000000879987
123240012324000879987

Los tiles “1, 2, 3, 2, 4” representan en nuestro “ejemplo” una plataforma de suelo sobre la que el jugador puede saltar, siendo el tile 1 un “borde izquierdo”, el tile 4 un “borde derecho” y los tiles 2, 3, 2 tiles que representan 3 bloques de suelo.

Los tiles “8, 7, 8, 7, 8, 7” representan bloques de “piedra” que forman parte de un castillo cuyo tile de “puerta” es el “9”.

En la anterior pantalla encontramos los siguientes patrones:

CODIF_HORIZONTAL  EQU   0
CODIF_VERTICAL    EQU   1
 
Patron0:
  DB CODIF_HORIZONTAL, 1, 2, 3, 2, 4, 255
 
Patron1:
  DB CODIF_VERTICAL, 8, 8, 8, 8, 8, 8, 8, 255
 
Patron2:
  DB CODIF_VERTICAL, 7, 7, 7, 7, 7, 7, 7, 255
 
Patron3:
  DB CODIF_VERTICAL, 7, 7, 7, 7, 7, 255
 
Patron4:
  DB CODIF_VERTICAL, 8, 8, 8, 8, 8, 255

A continuación creamos una tabla de direcciones de patrones relacionadas con su ID:

Tabla_Patrones:
  DW Patron0
  DW Patron1
  DW Patron2
  DW Patron3
  DW Patron4
  (...)

Mediante esta tabla podemos acceder a los datos de cualquier patrón como Tabla_Patrones + ID_PATRON*2.

Si llamamos a estos patrones en el orden que los hemos visto, como A, B, C, D y E, obtenemos 1 patrón horizontal y 4 verticales:

    X         11111111112
Y   012345678901234567890
  -------------------------
0 | 000000000000000BCEDBC |
1 | 00AAAAA00000000BCEDBC |
2 | 00000000AAAAA00BCEDBC |
3 | 000000000000000BCEDBC |
4 | 0AAAAA000000000BCEDBC |
5 | 000000000000000BC99BC |
6 | AAAAA00AAAAA000BC99BC |
  -------------------------

(Nota: se ha numerado la coordenada X y la Y para facilitar la codificación manual que realizaremos a continuación)

Vamos a codificar cada “patrón” en una línea de DBs diferente para simplificar su lectura. Nótese que los tiles “9” no forman parte de ningún patrón y se han codificado como 2 scanlines horizontales de tiles:

Pantalla:
  DB 2, 1, 253, 0
  DB 8, 2, 253, 0
  DB 1, 4, 253, 0
  DB 0, 6, 253, 0
  DB 7, 6, 253, 0
  DB 15, 0, 253, 1
  DB 19, 0, 253, 1
  DB 16, 0, 253, 2
  DB 20, 0, 253, 2
  DB 17, 0, 253, 3
  DB 18, 0, 253, 4
  DB 17, 5, 9, 9, 255
  DB 17, 6, 9, 9, 255

Nótese que las líneas de definición de patrón no necesitan acabar en 255 porque tienen un tamaño definido (4 bytes) y además el patrón en sí ya acaba en 255.

La pantalla original tenía un total de 67 tiles y se ha codificado con apenas 52 bytes. La codificación con métodos sin compresión habría sido de 67 bytes más los datos de posicionamiento y fin de scanline porque hay que almacenar esta información para cada “ristra”, es decir, 3 bytes por cada “scanline” (hay 11), lo que habría sumado 11*3 = 33 bytes adicionales dando un total de 101 bytes para codificar la pantalla (100 más el byte 254 de cambio de codificación).

Puede que viendo una sóla pantalla no parezca un gran ahorro, pero en el global del mapa de juego cada nueva pantalla que tenga repetido cualquiera de los patrones que tenemos en Tabla_Patrones permitirá codificarlo con apenas 4 bytes (X, Y, 253, ID_PATRON), ó 3 bytes si se codifican en el mismo byte la coordenada X y la coordenada Y.

Y cabe decir que dadas las limitaciones de memoria del Spectrum, los juegos tienen a repetir patrones de tiles para los diferentes tipos de suelos, techos, paredes, etc:


 Tres luces de Glaurung

Patrones codificables: suelo, paredes verticales, etc


Este sistema acaba consiguiendo ratios de compresión muy buenos pero basa toda su técnica en el programa codificador: el script/programa debe analizar todas las pantallas del mapeado en una pasada (no vale con analizar sólo la pantalla que estamos codificando).

A partir de esas pantallas debe de crear un diccionario de “patrones” formado por todas las combinaciones de bloques que aparezcan, así como las mismas combinaciones de menor tamaño, y cuantificar cuántas veces aparece cada “patrón potencial”. Finalmente, se utilizan las N entradas que producirían más sustituciones para su uso como patrón. Es un algoritmo muy parecido a la compresión LZW.

Además de modificar la rutina mapeadora para añadirle la impresión de patrones es necesario realizar un codificador específico más complejo que los que hemos visto en este capítulo.

Finalmente, es importante recordar que no estamos atados a utilizar una única técnica de las que acabamos de ver: podemos utilizar, por ejemplo, codificación mixta con compresión y patrones, o con patrones pero sin compresión, etc, mezclando las diferentes técnicas de codificación y de impresión en rutinas específicas a tal efecto.


A la hora de diseñar el mapa del juego tenemos que tener en cuenta las propiedades de cada uno de los tiles del mapeado. ¿Qué tiles deben de ser sólidos de forma que el personaje no pueda atravesarlos? ¿Qué tiles, pese a ser dibujados, deben de ser tomados como “parte del fondo” y el personaje puede pasar a través de ellos? ¿Qué tiles soportan el peso del personaje y cuáles deben “deshacerse” cuando el personaje los pise? ¿Debe un tile concreto permitir al personaje atravesarlo saltando desde debajo de él pero ser sólido al pisarlo?

Este tipo de características de los tiles se conoce como “propiedades o atributos”, y aunque no son usados por las rutinas de impresión de mapeados, sí que son utilizados por el bucle principal del programa a la hora de mover los personajes o enemigos para determinar si se puede atravesar una determinada zona de pantalla, si el personaje debe morir por pisar un tile concreto, etc.

La primera distinción suele ser marcar qué tiles son “sólidos” y a través de cuáles puede pasar el jugador. En muchos juegos se utiliza el tile 0 como tile “de fondo” y el resto de tiles como bloques sólidos (Manic Miner, etc) pero es posible que necesitemos que algunos tiles sean dibujados y sin embargo nuestro personaje pueda pasar a través de ellos. Todos los tiles serán iguales para la rutina de impresión, pero no lo serán para las rutinas de gestión de movimiento de nuestro personaje y de los enemigos.


 Atravesando tiles

Nuestro personaje tiene que poder atravesar la columna


En ese caso, podemos utilizar un valor numérico como límite entre tiles no sólidos y tiles sólidos. Por ejemplo, podemos considerar que los primeros tiles 0-99 son atravesables por el jugador y los tiles del 100 en adelante serán sólidos. La rutina que gestione el movimiento de nuestro personaje deberá obtener del mapa el identificador de tile y permitirnos pasar a través de él o no según el valor obtenido.

Para ciertos juegos es posible que ni siquiera necesitemos definir propiedades de solidez y que (según el tipo de juego) baste con que el personaje se pueda mover sobre el color de fondo y que no pueda atravesar cualquier color distinto de este.


 Turrican

Aparte de la típica clasificación entre bloque sólido y no sólido, puede sernos imprescindible otorgar ciertas propiedades a los tiles y en ese caso nos resultará necesario disponer de algún tipo de estructura de datos que almacene esta información. Por ejemplo, otro caso típico de propiedad de tile es el de indicar si un determinado bloque de pantalla produce o no la muerte del jugador: como pinchos, lava, fuego, etc.

Se pueden definir estos atributos bien en los tiles (un mismo identificador de tile siempre cumple una determinada propiedad) o bien en los mapas (es la posición de pantalla la que cumple esa propiedad).

En el primero de los casos (propiedades en los tiles), necesitaremos una tabla que relacione cada identificador de tile con sus propiedades (bien usando un byte por propiedad o un simple bit en el caso de propiedades tipo sí/no).

;;; Byte de propiedades de cada tile:
;;; Bit 0 -> Tile solido (1) o atravesable por el jugador (0)
;;; Bit 1 -> El tile se "rompe" al pisarlo el jugador (1), o no (0)
;;; Bit 2 -> Si 1, cuando el jugador toca el tile, muere.
;;; Bit 3 -> El tile es de tile "escalera" (permite subir y bajar)
;;; Bit 4 -> El tile es un teletransportador
;;; (etc...)
Propiedades_Tiles:
  DB %00000000        ; Propiedades tile 0 (tile vacio)
  DB %00000001        ; Propiedades tile 1 (bloque)
  DB %00000010        ; Propiedades tile 2 (suelo que se rompe)
  DB %00000001        ; Propiedades tile 3 (otro bloque)
  DB %00000101        ; Propiedades tile 4 ("pinchos")

En el segundo de los casos (propiedades en un mapa alternativo), mucho más costoso en términos de memoria, necesitamos una “copia” del mapa pero que en lugar de almacenar tiles almacene las propiedades de ese punto del mapa. Esto permite que un gráfico determinado tenga un efecto en una zona de la pantalla, y otro fuera de ella.

Por ejemplo, con un mapa de propiedades podemos conseguir que tiles que son sólidos en un lugar sean atravesables en otro al estilo de las zonas secretas de juegos como Super Mario World. Podemos simular este efecto utilizando propiedades de tiles si “duplicamos” el tile gráfico en cuestión y a uno le asignamos la propiedad de solidez y al otro no. Tendríamos 2 identificadores de tile diferente a la hora de generar las pantallas con el mismo gráfico, pero diferente comportamiento.

Asignar propiedades a Tiles resulta en general mucho menos costoso en términos de memoria que asignarlas a posiciones de pantalla.


Por normal general, los enemigos, personajes y objetos del juego no se definen dentro del mapeado, sino que se cargan desde estructuras de datos separadas. Estas estructuras pueden incluir por ejemplo la posición X, Y del objeto, el ID de Pantalla en la que están esos objetos, así como referencias al sprite que lo representan.

Realmente, en los juegos es posible ubicar llaves, items de comida, salud, vidas u otros objetos en el mapeado, pero esto tiene la desventaja de que su ubicación es la misma para todas las ejecuciones del juego.

Cuando tratemos el capítulo dedicados a estructuras de datos veremos ejemplos de cómo definir estructuras que almacenen la información de posición, datos gráficos y características de objetos, enemigos, interruptores, puertas, y otros items del juego.


Conocemos la teoría y la práctica sobre el diseño de pantallas y mapas en cuanto a los mecanismos de codificación e impresión, pero a la hora de programar un juego se hace palpable la necesidad de disponer de una herramienta para diseñar las pantallas de forma visual en lugar de componerlas manualmente mediante los identificadores de los tiles.

En la mayoría de los casos, lo más rápido puede ser diseñar un sencillo editor en nuestra plataforma de desarrollo (por ejemplo, usando python y pygame o C++ y SDL) que cargue el tileset y nos permita seleccionar bloques del mismo y “dibujar” en la pantalla utilizando el actual bloque seleccionado. A este programa le podemos añadir funciones de grabación y carga de pantallas, además de la imprescindible opción de “exportación” a formato ASM.

Al diseñar el editor específicamente para nuestras necesidades, podemos agregar no sólo gestión de las pantallas sino del mapa en sí mismo, de tal modo que el editor nos permita definir las conexiones entre pantallas y genere la estructura de mapa lista para usar. También podemos agregar algún tipo de gestión de los atributos de cada tile en pantalla (si es sólido o no, etc). Otra función interesante sería la de permitir modificar el orden de los tiles en el tileset alterando las pantallas para reflejar ese cambio, lo que haría más sencillo reubicar tiles o añadir nuevos tiles por debajo de un valor numérico dado.

Si no tenemos el tiempo o los recursos para realizar un programa de estas características, siempre podemos optar por utilizar alguno de los ya existentes para nuestra plataforma de desarrollo. Es imprescindible asumir que deberemos programar algún tipo de script/programa de conversión del formato de pantalla utilizado por la herramienta de mapeados al formato que nosotros deseamos, una ristra de “DBs” incluíble en nuestro programa.

Alguno de los programas más conocidos para este tipo de tareas son:

Map Editor es el editor más moderno de los 3, y soporta exportación del mapa a formato XML lo que puede facilitar la creación del script de conversión a formato ASM. Además, es una herramienta Free Software, por lo que disponemos tanto del código fuente como del permiso para modificarlo y adaptarlo a nuestras necesidades.


 Map Editor

Mappy es más antiguo y se diseñó para juegos basados en las librerías de PC “Allegro” y (actualmente) “SDL”. El código fuente está basado en la librería Allegro y puede ser también modificado a nuestro antojo.

Mappy Linux es un pequeño programa que utiliza las bibliotecas de MappySDL para actuar como un editor de mapeados. Al disponer del código fuente, puede ser directamente adaptado a nuestras necesidades. Como es un editor realmente sencillo, su código no es tan complejo como pueda serlo el de Map Editor.

No obstante, es probable que resulte más rápido el crear un sencillo programa desde cero que el adaptar el código de cualquiera de estos programas, sin olvidar que siempre nos queda la posibilidad de diseñar las pantallas manualmente.

Recordemos por otra parte que nuestro editor de mapeados grabará normalmente la pantalla en formato “crudo” y que debemos utilizar nuestro programa “codificador” para reducir el tamaño de cada pantalla. Si estamos creando un programa propio, podemos aprovechar el script codificador llamándolo desde el propio editor para exportar las pantallas directamente codificadas.


Aunque el tiempo de generación de una pantalla por bloques es practicamente imperceptible por el usuario, si queremos evitar que se vea la generación del mapa podemos utilizar una pantalla virtual como destino de la impresión de los tiles y después realizar un volcado de la pantalla virtual completa sobre la videoram.

Esto implica la necesidad de 6912 bytes de memoria para evitar que el usuario vea la generación del mapa, por lo que no es normal utilizar esta técnica a menos que esté realmente justificado.

Si utilizamos pantallas virtuales necesitaremos modificar todas las rutinas de impresión para que trabajen con una dirección destino diferente en lugar de sobre $4000. Para que las rutinas sigan pudiendo utilizar los trucos de composición de dirección en base a desplazamientos de bits que vimos en capítulos anteriores lo normal es que busquemos una dirección de memoria libre cuyos 3 bits más altos ya no sean “010b” (caso de la videoram → $4000 → 0100000000000000b) sino, por ejemplo “110” ($C000). De esta forma se pueden alterar las rutinas fácilmente para trabajar sobre un área de 7KB equivalente a la videoram pero comenzando en $C000.

Por otra parte, si nos estamos planteando el usar una pantalla virtual simplemente para que no se vea el proceso de construcción de la pantalla, podemos ahorrarnos la pantalla virtual separando la rutina de impresión de pantalla en 2: una de impresión de gráficos y otra de impresión de atributos. Así, rellenamos el área de pantalla que aloja el mapa con atributos “cero” (negro), trazamos los datos gráficos (que no podrá ver el usuario) y después trazamos los atributos. Estos atributos podemos trazarlos directamente en pantalla tras un HALT, o en una “tabla de atributos virtual” de 768 que después copiaremos sobre el área de atributos. De esta forma utilizamos una pantalla virtual de 768 bytes en lugar de requerir 6912.


Todo el capítulo se ha basado en impresión de tiles en posiciones exáctas de bloque y con tiles múltiplos de carácter, es decir, en impresión de tiles en baja resolución. Esta técnica es adecuada para prácticamente cualquier juego sin scroll pixel a pixel de los tiles.

Para poder imprimir tiles en posiciones de alta resolución que no coincidan con caracteres exactos se tienen que utilizar técnicas de rotación / prerotación de los tiles gráficos tal y como veremos en el próximo capítulo.