Advertisement
  1. Game Development
  2. Artificial Intelligence

Crear un juego IA de Hockey sobre hielo utilizando comportamientos de dirección: ataque

Scroll to top
Read Time: 22 min
This post is part of a series called Create AI for a Hockey Game Using Steering Behaviors.
Create a Hockey Game AI Using Steering Behaviors: Foundation
Create a Hockey Game AI Using Steering Behaviors: Defense

() 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:

Una máquina de estados finitos basada en pila que representa el proceso de ataque.

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:

El estado inactivo y sus transiciones en el FSM que describen el proceso de ataque.

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:

El estado perseguirPuck y sus transiciones en el FSM que describe el proceso de ataque.

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 de ataque y sus transiciones en el FSM que describen el proceso de ataque.

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:

Compañeros de equipo ayudando al líder.

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:

El estado de StealPuck y sus transiciones en el FSM que describen el proceso de ataque.

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:

Comportamiento de evitación de colisiones utilizado para evitar oponentes.

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

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Game Development tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.