Lección 09 – Scroll (1) por tiles

Aventuras en Megadrive: Scroll por Tiles

 

Voy a seguir el mismo esquema que en la entrada anterior y más tarde ampliaré las fantásticas posibilidades que nos da el scroll por tiles. De esta forma si entendiste la entrada de scroll por planos, te será fácil seguir esta entrada.

Un plano se compone de tiles. Por defecto 40×28 tiles visibles, siendo 28 filas y 40 columnas. La Megadrive nos da la posibilidad de hacer scroll por filas o columnas de tiles, que corresponden a las 28 filas horizontales o a las 40 columnas verticales.

 

SCROLL SIMPLE POR TILES

En primer lugar hemos de configurar el tipo de scroll:

VDP_setScrollingMode( scroll_horizontal, scroll_vertical );

Sólo necesitamos hacerlo una vez indicando tipo de scroll horizontal y vertical (usemos uno o ambos). Importante indicar que se aplica a ambos planos A y B. Por ejemplo:

VDP_setScrollingMode( HSCROLL_TILE , VSCROLL_2TILE);

Le indica al SGDK que vamos a mover los planos A y B por tile tanto en la horizontal como en la vertical (y no por plano o línea). De momento vamos a olvidarnos de mezclar distintos tipos de scroll para simplificar.

En segundo lugar, y ya dentro del bucle principal, hemos de decirle al VDP cuánto queremos que se mueva el plano:

VDP_setHorizontalScrollTile(plano, 1erTile, VectorOffset, NTiles, tm);
VDP_setVerticalScrollTile(  plano, 1erTile, VectorOffset, NTiles, tm);

Lo que realmente hace la consola es dibujar el las filas o columnas de TILES del plano desde el punto que nosotros le indiquemos. Por defecto es cero, pero si aumentamos el desplazamiento (scroll offset en inglés), el plano se comienza a dibujar desde dicho punto. Variando el desplazamiento, poco a poco, creamos la ilusión de movimiento del plano o scroll.

A diferencia de la función para mover por plano, las funciones arriba escritas tienen estos otros parámetros:

  • 1erTile: indica el primer tile (la fila) a partir del cual se aplicará scroll.
  • VectorOffset: Vector (tipo s16) con los desplazamientos indicados en PIXELS.
    • Offset positivo: Desplazamiento a dchas.
    • Offset negativo: Desplazamiento a izquierdas.
  • NTiles: Total de tiles a los que se aplica el scroll.
  • tm: Método de transferencia. De momento lo dejamos en CPU.

Como ejemplo, esta imagen, con sus tiles numerados del 0 a 27 en la vertical:

bgb

Si suponemos un scroll por tiles horizontal:

  • Si 1erTile = 18 y NTiles=10, podremos aplicar un scroll a toda la zona del mar. Del tile 18 al tile 27 (inclusive) hay 10 filas de tiles.
  • En cambio, si 1erTile=0 y NTiles=6, moveremos las nubes pero no las montañas.

El resto del escenario no se moverá.

 

El vector VectorOffset debería ser tan grande como filas o columnas queramos mover, cada elemento del vector controla el valor offset para cada dicha fila o columna. Aunque quisiéramos aplicar a todas el mismo valor, hemos de crear el vector con la dimensión correcta y el valor correspondiente.

Por ejemplo, para mover el mar y las nubes:

s16 vector_mar[10] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
s16 vector_nubes[6] = { 0, 0, 0, 0, 0, 0 };

Los valores son 0 porque así lo he querido, significa que se pintarán las filas sin desplazamiento. A medida que cambiemos dichos valores, moveremos=pintaremos las filas con dicho desplazamiento.

Nada nos impide utilizar otros valores inicialmente (distintos a cero) para comenzar a dibujar una pantalla con cierto scroll ya dado.

 

UN EJEMPLO SENCILLO

Vamos a aplicar al plano A scroll horizontal por tiles:

//SCROLL POR TILE (solo en la horizontal)
VDP_setScrollingMode(HSCROLL_TILE, VSCROLL_PLANE);

s16 val1[32] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 
16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 };

//bucle principal
while(1)
{
  //el 4º arg es la long del array
  VDP_setHorizontalScrollTile(PLAN_A, 0, val1, 32, FALSE);

  SPR_update();
  VDP_waitVSync();
}

 

Esto mostrará el fondo con las distintas filas de tiles desplazadas:

  • La primera tal cual está en el plano original.
  • La segunda desplazada un tile.
  • La tercera desplazada dos tiles.
  • Etc.

 

Un momento… ¡Esto no se mueve! Por supuesto, para que se mueva, hemos de variar el valor del vector de desplazamiento. Por ejemplo:

while(1)
{
  VDP_setHorizontalScrollTile(PLAN_A, 0, val1, 32, FALSE);
  for(int i=0;i<32;i++) val1[i]-=1;

  SPR_update();
  VDP_waitVSync();
}

Esto resta ‘1’ a cada valor del vector en cada ciclo, moviendo las filas de tiles hacia la izquierda.

 

SCROLL = PINTAR con desplazamientos

El scroll por tiles nos permite hacer scroll = pintar cada fila o columna con un offset diferente. Por tanto y a diferencia del scroll por plano, podemos desplazar unas filas más rápidas que otras. O bien desplazar algunas, y otras dejarlas quietas. O desplazar algunas en un sentido, y otras en sentido inverso.

Al igual que en el scroll con planos, si al dibujar se alcanza el final, se sigue pintando desde el principio de la imagen, aunque en este caso por filas o columnas. Es una técnica habitual de la época para simular planos sin fin.

Si movemos todos los tiles de un plano a la misma velocidad,
obviamente scroll por tiles = scroll por plano.

Si ponemos un sprite en pantalla éste no se moverá junto con el scroll. Recordemos que el plano de sprites es independiente de los planos de scroll y además es un plano sin scroll. Por tanto mover un sprite a la vez que se hace scroll requiere cierta planificación, lo veremos en una futura entrada. De momento nos centraremos en trabajar con los planos y no con los sprites.

 

UN EJEMPLO COMPLETO

Para muestra el ejemplo sobre scroll por tiles que puedes encontrar en mi github.

En este ejemplo esto es lo que nos vamos a encontrar:

  • Ejemplo 01: Plano A estático. Plano B: movemos las 6 primeras filas (velocidad constante).
    s16 vector01[6] = {1,2,3,4,5,6};
    
    while(1){
      VDP_setHorizontalScrollTile(PLAN_B, 0, vector01, 6, CPU);
      for(i=0; i<6; i++) vector01[i] = vector01[i]+1; 
    
    }

Captura018

Las filas las hemos separado inicialmente por 1 pixel (con vector01). Por tanto se pintan así en el primer frame, después las “movemos” a la misma velocidad, 1 pixel por ciclo ( vector01[i] = vector01[i]+1).

video13

 

  • Ejemplo 02: Mismo caso pero el vector inicial es distinto.
    s16 vector02[6] = {0,10,20,30,40,50};

    En el anterior ejemplo no se aprecia nada raro en las nubes a pesar de que hemos desplazado cada fila de tiles 1 pixel respecto a la anterior. Esto es así porque a pesar del desplazamiento, éste es pequeño y las nubes tienen formas irregulares, por tanto no se aprecia deformidad alguna. Pero en este caso, el desplazamiento son 10 pixels, y sí que se nota:

 

Captura019

 

Por tanto hemos de tener cuidado al aplicar este tipo de scroll porque podemos crear efectos no deseados.

 

  • Ejemplo 03: Mismo caso pero con distinta velocidad por fila.
    VDP_setHorizontalScrollTile(PLAN_B, 0, vector02, 6, CPU);
    for(i = 0; i < 6; i++) vector02[i] = vector02[i]+i;

La diferencia es obvia, cada elemento de vector02 va a moverse a una velocidad distinta. Estéticamente es horrible pero de lo que se trata es de aprender que podemos utilizar el scroll por tiles de diferentes formas:

video14

Como curiosidad, el primer elemento del vector siempre suma ‘i=0’, porque es la primera iteración del bucle for. Por tanto dicha fila no se moverá.

Por el mismo razonamiento, la última fila (i=5) será la más rápida.

 

  • Ejemplo 04: Mismo caso usando un vector de aceleración. utilizamos un vector para decirle a la negrita cuánto debe mover cada fila de tiles. Totalmente a medida.
    s16 v_aceleracion01[6] = {1,2,3,3,2,1};
    [..]
    VDP_setHorizontalScrollTile(PLAN_B, 0, vector02, 6, CPU);
    for(i = 0; i < 6; i++) vector02[i] += v_aceleracion01[i];
    

     

video15

En este caso, algunos tiles se mueven 1 pixels/ciclo, otros 2 pixels/ciclo y los del centro se mueven a 3 pixels/ciclo.

 

  • Ejemplo 05: Voy a aplicar el scroll por tiles en dos zonas diferentes de la pantalla: el cielo y el mar. Además lo haremos en sentido inverso. Es muy fácil:
s16 v_aceleracion02[10]= {1,2,3,4,5,6,7,8,9,10};
[..]
//nubes
VDP_setHorizontalScrollTile(PLAN_B, 0, vector01, 6, CPU);
for(int i = 0; i < 6; i++) vector01[i] += 2;
//mar
VDP_setHorizontalScrollTile(PLAN_B, 18, vector04, 10, CPU);
for(int i = 0; i < 10; i++) vector04[i] -= v_aceleracion02[i];

video16

El resultado es espectacular (el GIF no le hace justicia). Nótese como hemos utilizado una velocidad constante en el cielo, y una velocidad diferente para cada fila de tiles en el mar.

 

AÑADIENDO SPRITES Y DECIMALES

  • Ejemplo 06: Mismo caso de antes pero añadimos un sprite (Sonic), que permanece quieto sobre su posición inicial. Vamos a mover el plano A (suelo y palmeras) de forma sincronizada con los controles de Sonic. De forma que si movemos a Sonic movemos el fondo de palmeras, imitando un scroll a pantalla completa.
//nubes
VDP_setHorizontalScrollTile(PLAN_B, 0, vector01, 6, CPU);
for(int i = 0; i < 6; i++) vector01[i] -= 2;
//mar
VDP_setHorizontalScrollTile(PLAN_B, 18, vector03, 10, CPU);
for(int i = 0; i < 10; i++) vector03[i] -= v_aceleracion02[i];
//plano A
VDP_setHorizontalScrollTile(PLAN_A, 0, vector04, 28, CPU);
for(int i = 0; i < 28; i++) vector04[i] = offset_H_PlanoA;

Para cada ciclo, actualizamos los vectores vector01 y vector03 para ir pintando nubes y mar, tal y como hicimos en el ejemplo anterior.

El plano A lo vamos pintando dependiendo del vector04, que a su vez actualiza sus valores en función de la variable offset_H_PlanoA. ¿Y de dónde obtiene esta variable sus valores? De la entrada del PAD, según movamos a Sonic:

//inicio del programa, declaración de variables globales
//desplazamiento respecto al punto (0,0) de los planos
s16 offset = 0;
//para ir incrementando el movimiento de los planos
s16 aceleracionA = 6;

[..]

static void handleInput()
{
  u16 value = JOY_readJoypad(JOY_1);
  [..]
  //plano
  if (value & BUTTON_LEFT)
  {
    offset += aceleracionPlanoA; 
    SPR_setAnim(Sonic, ANIM_RUN); SPR_setHFlip(Sonic, TRUE);
  }
  if(value & BUTTON_RIGHT)
  {
    offset -= aceleracionPlanoA; 
    SPR_setAnim(Sonic, ANIM_RUN); SPR_setHFlip(Sonic, FALSE);    
  }

}

 

Ahora vamos a ver el código en acción:

video18

 

Suave, rápido y con un scroll muy conseguido (el gif no lo refleja, compila y ejecuta en tu consola o emulador favorito).

 

LET’S DO A SHOOT ‘EM UP

 

  • Ejemplo 07: Vamos a imitar la fase inicial del shmup Gleylancer.

En el plano B tendremos un fondo con estrellas y, fuera de la zona visible, un planeta. El plano se irá desplazando lentamente hacia la izquierda, para detenerse cuando el planeta alcance la mitad de la pantalla.

En el plano A tendremos una tormenta de asteroides. Los asteroides situados en los extremos sup e inferior se mueven a gran velocidad, siendo los situados en el centro los que se mueven más lentamente.

En el plano de sprites la nave y los enemigos, que se mueven en formación desde la derecha hacia la izquierda.

Para el plano B tenemos un problema. Si usamos s16 para el vector de offsets, el valor mínimo para un elemento del vector es 1, moveremos muy rápido el plano. Sin embargo hemos dicho que VDP_setHorizontalScrollTile necesita como tercer parámetro un vector s16 para los desplazamientos, ¿cómo lo solucionamos?
Con decimales y un vector auxiliar.

 

UTILIZANDO FIX16 PARA EL SCROLL

El procesador Motorola 68K no soporta números en coma flotante (simplificando, con decimales). No obstante el SGDK soporta variables y cálculos en punto fijo por si queremos usar decimales. Existen dos tipos llamados fix16 y fix32 que internamente usan s16 (fix16) y s32 (fix32) para simular 2 o 3 decimales respectivamente. Estas ops son especialmente lentas por tanto conviene no abusar de ellas. Ademas, existen pérdidas de precisión debido al redondeo.

    • fix16 : rango 16bits (signed,short =1 byte) -512.00 –> 511.00
    • fix32 : rango 32bits (signed,longs =2 bytes) -2097152.000 –> 2097151.000

La idea aquí es utilizar fix16 en vez de s16 para controlar el scroll. De esta forma, si suponemos una aceleración menor de 1 (p.e. 0.1 o 0.01), la velocidad del scroll pasará a ser algo más controlable.

Recordar que si queremos hacer debug sobre un fix16 tendremos que usar KLog_f1

¿Cómo pasamos los valores fix16 a VDP_setHorizontalScrollTile? Utilizando un vector auxiliar de tipo fix16 para los cálculos, y utilizando fix16ToInt para transformar un valor decimal en un valor sin coma flotante.

El código sería éste en vez del anterior:

//inicio del programa, declaración de variables globales
//para el plano A (asteroides)
s16 v_aceleracion03[28] ={9,9,8,8, 7,6,6,6, 5,5,4,3, 2,1,2,3, 4,5,5,6, 6,6,7,7, 8,8,9,9};
//para el plano B (planeta)
fix16 aceleracion04 = FIX16(0.06);
fix16 vector_aux[28]={ 0, 0, 0, ... 0 };
[..]

//plano B: scroll hasta que el planeta llega a mitad de pantalla aprox
if( vector05[0]>=(-200) )
{
  VDP_setHorizontalScrollTile(PLAN_B, 0, vector05, 28, CPU);
  for(int i = 0; i < 28; i++){
    vector_aux[i] = fix16Sub(vector_aux[i],aceleracion04);
    vector05[i]   = fix16ToInt(vector_aux[i]);
  }
}

//plano A
VDP_setHorizontalScrollTile(PLAN_A, 0, vector06, 28, CPU);
for(int i = 0; i < 28; i++) vector06[i] -= v_aceleracion03[i]

[..]

//PAD
if (value & BUTTON_LEFT)
{
  //plano  (fix16Add equivale a += )
  offsetA = fix16Add(offsetA, aceleracionA);
  //sprite
  SPR_setAnim(Sonic, ANIM_RUN); SPR_setHFlip(Sonic, TRUE);
}
if(value & BUTTON_RIGHT)
{
  //plano  (fix16Sub equivale a -= )
  offsetA = fix16Sub(offsetA, aceleracionA);
  //sprite
  SPR_setAnim(Sonic, ANIM_RUN); SPR_setHFlip(Sonic, FALSE);
}

 

Debido a las pérdidas por redondeo, hemos de probar empíricamente los valores a elegir para las variables fix16/32. Por ejemplo en el ejemplo actual si aceleracionA=FIX(0.01) no veremos moverse el scroll, pues el redondeo hará que aceleracionA=0.

Éste es el resultado:

 

video19

 

 

En éste último ejemplo uso VDP_drawTextBG para escribir en el plano B en vez de VDP_drawText, que escribe por defecto en el plano A. De no hacerlo así, tendremos un texto moviéndose a toda velocidad junto con los asteroides.

 

¿Y EL SCROLL VERTICAL POR TILES?

Ejemplo 08: Me he centrado tanto en el scroll horizontal, el más habitual, que he olvidado el vertical. En la entrada anterior, scroll por planos, explicamos que el scroll horizontal y vertical funciona de la misma manera. El scroll por tiles NO funciona igual en la vertical.

En vez de mover filas de tiles (siendo una fila 8 pixels), en la vertical moveremos bloques de 2 tiles. Por tanto cada columna serán 16 pixels. Éste es el funcionamiento interno del HW de la megadrive y es lo que nos da el SGDK.

Para entenderlo mejor tenemos esta imagen:

bgv

Se trata de una imagen similar a la del anterior ejemplo (320x256px), he numerado la primera fila de tiles, coloreándolos de 2 en 2, alternando texto blanco y amarillo.

Son 40 columnas. Al mover la megadrive las columnas de 2 en 2, nos hace falta un vector de 20 entradas:

s16 vector07[20] ={0,0,0,0,0, 0,0,0,0,0, 0,0,0,0,0, 0,0,0,0,0};

Y a su vez vamos a moverlo con un vector de aceleración, de 20 entradas también:

s16 v_aceleracion04[20] ={10,9,8,5,6, 5,4,3,2,1, 1,2,3,4,5, 6,7,8,9,10};

Para configurar el scroll vertical por tiles, hemos de hacer:

 //Configura el scroll (por TILES vertical, el horizontal no lo uso)
VDP_setScrollingMode(HSCROLL_PLANE , VSCROLL_2TILE);

Y finalmente lo movemos así:

 //plano A
VDP_setVerticalScrollTile(PLAN_A, 0, vector07, 20, CPU);
for(int i = 0; i < 20; i++) vector07[i] -= v_aceleracion04[i];

 

Nótese que no he tenido ningún cuidado al asignar las velocidades de cada columna, la idea es ver como afecta al fondo:

captura64

 

Como se puede ver, la mega va cogiendo las columnas de 2 en 2. El efecto es el de siempre, pero si no tenemos cuidado al asignar la velocidad con los fondos, puede quedar todo descuadrado, como es el caso.

Para que quedase bien, debería haber asignado en v_aceleracion04 la misma velocidad a las columnas que compartan elementos, teniendo en cuenta que van de 2 en 2. En esta imagen, con una máscara de 16x16px, se ve mejor:

captura65

Por ejemplo, las columnas 1 y 2 (que corresponden a los tiles 1,2,3 y 4), deberían tener la misma velocidad para que no se partan los asteroides grandes.

La columna 3 (que corresponde a los tiles 5 y 6) es indiferente, no hay nada en dichas columnas.

La columna 4 con una velocidad (tiles 7 y 8), la columna 5 con otra (tiles 9 y 10), etc.

Puedes ver todo esto en el ejemplo de esta entrada (ejemplo 08).

 

GITHUB

El código de esta lección, y de todas las demás, lo podrás encontrar en mi github:

https://github.com/danibusvlc/aventuras-en-megadrive