Las máquinas de estado finito y los comportamientos de dirección son una combinación perfecta: su naturaleza dinámica permite la combinación de estados simples y fuerzas para crear patrones de comportamiento complejos. En este tutorial, aprenderás cómo codificar un equipo patrón que utiliza una máquina de estados finitos basada en la pila combinada con comportamientos de dirección.
Todos los íconos de FSM hechos por Lorc y disponibles en http://game-icons.net. Activos en la demostración: Top / Down Shoot 'Em Up Spritesheet por takomogames y Alien Breed (esque) Top-Down Tilesheet por SpicyPixel.
Nota: Aunque este tutorial está escrito con AS3 y Flash, debería poder utilizar las mismas técnicas y conceptos en casi cualquier entorno de desarrollo de juegos..
Después de completar este tutorial, podrás implementar un patrón de escuadrón en el que un grupo de soldados seguirán al líder, cazando enemigos y saqueando objetos:
El tutorial anterior sobre máquinas de estados finitos describió lo útiles que son para implementar la lógica de inteligencia artificial: en lugar de escribir una pila muy compleja de códigos AI, la lógica se puede distribuir a través de un conjunto de estados simples, cada uno de los cuales realiza tareas muy específicas, como huyendo de un enemigo.
La combinación de estados da como resultado una inteligencia artificial sofisticada, pero fácil de entender, ajustar y mantener. Esa estructura es también uno de los pilares detrás de los comportamientos de dirección: la combinación de fuerzas simples para crear patrones complejos..
Es por eso que los FSM y los comportamientos de dirección hacen una gran combinación. Los estados se pueden usar para controlar qué fuerzas actuarán sobre un personaje, mejorando el ya poderoso conjunto de patrones que se pueden crear usando comportamientos de dirección.
Para organizar todos los comportamientos, se extenderán a los estados. Cada estado generará una fuerza de comportamiento específica, o un conjunto de ellos, como buscar, huir y evitar colisiones.
Cuando un estado en particular está activo, solo su fuerza resultante se aplicará al personaje, haciéndolo comportarse en consecuencia. Por ejemplo, si el estado activo actual es huir
y sus fuerzas son una combinacion de huir
y evitación de colisiones
, El personaje huirá de un lugar evitando cualquier obstáculo..
Las fuerzas de dirección se calculan cada actualización del juego y luego se agregan al vector de velocidad del personaje. Como consecuencia, cuando el estado activo cambia (y con él el patrón de movimiento), el personaje pasará suavemente al nuevo patrón a medida que se agreguen nuevas fuerzas después de cada actualización..
La naturaleza dinámica de los comportamientos de dirección asegura esta transición fluida; los estados simplemente coordinan qué fuerzas de dirección están activas en un momento dado.
La estructura para implementar un patrón de escuadrón encapsulará los FSM y los comportamientos de dirección dentro de las propiedades de una clase. Cualquier clase que represente a una entidad que se mueva o esté influenciada por fuerzas de dirección tendrá una propiedad llamada boid
, que es una instancia de la Boid
clase:
clase pública Boid public var position: Vector3D; public var speed: Vector3D; dirección de var público: Vector3D; misa de var. pública: Número; búsqueda de función pública (objetivo: Vector3D, desaceleración Radio: Número = 0): Vector3D (...) función pública huir (posición: Vector3D): Vector3D (...) actualización de función pública (): void (...) (... )
los Boid
clase se utilizó en la serie de comportamiento de dirección y proporciona propiedades como velocidad
y posición
(ambos vectores matemáticos), junto con métodos para agregar fuerzas de dirección, como buscar()
, huir()
, etc.
Una entidad que utiliza un FSM basado en pila tendrá la misma estructura de Hormiga
clase del anterior tutorial de FSM: el FSM basado en la pila es administrado por el cerebro
propiedad y cada estado se implementa como un método.
abajo esta el Soldado
clase, que tiene comportamiento de dirección y capacidades FSM:
soldado de clase pública cerebro de var privado: StackFSM; // Controla el material FSM privado var boid: Boid; // Controla los comportamientos de la dirección función pública Soldier (posX: Number, posY: Number, totalMass: Number) (…) brain = new StackFSM (); // Presiona el estado "seguir" para que el soldado siga al líder brain.pushState (seguir); actualización de la función pública (): void // Actualizar el cerebro. Se ejecutará la función de estado actual. brain.update (); // Actualizar los comportamientos de dirección boid.update ();
El patrón de escuadrón se implementará utilizando una máquina de estados finitos basada en la pila. Los soldados, que son los miembros del escuadrón, seguirán al líder (controlado por el jugador), persiguiendo a los enemigos cercanos..
Cuando un enemigo muere, puede soltar un objeto que puede ser bueno o malo (un botiquín o un badkit, respectivamente). Un soldado romperá la formación del escuadrón y recogerá buenos objetos cercanos, o evadirá el lugar para evitar cualquier mal elemento..
A continuación se muestra una representación gráfica del FSM basado en la pila que controla el "cerebro" del soldado:
Las siguientes secciones presentan la implementación de cada estado. Todos los fragmentos de código en este tutorial describen la idea principal de cada paso, omitiendo todos los detalles sobre el motor del juego utilizado (Flixel, en este caso).
El primer estado que se implementará es el que permanecerá activo casi todo el tiempo: Sigue al líder. La parte de saqueo se implementará más adelante, así que por ahora la seguir
El estado solo hará que el soldado siga al líder, cambiando el estado actual a cazar
Si hay un enemigo cerca:
función pública follow (): void var aLeader: Boid = Game.instance.boids [0]; // obtener un puntero al líder addSteeringForce (boid.followLeader (aLeader)); // sigue al líder // ¿Hay algún monstruo cerca? if (getNearestEnemy ()! = null) // ¡Sí, lo hay! ¡Cazarlo! // Empujar el estado de "caza". Hará que el soldado deje de seguir al líder y // comenzará a cazar al monstruo. brain.pushState (caza); función privada getNearestEnemy (): Monster // aquí va la implementación para obtener el enemigo más cercano
A pesar de la presencia de enemigos, mientras el estado está activo, siempre generará una fuerza para seguir al líder, usando el líder siguiendo el comportamiento.
Si getNearestEnemy ()
devuelve algo, significa que hay un enemigo alrededor. En ese caso, el cazar
el estado es empujado en la pila a través de la llamada brain.pushState (caza)
, haciendo que el soldado deje de seguir al líder y comience a cazar enemigos.
Por ahora, la implementación de la cazar()
El estado puede simplemente saltar de la pila, de esa manera los soldados no se quedarán atrapados en el estado de caza:
función pública hunt (): void // Por ahora, simplemente extraemos el estado hunt () del cerebro. brain.popState ();
Tenga en cuenta que no se pasa información a la cazar
estado, como quien es el enemigo mas cercano Esa información debe ser recogida por el cazar
Estado mismo, porque determina si el cazar
debe permanecer activo o salirse de la pila (devolviendo el control a la seguir
estado).
El resultado hasta ahora es un escuadrón de soldados siguiendo al líder (tenga en cuenta que los soldados no cazarán porque el cazar()
el método simplemente aparece en sí mismo):
Propina: Cada estado debe ser responsable de terminar con su existencia saltándose de la pila..
El siguiente estado a implementar es cazar
, Lo que hará que los soldados persigan a cualquier enemigo cercano. El código para cazar()
es:
función pública hunt (): void var aNearestEnemy: Monster = getNearestEnemy (); // ¿Tenemos un monstruo cerca? if (aNearestEnemy! = null) // Sí, lo hacemos. Vamos a calcular lo distante que está. var aDistance: Number = calculaDistance (aNearestEnemy, this); // ¿Está el monstruo lo suficientemente cerca para disparar? si <= 80) // Yes, so let's face it! faceEnemyStandingStill(aNearestEnemy); // Fire away! Take that, monster! shoot(); else // No, the monster is far away. Seek it until it gets close enough. addSteeringForce(boid.seek(aNearestEnemy.boid.position)); // Avoid crowding while seeking the target… addSteeringForce(boid.separation()); else // No, there is no monster nearby. Maybe it was killed or ran away. Let's pop the "hunt" // state and come back doing what we were doing before the hunting. brain.popState();
El estado comienza asignando aNearestEnemy
con el enemigo más cercano. Si aNearestEnemy
es nulo
significa que no hay ningún enemigo alrededor, por lo que el estado debe terminar. La llamada cerebro.popestado ()
aparece el cazar
Estado, cambiando el soldado al siguiente estado en la pila.
Si aNearestEnemy
no es nulo
, significa que hay un enemigo que debe ser perseguido y el estado debe permanecer activo. El algoritmo de caza se basa en la distancia entre el soldado y el enemigo: si la distancia es mayor que 80, el soldado buscará la posición del enemigo; Si la distancia es inferior a 80, el soldado se enfrentará al enemigo y disparará mientras esté parado..
Ya que cazar()
se invocará cada actualización del juego, si un enemigo está cerca, el soldado buscará o disparará a ese enemigo. La decisión de moverse o disparar está controlada dinámicamente por la distancia entre el soldado y el enemigo.
El resultado es un escuadrón de soldados capaces de seguir al líder y cazar a los enemigos cercanos:
Cada vez que un enemigo muere, puede soltar un objeto. El soldado debe recoger el artículo si es bueno, o huir del artículo si es malo. Ese comportamiento está representado por dos estados en el FSM descrito anteriormente:
coleccionar
y huir
estados. los coleccionar
estado hará que un soldado llegue al objeto que se ha caído, mientras que huir
El estado hará que el soldado huya de la ubicación del objeto malo. Ambos estados son casi idénticos, la única diferencia es la fuerza de llegada o huida:
public function runAway (): void var aItem: Item = getNearestItem (); if (aItem! = nulo && aItem.alive && aItem.type == Item.BADKIT) var aItemPos: Vector3D = new Vector3D (); aItemPos.x = aItem.x; aItemPos.y = aItem.y; addSteeringForce (boid.flee (aItemPos)); else brain.popState (); función pública collectItem (): void var aItem: Item = getNearestItem (); if (aItem! = nulo && aItem.alive && aItem.type == Item.MEDKIT) var aItemPos: Vector3D = new Vector3D (); aItemPos.x = aItem.x; aItemPos.y = aItem.y; addSteeringForce (boid.arrive (aItemPos, 50)); else brain.popState (); función privada getNearestItem (): Elemento // aquí va el código para obtener el elemento más cercano
Aquí una optimización sobre las transiciones es útil. El código para la transición de la seguir
estado a la coleccionar
o la huir
el estado es el mismo: compruebe si hay un elemento cerca, luego presione el nuevo estado.
El estado que se debe enviar depende del tipo de elemento. Como consecuencia, la transición a coleccionar
o huir
Se puede implementar como un solo método, llamado checkItemsNearby ()
:
función privada checkItemsNearby (): void var aItem: Item = getNearestItem (); if (aItem! = null) brain.pushState (aItem.type == Item.BADKIT? runAway: collectItem);
Este método verifica el artículo más cercano. Si es buena, la coleccionar
el estado es empujado hacia el cerebro; si es una mala, la huir
El estado es empujado. Si no hay un objeto para recoger, el método no hace nada..
Esa optimización permite el uso de checkItemsNearby ()
para controlar la transición de cualquier estado a coleccionar
o huir
. Según el soldado FSM, esa transición existe en dos estados: seguir
y cazar
.
Su implementación puede modificarse ligeramente para adaptarse a esa nueva transición:
función pública follow (): void var aLeader: Boid = Game.instance.boids [0]; // obtener un puntero al líder addSteeringForce (boid.followLeader (aLeader)); // sigue al líder // Comprueba si hay un elemento para recopilar (o huir de) checkItemsNearby (); // ¿Hay algún monstruo cerca? if (getNearestEnemy ()! = null) // ¡Sí, lo hay! ¡Cazarlo! // Empujar el estado de "caza". Hará que el soldado deje de seguir al líder y // comenzará a cazar al monstruo. brain.pushState (caza); función pública hunt (): void var aNearestEnemy: Monster = getNearestEnemy (); // Compruebe si hay un elemento para recopilar (o huir de) checkItemsNearby (); // ¿Tenemos un monstruo cerca? if (aNearestEnemy! = null) // Sí, lo hacemos. Vamos a calcular lo distante que está. var aDistance: Number = calculaDistance (aNearestEnemy, this); // ¿Está el monstruo lo suficientemente cerca para disparar? si <= 80) // Yes, so let's face it! faceEnemyStandingStill(aNearestEnemy); // Fire away! Take that, monster! shoot(); else // No, the monster is far away. Seek it until it gets close enough. addSteeringForce(boid.seek(aNearestEnemy.boid.position)); // Avoid crowding while seeking the target… addSteeringForce(boid.separation()); else // No, there is no monster nearby. Maybe it was killed or ran away. Let's pop the "hunt" // state and come back doing what we were doing before the hunting. brain.popState();
Mientras sigue al líder, un soldado comprobará si hay objetos cercanos. Al cazar a un enemigo, un soldado también buscará objetos cercanos..
El resultado es la demo a continuación. Tenga en cuenta que un soldado intentará recolectar o evadir un objeto en cualquier momento que haya uno cerca, aunque haya enemigos para cazar y el líder a seguir..
Un aspecto importante con respecto a los estados y transiciones es el prioridad entre ellos. Dependiendo de la línea donde se coloca una transición dentro de la implementación de un estado, la prioridad de esa transición cambia.
Utilizando la seguir
Estado y la transición hecha por checkItemsNearby ()
Como ejemplo, eche un vistazo a la siguiente implementación:
función pública follow (): void var aLeader: Boid = Game.instance.boids [0]; // obtener un puntero al líder addSteeringForce (boid.followLeader (aLeader)); // sigue al líder // Comprueba si hay un elemento para recopilar (o huir de) checkItemsNearby (); // ¿Hay algún monstruo cerca? if (getNearestEnemy ()! = null) // ¡Sí, lo hay! ¡Cazarlo! // Empujar el estado de "caza". Hará que el soldado deje de seguir al líder y // comenzará a cazar al monstruo. brain.pushState (caza);
Esa version de seguir()
hará un cambio de soldado a coleccionar
o huir
antes de comprobando si hay un enemigo alrededor. Como consecuencia, el soldado recolectará (o huirá) de un objeto, incluso cuando haya enemigos alrededor que deberían ser cazados por el cazar
estado.
Aquí hay otra implementación:
función pública follow (): void var aLeader: Boid = Game.instance.boids [0]; // obtener un puntero al líder addSteeringForce (boid.followLeader (aLeader)); // sigue al líder // ¿Hay algún monstruo cerca? if (getNearestEnemy ()! = null) // ¡Sí, lo hay! ¡Cazarlo! // Empujar el estado de "caza". Hará que el soldado deje de seguir al líder y // comenzará a cazar al monstruo. brain.pushState (caza); else // Compruebe si hay un elemento para recopilar (o huir de) checkItemsNearby ();
Esa version de seguir()
hará un cambio de soldado a coleccionar
o huir
solamente después descubre que no hay enemigos para matar.
La implementación actual de seguir()
, cazar()
y artículo de colección ()
Sufren de problemas prioritarios. El soldado intentará recolectar un artículo incluso cuando haya cosas más importantes que hacer. Para arreglar eso, se necesitan algunos ajustes..
Con respecto a seguir
Estado, el código se puede actualizar a:
(sigue () con prioridades)
función pública follow (): void var aLeader: Boid = Game.instance.boids [0]; // obtener un puntero al líder addSteeringForce (boid.followLeader (aLeader)); // sigue al líder // ¿Hay algún monstruo cerca? if (getNearestEnemy ()! = null) // ¡Sí, lo hay! ¡Cazarlo! // Empujar el estado de "caza". Hará que el soldado deje de seguir al líder y // comenzará a cazar al monstruo. brain.pushState (caza); else // Compruebe si hay un elemento para recopilar (o huir de) checkItemsNearby ();
los cazar
El estado debe cambiarse a:
función pública hunt (): void var aNearestEnemy: Monster = getNearestEnemy (); // ¿Tenemos un monstruo cerca? if (aNearestEnemy! = null) // Sí, lo hacemos. Vamos a calcular lo distante que está. var aDistance: Number = calculaDistance (aNearestEnemy, this); // ¿Está el monstruo lo suficientemente cerca para disparar? si <= 80) // Yes, so let's face it! faceEnemyStandingStill(aNearestEnemy); // Fire away! Take that, monster! shoot(); else // No, the monster is far away. Seek it until it gets close enough. addSteeringForce(boid.seek(aNearestEnemy.boid.position)); // Avoid crowding while seeking the target… addSteeringForce(boid.separation()); else // No, there is no monster nearby. Maybe it was killed or ran away. Let's pop the "hunt" // state and come back doing what we were doing before the hunting. brain.popState(); // Check if there is an item to collect (or run away from) checkItemsNearby();
Finalmente, el coleccionar
Se debe cambiar el estado para abortar cualquier saqueo si hay un enemigo cerca:
función pública collectItem (): void var aItem: Item = getNearestItem (); var aMonsterNearby: Boolean = getNearestEnemy ()! = null; if (! aMonsterNearby && aItem! = null && aItem.alive && aItem.type == Item.MEDKIT) var aItemPos: Vector3D = new Vector3D (); aItemPos.x = aItem.x; aItemPos.y = aItem.y; addSteeringForce (boid.arrive (aItemPos, 50)); else brain.popState ();
El resultado de todos esos cambios es la demostración desde el principio del tutorial:
En este tutorial, aprendiste a codificar un patrón de escuadrón donde un grupo de soldados seguirán a un líder, cazando y saqueando a los enemigos cercanos. La IA se implementa utilizando un FSM basado en pila combinado con varios comportamientos de dirección.
Como se demostró, las máquinas de estado finito y los comportamientos de dirección son una combinación poderosa y una gran combinación. Extendiendo la lógica sobre los estados FSM, es posible seleccionar dinámicamente qué fuerzas de dirección actuarán sobre un personaje, permitiendo la creación de patrones de IA complejos..
Combine los comportamientos de dirección que ya conoce con los FSM y cree patrones nuevos y destacados!