01 enero 2009

Remake (parcial) de Fruity Frank... 24 - Enemigos más inteligentes

Hasta ahora nuestros enemigos se movían simplemente de lado a lado. Ahora vamos a intentar que el movimiento sea un poco más "inteligente", y más parecido al del Fruity Frank original.

Antes teníamos un único método "Mover", común a todos los elementos de la clase "Enemigo". Ahora debemos hacer que no todos los enemigos se comporten igual:

  • En primer lugar, no todos aparecerán en el instante inicial: cada uno tendrá un retardo algo mayor que los anteriores, para que aparezcan uno por uno.

  • Para seguir, la inteligencia de los movimientos que hará cada uno dependerá del tipo de enemigo del que se trate. Para más detalles:


    • Los "señores nariz" se moverán casi siempre por los huecos de la pantalla, y pocas veces romperán paredes.

    • Los "señores pepino" serán mucho más impacientes, y romperán paredes con más facilidad.


  • Ambos tipos de enemigos se mueven básicamente al azar, tendiendo a proseguir su marcha hasta que choquen con algo, momento en el que cambiarán de sentido. Más adelante habrá un tercer enemigo, la "fresa", cuyo movimiento será básicamente para perseguir al personaje.

  • Además, cada tipo enemigo aparece en un punto distinto de la pantalla:


    • Los "nariz" salen desde una zona central de la pantalla, que llamaremos su "nido".

    • Los "pepino" caen desde la parte superior.



Vayamos viendo cómo hacer todo esto...

Para que los enemigos puedan saber si se pueden mover a una cierta casilla hará falta que se puedan comunicar con el nivel que los contiene, así que en el constructor les indicaremos cual es ese nivel:
    public Nariz(Nivel n) {
miNivel = n;
...

Además, la función "EsPosibleMover" que ya existía estaba pensada para el personaje, que podía atravesar paredes y comer frutas, pero los enemigos no pueden comer frutas, y se mueven preferentemente por espacios huecos. Por eso, nos interesará una nueva función, que podría llamarse "MovilidadEnemigo", y que tendría 3 valores posibles:

  • Valdría 0 para indicar cuando no puede entrar a esa posición, porque haya una fruta, una manzana u otro enemigo.

  • Sería 1 cuando puede entrar con dificultad, rompiendo paredes, algo que los Nariz pocas veces harán, pero los Pepino sí podrán hacer con más frecuencia.

  • Será 2 cuando sea espacio hueco, por el que se pueden desplazar sin problema.

  • También habría que comprobar si en esa casilla ya hay otro enemigo (o si ya está entrando en ella), porque no se deberán solapar dos en la misma posición de pantalla, pero esto lo dejamos para un poco más adelante.


    /// Indica si es el enemigo puede moverse a cierta posicion de la pantalla
/// (2 = sí, hueco; 1 = a veces, pared; 0 = nunca, fruta).
public byte MovilidadEnemigo(short x, short y)
{
short xMapa = (short) ((x-xIniPantalla)/anchoCasilla);
short yMapa = (short) ((y-yIniPantalla)/altoCasilla);

if ((x < xIniPantalla) || (xMapa >= MAXCOLS) || // Si se sale, no puede
(y < yIniPantalla) || (yMapa >= MAXFILAS)) return 0;

char simbolo = GetPosicion(xMapa, yMapa);

if ((simbolo == ' ') // Si es hueco, sí puede
|| (simbolo == 'N')) // También si es el nido
return 2;

if ((simbolo == 'X') // Si es pared, puede con dificultad
|| (simbolo == 'Y') || (simbolo == 'Z') )
return 1;

// En el resto de casos, sería una fruta y no puede
return 0;
}

Así, la lógica del movimiento de los enemigos "Nariz" podría ser así:

  • Si se estaba moviendo, continuará en la misma dirección hasta que tope con un obstáculo.

  • Cuando encuentre un obstáculo, elegirá una nueva direccción al azar. Empezará a a moverse en esa dirección si no hay obstáculos; si hay una pared, se empezará a mover con una probabilidad baja (por ejemplo, un 25%). Si hay una obstáculo, o bien si es una pared pero el número al azar ha indicado que no debe atravesarla, se quedará parado, y en el siguiente fotograma de juego se volverá a elegir una dirección al azar y a repetir el proceso.


    public override void Mover()
{
// Si no está activo, espero el tiempo estipulado para que aparezca
if (!activo)
{
contadorHastaRetardo ++;
if (contadorHastaRetardo >= retardo)
activo = true;
return;
}

// Si está parado, busco nueva dirección
if (parado) {
calcularNuevaDireccion();
return;
}

// Si se puede mover en horizontal o vertical, avanza
if ((!parado) && (incrX > 0)) {
if (miNivel.MovilidadEnemigo(
(short) (x+miNivel.GetAnchoCasilla()), y) == 2)
x += incrX;
else
parado = true;
}
...

Y la rutina de mover el enemigo "Pepino", que sí puede atravesar paredes, de momento podría ser muy similar, con la diferencia de que podrá entrar en casillas de "Movilidad 1" (paredes) y que deberá borrar la casilla a la que entra:
    ...
// Si se puede mover en horizontal o vertical, avanza
if ((!parado) && (incrX > 0)) {
if (miNivel.MovilidadEnemigo(
(short) (x+miNivel.GetAnchoCasilla()), y) >= 1)
{
x += incrX;
miNivel.BorrarPosicionPantalla(x,y);
}
else
parado = true;
}
...

En cuanto a la posición inicial de cada enemigo, la definimos desde el constructor de cada nivel. Por ejemplo, para el Nivel 1, podría ser:
    public  Nivel1()
{
byte i;

miMapa = new Mapa1();
enemigos = new Enemigo[NUMENEMIGOS];

enemigos[0] = new Nariz(this);
enemigos[0].MoverA(miMapa.posXnido, miMapa.posYnido);
enemigos[0].SetRetardo(25); // 1 segundo despues del comienzo

enemigos[1] = new Nariz(this);
enemigos[1].MoverA(miMapa.posXnido, miMapa.posYnido);
enemigos[1].SetRetardo(75); // 3 segundos despues del comienzo

enemigos[2] = new Pepino(this);
enemigos[2].MoverA(
(short) (miMapa.GetXIni() + 5*miMapa.GetAnchoCasilla()),
miMapa.GetYIni());
enemigos[2].SetRetardo(150); // 6 segundos despues del comienzo
enemigos[2].SetVelocidad(0,4);
}


Como siempre, todo el fuente del proyecto está en: code.google.com/p/fruityfrank