Advertisement
  1. Game Development
  2. Artificial Intelligence

Crear un juego de hockey IA Uso de los comportamientos de dirección: Mecánica de juegos

Scroll to top
Read Time: 14 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: Defense

() translation by (you can also view the original English article)

En publicaciones pasadas de esta serie, nos hemos centrado en los conceptos detrás de la inteligencia artificial que hemos estado aprendiendo. En esta parte, envolveremos toda la implementación en un juego de hockey completamente jugable. Aprenderá cómo agregar las piezas faltantes necesarias para convertir esto en un juego, como puntaje, potenciadores y un poco de diseño del juego.

Resultado final

A continuación se muestra el juego que se implementará utilizando todos los elementos descritos en este tutorial.

Diseño de juegos de pensamiento

Las partes anteriores de esta serie se centraron en explicar cómo funciona el juego AI. Cada parte detalla un aspecto particular del juego, como cómo se mueven los atletas y cómo se implementan el ataque y la defensa. Se basaban en conceptos como comportamientos de dirección y máquinas de estado finito basadas en pila.

Sin embargo, para hacer un juego completamente jugable, todos esos aspectos deben estar incluidos en una mecánica básica del juego. La elección más obvia sería implementar todas las reglas oficiales de un partido de hockey oficial, pero eso requeriría mucho trabajo y tiempo. Tomemos un enfoque de fantasía más simple en su lugar.

Todas las reglas de hockey serán reemplazadas por una sola: si llevas el disco y te toca un oponente, ¡te congelas y te rompes en un millón de piezas! Hará que el juego sea más simple de jugar y divertido para ambos jugadores: el que lleva el disco y el que intenta recuperarlo.

Para mejorar esta mecánica, agregaremos algunos potenciadores. Ayudarán al jugador a anotar y hacer que el juego sea un poco más dinámico.

Agregar la capacidad de anotación

Comencemos con el sistema de puntuación, responsable de determinar quién gana o pierde. Un equipo anota cada vez que el disco ingresa al objetivo del oponente.

La forma más fácil de implementar esto es usar dos rectángulos superpuestos:

Overlapped rectangles describing the goal area If the puck collides with the red rectangle the team scoresOverlapped rectangles describing the goal area If the puck collides with the red rectangle the team scoresOverlapped rectangles describing the goal area If the puck collides with the red rectangle the team scores
Rectángulos superpuestos que describen el área de la meta. Si el disco choca con el rectángulo rojo, el equipo anota.

El rectángulo verde representa el área ocupada por la estructura objetivo (el marco y la red). Funciona como un bloque sólido, por lo que el disco y los atletas no podrán moverse a través de él; ellos se recuperaran

El rectángulo rojo representa el "área de puntuación". Si el disco se superpone a este rectángulo, significa que un equipo acaba de marcar.

El rectángulo rojo es más pequeño que el verde, y se coloca delante de él, por lo que si el disco toca el objetivo en cualquier lado, excepto en el delantero, se recuperará y no se agregará ningún puntaje:

A few examples of how the puck would behave if it touched the rectangles while movingA few examples of how the puck would behave if it touched the rectangles while movingA few examples of how the puck would behave if it touched the rectangles while moving
Algunos ejemplos de cómo se comportaría el disco si tocaba los rectángulos mientras se movía.

Organizar todo después de que alguien puntúe

Después de que un equipo anota, todos los atletas deben regresar a su posición inicial y el disco debe colocarse nuevamente en el centro de la pista. Después de este proceso, el partido puede continuar.

Mover a los atletas a su posición inicial

Como se explicó en la primera parte de esta serie, todos los atletas tienen un estado de IA llamado prepareForMatch que los moverá hacia la posición inicial, y hará que se detengan sin problemas allí.

Cuando el disco se superpone a una de las "áreas de puntuación", cualquier estado AI activo de todos los atletas se elimina y prepareForMatch se introduce en el cerebro. Dondequiera que estén los atletas, volverán a su posición inicial después de unos segundos:

Mover la cámara hacia el centro de la pista

Dado que la cámara siempre sigue el disco, si es teletransportado directamente al centro de la pista después de que alguien anota, la vista actual cambiará abruptamente, lo que sería feo y confuso.

Una mejor forma de hacerlo es mover el disco suavemente hacia el centro de la pista; dado que la cámara sigue al disco, esto deslizará elegantemente la vista desde el objetivo al centro de la pista.

Esto se puede lograr cambiando el vector de velocidad del disco después de que golpee cualquier área de la portería. El nuevo vector de velocidad debe "empujar" el disco hacia el centro de la pista, por lo que se puede calcular como:

1
var c :Vector3D = getRinkCenter();
2
var p :Vector3D = puck.position;
3
4
var v :Vector3D = c - p;
5
v = normalize(v) * 100;
6
7
puck.velocity = v;

Al restar la posición del centro de la pista de la posición actual del disco, es posible calcular un vector que apunta directamente hacia el centro de la pista.

Después de normalizar este vector, se puede escalar por cualquier valor, como 100, que controla qué tan rápido se mueve el disco hacia el centro de la pista.

A continuación se muestra una imagen con una representación del nuevo vector de velocidad:

Calculation of a new velocity vector that will move the puck towards the rink centerCalculation of a new velocity vector that will move the puck towards the rink centerCalculation of a new velocity vector that will move the puck towards the rink center
Cálculo de un nuevo vector de velocidad que moverá el disco hacia el centro de la pista.

Este vector V se usa como el vector de velocidad del disco, por lo que el disco se moverá hacia el centro de la pista como se pretendía.

Para evitar cualquier comportamiento extraño mientras el disco se mueve hacia el centro de la pista, como una interacción con un atleta, el disco se desactiva durante el proceso. Como consecuencia, deja de interactuar con los atletas y se marca como invisible. El jugador no verá el disco en movimiento, pero la cámara lo seguirá.

Para decidir si el disco ya está en posición, la distancia entre él y el centro de la pista se calcula durante el movimiento. Si es menor a 10, por ejemplo, el disco está lo suficientemente cerca como para colocarse directamente en el centro de la pista y reactivarse para que la coincidencia pueda continuar.

Añadiendo Potenciadores

La idea detrás de los potenciadores es ayudar al jugador a alcanzar el objetivo principal del juego, que es anotar llevando el disco a la meta del oponente.

En aras del alcance, nuestro juego tendrá solo dos poderes: Ghost Help y Fear The Puck. El primero agrega tres atletas adicionales al equipo del jugador durante un tiempo, mientras que el segundo hace que los oponentes huyan del disco durante unos segundos.

Los potenciadores se agregan a ambos equipos cuando alguien anota.

Implementando el potenciador de "Ayuda fantasma"

Dado que todos los atletas agregados por el poder de Ayuda de Ghost son temporales, la clase de Athlete debe modificarse para permitir que un atleta se marque como un "fantasma". Si un atleta es un fantasma, se eliminará del juego luego de unos segundos.

A continuación se muestra la clase de Athlete, que resalta solo las adiciones realizadas para acomodar la funcionalidad fantasma:

1
public class Athlete
2
{
3
    // (...)

4
  private var mGhost :Boolean;        // tells if the athlete is a ghost (a powerup that adds new athletes to help steal the puck).

5
	private var mGhostCounter :Number;  // counts the time a ghost will remain active

6
	
7
	public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number) {
8
		// (...)

9
		mGhost = false;
10
		mGhostCounter = 0;
11
		
12
		// (...)

13
	}
14
    
15
    public function setGhost(theStatus :Boolean, theDuration :Number) :void {
16
		mGhost = theStatus;
17
		mGhostCounter = theDuration;
18
	}
19
	
20
	public function amIAGhost() :Boolean {
21
		return mGhost;
22
	}
23
	
24
	public function update() :void {
25
		// (...)

26
		
27
		// Update powerup counters and stuff

28
		updatePowerups();
29
		
30
		// (...)

31
	}
32
    
33
    public function updatePowerups() :void {
34
        // TODO.

35
    }
36
}

La propiedad mGhost es un booleano que indica si el atleta es un fantasma o no, mientras que mGhostCounter contiene la cantidad de segundos que el atleta debe esperar antes de retirarse del juego.

Esas dos propiedades son utilizadas por el método updatePowerups ():

1
private function updatePowerups():void {		
2
	// If the athlete is a ghost, it has a counter that controls

3
	// when it must be removed.

4
	if (amIAGhost()) {
5
		mGhostCounter -= time_elapsed;
6
		
7
		if (mGhostCounter <= 2) {
8
			// Make athlete flicker when it is about to be removed.

9
			flicker(0.5);
10
		}
11
		
12
		if (mGhostCounter <= 0) {
13
			// Time to leave this world! (again)

14
			kill();
15
		}
16
	}
17
}

El método updatePowerups (), llamado dentro de la rutina de actualización () del atleta, manejará todo el proceso de encendido en el atleta. En este momento todo lo que hace es verificar si el atleta actual es un fantasma o no. Si es así, la propiedad mGhostCounter se reduce por la cantidad de tiempo transcurrido desde la última actualización.

Cuando el valor de mGhostCounter llega a cero, significa que el atleta temporal ha estado activo durante el tiempo suficiente, por lo que debe retirarse del juego. Para que el jugador sea consciente de eso, el atleta comenzará a parpadear durante los últimos dos segundos antes de desaparecer.

Finalmente, es hora de implementar el proceso de agregar atletas temporales cuando se activa el encendido. Eso se realiza en el método powerupGhostHelp (), disponible en la lógica principal del juego:

1
private function powerupGhostHelp() :void {
2
	var aAthlete :Athlete;	
3
	
4
	for (var i:int = 0; i < 3; i++) {
5
        // Add the new athlete to the list of athletes

6
		aAthlete = addAthlete(RINK_WIDTH / 2, RINK_HEIGHT - 100);
7
        
8
        // Mark the athlete as a ghost which will be removed after 10 seconds.

9
		aAthlete.setGhost(true, 10);
10
	}
11
}

Este método itera sobre un ciclo que corresponde a la cantidad de atletas temporales que se agregan. Cada nuevo atleta se agrega al fondo de la pista y se marca como un fantasma.

Como se describió anteriormente, los atletas fantasmas se eliminarán del juego.

Implementando el potenciador "Temer al disco"

El poder de Fear The Puck hace que todos los oponentes huyan del disco por unos segundos.

Al igual que el encendido de Ayuda Fantastma, la clase de Athlete debe modificarse para adaptarse a esa funcionalidad:

1
public class Athlete
2
{
3
    // (...)

4
    private var mFearCounter :Number; // counts the time the athlete should evade from puck (when fear powerup is active).

5
6
	
7
	public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number) {
8
		// (...)

9
		mFearCounter = 0;
10
		
11
		// (...)

12
	}
13
    
14
	public function fearPuck(theDuration: Number = 2) :void {
15
		mFearCounter = theDuration;
16
	}
17
	
18
    // Returns true if the mFearCounter has a value and the athlete

19
    // is not idle or preparing for a match.

20
	private function shouldIEvadeFromPuck() :Boolean {
21
		return mFearCounter > 0 && mBrain.getCurrentState() != idle && mBrain.getCurrentState() != prepareForMatch;
22
	}
23
    
24
    private function updatePowerups():void {
25
    	if(mFearCounter > 0) {
26
			mFearCounter -= elapsed_time;
27
		}
28
		
29
		// (...)

30
	}
31
	
32
	public function update() :void {
33
		// (...)

34
		
35
		// Update powerup counters and stuff

36
		updatePowerups();
37
        
38
		// If the athlete is an AI-controlled opponent

39
		if (amIAnAiControlledOpponent()) {
40
			// Check if "fear of the puck" power-up is active.

41
            // If that's true, evade from puck.

42
			if(shouldIEvadeFromPuck()) {
43
				evadeFromPuck();
44
			}
45
        }			
46
		
47
		// (...)

48
	}
49
    
50
    public function evadeFromPuck() :void {
51
        // TODO

52
    }
53
}

Primero, se cambia el método updatePowerups () para disminuir la propiedad mFearCounter, que contiene la cantidad de tiempo que el atleta debe evitar el disco. La propiedad mFearCounter se cambia cada vez que se llama al método fearPuck ().

En el método de Athlete's update(), se agrega una prueba para verificar si el encendido debería llevarse a cabo. Si el atleta es un oponente controlado por la IA (amIAnAiControlledOpponent () devuelve true) y el atleta debe evadir el puck (ifIEvadeFromPuck () también es true), se invoca el método evadeFromPuck ().

El método evadeFromPuck () usa el comportamiento de evadir, lo que hace que una entidad evite por completo cualquier objeto y su trayectoria:

1
private function evadeFromPuck() :void {
2
	mBoid.steering = mBoid.steering + mBoid.evade(getPuck().getBoid());
3
}

Todo lo que el método evadeFromPuck () hace es agregar una fuerza de evacuación a la fuerza de dirección del atleta actual. Le hace evadir el disco sin ignorar las fuerzas de dirección ya agregadas, como la creada por el estado AI actualmente activo.

Para ser evadible, el disco debe comportarse como un boid, como lo hacen todos los atletas (más información sobre eso en la primera parte de la serie). Como consecuencia, una propiedad boid, que contiene la posición y velocidad actual del disco, debe agregarse a la clase Puck:

1
class Puck {
2
    // (...)

3
    private var mBoid :Boid;
4
    
5
    // (...)

6
    
7
    public function update() {
8
        // (...)

9
        mBoid.update();
10
    }
11
    
12
    public function getBoid() :Boid {
13
        return mBoid;
14
    }
15
    
16
    // (...)

17
}

Finalmente, actualizamos la lógica principal del juego para hacer que los oponentes teman al disco cuando se activa el encendido:

1
private function powerupFearPuck() :void {
2
	var i           :uint,
3
		athletes    :Array  = rightTeam.members,
4
		size        :uint   = athletes.length;
5
			
6
	for (i = 0; i < size; i++) {
7
		if (athletes[i] != null) {
8
            // Make athlete fear the puck for 3 seconds.

9
			athletes[i].fearPuck(3);
10
		}
11
	}
12
}

El método itera sobre todos los atletas oponentes (el equipo correcto, en este caso), llamando al método fearkPuck () de cada uno de ellos. Esto activará la lógica que hace que los atletas teman el disco durante unos segundos, como se explicó anteriormente.

Congelación y destrozo

La última adición al juego es la parte helada y demoledora. Se realiza en la lógica principal del juego, donde una rutina verifica si los atletas del equipo izquierdo se superponen con los atletas del equipo correcto.

Este control superpuesto se realiza automáticamente por el motor de juego Flixel, que invoca una devolución de llamada cada vez que se encuentra una superposición:

1
private function athletesOverlapped(theLeftAthlete :Athlete, theRightAthlete :Athlete) :void {
2
    // Does the puck have an owner?	

3
	if (mPuck.owner != null) {
4
        // Yes, it does.

5
		if (mPuck.owner == theLeftAthlete) {
6
            //Puck's owner is the left athlete

7
			theLeftAthlete.shatter();
8
			mPuck.setOwner(theRightAthlete);
9
10
		} else if (mPuck.owner == theRightAthlete) {
11
            //Puck's owner is the right athlete

12
			theRightAthlete.shatter();
13
			mPuck.setOwner(theLeftAthlete);
14
		}
15
	}
16
}

Esta devolución de llamada recibe como parámetros a los atletas de cada equipo que se superponen. Una prueba comprueba si el propietario del disco no es nulo, lo que significa que lo está llevando alguien.

En ese caso, el dueño del disco se compara con los atletas que se solaparon. Si uno de ellos lleva el disco (por lo que es el propietario del disco), queda destrozado y la propiedad del disco pasa al otro atleta.

El método shatter () en la clase de Athlete marcará al atleta como inactivo y lo colocará en el fondo de la pista después de unos segundos. También emitirá varias partículas que representan piezas de hielo, pero este tema se tratará en otra publicación.

Conclusión

En este tutorial, implementamos algunos elementos necesarios para convertir nuestro prototipo de hockey en un juego completamente jugable. Intenté centrarme en los conceptos detrás de cada uno de esos elementos, en lugar de ponerlos en práctica en el motor del juego X o Y.

El enfoque de congelación y fragmentación utilizado para el juego puede sonar demasiado fantástico, pero ayuda a mantener el proyecto manejable. Las reglas deportivas son muy específicas y su implementación puede ser complicada.

¡Al agregar algunas pantallas y algunos elementos de HUD, puedes crear tu propio juego de hockey completo a partir de esta demostración!

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.