Crear un juego IA de Hockey sobre hielo utilizando comportamientos de dirección: ataque
() translation by (you can also view the original English article)
En este tutorial, continuamos codificando la inteligencia artificial para un juego de hockey usando comportamientos de dirección y máquinas de estado finito. En esta parte de la serie, aprenderás sobre la IA requerida por las entidades del juego para coordinar un ataque, lo que implica interceptar y llevar el disco al objetivo del oponente.
Algunas palabras sobre atacar
Coordinar y realizar un ataque en un juego de deporte cooperativo es una tarea muy compleja. En el mundo real, cuando los humanos juegan un juego de hockey, toman varias decisiones basadas en muchas variables.
Esas decisiones implican cálculos y comprensión de lo que está sucediendo. Un humano puede decir por qué un oponente se está moviendo según las acciones de otro oponente, por ejemplo, "se está moviendo para estar en una mejor posición estratégica". No es trivial transferir esa comprensión a una computadora.
Como consecuencia, si tratamos de codificar la inteligencia artificial para que siga todos los matices y percepciones humanas, el resultado será una enorme cantidad de código aterrador. Además, el resultado puede no ser preciso o fácilmente modificable.
Esa es la razón por la cual nuestra IA de ataque intentará imitar el resultado de un grupo de humanos jugando, no la percepción humana en sí misma. Ese enfoque conducirá a aproximaciones, pero el código será más fácil de entender y modificar. El resultado es lo suficientemente bueno para varios casos de uso.
Organizando el ataque con los estados
Desmenuzaremos el proceso de ataque en piezas más pequeñas, cada una de las cuales realizará una acción muy específica. Esas piezas son los estados de una máquina de estados finitos basada en pila. Como se explicó anteriormente, cada estado producirá una fuerza de dirección que hará que el atleta se comporte en consecuencia.
La orquestación de esos estados y las condiciones para cambiar entre ellos definirán el ataque. La imagen a continuación presenta el FSM completo utilizado en el proceso:



Como se ilustra en la imagen, las condiciones para cambiar entre los estados se basarán únicamente en la distancia y la propiedad del disco. Por ejemplo, el equipo tiene el disco
o disco está demasiado lejos
.
El proceso de ataque se compondrá de cuatro estados: idle
, attack
, stealPuck
, y pursuePuck
. El estado inactivo idle
ya se implementó en el tutorial anterior, y es el punto de partida del proceso. A partir de ahí, un atleta cambiará a attack
si el equipo tiene el disco, para robarlo stealPuck
si el equipo del oponente tiene el disco, o para perseguirlo pursuePuck
si el disco no tiene dueño y está lo suficientemente cerca para ser recogido.
El estado de ataque attack
representa un movimiento ofensivo. Mientras está en ese estado, el atleta que lleva el disco (líder designado leader
) tratará de alcanzar la meta del oponente. Los compañeros de equipo avanzarán, tratando de apoyar la acción.
El estado de StealPuck
representa algo entre un movimiento defensivo y uno ofensivo. Mientras esté en ese estado, un atleta se centrará en perseguir al oponente que lleva el disco. El objetivo es recuperar el disco, para que el equipo pueda comenzar a atacar de nuevo.
Finalmente, el estado pursuePuck
no está relacionado con el ataque o la defensa; solo guiará a los atletas cuando el disco no tenga dueño. Mientras esté en ese estado, un atleta tratará de obtener el disco que se mueve libremente en la pista (por ejemplo, después de ser golpeado por el bastón de alguien).
Actualización del estado inactivo
El estado inactivo idle
que se implementó previamente no tenía transiciones. Como este estado es el punto de partida para toda la IA, actualicémosla y hagamos que sea capaz de cambiar a otros estados.
El estado idle
tiene tres transiciones:



Si el equipo del atleta tiene el disco, idle
debe levantarse del cerebro y el attack
debe ser empujado. Del mismo modo, si el equipo del oponente tiene el disco, el motor inactivo idle
debe reemplazarse por stealPuck
. La transición restante ocurre cuando nadie posee el disco y está cerca del atleta; en ese caso, pursuePuck
debería ser empujado al cerebro.
La versión actualizada de inactividad idle
es la siguiente (todos los demás estados se implementarán más adelante):
1 |
class Athlete { |
2 |
// (...)
|
3 |
private function idle() :void { |
4 |
var aPuck :Puck = getPuck(); |
5 |
|
6 |
stopAndlookAt(aPuck); |
7 |
|
8 |
// This is a hack to help test the AI.
|
9 |
if (mStandStill) return; |
10 |
|
11 |
// Does the puck has an owner?
|
12 |
if (getPuckOwner() != null) { |
13 |
// Yeah, it has.
|
14 |
mBrain.popState(); |
15 |
|
16 |
if (doesMyTeamHaveThePuck()) { |
17 |
// My team just got the puck, it's attack time!
|
18 |
mBrain.pushState(attack); |
19 |
} else { |
20 |
// The opponent team got the puck, let's try to steal it.
|
21 |
mBrain.pushState(stealPuck); |
22 |
}
|
23 |
} else if (distance(this, aPuck) < 150) { |
24 |
// The puck has no owner and it is nearby. Let's pursue it.
|
25 |
mBrain.popState(); |
26 |
mBrain.pushState(pursuePuck); |
27 |
}
|
28 |
}
|
29 |
|
30 |
private function attack() :void { |
31 |
}
|
32 |
|
33 |
private function stealPuck() :void { |
34 |
}
|
35 |
|
36 |
private function pursuePuck() :void { |
37 |
}
|
38 |
}
|
Procedamos con la implementación de los otros estados.
Perseguir el disco
Ahora que el atleta ha ganado algo de percepción sobre el medio ambiente y puede pasar de inactivo idle
a cualquier estado, centrémonos en perseguir el disco cuando no tenga dueño.
Un atleta se cambiará a pursuePuck
inmediatamente después de que comience el partido, porque el disco se colocará en el centro de la pista sin dueño. El estado pursuePuck
tiene tres transiciones:



La primera transición es que el disco está demasiado lejos puck is too far away
, y trata de simular lo que sucede en un juego real con respecto a perseguir el disco. Por razones estratégicas, generalmente el atleta más cercano al disco es el que intenta atraparlo, mientras que los demás esperan o intentan ayudar.
Sin cambiar a idle
cuando el disco está distante, todos los atletas controlados por IA perseguirían el disco al mismo tiempo, incluso si están lejos de él. Al verificar la distancia entre el atleta y el disco, pursuePuck
que sale del cerebro y lo deja idle
cuando el disco está demasiado distante, lo que significa que el atleta simplemente "abandonó" la persecución del disco:
1 |
class Athlete { |
2 |
// (...)
|
3 |
|
4 |
private function pursuePuck() :void { |
5 |
var aPuck :Puck = getPuck(); |
6 |
|
7 |
if (distance(this, aPuck) > 150) { |
8 |
// Puck is too far away from our current position, so let's give up
|
9 |
// pursuing the puck and hope someone will be closer to get the puck
|
10 |
// for us.
|
11 |
mBrain.popState(); |
12 |
mBrain.pushState(idle); |
13 |
} else { |
14 |
// The puck is close, let's try to grab it.
|
15 |
}
|
16 |
}
|
17 |
|
18 |
// (...)
|
19 |
}
|
Cuando el disco está cerca, el atleta debe ir tras él, lo que se puede lograr fácilmente con el comportamiento de búsqueda. Utilizando la posición del disco como el destino de la búsqueda, el atleta persigue con gracia el disco y ajusta su trayectoria a medida que se mueve el disco:
1 |
class Athlete { |
2 |
// (...) |
3 |
private function pursuePuck() :void { |
4 |
var aPuck :Puck = getPuck(); |
5 |
|
6 |
mBoid.steering = mBoid.steering + mBoid.separation(); |
7 |
|
8 |
if (distance(this, aPuck) > 150) { |
9 |
// Puck is too far away from our current position, so let's give up |
10 |
// pursuing the puck and hope someone will be closer to get the puck |
11 |
// for us. |
12 |
mBrain.popState(); |
13 |
mBrain.pushState(idle); |
14 |
} else { |
15 |
// The puck is close, let's try to grab it. |
16 |
if (aPuck.owner == null) { |
17 |
// Nobody has the puck, it's our chance to seek and get it! |
18 |
mBoid.steering = mBoid.steering + mBoid.seek(aPuck.position); |
19 |
|
20 |
} else { |
21 |
// Someone just got the puck. If the new puck owner belongs to my team, |
22 |
// we should switch to 'attack', otherwise I should switch to 'stealPuck' |
23 |
// and try to get the puck back. |
24 |
mBrain.popState(); |
25 |
mBrain.pushState(doesMyTeamHaveThePuck() ? attack : stealPuck); |
26 |
} |
27 |
} |
28 |
} |
29 |
} |
Las dos transiciones restantes en el estado perseguirPuck
, el equipo tiene el disco team has the puck
y el oponente tiene el disco opponent has the puck
, están relacionadas con el disco que se atrapó durante el proceso de persecución. Si alguien atrapa el disco, el atleta debe hacer estallar el estado pursuePuck
y poner uno nuevo en el cerebro.
El estado a empujar depende de la propiedad del disco. Si la llamada a doesMyTeamHaveThePuck()
devuelve true
, significa que un compañero de equipo obtuvo el disco, por lo que el atleta debe attack
, lo que significa que es hora de dejar de perseguir el disco y comenzar a avanzar hacia la meta del oponente. Si un oponente tiene el disco, el atleta debe empujar stealPuck
, lo que hará que el equipo intente recuperar el disco.
Como una pequeña mejora, los atletas no deben permanecer demasiado cerca el uno del otro durante el estado perseguirPuck
, porque un movimiento perseguidor "lleno de gente" no es natural. Agregar separación a la fuerza de dirección del estado (línea 6
en el código anterior) asegura que los atletas mantendrán una distancia mínima entre ellos.
El resultado es un equipo que puede perseguir el disco. Por el bien de las pruebas, en esta demostración, el disco se coloca en el centro de la pista cada pocos segundos, para que los atletas se muevan continuamente:
Atacando con el disco
Después de obtener el disco, un atleta y su equipo deben avanzar hacia el objetivo del oponente para anotar. Ese es el propósito del estado attack
:



El estado attack
tiene solo dos transiciones: el oponente tiene el disco opponent has the puck
y el disco no tiene dueño puck has no owner
. Dado que el estado está diseñado exclusivamente para hacer que los atletas se muevan hacia la meta del oponente, no tiene sentido seguir atacando si el disco ya no está bajo la posesión del equipo.
En cuanto al movimiento hacia la meta del oponente: el atleta que lleva el disco (líder) y los compañeros de equipo que lo ayudan deben comportarse de manera diferente. El líder debe alcanzar la meta del oponente, y los compañeros de equipo deben ayudarlo en el camino.
Esto se puede implementar comprobando si el atleta que ejecuta el código tiene el disco:
1 |
class Athlete { |
2 |
// (...)
|
3 |
private function attack() :void { |
4 |
var aPuckOwner :Athlete = getPuckOwner(); |
5 |
|
6 |
// Does the puck have an owner?
|
7 |
if (aPuckOwner != null) { |
8 |
// Yeah, it has. Let's find out if the owner belongs to the opponents team.
|
9 |
if (doesMyTeamHaveThePuck()) { |
10 |
if (amIThePuckOwner()) { |
11 |
// My team has the puck and I am the one who has it! Let's move
|
12 |
// towards the opponent's goal.
|
13 |
mBoid.steering = mBoid.steering + mBoid.seek(getOpponentGoalPosition()); |
14 |
|
15 |
} else { |
16 |
// My team has the puck, but a teammate has it. Let's just follow him
|
17 |
// to give some support during the attack.
|
18 |
mBoid.steering = mBoid.steering + mBoid.followLeader(aPuckOwner.boid); |
19 |
mBoid.steering = mBoid.steering + mBoid.separation(); |
20 |
}
|
21 |
} else { |
22 |
// The opponent has the puck! Stop the attack
|
23 |
// and try to steal it.
|
24 |
mBrain.popState(); |
25 |
mBrain.pushState(stealPuck); |
26 |
}
|
27 |
} else { |
28 |
// Puck has no owner, so there is no point to keep
|
29 |
// attacking. It's time to re-organize and start pursuing the puck.
|
30 |
mBrain.popState(); |
31 |
mBrain.pushState(pursuePuck); |
32 |
}
|
33 |
}
|
34 |
}
|
Si amIThePuckOwner()
devuelve verdadero true
(línea 10), el atleta que ejecuta el código tiene el disco. En ese caso, solo buscará la posición de gol del oponente. Esa es prácticamente la misma lógica que se usa para perseguir el disco en el estado de persecución pursuePuck
.
.Si amIThePuckOwner()
devuelve false
, el atleta no tiene el disco, por lo que debe ayudar al líder. Ayudar al líder es una tarea complicada, entonces lo simplificaremos. Un atleta ayudará al líder simplemente buscando un puesto por delante de él:



A medida que el líder se mueve, estará rodeado de compañeros de equipo mientras siguen el punto ahead
. Esto le da al líder algunas opciones para pasar el disco si hay algún problema. Al igual que en un juego real, los compañeros de equipo de los alrededores también deben mantenerse fuera del camino del líder.
Este patrón de asistencia se puede lograr agregando una versión ligeramente modificada del comportamiento siguiente del líder (línea 18). La única diferencia es que los atletas seguirán un punto por delante del líder, en lugar de uno detrás de él, como se implementó originalmente en ese comportamiento.
Los atletas que asisten al líder también deben mantener una distancia mínima entre ellos. Eso se implementa al agregar una fuerza de separación (línea 19).
El resultado es un equipo capaz de avanzar hacia la meta del oponente, sin amontonarse y simulando un movimiento de ataque asistido:
Mejorando el Soporte de Ataque
La implementación actual del estado attack
es lo suficientemente buena para algunas situaciones, pero tiene un defecto. Cuando alguien atrapa el disco, se convierte en el líder y es seguido inmediatamente por sus compañeros de equipo.
¿Qué sucede si el líder se está moviendo hacia su propio objetivo cuando atrapa el disco? Eche un vistazo más de cerca a la demostración anterior y observe el patrón antinatural cuando los compañeros de equipo comienzan a seguir al líder.
Cuando el líder atrapa el disco, el comportamiento de búsqueda toma un tiempo para corregir la trayectoria del líder y hacer que se mueva hacia la meta del oponente. Incluso cuando el líder está "maniobrando", los compañeros intentarán buscar su punto de ventaja ahead
, lo que significa que se moverán hacia su propia meta (o el lugar que el líder está mirando).
Cuando el líder finalmente esté en posición y listo para avanzar hacia la meta del oponente, los compañeros de equipo estarán "maniobrando" para seguir al líder. El líder se moverá sin el apoyo de un compañero de equipo mientras los demás estén ajustando sus trayectorias.
Esta falla se puede solucionar comprobando si el compañero de equipo está por delante del líder cuando el equipo recupera el disco. Aquí, la condición "adelante" significa "más cerca de la meta del oponente":
1 |
class Athlete { |
2 |
// (...)
|
3 |
private function isAheadOfMe(theBoid :Boid) :Boolean { |
4 |
var aTargetDistance :Number = distance(getOpponentGoalPosition(), theBoid); |
5 |
var aMyDistance :Number = distance(getOpponentGoalPosition(), mBoid.position); |
6 |
|
7 |
return aTargetDistance <= aMyDistance; |
8 |
}
|
9 |
|
10 |
private function attack() :void { |
11 |
var aPuckOwner :Athlete = getPuckOwner(); |
12 |
|
13 |
// Does the puck have an owner?
|
14 |
if (aPuckOwner != null) { |
15 |
// Yeah, it has. Let's find out if the owner belongs to the opponents team.
|
16 |
if (doesMyTeamHaveThePuck()) { |
17 |
if (amIThePuckOwner()) { |
18 |
// My team has the puck and I am the one who has it! Let's move
|
19 |
// towards the opponent's goal.
|
20 |
mBoid.steering = mBoid.steering + mBoid.seek(getOpponentGoalPosition()); |
21 |
|
22 |
} else { |
23 |
// My team has the puck, but a teammate has it. Is he ahead of me?
|
24 |
if (isAheadOfMe(aPuckOwner.boid)) { |
25 |
// Yeah, he is ahead of me. Let's just follow him to give some support
|
26 |
// during the attack.
|
27 |
mBoid.steering = mBoid.steering + mBoid.followLeader(aPuckOwner.boid); |
28 |
mBoid.steering = mBoid.steering + mBoid.separation(); |
29 |
} else { |
30 |
// Nope, the teammate with the puck is behind me. In that case
|
31 |
// let's hold our current position with some separation from the
|
32 |
// other, so we prevent crowding.
|
33 |
mBoid.steering = mBoid.steering + mBoid.separation(); |
34 |
}
|
35 |
}
|
36 |
} else { |
37 |
// The opponent has the puck! Stop the attack
|
38 |
// and try to steal it.
|
39 |
mBrain.popState(); |
40 |
mBrain.pushState(stealPuck); |
41 |
}
|
42 |
} else { |
43 |
// Puck has no owner, so there is no point to keep
|
44 |
// attacking. It's time to re-organize and start pursuing the puck.
|
45 |
mBrain.popState(); |
46 |
mBrain.pushState(pursuePuck); |
47 |
}
|
48 |
}
|
49 |
}
|
Si el líder (que es el propietario del disco) está por encima del atleta que ejecuta el código, entonces el atleta debe seguir al líder tal como lo hacía antes (líneas 27 y 28). Si el líder está detrás de él, el atleta debe mantener su posición actual, manteniendo una distancia mínima entre los demás (línea 33).
El resultado es un poco más convincente que la implementación del ataque attack
inicial:
Consejo: Al ajustar los cálculos de distancia y las comparaciones en el método isAheadOfMe()
, es posible modificar la forma en que los atletas mantienen sus posiciones actuales.
Robando el disco
El estado final en el proceso de ataque es stealPuck
, que se activa cuando el equipo contrario tiene el disco. El objetivo principal del estado de StealPuck
es robar el disco del oponente que lo lleva, para que el equipo pueda comenzar a atacar de nuevo:



Dado que la idea detrás de este estado es robar el disco del oponente, si el disco es recuperado por el equipo o se libera (es decir, no tiene dueño), stealPuck
saltará del cerebro y empujará el estado correcto al lidiar con la nueva situación:
1 |
class Athlete { |
2 |
// (...)
|
3 |
private function stealPuck() :void { |
4 |
// Does the puck have any owner?
|
5 |
if (getPuckOwner() != null) { |
6 |
// Yeah, it has, but who has it?
|
7 |
if (doesMyTeamHaveThePuck()) { |
8 |
// My team has the puck, so it's time to stop trying to steal
|
9 |
// the puck and start attacking.
|
10 |
mBrain.popState(); |
11 |
mBrain.pushState(attack); |
12 |
} else { |
13 |
// An opponent has the puck.
|
14 |
var aOpponentLeader :Athlete = getPuckOwner(); |
15 |
|
16 |
// Let's pursue him while mantaining a certain separation from
|
17 |
// the others to avoid that everybody will ocuppy the same
|
18 |
// position in the pursuit.
|
19 |
mBoid.steering = mBoid.steering + mBoid.pursuit(aOpponentLeader.boid); |
20 |
mBoid.steering = mBoid.steering + mBoid.separation(); |
21 |
}
|
22 |
} else { |
23 |
// The puck has no owner, it is probably running freely in the rink.
|
24 |
// There is no point to keep trying to steal it, so let's finish the 'stealPuck' state
|
25 |
// and switch to 'pursuePuck'.
|
26 |
mBrain.popState(); |
27 |
mBrain.pushState(pursuePuck); |
28 |
}
|
29 |
}
|
30 |
}
|
Si el disco tiene un dueño y él pertenece al equipo del oponente, el atleta debe perseguir al líder contrario y tratar de robar el disco. Para perseguir al líder del oponente, un atleta debe predecir dónde estará en el futuro cercano, para que pueda ser interceptado en su trayectoria. Eso es diferente de solo buscar al líder opositor.
Afortunadamente, esto se puede lograr fácilmente con el comportamiento perseguir (línea 19). Mediante el uso de una fuerza de persecución en el estado de StealPuck
, los atletas intentarán interceptar al líder del oponente, en lugar de simplemente seguirlo:
Previniendo un movimiento de robo lleno de gente
La implementación actual de StealPuck
funciona, pero en un juego real solo uno o dos atletas se acercan al líder oponente para robar el disco. El resto del equipo permanece en las áreas circundantes tratando de ayudar, lo que evita un patrón de robo abarrotado.
Se puede arreglar agregando un control de distancia (línea 17) antes de la búsqueda del líder del oponente:
1 |
class Athlete { |
2 |
// (...)
|
3 |
private function stealPuck() :void { |
4 |
// Does the puck have any owner?
|
5 |
if (getPuckOwner() != null) { |
6 |
// Yeah, it has, but who has it?
|
7 |
if (doesMyTeamHaveThePuck()) { |
8 |
// My team has the puck, so it's time to stop trying to steal
|
9 |
// the puck and start attacking.
|
10 |
mBrain.popState(); |
11 |
mBrain.pushState(attack); |
12 |
} else { |
13 |
// An opponent has the puck.
|
14 |
var aOpponentLeader :Athlete = getPuckOwner(); |
15 |
|
16 |
// Is the opponent with the puck close to me?
|
17 |
if (distance(aOpponentLeader, this) < 150) { |
18 |
// Yeah, he is close! Let's pursue him while mantaining a certain
|
19 |
// separation from the others to avoid that everybody will ocuppy the same
|
20 |
// position in the pursuit.
|
21 |
mBoid.steering = mBoid.steering.add(mBoid.pursuit(aOpponentLeader.boid)); |
22 |
mBoid.steering = mBoid.steering.add(mBoid.separation(50)); |
23 |
|
24 |
} else { |
25 |
// No, he is too far away. In the future, we will switch
|
26 |
// to 'defend' and hope someone closer to the puck can
|
27 |
// steal it for us.
|
28 |
// TODO: mBrain.popState();
|
29 |
// TODO: mBrain.pushState(defend);
|
30 |
}
|
31 |
}
|
32 |
} else { |
33 |
// The puck has no owner, it is probably running freely in the rink.
|
34 |
// There is no point to keep trying to steal it, so let's finish the 'stealPuck' state
|
35 |
// and switch to 'pursuePuck'.
|
36 |
mBrain.popState(); |
37 |
mBrain.pushState(pursuePuck); |
38 |
}
|
39 |
}
|
40 |
}
|
En lugar de perseguir ciegamente al líder del oponente, un atleta verificará si la distancia entre él y el líder oponente es menor que, digamos, 150
. Si eso es true
, la persecución ocurre normalmente, pero si la distancia es mayor que 150
, significa que el el atleta está demasiado lejos del líder oponente.
Si eso sucede, no tiene sentido seguir intentando robar el disco, ya que está demasiado lejos y probablemente ya haya compañeros de equipo tratando de hacer lo mismo. La mejor opción es hacer estallar StealPuck
desde el cerebro e impulsar el estado de defensa defense
(que se explicará en el siguiente tutorial). Por ahora, un atleta solo mantendrá su posición actual si el líder oponente está demasiado lejos.
El resultado es un patrón de robo más convincente y natural (sin amontonamiento):
Evitar oponentes mientras atacas
Hay un último truco que los atletas deben aprender para poder atacar con eficacia. En este momento, se mueven hacia la meta del oponente sin considerar a los oponentes en el camino. Un oponente debe ser visto como una amenaza, y debe evitarse.
Usando el comportamiento de evitar colisiones, los atletas pueden esquivar oponentes mientras se mueven:



Los oponentes serán vistos como obstáculos circulares. Como resultado de la naturaleza dinámica de los comportamientos de dirección, que se actualizan en cada bucle del juego, el patrón de evitación funcionará con gracia y suavidad para mover obstáculos (que es el caso aquí).
Para hacer que los atletas eviten oponentes (obstáculos), se debe agregar una sola línea al estado de ataque (línea 14):
1 |
class Athlete { |
2 |
// (...)
|
3 |
private function attack() :void { |
4 |
var aPuckOwner :Athlete = getPuckOwner(); |
5 |
|
6 |
// Does the puck have an owner?
|
7 |
if (aPuckOwner != null) { |
8 |
// Yeah, it has. Let's find out if the owner belongs to the opponents team.
|
9 |
if (doesMyTeamHaveThePuck()) { |
10 |
if (amIThePuckOwner()) { |
11 |
// My team has the puck and I am the one who has it! Let's move
|
12 |
// towards the opponent's goal, avoding any opponents along the way.
|
13 |
mBoid.steering = mBoid.steering + mBoid.seek(getOpponentGoalPosition()); |
14 |
mBoid.steering = mBoid.steering + mBoid.collisionAvoidance(getOpponentTeam().members); |
15 |
|
16 |
} else { |
17 |
// My team has the puck, but a teammate has it. Is he ahead of me?
|
18 |
if (isAheadOfMe(aPuckOwner.boid)) { |
19 |
// Yeah, he is ahead of me. Let's just follow him to give some support
|
20 |
// during the attack.
|
21 |
mBoid.steering = mBoid.steering + mBoid.followLeader(aPuckOwner.boid); |
22 |
mBoid.steering = mBoid.steering + mBoid.separation(); |
23 |
} else { |
24 |
// Nope, the teammate with the puck is behind me. In that case
|
25 |
// let's hold our current position with some separation from the
|
26 |
// other, so we prevent crowding.
|
27 |
mBoid.steering = mBoid.steering + mBoid.separation(); |
28 |
}
|
29 |
}
|
30 |
} else { |
31 |
// The opponent has the puck! Stop the attack
|
32 |
// and try to steal it.
|
33 |
mBrain.popState(); |
34 |
mBrain.pushState(stealPuck); |
35 |
}
|
36 |
} else { |
37 |
// Puck has no owner, so there is no point to keep
|
38 |
// attacking. It's time to re-organize and start pursuing the puck.
|
39 |
mBrain.popState(); |
40 |
mBrain.pushState(pursuePuck); |
41 |
}
|
42 |
}
|
43 |
}
|
Esta línea agregará una fuerza para evitar colisiones al atleta, que se combinará con las fuerzas que ya existen. Como resultado, el atleta evitará obstáculos al mismo tiempo que busca la meta del oponente.
A continuación se muestra una demostración de un atleta corriendo el estado de ataque attack
. Los oponentes son inamovibles para resaltar el comportamiento de evitación de colisiones:
Conclusión
Este tutorial explicaba la implementación del patrón de ataque utilizado por los atletas para robar y llevar el disco hacia el objetivo del oponente. Usando una combinación de conductas de manejo, los atletas ahora pueden realizar patrones de movimiento complejos, como seguir a un líder o perseguir al oponente con el disco.
Como se discutió anteriormente, la implementación del ataque tiene como objetivo simular lo que hacen los humanos, por lo que el resultado es una aproximación de un juego real. Al ajustar individualmente los estados que componen el ataque, puede producir una mejor simulación, o una que se ajuste a sus necesidades.
En el próximo tutorial, aprenderá cómo hacer que los atletas se defiendan. La inteligencia artificial se convertirá en una característica completa, capaz de atacar y defender, lo que resulta en un partido con 100% de equipos controlados por IA jugando uno contra el otro.
Referencias
- Sprite: Hockey Stadium en GraphicRiver
- Sprites: Jugadores de Hockey de Taylor J Glidden