Advertisement
  1. Code
  2. Coding Fundamentals
  3. Game Development

Comprender los comportamientos de dirección: Gestor de movimiento

Scroll to top
Read Time: 13 min
This post is part of a series called Understanding Steering Behaviors.
Understanding Steering Behaviors: Pursuit and Evade
Understanding Steering Behaviors: Collision Avoidance

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

Los comportamientos de dirección son excelentes para crear patrones de movimiento realistas, pero son aún mayores si puede controlarlos, utilizarlos y combinarlos fácilmente. En este tutorial, discutiré y cubriré la implementación de un gestor de movimiento para todos nuestros comportamientos previamente discutidos.

Nota: Aunque este tutorial está escrito usando AS3 y Flash, debería ser capaz de utilizar las mismas técnicas y conceptos en casi cualquier entorno de desarrollo de juegos. Debe tener una comprensión básica de los vectores matemáticos.


Combinando las Fuerzas Directivas

Como se discutió anteriormente, cada comportamiento de dirección produce una fuerza resultante (llamada "fuerza de dirección") que se agrega al vector de velocidad. La dirección y la magnitud de esa fuerza impulsarán al personaje, lo que hará que se mueva según un patrón (seek, flee, wander, etc.). El cálculo general es:

1
steering = seek(); // this can be any behavior

2
steering = truncate (steering, max_force)
3
steering = steering / mass
4
5
velocity = truncate (velocity + steering , max_speed)
6
position = position + velocity

Como la fuerza de dirección es un vector, se puede agregar a cualquier otro vector (como la velocidad). Sin embargo, la verdadera "magia" radica en el hecho de que puede agregar varias fuerzas de dirección juntas: es tan simple como:

1
steering = nothing(); // the null vector, meaning "zero force magnitude"

2
steering = steering + seek();
3
steering = steering + flee();
4
(...)
5
steering = truncate (steering, max_force)
6
steering = steering / mass
7
8
velocity = truncate (velocity + steering , max_speed)
9
position = position + velocity

Las fuerzas de dirección combinadas darán como resultado un vector que representa todas esas fuerzas. En el fragmento de código anterior, la fuerza de dirección resultante hará que el personaje busque algo mientras que al mismo tiempo huirá de otra cosa.

Compruebe a continuación algunos ejemplos de fuerzas de dirección combinadas para producir una sola fuerza de dirección:

Steering forces combined to produce a single steering forceSteering forces combined to produce a single steering forceSteering forces combined to produce a single steering force
Fuerzas de dirección combinadas.

Patrones complejos sin esfuerzo

La combinación de fuerzas de dirección producirá patrones de movimiento extremadamente complejos sin esfuerzo. Imagine lo difícil que sería escribir código para que un personaje busque algo, pero al mismo tiempo evite un área específica, sin usar vectores y fuerzas.

Eso requeriría el cálculo de distancias, áreas, caminos, gráficos y similares. Si las cosas se están moviendo, todos esos cálculos deben repetirse de vez en cuando, porque el entorno cambia constantemente.

Con comportamientos de dirección, todas las fuerzas son dinámicas. Deben calcularse cada actualización del juego, por lo que reaccionarán natural y sin problemas ante los cambios del entorno.

La demostración a continuación muestra barcos que buscarán el cursor del mouse, pero huirán del centro de la pantalla, ambos al mismo tiempo:


Los barcos buscarán el cursor del mouse (gris), pero huirán del centro de la pantalla (naranja). Haga clic para mostrar las fuerzas.

Gestor de movimiento

Con el fin de utilizar varios comportamientos de dirección al mismo tiempo de una manera simple y fácil, un gestor de movimientos es útil. La idea es crear una "caja negra" que se pueda conectar a cualquier entidad existente, lo que le permitirá realizar esos comportamientos.

El administrador tiene una referencia a la entidad a la que está conectado (el "host"). El administrador proporcionará al host un conjunto de métodos, como seek()flee(). Cada vez que se invocan tales métodos, el administrador actualiza sus propiedades internas para producir un vector de fuerza de dirección.

Después de que el administrador procese todas las invocaciones, agregará la fuerza de dirección resultante al vector de velocidad del host. Eso cambiará la dirección y magnitud del vector de velocidad del host de acuerdo con los comportamientos activos.

La siguiente figura muestra la arquitectura:

Movement manager: plugin architecture.
Gestor de movimientos: arquitectura de complementos.

Hacer cosas genéricas

El administrador tiene un conjunto de métodos, cada uno representa un comportamiento distinto. Cada comportamiento debe ser provisto con diferentes piezas de información externa para poder funcionar.

El comportamiento de búsqueda seek, por ejemplo, necesita un punto en el espacio que se usa para calcular la fuerza de dirección hacia ese lugar; persigue pursue necesita varias piezas de información de su objetivo, como la posición actual y la velocidad. Un punto en el espacio se puede expresar como una instancia de Point o Vector2D, ambas clases bastante comunes en cualquier marco.

Sin embargo, el objetivo utilizado en el comportamiento perseguido puede ser cualquier cosa. Para que el gestor de movimientos sea lo suficientemente genérico, necesita recibir un objetivo que, independientemente de su tipo, pueda responder algunas "preguntas", como "¿Cuál es su velocidad actual?". Usando algunos principios de programación orientada a objetos, se puede lograr con interfaces.

Suponiendo que la interfaz IBoid describe una entidad que puede ser manejada por el gestor de movimiento, cualquier clase en el juego puede usar comportamientos de dirección, siempre que implemente IBoid. Esa interfaz tiene la siguiente estructura:

1
public interface IBoid
2
{
3
  function getVelocity() :Vector3D;
4
	function getMaxVelocity() :Number;
5
	function getPosition() :Vector3D;
6
	function getMass() :Number;
7
}

Estructura del gestor de movimiento

Ahora que el administrador puede interactuar con todas las entidades del juego de forma genérica, se puede crear su estructura básica. El administrador se compone de dos propiedades (la fuerza de dirección resultante y la referencia del host) y un conjunto de métodos públicos, uno para cada comportamiento:

1
public class SteeringManager
2
{
3
	public var steering :Vector3D;
4
	public var host :IBoid;
5
6
	// The constructor

7
	public function SteeringManager(host :IBoid) {
8
		this.host	= host;
9
		this.steering 	= new Vector3D(0, 0);
10
	}
11
12
	// The public API (one method for each behavior)

13
	public function seek(target :Vector3D, slowingRadius :Number = 20) :void {}
14
	public function flee(target :Vector3D) :void {}
15
	public function wander() :void {}
16
	public function evade(target :IBoid) :void {}
17
	public function pursuit(target :IBoid) :void {}
18
19
	// The update method. 

20
	// Should be called after all behaviors have been invoked

21
	public function update() :void {}
22
23
	// Reset the internal steering force.

24
	public function reset() :void {}
25
26
	// The internal API

27
	private function doSeek(target :Vector3D, slowingRadius :Number = 0) :Vector3D {}
28
	private function doFlee(target :Vector3D) :Vector3D {}
29
	private function doWander() :Vector3D {}
30
	private function doEvade(target :IBoid) :Vector3D {}
31
	private function doPursuit(target :IBoid) :Vector3D {}
32
}

Cuando se crea una instancia del administrador, debe recibir una referencia al host al que está conectado. Permitirá al administrador cambiar el vector de velocidad del host de acuerdo con los comportamientos activos.

Cada comportamiento está representado por dos métodos, uno público y uno privado. Usando seek como ejemplo:

1
public function seek(target :Vector3D, slowingRadius :Number = 20) :void {}
2
private function doSeek(target :Vector3D, slowingRadius :Number = 0) :Vector3D {}

Se invocará seek() para decirle al administrador que aplique ese comportamiento específico. El método no tiene valor de retorno y sus parámetros están relacionados con el comportamiento en sí, como un punto en el espacio. Bajo el capó se invocará el método privado doSeek () y su valor de retorno, la fuerza de manejo calculada para ese comportamiento específico, se agregará a la propiedad steering.

El siguiente código demuestra la implementación de seek:

1
// The publish method. 

2
// Receives a target to seek and a slowingRadius (used to perform arrive).

3
public function seek(target :Vector3D, slowingRadius :Number = 20) :void {
4
	steering.incrementBy(doSeek(target, slowingRadius));
5
}
6
7
// The real implementation of seek (with arrival code included)

8
private function doSeek(target :Vector3D, slowingRadius :Number = 0) :Vector3D {
9
	var force :Vector3D;
10
	var distance :Number;
11
12
	desired = target.subtract(host.getPosition());
13
14
	distance = desired.length;
15
	desired.normalize();
16
17
	if (distance <= slowingRadius) {
18
		desired.scaleBy(host.getMaxVelocity() * distance/slowingRadius);
19
	} else {
20
		desired.scaleBy(host.getMaxVelocity());
21
	}
22
23
	force = desired.subtract(host.getVelocity());
24
25
	return force;
26
}

Todos los demás métodos de comportamiento se implementan de una manera muy similar. El método pursuit(), por ejemplo, se verá así:

1
public function pursuit(target :IBoid) :void {
2
	steering.incrementBy(doPursuit(target));
3
}
4
5
private function doPursuit(target :IBoid) :Vector3D {
6
	distance = target.getPosition().subtract(host.getPosition());
7
8
	var updatesNeeded :Number = distance.length / host.getMaxVelocity();
9
10
	var tv :Vector3D = target.getVelocity().clone();
11
	tv.scaleBy(updatesNeeded);
12
13
	targetFuturePosition = target.getPosition().clone().add(tv);
14
15
	return doSeek(targetFuturePosition);
16
}

Usando el código de los tutoriales anteriores, todo lo que tienes que hacer es adaptarlos en forma de behavior() y doBehavior(), para que puedan agregarse al gestor de movimientos.


Aplicación y actualización de las fuerzas de dirección

Cada vez que se invoca un método de comportamiento, la fuerza resultante que produce se agrega a la propiedad steering. Como consecuencia, la propiedad acumulará todas las fuerzas de dirección.

Cuando se han invocado todos los comportamientos, el administrador debe aplicar la fuerza de dirección actual a la velocidad de los hosts, por lo que se moverá de acuerdo con los comportamientos activos. Se realiza en el método update() del gestor de movimientos:

1
public function update():void {
2
	var velocity :Vector3D = host.getVelocity();
3
	var position :Vector3D = host.getPosition();
4
5
	truncate(steering, MAX_FORCE);
6
	steering.scaleBy(1 / host.getMass());
7
8
	velocity.incrementBy(steering);
9
	truncate(velocity, host.getMaxVelocity());
10
11
	position.incrementBy(velocity);
12
}

El método anterior debe ser invocado por el host (o cualquier otra entidad de juego) después de haber invocado todos los comportamientos, de lo contrario, el host nunca cambiará su vector de velocidad para que coincida con los comportamientos activos.


Uso

Supongamos que una clase llamada Prey debe moverse usando el comportamiento de la dirección, pero por el momento no tiene código de dirección ni gestor de movimiento. Su estructura se verá así:

1
public class Prey
2
{
3
	public var position  :Vector3D;
4
	public var velocity  :Vector3D;
5
	public var mass      :Number;
6
7
	public function Prey(posX :Number, posY :Number, totalMass :Number) {
8
		position 	= new Vector3D(posX, posY);
9
		velocity 	= new Vector3D(-1, -2);
10
		mass	 	= totalMass;
11
12
		x = position.x;
13
		y = position.y;
14
	}
15
16
	public function update():void {
17
		velocity.normalize();
18
		velocity.scaleBy(MAX_VELOCITY);
19
		velocity.scaleBy(1 / mass);
20
21
		truncate(velocity, MAX_VELOCITY);
22
		position = position.add(velocity);
23
24
		x = position.x;
25
		y = position.y;
26
	}
27
}

Usando esa estructura, las instancias de clase pueden moverse usando la integración de Euler, al igual que la primera demostración del tutorial seek. Para poder usar el administrador, necesita una propiedad que haga referencia al gestor de movimiento y debe implementar la interfaz de IBoid:

1
public class Prey implements IBoid
2
{
3
	public var position  :Vector3D;
4
	public var velocity  :Vector3D;
5
	public var mass      :Number;
6
	public var steering  :SteeringManager;
7
8
	public function Prey(posX :Number, posY :Number, totalMass :Number) {
9
		position 	= new Vector3D(posX, posY);
10
		velocity 	= new Vector3D(-1, -2);
11
		mass	 	= totalMass;
12
		steering 	= new SteeringManager(this);
13
14
		x = position.x;
15
		y = position.y;
16
	}
17
18
	public function update():void {
19
		velocity.normalize();
20
		velocity.scaleBy(MAX_VELOCITY);
21
		velocity.scaleBy(1 / mass);
22
23
		truncate(velocity, MAX_VELOCITY);
24
		position = position.add(velocity);
25
26
		x = position.x;
27
		y = position.y;
28
	}
29
30
	// Below are the methods the interface IBoid requires.

31
32
	public function getVelocity() :Vector3D {
33
		return velocity;
34
	}
35
36
	public function getMaxVelocity() :Number {
37
		return 3;
38
	}
39
40
	public function getPosition() :Vector3D {
41
		return position;
42
	}
43
44
	public function getMass() :Number {
45
		return mass;
46
	}
47
}

El método update() se debe cambiar en consecuencia para que el administrador se pueda actualizar también:

1
public function update():void {
2
	// Make the prey wander around...

3
	steering.wander();
4
5
	// Update the manager so it will change the prey velocity vector.

6
	// The manager will perform the Euler intergration as well, changing

7
	// the "position" vector.

8
	steering.update();
9
10
	// After the manager has updated its internal structures, all we must

11
	// do is update our position according to the "position" vector.

12
	x = position.x;
13
	y = position.y;
14
}

Todos los comportamientos se pueden usar al mismo tiempo, siempre que todas las llamadas a métodos se realicen antes de la invocación del administrador update(), que aplica la fuerza de dirección acumulada al vector de velocidad del host.

El siguiente código muestra otra versión del método update() de Prey , pero esta vez buscará una posición en el mapa y evadirá a otro personaje (ambos al mismo tiempo):

1
public function update():void {
2
	var destination :Vector3D = getDestination(); // the place to seek

3
	var hunter :IBoid = getHunter(); // get the entity who is hunting us

4
5
	// Seek the destination and evade the hunter (at the same time!)

6
	steering.seek(destination);
7
	steering.evade(hunter);
8
9
	// Update the manager so it will change the prey velocity vector.

10
	// The manager will perform the Euler intergration as well, changing

11
	// the "position" vector.

12
	steering.update();
13
14
	// After the manager has updated its internal structures, all we must

15
	// do is update our position according to the "position" vector.

16
	x = position.x;
17
	y = position.y;
18
}

Demo

ManifestaciónLa demostración a continuación muestra un patrón de movimiento complejo donde se combinan varios comportamientos. Hay dos tipos de personajes en la escena: el Cazador Hunter y la presa Prey.

El cazador perseguirá una presa si se acerca lo suficiente; perseguirá mientras dure el suministro de resistencia; cuando se queda sin energía, la persecución se interrumpe y el cazador vagará hasta que recupere sus niveles de resistencia.

Aquí está el método update() de Hunter:

1
public function update():void {
2
	if (resting && stamina++ >= MAX_STAMINA) {
3
		resting = false;
4
	}
5
6
	if (prey != null && !resting) {
7
		steering.pursuit(prey);
8
		stamina -= 2;
9
10
		if (stamina <= 0) {
11
			prey = null;
12
			resting = true;
13
		}
14
	} else {
15
		steering.wander();
16
		prey = getClosestPrey(position);
17
	}
18
19
	steering.update();
20
21
	x = position.x;
22
	y = position.y;
23
}

La presa vagará indefinidamente. Si el cazador se acerca demasiado, evadirá. Si el cursor del mouse está cerca y no hay ningún cazador alrededor, la presa buscará el cursor del mouse.

Aquí está el método update() Prey:

1
public function update():void {
2
	var distance :Number = Vector3D.distance(position, Game.mouse);
3
4
	hunter = getHunterWithinRange(position);
5
6
	if (hunter != null) {
7
		steering.evade(hunter);
8
	}
9
10
	if (distance <= 300 && hunter == null) {
11
		steering.seek(Game.mouse, 30);
12
13
	} else if(hunter == null){
14
		steering.wander();
15
	}
16
17
	steering.update();
18
19
	x = position.x;
20
	y = position.y;
21
}

El resultado final (gris es wander, verde es seek, naranja es pursue, rojo es evade):


La caza. Haga clic para mostrar las fuerzas.

Conclusión

Un gestor de movimiento es muy útil para controlar varios comportamientos de dirección al mismo tiempo. La combinación de tales comportamientos puede producir patrones de movimiento muy complejos, permitiendo que una entidad de juego busque una cosa al mismo tiempo que evade a otra.

Espero que les haya gustado el sistema de gestión discutido e implementado en este tutorial y usarlo en sus juegos. ¡Gracias por leer! No olvides mantenerte al día siguiéndonos en Twitter, Facebook o Google+.

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