MODO GRAFICO
______________________________________
El modo gráfico que vamos a usar es el 8. Si queremos incializar la pantalla y usarla al completo debemos hacer
desde BASIC:
10 MODE 8
20 WINDOW 512, 256, 0, 0
Con la línea 10 escogemos el modo 8, que es de 256x256 pixels de resolución con cada pixel de un color (de 8 posibles+flash). Debido a que el BASIC del QL siempre piensa en el modo más alto de resolución, debemos abrir una ventana de juego como si estuviéramos en dicho modo, de ahí la línea 20, que abre una ventana desde la esquina 0,0
(x,y) a la 512,256.
Con estas dos líneas ya tenemos preparada la pantalla para empezar, aunque si hacemos un:
30 CLS
Tampoco viene mal.
DEFINICION DEL SPRITE
______________________________________
Desde BASIC hay que cargar cada sprite en memoria, para lo cual primero hay que reservar espacio:
90 SPRITE=RESPR (x)
donde x es el número de bytes a reservar. Tengamos en cuenta que los pixels en el modo 8 van agrupados de 4 en 4 (dos bytes que forman una palabra), por lo que el sprite debe ser múltiplo de 2 bytes.
Para la carga del sprite en si tenemos dos formas: desde BASIC o leyendo desde disco
Si lo hacemos desde BASIC hay que cargarlo con un DATA, ejemplo con un sprite de 4x4 pixels:
100 DATA 7,255,512,128
La rutina de carga en memoria sería:
110 RESTORE 100
120 FOR I=1 to 4 SETP 1
130 READ B
140 POKE_W, I, B
150 END FOR I
Ojo, tal y como vemos, al pokear con POKE_W, tenemos que hacerlo por pares de bytes, es decir una palabra de 16 bits (por eso hemos dicho que lo suyo es usar múltiplos de 4 pixels, dos bytes).
Este mismo ejemplo cargado desde microdrive puede ser:
100 LBYTES "mdv1_sprite", SPRITE
Es evidente que es mucho más práctico este último método, ya que podemos coger nuestro programa favorito de diseño y evitarnos engorros. Basta recordar que el flash lo vamos a usar como máscara...
EL SPRITE
______________________________________
Como somos muy listos vamos a realizar un truco para incluir en el propio sprite la máscara sin necesitar bytes adicionales. ¿Cómo? pues usando el bit de flash como máscara, ya que es muy improbable que queramos usar dicho byte.
Si el bit de máscara es 0 (flash desactivado) se imprimirá el valor de color del sprite.
Si el bit de máscara es 1 (flash activado) no se imprimirá nada y se respetará el pixel actual de fondo.
La razón no es otra que comprobar dicho byte en la rutina, y si es 1 se saltará la parte que imprime el pixel.
Por lo tanto, resumiendo todo lo visto:
- Los sprites tienen que ser múltiplos de 4 pixels de ancho
- Cada palabra (dos bytes) contiene 4 pixels
- El bit de flash lo usaremos como máscara, si es 1 no borrará el fondo, si es 0 imprimirá el sprite
Si no se pretende aprender ensamblador o trastear el mapa de pantalla con POKE se puede uno saltar el siguiente epígrafe.
LA PANTALLA
______________________________________
Para crear nuestra rutina de sprites debemos saber como es la pantalla del QL desde el punto de vista del procesador. Olvidad el BASIC porque esto es otro Mundo.
El comienzo del mapa de pantala está en 131072 (20000 en hexadecimal), es decir, al comienzo de la RAM.
Si nos centramos en el modo 8 nos encontramos que la pantalla está dividida en 256x256 pixels, con cada pixel ocupando 4 bits y posibilitando 8 colores más flash... pero, y aquí es cuando queremos matar a los ingenieros de Sincair, cada pixel no está definido como debería ser lo lógico en 4 bits agrupados.
Me explico, los pixels están agrupados en pantalla de 4 en 4, con la información mezclada en una palabra de 16 bits (dos bytes). Hay dos formas de comprenderlo, por pixel o por bit. Veamos primero por pixel, donde el pixel 1 es el de la izquierda y el pixel 4 el de la derecha:
PIXEL 1:
bit 15: verde (G) - RGB es el valor del color en esta norma
bit 14: flash (F) - Este es el bit de Flash
bit 7: rojo (R)
bit 6: azul (B)
PIXEL 2:
bit 13: verde (G)
bit 12: flash (F)
bit 5: rojo (R)
bit 4: azul (B)
PIXEL 3:
bit 11: verde (G)
bit 10: flash (F)
bit 3: rojo (R)
bit 2: azul (B)
PIXEL 4:
bit 9: verde (G)
bit 8: flash (F)
bit 1: rojo (R)
bit 0: azul (B)
El bit 0 es el menos significativo de los 16 bits.
Si los agrupamos por bits:
VERDE:
bit 15: pixel 1
bit 13: pixel 2
bit 11: pixel 3
bit 9: pixel 4
FLASH:
bit 14: pixel 1
bit 12: pixel 2
bit 10: pixel 3
bit 8: pixel 4
ROJO:
bit 7: pixel 1
bit 5: pixel 2
bit 3: pixel 3
bit 1: pixel 4
AZUL:
bit 6: pixel 1
bit 4: pixel 2
bit 2: pixel 3
bit 0: pixel 4
Y ahora es cuando debemos tomar la terrible decisión... ¿cuál será la precisión de la rutina? es decir, con cual precisión vamos a imprimir los sprites en pantalla. Si usamos palabras completas los sprites se moverán cada 4 pixels (nada mal para usarlo desde BASIC), pero si queremos usar mayor precisión debemos entrar en una rutina que vaya situando los bits en el orden correcto.
Empecemos por lo fácil...
LA RUTINA
______________________________________
Bueno, comenzamos a programar el asunto y aprovecho de paso para dar unas lecciones de ensamblador.
COMO ACABAR LA RUTINA
Si, lo último es lo primero, y para hacer bien la rutina debemos saber como volver a BASIC sin liarla. Para ello basta hacer:
Código: Seleccionar todo
RUTINA:
MOVEQ #0,D0
RTS
END RUTINA
Explico lo que hay:
- Con "RUTINA:" y "END RUTINA" delimitamos donde está la rutina. Se trata de decirle al ensamblador que parte del texto del editor es lo que vamos a ensamblar.
- "MOVEQ #0,D0" y "RTS" devuelven el control al Superbasic y dan por finalizada la ejecución del código máquina. Estamos cargando en el registro D0 e valor 0 para indicar que no ha habido ningún error, y con RTS salimos de la ejecución.
PASANDO VARIABLES
Para pasar variables a la rutina en ensamblador basta realizar una llamada a la misma con las variables de paso tras comas. Ejemplo:
CALL rutina, x, y, xx, yy, sprite
Aquí x e y sería la posición en pixels de la esquina superior izquierda del sprite, xx e yy indicarían el tamaño del sprite., y "sprite" sería la posición de memoria donde almacenamos el sprite a dibujar en pantalla.
Cuando lo hacemos así el código máquina mete los valores que pasemos a la llamada en un orden determinado dentro de los registros internos del procesador:
D1, D2, D3, D4, D5, D6, D7, A0, A1, A2, A3, A4, A5
No debemos usar D0, A6, y A7, ya que se usan para el SuperBasic (D0 es el retorno, por ejemplo).
PREPARANDO REGISTROS
Ya estamos en condiciones de iniciar nuestra rutina, y para ello debemos preparar los registros con los que vamos a trabajar.
Código: Seleccionar todo
LSR #2,D1 ;Dividimos la posición X entre cuatro (0-64)
;ya que los pixels van agrupados en 4 en cada palabra
LEA.L $20000,A0 ;Ponemos en A0 el comienzo de la pantalla
MOVE.L D5, A1 ;Ponemos en A1 la posición del sprite
Para empezar, debido a que los pixels van mezclados de 4 en 4 en una palabra de 16 bits (hablamos del modo , tenemos que dividir la posición x entre cuatro para apuntar correctamente a donde tenemos que poner el sprite horizontalmente. En esta primera versión de la rutina vamos a hacerlo así por sencillez (y velocidad), más adelante tenemos tiempo de mejorar la cosa. El mejor truco para dividir por 4 en ensamblador es desplazar a la derecha dos bits la palabra, si entendéis un poco de binario os daréis cuenta que es así.
También preparamos los registros A0 y A1, para situar la posición inicial de pantalla y la del sprite.
Ahora hay que jugar con los registros para ir metiendo cada cosa en su lugar...
RUTINAS!!!
Bueno, os dejo la rutina final sin máscara, arreglada por McLeod porque cometí un par de errores imperdonables.
Código: Seleccionar todo
*-----------------------------------------------------------
* Autor : Mc Leod
* Descripcion : Rutina de sprites
*-----------------------------------------------------------
* X es la posición donde queremos poner el Sprite (0 izquierda) (múltiplo de 4)
* Y es la posición donde queremos poner el sprite (0 arriba)
* XX es el tamaño horizontal del Sprite (múltiplo de 4)
* YY es el tamaño vertical del Sprite
* SPRITE es la dirección de memoria del Sprite
*
* ENTRADA: X (D1), Y (D2), TamX (D3), TamY (D4), SPRITE (D5)
PonSprite
move.l #131072,a0 ; a0 <- comienzo de la pantalla
movea.l d5,a1 ; a1 <- comienzo de la definicion del sprite
lsr.l #1,d1 ; X dividido entre 2
andi.b #$fe,d1 ; y nos aseguramos de que sea par
lsr #2,d3 ; TamX dividido entre 4
subq #1,d3 ; restamos 1 a TamX porque asi lo necesitan los DBRA
subq #1,d4 ; y lo mismo con TamY.
add.l d1,a0 ; sumamos X a comienzo de pantalla
lsl #7,d2 ; un scan de la pantalla tiene 128 bytes
adda.l d2,a0 ; a0 apunta a la coordenada X,Y
move.l d3,d5 ; d5 es backup de d3
move.l a0,d6 ; d6 es backup de a0
PonScan
move.l d5,d3
move.l d6,a0
Pon4pixels ;o pon 8, si estamos en modo 4
move.w (a1)+,(a0)+
dbra d3,Pon4pixels
addi.l #128,d6
dbra d4,PonScan
moveq #0,d0
rts
end PonSprite
Creo que no hace falta decir mucho más, funciona a las mil maravillas.
Podéis descargar el ejemplo de McLeod, con el código fuente, los binarios, etc, en:
http://www.bytemaniacos.com/ficheros/sinclairql/rutinas/ejemplo_sprites.zip
### RUTINA DE SPRITES USANDO MÁSCARA ###
Ya sabéis que McLeod es de los que les gusta hacer las cosas bien, y me mandó otra versión que cogía otro sprite como máscara. La máscara es la clásica donde 1 es la zona que queremos mantener y 0 la que vamos a borrar. Normalmente debería ser una silueta que envuelve al sprite.
Código: Seleccionar todo
;---------------------------------------------------------------------------------
* X es la posición donde queremos poner el Sprite (0 izquierda) (múltiplo de 4)
* Y es la posición donde queremos poner el sprite (0 arriba)
* TamX es el tamaño horizontal del Sprite (múltiplo de 4)
* TamY es el tamaño vertical del Sprite
* SPRITE es la dirección de memoria del Sprite
* Rutina pensada para MODE 8, usando el bit de flash como transparencia.
* ENTRADA: X (D1), Y (D2), TamX (D3), TamY (D4), Sprite (D5), Máscara (D6), Pantalla (D7)
SpriteTrans8
movea.l d7,a0 ; a0 <- comienzo de la pantalla
movea.l d5,a1 ; a1 <- comienzo de la definicion del sprite
movea.l d6,a2 ; a2 <- comienzo de la definición de la máscara del sprite
lsr.l #1,d1 ; X dividido entre 2
andi.b #$fe,d1 ; y nos aseguramos de que sea par
lsr #2,d3 ; TamX dividido entre 4
subq #1,d3 ; restamos 1 a TamX porque asi lo necesitan los DBRA
subq #1,d4 ; y lo mismo con TamY.
add.l d1,a0 ; sumamos X a comienzo de pantalla
lsl #7,d2 ; un scan de la pantalla tiene 128 bytes
adda.l d2,a0 ; a0 apunta a la coordenada X,Y
move.l d3,d5 ; d5 es backup de d3
move.l a0,d6 ; d6 es backup de a0
PonScanT8
move.l d5,d3
move.l d6,a0
Pon4pixelsT8
move.w (a0),d0 ; D0 contiene el cacho de pantalla que va a usarse
move.w (a1)+,d1 ; D1 contiene el cacho de sprite a mostrar
move.w (a2)+,d2 ; D2 va a ser una máscara para determinar qué bits del sprite se quedan
and.w d2,d1 ; Enmascaramos el sprite
not.w d2 ; Negamos la mascara...
and.w d2,d0 ; ... y la aplicamos a la pantalla
or.w d1,d0 ; juntamos sprite enmascarado con pantalla enmascarada
move.w d0,(a0)+ ; y lo plantamos en la pantalla
dbra d3,Pon4pixelsT8
addi.l #128,d6
dbra d4,PonScanT8
moveq #0,d0
rts
;---------------------------------------------------------------------------------
* X es la posición donde queremos poner el Sprite (0 izquierda) (múltiplo de 8)
* Y es la posición donde queremos poner el sprite (0 arriba)
* TamX es el tamaño horizontal del Sprite (múltiplo de 8)
* TamY es el tamaño vertical del Sprite
* SPRITE es la dirección de memoria del Sprite
* Rutina pensada para MODE 8, usando el bit de flash como transparencia.
* ENTRADA: X (D1), Y (D2), TamX (D3), TamY (D4), Sprite (D5), Máscara (D6), Pantalla (D7)
SpriteTrans4
movea.l d7,a0 ; a0 <- comienzo de la pantalla
movea.l d5,a1 ; a1 <- comienzo de la definicion del sprite
movea.l d6,a2 ; a2 <- comienzo de la definición de la máscara del sprite
lsr.l #2,d1 ; X dividido entre 4
andi.b #$fe,d1 ; y nos aseguramos de que sea par
lsr #3,d3 ; TamX dividido entre 8
subq #1,d3 ; restamos 1 a TamX porque asi lo necesitan los DBRA
subq #1,d4 ; y lo mismo con TamY.
add.l d1,a0 ; sumamos X a comienzo de pantalla
lsl #7,d2 ; un scan de la pantalla tiene 128 bytes
adda.l d2,a0 ; a0 apunta a la coordenada X,Y
move.l d3,d5 ; d5 es backup de d3
move.l a0,d6 ; d6 es backup de a0
PonScanT4
move.l d5,d3
move.l d6,a0
Pon4pixelsT4
move.w (a0),d0 ; D0 contiene el cacho de pantalla que va a usarse
move.w (a1)+,d1 ; D1 contiene el cacho de sprite a mostrar
move.w (a2)+,d2 ; D2 va a ser una máscara para determinar qué bits del sprite se quedan
and.w d2,d1 ; Enmascaramos el sprite
not.w d2 ; Negamos la mascara...
and.w d2,d0 ; ... y la aplicamos a la pantalla
or.w d1,d0 ; juntamos sprite enmascarado con pantalla enmascarada
move.w d0,(a0)+ ; y lo plantamos en la pantalla
dbra d3,Pon4pixelsT4
addi.l #128,d6
dbra d4,PonScanT4
moveq #0,d0
rts
;---------------------------------------------------------------------------------
ClearScreen
;D1 = direccion de comienzo de la pantalla a borrar
move.l #8191,d2
movea.l d1,a0
BucleClear
move.l #0,(a0)+
dbra d2,BucleClear
moveq #0,d0
rts
end 0
Podéis descargar el ejemplo de McLeod, con el código fuente, los binarios, etc, en:
http://www.bytemaniacos.com/ficheros/sinclairql/rutinas/gusano.zip
Contenido del fichero:
- El fichero .C convierte un bitmap de 24 bits en formato RAW a dos ficheros: el sprite y la máscara. En el bitmap original, cualquier pixel que tenga como color el (128,0,128) será tratado como transparente.
- Un sprite de 32x64 píxeles (para que se vea bien en modo en formato PNG
- El mismo sprite, pero en formato RAW
- Los dos ficheros _bin que salen al procesar el RAW por el programa C: el sprite y la máscara.
- Código fuente de las rutinas empleadas.
- Código bnario de dichas rutinas.
- Programa en SuperBASIC que implementa la demo.
Para que el programa funcione bien, la segunda pantalla debe estar disponible. Esto significa que hay que arrancar con Minerva usando F4 (modo 8, doble pantalla). Con la ROM original no se puede, ya que pone las variables del sistema en la pantalla secundaria.
Y para finalizar, un gran aplauso a McLeod por la proeza, ahora ya no hay excusa para programar juegos desde BASIC con una calidad gráfica decente.