A quien vio la parte 1 y la siguió al pie de la letra, le diría que borre el final del código (el que ubiqué para mostrar lo que el código hacía) para dejar solo las funciones. Además, en la primera parte cometí 2 errores ya corregidos. La oveja se mueve 20% más lento, no rápido que los lobos cuando está tranquila. Además, en la función calcularDistancia, el Math.round estaba de más. Pequeños errores que saltaron a la vista al proseguir con el código.
Bien, lo que vamos a ver ahora es como programar la oveja para escapar (de los lobos nos ocuparemos más adelante, por su mayor complejidad y distinto enfoque).
Lo primero es aclarar que vamos a emplear prototipos y que un prototipo es, básicamente, una función que simula el código escrito en un símbolo.
Entonces, analicemos lo que queremos que haga nuestra oveja. Lo principal es huir de los lobos, pero el asunto es cómo. Como ya expuse en el post anterior, la oveja tendrá dos estados, determinados por una variable booleana (que es o bien true o false). En el primero escapará del lobo que tiene más cerca llendo en la dirección contraria y en el segundo calculará el ángulo de los dos lobos que tiene más cerca y escapará entre ellos; además, su velocidad variará dado que al estar asustada correrá más rápido.
Ok. Ahora, la sintaxis básica de un prototipo:
Código :
MovieClip.prototype.comportamientoOveja = function (parámetros) { // // Acciones // } mi_clip.comportamientoOveja (parámetros);
En este caso, vamos a ubicar un onEnterFrame y un if dentro de este (aclaro desde ahora que la velocidad a la que configuré la película es de 30 fps), el primer parámetro será el estado inicial y el segundo el nombre del clip (que usaremos para llamar a las funciones) y usaremos un switch para setear la variable "tranquila", esto solo para simplificar la tarea de adaptar el código al rehusarlo
Código :
MovieClip.prototype.comportamientoOveja = function (estado:String, nombre:String) { // // switch (estado) { case "tranquila" : this.tranquila = true; break; case "asustada" : this.tranquila = false; break; default : trace ("Estado desconocido en el prototipo comportamientoOveja"); } // // this.onEnterFrame = function () { // if (tranquila) { } }; };
¿Recuerdan la función calcularAngulo de la primera parte? ¿Y calcularDistancia? Bien, este es el momento de usarlas. Dentro del onEnterFrame ubicamos este código:
Código :
if (this.tranquila) { // this.angulo = calcularAngulo (nombre, buscarLobos (nombre, 2)); // this._y -= _root.velocidad * Math.cos (this.angulo * (Math.PI / 180)); this._x += _root.velocidad * Math.sin (this.angulo * (Math.PI / 180)); }
Ahora, en la función crearObjetos de la parte 1, abajo del attachMovie con el que agregábamos la oveja, ubicamos este código que aplica nuestro prototipo y setea sus 2 parámetros:
Código :
_root["oveja" + i].comportamientoOveja ("tranquila", ("oveja" + i).toString ());
De todos modos, todavía no funciona, falta lo principal que es setear la variable velocidad. Esto lo hacemos en la capa variables de la que hablé en la parte 1.
Código :
var velocidad:Number = 1;
Ahora el otro estado, el que se activa cuando está asustada.
Bueno, es importante saber que cuando se asusta la oveja se comporta distinto. Lo que hace es calcular el ángulo intermedio entre los 2 lobos más cercanos. Entonces, además de la función buscarLobos, hay que usar un aque nos diga cuál es el segundo lobo. Bien, aquí está. Se puede observar que es casi idéntica a buscarLobos, pero que no puede tomar el valor que devuelve buscarLobos.
Código :
buscarSegundoLobo = function (oveja:String, numeroLobos:Number) { // var distMin = 1000; var lobo; var distancia2 = calcularDistancia (oveja, buscarLobos (oveja, numeroLobos)); // for (i = 1; i <= numeroLobos; i++) { // var distancia = calcularDistancia (oveja, "lobo" + i); // if (distancia < distMin && distancia > distancia2) { distMin = distancia; lobo = "lobo" + i; } } return lobo; };
Entonces, hace falta una nueva función que calcule el ángulo en el que la oveja escapará cuando se asuste. A esta función la llamamos calcularAngulo2.
Código :
// // Calcular el ángulo en el que se moverá la oveja para escapar de varios lobos calcularAngulo2 = function (oveja:String, lobo1:String, lobo2:String) { // // Calculamos la distancia en X y Y entre la oveja y el lobo1 var deltaX = _root[oveja]._x - _root[lobo1]._x; var deltaY = _root[oveja]._y - _root[lobo1]._y; // // Calculamos el ángulo en base a esas distancias var angulo1 = (-Math.atan2 (deltaX, deltaY) / (Math.PI / 180)) + 180; // if (angulo1 < 0) { angulo1 += 360; } // // Calculamos la distancia en X y Y entre la oveja y el lobo2 var deltaX = _root[oveja]._x - _root[lobo2]._x; var deltaY = _root[oveja]._y - _root[lobo2]._y; // // Calculamos el ángulo2 en base a esas nuevas distancias var angulo2 = (-Math.atan2 (deltaX, deltaY) / (Math.PI / 180)) + 180; // if (angulo2 < 0) { angulo2 += 360; } // // Ahora, el ángulo final var angulo = (Math.max (angulo1, angulo2) - Math.min (angulo1, angulo2)) / 2 + Math.min (angulo1, angulo2); // if (Math.max (angulo1, angulo2) - Math.min (angulo1, angulo2) > 180) { angulo += 180; } // return angulo; };
Aunque no lo parezca, esta función no es realmente complicada. Aunque quizá, con más conocimientos de trigonometría de los que dispongo (la verdad, no son muchos) se podría acortar. De una forma u otra, esta función es casi la misma que calcularAngulo o, al menos, tiene la misma lógica.
Pero si la prueban (más abajo, dejo el prototipo para hacerlo) verán que la oveja no parece tomar en cuenta la cercanía de los lobos. Ya que la considera (sí, supongamos que está pensando) igual. En otras palabras, escapa considerando que la distancia entre ella y cada uno de los lobos es idéntica.
Pero como no lo es, hay que cambiar algo en el programa.
Quiero que por un minuto imaginen la situación (así razonamos la respuesta). Bien, la oveja calcula el ángulo medio que se forma entre los dos ángulos de escape formados por cada lobo. Si un lobo se halla a 10 píxeles de la oveja y el otro a 300, el lobo que se halla más cerca va a tener buenas oportunidades de atraparla. Esto es porque la oveja no escapa en la dirección opuesta a la del lobo.
En este momento sería útil que analizaran cómo solucionar el problema. Por si acaso, les dejo el prototipo y las acciones, para que puedan hacerlo apropiadamente:
PROTOTIPO:
Código :
MovieClip.prototype.comportamientoOveja = function (estado:String, nombre:String) { // // switch (estado) { case "tranquila" : this.tranquila = true; break; case "asustada" : this.tranquila = false; break; default : trace ("Estado desconocido en el prototipo comportamientoOveja"); } // // this.onEnterFrame = function () { // if (!this.tranquila) { // this.angulo = calcularAngulo (nombre, buscarLobos (nombre, 2)); } if (this.tranquila) { // this.angulo = calcularAngulo2 (nombre, buscarLobos (nombre, 2), buscarSegundoLobo (nombre, 2)); } this._y -= _root.velocidad * Math.cos (this.angulo * (Math.PI / 180)); this._x += _root.velocidad * Math.sin (this.angulo * (Math.PI / 180)); }; };
ACCIONES:
Código :
// crearObjetos (1, 2); // oveja1._y = 200; oveja1._x = 275; // lobo1._x = 540; lobo1._y = 10; lobo2._y = 290; lobo2._x = 300; // lobo2._xscale = 180; // onMouseMove = function () { lobo1._x = _xmouse; lobo1._y = _ymouse; };
Entonces, si lo prueban, verán que el lobo controlado por el mouse tendría altas probabilidades de atrapar a la oveja si no escapa en otro ángulo.
Sería un buen ejercicio que se detuvieran a pensar las distintas soluciones que existen para solucionar el problema.
Lo hicieron? Bien, prosigamos (si, ya sé que muchos no le dedicaron tiempo, pero de haberlo hecho ya tendrían la solución y la satisfacción de haberla encontrado).
A mí se me ocurrieron dos modos relativamente simples. El primero sería hacer un sub-estado del estado "asustada" que se active sólo cuando la oveja tiene al primer lobo muy cerca y al segundo lejos. La otra solución es que luego de calculado el ángulo de escape entre 2 lobos, sume o reste un par de números a este ángulo para acomodarlo al nuevo peligro.
Particularmente, me gusta la segunda idea ya que es más simple y consigue el mismo efecto.
Lo primero que hay que hacer es comparar la distancia entre la oveja y cada uno de los lobos.
Esta función se encarga de hacerlo:
Código :
calcularPeligro = function (oveja:String, lobo1:String, lobo2:String) { // var peligro = false; // var dist1 = calcularDistancia (oveja, lobo1); var dist2 = calcularDistancia (oveja, lobo2); // var distMin = Math.min (dist1, dist2); var distMax = Math.max (dist1, dist2); // var difDist = distMax / distMin; // // Aquí van las constantes que definen si la oveja corre o no peligro // En caso de que algono funcione suficientemente bien, se cambian los números if (distMin < 65) { peligro = true; } else if (difDist > 4 && distMin < 100) { peligro = true; } // return peligro; };
(como todas las otras funciones que armé hasta ahora, sacrifica brevedad por legibilidad, si la hubiese abreviado, simplemente sería más difícil de leer y, tomando en cuenta que este es un tutorial, eso no es bueno )
Ahora, la pregunta clave, cómo la utilizamos?
Bien, a calcularAngulo2 le agregamos estas líneas:
Código :
// // El ángulo final puede cambiar si la oveja se halla en peligro inmediato // En ese caso, la oveja calcula una nueva ruta de escape if (calcularPeligro (oveja, lobo1, lobo2)) { // // AQUÍ VA EL CÓDIGO QUE CAMBIA EL ÁNGULO // }
Bien, parece simple, esto lo agregamos entre el return y la llave que cierra el último if.
Ahora, el código para variar el ángulo, se puede observar que casi no escribimos nada nuevo, sino que reusamos partes de código ya escritas:
Código :
var nuevoAngulo = calcularAngulo (oveja, buscarLobos (oveja, 2)); var anguloMedio = (Math.max (angulo, nuevoAngulo) - Math.min (angulo, nuevoAngulo)) / 2 + Math.min (angulo, nuevoAngulo); // if (Math.max (angulo, nuevoAngulo) - Math.min (angulo, nuevoAngulo) > 180) { anguloMedio += 180; } // return anguloMedio;
Claro, esto va reemplazando el comentario del pedazo de código de arriba.
Para cerrar, el código del prototipo que incluye el código por el cuál se asusta la oveja (se acuerdan??)
Código :
if (buscarLobos (nombre, 2) != this.lobo) { this.miedo++; } if (this.miedo > this.tolerancia) { this.tranquila = false; trace ("Oveja asustada"); } // this.lobo = buscarLobos (nombre, 2);
Ese código, necesita las variables this.miedo y this.tolerancia. Luego de definirlas debajo del switch, el código nos queda así:
Código :
MovieClip.prototype.comportamientoOveja = function (estado:String, nombre:String) { // // switch (estado) { case "tranquila" : this.tranquila = true; break; case "asustada" : this.tranquila = false; break; default : trace ("Estado desconocido en el prototipo comportamientoOveja"); } // this.tolerancia = 10; this.miedo = 0; // // this.onEnterFrame = function () { // if (this.tranquila) { // this.angulo = calcularAngulo (nombre, buscarLobos (nombre, 2)); // if (buscarLobos (nombre, 2) != this.lobo) { this.miedo++; } if (this.miedo > this.tolerancia) { this.tranquila = false; trace ("Oveja asustada"); } // this.lobo = buscarLobos (nombre, 2); } if (!this.tranquila) { // this.angulo = calcularAngulo2 (nombre, buscarLobos (nombre, 2), buscarSegundoLobo (nombre, 2)); } this._y -= _root.velocidad * Math.cos (this.angulo * (Math.PI / 180)); this._x += _root.velocidad * Math.sin (this.angulo * (Math.PI / 180)); }; };
Claro, el trace ("oveja asustada") sólo está ahí para mostrar cuándo exactamente se asusta la oveja. No tiene ningún uso y hasta resulta molesto. Luego de ver que funciona, la idea es sacarlo. En mi opinión es una buena costumbre poner trace en ciertas variables para ver que es lo que está fallando o cómo funcionan ciertas cosas. Ya que hacer un debug es algo engorroso.
Para cerrar esta "entrega", voy a aclarar lo que acabamos de hacer. Básicamente, aplicamos la lógica (y un poco de matemática) para resolver una serie de problemas que se fueron presentando.
Luego de pensarlo seriamente, opté por incluir paso a paso el código que usé, no para entregar el programa ya hecho, sino para que quien lea esta explicación, sea capaz de razonarlo del mismo modo que lo hice. La idea, sería detenerse en cada paso y probar el código. Incluso, me alegraría mucho recibir feedback que me ayude a mejorarlo.
Reitero, la idea no es copiar todo el código y simplemente adaptarlo, sino usarlo como guía para ver cómo se pueden solucionar ciertos problemas relativos a la creación de personajes controlados por computadora.
Los objetivos puntuales fueron los siguientes:
1) Mostrar una forma simple de cambiar estados en la oveja, elemento importante en la creación de un videojuego relativamente complejo. Una variación de esto sería setear la dificultad del juego.
2) Ejemplificar el código reutilizable o adaptable, lo que simplifica no sólo el escribir el código, sino también pensar en usarlo en otra aplicación. Un buen ejemplo de esto es la función calcularDistancia, que estoy seguro tiene muchos más usos de los que expuse.
3) Mostrar que uno de los acercamientos más acertados para crear IA es armarla modularmente, con varias funciones que se puedan emplear en distintos momentos y con distintos motivos. Retomando el ejemplo de calcularDistancia, esa función se puede usar para la oveja o los lobos (esto lo trataré más adelante).
Quizá algún lector se halla dado cuenta que el agregado a calcularAngulo2 es inútil (ese que modifica el ángulo con la función calcularPeligro), porque dado el modo en el que la oveja se asusta, logra tener a los 2 lobos prácticamente a la misma distancia. Le recuerdo al avispado lector que la oveja podría estar ya asustada "de nacimiento" y que no necesariamente vamos a trabajar con sólo 2 lobos
Realmente, espero que esto sirva y cualquier pregunta, estoy ahí para responderla. Nos vemos en la última entrega.
EDITADO: Corregí todas las etiquetas code que estaban mal puestas