cursos:ensamblador:gfx4_fuentes

No renderer 'odt' found for mode 'odt'

Gráficos (y IV): Fuentes de texto

En prácticamente cualquier programa o juego nos encontraremos con la necesidad de imprimir en pantalla texto y datos numéricos en diferentes posiciones de pantalla: los menúes, los nombres de los niveles, los marcadores y puntuaciones, etc.

La impresión de fuentes de texto es una aplicación directa de las rutinas de impresión de sprites en baja resolución que creamos en el capítulo anterior. Cada carácter es un sprite de 8×8 píxeles a dibujar en coordenadas (COLUMNA, FILA) de baja resolución.

Crearemos una rutina de impresión de caracteres y, basada en esta, una rutina de impresión de cadenas, con la posibilidad de añadir códigos de control y de formato, e impresión de datos numéricos o variables decimales, hexadecimales, binarias y de cadena.

Aprovechando la rutina de escaneo de teclado de la ROM, analizaremos también una rutina de INPUT de datos para nuestros programas en ensamblador basados en texto (aventuras, juegos de gestión, etc).

Finalmente, examinaremos una fuente de texto de 4×8 píxeles que nos permitirá una resolución en pantalla de 64 columnas de caracteres en pantalla.



Si creamos un juego gráfico de caracteres como un tileset de sprites de 8×8 píxeles cada uno conteniendo una letra del alfabeto, podemos utilizar las rutinas de impresión de sprites de 8×8 para trazar en pantalla cualquier carácter (visto como un sprite de 1×1 bloques).


 Parte de un charset en SevenuP

Parte de un tileset de caracteres en SevenuP


Ubicando las letras en el spriteset con el mismo orden que el código ASCII (empezando por el espacio, código 32, como sprite cero), podemos utilizar el código ASCII de la letra a imprimir como número de sprite dentro del tileset.


Los 95 caracteres imprimibles (96 en el Spectrum) del código ASCII.
Fuente: Wikipedia


Podríamos diseñar una fuente de 256 caracteres en el editor de Sprites, pero se requerirían 256*8 = 4096 bytes (4KB!) para almacenar los datos en memoria. En realidad, no es necesario crear 256 caracteres en el editor, puesto que en el Spectrum:

  • Los primeros 31 códigos ASCII son códigos de control.
  • Los códigos ASCII del 128 al 143 son los UDGs predefinidos e imprimibles.
  • Los códigos ASCII del 144 al 164 son los UDGs programables en modo 48K (del 144 al 162 en modo 128K)
  • Los códigos ASCII del 165 al 255 son los códigos de control (tokens) de las instrucciones de BASIC en modo 48K (del 163 al 255 en modo 128K).


 Juego de caracteres del Spectrum 32-164

Códigos ASCII en el Spectrum desde el 32 al 164
Fuente: Wikipedia


Como hay un total de 96 caracteres de texto imprimibles (desde el 32 al 127), para disponer de un juego de caracteres suficiente con minúsculas, mayúsculas, signos de puntuación y dígitos nos bastaría con una fuente de 96 sprites de 8 bytes cada uno, es decir, un total de 768 bytes.

Podemos agregar “caracteres” adicionales para disponer de códigos de control que nos permitan imprimir vocales con acentos, eñes, cedilla (ç), etc. Al definir los textos de nuestro programa habría que utilizar estos códigos de control en formato numérico (DB) intercalados con el texto ASCII (“España” = DB “Espa”, codigo_enye, “a).

Si tenemos problemas de espacio en nuestro programa también podemos utilizar un set de caracteres más reducido que acaben en el ASCII 'Z' (ASCII 90), lo que nos dejaría un charset con números, signos de puntuación y letras mayúsculas (sin minúsculas). El espacio ocupado por este spriteset sería de 90-32=58 caracteres, es decir, 464 bytes.

Por otra parte, no es necesario exportar los atributos de la fuente ya que lo normal es utilizar un atributo determinado para todos los caracteres de la misma, aunque nada nos impide dotar de diferente color a cada letra si así lo deseamos y exportar también los datos de atributo. En este capítulo sólo exportaremos los gráficos puesto que utilizaremos un atributo común para todos los elementos de la fuente, o cambios de formato (tinta y papel) manuales allá donde sea necesario.

Sea cual sea el tamaño de nuestro SpriteSet (desde el juego reducido de 58 caracteres hasta el set completo de 256, pasando por el estándar de 96), podemos imprimir caracteres de dicha fuente con las rutinas de impresión de 8×8 del capítulo anterior.

Para los ejemplos de este capítulo utilizaremos una fuente personalizada de texto de 64 caracteres (signos de puntuación, dígitos numéricos y letras mayúsculas, incluídas 5 vocales con acentos) dibujada por Javier Vispe y convertida a código con SevenuP:


 Fuente de ejemplo

Exportando los datos en SevenuP en formato X Char, Char Line, Y Char como Gfx only, obtenemos el siguiente fichero .asm:

; ASM source file created by SevenuP v1.20
; SevenuP (C) Copyright 2002-2006 by Jaime Tejedor Gomez, aka Metalbrain
 
;GRAPHIC DATA:
;Pixel Size:      (  8, 512)
;Char Size:       (  1,  64)
;Sort Priorities: Char X, Char line, Y char
;Data Outputted:  Gfx
;Interleave:      Sprite
 
charset1:
    DEFB   0,  0,  0,  0,  0,  0,  0,  0,   0, 24, 24, 24, 24,  0, 24,  0
    DEFB   0,108,108, 72,  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  56,  0, 76, 56,110,196,122,  0,   0, 12, 12,  8,  0,  0,  0,  0
    DEFB   0, 24,  0, 48, 48, 48, 24,  0,   0, 24,  0, 12, 12, 12, 24,  0
    DEFB   0,  0,  0,  0,  0,  0,  0,  0,   0, 24,  0,126,126, 24, 24,  0
    DEFB   0,  0,  0,  0,  0, 12, 12,  8,   0,  0,  0,126,126,  0,  0,  0
    DEFB   0,  0,  0,  0,  0, 56, 56,  0,   0,  0,  6, 12, 24, 48, 96,  0
    DEFB 124,  0,206,214,230,254,124,  0,  28,  0,124, 28, 28, 28, 28,  0
    DEFB 124,  0,198, 28,112,254,254,  0, 124,  0,198, 12,198,254,124,  0
    DEFB  12,  0, 60,108,254,254, 12,  0, 254,  0,192,252, 14,254,252,  0
    DEFB  60,  0,224,252,198,254,124,  0, 254,  0, 14, 28, 28, 56, 56,  0
    DEFB 124,  0,198,124,198,254,124,  0, 124,  0,198,126,  6,126,124,  0
    DEFB   0,  0, 24, 24,  0, 24, 24,  0, 118,  6,192,254,254,198,198,  0
    DEFB 246,  6,192,252,192,254,254,  0,  12, 12, 48, 56, 56, 56, 56,  0
    DEFB 118,  6,192,198,198,254,124,  0, 198,  6,192,198,198,254,124,  0
    DEFB   0, 24,  0, 24, 24, 24, 24,  0, 124,  0,198,254,254,198,198,  0
    DEFB 252,  0,198,252,198,254,252,  0, 124,  0,198,192,198,254,124,  0
    DEFB 252,  0,198,198,198,254,252,  0, 254,  0,192,252,192,254,254,  0
    DEFB 254,  0,224,252,224,224,224,  0, 124,  0,192,206,198,254,124,  0
    DEFB 198,  0,198,254,254,198,198,  0,  56,  0, 56, 56, 56, 56, 56,  0
    DEFB   6,  0,  6,  6,198,254,124,  0, 198,  0,220,248,252,206,198,  0
    DEFB 224,  0,224,224,224,254,254,  0, 198,  0,254,254,214,198,198,  0
    DEFB 198,  0,246,254,222,206,198,  0, 124,  0,198,198,198,254,124,  0
    DEFB 252,  0,198,254,252,192,192,  0, 124,  0,198,198,198,252,122,  0
    DEFB 252,  0,198,254,252,206,198,  0, 126,  0,224,124,  6,254,252,  0
    DEFB 254,  0, 56, 56, 56, 56, 56,  0, 198,  0,198,198,198,254,124,  0
    DEFB 198,  0,198,198,238,124, 56,  0, 198,  0,198,198,214,254,108,  0
    DEFB 198,  0,124, 56,124,238,198,  0, 198,  0,238,124, 56, 56, 56,  0
    DEFB 254,  0, 28, 56,112,254,254,  0,  60,102,219,133,133,219,102, 60
    DEFB   0,  0, 96, 48, 24, 12,  6,  0,  24,  0, 24, 48, 96,102, 60,  0
    DEFB  60,  0, 70, 12, 24,  0, 24,  0,   0,  0,  0,  0,  0,  0,  0,126


La rutina de impresión de caracteres debe de recoger el código ASCII a dibujar y realizar el cálculo para posicionar un puntero “origen” dentro del tileset contra el ASCII correspondiente. También debe calcular la dirección destino en la pantalla en base a las coordenadas en baja resolución. Una vez trazado el carácter, establecerá el atributo del mismo con el valor contenido en FONT_ATTRIB.

;-------------------------------------------------------------
; PrintChar_8x8:
; Imprime un caracter de 8x8 pixeles de un charset.
;
; Entrada (paso por parametros en memoria):
; -----------------------------------------------------
; FONT_CHARSET = Direccion de memoria del charset.
; FONT_X       = Coordenada X en baja resolucion (0-31)
; FONT_Y       = Coordenada Y en baja resolucion (0-23)
; FONT_ATTRIB  = Atributo a utilizar en la impresion.
; Registro A   = ASCII del caracter a dibujar.
;-------------------------------------------------------------
PrintChar_8x8:
 
    ld bc, (FONT_X)      ; B = Y,  C = X
    ex af, af'           ; Nos guardamos el caracter en A'
 
    ;;; Calculamos las coordenadas destino de pantalla en DE:
    ld a, b
    and $18
    add a, $40
    ld d, a
    ld a, b
    and 7
    rrca
    rrca
    rrca
    add a, c
    ld e, a              ; DE contiene ahora la direccion destino.
 
    ;;; Calcular posicion origen (array sprites) en HL como:
    ;;;     direccion = base_sprites + (NUM_SPRITE*8)
    ex af, af'           ; Recuperamos el caracter a dibujar de A'
    ld bc, (FONT_CHARSET)
    ld h, 0
    ld l, a
    add hl, hl
    add hl, hl
    add hl, hl
    add hl, bc         ; HL = BC + HL = FONT_CHARSET + (A * 8)
 
    ex de, hl          ; Intercambiamos DE y HL (DE=origen, HL=destino)
 
    ;;; Dibujar 7 scanlines (DE) -> (HL) y bajar scanline (y DE++)
    ld b, 7            ; 7 scanlines a dibujar
 
drawchar8_loop:
    ld a, (de)         ; Tomamos el dato del caracter
    ld (hl), a         ; Establecemos el valor en videomemoria
    inc de             ; Incrementamos puntero en caracter
    inc h              ; Incrementamos puntero en pantalla (scanline+=1)
    djnz drawchar8_loop
 
    ;;; La octava iteracion (8o scanline) aparte, para evitar los INCs
    ld a, (de)         ; Tomamos el dato del caracter
    ld (hl), a         ; Establecemos el valor en videomemoria
 
    ld a, h            ; Recuperamos el valor inicial de HL
    sub 7              ; Restando los 7 "inc h"'s realizados
 
    ;;; Calcular posicion destino en area de atributos en DE.
                       ; Tenemos A = H
    rrca               ; Codigo de Get_Attr_Offset_From_Image
    rrca
    rrca
    and 3
    or $58
    ld d, a
    ld e, l
 
    ;;; Escribir el atributo en memoria
    ld a, (FONT_ATTRIB)
    ld (de), a         ; Escribimos el atributo en memoria
    ret

La rutina es muy parecida a las que ya vimos en el capítulo anterior para impresión de sprites 8×8, pero eliminando el cálculo del atributo origen al establecerlo desde la variable FONT_ATTRIB.

Además, hemos semi-desenrollado el bucle de impresión para hacer 7 iteraciones y después escribir el último scanline fuera del bucle. De esta forma evitamos el inc de + inc h que se realizaría para el último scanline y que es innecesario. De nuevo, desenrollar totalmente el bucle sería lo más óptimo, ya que evitaríamos el ld b, 7 y el DJNZ.

La impresión se realiza por transferencia, pero podemos convertirla en impresión por operación lógica cambiando los:

    ld a, (de)       ; Tomamos el dato del caracter
    ld (hl), a       ; Establecemos el valor en videomemoria

Por:

    ld a, (de)       ; Tomamos el dato del caracter
    or (hl)          ; Hacemos OR entre dato y pantalla
    ld (hl), a       ; Establecemos el valor en videomemoria

Para llamar a nuestra nueva rutina establecemos los valores de impresión en las variables de memoria y realizamos el call correspondiente. Aunque en esta rutina podríamos utilizar el paso por registros, vamos a emplear paso de parámetros por variables de memoria por 2 motivos:

  • Para que sean utilizables desde BASIC.
  • Para crear un sistema de coordenadas de texto X, Y y de atributos que nos permita, como veremos más adelante en el capítulo, gestionar la impresión de texto de una manera más cómoda.

Nótese que nuestro set de caracteres empieza en el ASCII 32 (espacio) el cual se corresponde con el sprite 0 (sprite en blanco). En teoría, la rutina debería restar 32 a cada código ASCII recibido en el registro A para encontrar el identificador de sprite correcto en el tileset.

En lugar de realizar una resta en cada impresión, podemos establecer el valor de FONT_CHARSET a la dirección del array menos 256 (32*8), con lo que cuando se calcule la dirección origen usando el ASCII real estaremos accediendo al sprite correcto.

Así, asignando a FONT_CHARSET el valor de “charset1-256”, cuando solicitemos imprimir el ASCII 32, estaremos accediendo realmente a charset1-256+(32*8) = charset1-256+256 = charset1. De esta forma no necesitamos restar 32 a ningun carácter ASCII para que la rutina imprima el carácter correspondiente real a un valor ASCII.

El código de asignación de variables iniciales y llamada a la función sería pues similar al siguiente:

    ;;; Establecimiento de valores iniciales
 
    ld hl, charset1-256        ; Saltamos los 32 caracteres iniciales
    ld (FONT_CHARSET), hl      ; Establecemos el valor del charset
    ld a, 6
    ld (FONT_ATTRIB), a        ; Color amarillo sobre negro
 
    ;;; Impresion de un caracter "X" en 15,8
    ld a, 15
    ld (FONT_X), a
    ld a, 8
    ld (FONT_Y), a
    ld a, 'X'
    call PrintChar_8x8         ; Imprimimos el caracter

Nótese que esta implementación de PrintChar_8x8 modifica registros y no los preserva, por lo que si tenemos que salvaguardar el valor de algún registro, debemos realizar PUSH antes de llamar a la función y POP al volver de ella.

El siguiente ejemplo (donde se ha omitido el código de las funciones ya vistas hasta ahora) muestra el charset 8×8 de 64 caracteres en pantalla, en un bucle de 4 líneas de 16 caracteres cada una:

; Visualizacion del charset de ejemplo
    ORG 35000
 
    ld h, 0
    ld l, a
    call ClearScreenAttrib     ; Borramos la pantalla
 
    ld hl, charset1-256        ; Saltamos los 32 caracteres iniciales
    ld (FONT_CHARSET), hl
    xor a
    ld (FONT_X), a
    inc a
    ld (FONT_Y), a             ; Empezamos en (0,1)
    ld a, 6
    ld (FONT_ATTRIB), a        ; Color amarillo sobre negro
 
    ld c, 32                   ; Empezamos en caracter 32
 
    ;;; Bucle vertical
    ld e, 4
 
bucle_y:
 
    ;;; Bucle horizontal
    ld b, 16                   ; Imprimimos 16 caracteres horiz
 
bucle_x:
    ld a, (FONT_X)
    inc a
    ld (FONT_X), a              ; X = X + 1
 
    ld a, c
    push bc
    push de                     ; Preservamos registros
    call PrintChar_8x8          ; Imprimimos el caracter "C"
    pop de
    pop bc
 
    inc c                       ; Siguiente caracter
    djnz bucle_x                ; Repetir 16 veces
 
    ld a, (FONT_Y)              ; Siguiente linea:
    inc a
    inc a                       ; Y = Y + 2
    ld (FONT_Y), a
    xor a
    ld (FONT_X), a              ; X = 0
 
    dec e
    jr nz, bucle_y              ; Repetir 4 veces (16*4=64 caracteres)
 
loop:
    jr loop
    ret
 
;-------------------------------------------------------------
FONT_CHARSET   DW    0
FONT_ATTRIB    DB    56       ; Negro sobre gris
FONT_X         DB    0
FONT_Y         DB    0
;-------------------------------------------------------------
 
    END 35000

Un apunte: en este caso hemos usado FONT_CHARSET como una variable DW (2 bytes) embebida en el código, pero podríamos haber aprovechado la variable del sistema CHARS para ello, cambiando el FONT_CHARSET DW 0 por FONT_CHARSET EQU $5c36 (una referencia a memoria, no un espacio en ella), de forma que FONT_CHARSET apunte a CHARS, y ahorrar esos 2 bytes.


Nuestro siguiente objetivo es el de poder agrupar diferentes caracteres en una cadena y diseñar una función que permita imprimir toda la cadena mediante llamadas consecutivas a PrintChar_8x8.

Definiremos las cadenas de texto como ristras de códigos ASCII acabadas en un byte 0 (valor 0, no ASCII '0'):

cadena  DB   "EJEMPLO DE CADENA", 0

El pseudocódigo de la rutina sería el siguiente:

Pseudocódigo de la rutina:

; Recoger parametros

; Repetir:
;    Coger caracter actual de la cadena (apuntado por HL).
;    Si el caracter es 0, fin de la cadena (salir)
;    Incrementar HL para apuntar al siguiente caracter.
;    Imprimir el carácter llamando a PrintChar_8x8
;    Ajustar coordenada X en FONT_X
;    Ajustar coordenada Y en FONT_Y

Es extremadamente importante acabar la cadena con un cero para que la rutina pueda ejecutar su condición de salida. Si no se define la cadena correctamente, se continuará imprimiendo todo aquello apuntado por HL hasta encontrar un valor cero en memoria.

Veamos el código de la rutina:

;-------------------------------------------------------------
; PrintString_8x8:
; Imprime una cadena de texto de un charset de fuente 8x8.
;
; Entrada (paso por parametros en memoria):
; -----------------------------------------------------
; FONT_CHARSET = Direccion de memoria del charset.
; FONT_X       = Coordenada X en baja resolucion (0-31)
; FONT_Y       = Coordenada Y en baja resolucion (0-23)
; FONT_ATTRIB  = Atributo a utilizar en la impresion.
; Registro HL  = Puntero a la cadena de texto a imprimir.
;                Debe acabar en
;-------------------------------------------------------------
PrintString_8x8:
 
    ;;; Bucle de impresion de caracter
pstring8_loop:
    ld a, (hl)                ; Leemos un caracter de la cadena
    or a
    ret z                     ; Si es 0 (fin de cadena) volver
    inc hl                    ; Siguiente caracter en la cadena
    push hl                   ; Salvaguardamos HL
    call PrintChar_8x8        ; Imprimimos el caracter
    pop hl                    ; Recuperamos HL
 
    ;;; Ajustamos coordenadas X e Y
    ld a, (FONT_X)            ; Incrementamos la X
    inc a                     ; pero comprobamos si borde derecho
    cp 31                     ; X > 31?
    jr c, pstring8_noedgex    ; No, se puede guardar el valor
 
    ld a, (FONT_Y)            ; Cogemos coordenada Y
    cp 23                     ; Si ya es 23, no incrementar
    jr nc, pstring8_noedgey   ; Si es 23, saltar
 
    inc a                     ; No es 23, cambiar Y
    ld (FONT_Y), a
 
pstring8_noedgey:
    ld (FONT_Y), a            ; Guardamos la coordenada Y
    xor a                     ; Y ademas hacemos A = X = 0
 
pstring8_noedgex
    ld (FONT_X), a            ; almacenamos el valor de X
    jr pstring8_loop

La anterior rutina sería llamada como en el siguiente ejemplo:

    ;;; Prueba de impresion de cadenas:
 
    ld hl, charset1-256       ; Saltamos los 32 caracteres iniciales
    ld (FONT_CHARSET), hl
    ld a, 64+3
    ld (FONT_ATTRIB), a
    ld hl, cadena
    ld a, 0
    ld (FONT_X), a
    ld a, 15
    ld (FONT_Y), a
    call PrintString_8x8
    ret
 
cadena DB "PRUEBA DE IMPRESION DE TEXTO DE UNA CADENA LARGA.", 0

Y el resultado en pantalla es:


 Impresion de cadenas

La forma más óptima de programar la rutina de impresión consistiría en integrar el código de PrintChar_8x8 dentro de PrintString_8x8 evitando el recálculo de offset en pantalla en cada carácter. Se utilizaría DE como puntero en la fuente y HL como puntero en pantalla. Una vez calculada la posición inicial en pantalla para el primer carácter se variaría HL apropiadamente, incrementandolo en 1 para avances hacia la derecha tras la impresión de un carácter. Los retornos de carro se realizarían restando 31 a L y ejecutando Caracter_Abajo_HL. De esta forma se evitaría no sólo el call y RET contra PrintChar_8x8 sino que tampoco habría que realizar el continuo cálculo de la posición destino. Después habría que trazar los atributos repitiendo el proceso desde el inicio de la cadena.

Normalmente no se suele imprimir texto durante el desarrollo del juego (al menos no en el bucle principal de juego), por lo que puede no ser necesario llegar a este extremo de optimización a cambio de una mayor ocupación de las rutinas en memoria.

Si vamos a utilizar la impresión de cadenas en la introducción del juego o programa, los menúes, la descripción de las fases, los créditos, la pausa o los mensajes entre nivel y nivel o el final del juego, probablemente será suficiente con la rutina que acabamos de ver.

Las cadenas de texto que se puedan usar en el bucle principal del programa deberían imprimirse como Sprites, y los valores numéricos como impresión tipo PrintChar_8x8 de cada uno de sus dígitos, realizando sólo una conversión numérica del número de dígitos realmente necesario.


Hemos visto hasta ahora cómo imprimir cadenas de texto ASCII, pero en muchas ocasiones nos veremos en la necesidad de imprimir el valor numérico contenido por un registro o una variable de memoria.

Para poder realizar esta impresión necesitamos rutinas que conviertan un valor numérico en una representación ASCII, la cual imprimiremos luego con PrintString_8x8. Según la base de conversión, necesitaremos las siguientes rutinas:


  • Bin2String : Convierte el valor numérico de un registro o variable en una cadena ASCII con la representación binaria (unos y ceros) de dicho valor (base=2).
  • Hex2String : Convierte el valor numérico de un registro o variable en una cadena ASCII con la representación en hexadecimal (2 o 4 dígitos del 0 a la F) de dicho valor (base=16).
  • Int2String : Convierte el valor numérico sin signo de un registro o variable en una cadena ASCII con la representación decimal (N dígitos 0-9) de dicho valor (base=10).


El parámetro de entrada a las rutinas será L para valores de 8 bits o HL para valores de 16 bits, y todas las rutinas utilizarán un área temporal de memoria para escribir en ella la cadena resultante de la conversión:

;;; Espacio de almacenamiento de las rutinas de conversion:
 
conv2string DS 18

Las rutinas acabarán el proceso de conversión agregrando un 0 (END OF STRING) tras los ASCIIs obtenidos, con el objetivo de que la cadena pueda ser directamente impresa con las funciones vistas hasta ahora.

De esta forma, para imprimir en pantalla en formato Decimal el valor de 8 bits del registro A, haríamos lo siguiente:

    ;;; Guardar en L el valor a convertir y llamar a rutina
    ld l, a
    call Dec2String_8
 
    ;;; Imprimir la cadena resultante de la conversion:
    ld hl, conv2string
    call PrintString_8x8

Veamos las diferentes rutinas:



Bin2String: Conversión a representación binaria

La conversión de un valor numérico a una representación binaria se basa en testear el estado de cada uno de los bits del registro “parámetro” y almacenar en la cadena destino apuntada por DE un valor ASCII '0' ó '1' según el resultado del testeo.

En lugar de ejecutar 8 ó 16 comparaciones con el comando BIT, utilizaremos la rotación a la derecha del registro parámetro de forma que el bit a comprobar sea desplazado al Carry Flag y podamos testear su valor con un JR NC o JR C.

La rutina de conversión tiene 2 puntos de entrada diferentes según necesitemos convertir un número de 8 bits (valor en registro L) o de 16 bits (valor en registro HL), pero utiliza el mismo core de conversión para ambos casos:

;-------------------------------------------------------------
; Bin2String_8 y Bin2String_16:
; Convierte el valor numerico de L o de HL en una cadena de
; texto con su representacion en binario (acabada en 0).
;
; IN:   L = Numero a convertir para la version de 8 bits
;       HL = Numero a convertir para la version de 16 bits
; OUT:  [conv2string] = Cadena con la conversion a BIN.
; Usa:  BC
;-------------------------------------------------------------
 
Bin2String_16:                ; Punto de entrada de 16 bits
    ld de, conv2string        ; DE = puntero cadena destino
    ld c, h                   ; C = a convertir (parte alta)
    call Bin2String_convert   ; Llamamos a rutina conversora
    jr Bin2String_8b          ; Convertimos la parte baja,
                              ; saltando el ld de, conv2string
 
Bin2String_8:                 ; Punto de entrada de 8 bits
    ld de, conv2string        ; DE = puntero cadena destino
 
Bin2String_8b:
    ld c, l                   ; C = a convertir (parte baja)
    call Bin2String_convert   ; Llamamos a rutina conversora
    xor a
    ld (de), a                ; Guardar End Of String
    ret
 
Bin2String_convert:
    ld b, 8                   ; 8 iteraciones
b2string_loop:                ; Bucle de conversion
    rl c
    ld a, '1'                 ; Valor en A por defecto
    jr nc, b2string_noC
    ld (de), a                ; Lo almacenamos en la cadena
    inc de
    djnz b2string_loop
    ret
b2string_noC:
    dec a                     ; A = '0'
    ld (de), a
    inc de                    ; Lo almacenamos y avanzamos
    djnz b2string_loop
    ret

En el área de memoria apuntada por conv2string tendremos la representación binaria en ASCII del valor en L o HL, acabado en un carácter 0, lista para imprimir con PrintString_8x8.



Hex2String: Conversión a representación hexadecimal

Veamos la rutina de conversión de un valor numérico a una representación hexadecimal:

;-------------------------------------------------------------
; Hex2String_8 y Hex2String_16:
; Convierte el valor numerico de L o de HL en una cadena de
; texto con su representacion en hexadecimal (acabada en 0).
; Rutina adaptada de http://baze.au.com/misc/z80bits.html .
;
; IN:   L = Numero a convertir para la version de 8 bits
;       HL = Numero a convertir para la version de 16 bits
; OUT:  [conv2string] = Cadena con la conversion a HEX.
;-------------------------------------------------------------
Hex2String_16:
    ld de, conv2string        ; Cadena destino
    ld a, h
    call B2AHex_Num1          ; Convertir Hex1 de H
    ld a, h
    call B2AHex_Num2          ; Convertir Hex2 de H
    jr Hex2String_8b          ; Realizar conversion de L
 
Hex2String_8:                 ; Entrada para la rutina de 8 bits
    ld de, conv2string
 
Hex2String_8b:
    ld a, l
    call B2AHex_Num1          ; Convertir Hex1 de L
    ld a, l
    call B2AHex_Num2          ; Convertir Hex2 de L
    xor a
    ld (de), a                ; Guardar End Of String
    ret
 
B2AHex_Num1:
    rra
    rra
    rra                       ; Desplazamos 4 veces >>
    rra                       ; para poder usar el siguiente bloque
 
B2AHex_Num2:
    or $f0                    ; Enmascaramos 11110000
    daa                       ; Ajuste BCD
    add a, $a0
    adc a, $40
    ld (de), a                ; Guardamos dato
    inc de
    ret

La rutina siempre devolverá una cadena de 2 caracteres + End_Of_String para valores de 8 bits, y de 4 caracteres + End_Of_String para valores de 16 bits.



Int2String: Conversión a representación decimal

Finalmente, veamos la rutina que convierte un valor numérico en su representación en formato decimal natural (representación de número positivo).

;-----------------------------------------------------------------
; Int2String_8 e Int2String_16:
; Convierte un valor de 16 bits a una cadena imprimible.
;
; Entrada:   HL = valor numerico a convertir
; Salida:    Cadena en int2dec16_result .
; De:        Milos Bazelides http://baze.au.com/misc/z80bits.html
;-----------------------------------------------------------------
Int2String_16:
    ld de, conv2string           ; Apuntamos a cadena destino
    ld bc, -10000                ; Calcular digito decenas de miles
    call Int2Dec_num1
    ld bc, -1000                 ; Calcular digito miles
    call Int2Dec_num1
    jr Int2String_8b             ; Continuar en rutina de 8 bits (2)
 
Int2String_8:                    ; Punto de entrada de rutina 8 bits
    ld de, conv2string           ; Apuntar a cadena destino
    ld h, 0                      ; Parte alta de HL = 0
 
Int2String_8b:                  ; rutina de 8 bits (2)
    ld bc, -100                  ; Calcular digito de centenas
    call Int2Dec_num1
    ld c, -10                    ; Calcular digito de decenas
    call Int2Dec_num1
    ld c, b                      ; Calcular unidades
    call Int2Dec_num1
    xor a
    ld (de), a                   ; Almacenar un fin de cadena
    ret
 
Int2Dec_num1:
    ld a,'0'-1                   ; Contador unidades, empieza '0'-1
 
Int2Dec_num2:
    inc a                        ; Incrementamos el digito ('0', ... '9')
    add hl, bc                   ; Restamos "unidades" hasta sobrepasarlo
    jr c, Int2Dec_num2           ; Repetir n veces
    sbc hl, bc                   ; Deshacemos el último paso
    ld (de), a                   ; Almacenamos el valor
    inc de
    ret

Esta rutina almacena en la cadena todos los valores '0' que obtiene, incluídos los de las unidades superiores al primer dígito del número (leading zeros). Así, la conversión del valor de 8 bits 17 generará una cadena “017” y la conversión del número de 16 bits 1034 generará la cadena “01034”.



Eliminando los “leading zeros”

Si no queremos imprimir los ceros que hay al principio de una cadena apuntada por HL, podemos utilizar la siguiente rutina que incrementa HL mientras encuentre ceros ASCII al principio de la cadena, y sale de la rutina cuando encuentra el fin de cadena o un carácter distinto de '0'.

;-----------------------------------------------------------------
; Incrementar HL para saltar los 0's a principio de cadena
; (utilizar tras llamada a Int2String_8 o Int2String_16).
;-----------------------------------------------------------------
INC_HL_Remove_Leading_Zeros:
    ld a, (hl)                ; Leemos caracter de la cadena
    or a
    ret z                     ; Fin de cadena -> volver
    cp '0'
    ret nz                    ; Distinto de '0', volver
    inc hl                    ; '0', incrementar HL y repetir
    jr INC_HL_Remove_Leading_Zeros

La rutina de conversión, en conjunción con la subrutina para eliminar los leading zeros se utilizaría de la siguiente forma:

    ;;; Imprimir variable de 8 bits (podría ser un registro)
    ld a, (variable_8bits)
    ld l, a
    call Int2String_8
    ld hl, conv2string
    call INC_HL_Remove_Leading_Zeros
    call PrintString_8x8
 
    ;;; Imprimir variable de 16 bits
    ld bc, (variable_16bits)
    push bc
    pop hl                               ; HL = BC
    call Int2String_16
    ld hl, conv2string
    call INC_HL_Remove_Leading_Zeros
    call PrintString_8x8



Justificando los “leading zeros”

Finalmente, el usuario climacus en los foros de Speccy.org nos ofrece la siguiente variación de INC_HL_Remove_Leading_Zeros para que la rutina imprima espacios en lugar de “leading zeros”, lo que provoca que el texto en pantalla esté justificado a la derecha en ocupando siempre 3 (valores de 8 bits) ó 5 (valores de 16 bits) caracteres. Esto permite que los valores impresos puedan sobreescribir en pantalla valores anteriores aunque estemos imprimiendo un valor “menor” que el que reside en pantalla:

INC_HL_Justify_Leading_Zeros:
    ld a, (hl)                ; Leemos caracter de la cadena
    or a
    ret z                     ; Fin de cadena -> volver
    cp '0'
    ret nz                    ; Distinto de '0', volver
    inc hl                    ; '0', incrementar HL y repetir
    ld a, ' '
    call Font_Blank           ; Imprimimos espacio y avanzamos
    jr INC_HL_Justify_Leading_Zeros

De esta forma, podemos tener en pantalla un valor “12345” que se vea sobreescrito por un valor “100” al imprimir ” 100“ (con 2 espacios delante).

También podemos sustituir call Font_Blank por un simple call Font_Inc_X para que se realice el avance del cursor horizontalmente pero sin la impresión del carácter espacio.



Int2String_8_2Digits: Valores enteros de 2 dígitos

En el caso de que necesitemos imprimir un valor numérico de 2 digítos (00-99), incluyendo el posible cero inicial (en valores 1-9) o los dos ceros para el valor 00, podemos utilizar la siguiente rutina:

;-----------------------------------------------------------------------
; Int2String_8_2Digits: Convierte el valor del registro A en una
; cadena de texto de max. 2 caracteres (0-99) decimales en DE.
; IN:   A = Numero a convertir
; OUT:  DE = 2 bytes con los ASCIIs
; Basado en rutina dtoa2d de:
; http://99-bottles-of-beer.net/language-assembler-%28z80%29-813.html
;-----------------------------------------------------------------------
Int2String_8_2Digits:
    ld d, '0'                      ; Empezar en ASCII '0'
    dec d                          ; Decrementar porque el bucle hace INC
    ld e, 10                       ; Base 10
    and a                          ; Carry Flag = 0
 
dtoa2dloop:
    inc d                          ; Incrementar numero de decenas
    sub e                          ; Quitar una unidad de decenas de A
    jr nc, dtoa2dloop              ; Si A todavia no es negativo, seguir
    add a, e                       ; Decrementado demasiado, volver atras
    add a, '0'                     ; Convertir a ASCII
    ld e, a                        ; E contiene las unidades
    ret

Este formato resulta especialmente útil para la impresión de variables temporales (horas, minutos, segundos), o datos con valores máximos limitados como vidas (0-99) o niveles (0-99).

La forma de imprimir un valor con esta rutina sería el siguiente:

    ld a, (vidas)
    call Int2String_8_2Digits
 
    ;; Imprimir parte alta (decenas)
    ld a, 0
    ld (FONT_X), a
    ld a, d
    call PrintChar_8x8
 
    ;;; Imprimir parte baja (unidades)
    ld a, 1
    ld (FONT_X), a
    ld a, e
    call PrintChar_8x8

En un posterior apartado de este mismo capítulo veremos cómo integrar estas funciones en las rutinas de impresión de cadenas con formato.



En el Spectrum disponemos de un tipo de letra estándar de 8×8 pregrabado en ROM. Los caracteres imprimibles alojados en la ROM del Spectrum van desde el 32 (espacio) al 127 (carácter de copyright), empezando el primero en $3d00 (15161 decimal) y acabando el último en $3fff (16383, el último byte de la ROM).

Existe una variable del sistema llamada CHARS (de 16 bits, ubicada en las direcciones 23606 y 23607) que contiene la dirección de memoria del juego de caracteres que esté en uso por BASIC.

Por defecto, CHARS contiene el valor $3d00 (la fuente de letras estándar) menos 256. El hecho de restar 256 al inicio real de la fuente es porque los caracteres definidos en la ROM empiezan en el 32 y restando 256 (8 bytes por carácter para 32 caracteres = 256 bytes), al igual que hicimos nosotros con nuestro charset personalizado, podemos hacer coincidir un ASCII > 32 con CHARS + (8 * VALOR_ASCII).

El valor por defecto de CHARS es, pues, $3d00 - $0100 = $3c00.

El juego de caracteres estándar es inmutable al estar en ROM. La variable CHARS permite, en el BASIC del Spectrum, definir un juego de caracteres personalizado en RAM y apuntar CHARS a su dirección en memoria. La definición de los 21 UDGs (19 en el +2A/+3) también está en RAM (desde $ff58 a $ffff), ya que deben de ser personalizables por el usuario.

Veamos el aspecto de la tipográfia estándar del Spectrum:


 El juego de caracteres de la ROM

El juego de caracteres estándar de la ROM


El formato en memoria de la fuente de la ROM es idéntico a un spriteset de 8×8 sin atributos, tal y como hemos definido las fuentes de texto personalizadas de nuestras rutinas y ejemplos anteriores. A partir de $3d00 empiezan los 8 bytes de datos (8 scanlines) del carácter 32, a los que siguen los 8 scanlines del carácter 33, etc., así hasta el carácter 127.

Gracias a esto podemos utilizar esta tipografía en nuestros juegos y programas (ahorrando así tener que definir nuestro propio charset y ocupar memoria con él) directamente con las rutinas de impresión de caracteres y cadenas que hemos utilizado con las fuentes de texto personalizables. Basta con establecer FONT_CHARSET a la dirección adecuada, $3c00:

    ld hl, 15616-256           ; Saltamos los 32 caracteres iniciales
    ld (FONT_CHARSET), hl      ; Ya podemos utilizar la tipografia del
                               ; Spectrum con nuestras rutinas.

Esto nos permite ahorrar 768 bytes de memoria en nuestro programa (el de un charset personalizado) y disponer de un recurso para la impresión de texto con un aspecto “estándar” (al que el usuario ya está acostumbrado).



Si estamos escribiendo un programa o juego que requiera la impresión de gran cantidad de texto puede resultar imprescindible disponer de un pequeño sistema para almacenar los datos sobre la posición actual del “cursor” en pantalla, color y estilo de la fuente, etc. Asímismo, también resulta especialmente útil un set de rutinas para modificar todas estas variables fácilmente e imprimir cadenas con códigos de control directamente embebidos dentro de las mismas.

Normalmente no se usará este tipo de rutinas en un juego arcade o videoaventura, pero puede aprovecharse en programas no lúdicos y en juegos basados en texto o con gran cantidad de texto (managers deportivos, aventuras de texto, RPGs, etc).

Ya hemos visto cómo las rutinas PrintChar_8x8 y PrintString_8x8 hacen uso de las variables FONT_X, FONT_Y, FONT_CHARSET y FONT_ATTRIB. En este apartado definiremos más variables, funciones para manipularlas y nuevas funciones de impresión que hagan uso avanzado de ambas.

La sección sobre sistemas de gestión de texto se divide en:

  • Descripción de las variables globales que se usarán.
  • Rutina de impresión de caracteres con diferentes estilos.
  • Descripción de códigos de control que se usarán en las cadenas.
  • Rutina de impresión de cadenas avanzada que haga uso de los códigos de control.
  • Rutina de impresión de cadenas extendida que soporte códigos de variable.

Cuando trabajamos con texto tenemos que tener en cuenta que no se suelen requerir niveles de optimización de tiempos de ejecución como en el caso de la rutinas de impresión de sprites. En este caso, dado que el tiempo de “lectura” del usuario es significativamente mayor que el tiempo de ejecución, las rutinas deben tratar de ahorrar espacio en memoria para que el programa pueda disponer de la mayor cantidad de texto posible.


Estas son las variables que utilizaremos en nuestras rutinas:

  • FONT_X, FONT_Y : Coordenadas X e Y en baja resolución (comenzando en 0) de la posición actual para la próxima impresión de un carácter (cursor).
  • FONT_CHARSET : Apunta al spriteset de la fuente de texto (charset) a utilizar. El valor por defecto es $3d00-256 (la fuente de la ROM).
  • FONT_ATTRIB : Almacena el atributo en uso para la impresión de caracteres.
  • FONT_STYLE : Almacena un valor numérico que define el estilo de impresión a utilizar. Por defecto es 0 (estilo normal).

También utilizaremos algunas constantes que se modificarán sólo en tiempo de ensamblado:

  • FONT_SRCWIDTH y FONT_SRCHEIGHT : Definen el ancho y alto de la pantalla en caracteres. Para fuentes de 8×8, vale 32 y 24 respectivamente.

En código de programa:

FONT_CHARSET     DEFW    $3d00-256
FONT_ATTRIB      DEFB    56         ; Negro sobre gris
FONT_STYLE       DEFB    0
FONT_X           DEFB    0
FONT_Y           DEFB    0
FONT_SCRWIDTH    EQU     32
FONT_SCRHEIGHT   EQU     24

El resto de constantes (como los códigos de control) los veremos más adelante en sus correspondientes apartados.


Una vez definidas las diferentes variables, necesitamos funciones básicas para gestionarlas. Nótese que ninguna de las siguientes funciones provoca impresión de datos en pantalla, sólo altera el estado de las variables que acabamos de ver y que después utilizarán las funciones de impresión:

;-------------------------------------------------------------
FONT_CHARSET     DW    $3d00-256
FONT_ATTRIB      DB    56       ; Negro sobre gris
FONT_STYLE       DB    0
FONT_X           DB    0
FONT_Y           DB    0
FONT_SCRWIDTH    EQU   32
FONT_SCRHEIGHT   EQU   24
 
FONT_NORMAL      EQU   0
FONT_BOLD        EQU   1
FONT_UNDERSC     EQU   2
FONT_ITALIC      EQU   3
 
;-------------------------------------------------------------
; Establecer el CHARSET en USO
; Entrada :  HL = direccion del charset en memoria
;-------------------------------------------------------------
Font_Set_Charset:
    ld (FONT_CHARSET), hl
    ret
 
;-------------------------------------------------------------
; Establecer el estilo de texto en uso.
; Entrada :  A = estilo
;-------------------------------------------------------------
Font_Set_Style:
    ld (FONT_STYLE), a
    ret
 
;-------------------------------------------------------------
; Establecer la coordenada X en pantalla.
; Entrada :  A = coordenada X
;-------------------------------------------------------------
Font_Set_X:
    ld (FONT_X), a
    ret
 
;-------------------------------------------------------------
; Establecer la coordenada Y en pantalla.
; Entrada :  A = coordenada Y
;-------------------------------------------------------------
Font_Set_Y:
    ld (FONT_Y), a
    ret
 
;-------------------------------------------------------------
; Establecer la posicion X,Y del curso de fuente en pantalla.
; Entrada :  B = Coordenada Y
;            C = Coordenada X
;-------------------------------------------------------------
Font_Set_XY:
    ld (FONT_X), bc
    ret
 
;-------------------------------------------------------------
; Establecer un valor de tinta para el atributo en curso.
; Entrada :  A = Tinta (0-7)
; Modifica:  AF
;-------------------------------------------------------------
Font_Set_Ink:
    push bc                   ; Preservamos registros
    and 7                     ; Borramos bits 7-3
    ld b, a                   ; Lo guardamos en B
    ld a, (FONT_ATTRIB)       ; Cogemos el atributo actual
    and %11111000             ; Borramos el valor de INK
    or b                      ; Insertamos INK en A
    ld (FONT_ATTRIB), a       ; Guardamos el valor de INK
    pop bc
    ret
 
;-------------------------------------------------------------
; Establecer un valor de papel para el atributo en curso.
; Entrada :  A = Papel (0-7)
; Modifica:  AF
;-------------------------------------------------------------
Font_Set_Paper:
    push bc                   ; Preservamos registros
    and 7                     ; Borramos bits 7-3
    rlca                      ; A = 00000XXX -> 0000XXX0
    rlca                      ; A = 000XXX00
    rlca                      ; A = 00XXX000 <-- Valor en paper
    ld b, a                   ; Lo guardamos en B
    ld a, (FONT_ATTRIB)       ; Cogemos el atributo actual
    and %11000111             ; Borramos los datos de PAPER
    or b                      ; Insertamos PAPER en A
    ld (FONT_ATTRIB), a       ; Guardamos el valor de PaPER
    pop bc
    ret
 
;-------------------------------------------------------------
; Establecer un valor de atributo para la impresion.
; Entrada :  A = Tinta
;-------------------------------------------------------------
Font_Set_Attrib:
    ld (FONT_ATTRIB), a
    ret
 
;-------------------------------------------------------------
; Establecer un valor de brillo (1/0) en el atributo actual.
; Entrada :  A = Brillo (0-7)
; Modifica:  AF
;-------------------------------------------------------------
Font_Set_Bright:
    and 1                     ; A = solo bit 0 de A
    ld a, (FONT_ATTRIB)       ; Cargamos en A el atributo
    jr nz, fsbright_1         ; Si el bit solicitado era
    res 6, a                  ; Seteamos a 0 el bit de flash
    ld (FONT_ATTRIB), a       ; Escribimos el atributo
    ret
fsbright_1:
    set 6, a                  ; Seteamos a 1 el bit de brillo
    ld (FONT_ATTRIB), a       ; Escribimos el atributo
    ret
 
;-------------------------------------------------------------
; Establecer un valor de flash (1/0) en el atributo actual.
; Entrada :  A = Flash (1/0)
; Modifica:  AF
;-------------------------------------------------------------
Font_Set_Flash:
    and 1                     ; A = solo bit 0 de A
    ld a, (FONT_ATTRIB)       ; Cargamos en A el atributo
    jr nz, fsflash_1          ; Si el bit solicitado era
    res 7, a                  ; Seteamos a 0 el bit de flash
    ld (FONT_ATTRIB), a       ; Escribimos el atributo
    ret
fsflash_1:
    set 7, a                  ; Seteamos a 1 el bit de flash
    ld (FONT_ATTRIB), a       ; Escribimos el atributo
    ret
 
;-------------------------------------------------------------
; Imprime un espacio, sobreescribiendo la posicion actual del
; cursor e incrementando X en una unidad.
; de la pantalla (actualizando Y en consecuencia).
; Modifica:  AF
;-------------------------------------------------------------
Font_Blank:
    ld a, ' '                 ; Imprimir caracter espacio
    push bc
    push de
    push hl
    call PrintChar_8x8        ; Sobreescribir caracter
    pop hl
    pop de
    pop bc
    call Font_Inc_X           ; Incrementamos la coord X
    ret
 
;-------------------------------------------------------------
; Incrementa en 1 la coordenada X teniendo en cuenta el borde
; de la pantalla (actualizando Y en consecuencia).
; Modifica:  AF
;-------------------------------------------------------------
Font_Inc_X:
    ld a, (FONT_X)            ; Incrementamos la X
    inc a                     ; pero comprobamos si borde derecho
    cp FONT_SCRWIDTH-1        ; X > ANCHO-1?
    jr c, fincx_noedgex       ; No, se puede guardar el valor
    call Font_CRLF
    ret
 
fincx_noedgex:
    ld (FONT_X), a            ; Establecemos el valor de X
    ret
 
;-------------------------------------------------------------
; Produce un LineFeed (incrementa Y en 1). Tiene en cuenta
; las variables de altura de la pantalla.
; Modifica:  AF
;-------------------------------------------------------------
Font_LF:
    ld a, (FONT_Y)            ; Cogemos coordenada Y
    cp FONT_SCRHEIGHT-1       ; Estamos en la parte inferior
    jr nc, fontlf_noedge      ; de pantalla? -> No avanzar
    inc a                     ; No estamos, avanzar
    ld (FONT_Y), a
 
fontlf_noedge:
    ret
 
;-------------------------------------------------------------
; Produce un Retorno de Carro (Carriage Return) -> X=0.
; Modifica:  AF
;-------------------------------------------------------------
Font_CR:
    xor a
    ld (FONT_X), a
    ret
 
;-------------------------------------------------------------
; Provoca un LF y un CR en ese orden.
; Modifica:  AF
;-------------------------------------------------------------
Font_CRLF:
    call Font_LF
    call Font_CR
    ret
 
;-------------------------------------------------------------
; Imprime un tabulador (3 espacios) mediante PrintString.
; Modifica:  AF
;-------------------------------------------------------------
Font_Tab:
    push bc
    push de
    push hl
    ld hl, font_tab_string
    call PrintString_8x8      ; Imprimimos 3 espacios
    pop hl
    pop de
    pop bc
    ret
 
font_tab_string  DB  "   ", 0
 
;-------------------------------------------------------------
; Decrementa la coordenada X, simultando un backspace.
; No realiza el borrado en si.
; Modifica:  AF
;-------------------------------------------------------------
Font_Dec_X:
    ld a, (FONT_X)            ; Cargamos la coordenada X
    or a
    ret z                     ; Es cero? no se hace nada (salir)
    dec a                     ; No es cero? Decrementar
    ld (FONT_X), a
    ret                       ; Salir
 
;-------------------------------------------------------------
; Decrementa la coordenada X, simultando un backspace.
; Realiza el borrado imprimiendo un espacio con LD.
; Modifica:  AF
;-------------------------------------------------------------
Font_Backspace:
    call Font_Dec_X
    ld a, ' '                 ; Imprimir caracter espacio
    push bc
    push de
    push hl
    call PrintChar_8x8        ; Sobreescribir caracter
    pop hl
    pop de
    pop bc
    ret                       ; Salir

Podemos llamar a estas funciones desde nuestro programa en lugar de incluir una y otra vez el código necesario para efectuar una acción:

    ld hl, micharset
    call Font_Set_Charset
 
    ld a, 1+(7*8)
    Call Font_Set_Attrib
 
    ld a, FONT_NORMAL
    call Font_Set_Style
    (...)

Nótese que hemos creado funciones del tipo Font_Set_X o Font_Set_Y que simplemente modifican valores en nuestras variables por temas ilustrativos, ya que lo normal sería en nuestro ejemplo escribir directamente en las variables en lugar de hacer un call.



Aunque ya hemos visto una rutina PrintChar_8x8 para impresión de caracteres, vamos a implementar a continuación una nueva versión de la misma con la posibilidad de utilizar diferente estilos de fuente a partir de la fuente original.

Mediante un único juego de caracteres podemos simular estilos de texto a través de código, manipulando “al vuelo” los datos del charset antes de imprimirlos. Esto nos evita la necesidad de tener múltiples charsets de texto para distintos estilos con la consiguiente ocupación de espacio en memoria.

Los estilos básicos que podemos conseguir al vuelo son normal, negrita, cursiva y subrayado.

Las rutinas de impresión para los 4 estilos esencialmente iguales salvo por el bucle de impresión, por lo que vamos a utilizar una variable global llamada FONT_STYLE para indicar el estilo actual en uso, y modificaremos la rutina PrintChar_8x8 para que haga uso del valor del estilo y modifique el bucle de impresión en consecuencia.

;--- Variables de fuente --------------------
FONT_CHARSET     DW    $3d00-256
FONT_ATTRIB      DB    56       ; Negro sobre gris
FONT_STYLE       DB    0
FONT_X           DB    0
FONT_Y           DB    0

;--- Constantes predefinidas ----------------
FONT_NORMAL      EQU   0
FONT_BOLD        EQU   1
FONT_UNDERSC     EQU   2
FONT_ITALIC      EQU   3


;--- Nueva funcion PrintChar_8x8 ------------
PrintChar_8x8:
   ;;; Calcular coordenadas destino en Pantalla en DE
   ;;; Calcular posicion origen (array sprites) en HL
   ;;; Obtener el valor de FONT_STYLE
       ;;; Si es == FONT_NORMAL  -> Bucle_Impresion_Normal
       ;;; Si es == FONT_BOLD    -> Bucle_Impresion_Negrita
       ;;; Si es == FONT_UNDERSC -> Bucle_Impresion_Subrayado
       ;;; Si es == FONT_ITALIC  -> Bucle_Impresion_Cursiva
   ;;; Calcular posicion destino atributo.
   ;;; Imprimir Atributo.

El esqueleto de la función PrintChar_8x8, a falta de introducir los bucles de impresión, ya lo conocemos:

;-------------------------------------------------------------
; PrintChar_8x8:
; Imprime un caracter de 8x8 pixeles de un charset usando
; el estilo especificado en FONT_STYLE.
;-------------------------------------------------------------
PrintChar_8x8:
 
    ld bc, (FONT_X)      ; B = Y,  C = X
    ex af, af'           ; Nos guardamos el caracter en A'
 
    ;;; Calculamos las coordenadas destino de pantalla en DE:
    ld a, b
    and $18
    add a, $40
    ld d, a
    ld a, b
    and 7
    rrca
    rrca
    rrca
    add a, c
    ld e, a              ; DE contiene ahora la direccion destino.
 
    ;;; Calcular posicion origen (array sprites) en HL como:
    ;;;     direccion = base_sprites + (NUM_SPRITE*8)
    ex af, af'           ; Recuperamos el caracter a dibujar de A'
    ld bc, (FONT_CHARSET)
    ld h, 0
    ld l, a
    add hl, hl
    add hl, hl
    add hl, hl
    add hl, bc         ; HL = BC + HL = FONT_CHARSET + (A * 8)
 
    ex de, hl          ; Intercambiamos DE y HL (DE=origen, HL=destino)
 
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;;; INSERTAR AQUI BUCLES DE IMPRESION SEGUN ESTILO
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
    ;;; (...)
 
    ;;; Impresion del caracter finalizada
 
    ;;; Impresion de atributos
    ld a, h            ; Recuperamos el valor inicial de HL
    sub 8              ; Restando los 8 scanlines avanzados
 
    ;;; Calcular posicion destino en area de atributos en DE.
                        ; A = H
    rrca               ; Codigo de Get_Attr_Offset_From_Image
    rrca
    rrca
    and 3
    or $58
    ld d, a
    ld e, l
 
    ;;; Escribir el atributo en memoria
    ld a, (FONT_ATTRIB)
    ld (de), a         ; Escribimos el atributo en memoria
    ret

La manera de obtener los diferentes estilos es la siguiente:



Estilo normal: No se realiza ningún tipo de modificación sobre los datos del carácter: se imprimen tal cual se obtienen del spriteset en un bucle de 8 iteraciones:

    ;;;;;; Estilo NORMAL ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ld b, 8                      ; 8 scanlines a dibujar
drawchar_loop_normal:
    ld a, (de)                   ; Tomamos el dato del caracter
    ld (hl), a                   ; Establecemos el valor en videomemoria
    inc de
    inc h
    djnz drawchar_loop_normal
    jr pchar8_printattr          ; Impresion de atributos



Estilo negrita (bold): Para simular el efecto de la negrita necesitamos aumentar el grosor del carácter. Para eso, leemos cada scanline y realizamos un OR de dicho scanline con una copia del mismo desplazada hacia la derecha o hacia la izquierda.

    ;;;;;; Estilo NEGRITA ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ld b, 8                      ; 8 scanlines a dibujar
drawchar_loop_bold:
    ld a, (de)                   ; Tomamos el dato del caracter
    ld c, a                      ; Creamos copia de A
    rrca                         ; Desplazamos A
    or c                         ; Y agregamos C
    ld (hl), a                   ; Establecemos el valor en videomemoria
    inc de
    inc h
    djnz drawchar_loop_bold
    jr pchar8_printattr          ; Impresion de atributos



Estilo Subrayado (underscore): Basta con dibujar los 7 primeros scanlines del carácter correctamente, y trazar como octavo scanline un valor 255 (8 píxeles a 1, una línea horizontal). De esta forma el último scanline se convierte en el subrayado.

    ;;;;;; Estilo SUBRAYADO ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ld b, 7                      ; 7 scanlines a dibujar normales
drawchar_loop_undersc:
    ld a, (de)                   ; Tomamos el dato del caracter
    ld (hl), a                   ; Establecemos el valor en videomemoria
    inc de
    inc h
    djnz drawchar_loop_undersc
 
    ;;; El octavo scanline, una linea de subrayado
    ld a, 255                    ; Ultima linea = subrayado
    ld (hl), a
    inc h                        ; Necesario para el sub a, 8
    jr pchar8_printattr          ; Impresion de atributos



Estilo Cursiva (italic): Finalmente, el estilo de letra cursiva implica imprimir los 3 primeros scanlines desplazados hacia la derecha, los 2 centrales sin modificación y los 3 últimos desplazados hacia la izquierda:

    ;;;;;; Estilo ITALICA ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;;; 3 primeros scanlines, a la derecha,
    ld b, 3
drawchar_loop_italic1:
    ld a, (de)         ; Tomamos el dato del caracter
    sra a              ; Desplazamos A a la derecha
    ld (hl), a         ; Establecemos el valor en videomemoria
    inc de
    inc h
    djnz drawchar_loop_italic1
 
    ;;; 2 siguientes scanlines, sin tocar
    ld b, 2
 
drawchar_loop_italic2:
    ld a, (de)         ; Tomamos el dato del caracter
    ld (hl), a         ; Establecemos el valor en videomemoria
    inc de
    inc h
    djnz drawchar_loop_italic2
 
    ld b, 3
drawchar_loop_italic3:
    ;;; 3 ultimos scanlines, a la izquierda,
    ld a, (de)         ; Tomamos el dato del caracter
    sla a              ; Desplazamos A
    ld (hl), a         ; Establecemos el valor en videomemoria
    inc de
    inc h
    djnz drawchar_loop_italic3
    jr pchar8_printattr

El código completo y definitivo de la función de impresión de caracteres con estilo es el siguiente:

;-------------------------------------------------------------
; PrintChar_8x8:
; Imprime un caracter de 8x8 pixeles de un charset usando
; el estilo especificado en FONT_STYLE
;
; Entrada (paso por parametros en memoria):
; -----------------------------------------------------
; FONT_CHARSET = Direccion de memoria del charset.
; FONT_X       = Coordenada X en baja resolucion (0-31)
; FONT_Y       = Coordenada Y en baja resolucion (0-23)
; FONT_ATTRIB  = Atributo a utilizar en la impresion.
; FONT_STYLE   = Estilo a utilizar (0-N).
; Registro A   = ASCII del caracter a dibujar.
;-------------------------------------------------------------
PrintChar_8x8:
 
    ld bc, (FONT_X)      ; B = Y,  C = X
    ex af, af'           ; Nos guardamos el caracter en A'
 
    ;;; Calculamos las coordenadas destino de pantalla en DE:
    ld a, b
    and $18
    add a, $40
    ld d, a
    ld a, b
    and 7
    rrca
    rrca
    rrca
    add a, c
    ld e, a              ; DE contiene ahora la direccion destino.
 
    ;;; Calcular posicion origen (array sprites) en HL como:
    ;;;     direccion = base_sprites + (NUM_SPRITE*8)
    ex af, af'           ; Recuperamos el caracter a dibujar de A'
    ld bc, (FONT_CHARSET)
    ld h, 0
    ld l, a
    add hl, hl
    add hl, hl
    add hl, hl
    add hl, bc         ; HL = BC + HL = FONT_CHARSET + (A * 8)
 
    ex de, hl          ; Intercambiamos DE y HL (DE=origen, HL=destino)
 
    ;;; NUEVO: Verificacion del estilo actual
    ld a, (FONT_STYLE)           ; Obtenemos el estilo actual
    or a
    jr nz, pchar8_estilos_on     ; Si es != cero, saltar
 
    ;;;;;; Estilo NORMAL ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ld b, 8                      ; 8 scanlines a dibujar
drawchar_loop_normal:
    ld a, (de)                   ; Tomamos el dato del caracter
    ld (hl), a                   ; Establecemos el valor en videomemoria
    inc de
    inc h
    djnz drawchar_loop_normal
    jr pchar8_printattr          ; Imprimir atributos
 
pchar8_estilos_on:
    cp FONT_BOLD                 ; ¿Es estilo NEGRITA?
    jr nz, pchar8_nobold         ; No, saltar
 
    ;;;;;; Estilo NEGRITA ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ld b, 8            ; 8 scanlines a dibujar
drawchar_loop_bold:
    ld a, (de)         ; Tomamos el dato del caracter
    ld c, a            ; Creamos copia de A
    rrca               ; Desplazamos A
    or c               ; Y agregamos C
    ld (hl), a         ; Establecemos el valor en videomemoria
    inc de             ; Incrementamos puntero en caracter
    inc h              ; Incrementamos puntero en pantalla (scanline+=1)
    djnz drawchar_loop_bold
    jr pchar8_printattr
 
pchar8_nobold:
    cp FONT_UNDERSC              ; ¿Es estilo SUBRAYADO?
    jr nz, pchar8_noundersc      ; No, saltar
 
    ;;;;;; Estilo SUBRAYADO ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ld b, 7            ; 7 scanlines a dibujar normales
drawchar_loop_undersc:
    ld a, (de)         ; Tomamos el dato del caracter
    ld (hl), a         ; Establecemos el valor en videomemoria
    inc de
    inc h
    djnz drawchar_loop_undersc
 
    ;;; El octavo scanline, una linea de subrayado
    ld a, 255          ; Ultima linea = subrayado
    ld (hl), a
    inc h              ; Necesario para el sub a, 8
    jr pchar8_printattr
 
pchar8_noundersc:
    cp FONT_ITALIC               ; ¿Es estilo ITALICA?
    jr nz, pchar8_UNKNOWN        ; No, saltar
 
    ;;;;;; Estilo ITALICA ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;;; 3 primeros scanlines, a la derecha,
    ld b, 3
drawchar_loop_italic1:
    ld a, (de)         ; Tomamos el dato del caracter
    sra a              ; Desplazamos A a la derecha
    ld (hl), a         ; Establecemos el valor en videomemoria
    inc de
    inc h
    djnz drawchar_loop_italic1
 
    ;;; 2 siguientes scanlines, sin tocar
    ld b, 2
 
drawchar_loop_italic2:
    ld a, (de)         ; Tomamos el dato del caracter
    ld (hl), a         ; Establecemos el valor en videomemoria
    inc de
    inc h
    djnz drawchar_loop_italic2
 
    ld b, 3
drawchar_loop_italic3:
    ;;; 3 ultimos scanlines, a la izquierda,
    ld a, (de)         ; Tomamos el dato del caracter
    sla a              ; Desplazamos A
    ld (hl), a         ; Establecemos el valor en videomemoria
    inc de
    inc h
    djnz drawchar_loop_italic3
    jr pchar8_printattr
 
pchar8_UNKNOWN:                 ; Estilo desconocido...
    ld b, 8                      ; Lo imprimimos con el normal
    jr drawchar_loop_normal      ; (estilo por defecto)
 
    ;;; Impresion de los atributos
pchar8_printattr:
 
    ld a, h            ; Recuperamos el valor inicial de HL
    sub 8              ; Restando los 8 scanlines avanzados
 
    ;;; Calcular posicion destino en area de atributos en DE.
                        ; A = H
    rrca               ; Codigo de Get_Attr_Offset_From_Image
    rrca
    rrca
    and 3
    or $58
    ld d, a
    ld e, l
 
    ;;; Escribir el atributo en memoria
    ld a, (FONT_ATTRIB)
    ld (de), a         ; Escribimos el atributo en memoria
    ret

Si definimos esta función PrintChar_8x8 en nuestro programa, la función PrintString_8x8 hará uso de ella y podremos imprimir cadenas en diferentes estilos, como en el siguiente ejemplo:

; Ejemplo de estilos de fuente
    ORG 35000
 
    ld hl, $3d00-256        ; Saltamos los 32 caracteres iniciales
    ld (FONT_CHARSET), hl
    ld a, 1+(7*8)
    ld (FONT_ATTRIB), a
 
    ;;; Probamos los diferentes estilos: NORMAL
    ld a, FONT_NORMAL
    ld (FONT_STYLE), a
    ld hl, cadena1
    ld a, 4
    ld (FONT_Y), a
    xor a
    ld (FONT_X), a
    call PrintString_8x8
 
    ;;; Probamos los diferentes estilos: NEGRITA
    ld a, FONT_BOLD
    ld (FONT_STYLE), a
    ld hl, cadena2
    ld a, 6
    ld (FONT_Y), a
    xor a
    ld (FONT_X), a
    call PrintString_8x8
 
    ;;; Probamos los diferentes estilos: CURSIVA
    ld a, FONT_ITALIC
    ld (FONT_STYLE), a
    ld hl, cadena3
    ld a, 8
    ld (FONT_Y), a
    xor a
    ld (FONT_X), a
    call PrintString_8x8
 
    ;;; Probamos los diferentes estilos: SUBRAYADO
    ld a, FONT_UNDERSC
    ld (FONT_STYLE), a
    ld hl, cadena4
    ld a, 10
    ld (FONT_Y), a
    xor a
    ld (FONT_X), a
    call PrintString_8x8
 
loop:
    jr loop
 
    ret
 
cadena1 DB "IMPRESION CON ESTILO NORMAL.", 0
cadena2 DB "IMPRESION CON ESTILO NEGRITA.", 0
cadena3 DB "IMPRESION CON ESTILO CURSIVA.", 0
cadena4 DB "IMPRESION CON ESTILO SUBRAYADO.", 0
 
;-------------------------------------------------------------
FONT_CHARSET     DW    $3d00-256
FONT_ATTRIB      DB    56       ; Negro sobre gris
FONT_STYLE       DB    0
FONT_X           DB    0
FONT_Y           DB    0
FONT_NORMAL      EQU   0
FONT_BOLD        EQU   1
FONT_UNDERSC     EQU   2
FONT_ITALIC      EQU   3
FONT_SCRWIDTH    EQU   32
FONT_SCRHEIGHT   EQU   24
 
    END 35000

El anterior ejemplo (que usa el juego de caracteres estándar de la ROM) produce el siguiente resultado en pantalla:


 Estilos de texto con PrintChar_8x8

La rutina de impresión PrintChar_8x8 es ahora ligeramente más lenta que la original, pero a cambio nos permite diferentes estilos de texto. Para la impresión de texto con estilo normal, sólo le hemos añadido el siguiente código adicional a ejecutar:

    ld a, (FONT_STYLE)
    or a
    jr nz, pchar8_estilos_on      ; Aqui no se produce salto
 
    +
 
    jr pchar8_printattr           ; Este salto si se produce

Son 13 (LD) + 4 (OR) + 7 (jr NZ sin salto) + 12 (jr) = 36 t-estados adicionales por carácter en estilo normal a cambio de la posibilidad de disponer de 4 estilos de texto diferentes para cualquier charset, incluído el de la ROM.

En la rutina se han utilizado operaciones de transferencia LD para imprimir los caracteres, lo que implica que no se respeta el fondo sobre el que se imprime, y se ponen a cero en pantalla todos los píxeles a cero en el charset. Este suele ser el sistema de impresión habitual puesto que el texto, para hacerlo legible, suele imprimirse sobre áreas en blanco de la pantalla, y un caracter impreso sobre otro debe sobreescribir totalmente al primero.

No obstante, la rutina PrintChar_8x8 puede ser modificada por el lector, para utilizar operaciones OR en la transferencia a pantalla y por tanto respetar el contenido de pantalla al imprimir el carácter.


Llegados a este punto, tenemos:


  • Un sistema de variables que controlan los datos sobre la impresión: posición, estilo, color, fuente de texto, etc.
  • Un set de funciones que permite modificar estas variables fácilmente.
  • Una función de impresión de caracteres 8×8 que utiliza estas variables para imprimir el carácter indicado en la posición adecuada, con el estilo seleccionado y con el color y fondo elegidos.


El siguiente paso en la escala de la gestión del texto sería la impresión de cadenas con formato para que aproveche nuestras nuevas funciones extendidas. Para esto modificaremos la rutina PrinString_8x8 vista al principio del capítulo de forma que haga uso no sólo de FONT_X y FONT_Y sino también de funciones adicionales que especificaremos en la cadena mediante códigos de control o tokens.

Los códigos de control que vamos a definir y utilizar serán los siguientes:


Código de control Significado
0 Indica Fin de cadena
1 Cambiar estilo (seguido del byte con el estilo)
2 Cambiar posicion x (seguido de la coordenada x)
3 Cambiar posicion y (seguido de la coordenada y)
4 Cambiar color tinta (seguido del color de tinta)
5 Cambiar color de papel (seguido del color de papel)
6 Cambiar color de atributo (seguido del byte de atributo)
7 Cambiar Brillo ON / OFF (seguido de 1 ó 0)
8 Cambiar Flash ON / OFF (seguido de 1 ó 0)
10 Provocar LF (LineFeed = avance de linea) (y+=1)
11 Provocar avance de linea y retorno de carro (LF y CR) (y+=1 y x=0)
12 Avanzar 1 caracter sin imprimir nada (x+=1)
13 Provocar CR (retorno de carro o Carriage Return) (x=0)
14 Backspace: borrar el caracter de (x-1 si x>0)
imprimiendo un espacio y decrementando x.
15 Tabulador (impresion de 3 espacios por llamada a PrintString)


Así pues, declaramos las siguientes constantes predefinidas:

FONT_EOS            EQU 0      ; End of String
FONT_SET_STYLE      EQU 1
FONT_SET_X          EQU 2
FONT_SET_Y          EQU 3
FONT_SET_INK        EQU 4
FONT_SET_PAPER      EQU 5
FONT_SET_ATTRIB     EQU 6
FONT_SET_BRIGHT     EQU 7
FONT_SET_FLASH      EQU 8
FONT_XXXXXX         EQU 9       ; Libre para ampliaciones
FONT_LF             EQU 10
FONT_CRLF           EQU 11
FONT_BLANK          EQU 12
FONT_CR             EQU 13
FONT_BACKSPACE      EQU 14
FONT_TAB            EQU 15
FONT_INC_X          EQU 16      ; De la 17 a la 31 libres

Esto nos permitirá definir las cadenas como, por ejemplo:

cadena DB "Cadena de texto ", FONT_SET_INK, 2, "ROJO ", FONT_CRLF
       DB FONT_SET_INK, 1, "AHORA AZUL"
       DB FONT_SET_Y, 20, "AHORA EN LA LINEA 20", FONT_EOS

La rutina de impresión de cadenas deberá interpretar cada byte de la misma determinando:

  • Si es un código ASCII >= 32 → Imprimir el caracter en FONT_X, FONT_Y (y variar las coordenadas).
  • Si es un código de control 0 (EOS) → Fin de la rutina. Nótese que, al contrario que en caso de la rutina PrintString que nos fabricamos para la ROM en nuestra librería utils.asm, en esta rutina podemos leer los parámetros de FLASH/BRIGHT e interpretarlos nosotros, por lo que un 0 que vaya detrás de un código de control, no se contabilizará como END_OF_STRING.
  • Si es un código entre el 1 y el 9 → Recoger parámetro en cadena (siguiente byte) y llamar a la función apropiada.
  • Si es un código entre el 10 y el 31 → Llamar a la función apropiada (no hay parámetro).

Esto nos permitirá trabajar con cadenas de texto con múltiples formatos sin tener que realizar el posicionamiento, cambio de color, de papel, gestión de los retornos de carro, etc. en nuestro código, con un gran ahorro en código de manipulación gracias a nuestra nueva rutina genérica de impresión.

Llamaremos a esta rutina de impresión con formato PrintString_8x8_Format, y tendrá el siguiente pseudocódigo:

;;; HL = Cadena que imprimir

PrintString_8x8_Format:
   ;;;
bucle:
      ;;; Coger caracter apuntado por HL.
      ;;; Incrementar HL
      ;;; Si HL es mayor que 32 :
          ;;; Imprimir caracter en FONT_X,FONT_Y
          ;;; Avanzar el puntero FONT_X
      ;;; Si HL es menor que 31 :
          ;;; Si es CERO, salir de la rutina con ret z.
          ;;; Si es FONT_SET_SETSTYLE (1):
              ;;; Coger el siguiente byte de la cadena (el estilo)
              ;;; Llamar a función Font_Set_Style
          ;;; Si es FONT_SET_X (2):
              ;;; Coger el siguiente byte de la cadena (coordenada X)
              ;;; Llamar a función Font_Set_X
          ;;; Si es FONT_SET_Y (3):
              ;;; Coger el siguiente byte de la cadena (coordenada X)
              ;;; Llamar a función Font_Set_X
          ;;; (...)
          ;;; (...)
          ;;; (...)
          ;;; Si es FONT_BACKSPACE (14):
              ;;; Llamar a funcion Font_Backspace
          ;;; Si es FONT_TAB (15):
              ;;; Llamar a funcion Font_Tab
      ;;; Saltar a bucle (se saldrá con el ret z)

El pseudocódigo que acabamos de ver utiliza gran cantidad de controles de flujo condicionales para decidir a qué rutina debemos de saltar y si tenemos que recoger un parámetro de la cadena (leer valor apuntado por HL e incrementar HL) o no.

En lugar de utilizar un enorme bloque de código con gran cantidad de saltos, vamos a emplear una tabla de saltos. Para ello creamos una tabla en memoria que contenga las direcciones de salto de todos los códigos de control, excepto el cero:

;-------------------------------------------------------------
; Tabla con las direcciones de las 16 rutinas de cambio.
;-------------------------------------------------------------
FONT_CALL_JUMP_TABLE:
    DW 0000, Font_Set_Style, Font_Set_X, Font_Set_Y, Font_Set_Ink
    DW Font_Set_Paper, Font_Set_Attrib, Font_Set_Bright
    DW Font_Set_Flash, 0000, Font_LF, Font_CRLF, Font_Blank
    DW Font_CR, Font_Backspace, Font_Tab, Font_Inc_X

Ahora, suponiendo que tengamos en A el código de control, la rutina en pseudocódigo podría ser así:

PrintString_8x8_Format:
bucle:
      ;;; Coger caracter apuntado por HL.
      ;;; Incrementar HL
      ;;; Si HL es mayor que 32 :
          ;;; Imprimir caracter en FONT_X,FONT_Y
          ;;; Avanzar el puntero FONT_X
      ;;; Si HL es menor que 31 :
          ;;; Si es CERO, salir de la rutina con ret z.
          ;;; Calculamos DIR_SALTO = TABLA_SALTOS [ COD_CONTROL ]
          ;;; Como la tabla es de 2 bytes -> DIR_SALTO = TABLA_SALTOS + COD_CONTROL*2
          ;;; Si es menor que 10, requiere recoger parametro
              ;;; Recoger parametro
          ;;; Si es mayor que 10, no requiere recoger parametro
          ;;; Saltar a la dirección DIR_SALTO
      ;;; Saltar a bucle (se saldrá con el ret z)

Veamos el código de la rutina definitiva:

;-------------------------------------------------------------
; Tabla con las direcciones de las 16 rutinas de cambio.
; Notese que la 9 queda libre para una posible ampliacion.
;-------------------------------------------------------------
FONT_CALL_JUMP_TABLE:
    DW 0000, Font_Set_Style, Font_Set_X, Font_Set_Y, Font_Set_Ink
    DW Font_Set_Paper, Font_Set_Attrib, Font_Set_Bright
    DW Font_Set_Flash, 0000, Font_LF, Font_CRLF, Font_Blank
    DW Font_CR, Font_Backspace, Font_Tab, Font_Inc_X
 
 
;-------------------------------------------------------------
; PrintString_8x8_Format:
; Imprime una cadena de texto de un charset de fuente 8x8.
;
; Entrada (paso por parametros en memoria):
; -----------------------------------------------------
; FONT_CHARSET = Direccion de memoria del charset.
; FONT_X       = Coordenada X en baja resolucion (0-31)
; FONT_Y       = Coordenada Y en baja resolucion (0-23)
; FONT_ATTRIB  = Atributo a utilizar en la impresion.
; Registro HL  = Puntero a la cadena de texto a imprimir.
;                Debe acabar en un cero.
; Usa: DE, BC
;-------------------------------------------------------------
PrintString_8x8_Format:
 
    ;;; Bucle de impresion de caracter
pstring8_loop:
    ld a, (hl)                ; Leemos un caracter de la cadena
    inc hl                    ; Apuntamos al siguiente caracter
 
    cp 32                     ; Es menor que 32?
    jp c,  pstring8_ccontrol  ; Si, es un codigo de control, saltar
 
    push hl                   ; Salvaguardamos HL
    call PrintChar_8x8        ; Imprimimos el caracter
    pop hl                    ; Recuperamos HL
 
    ;;; Avanzamos el cursor usando Font_Blank, que incrementa X
    ;;; y actualiza X e Y si se llega al borde de la pantalla
    call Font_Inc_X           ; Avanzar coordenada X
    jr pstring8_loop          ; Continuar impresion hasta CHAR=0
 
pstring8_ccontrol:
    or a                      ; A es cero?
    ret z                     ; Si es 0 (fin de cadena) volver
 
    ;;; Si estamos aqui es porque es un codigo de control distinto > 0
    ;;; Ahora debemos calcular la direccion de la rutina que lo atendera.
 
    ;;; Calculamos la direccion destino a la que saltar usando
    ;;; la tabla de saltos y el codigo de control como indice
    ex de, hl
    ld hl, FONT_CALL_JUMP_TABLE
    rlca                      ; A = A * 2 = codigo de control * 2
    ld c, a
    ld b, 0                   ; BC = A*2
    add hl, bc                ; HL = DIR FONT_CALL_JUMP_TABLE+(CodControl*2)
    ld c, (hl)                ; Leemos la parte baja de la direccion en C...
    inc hl                    ; ... para no corromper HL y poder leer ...
    ld h, (hl)                ; ... la parte alta sobre H ...
    ld l, c                   ; No hemos usado A porque se usa en el CP
 
    ;;; Si CCONTROL>0 y CCONTROL<10 -> recoger parametro y saltar a rutina
    ;;; Si CCONTROL>9 y CCONTROL<32 -> saltar a rutina sin recogida
    cp 18                     ; Comprobamos si (CCONTROL-1)*2 < 18
    jp nc, pstring8_noparam   ; Es decir, si CCONTROL > 9, no hay param
 
    ;;; Si CCONTROL < 10 -> recoger parametro:
    ld a, (de)                ; Cogemos el parametro en cuestion de la cadena
    inc de                    ; Apuntamos al siguiente caracter
 
    ;;; Realizamos el salto a la rutina con o sin parametro recogido
pstring8_noparam:
    ld bc, pstring8_retaddr   ; Ponemos en BC la dir de retorno
    push bc                   ; Hacemos un push de la dir de retorno
    jp (hl)                   ; Saltamos a la rutina seleccionada
 
    ;;; Este es el punto al que volvemos tras la rutina
pstring8_retaddr:
    ex de, hl                 ; Recuperamos en HL el puntero a cadena
    jr pstring8_loop          ; Continuamos en el bucle

El esqueleto de la rutina y la parte de impresión ya la conocemos, porque es idéntica a PrintString_8x8. El principal añadido es la interpretación de los códigos de control, donde la parte más interesante es la construcción y uso de la tabla de saltos:

Una vez ubicadas todas las diferentes direcciones de las rutinas en FONT_CALL_JUMP_TABLE, podemos utilizar el valor del registro A para direccionar la tabla. Para ello debemos multiplicar A por 2 ya que cada dirección consta de 2 bytes. Cargando A*2 en BC podemos calcular la dirección destino en la tabla como HL+BC (BASE+DESPLAZAMIENTO = BASE+COD_CONTROL*2). Leyendo el valor apuntado por HL obtenemos la dirección de la tabla, es decir, la dirección de la rutina que puede interpretar el código de control que hemos recibido.

    ;;; Calculamos la direccion destino a la que saltar usando
    ;;; la tabla de saltos y el codigo de control como indice
    ex de, hl
    ld hl, FONT_CALL_JUMP_TABLE
    rlca                      ; A = A * 2 = codigo de control * 2
    ld c, a
    ld b, 0                   ; BC = A*2
    add hl, bc                ; HL = DIR FONT_CALL_JUMP_TABLE+(CodControl*2)
    ld c, (hl)                ; Leemos la parte baja de la direccion en C...
    inc hl                    ; ... para no corromper HL y poder leer ...
    ld h, (hl)                ; ... la parte alta sobre H ...
    ld l, c                   ; No hemos usado A porque se usa en el CP
                              ; HL = FONT_CALL_JUMP_TABLE+(CodControl*2)
 
    ; (...)                   ; Codigo de recogida de parametro si procede
 
    ld bc, pstring8_retaddr   ; Ponemos en BC la dir de retorno
    push bc                   ; Hacemos un push de la dir de retorno
    jp (hl)                   ; Saltamos a la rutina seleccionada
 
    ;;; Este es el punto al que volvemos tras la rutina
pstring8_retaddr:

Con el anterior cálculo, por ejemplo, si recibimos un código de control 6, se pondrá en HL la dirección de memoria contenida en FONT_CALL_JUMP_TABLE+(6*2), que es el valor Font_Set_Attrib, que el ensamblador sustituirá en la tabla durante el proceso de ensamblado por la dirección de memoria de dicha rutina.

Nótese cómo después de calcular el valor de salto correcto para HL tenemos que simular un call HL que no forma parte del juego de instrucciones del Spectrum. ¿Cómo realizamos esto? Utilizando la pila y la instrucción jp. Recordemos que un call es un salto a subrutina, lo cual implica introducir en la pila la dirección de retorno y salta a la dirección de la rutina. Cuando la rutina realice el ret, se extrae de la pila la dirección de retorno para continuar el flujo del programa.

En el código anterior introducimos en el registro BC la dirección de la etiqueta pstring8_retaddr, que es la posición exacta de memoria después del salto. Una vez introducida en la pila la dirección de retorno, saltamos con el salto incondicional jp (hl) a la rutina especificada por el código de control. La subrutina efectuará la tarea correspondiente y volverá con un ret, provocando que la rutina de impresión de cadenas continúe en pstring8_retaddr, que es la dirección que el RET extraerá de la pila para volver.

Hemos hecho distinción de 2 tipos de subrutinas de control, ya que las 9 primeras requieren recoger un parámetro de la cadena (apuntado por HL) y las restantes no. El cálculo de la dirección de salto es igual en todos los casos pero para las 9 primeras es necesario obtener el dato adicional al código de control en el registro A antes de saltar. El registro A es el parámetro común en todas las subrutinas de gestión de códigos de control que requieren parámetros, algo necesario para poder usar las rutinas vía tabla de saltos.

La comprobación de si debemos recoger o no parámetro desde la cadena la realizamos con el siguiente código:

    ;;; Si CCONTROL>0 y CCONTROL<10 -> recoger parametro y saltar a rutina
    ;;; Si CCONTROL>9 y CCONTROL<32 -> saltar a rutina sin recogida
    cp 18                     ; Comprobamos si (CCONTROL-1)*2 < 18
    jp nc, pstring8_noparam   ; Es decir, si CCONTROL > 9, no hay param

En lugar de volver a dividir el código de control entre 2 (recordemos que se multiplicó por 2 para el cálculo de la dirección de salto) y comprobar si es > 9, podemos comprobar directamente si es > 9*2 = 18.

Tras interpretar el código de control, bastará con volver a saltar al principio de la rutina para continuar con el siguiente carácter. Todo el proceso se repetirá hasta recibir en A un código de control 0 (FONT_EOS, de FONT_END_OF_STRING).

Una vez explicada la rutina, veamos un ejemplo de cómo podríamos utilizarla en nuestros programas:

; Ejemplo de gestion de texto
    ORG 35000
 
    ld hl, $3d00-256        ; Saltamos los 32 caracteres iniciales
    call Font_Set_Charset
 
    ld a, 1+(7*8)
    Call Font_Set_Attrib
 
    ;;; Probamos los diferentes estilos: NORMAL
    ld a, FONT_NORMAL
    call Font_Set_Style
    ld hl, cadena1
    ld b, 4
    ld c, 0
    call Font_Set_XY
    call PrintString_8x8_Format
 
loop:
    jr loop
 
cadena1 DB "SALTO DE", FONT_LF, "LINEA ", FONT_SET_X, 4, FONT_SET_Y, 9
        DB "IR A (4,9) ", FONT_SET_INK, 2, "ROJO "
        DB FONT_SET_INK, 0, "NEGRO", FONT_CRLF, FONT_LF
        DB "CRLF+LF ", FONT_SET_STYLE, FONT_UNDERSC, "ESTILO SUBRAYADO"
        DB FONT_CRLF, FONT_CRLF, FONT_SET_STYLE, FONT_BOLD, "NEGRITA"
        DB FONT_CRLF, FONT_CRLF, FONT_SET_STYLE, FONT_ITALIC, "CURSIVA"
        DB FONT_CRLF, FONT_CRLF, FONT_TAB, FONT_SET_PAPER, 0
        DB FONT_SET_INK, 6, "TABULADOR + INK 6 PAPER 0"
        DB FONT_EOS
 
    END 35000

La salida en pantalla del anterior ejemplo (añadiendo las funciones correspondientes al código):


 Impresion de cadenas con codigos de control

Es importante destacar que podríamos ampliar la rutina de impresión con más códigos de control y funciones. Con la configuración que hemos visto, el código de control 9 queda libre para la introducción de una función adicional que requiera parámetro, y del 17 al 31 podemos añadir más funciones de formato que no requieran parámetros (por ejemplo, combinaciones de color y estilos concretos en una función que cambie BRIGHT, FLASH, INK, PAPER y STYLE, o incluso cambios entre diferentes charsets).

Si necesitaremos más “espacio” para rutinas con parámetro, podríamos “reubicar” los códigos de control por encima del 10 (cambiando los EQUs) y modificando el CP de la rutina que determinar si el control-code tiene parámetro o no.

Recomendamos al lector que utilice siempre en sus cadenas los códigos de control mediante las constantes EQU en lugar de utilizar los códigos numéricos en sí mismos. Esto permite reubicar los valores numéricos (los EQUs) sin modificar las cadenas. Recordemos que el ensamblador sustituirá las constantes por sus valores numéricos durante el proceso de ensamblado, por lo que la ocupación en las cadenas definitivas no será “mayor” al usar las constantes. El único código de control que no debe reubicarse nunca es FONT_EOS (0).

Finalmente, creemos importante indicar al lector que para marcar claramente la dirección de salto del código de control 9 (que no está en uso) se ha usado la cadena “0000”, pero probablemente sería más seguro el colocar la dirección de una rutina como FONT_TAB o FONT_CRLF. De esta forma, ante un error del programador al escribir una cadena y utilizar el inexistente código 9 en ella, evitaremos que se produzca un reset (jp $0000) que nos cueste gran cantidad de horas de encontrar / depurar.

En cuanto a las diferencias en tiempo de ejecución de PrintString_8x8_Format vs PrintString_8x8, cabe destacar que el coste adicional de la rutina para la impresión del texto normal (ASCIIs < 32) se reduce a las siguientes 2 instrucciones adicionales:

    cp 32                     ; Es menor que 32?
    jp c,  pstring8_ccontrol  ; Si, es un codigo de control, saltar

Aparte de eso, se ha sustituído el código de avance de la coordenada X por el de las rutinas genéricas vistas anteriormente, lo que añade un call Font_Inc_X (y su RET) adicional. Así pues, el coste en tiempo de ejecución no difiere apenas de la función sin códigos de control.

En el caso del código de fin de cadena (EOS = 0), ya no se sale de la rutina con un ret z sino que se pasa por el cp 32 y se realiza el salto a pstring8_ccontrol.

Sí que hay un coste real en la ocupación de memoria, puesto que todas las funciones auxiliares de control que hemos definido seguramente pueden no resultarnos útiles en la programación de un juego donde no se utilice apenas texto. Ese código adicional sumado a la gestión de códigos de control de la rutina y a la tabla de saltos puede ser espacio utilizable por nosotros si empleados la rutina sin formato PrintString_8x8.

Donde no hay duda de la gran utilidad de las anteriores rutinas es en cualquier juego basado en texto, donde nos evitamos realizar el formato de los textos en base a programación y llamadas continuadas a las funciones de formato, posicionamiento, etc. Bastará con definir las cadenas en nuestro programa con el formato adecuado. El ahorro en líneas de código será muy considerable.


Nuestro siguiente objetivo es extender PringString_8x8_Format para permitir la utilización de códigos de control que representen valores de variables. El objetivo es simular la funcionalidad de la función printf() del lenguaje C, el cual permite impresiones de cadena como la siguiente:

printf( "Jugador %s: Tienes %d vidas.", nombre, vidas );

Para eso vamos a crear una nueva rutina PrintString_8x8_Format_Args que además de los códigos de control de formato, comprenda códigos para la impresión de variables de cadena o numéricas en representación decimal, hexadecimal o binaria.

Los nuevos códigos de control imitarán el formato de C (símbolo de % seguido de un identificador del tipo de variable) y podrán estar así integrados dentro del propio texto:


Código de control Significado
%d Imprimir argumento número entero de 8 bits en formato decimal
%D Imprimir argumento número entero de 16 bits en formato decimal
%t Imprimir argumento número entero 0-99 con 2 dígitos incluyendo ceros
%x Imprimir argumento de 8 bits en formato hexadecimal
%X Imprimir argumento de 16 bits en formato hexadecimal
%b Imprimir argumento de 8 bits en formato binario
%B Imprimir argumento de 16 bits en formato binario
%s Imprimir argumento de tipo cadena (acabada en 0 / EOS)
%% Símbolo del porcentaje (%)


De esta forma podremos definir cadenas como:

cadena1   DB   "Has obtenido %D puntos", FONT_EOS
cadena2   DB   "El numero %d en binario es %b", FONT_EOS
cadena3   DB   "Has tenido un %d %% de aciertos", FONT_EOS
cadena4   DB   "Bienvenido al juego, %s", FONT_EOS
cadena5   DB   "Hora: %t:%t", FONT_EOS

Nótese que podríamos haber empleado el sistema de códigos de formato con los ASCIIs libres entre el 17 y el 31. El lector puede adaptar fácilmente la rutina a ese sistema si así lo deseara.

Volvamos a PrintString_8x8_Format_Args: Nuestra nueva rutina deberá recibir ahora un parámetro adicional: además de la cadena a imprimir en HL, deberemos apuntar el registro IX a un array con los datos a sustuitir, o apuntando a una única variable de memoria si sólo hay un parámetro.

La rutina es similar a PrintString_8x8_Format, pero añadiendo lo siguiente:

;;; HL = Cadena que imprimir

PrintString_8x8_Format_Args:
   ;;;
bucle:
      ;;; Coger caracter apuntado por HL.
      ;;; Incrementar HL
      ;;; Si HL es mayor que 32 :
          ;;; Si es un caracter '%':
              ;;; Si el siguiente caracter no es %:
              ;;; Saltamos a seccion de codigo_gestion_ARGS
          ;;; Imprimir caracter en FONT_X,FONT_Y
          ;;; Avanzar el puntero FONT_X
      ;;; Si HL es menor que 31 :
          ;;; Si es CERO, salir de la rutina con ret z.
          (...)

codigo_gestion_ARGS:
   ;;; Llegamos aqui con el codigo en A
   ;;; Si el codigo es 'd' -> Saltar a gestion de tipo int8
   ;;; Si el codigo es 'D' -> Saltar a gestion de tipo int16
   ;;; Si el codigo es 't' -> Saltar a gestion de tipo int8_2digits
   ;;; Si el codigo es 'x' -> Saltar a gestion de tipo hex8
   ;;; Si el codigo es 'X' -> Saltar a gestion de tipo hex16
   ;;; Si el codigo es 'b' -> Saltar a gestion de tipo bin8
   ;;; Si el codigo es 'B' -> Saltar a gestion de tipo bin16
   ;;; Si el codigo es 's' -> Saltar a gestion de tipo string

En código:

loop:
    ld a, (hl)                ; Leemos un caracter de la cadena
 
    ;;; (...)
 
    cp '%'                    ; Es un caracter %?
    jr nz, pstring8_novar     ; Comprobamos si es variable
    ld a, (hl)                ; Cogemos en A el siguiente char
    inc hl
    cp '%'                    ; Es otro caracter %? (leido %%?)
    jr nz, pstring8v_var      ; No, es una variable -> Saltar
                              ; Si, era %, seguir para imprimirlo
(...)
    ;;; Aqui se gestionan los codigos de control con % (tipo = A)
pstring8v_var:
    ;;; comprobamos los tipos y saltamos a sus rutinas de gestion
    cp 'd'
    jr z, pstring8v_int8
    cp 't'
    jr z, pstring8v_int8_2d
    cp 'D'
    jr z, pstring8v_int16
    cp 's'
    jr z, pstring8v_string
    cp 'x'
    jr z, pstring8v_hex8
    cp 'X'
    jr z, pstring8v_hex16
    cp 'b'
    jp z, pstring8v_bin8
    cp 'B'
    jp z, pstring8v_bin16
    jp pstring8_novar         ; Otro: imprimir caracter tal cual

Las diferentes porciones de código a las que saltaremos según el tipo de dato a imprimir harán uso de las funciones de conversión de valor numérico a cadena que ya vimos en un anterior apartado de este capítulo. Por ejemplo:

    cp 'd'
    jr z, pstring8v_int8
 
(...)
pstring8v_int8:
    push hl
    ld l, (ix+0)
    inc ix
    call Int2String_8
    ld hl, conv2string
    call INC_HL_Remove_Leading_Zeros
    call PrintString_8x8
    pop hl
    jp pstring8v_loop         ; Volvemos al bucle principal

El código completo de la rutina es el siguiente:

;-------------------------------------------------------------
; PrintString_8x8_Format_Args:
; Imprime una cadena de texto de un charset de fuente 8x8.
; Soporta codigos de control y argumentos.
;
; Entrada (paso por parametros en memoria):
; -----------------------------------------------------
; FONT_CHARSET = Direccion de memoria del charset.
; FONT_X       = Coordenada X en baja resolucion (0-31)
; FONT_Y       = Coordenada Y en baja resolucion (0-23)
; FONT_ATTRIB  = Atributo a utilizar en la impresion.
; Registro HL  = Puntero a la cadena de texto a imprimir.
;                Debe acabar en cero (FONT_EOS).
; Registro IX  = Puntero al listado de argumentos (si hay).
; Usa: DE, BC
;-------------------------------------------------------------
PrintString_8x8_Format_Args:
 
    ;;; Bucle de impresion de caracter
pstring8v_loop:
    ld a, (hl)                ; Leemos un caracter de la cadena
    inc hl                    ; Apuntamos al siguiente caracter
 
    cp 32                     ; Es menor que 32?
    jp c, pstring8v_ccontrol  ; Si, es un codigo de control, saltar
 
    cp '%'                    ; Es un caracter %?
    jr nz, pstring8_novar     ; Comprobamos si es variable
    ld a, (hl)                ; Cogemos en A el siguiente char
    inc hl
    cp '%'                    ; Es otro caracter %? (leido %%?)
    jr nz, pstring8v_var      ; No, es una variable -> Saltar
                              ; Si, era %, seguir para imprimirlo
pstring8_novar:
    push hl                   ; Salvaguardamos HL
    call PrintChar_8x8        ; Imprimimos el caracter
    pop hl                    ; Recuperamos HL
 
    ;;; Avanzamos el cursor usando Font_Blank, que incrementa X
    ;;; y actualiza X e Y si se llega al borde de la pantalla
    call Font_Inc_X           ; Avanzar coordenada X
    jr pstring8v_loop         ; Continuar impresion hasta CHAR=0
 
pstring8v_ccontrol:
    or a                      ; A es cero?
    ret z                     ; Si es 0 (fin de cadena) volver
 
    ;;; Si estamos aqui es porque es un codigo de control distinto > 0
    ;;; Ahora debemos calcular la direccion de la rutina que lo atendera.
 
    ;;; Calculamos la direccion destino a la que saltar usando
    ;;; la tabla de saltos y el codigo de control como indice
    ex de, hl
    ld hl, FONT_CALL_JUMP_TABLE
    dec a                     ; Decrementamos A (puntero en tabla)
    rlca                      ; A = A * 2 = codigo de control * 2
    ld c, a
    ld b, 0                   ; BC = A*2
    add hl, bc                ; HL = DIR FONT_CALL_JUMP_TABLE+(CodControl*2)
    ld c, (hl)
    inc hl
    ld h, (hl)
    ld l, c                   ; Leemos la direccion de la tabla en HL
 
    ;;; Si CCONTROL>0 y CCONTROL<10 -> recoger parametro y saltar a rutina
    ;;; Si CCONTROL>9 y CCONTROL<32 -> saltar a rutina sin recogida
    cp 18                     ; Comprobamos si (CCONTROL-1)*2 < 18
    jp nc, pstring8v_noparam  ; Es decir, si CCONTROL > 9, no hay param
 
    ;;; Si CCONTROL < 10 -> recoger parametro:
    ld a, (de)                ; Cogemos el parametro en cuestion de la cadena
    inc de                    ; Apuntamos al siguiente caracter
 
    ;;; Realizamos el salto a la rutina con o sin parametro recogido
pstring8v_noparam:
    ld bc, pstring8v_retaddr  ; Ponemos en BC la dir de retorno
    push bc                   ; Hacemos un push de la dir de retorno
    jp (hl)                   ; Saltamos a la rutina seleccionada
 
    ;;; Este es el punto al que volvemos tras la rutina
pstring8v_retaddr:
    ex de, hl                 ; Recuperamos en HL el puntero a cadena
    jr pstring8v_loop         ; Continuamos en el bucle
 
    ;;; Aqui se gestionan los codigos de control con % (tipo = A)
pstring8v_var:
    ;;; comprobamos los tipos y saltamos a sus rutinas de gestion
    cp 'd'
    jr z, pstring8v_int8
    cp 't'
    jr z, pstring8v_int8_2d
    cp 'D'
    jr z, pstring8v_int16
    cp 's'
    jr z, pstring8v_string
    cp 'x'
    jr z, pstring8v_hex8
    cp 'X'
    jp z, pstring8v_hex16
    cp 'b'
    jp z, pstring8v_bin8
    cp 'B'
    jp z, pstring8v_bin16
    jp pstring8_novar         ; Otro: imprimir caracter tal cual
 
;----------------------------------------------------------
pstring8v_int8:
    push hl
    ld l, (ix+0)
    inc ix
    call Int2String_8
    ld hl, conv2string
    call INC_HL_Remove_Leading_Zeros
    call PrintString_8x8
    pop hl
    jp pstring8v_loop         ; Volvemos al bucle principal
 
;----------------------------------------------------------
pstring8v_int8_2d:
    push hl
    ld a, (ix+0)
    inc ix
    call Int2String_8_2Digits
    ld a, d                   ; Resultado conversion en DE
    push de
    call PrintChar_8x8        ; Imprimir parte alta (decenas)
    call Font_Inc_X
    pop de
    ld a, e
    call PrintChar_8x8        ; Imprimir parte alta (decenas)
    call Font_Inc_X
    pop hl
    jp pstring8v_loop         ; Volvemos al bucle principal
 
;----------------------------------------------------------
pstring8v_int16:
    push hl
    ld l, (ix+0)
    inc ix
    ld h, (ix+0)
    inc ix
    call Int2String_16
    ld hl, conv2string
    call INC_HL_Remove_Leading_Zeros
    call PrintString_8x8
    pop hl
    jp pstring8v_loop         ; Volvemos al bucle principal
 
;----------------------------------------------------------
pstring8v_string:
    push hl
    push ix                   ; HL = IX
    pop hl
    call PrintString_8x8      ; Imprimimos cadena
    pop hl
pstring8v_strloop:            ; Incrementamos IX hasta el fin
    ld a, (ix+0)              ; de la cadena, recorriendola
    inc ix                    ; hasta (IX) = 0
    or a
    jr nz, pstring8v_strloop
 
    ; De esta forma IX ya apunta al siguiente argumento
    jp pstring8v_loop         ; Volvemos al bucle principal
 
;----------------------------------------------------------
pstring8v_hex8:
    push hl
    ld l, (ix+0)
    inc ix
    ld l, 40
    call Hex2String_8
    ld hl, conv2string
    call PrintString_8x8
    pop hl
    jp pstring8v_loop         ; Volvemos al bucle principal
 
;----------------------------------------------------------
pstring8v_hex16:
    push hl
    ld l, (ix+0)
    inc ix
    ld h, (ix+0)
    inc ix
    call Hex2String_16
    ld hl, conv2string
    call PrintString_8x8
    pop hl
    jp pstring8v_loop         ; Volvemos al bucle principal
 
;----------------------------------------------------------
pstring8v_bin8:
    push hl
    ld l, (ix+0)
    inc ix
    call Bin2String_8
    ld hl, conv2string
    call PrintString_8x8
    pop hl
    jp pstring8v_loop         ; Volvemos al bucle principal
 
;----------------------------------------------------------
pstring8v_bin16:
    push hl
    ld l, (ix+0)
    inc ix
    ld h, (ix+0)
    inc ix
    call Bin2String_16
    ld hl, conv2string
    call PrintString_8x8
    pop hl
    jp pstring8v_loop         ; Volvemos al bucle principal

La llamada a la rutina de impresión con parámetros se realiza apuntando HL a la cadena con formato e IX al listado de parámetros:

; Ejemplo de impresion de texto con argumentos
    ORG 35000
 
    ld hl, $3d00-256
    call Font_Set_Charset
 
    ld bc, $0400
    call Font_Set_XY
 
    ld hl, cadena1
    ld ix, args1
    call PrintString_8x8_Format_Args
 
    ld hl, cadena2
    ld ix, args2
    call PrintString_8x8_Format_Args
 
loop:
    jr loop
    ret
 
cadena1 DB "Valor 8 bits: 40", FONT_CRLF, FONT_CRLF
        DB "Decimal: %d" , FONT_CRLF
        DB "Hexadecimal: $%x" , FONT_CRLF
        DB "Binario: %%%b" , FONT_CRLF
        DB FONT_CRLF, FONT_CRLF
        DB "Valor 16 bits: 1205", FONT_CRLF, FONT_CRLF
        DB "Decimal: %D" , FONT_CRLF
        DB "Hexadecimal: $%X" , FONT_CRLF
        DB "Binario: %%%B" , FONT_CRLF, FONT_CRLF, FONT_CRLF
        DB FONT_EOS
 
args1   DB 40, 40, 040
        DW 1205, 1205, 1205
 
cadena2 DB "2 CADENAS:", FONT_CRLF, FONT_CRLF
        DB "Cadenas: %t: %s y %s"
        DB FONT_EOS
 
args2   DB 2, "cad 1", FONT_EOS, "cad 2", FONT_EOS
 
    END 35000


 Procesado de parametros

No es necesario que el vector de parámetros contenga más de un elemento. Podemos utilizar la rutina directamente con una variable de datos para imprimir su valor:

    ld hl, cadvidas                     ; Cadena
    ld ix, vidas                        ; Variable de argumentos
    call PrintString_8x8_Format_Args    ; Imprimir
 
  (...)
 
cadvidas DB "Tienes %d vidas", FONT_EOS
vidas    DB 10

Sí que hay que ser especialmente cuidadoso a la hora de definir los parámetros en la variable que apuntamos con IX: es importante que cada parámetro tenga su tamaño adecuado (DB, DW), y que no le falten los End Of String (0) a las cadenas.

Nótese que los parámetros que se imprimen pueden ser modificados por el programa, por lo que esta rutina es muy útil en juegos o programas que trabajen con muchos datos a mostrar.



Añadiendo más códigos de control

El sistema que acabamos de ver permite su ampliación con nuevos tipos de datos o métodos de impresión específicos. Supongamos por ejemplo que queremos añadir 2 nuevos tipos de impresión de valores enteros, uno en el que se añadan los ceros al inicio de las cadenas resultantes de la conversión, y otro que permita la impresión justificada.

Para ello creamos los nuevos “códigos de control”:


Código de control Significado
%z Imprimir argumento número entero de 8 bits en formato decimal con sus leading zeros
%Z Imprimir argumento número entero de 16 bits en formato decimal con sus leading zeros
%j Imprimir argumento número entero de 8 bits en formato decimal justificado a derecha (3 caracteres)
%J Imprimir argumento número entero de 16 bits en formato decimal justificado a derecha (5 caracteres)


A continuación realizamos las modificaciones adecuadas a la rutina PrintString_8x8_Format_Args:

PrintString_8x8_Format_Args:
 
    ;;; (...)
 
    ;;; Aqui se gestionan los codigos de control con % (tipo = A)
pstring8v_var:
    ;;; comprobamos los tipos y saltamos a sus rutinas de gestion
    (...)
    cp 'z'
    jr z, pstring8v_int8_zeros
    cp 'Z'
    jr z, pstring8v_int16_zeros
    cp 'j'
    jr z, pstring8v_int8_justify
    cp 'J'
    jr z, pstring8v_int16_justify
    jp pstring8_novar
 
;----------------------------------------------------------
pstring8v_int8_zeros:
    push hl
    ld l, (ix+0)
    inc ix
    call Int2String_8
    ld hl, conv2string
    call PrintString_8x8      ; No llamamos a Remove_Leading_Zeros
    pop hl
    jp pstring8v_loop         ; Volvemos al bucle principal
 
;----------------------------------------------------------
pstring8v_int16_zeros:
    push hl
    ld l, (ix+0)
    inc ix
    ld h, (ix+0)
    inc ix
    call Int2String_16
    ld hl, conv2string
    call PrintString_8x8      ; No llamamos a Remove_Leading_Zeros
    pop hl
    jp pstring8v_loop         ; Volvemos al bucle principal
 
;----------------------------------------------------------
pstring8v_int8_justify:
    push hl
    ld l, (ix+0)
    inc ix
    call Int2String_8
    ld hl, conv2string        ; Llamamos a funcion Justify
    call INC_HL_Justify_Leading_Zeros
    call PrintString_8x8
    pop hl
    jp pstring8v_loop         ; Volvemos al bucle principal
 
;----------------------------------------------------------
pstring8v_int16_justify:
    push hl
    ld l, (ix+0)
    inc ix
    ld h, (ix+0)
    inc ix
    call Int2String_16
    ld hl, conv2string        ; Llamamos a funcion Justify
    call INC_HL_Justify_Leading_Zeros
    call PrintString_8x8
    pop hl
    jp pstring8v_loop         ; Volvemos al bucle principal
 
    (...)

Lo normal a la hora de utilizar PrintString_Format_Args en nuestro programa es que eliminemos todos aquellos códigos de control (y sus rutinas de chequeo y de gestión) de los cuales no vayamos a hacer uso, con el consiguiente ahorro de memoria (desaparecen las instrucciones de las subrutinas de gestión).



La posibilidad de leer una cadena tecleada por el usuario puede resultar fundamental en programas basados en texto. En algunos juegos podríamos aprovecharla para introducción de claves de acceso, pero en aventuras de texto o programas puede resultar imprescindible.

Una rutina de lectura de teclado recibirá como parámetro un puntero a un área vacía en memoria con suficiente espacio libre para la cadena, así como el tamaño máximo de cadena que queremos leer (un límite que asegure que no escribimos contenido fuera del área reservada y apuntada por HL).

En el artículo dedicado al teclado estudiamos rutinas de lectura del mismo que nos proporcionaban scancodes de las teclas pulsadas. También vimos rutinas de obtención del código ASCII correspondiente a un scancode dado.

En este caso vamos a utilizar para nuestro ejemplo la rutina de la ROM KEY_SCAN, la cual permite detectar el uso de CAPS SHIFT y DELETE (CAPS SHIFT + '0') y que distingue por tanto entre mayúsculas y minúsculas. KEY_SCAN está ubicada en la dirección de memoria $028e de la ROM.

Al realizar un call a KEY_SCAN se produce una lectura de todas las filas del teclado seguida de una decodificación del resultado de la lectura. La rutina de la ROM coloca entonces en la variable del sistema LAST_K (dirección 23560) el código ASCII de la tecla pulsada. KEY_SCAN también decodifica las teclas extendidas y LAST_K nos puede servir también para detectar ENTER (código ASCII 13) o DELETE (código ASCII 12).

El desarrollo de la rutina será el siguiente:

;;; Utilizar HL como puntero a una cadena vacía o con contenido.
;;; Utilizar un contador de tamaño de la cadena con valor inicial = longitud maxima - 1
;;; Imprimir cursor.
;;; En un bucle:
    ; Bucle lectura teclado:
       ; Hacer LAST_K = 0
       ; Leer el estado del teclado con KEY_SCAN.
       ; Repetir hasta que LAST_K sea distinto de 0.
    ; Si se ha pulsado la tecla de ENTER (LAST_K==13):
       ; Borrar el cursor en pantalla.
       ; Insertar un cero en la cadena (End Of String) y salir.
    ; Si se ha pulsado la tecla de DELETE o CAPS+'0' (LAST_K==12):
       ; Si no estamos en el primer caracter de la cadena:
         ; Borrar el cursor en pantalla.
         ; Realizar un FONT_BACKSPACE.
         ; Incrementar contador de longitud (cabe un caracter más).
    ; Si se ha pulsado una tecla de ASCII >= 32:
       ; Si el contador de longitud es mayor que 0:
         ; Insertar el ASCII en (HL) e incrementar HL.
         ; Imprimirla en pantalla e incrementar FONT_X.
         ; Decrementar contador de longitud.
         ; Reimprimir cursor.

Veamos el código de la rutina InputString_8x8:

LAST_K      EQU    23560
KEY_SCAN    EQU    $028e
 
;-------------------------------------------------------------
; InputString_8x8:
; Obtiene una cadena de texto desde teclado.
;
; Entrada:
; Registro HL = Puntero a la cadena de texto a obtener.
; Registro A = Longitud maxima de la cadena a obtener
; Usa:
;-------------------------------------------------------------
InputString_8x8:
 
    push hl                   ; Guardamos el puntero a la cadena
    push de
    push bc                   ; Modificados por KEY_SCAN
 
    ld (inputs_counter), a    ; Contador de caracteres a usar
    ld (inputs_limit), a      ; Guardamos la longitud maxima
 
inputs_start:
    ld a, '_'                 ; Imprimir nuevo cursor
    call Font_SafePrintChar_8x8
 
    xor a
    ld (LAST_K), a            ; Limpiar ultima tecla pulsada
 
inputs_loop:
    push hl                   ; KEY_SCAN modifica HL -> preservar
    call KEY_SCAN             ; Escanear el teclado
    pop hl
    ld a, (LAST_K)            ; Obtener el valor decodificado
 
    cp 13
    jr z, inputs_end          ; Es enter? -> fin de rutina
 
    cp 12
    jr z, inputs_delete       ; Es DELETE? -> borrar caracter
 
    cp 32
    jr c, inputs_loop         ; Es < 32? -> repetir bucle escaneo
 
    ;;; Si estamos aqui, ASCII >= 32 -> Es caracter valido -> Guardiar
    ex af, af'                ; Nos guardamos el valor ASCII en A'
 
    ;;; Comprobacion de longitud maxima de cadena
    ld a, (inputs_counter)    ; A = caracteres disponibles
    or a                      ; Comprobar si es 0
    jr z, inputs_loop         ; Si es cero, no insertar caracter
    dec a
    ld (inputs_counter), a    ; Decrementar espacio disponible
 
    ex af, af'                ; Recuperamos ASCII de A'
    ld (hl), a                ; Guardamos el caracter leido
    inc hl                    ; Avanzamos al siguiente caracter e imprimir
    call Font_SafePrintChar_8x8
    call Font_Inc_X
    jr inputs_start           ; Repetir continuamente hasta ENTER
 
;;; Codigo a ejecutar cuando se pulsa enter
inputs_end:                  ; ENTER pulsado -> Fin de la rutina
 
    ld a, ' '                 ; Borramos de la pantalla el cursor
    call Font_SafePrintChar_8x8
    xor a
    ld (hl), a                ; Almacenamos un FIN DE CADENA en HL
    pop bc
    pop de                    ; Recuperamos valores de registros
    pop hl                    ; Recuperamos el inicio de la cadena
    ret
 
;;; Codigo a ejecutar cuando se pulsa DELETE
inputs_delete:               ; DELETE pulsado -> Borrar caracter
    ld a, (inputs_limit)
    ld b, a
    LD , (inputs_counter)
    cp b                      ; Si char_disponibles-limite == 0 ...
    jr z, inputs_loop         ; ... no podemos borrar (inicio de cadena)
 
    inc a                     ; Si no, si que podemos borrar:
    ld (inputs_counter), a    ; incrementar espacio disponible
 
    dec hl                    ; Decrementar puntero en la cadena
    ld a, ' '                 ; Borrar cursor y caracter anterior
    call Font_SafePrintChar_8x8
    call Font_Dec_X
    jr inputs_start           ; Bucle principal
 
inputs_counter   DB  0
inputs_limit     DB  0

InputString_8x8 utiliza una nueva subrutina llamada Font_SafePrintChar8x8 que no es más que una encapsulación del PrintChar_8x8 original en la que se preservan y restauran los valores de los registros que modifica internamente PrintChar:

;-------------------------------------------------------------
; Ejecuta PrintChar_8x8 preservando registros
;-------------------------------------------------------------
Font_SafePrintChar_8x8
    push bc
    push de
    push hl                   ; Preservar registros
    call PrintChar_8x8        ; Imprimir caracter
    pop hl                    ; Recuperar registros
    pop de
    pop bc
    ret

Veamos un ejemplo de uso de nuestra nueva función de INPUT:

; Ejemplo de input de texto
    ORG 35000
 
    ld hl, $3d00-256
    call Font_Set_Charset
 
    ;;; Imprimir cadena "Introduce un texto:"
    ld hl, cadena1
    ld b, 4
    ld c, 0
    call Font_Set_XY
    call PrintString_8x8_Format
 
    ;;; Obtener el input del usuario
    ld hl, input1
    ld a, 20
    call InputString_8x8
 
    ;;; Imprimir "Tu cadena es:" + la cadena resultante
    ld hl, cadena2
    call PrintString_8x8_Format
 
    ld hl, input1
    call PrintString_8x8_Format
 
    ret
 
cadena1 DB "Introduce un texto:", FONT_CRLF, FONT_CRLF
        DB FONT_SET_INK, 2, FONT_SET_STYLE, FONT_BOLD
        DB "> ", FONT_EOS
 
cadena2 DB FONT_CRLF, FONT_CRLF, FONT_SET_STYLE, FONT_NORMAL
        DB FONT_SET_INK, 0, "Tu cadena es: ", FONT_CRLF, FONT_CRLF
        DB FONT_SET_INK, 2, FONT_SET_STYLE, FONT_BOLD, FONT_EOS
 
input1  DS 35
        DB 0
 
    END 35000


 Input de texto

Si nuestro programa o juego va a requerir un posibilidades de introducción o edición de textos avanzadas, sería aconsejable ampliar la rutina anterior con nuevas opciones o mejoras como las siguientes:


  • Permitir edición multilínea. La rutina actual no permite trabajar (al menos en cuanto al borrado) con entrada de texto de múltiples líneas. Se podría editar la rutina para permitir editar más de una línea de texto, realizando una versión especial de Font_Dec_X que decremente el valor de FONT_Y y ponga FONT_X=0 cuando tratemos de borrar desde el margen izquierdo de la pantalla.
  • Habilitar el uso de las teclas de cursor para moverse entre los caracteres de la cadena y así permitir edición avanzada. La rutina debería basarse entonces en un FONT_X y FONT_Y propios y ya no se podría utilizar FONT_BACKSPACE para el borrado. Además, al insertar un carácter en el interior de la cadena habría que mover todos los caracteres en memoria una posición a la derecha y redibujar la cadena completa en pantalla. El cursor podría simularse entonces con FLASH o subrayando la letra actual (por lo que no serviría para editar texto subrayado).
  • Permitir llamar a la función con una cadena ya en la zona apuntada por HL. En conjunción con la mejora anterior permitiría editar texto anteriormente introducido.


En cualquier caso, la rutina que acabamos de ver es más que suficiente para recoger cadenas simples en nuestro programa.



La resolución de texto del Spectrum es bastante reducida, con sus 32 caracteres en pantalla por línea. Esto provoca limitaciones en programas o juegos de texto que requieren mostrar muchas cadenas de caracteres por pantalla.

Para solucionar esto podemos utilizar una fuente de tamaño 4×8 que nos permita ubicar 2 letras en un mismo bloque de pantalla, proporcionándonos una resolución de 64×24 caracteres.

Dibujar letras distinguibles en 4×8 píxeles no es sencillo, pero existe una fuente de texto ya creada y código para su impresión, creados por Andrew Owen (http://chuntey.wordpress.com) y Tony Samuels (Your Spectrum #13, Abril de 1985).

La fuente de texto en 4×8 tiene el siguiente aspecto:


 Charset 4x8

Para utilizar este set de caracteres sólo tendremos que realizar una nueva rutina de impresión llamada PrintChar_4x8 y modificar la variable que define la anchura de la pantalla, FONT_SWIDTH (que pasa de valer 32 a 64).

La definición de la fuente de 4×8 píxeles en formato DB es la siguiente:

; half width 4x8 font - 384 bytes
charset_4x8:
    DB $00,$02,$02,$02,$02,$00,$02,$00,$00,$52,$57,$02,$02,$07,$02,$00
    DB $00,$25,$71,$62,$32,$74,$25,$00,$00,$22,$42,$30,$50,$50,$30,$00
    DB $00,$14,$22,$41,$41,$41,$22,$14,$00,$20,$70,$22,$57,$02,$00,$00
    DB $00,$00,$00,$00,$07,$00,$20,$20,$00,$01,$01,$02,$02,$04,$14,$00
    DB $00,$22,$56,$52,$52,$52,$27,$00,$00,$27,$51,$12,$21,$45,$72,$00
    DB $00,$57,$54,$56,$71,$15,$12,$00,$00,$17,$21,$61,$52,$52,$22,$00
    DB $00,$22,$55,$25,$53,$52,$24,$00,$00,$00,$00,$22,$00,$00,$22,$02
    DB $00,$00,$10,$27,$40,$27,$10,$00,$00,$02,$45,$21,$12,$20,$42,$00
    DB $00,$23,$55,$75,$77,$45,$35,$00,$00,$63,$54,$64,$54,$54,$63,$00
    DB $00,$67,$54,$56,$54,$54,$67,$00,$00,$73,$44,$64,$45,$45,$43,$00
    DB $00,$57,$52,$72,$52,$52,$57,$00,$00,$35,$15,$16,$55,$55,$25,$00
    DB $00,$45,$47,$45,$45,$45,$75,$00,$00,$62,$55,$55,$55,$55,$52,$00
    DB $00,$62,$55,$55,$65,$45,$43,$00,$00,$63,$54,$52,$61,$55,$52,$00
    DB $00,$75,$25,$25,$25,$25,$22,$00,$00,$55,$55,$55,$55,$27,$25,$00
    DB $00,$55,$55,$25,$22,$52,$52,$00,$00,$73,$12,$22,$22,$42,$72,$03
    DB $00,$46,$42,$22,$22,$12,$12,$06,$00,$20,$50,$00,$00,$00,$00,$0f
    DB $00,$20,$10,$03,$05,$05,$03,$00,$00,$40,$40,$63,$54,$54,$63,$00
    DB $00,$10,$10,$32,$55,$56,$33,$00,$00,$10,$20,$73,$25,$25,$43,$06
    DB $00,$42,$40,$66,$52,$52,$57,$00,$00,$14,$04,$35,$16,$15,$55,$20
    DB $00,$60,$20,$25,$27,$25,$75,$00,$00,$00,$00,$62,$55,$55,$52,$00
    DB $00,$00,$00,$63,$55,$55,$63,$41,$00,$00,$00,$53,$66,$43,$46,$00
    DB $00,$00,$20,$75,$25,$25,$12,$00,$00,$00,$00,$55,$55,$27,$25,$00
    DB $00,$00,$00,$55,$25,$25,$53,$06,$00,$01,$02,$72,$34,$62,$72,$01
    DB $00,$24,$22,$22,$21,$22,$22,$04,$00,$56,$a9,$06,$04,$06,$09,$06

Dado que cada caracter es de 4×8 bytes, podemos almacenar en un mismo byte bien 2 scanlines de un mismo ASCII o bien 2 scanlines de 2 ASCIIs consecutivos.

En nuestro caso, cada byte de la fuente contiene los datos de 2 caracteres, por lo que 8 bytes del array tienen los 8 scanlines de 2 ASCIIs consecutivos. El nibble superior (4 bits superiores) de cada byte tiene los datos de un carácter ASCII y el nibble inferior los del siguiente. Así, el primer byte del array tiene el scanline superior (0) de los ASCIIs 32 (nibble alto) y 33 (nibble bajo).

Esta disposición de 2 scanlines por byte permite un ahorro de memoria tal que la fuente completa de texto con 96 caracteres ocupe 768/2 = 384 bytes.

Para posicionarnos en esta fuente desde su base con el objetivo de localizar un carácter en A, deberemos dividir el valor del carácter entre 2 ya que cada byte referencia a 2 caracteres. El resto de la división entre 2 (par o impar) nos indica si el carácter que buscamos está en el nibble superior o el inferior de los 8 bytes consecutivos a leer.

Por otra parte, la pantalla tiene “físicamente” 32 bloques, pero nosotros vamos a imprimir 64 caracteres, por lo que cada bloque puede contener 2 caracteres. Cuando especificamos una coordenada X para imprimir, la rutina necesita dividirla por 2 para saber en qué carácter de pantalla irá impresa la letra. Una vez calculado este carácter, la letra puede ir en la parte “izquierda” del bloque de pantalla (4 bits superiores) o en la parte derecha (4 bits inferiores del bloque). El resto de la división entre 2 de la coordenada X nos indica en cuál de las 2 partes se debe imprimir el carácter.

La rutina de impresión de caracteres de 4×8 es bastante parecida a la rutina estándar de 8×8 salvo por las divisiones entre 2 del carácter ASCII y de la posición X en pantalla para el cálculo del origen en la fuente y del destino en vram.

La impresión del carácter en sí mismo también cambia, ya que existen 4 posibilidades de impresión que requieren 4 porciones de código diferentes:

Como en cada byte de la fuente se definen 2 caracteres (izquierdo y derecho) y a su vez a la hora imprimir en pantalla hay 2 posibilidades de impresión en el mismo bloque (parte izquierda y parte derecha del bloque), necesitamos 4 rutinas que cubran esas cuatro posibilidades.


  • Imprimir un carácter de la “parte izquierda” (nibble alto en datos de caracter) de la fuente en la “parte izquierda” de un bloque de pantalla (nibble alto del byte en videoram).
  • Imprimir un carácter de la “parte derecha” (nibble bajo en datos de caracter) de la fuente en la “parte izquierda” de un bloque de pantalla (nibble alto del byte en videoram).
  • Imprimir un carácter de la “parte izquierda” (nibble alto en datos de caracter) de la fuente en la “parte derecha” de un bloque de pantalla (nibble bajo del byte en videoram).
  • Imprimir un carácter de la “parte derecha” (nibble bajo en datos de caracter) de la fuente en la “parte derecha” de un bloque de pantalla (nibble bajo del byte en videoram).


Al trazar el carácter en pantalla tenemos que hacerlo con OR para respetar otro posible carácter de 4×8 que pueda haber en el mismo bloque, ya lo estemos imprimiendo en la parte izquierda de un bloque (respetar el nibble de la derecha) o en la derecha (respetar el nibble de la izquierda).

Veamos la rutina de impresión PrintChar_4x8 seguida de la 4 subrutinas de volcado de carácter que son llamados una vez calculados HL y DE como origen y destino.

;-------------------------------------------------------------
; PrintChar_4x8:
; Imprime un caracter de 4x8 pixeles de un charset.
;
; Entrada (paso por parametros en memoria):
; -----------------------------------------------------
; FONT_CHARSET = Direccion de memoria del charset.
; FONT_X       = Coordenada X en baja resolucion (0-31)
; FONT_Y       = Coordenada Y en baja resolucion (0-23)
; FONT_ATTRIB  = Atributo a utilizar en la impresion.
; Registro A   = ASCII del caracter a dibujar.
;-------------------------------------------------------------
PrintChar_4x8:
 
    rra                       ; Dividimos A por 2 (resto en CF)
    push af                   ; Guardamos caracter y CF en A'
 
    ;;; Calcular posicion origen (array fuente) en HL como:
    ;;;     direccion = base_charset + ((CARACTER/2)*8)
    ld bc, (FONT_CHARSET)
    ld h, 0
    ld l, a
    add hl, hl
    add hl, hl
    add hl, hl
    add hl, bc                ; HL = Direccion origen de A en fuente
 
    ;;; Calculamos las coordenadas destino de pantalla en DE:
    ld bc, (FONT_X)           ; B = Y,  C = X
    rr c
    ld a, b
    and $18
    add a, $40
    ld d, a
    ld a, b
    and 7
    rrca
    rrca
    rrca
    add a, c
    ld e, a                  ; DE contiene ahora la direccion destino.
 
    ;;; Calculamos posición en pantalla. Tenemos que dividirla por 2 porque
    ;;; en cada columna de pantalla caben 2 caracteres. Usaremos el resto
    ;;; (Carry) para saber si va en la izq (CF=0) o der (CF=1) del caracter.
    ld a, (FONT_X)            ; Volvemos a leer coordenada X
    rra                       ; Dividimos por 2 (posicion X en pantalla)
                                ; Ademas el carry tiene el resto (par/impar)
    jr nc, pchar4_x_odd       ; Saltar si es columna impar (por el CF)
 
    ;;; Ahora tenemos que imprimir el caracter en pantalla. Hemos saltado
    ;;; a pchar4_x_even o pchar4_x_odd segun si la posicion en pantalla es
    ;;; par o impar, pero cada una de estas 2 opciones nos da la posibilidad
    ;;; de usar una rutina u otra segun si el caracter ASCII es par o impar
    ;;; ya que tenemos que cogerlo de la fuente de una forma u otra
    ;;; Posicion de columna en pantalla par:
pchar4_x_even  :
    pop af                    ; Restaura A=char y CF=si es char par/impar
    jr c, pchar4_l_on_l
    jr pchar4_r_on_l
 
pchar4_x_odd:
    pop af                    ; Restaura A=char y CF=si es char par/impar
    jr nc, pchar4_r_on_r
    jr pchar4_l_on_r
 
pchar4_continue:
 
   ;;; Impresion de los atributos
pchar4_printattr:
 
    ld a, d            ; Recuperamos el valor inicial de DE
    sub 8              ; Restando los 8 scanlines avanzados
 
    ;;; Calcular posicion destino en area de atributos en HL.
    rrca               ; A ya es = D, listo para rotar
    rrca               ; Codigo de Get_Attr_Offset_From_Image
    rrca
    and 3
    or $58
    ld h, a
    ld l, e
 
    ;;; Escribir el atributo en memoria
    ld a, (FONT_ATTRIB)
    ld (hl), a         ; Escribimos el atributo en memoria
    ret
 
;;;------------------------------------------------------------------
;;; "Subrutinas" de impresion de caracter de 4x8
;;; Entrada: HL = posicion del caracter en la fuente 4x8
;;;          DE = posicion en pantalla del primer scanline
;;;------------------------------------------------------------------
 
;;;----------------------------------------------------
pchar4_l_on_l:
    ld b, 8                   ; 8 scanlines / iteraciones
pchar4_ll_lp:
    ld a, (de)                ; Leer byte de la pantalla
    and %11110000
    ld c, a                   ; Nos lo guardamos en C
    ld a, (hl)                ; Cogemos el byte de la fuente
    and %00001111
    or c                      ; Lo combinamos con el fondo
    ld (de), a                ; Y lo escribimos en pantalla
    inc d                     ; Siguiente scanline
    inc hl                    ; Siguiente dato del "sprite"
    djnz pchar4_ll_lp
    jr pchar4_continue        ; Volver tras impresion
 
;;;----------------------------------------------------
pchar4_r_on_r:
    ld b, 8                   ; 8 scanlines / iteraciones
pchar4_rr_lp:
    ld a, (de)                ; Leer byte de la pantalla
    and %00001111
    ld c, a                   ; Nos lo guardamos en C
    ld a, (hl)                ; Cogemos el byte de la fuente
    and %11110000
    or c                      ; Lo combinamos con el fondo
    ld (de), a                ; Y lo escribimos en pantalla
    inc d                     ; Siguiente scanline
    inc hl                    ; Siguiente dato del "sprite"
    djnz pchar4_rr_lp
    jr pchar4_continue        ; Volver tras impresion
 
;;;----------------------------------------------------
pchar4_l_on_r:
    ld b, 8                   ; 8 scanlines / iteraciones
pchar4_lr_lp:
    ld a, (de)                ; Leer byte de la pantalla
    and %00001111
    ld c, a                   ; Nos lo guardamos en C
    ld a, (hl)                ; Cogemos el byte de la fuente
    rrca                      ; Lo desplazamos 4 veces >> dejando
    rrca                      ; lo bits 4 al 7 vacios
    rrca
    rrca
    and %11110000
    or c                      ; Lo combinamos con el fondo
    ld (de), a                ; Y lo escribimos en pantalla
    inc d                     ; Siguiente scanline
    inc hl                    ; Siguiente dato del "sprite"
    djnz pchar4_lr_lp
    jr pchar4_continue        ; Volver tras impresion
 
;;;----------------------------------------------------
pchar4_r_on_l:
    ld b, 8                   ; 8 scanlines / iteraciones
pchar4_rl_lp:
    ld a, (de)                ; Leer byte de la pantalla
    and %11110000
    ld c, a                   ; Nos lo guardamos en C
    ld a, (hl)                ; Cogemos el byte de la fuente
    rlca                      ; Lo desplazamos 4 veces << dejando
    rlca                      ; los bits 0 al 3 vacios
    rlca
    rlca
    and %00001111
    or c                      ; Lo combinamos con el fondo
    ld (de), a                ; Y lo escribimos en pantalla
    inc d                     ; Siguiente scanline
    inc hl                    ; Siguiente dato del "sprite"
    djnz pchar4_rl_lp
    jr pchar4_continue        ; Volver tras impresion

Para poder utilizar estas rutinas con nuestro sistema de impresión habría que cambiar la constante que define el tamaño de anchura de la pantalla en caracteres:

FONT_SCRWIDTH    EQU   64

También habría que crear un PrintString_4x8 idéntico a PrintString_8x8 pero que llame a PrintChar_4x8, y modificar aquellas rutinas que hagan referencia a una de estas 2 funciones, como por ejemplo:

Font_Backspace:
    call Font_Dec_X
    ld a, ' '                 ; Imprimir caracter espacio
    push bc
    push de
    push hl
    call PrintChar_4x8        ; Sobreescribir caracter
    pop hl
    pop de
    pop bc
    ret                       ; Salir

Una vez realizados estos cambios, podemos utilizar la fuente de 4×8 con nuestro sistema de gestión de texto. Veamos un sencillo ejemplo:

; Ejemplo de fuente de 4x8 pixeles (64 caracteres por linea)
    ORG 35000
 
    ld hl, charset_4x8-128                    ; Inicio charset - (256/2)
    call Font_Set_Charset
 
    ld hl, cadena1
    ld bc, $0400                              ; X=00, Y=04
    call Font_Set_XY
    call PrintString_4x8_Format
 
loop:
    jr loop
    ret
 
cadena1 DB "Fuente de 4x8", FONT_CRLF, FONT_CRLF
        DB "Esto es un texto impreso a 64 columnas con fuente "
        DB "de 4x8 pixeles. La letra tiene la mitad de anchura "
        DB "que la estandar pero todavia se lee con facilidad."
        DB FONT_CRLF, FONT_CRLF
        DB "Con esta fuente se pueden realizar juegos basados en "
        DB "texto pero hay que tener en cuenta que los atributos "
        DB FONT_SET_INK, 2, "afectan a 2 caracteres ", FONT_SET_INK, 0
        DB "a la vez, por lo que es mejor no cambiar el paper y "
        DB "modificarlos solo antes o tras un espacio."
        DB FONT_EOS
 
    END 35000


 Ejemplo de impresión a 64 columnas

Nótese cómo hemos inicializado FONT_CHARSET a la dirección de la fuente menos 128 en lugar de restarle 256. Esto se debe a que la fuente tiene 2 caracteres definidos en cada byte y vamos a dividir el ASCII entre 2 en nuestra rutina, por lo que el carácter en que empieza nuestra fuente, el 32, está en charset4x8 - (256/2) = charset4x8 - 128.

Otro detalle importante es el tema de los atributos: como cada bloque de pantalla contiene 2 caracteres, no podemos establecer atributos diferentes para 2 caracteres del mismo byte. Por esto, hay que ser cauto a la hora de establecer atributos. La solución más sencilla es cambiar las tintas en posiciones donde haya espacios, ya que en ese caso el cambio será efectivo en la letra deseada si ésta es la primera del byte, o en el espacio seguido de la letra deseada si está en la parte derecha. Los cambios de PAPER, BRIGHT o FLASH supondrán problemas si no se realizan siempre en posiciones de pantalla pares.

Finalmente, debido al reducido tamaño de la fuente no se han definido funciones de estilo, ya que es muy difícil realizar estilos de negrita o cursiva con una definición de 4×8. La única opción viable es la creación de un estilo de subrayado creando 4 funciones de impresión 4×8 adicionales (para las 4 combinaciones de par/impar en cuanto a ASCII/pantalla) donde se tracen 7 scanlines y el último se trace como %11110000 ó %00001111.

  • cursos/ensamblador/gfx4_fuentes.txt
  • Última modificación: 19-01-2024 08:23
  • por sromero