Aventuras en Megadrive:
Scroll por Líneas
Voy a seguir el mismo esquema que en las entradas anteriores, vamos a ver las nuevas posibilidades que nos da el scroll por líneas. De esta forma si entendiste las entradas de scroll por plano y tile, te será fácil seguir esta entrada.
Un plano se puede descomponer en líneas horizontales. Por defecto la resolución son 320×224 px, es decir, 224 líneas. La Megadrive nos da la posibilidad de hacer scroll por líneas horizontales, pero NO por líneas verticales.
El scroll por línea también es conocido como scroll por píxel, ya que desplazamos líneas de 1 píxel, a diferencia de las filas de 8 píxels (1 tile) de la entrada anterior.
SCROLL SIMPLE POR LÍNEAS
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. El vertical puede ser diferente (por plano o por tile, pero no por línea).
Importante indicar que se aplica a ambos planos A y B. Por ejemplo:
VDP_setScrollingMode( HSCROLL_LINE, VSCROLL_PLANE);
Le indica al SGDK que vamos a mover los planos A y B por línea en la horizontal y por plano en la vertical. 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_setHorizontalScrollLine(plano, 1eraLinea, VectorOffset, NLineas, tm);
Lo que realmente hace la consola es dibujar cada línea 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 o tile, la función arriba escrita tiene estos otros parámetros:
- 1eraLinea: indica la línea a partir de la cual se aplicará scroll.
- VectorOffset: Vector (tipo s16) con los desplazamientos indicados en PIXELS.
- Offset positivo: Desplazamiento a dchas.
- Offset negativo: Desplazamiento a izquierdas.
- NLineas: Total de líneas a las que se aplica el scroll.
- tm: Método de transferencia. De momento lo dejamos en CPU.
Si suponemos un scroll por líneas horizontal:
- Si 1eraLinea= 0 y NLineas=224, estamos aplicando scroll a toda la pantalla (suponiendo una resolución de 320x224px).
- Si en vez de lo anterior, ponemos 1eraLinea= 184 y NLineas=40, moveremos sólo dicha zona de la pantalla, es lo que se hace p.e. con el suelo de juegos como Street Fighter 2. El resto del escenario no se movería.
El vector VectorOffset debería ser tan grande como líneas queramos mover, cada elemento del vector controla el valor offset para cada dicha línea.
Aunque queramos aplicar a todas el mismo valor, hemos de crear el vector con la dimensión correcta y el valor correspondiente. Tal y como hicimos en las entradas anteriores, en la práctica sería como si tuviésemos scroll horizontal por plano
Si tenemos un marcador en la zona superior, podemos evitar moverlo junto con el resto del plano haciendo que 1eraLinea=X (siendo X la línea a partir de la cual ya no hay marcador) y NLineas = 224 – lineas_que_ocupa_el_marcador.
UN EJEMPLO SENCILLO
Cogemos el ejemplo de Sonic de entradas anteriores, moveremos el plano A completo y, al mismo tiempo, en el plano B moveremos las zonas de nubes y agua:
//SCROLL POR LINEA (solo en la horizontal) VDP_setScrollingMode(HSCROLL_LINE, VSCROLL_PLANE); //Declaración de variables //ejemplo 01 s16 offsetA = 0; //desplazamiento plano A s16 vectorA[224]; //todo el plano A s16 vectorB1[50]; //de y=0 a y=50 s16 vectorB2[80]; //de y=144 a y=224 fix16 vectorB2_aux[80]; //vector auxiliar fix16 vectorB2_ace = FIX16(0.2); //constante para la aceleracion [..] //Inicialización de variables //pone a 0 todos los valores offsetA = 0; for(int i = 0; i < 224; i++) vectorA[i] = 0; for(int i = 0; i < 50; i++) vectorB1[i] = 0; for(int i = 0; i < 80; i++) vectorB2[i] = 0; for(int i = 0; i < 80; i++) vectorB2_aux[i]= FIX16(0); [..] //FUNCION CAMARA (se llama desde el loop ppal) //plano A VDP_setHorizontalScrollLine(PLAN_A, 0, vectorA, 224, CPU); for(int i = 0; i < 224; i++) vectorA[i] = offsetA; offsetA--; //plano B: nubes VDP_setHorizontalScrollLine(PLAN_B, 0, vectorB1, 50, CPU); for(int i = 0; i < 50; i++) vectorB1[i] -= 2; //plano B: mar VDP_setHorizontalScrollLine(PLAN_B,144, vectorB2, 80, CPU); for(int i = 0; i < 80; i++){ vectorB2_aux[i] = fix16Sub(vectorB2_aux[i],vectorB2_ace+i); vectorB2[i] = fix16ToInt(vectorB2_aux[i]); [..]
El plano A se mueve completo. Todas las líneas a la vez = mismo scroll para todas. Al ser el plano completo, 224 líneas, necesitamos un vector con dicho tamaño.
El plano B tiene dos zonas a mover: las nubes, cuyas líneas se mueven todas a la vez, y la zona del agua, donde cada línea lleva una velocidad distinta. El resto del plano, la parte central de las montañas, no se mueve.
Como en la entrada anterior, usar enteros (s16) para mover el agua no sería buena idea, ya que sumando el mínimo posible de un s16 (que es 1), y para cada línea (el agua son 80 líneas aprox) daría un desplazamiento para la línea inferior de 80px cada ciclo… si hacemos 60 ciclos por seg… más que un mar parecerá una discoteca. Por ello de nuevo usamos fix16 para poder sumar decimales e ir más lentamente.
Este ejemplo podrás encontrarlo como ejemplo 01 en esta entrada en github.
STREET FIGHTER FLOOR
Vamos a utilizar el scroll por líneas para crear el movimiento del suelo tan característico que podemos ver en Street Fighter 2. Éste es el ejemplo 2.
En ningún caso me propongo hacer un pixel perfect, simplemente voy a utilizar las herramientas que me da el SGDK para simularlo. Será Ryu, en su escenario clásico, quien nos ilustre, veamos este GIF de una entrada anterior (sin scroll):
Voy a aprovechar el HW de la Mega para crear el escenario y su scroll:
- En el plano B situaremos el fondo lejano (la luna y la silueta de la pagoda). Si Ryu camina hacia la derecha, el plano B se moverá lentamente a la izquierda (todas sus líneas a la vez), y viceversa. Por tanto usaremos un vector fix16 para usar decimales y moverlo muy despacio.
- En el plano A situamos tanto el suelo como la pagoda cercana y el tejado. Moveremos de forma diferente el suelo del resto del plano. Esta es la magia del scroll por líneas:
- De la línea 0 a la 179, moveremos todas las líneas a la vez. Ligeramente más rápido que el plano B, en este caso moveremos de 1 px en 1 px. Nos basta un vector s16. Recordemos: Tejado, la pagoda cercana y tejado.
- De la línea 180 a 219, moveremos el suelo. Cada línea se moverá ligeramente más deprisa que la anterior, siendo la más inferior la que más rápido se mueve. Usaremos un vector fix16 para ir actualizando los resultados y un vector fix16 fijo que contiene cuanto se puede mover cada línea cada vez, el vector de aceleración. Además nos hará falta otro vector s16, que iremos actualizando cada ciclo, necesario para pasarle los datos a la función VDP_setHorizontalScrollLine, ya que sólo admite enteros.
- De la línea 220 a la 224, nada se hace, es un fondo negro.
Aquí se ve mucho mejor:
Ahora bien, hay otro problema a considerar. El escenario es más grande que la parte visible, lógico, a ambos lados hay tejados, fondo lejano y el suelo. En concreto las imágenes de fondo tienen el tamaño máx. habitual que es 512x224px:
El problema mencionado es colocar la cámara ( y el sprite ) aprox en el medio, de forma que cuando movamos el sprite movamos también el escenario y el suelo. Hasta ahora siempre partíamos de la izquierda de un escenario, offset=0, para movernos hacia la derecha, y en ese momento se hacía scroll. Ahora debemos centrar la «cámara» en el centro, y hacer scroll al movernos a ambos lados.
La solución es sencilla, dar un valor inicial a los vectores que afectan a VDP_setHorizontalScrollLine para que directamente se aplique un determinado scroll en el primer ciclo del juego, un valor fijo tanto para todo (plano A y plano B, suelo y tejados), es cuestión del juego ir probando hasta dar con el valor más adecuado.
En el caso que nos ocupa, el valor es 96 (sacado a ojo, pégale un vistazo al código).
Por otro lado, nos cuidaremos de no permitir scroll a partir de cierto punto, a ambos lados del escenario, para que no se mueva el escenario indefinidamente y rote sobre sí mismo (roll en inglés).
Este es el resultado:
Fíjate como, al llegar al tope derecho o izquierdo, el personaje sigue con su animación de movimiento, pero el scroll se detiene (tal y como sucede en el juego).
La parte superior del suelo se mueve al mismo ritmo que la parte superior del plano A, y a partir de ahí, hacia abajo, el suelo se mueve cada vez a un ritmo mayor.
No es exactamente como en el juego real, pero se parece bastante.
SEGA LOGO: Deformando un fondo
Vamos con el ejemplo 3. Si habéis seguido estas entradas en orden, habréis visto que ya trabajamos en el scroll por líneas en la entrada de Castlevania. He aprovechado el mismo código para hacer un ejemplo de deformación de un plano. En concreto voy a deformar el logo de Sega, que voy a situar en el plano A.
La explicación al código lo podéis encontrar en la entrada de Castlevania, como siempre utilizo 3 vectores:
- fix16 line_scroll_data[]: valor actual de scroll para cada línea.
- fix16 line_speed_data[]: valor de aceleración para cada línea. Como en el suelo del SF2, va aumentando el valor con cada línea.
- s16 aux[]: Valor de scroll en enteros, para poder aplicarlo en la función VDP_setHorizontalScrollLine.
Recomiendo ver la explicación detallada en la entrada correspondiente. La hice antes de llegar a aprender sobre scroll por planos y por tiles, pero se parece bastante a lo visto en esta entrada.
La idea es que podemos deformar cualquier fondo de distintas maneras para obtener efectos sorprendentes. Por ejemplo, el scroll por líneas se puede usar, junto con otras técnicas, para deformar la carretera de un juego de coches. En memoria tenemos una carretera recta y la vamos deformando para simular curvas.
También podemos simular llamas o agua en movimiento. Depende del artista que llevamos dentro. Puedes ver este tipo de llamas en Thunder Force 3 o en Golden Axe 2.
SEGA LOGO: Separación por líneas
Cuarto y último ejemplo. Un efecto muy chulo es separar las líneas pares e impares para luego juntarlas. Esto se ha hecho en infinidad de juegos. Haremos lo mismo con el logo de Sega. Es muy sencillo. Utilizando la misma lógica que en los ejemplos anteriores, tendremos 3 vectores:
- s16 vSeparacion[224] : Aquí se guarda el valor entero de scroll.
- fix16 vSeparacion_ace[224] : Aquí se guarda el valor de aceleración de cada línea. Será un valor fix16 (en el ejemplo 0.05) para líneas pares, el mismo valor negativo para las líneas impares.
- fix16 vSeparacion_aux[224] : Aquí se guarda el valor fix16 del scroll y es el vector con el que trabajamos.
Como lo aplico a todo el plano, uso 224 posiciones para todos los vectores.
Y este es el resultado final, todos los ejemplos, incluido la separación por líneas:
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