Crear un juego de hockey AI utilizando comportamientos de dirección Fundación

Hay diferentes maneras de hacer cualquier juego en particular. Por lo general, un desarrollador elige algo que se ajuste a sus habilidades, utilizando las técnicas que ya conoce para producir el mejor resultado posible. A veces, las personas aún no saben que necesitan una cierta técnica, tal vez incluso una más fácil y mejor, simplemente porque ya saben cómo crear ese juego.. 

En esta serie de tutoriales, aprenderá cómo crear inteligencia artificial para un juego de hockey utilizando una combinación de técnicas, como los comportamientos de dirección, que he explicado anteriormente como conceptos..

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..


Introducción

El hockey es un deporte divertido y popular y, como videojuego, incorpora muchos temas de gamedev, como patrones de movimiento, trabajo en equipo (ataque, defensa), inteligencia artificial y tácticas. Un juego de hockey jugable es ideal para demostrar la combinación de algunas técnicas útiles.

Simular al mecánico de hockey, con atletas corriendo y moviéndose, es un desafío. Si los patrones de movimiento están predefinidos, incluso con diferentes caminos, el juego se vuelve predecible (y aburrido). ¿Cómo podemos implementar un entorno tan dinámico sin dejar de mantener el control sobre lo que está pasando? La respuesta es: utilizar comportamientos de dirección..

Los comportamientos de dirección apuntan a crear patrones de movimiento realistas con la navegación de improvisación. Se basan en fuerzas simples que se combinan en cada actualización del juego, por lo que son extremadamente dinámicas por naturaleza. Esto los convierte en la opción perfecta para implementar algo tan complejo y dinámico como un juego de hockey o de fútbol..

El alcance del trabajo

Por el tiempo y la enseñanza, reduzcamos un poco el alcance del juego. Nuestro juego de hockey seguirá solo un pequeño conjunto de reglas originales del deporte: en nuestro juego no habrá penalizaciones ni guardametas, por lo que cada atleta puede moverse por la pista:

Juego de hockey utilizando reglas simplificadas..

Cada gol será reemplazado por un pequeño "muro" sin red. Para anotar, un equipo debe mover el puck (el disco) para que toque cualquier lado de la portería del oponente. Cuando alguien anota, ambos equipos se reorganizarán y el disco se colocará en el centro; El partido se reiniciará unos segundos después..

Con respecto al manejo del puck: si un atleta, digamos A, tiene el puck y es tocado por un oponente, diga B, entonces B gana el puck y A se queda inmóvil durante unos segundos. Si el disco sale de la pista, se colocará en el centro de la pista inmediatamente..

Usaré el motor del juego Flixel para ocuparme de la parte gráfica del código. Sin embargo, el código del motor se simplificará u omitirá en los ejemplos, para mantener el enfoque en el juego en sí..

Estructurando el medio ambiente

Comencemos con el entorno de juego, que se compone de una pista, una serie de atletas y dos objetivos. La pista está formada por cuatro rectángulos colocados alrededor del área de hielo; estos rectángulos colisionarán con todo lo que los toque, por lo que nada dejará el área de hielo.

Un atleta será descrito por el Atleta clase:

Clase pública Atleta privado var mBoid: Boid; // controla el comportamiento de la dirección material private var mId: int; // un identificador único para la función pública atleta atleta (thePosX: Number, thePosY: Number, theTotalMass: Number) mBoid = new Boid (thePosX, thePosY, theTotalMass);  public function update (): void // Borrar todas las fuerzas de dirección mBoid.steering = null; // Deambular por wanderInTheRink (); // Actualizar todo el material de dirección mBoid.update ();  función privada wanderInTheRink (): void var aRinkCenter: Vector3D = getRinkCenter (); // Si la distancia desde el centro es mayor que 80, // regresa al centro, de lo contrario sigue vagando. if (Utils.distance (this, aRinkCenter)> = 80) mBoid.steering = mBoid.steering + mBoid.seek (aRinkCenter);  else mBoid.steering = mBoid.steering + mBoid.wander (); 

La propiedad mBoid es una instancia de la Boid clase, una encapsulación de la lógica matemática utilizada en la serie de conductas de dirección. los mBoid La instancia tiene, entre otros elementos, vectores matemáticos que describen la dirección actual, la fuerza de dirección y la posición de la entidad..

los actualizar() método en el Atleta La clase será invocada cada vez que se actualice el juego. Por ahora, solo elimina cualquier fuerza de dirección activa, agrega una fuerza de desplazamiento y finalmente llama. mBoid.update (). El comando anterior actualiza toda la lógica de comportamiento de dirección encapsulada dentro mBoid, Hacer que el atleta se mueva (utilizando la integración de Euler)..

La clase de juego, que es responsable del bucle de juego, se llamará PlayState. Tiene la pista, dos grupos de atletas (un grupo para cada equipo) y dos metas:

clase pública PlayState private var mAthletes: FlxGroup; privado var mRightGoal: Goal; privado var mLeftGoal: Objetivo; public function create (): void // Aquí todo se crea y se agrega a la pantalla.  anular la función pública update (): void // Hacer que la pista choque con los atletas chocando (mRink, mAthletes); // Asegurar que todos los atletas permanecerán dentro de la pista. applyRinkContraints ();  private function applyRinkContraints (): void // verifique si los atletas están dentro de los límites de la pista //. 

Suponiendo que se haya agregado un solo atleta al partido, a continuación se muestra el resultado de todo:

Siguiendo el cursor del mouse

El atleta debe seguir el cursor del mouse para que el jugador pueda controlar algo. Dado que el cursor del mouse tiene una posición en la pantalla, se puede usar como destino para el comportamiento de llegada.

El comportamiento de llegada hará que un atleta busque la posición del cursor, disminuya suavemente la velocidad a medida que se acerca al cursor y, finalmente, se detiene allí.. 

En el Atleta clase, vamos a reemplazar el método errante con el comportamiento de llegada:

public class Athlete // (...) public function update (): void // Borrar todas las fuerzas de dirección mBoid.steering = null; // El atleta es controlado por el jugador, // solo sigue el cursor del mouse. followMouseCursor (); // Actualizar todo el material de dirección mBoid.update ();  función privada followMouseCursor (): void var aMouse: Vector3D = getMouseCursorPosition (); mBoid.steering = mBoid.steering + mBoid.arrive (aMouse, 50); 

El resultado es un atleta que puede el cursor del ratón. Dado que la lógica del movimiento se basa en los comportamientos de la dirección, los atletas navegan por la pista de manera convincente y suave.. 

Usa el cursor del ratón para guiar al atleta en la siguiente demostración:

Añadiendo y controlando el puck

El puck estará representado por la clase. Disco. Las partes más importantes son las actualizar() método y el mOwner propiedad:

clase pública Puck public var speed: Vector3D; posición var pública: Vector3D; propietario privado: atleta; // el atleta que actualmente lleva el puck. función pública setOwner (theOwner: Athlete): void if (mOwner! = theOwner) mOwner = theOwner; velocidad = nula;  public function update (): void  public function get owner (): Athlete return mOwner; 

Siguiendo la misma lógica del deportista, el puck's actualizar() El método será invocado cada vez que se actualice el juego. los mOwner la propiedad determina si el puck está en posesión de algún atleta. Si mOwner es nulo, significa que el puck es "libre", y se moverá alrededor, eventualmente rebotando en las pistas de la pista.

Si mOwner no es nulo, significa que el puck está siendo llevado por un atleta. En este caso, ignorará los controles de colisión y se colocará con fuerza por delante del atleta. Esto se puede lograr utilizando el atleta. velocidad Vector, que también coincide con la dirección del atleta:

Explicación de cómo se coloca el puck delante del atleta.

los adelante Vector es una copia del deportista. velocidad Vector, por lo que apuntan en la misma dirección. Después adelante está normalizado, puede ser escalado por cualquier valor, por ejemplo, 30-para controlar qué tan lejos se colocará el puck delante del atleta.

Finalmente, el puck's. posición recibe del atleta posición añadido a adelante, colocando el disco en la posición deseada. 

A continuación se muestra el código para todo eso:

public class Puck // (...) private function placeAheadOfOwner (): void var ahead: Vector3D = mOwner.boid.velocity.clone (); adelante = normalizar (adelante) * 30; position = mOwner.boid.position + ahead;  sobrescribir la función pública update (): void if (mOwner! = null) placeAheadOfOwner ();  // (…)

En el PlayState clase, hay una prueba de colisión para verificar si el puck se superpone a cualquier atleta. Si lo hace, el atleta que acaba de tocar el puck se convierte en su nuevo propietario. El resultado es un puck que se "pega" al atleta. En la siguiente demostración, guíe al atleta a tocar el disco en el centro de la pista para ver esto en acción:


Golpeando el puck

Es hora de hacer que el disco se mueva como resultado de ser golpeado por el palo. Independientemente del atleta que lleve el puck, todo lo que se requiere para simular un golpe del palo es calcular un nuevo vector de velocidad. Esa nueva velocidad moverá el disco hacia el destino deseado..

Un vector de velocidad puede ser generado por un vector de posición desde otro; el vector recién generado irá de una posición a otra. Eso es exactamente lo que se necesita para calcular el nuevo vector de velocidad del disco después de un golpe:

Cálculo de la nueva velocidad de puck después de un golpe del palo.

En la imagen de arriba, el punto de destino es el cursor del mouse. La posición actual del disco se puede usar como punto de inicio, mientras que el punto donde debería estar el disco después de haber sido golpeado por el palo se puede usar como punto final. 

El siguiente pseudocódigo muestra la implementación de goFromStickHit (), un método en el Disco Clase que implementa la lógica ilustrada en la imagen de arriba:

public class Puck // (...) public function goFromStickHit (theAthlete: Athlete, theDestination: Vector3D, theSpeed: Number = 160): void // Coloque el puck delante del propietario para evitar trayectorias inesperadas // (p. ej., puck que choca con el atleta que acaba de golpearlo) placeAheadOfOwner (); // Marcar el puck como libre (sin propietario) setOwner (null); // Calcula la nueva velocidad del puck var new_velocity: Vector3D = theDestination - position; velocidad = normalizar (new_velocity) * theSpeed; 

los nueva_velocidad el vector va desde la posición actual del disco hasta el objetivo (el destino). Después de eso, es normalizado y escalado por la velocidad, que define la magnitud (longitud) de nueva_velocidad. Esa operación, en otras palabras, define qué tan rápido se moverá el disco desde su posición actual hasta el destino. Finalmente, el puck's. velocidad vector es reemplazado por nueva_velocidad.

En el PlayState clase, la goFromStichHit () El método se invoca cada vez que el jugador hace clic en la pantalla. Cuando esto sucede, el cursor del mouse se usa como destino del golpe. El resultado se ve en esta demo:

Añadiendo el A.I.

Hasta ahora, solo hemos tenido un solo atleta moviéndose alrededor de la pista. A medida que se agreguen más atletas, se debe implementar la IA para que todos estos atletas parezcan estar vivos y pensando..

Para lograrlo, utilizaremos una máquina de estados finitos basada en la pila (FSM basado en la pila, para abreviar). Como se describió anteriormente, los FSM son versátiles y útiles para implementar AI en juegos. 

Para nuestro juego de hockey, una propiedad llamada mBrain será añadido a la Atleta clase:

public class Athlete // (…) private var mBrain: StackFSM; // controla la función pública de cosas AI Atleta (thePosX: Number, thePosY: Number, theTotalMass: Number) // (...) mBrain = new StackFSM ();  // (…)

Esta propiedad es una instancia de StackFSM, una clase previamente utilizada en el tutorial de FSM. Utiliza una pila para controlar los estados de inteligencia artificial de una entidad. Cada estado se describe como un método; cuando se empuja un estado en la pila, se convierte en el activo Método y se llama durante cada actualización del juego..

Cada estado realizará una tarea específica, como mover al atleta hacia el disco. Cada estado es responsable de terminarse a sí mismo, lo que significa que es responsable de sacarse de la pila.

El atleta puede ser controlado por el jugador o por la IA ahora, por lo que actualizar() método en el Atleta La clase debe ser modificada para verificar esa situación:

public class Athlete // (...) public function update (): void // Borrar todas las fuerzas de dirección mBoid.steering = null; if (mControlledByAI) // El atleta está controlado por la IA. Actualiza el cerebro (FSM) y // mantente alejado de las paredes de la pista. mBrain.update ();  else // El atleta está controlado por el jugador, así que solo sigue // el cursor del mouse. followMouseCursor ();  // Actualizar todo el material de dirección mBoid.update (); 

Si la IA está activa, mBrain se actualiza, lo que invoca el método del estado activo actual, haciendo que el atleta se comporte en consecuencia. Si el jugador está en control, mBrain se ignora todos juntos y el atleta se mueve guiado por el jugador. 

Con respecto a los estados para empujar hacia el cerebro: por ahora implementemos solo dos de ellos. Un estado permitirá que un atleta se prepare para un partido; Al prepararse para el partido, un atleta se moverá a su posición en la pista y se quedará quieto, mirando fijamente el disco. El otro estado hará que el atleta simplemente se quede quieto y se quede mirando el disco..

En las siguientes secciones, implementaremos estos estados..

El estado de inactividad

Si el atleta está en el ocioso Estado, dejará de moverse y se quedará mirando el disco. Este estado se usa cuando el atleta ya está en posición en la pista y está esperando que algo suceda, como el comienzo del partido..

El estado será codificado en el Atleta clase, bajo la ocioso() método:

public class Athlete // (...) public function Athlete (thePosX: Number, thePosY: Number, theTotalMass: Number, theTeam: FlxGroup) // (...) // Dígale al cerebro que el estado actual es 'inactivo' mBrain.pushState (ocioso);  función privada idle (): void var aPuck: Puck = getPuck (); stopAndlookAt (aPuck.position);  función privada stopAndlookAt (thePoint: Vector3D): void mBoid.velocity = thePoint - mBoid.position; mBoid.velocity = normalize (mBoid.velocity) * 0.01; 

Dado que este método no se desprende de la pila, permanecerá activo para siempre. En el futuro, este estado se abrirá para dejar espacio para otros estados, como ataque, pero por ahora hace el truco.

los stopAndStareAt () El método sigue el mismo principio utilizado para calcular la velocidad del disco después de un golpe. Se calcula un vector desde la posición del atleta hasta la posición del puck. thePoint - mBoid.position y usado como el nuevo vector de velocidad del atleta.

Ese nuevo vector de velocidad moverá al atleta hacia el disco. Para asegurarse de que el atleta no se moverá, el vector se escala por 0.01 , "encogiendo" su longitud a casi cero. Hace que el atleta deje de moverse, pero lo mantiene mirando el disco..

Preparándose para un partido

Si el atleta está en el prepareForMatch Estado, se moverá hacia su posición inicial, deteniéndose suavemente allí. La posición inicial es donde el atleta debe estar justo antes de que comience el partido. Dado que el atleta debe detenerse en el destino, el comportamiento de llegada se puede usar nuevamente:

public class Athlete // (…) private var mInitialPosition: Vector3D; // la posición en la pista donde se debe colocar al atleta función pública Atleta (thePosX: Number, thePosY: Number, theTotalMass: Number, theTeam: FlxGroup) // (...) mInitialPosition = new Vector3D (thePosX, thePosY); // Dígale al cerebro que el estado actual es 'inactivo' mBrain.pushState (inactivo);  función privada prepareForMatch (): void mBoid.steering = mBoid.steering + mBoid.arrive (mInitialPosition, 80); // ¿Estoy en la posición inicial? if (distance (mBoid.position, mInitialPosition) <= 5)  // I'm in position, time to stare at the puck. mBrain.popState(); mBrain.pushState(idle);   // (… ) 

El estado utiliza el comportamiento de llegada para mover al atleta hacia la posición inicial. Si la distancia entre el atleta y su posición inicial es menor que 5, Significa que el atleta ha llegado al lugar deseado. Cuando esto pasa, prepareForMatch salta de la pila y empuja ocioso, convirtiéndolo en el nuevo estado activo.

A continuación se muestra el resultado de usar un FSM basado en la pila para controlar a varios atletas. prensa sol colocarlos en posiciones aleatorias en la pista, empujando el prepareForMatch estado:


Conclusión

Este tutorial presentó los fundamentos para implementar un juego de hockey utilizando comportamientos de dirección y máquinas de estados finitos basadas en pila. Usando una combinación de esos conceptos, un atleta puede moverse en la pista, siguiendo el cursor del mouse. El atleta también puede golpear el disco hacia un destino..

Usando dos estados y un FSM basado en la pila, los atletas pueden reorganizarse y moverse a su posición en la pista, preparándose para el partido.

En el siguiente tutorial, aprenderás cómo hacer que los atletas ataquen, llevando el disco hacia la meta y evitando a los oponentes..

Referencias

  • Sprite: Estadio de hockey en GraphicRiver
  • Sprites: jugadores de hockey por Taylor J Glidden