Cree Un Juego de Hockey Utilizando Comportamientos de Direccionamiento de IA: Fundamentos
() translation by (you can also view the original English article)
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 sabe 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 una manera de crear ese juego.
En esta serie de tutoriales, aprenderá cómo crear inteligencia artificial para un juego de hockey usando una combinación de técnicas tales como comportamientos de direccionamiento, que he explicado anteriormente como conceptos.
Nota: Aunque este tutorial está escrito con AS3 y Flash, debería ser capaz de usar 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 desarrollo de juegos, tales como patrones de movimiento, trabajo en equipo (ataque, defensa), inteligencia artificial y tácticas. Un juego de hockey jugable encaja muy bien para demostrar la combinación de algunas técnicas útiles.
Simular la mecánica del hockey, con los atletas corriendo y moviéndose, es un desafío. Si los patrones de movimiento están predefinidos, incluso con caminos diferentes, el juego se vuelve predecible (y aburrido). ¿Cómo podemos implementar un entorno tan dinámico manteniendo a su vez el control sobre lo que está pasando? La respuesta es: usar comportamientos de direccionamiento.
Los comportamientos de direccionamiento tienen como objetivo crear patrones de movimiento realistas con la navegación improvisada. Se basan en fuerzas simples que se combinan cada actualización del juego, por lo que son muy dinámicos por naturaleza. Esto los convierte en la opción perfecta para implementar algo tan complejo y dinámico como un partido de hockey o de fútbol.
Determinación del Alcance del Trabajo
Por el bien del tiempo y la enseñanza, vamos a reducir el alcance del juego un poco. Nuestro juego de hockey seguirá sólo un pequeño conjunto de reglas originales del deporte: en nuestro juego no habrá penalizaciones ni porteros, así que cada atleta puede moverse por la pista:



Cada portería será reemplazada por una pequeña "pared" sin red. Para anotar, un equipo debe mover el puck (el disco) para que toque cualquier lado de la meta del oponente. Cuando alguien marca, ambos equipos se reorganizarán, y el puck será colocado en el centro; el partido se reiniciará unos segundos después de eso.
Respecto a la manipulación del puck: si un atleta, digamos A, tiene el puck, y es tocado por un oponente, digamos B, entonces B gana el puck y A se vuelve inamovible por unos segundos. Si el puck alguna vez deja la pista, se colocará en el centro de la pista inmediatamente.
Voy a utilizar el motor de juego Flixel para ocuparme de la parte gráfica del código. Sin embargo, el código del motor se simplificará o se omitirá en los ejemplos, para mantener el foco en el juego en sí.
Estructuración del Entorno
Comencemos con el entorno del juego, que está compuesto por una pista, un número de atletas y dos porterías. La pista está formada por cuatro rectángulos colocados alrededor del área de hielo; estos rectángulos chocarán con todo lo que los toque, por lo que nada saldrá del área de hielo.
Un atleta será descrito por la clase Athlete
:
1 |
public class Athlete |
2 |
{
|
3 |
private var mBoid :Boid; // controls the steering behavior stuff |
4 |
private var mId :int; // a unique identifier for the athelete |
5 |
|
6 |
public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number) { |
7 |
mBoid = new Boid(thePosX, thePosY, theTotalMass); |
8 |
}
|
9 |
|
10 |
public function update():void { |
11 |
// Clear all steering forces
|
12 |
mBoid.steering = null; |
13 |
|
14 |
// Wander around
|
15 |
wanderInTheRink(); |
16 |
|
17 |
// Update all steering stuff
|
18 |
mBoid.update(); |
19 |
}
|
20 |
|
21 |
private function wanderInTheRink() :void { |
22 |
var aRinkCenter :Vector3D = getRinkCenter(); |
23 |
|
24 |
// If the distance from the center is greater than 80,
|
25 |
// move back to the center, otherwise keep wandering.
|
26 |
if (Utils.distance(this, aRinkCenter) >= 80) { |
27 |
mBoid.steering = mBoid.steering + mBoid.seek(aRinkCenter); |
28 |
} else { |
29 |
mBoid.steering = mBoid.steering + mBoid.wander(); |
30 |
}
|
31 |
}
|
32 |
}
|
La propiedad mBoid
es una instancia de la clase Boid
, una encapsulación de la lógica matemática utilizada en la serie de comportamientos de direccionamiento. La instancia mBoid
tiene, entre otros elementos, vectores matemáticos que describen la dirección actual, la fuerza de direccionamiento y la posición de la entidad.
El método update()
en la clase Athlete
se invocará cada vez que se actualice el juego. Por ahora, sólo borra cualquier fuerza de direccionamiento activa, añade una fuerza errante, y finalmente llama a mBoid.update()
. El comando anterior actualiza toda la lógica de comportamiento de direccionamiento encapsulada dentro de mBoid
, haciendo que el atleta se mueva (usando la integración de Euler).
La clase de juego, que es responsable del bucle del juego, se llamará PlayState
. Tiene la pista, dos grupos de atletas (un grupo por equipo) y dos porterías:
1 |
public class PlayState |
2 |
{
|
3 |
private var mAthletes :FlxGroup; |
4 |
private var mRightGoal :Goal; |
5 |
private var mLeftGoal :Goal; |
6 |
|
7 |
public function create():void { |
8 |
// Here everything is created and added to the screen.
|
9 |
}
|
10 |
|
11 |
override public function update():void { |
12 |
// Make the rink collide with athletes
|
13 |
collide(mRink, mAthletes); |
14 |
|
15 |
// Ensure all athletes will remain inside the rink.
|
16 |
applyRinkContraints(); |
17 |
}
|
18 |
|
19 |
private function applyRinkContraints() :void { |
20 |
// check if athletes are within the rink
|
21 |
// boundaries.
|
22 |
}
|
23 |
}
|
Asumiendo que un solo atleta fue agregado al partido, abajo está el resultado de todo hasta ahora:
Siguiendo el cursor del ratón
El atleta debe seguir el cursor del ratón, por lo que el jugador realmente puede controlar algo. Ya que el cursor del ratón tiene una posición en la pantalla, puede utilizarse como destino para el comportamiento de llegada.
El comportamiento de llegada hará que un atleta busque la posición del cursor, suavemente disminuya la velocidad cuando se aproxima al cursor, y finalmente se detienga allí.
En la clase Athlete
, reemplacemos el método errante con el comportamiento de llegada:
1 |
public class Athlete |
2 |
{
|
3 |
// (...)
|
4 |
|
5 |
public function update():void { |
6 |
// Clear all steering forces
|
7 |
mBoid.steering = null; |
8 |
|
9 |
// The athlete is controlled by the player,
|
10 |
// so just follow the mouse cursor.
|
11 |
followMouseCursor(); |
12 |
|
13 |
// Update all steering stuff
|
14 |
mBoid.update(); |
15 |
}
|
16 |
|
17 |
private function followMouseCursor() :void { |
18 |
var aMouse :Vector3D = getMouseCursorPosition(); |
19 |
mBoid.steering = mBoid.steering + mBoid.arrive(aMouse, 50); |
20 |
}
|
21 |
}
|
El resultado es un atleta que puede seguir el cursor del ratón. Ya que la lógica del movimiento se basa en comportamientos de dirección, los atletas navegan la pista de una manera convincente y suave.
Utilice el cursor del ratón para guiar al atleta en la demostración a continuación:
Agregar y Controlar el Puck
El puck será representado por la clase Puck
. Las partes más importantes son el método update()
y la propiedad mOwner
:
1 |
public class Puck |
2 |
{
|
3 |
public var velocity :Vector3D; |
4 |
public var position :Vector3D; |
5 |
private var mOwner :Athlete; // the athlete currently carrying the puck. |
6 |
|
7 |
public function setOwner(theOwner :Athlete) :void { |
8 |
if (mOwner != theOwner) { |
9 |
mOwner = theOwner; |
10 |
velocity = null; |
11 |
}
|
12 |
}
|
13 |
|
14 |
public function update():void { |
15 |
}
|
16 |
|
17 |
public function get owner() :Athlete { return mOwner; } |
18 |
}
|
Siguiendo la misma lógica del atleta, el método update()
del puck será invocado cada vez que el juego se actualiza. La propiedad mOwner
determina si el puck está en posesión de cualquier atleta. Si mOwner
es null
, significa que el disco está "libre", y se moverá, eventualmente rebotando en las paredes de pista.
Si mOwner
no es null
, significa que el puck está siendo llevado por un atleta. En este caso, ignorará cualquier control de colisión y se ubicará a la fuerza delante del atleta. Esto se puede lograr usando el vector de velocity
del atleta, que también coincide con la dirección del atleta:



El vector ahead
es una copia del vector de velocity
del atleta, por lo que apuntan en la misma dirección. Después de que ahead
se normaliza, puede ser escalado por cualquier valor-digamos 30
-para controlar hasta qué punto el puck se colocará por delante del atleta.
Finalmente, la position
del puck recibe la position
del atleta añadida al ahead
, colocando el puck en la posición deseada.
A continuación se muestra el código para todo aquello:
1 |
public class Puck |
2 |
{
|
3 |
// (...)
|
4 |
|
5 |
private function placeAheadOfOwner() :void { |
6 |
var ahead :Vector3D = mOwner.boid.velocity.clone(); |
7 |
|
8 |
ahead = normalize(ahead) * 30; |
9 |
position = mOwner.boid.position + ahead; |
10 |
}
|
11 |
|
12 |
override public function update():void { |
13 |
if (mOwner != null) { |
14 |
placeAheadOfOwner(); |
15 |
}
|
16 |
}
|
17 |
|
18 |
// (...)
|
19 |
}
|
En la clase PlayState
, hay una prueba de colisión para comprobar 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 disco que "pega" al atleta. En la demostración de abajo, guíe al atleta para que toque 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 lleva el disco, todo lo que se requiere para simular un golpe por el 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 desde un vector de posición hacia otro; el vector recién generado irá entonces de una posición a otra. Eso es exactamente lo que se necesita para calcular el nuevo vector de velocidad del puck después de un golpe:



En la imagen de arriba, el punto de destino es el cursor del ratón. La posición actual del puck puede ser usada como punto de partida, mientras que el punto donde debe estar el puck después de haber sido golpeado por el palo puede ser usado como el punto final.
El pseudo-código siguiente muestra la implementación de goFromStickHit()
, un método en la clase Puck
que implementa la lógica ilustrada en la imagen anterior:
1 |
public class Puck |
2 |
{
|
3 |
// (...)
|
4 |
|
5 |
public function goFromStickHit(theAthlete :Athlete, theDestination :Vector3D, theSpeed :Number = 160) :void { |
6 |
// Place the puck ahead of the owner to prevent unexpected trajectories
|
7 |
// (e.g. puck colliding the athlete that just hit it)
|
8 |
placeAheadOfOwner(); |
9 |
|
10 |
// Mark the puck as free (no owner)
|
11 |
setOwner(null); |
12 |
|
13 |
// Calculate the puck's new velocity
|
14 |
var new_velocity :Vector3D = theDestination - position; |
15 |
velocity = normalize(new_velocity) * theSpeed; |
16 |
}
|
17 |
}
|
El vector new_velocity
va desde la posición actual del puck hasta el destino (theDestination
) Después de eso, es normalizado y escalado por theSpeed
, que define la magnitud (longitud) de new_velocity
. Esa operación, en otras palabras, define la rapidez con que el puck se moverá desde su posición actual hasta el destino. Finalmente, el vector velocity
del disco es reemplazado por new_velocity
.
En la clase PlayState
, se invoca el método goFromStichHit()
cada vez que el reproductor hace clic en la pantalla. Cuando sucede, el cursor del ratón se utiliza como destino para el golpe. El resultado se ve en esta demostración:
Añadiendo la I.A.
Hasta ahora, hemos tenido un solo atleta moviéndose por la pista. A medida que se agregan más atletas, la IA debe ser implementada para hacer que todos estos atletas parezcan estar vivos y pensando.
Para lograr esto, usaremos una máquina de estado finito basada en pila (FSM de pila, para abreviar). Como se ha descrito anteriormente, las FSM son versátiles y útiles para implementar IA en juegos.
Para nuestro juego de hockey, una propiedad llamada mBrain
será añadida a la clase de Athlete
:
1 |
public class Athlete |
2 |
{
|
3 |
// (...)
|
4 |
private var mBrain :StackFSM; // controls the AI stuff |
5 |
|
6 |
public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number) { |
7 |
// (...)
|
8 |
mBrain = new StackFSM(); |
9 |
}
|
10 |
|
11 |
// (...)
|
12 |
}
|
Esta propiedad es una instancia de StackFSM
, una clase utilizada anteriormente en el tutorial FSM. Utiliza una pila para controlar los estados de IA de una entidad. Cada estado se describe como un método; cuando un estado es empujado a la pila, se convierte en el método activo y se invoca durante cada actualización del juego.
Cada estado llevará a cabo una tarea específica, como mover al atleta hacia el puck. Cada estado es responsable de terminarse a sí mismo, lo que significa que es responsable de quitarse a sí mismo de la pila.
El atleta puede ser controlado por el jugador o por la IA ahora, por lo que el método update()
en la clase Athlete
debe ser modificado para verificar esa situación:
1 |
public class Athlete |
2 |
{
|
3 |
// (...)
|
4 |
|
5 |
public function update():void { |
6 |
// Clear all steering forces
|
7 |
mBoid.steering = null; |
8 |
|
9 |
if (mControlledByAI) { |
10 |
// The athlete is controlled by the AI. Update the brain (FSM) and
|
11 |
// stay away from rink walls.
|
12 |
mBrain.update(); |
13 |
|
14 |
} else { |
15 |
// The athlete is controlled by the player, so just follow
|
16 |
// the mouse cursor.
|
17 |
followMouseCursor(); |
18 |
}
|
19 |
|
20 |
// Update all steering stuff
|
21 |
mBoid.update(); |
22 |
}
|
23 |
}
|
Si la IA está activa, se actualiza mBrain
, que invoca el método de estado actualmente activo, haciendo que el atleta se comporte en concordancia. Si el jugador está en control, mBrain
se ignora del todo y el atleta se mueve según lo guiado por el jugador.
En cuanto a los estados para empujar en el cerebro: por ahora vamos a implementar sólo dos de ellos. Un estado permitirá que un atleta se prepare para un partido; cuando se prepara para el partido, un atleta se moverá a su posición en la pista y se quedará quieto, mirando al disco. El otro estado hará que el atleta simplemente se quede quieto y contemple el disco.
En las siguientes secciones, implementaremos estos estados.
El Estado Libre
Sie el atleta está en estado idle
, dejará de moverse y mirará el puck. Este estado se utiliza cuando el atleta ya está en posición en la pista y está esperando que algo suceda, como el comienzo del partido.
El estado se codificará en la clase Athlete
, bajo el método idle()
:
1 |
public class Athlete |
2 |
{
|
3 |
// (...)
|
4 |
public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number, theTeam :FlxGroup) { |
5 |
// (...)
|
6 |
|
7 |
// Tell the brain the current state is 'idle'
|
8 |
mBrain.pushState(idle); |
9 |
}
|
10 |
|
11 |
private function idle() :void { |
12 |
var aPuck :Puck = getPuck(); |
13 |
stopAndlookAt(aPuck.position); |
14 |
}
|
15 |
|
16 |
private function stopAndlookAt(thePoint :Vector3D) :void { |
17 |
mBoid.velocity = thePoint - mBoid.position; |
18 |
mBoid.velocity = normalize(mBoid.velocity) * 0.01; |
19 |
}
|
20 |
}
|
Debido a que este método no se retira a sí mismo de la pila, permanecerá activo para siempre. En el futuro, este estado se abrirá para dejar espacio a otros estados, como ataque, pero por ahora basta con lo que hace.
El método stopAndStareAt()
sigue el mismo principio utilizado para calcular la velocidad del disco después de un golpe. Un vector de la posición del atleta a la posición del puck es calculado por the Point - mBoid.position
y usado como el nuevo vector de la velocidad del atleta.
Ese nuevo vector de velocidad moverá al atleta hacia el puck. Para asegurar que el atleta no se mueva, el vector es escalado en 0.01
, "encogiendo" su longitud a casi cero. Hace que el atleta deje de moverse, pero lo mantiene mirando el disco.
Preparación Para un Partido
Si el atleta está en el estado prepareForMatch
, se moverá hacia su posición inicial, deteniéndose sin suavemente allí. La posición inicial es donde el atleta debe estar justo antes de que empiece el partido. Dado que el atleta debe detenerse en el destino, el comportamiento de llegada puede ser utilizado de nuevo:
1 |
public class Athlete |
2 |
{
|
3 |
// (...)
|
4 |
private var mInitialPosition :Vector3D; // the position in the rink where the athlete should be placed |
5 |
|
6 |
public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number, theTeam :FlxGroup) { |
7 |
// (...)
|
8 |
mInitialPosition = new Vector3D(thePosX, thePosY); |
9 |
|
10 |
// Tell the brain the current state is 'idle'
|
11 |
mBrain.pushState(idle); |
12 |
}
|
13 |
|
14 |
private function prepareForMatch() :void { |
15 |
mBoid.steering = mBoid.steering + mBoid.arrive(mInitialPosition, 80); |
16 |
|
17 |
// Am I at the initial position?
|
18 |
if (distance(mBoid.position, mInitialPosition) <= 5) { |
19 |
// I'm in position, time to stare at the puck.
|
20 |
mBrain.popState(); |
21 |
mBrain.pushState(idle); |
22 |
}
|
23 |
}
|
24 |
|
25 |
// (...)
|
26 |
}
|
El estado usa 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 de 5
, significa que el atleta ha llegado al lugar deseado. Cuando esto sucede, prepareForMatch
se retira de la pila y empuja a idle
, convirtiéndolo en el nuevo estado activo.
A continuación se muestra el resultado del uso de un FSM basado en pilas para controlar a varios atletas. Presione G
para colocarlos en posiciones aleatorias en la pista, empujando el estado prepareForMatch
:
Conclusión
Este tutorial presentó las bases para implementar un juego de hockey utilizando comportamientos de direccionamiento y máquinas de estado finito basadas en pila. Usando una combinación de estos conceptos, un atleta es capaz de moverse en la pista, siguiendo el cursor del ratón. El atleta también puede golpear el puck hacia un destino.
Usando dos estados y un FSM basado en pilas, los atletas pueden reorganizarse y moverse a su posición en la pista, preparándose para el partido.
En el siguiente tutorial, aprenderás a hacer que los atletas ataquen, llevando el puck hacia el gol, evitando a los oponentes.
Referencias
- Sprite: Estadio de Hockey en GraphicRiver
- Sprites: Jugadores de Hockey por Taylor J Glidden