Lección 11 – Escenarios Interactivos

 

Si queremos crear estructuras o personajes enormes, nos veremos en el dilema de utilizar múltiples sprites o utilizar un fondo para ello.

Usar sprites es más sencillo: animaciones, gestión de colisiones, mover sprites por el escenario… usar sprites parece lógico, pero esto implica una serie de problemas:

  • Gestionar múltiples sprites, habrá que hacerlo de forma manual.
  • Usar un gran número de sprites implica no poder usar sprites para otras cosas.
  • Sobrepasar el máx. de sprites por línea (20 al usar el modo 320px): parpadeos.

Usar un fondo es el otro camino. ¿Podemos crear elementos destructibles usando un fondo? ¿Cómo interactuamos con un conjunto de tiles de un fondo? ¿Cómo actualizamos el fondo? ¿Cómo se gestionan las colisiones? Vamos a verlo.

 

ESCENARIOS INTERACTIVOS

Vamos a utilizar un plano para pintar un elemento. Normalmente se hace para crear una estructura o un Boss de tamaño considerable. Los tiles de un plano no tienen animaciones, como sucede con un sprite, así que para actualizar su aspecto hay que a gestionar los tiles del elemento en cuestión.

La idea es pintar inicialmente el plano completo, con VDP_drawImageEx() y, cuando nos interese, utilizar VDP_setMapEx() para repintar sólo las zonas que van a cambiar.

Podríamos utilizar VDP_drawImageEx() para pintar el plano completo, esto implica tener una pantalla completa distinta por cada variación posible del elemento, un gasto absurdo de ROM. Y es una función lenta, puede ralentizarnos el juego.

Es mucho más inteligente guardar las posibles variaciones del elemento en un archivo y cargar+repintar solo las partes que nos interese.

 

LAS DOS TORRES

Vamos a pintar una torre destructible en el plano A. Tiene unas dimensiones de 72x88px (9×11 tiles). En el plano B pintaremos un fondo.

Vamos a usar las torres y el fondo de la fase 1-1 de «Hijos de Camelot» para Android.
Si recordáis esta entrada del blog, hicimos un análisis rápido de esta fase para crear el  port para la negrita: Un fondo en el plano B y las torres, por delante, en el plano A.

 

Y tenemos no una, si no dos torres en pantalla, situadas a ambos lados. Tener dos torres es otro buen motivo para no usar sprites, pues al meter enemigos, disparos, power ups y las torres, de usar sprites superaremos el máx. de sprites por línea en algunos momentos y tendremos un festival de parpadeos.

Ambas torres pueden ser dañadas hasta ser destruidas, a medida que reciben daños van cambiado su estado.

La torre se guarda en un sólo archivo PNG con sus 3 estados:

fase01_01_torres

Cuando una de las torres reciba suficiente daño, usaremos VDP_setMapEx() para re-pintar dicha zona del plano A, borrando lo que había antes. Los tiles se repintan completos, incluso aquellos con color transparente borran los que estaban anteriormente en memoria.

Vamos a ver primero como mostrar daños sobre las torres y luego como re-pintarlas.

 

INTERACCIÓN: COLISIONES

¿Cómo gestiona cada torre los disparos del player? ¿Cómo hacemos para que el player entienda que efectivamente sus acciones tienen consecuencias?

Para gestionar las colisiones tendremos que comparar la posición de la bala del player con la zona de colisión de cada torre. Para una gestión más sencilla, la zona colisionable será un rectángulo y guardaremos sus coordenadas en la estructura de la Torre:

struct { 
  int vida;           //puntos vida
  int estado;         //estado (0=inicial, 1=semidestruida, 2= cimientos)
  s16 colX1,colY1;    //caja colision torre (punto sup izq)
  s16 colX2,colY2;    //caja colision torre (punto inf dch)
  s16 temporizador;   //para animaciones ataque y muerte
  s16 activa_impacto; //la torre está siendo disparada
  s16 t_impacto;      //temporizador para los impactos
}Torre[2];            //hay 2 torres

 

Y este es el código donde compruebo la colisión:

//DETECCIÓN DE IMPACTO EN LAS TORRES
for(s16 cont=0; cont<MAX_TORRES; cont++)
{
  if(Torre[cont].vida>0) //la torre está viva+disparo en zona de impacto
  {
    if(DisparoPlayer.x > Torre[cont].colX1 && DisparoPlayer.x < Torre[cont].colX2 &&
    DisparoPlayer.y > Torre[cont].colY1 && DisparoPlayer.y < Torre[cont].colY2 )
    {
      Torre[cont].vida--;
      Torre[cont].activa_impacto++;
    }
  }
}
  • DisparoPlayer.x+DisparoPlayer.y : punto donde disparamos.
  • Torre[cont].colX1 – colX2 – colY1 – colY2 : zona de impacto de la torre
  • Cuando la torre recibe un impacto, pierde vida y activa_impacto pasa de 0 a 1.
  • activa_impacto activa la interacción deseada. Lo vemos en los sig. apartados.

 

INTERACCIÓN: CAMBIO DE PALETA

La idea es pintar la torre dañada temporalmente con una paleta de un color llamativo. Podemos hacerlo con una paleta de colores rojos. Pasado un tiempo, volvemos a pintar la torre con su paleta original.

En este ejemplo, las torres utilizan la PAL0 en su estado original. Antes de iniciar la fase asigno la paleta de rojos del SGDK a una paleta sin usar (en este caso la PAL3):

VDP_setPalette(PAL3, palette_red);

Cuando la torre recibe un impacto, pierde vida y activa_impacto pasa de 0 a 1.
Ahora que activa_impacto es distinto de cero, empiezo a incrementar un contador, t_impacto. Con el valor ‘1’ pinto la torre de rojo, con PAL3 (lo hago así para pintar la torre de rojo una sola vez y no estar usando VDP_setMapEx() constantemente).

Cuando t_impacto supere un valor determinado, en mi caso la constante T_TINTE, se resetean los contadores y se vuelve a pintar la torre con la paleta original, PAL0.

Este es el código que uso, tendrás que adaptarlo a tu juego:

if(Torre[0].activa_impacto!=0)
{
  Torre[0].t_impacto++;  //contador
    if(Torre[0].t_impacto==1) //pinta de rojo, sólo con este valor
    {
      VDP_setMapEx(PLAN_A, Game.Estructura1, TILE_ATTR_FULL(PAL3, TRUE, FALSE, FALSE, Game.id_tile_final_planB), -1, 3, 0, 0, 10, 11);
    }else //resto de valores hasta que t_impacto...
      if(Torre[0].t_impacto>=T_TINTE) //...alcance este valor
      {
        Torre[0].activa_impacto = 0; //reseteo de contador y detector
        Torre[0].t_impacto = 0;      //de impacto, solo falta re-pintar
        VDP_setMapEx(PLAN_A, Game.Estructura1, TILE_ATTR_FULL(PAL0, TRUE, FALSE, FALSE, Game.id_tile_final_planB), -1, 3, 0, 0, 10, 11);
      }
}

 

Este es el resultado, funciona muy bien:

video27

 

No obstante hay un inconveniente muy grande. Estamos utilizando una paleta completa sólo para mostrar daños al jugador, y la Mega no va sobrada de colores.

 

INTERACCIÓN: CAMBIO DE PALETA COMPLETA (II)

Este caso es similar al anterior, la diferencia es que usaremos una paleta que ya esté en uso para representar los daños, de esta forma no perdemos ninguna paleta. Seguro que hemos visto juegos donde al darle a un boss este cambia brevemente a colores raros, por así decirlo. Es precisamente esta técnica. Veamos que ocurre si utilizo PAL2:

video26

 

Como se puede ver, la torre se pinta en tonos verdes, ¿por qué? Veamos el VDP

captura67

PAL0 y PAL1 las uso para escenario(+torres) y player respectivamente, la PAL2 y la PAL3 hasta este momento no las estoy usando (y lo haré, me falta el marcador y alguna cosilla más), en este caso el SGDK las inicializa por defecto (verdes y azules).

No importa, puede quedar mejor o peor pero si se hace rápido… da el pego.

 

 

INTERACCIÓN: CAMBIADO UN SOLO COLOR

Hay soluciones intermedias para no usar una paleta completa. Por ejemplo cambiar un color de la paleta, de forma temporal, y luego restaurarlo. De nuevo, se activa cuando activa_impacto es distinto de 0:

if(Torre[0].activa_impacto!=0)
{
  Torre[0].t_impacto++;
    if(Torre[0].t_impacto==1) {
      VDP_setPaletteColor(9,RGB24_TO_VDPCOLOR(0x0098e5));
    }else 
    if(Torre[0].t_impacto>=T_TINTE)
    {
      VDP_setPaletteColor(9,RGB24_TO_VDPCOLOR(0x4c260c));
      Torre[0].activa_impacto = 0;
      Torre[0].t_impacto = 0;
    }
}

Lo que hago es cambiar el color 9 de la paleta, que es el marrón oscuro de las torres, a otro color utilizando VDP_setPaletteColor(). En este caso un color azul, para variar.

De nuevo cuando el contador supere un determinado valor, devuelvo el color original.

Si no recuerdas como usar esta función, repasa esta lección.

video28

 

Como ves funciona, pero esto nos lleva a un problema secundario, si la paleta es compartida por otros elementos, ya sean sprites o tiles del escenario, también ellos cambiarán de color si usan precisamente ese color. Así que hay que ser muy cuidadoso, una mala planificación con los colores nos lleva al desastre.

En este caso tanto el escenario como las torres están pintadas con la misma paleta y el color 9 se usa ampliamente en todos ellos. Por tanto cambiar dicho color es hacerlo en todos estos elementos, pues se re-pintan cada ciclo por el VDP.

Con una buena planificación hubiese reservado ese color sólo para las torres, o mejor, un color para la torre izquierda y otro para la torre derecha (aunque sea el mismo color), para evitar que parpadeen a la vez, al ser independientes. Gastamos dos colores pero mejor eso que una paleta entera.

Hay juegos que utilizan esta técnica para los jefes finales, «parpadea» todo el escenario y es precisamente lo que busca el programador, llamar la atención e indicar al player que sus acciones hacen daño al boss.

 

 

INTERACCIÓN: PARPADEO ( BLINK )

Otra forma de representar daño es hacer parpadear las torres, es decir, al recibir impacto alternar frames donde pintamos la torre con frames donde no la pinto.

Si las torres fuesen sprites, sería muy sencillo. El SGDK nos da esta función:

SPR_setVisibility(sprite, value)  //value = VISIBLE | HIDDEN

El secreto es cambiar entre VISIBLE y HIDDEN cada 2 o 3 frames, es lo ideal para 60/50Hz.

Ahora bien, en mi caso las torres son parte de un plano, no son sprites, por tanto no podemos usar dicha función, no obstante podemos usar estas otras funciones:

Borrar un plano:

VDP_clearPlan(plan, TRUE | FALSE); //borra un plano completo

Podemos borrar el plano A completo, ¡pero esto borra ambas torres! Así que tras borrar el plano, hay que repintar la otra torre.

En el caso de tener un sólo enemigo gigante, esta función es buena opción para hacerlo parpadear.

 

Borrar parte de un plano:

VDP_clearTileMapRect(plan,x,y,w,h) //(x,y)=posición,(w,h)=alto,ancho(TILES)

Hay un problema. En los frames donde no pintemos la torre, se verá el sprite del arquero que está detrás de la torre (fíjate en la animación del principio de la entrada). Puede quedar bien o no, dependiendo de nuestro juego.

No obstante, ahora sí, podemos usar la función SPR_setVisibility() (mencionada antes) para hacer invisible el sprite al borrar la torre:

if(Torre[0].activa_impacto!=0)
{
  Torre[0].t_impacto++;
  if(Torre[0].t_impacto==1)
  {
    VDP_clearTileMapRect(PLAN_A,0,3,10,11);             //borra plano
    SPR_setVisibility( Arquero[0].spr_arquero, HIDDEN );//esconde sprite
  }else
    if(Torre[0].t_impacto>=T_TINTE)
    { //reseteo
      Torre[0].activa_impacto = 0;
      Torre[0].t_impacto = 0;
      //repinta plano y muestra sprite
      VDP_setMapEx(PLAN_A, Game.Estructura1, TILE_ATTR_FULL(PAL0, TRUE, FALSE, FALSE, Game.id_tile_final_planB), -1, 3, 0, 0, 10, 11);
      SPR_setVisibility( Arquero[0].spr_arquero, VISIBLE );
    }
  }
}

 

Y éste es el resultado:

video29

 

Dependiendo de lo que busquemos, será o no una opción a tener en cuenta.

 

INTERACCIÓN: TORRES DESTRUCTIBLES

Tal y como explicamos al principio de la entrada, la idea es substituir la torre por su versión dañada al recibir daño, y más adelante volverlo a hacer para mostrar los cimientos.

fase01_01_torres

En mi caso cada torre comienza con el estado 0 (gráfico original). Cuando reciba daños y su vida sea inferior a 40 :

  • Su estado cambia a 1 ( de esta forma NO vuelve a entrar en éste IF).
  • Descomprimimos el Map (si en el RES lo tenemos como compresión BEST es recomendable).
  • Cargamos el tileset en memoria del VDP.
  • Pintamos con VDP_setMapEx() la torre utilizando el tileset recién cargado.
  • Actualizamos el índice de tiles correspondiente.
if(Torre[0].vida<40 && Torre[0].estado == 0){
  Torre[0].estado = 1;
  Game.Estructura1 = unpackMap(fondo01_01_torres.map, NULL);
  VDP_loadTileSet(fondo01_01_torres.tileset, Game.id_tile_final_planB, CPU);
  VDP_setMapEx(PLAN_A, Game.Estructura1, TILE_ATTR_FULL(PAL0, TRUE, FALSE, FALSE, 
                       Game.id_tile_final_planB), 0, 3, 11, 0, 10, 11);
  Game.id_tile_final_planA += fondo01_01.tileset->numTile;

}
if(Torre[0].vida<20 && Torre[0].estado == 1){
  Torre[0].estado = 2;
  VDP_setMapEx(PLAN_A, Game.Estructura1, TILE_ATTR_FULL(PAL0, TRUE, FALSE, FALSE, 
                       Game.id_tile_final_planB), 0, 3, 21, 0, 10, 11);
}

El segundo IF es similar al primero, pero como ya hicimos el trabajo de cargar los tiles en memoria, en este paso nos lo ahorramos y directamente pintamos los tiles de los cimientos.

Importante fijarse los colores:

  • (0,3): lugar donde se pinta en la pantalla (en TILES), donde se «pegan» los tiles.
  • (10,0) y (20,0): lugar de dónde se leen los tiles en el archivo fuente (PNG).
    En el caso (10,0) lee la torre dañada, la del centro. En el caso (20,0) lee los cimientos, situados a la derecha. Con (0,0) leeríamos la torre original para restaurarla.
  • (10,11): tamaño en tiles a «copiar» (ancho x alto), desde el archivo fuente (PNG).

Aquí se ve mejor:

Captura030

 

Para la torre de la derecha habría que cambiar entre otras cosas las coordenadas en negrita. Es un código muy mejorable pero espero que cualquiera lo pueda entender.

La carga del MAP y la descompresión de tiles podría hacerse en otra parte del programa, mejor antes de empezar la fase. Y al terminar la fase sería recomendable liberar memoria, pero igualmente lo dejo tal cual está por sencillez.

 

 

 

CODIGO DE EJEMPLO

He preparado un ejemplo muy sencillo donde podrás ver todos estos ejemplo.

  • Mueve la mira con el DPAD
  • Pulsa A para disparar.
  • Pulsa B para ir cambiando de ejemplo.

Todos los ejemplo se prueban disparando a la torre de la izquierda, excepto con el último ejemplo que podemos disparar a ambas torres.

He cambiado las paletas, en el ejemplo de cambio de paleta (existente) utilizaré la PAL1, que es la que uso para para la mira, en vez de usar la PAL2 (que no se usaba y estaba inicializada a verdes), de esta forma me quedarían 2 palestas, PAL2 y PAL3, libres para usar como quieras.

 

video30

 

UN MOMENTO, ¿Y LOS ENEMIGOS GIGANTES?

Pues claro, ¿de eso se trataba no?

Esta técnica es muy recomendable para crear bosses enormes, actualizando sólo los tiles de zonas dañables o animadas, y no tocar el resto. De esta forma y si hacemos un buen diseño, un fondo estático puede parecer dinámico y muy vivo.

Además podemos combinarlo con uno o varios sprites animados. A gusto del diseñador.

Eso lo veremos en las siguientes entradas.

 

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