Spanish (Español) translation by CYC (you can also view the original English article)
Esta es la segunda parte de una introducción a GameplayKit Si aún no has revisado la primera parte, te recomiendo que leas primero ese tutorial antes de continuar con este.
Introducción
En este tutorial, voy a enseñarte sobre dos características más del framework GameplayKit que puedes aprovechar:
- agentes, objetivos y comportamientos
- búsqueda de ruta
Al utilizar agentes, objetivos y comportamientos, vamos a incorporar cierta inteligencia artificial básica (IA) en el juego que comenzamos en la primera parte de esta serie. La IA permitirá que nuestros puntos enemigos rojos y amarillos apunten y se muevan hacia nuestro punto jugador azul. También vamos a implementar el pathfinding (búsqueda de ruta) para extenderlo en esta IA y navegar alrededor de los obstáculos.
Para este tutorial, puedes usar tu copia del proyecto completo de la primera parte de esta serie o descargar una copia nueva del código fuente de GitHub.
1. Agentes, objetivos y comportamientos
En GameplayKit, los agentes, los objetivos y los comportamientos se utilizan en combinación entre sí para definir cómo se mueven los diferentes objetos en relación con el otro a lo largo de la escena. Para un solo objeto (o SKShapeNode
en nuestro juego), comienza creando un agent, representado por la clase GKAgent
. Sin embargo, para juegos 2D, como el nuestro, necesitamos usar la clase concreta GKAgent2D
.
La clase GKAgent
es una subclase de GKComponent
. Esto significa que tu juego necesita estar usando una estructura basada en entidades y componentes como les mostré en el primer tutorial de esta serie.
Los agentes representan la posición, el tamaño y la velocidad de un objeto. A continuación, agrega un behavior (comportamiento), representado por la clase GKBehaviour
, a este agente. Finalmente, crea un conjunto de goals (objetivos), representados por la clase GKGoal
, y los agrega al objeto de comportamiento. Los objetivos se pueden usar para crear muchos elementos de juego diferentes, por ejemplo:
- moviéndose hacia un agente
- alejándose de un agente
- agruparse junto con otros agentes
- vagar por una posición específica
Tu objeto de comportamiento supervisa y calcula todos los objetivos que agregas y luego los retransmite al agente. Veamos cómo funciona esto en la práctica.
Abre tu proyecto Xcode y navega hasta PlayerNode.swift. Primero debemos asegurarnos de que la clase PlayerNode
cumpla con el protocolo GKAgentDelegate
.
1 |
class PlayerNode: SKShapeNode, GKAgentDelegate { |
2 |
...
|
A continuación, agrega el siguiente bloque de código a la clase PlayerNode
.
1 |
var agent = GKAgent2D() |
2 |
|
3 |
// MARK: Agent Delegate
|
4 |
func agentWillUpdate(agent: GKAgent) { |
5 |
if let agent2D = agent as? GKAgent2D { |
6 |
agent2D.position = float2(Float(position.x), Float(position.y)) |
7 |
}
|
8 |
}
|
9 |
|
10 |
func agentDidUpdate(agent: GKAgent) { |
11 |
if let agent2D = agent as? GKAgent2D { |
12 |
self.position = CGPoint(x: CGFloat(agent2D.position.x), y: CGFloat(agent2D.position.y)) |
13 |
}
|
14 |
}
|
Comenzamos agregando una propiedad a la clase PlayerNode
para que siempre tengamos una referencia al objeto agente del jugador actual. A continuación, implementamos los dos métodos del protocolo GKAgentDelegate
. Al implementar estos métodos, nos aseguramos de que el punto del jugador que se muestra en la pantalla siempre refleje los cambios que GameplayKit realiza.
El método agentWillUpdate(_ :)
se llama justo antes de GameplayKit examina el comportamiento y los objetivos de ese agente para determinar dónde debe moverse. Del mismo modo, el método agentDidUpdate(_ :)
se llama directamente después de que GameplayKit haya completado este proceso.
Nuestra implementación de estos dos métodos asegura que el nodo que vemos en la pantalla refleje los cambios que GameplayKit realiza y que GameplayKit use la última posición del nodo al realizar sus cálculos.
A continuación, abre ContactNode.swift y reemplaza los contenidos del archivo con la siguiente implementación:
1 |
import UIKit |
2 |
import SpriteKit |
3 |
import GameplayKit |
4 |
|
5 |
class ContactNode: SKShapeNode, GKAgentDelegate { |
6 |
|
7 |
var agent = GKAgent2D() |
8 |
|
9 |
// MARK: Agent Delegate
|
10 |
func agentWillUpdate(agent: GKAgent) { |
11 |
if let agent2D = agent as? GKAgent2D { |
12 |
agent2D.position = float2(Float(position.x), Float(position.y)) |
13 |
}
|
14 |
}
|
15 |
|
16 |
func agentDidUpdate(agent: GKAgent) { |
17 |
if let agent2D = agent as? GKAgent2D { |
18 |
self.position = CGPoint(x: CGFloat(agent2D.position.x), y: CGFloat(agent2D.position.y)) |
19 |
}
|
20 |
}
|
21 |
}
|
Al implementar el protocolo GKAgentDelegate
en la clase ContactNode
, permitimos que todos los demás puntos de nuestro juego estén actualizados con GameplayKit y nuestro punto de jugador.
Ahora es el momento de establecer los comportamientos y los objetivos. Para que esto funcione, debemos ocuparnos de tres cosas:
- Agregar el agente del nodo del jugador a su entidad y configurar su delegado.
- Configurar agentes, comportamientos y objetivos para todos nuestros puntos enemigos.
- Actualizar todos estos agentes en el momento correcto.
En primer lugar, abre GameScene.swift y, al final del método didMoveToView(_:)
, agrega las siguientes dos líneas de código:
1 |
playerNode.entity.addComponent(playerNode.agent) |
2 |
playerNode.agent.delegate = playerNode |
Con estas dos líneas de código, agregamos el agente como un componente y establecemos que el delegado del agente sea el nodo en sí.
A continuación, reemplaza la implementación del método initialSpawn
con la siguiente implementación:
1 |
func initialSpawn() { |
2 |
for point in self.spawnPoints { |
3 |
let respawnFactor = arc4random() % 3 // Will produce a value between 0 and 2 (inclusive) |
4 |
|
5 |
var node: SKShapeNode? = nil |
6 |
|
7 |
switch respawnFactor { |
8 |
case 0: |
9 |
node = PointsNode(circleOfRadius: 25) |
10 |
node!.physicsBody = SKPhysicsBody(circleOfRadius: 25) |
11 |
node!.fillColor = UIColor.greenColor() |
12 |
case 1: |
13 |
node = RedEnemyNode(circleOfRadius: 75) |
14 |
node!.physicsBody = SKPhysicsBody(circleOfRadius: 75) |
15 |
node!.fillColor = UIColor.redColor() |
16 |
case 2: |
17 |
node = YellowEnemyNode(circleOfRadius: 50) |
18 |
node!.physicsBody = SKPhysicsBody(circleOfRadius: 50) |
19 |
node!.fillColor = UIColor.yellowColor() |
20 |
default: |
21 |
break
|
22 |
}
|
23 |
|
24 |
if let entity = node?.valueForKey("entity") as? GKEntity, |
25 |
let agent = node?.valueForKey("agent") as? GKAgent2D where respawnFactor != 0 { |
26 |
|
27 |
entity.addComponent(agent) |
28 |
agent.delegate = node as? ContactNode |
29 |
agent.position = float2(x: Float(point.x), y: Float(point.y)) |
30 |
agents.append(agent) |
31 |
|
32 |
let behavior = GKBehavior(goal: GKGoal(toSeekAgent: playerNode.agent), weight: 1.0) |
33 |
agent.behavior = behavior |
34 |
|
35 |
agent.mass = 0.01 |
36 |
agent.maxSpeed = 50 |
37 |
agent.maxAcceleration = 1000 |
38 |
}
|
39 |
|
40 |
node!.position = point |
41 |
node!.strokeColor = UIColor.clearColor() |
42 |
node!.physicsBody!.contactTestBitMask = 1 |
43 |
self.addChild(node!) |
44 |
}
|
45 |
}
|
El código más importante que hemos agregado se encuentra en la declaración if
que sigue a la instrucción switch
. Veamos este código línea por línea:
- Primero agregamos el agente a la entidad como un componente y configuramos su delegado.
- A continuación, asignamos la posición del agente y agregamos el agente a una matriz almacenada,
agents
. Añadimos esta propiedad a la claseGameScene
en un momento. - Luego creamos un objeto
GKBehavior
con un soloGKGoal
para apuntar al agente del jugador actual. El parámetroweight
en este inicializador se usa para determinar qué objetivos deben prevalecer sobre los demás. Por ejemplo, imagina que tienes un objetivo para dirigirte a un agente en particular y un objetivo para alejarte de otro agente, pero deseas que el objetivo de orientación tenga preferencia. En este caso, podrías asignarle un peso de1
a la meta de segmentación y un peso de0,5
a la meta de alejamiento. Este comportamiento se asigna al agente del nodo enemigo. - Por último, configuramos las propiedades de
mass
,maxSpeed
ymaxAcceleration
. Estos afectan la rapidez con que los objetos se pueden mover y girar. Siéntete libre de jugar con estos valores y ver cómo afecta el movimiento de los puntos enemigos.
A continuación, agrega las siguientes dos propiedades a la clase GameScene
:
1 |
var agents: [GKAgent2D] = [] |
2 |
var lastUpdateTime: CFTimeInterval = 0.0 |
La matriz agents
se usará para mantener una referencia a los agentes enemigos en la escena. La propiedad lastUpdateTime
se usará para calcular el tiempo transcurrido desde la última actualización de la escena.
Finalmente, reemplaza la implementación del método update(_:)
de la clase GameScene
con la siguiente implementación:
1 |
override func update(currentTime: CFTimeInterval) { |
2 |
/* Called before each frame is rendered */
|
3 |
self.camera?.position = playerNode.position |
4 |
|
5 |
if self.lastUpdateTime == 0 { |
6 |
lastUpdateTime = currentTime |
7 |
}
|
8 |
|
9 |
let delta = currentTime - lastUpdateTime |
10 |
lastUpdateTime = currentTime |
11 |
|
12 |
playerNode.agent.updateWithDeltaTime(delta) |
13 |
|
14 |
for agent in agents { |
15 |
agent.updateWithDeltaTime(delta) |
16 |
}
|
17 |
}
|
En el método update(_:)
, calculamos el tiempo transcurrido desde la última actualización de escena y luego actualizamos los agentes con ese valor.
Crea y ejecuta tu aplicación, y comienza a moverte por la escena. Verás que los puntos enemigos lentamente comenzarán a moverse hacia ti.
Como puedes ver, mientras los puntos enemigos apuntan al jugador actual, no navegan alrededor de las barreras blancas, sino que intentan moverse a través de ellos. Hagamos que los enemigos sean un poco más inteligentes con pathfinding.
2. Pathfinding o Búsqueda de ruta
Con el framework GameplayKit, puedes agregar rutas complejas a tu juego combinando cuerpos físicos con clases y métodos GameplayKit. Para nuestro juego, vamos a configurarlo de modo que los puntos enemigos apunten al punto del jugador y al mismo tiempo navegar alrededor de los obstáculos.
El Pathfinding en GameplayKit comienza con la creación de un gráfico de tu escena. Este gráfico es una colección de ubicaciones individuales, también conocidas como nodos, y conexiones entre estas ubicaciones. Estas conexiones definen cómo un objeto en particular puede moverse de una ubicación a otra. Un gráfico puede modelar los caminos disponibles en tu escena de una de tres maneras:
- Un espacio continuo que contiene obstáculos: este modelo de gráfico permite trayectorias suaves alrededor de obstáculos de un lugar a otro. Para este modelo, la clase
GKObstacleGraph
se usa para el gráfico, la claseGKPolygonObstacle
para obstáculos y la claseGKGraphNode2D
para nodos (ubicaciones). - Una grilla 2D simple: en este caso, las ubicaciones válidas solo pueden ser aquellas con coordenadas enteras. Este modelo de gráfico es útil cuando su escena tiene un diseño de cuadrícula distinto y no necesita caminos uniformes. Al usar este modelo, los objetos solo pueden moverse horizontal o verticalmente en una sola dirección a la vez. Para este modelo, la clase
GKGridGraph
se usa para el gráfico y la claseGKGridGraphNode
para nodos. - Una colección de ubicaciones y las conexiones entre ellas: este es el modelo de gráfico más genérico y se recomienda para los casos en que los objetos se mueven entre espacios distintos, pero su ubicación específica dentro de ese espacio no es esencial para la jugabilidad. Para este modelo, la clase
GKGraph
se usa para el gráfico y la claseGKGraphNode
para nodos.
Como queremos que el punto de nuestro jugador navegue alrededor de las barreras blancas, vamos a usar la clase GKObstacleGraph
para crear un gráfico de nuestra escena. Para comenzar, reemplaza la propiedad spawnPoints
en la clase GameScene
con lo siguiente:
1 |
let spawnPoints = [ |
2 |
CGPoint(x: 245, y: 3900), |
3 |
CGPoint(x: 700, y: 3500), |
4 |
CGPoint(x: 1250, y: 1500), |
5 |
CGPoint(x: 1200, y: 1950), |
6 |
CGPoint(x: 1200, y: 2450), |
7 |
CGPoint(x: 1200, y: 2950), |
8 |
CGPoint(x: 1200, y: 3400), |
9 |
CGPoint(x: 2550, y: 2350), |
10 |
CGPoint(x: 2500, y: 3100), |
11 |
CGPoint(x: 3000, y: 2400), |
12 |
CGPoint(x: 2048, y: 2400), |
13 |
CGPoint(x: 2200, y: 2200) |
14 |
]
|
15 |
var graph: GKObstacleGraph! |
La matriz spawnPoints
contiene algunas ubicaciones de spawn alteradas para los propósitos de este tutorial. Esto se debe a que actualmente GameplayKit solo puede calcular rutas entre objetos que están relativamente cerca uno del otro.
Debido a la gran distancia predeterminada entre los puntos en este juego, se deben agregar un par de nuevos puntos de generación para ilustrar la ruta. Ten en cuenta que también declaramos una propiedad graph
de tipo GKObstacleGraph
para mantener una referencia al gráfico que crearemos.
A continuación, agrega las siguientes dos líneas de código al comienzo del método didMoveToView(_:)
:
1 |
let obstacles = SKNode.obstaclesFromNodePhysicsBodies(self.children) |
2 |
graph = GKObstacleGraph(obstacles: obstacles, bufferRadius: 0.0) |
En la primera línea, creamos una serie de obstáculos de los cuerpos físicos en la escena. Luego creamos el objeto gráfico usando estos obstáculos. El parámetro bufferRadius
en este inicializador se puede usar para forzar a los objetos a no estar dentro de una cierta distancia de estos obstáculos. Estas líneas deben agregarse al inicio del método didMoveToView(_:)
, porque el gráfico que creamos se necesita para cuando se llama al método initialSpawn
.
Finalmente, reemplaza el método initialSpawn
con la siguiente implementación:
1 |
func initialSpawn() { |
2 |
let endNode = GKGraphNode2D(point: float2(x: 2048.0, y: 2048.0)) |
3 |
self.graph.connectNodeUsingObstacles(endNode) |
4 |
|
5 |
for point in self.spawnPoints { |
6 |
let respawnFactor = arc4random() % 3 // Will produce a value between 0 and 2 (inclusive) |
7 |
|
8 |
var node: SKShapeNode? = nil |
9 |
|
10 |
switch respawnFactor { |
11 |
case 0: |
12 |
node = PointsNode(circleOfRadius: 25) |
13 |
node!.physicsBody = SKPhysicsBody(circleOfRadius: 25) |
14 |
node!.fillColor = UIColor.greenColor() |
15 |
case 1: |
16 |
node = RedEnemyNode(circleOfRadius: 75) |
17 |
node!.physicsBody = SKPhysicsBody(circleOfRadius: 75) |
18 |
node!.fillColor = UIColor.redColor() |
19 |
case 2: |
20 |
node = YellowEnemyNode(circleOfRadius: 50) |
21 |
node!.physicsBody = SKPhysicsBody(circleOfRadius: 50) |
22 |
node!.fillColor = UIColor.yellowColor() |
23 |
default: |
24 |
break
|
25 |
}
|
26 |
|
27 |
if let entity = node?.valueForKey("entity") as? GKEntity, |
28 |
let agent = node?.valueForKey("agent") as? GKAgent2D where respawnFactor != 0 { |
29 |
|
30 |
entity.addComponent(agent) |
31 |
agent.delegate = node as? ContactNode |
32 |
agent.position = float2(x: Float(point.x), y: Float(point.y)) |
33 |
agents.append(agent) |
34 |
|
35 |
/*let behavior = GKBehavior(goal: GKGoal(toSeekAgent: playerNode.agent), weight: 1.0)
|
36 |
agent.behavior = behavior*/
|
37 |
|
38 |
/*** BEGIN PATHFINDING ***/
|
39 |
let startNode = GKGraphNode2D(point: agent.position) |
40 |
self.graph.connectNodeUsingObstacles(startNode) |
41 |
|
42 |
let pathNodes = self.graph.findPathFromNode(startNode, toNode: endNode) as! [GKGraphNode2D] |
43 |
|
44 |
if !pathNodes.isEmpty { |
45 |
let path = GKPath(graphNodes: pathNodes, radius: 1.0) |
46 |
|
47 |
let followPath = GKGoal(toFollowPath: path, maxPredictionTime: 1.0, forward: true) |
48 |
let stayOnPath = GKGoal(toStayOnPath: path, maxPredictionTime: 1.0) |
49 |
|
50 |
let behavior = GKBehavior(goals: [followPath, stayOnPath]) |
51 |
agent.behavior = behavior |
52 |
}
|
53 |
|
54 |
self.graph.removeNodes([startNode]) |
55 |
/*** END PATHFINDING ***/
|
56 |
|
57 |
agent.mass = 0.01 |
58 |
agent.maxSpeed = 50 |
59 |
agent.maxAcceleration = 1000 |
60 |
}
|
61 |
|
62 |
node!.position = point |
63 |
node!.strokeColor = UIColor.clearColor() |
64 |
node!.physicsBody!.contactTestBitMask = 1 |
65 |
self.addChild(node!) |
66 |
}
|
67 |
|
68 |
self.graph.removeNodes([endNode]) |
69 |
}
|
Comenzamos el método creando un objeto GKGraphNode2D
con las coordenadas de generación de jugador predeterminadas. A continuación, conectamos este nodo al gráfico para que se pueda usar al encontrar rutas.
La mayor parte del método InitialSpawn
permanece sin cambios. He agregado algunos comentarios para mostrarte dónde se ubica la parte de identificación de la ruta en el primer enunciado if
. Repasemos este código paso a paso:
- Creamos otra instancia de
GKGraphNode2D
y conectamos esto al gráfico. - Creamos una serie de nodos que conforman una ruta llamando al método
findPathFromNode(_:toNode:)
en nuestro gráfico. - Si una serie de nodos de ruta se ha creado con éxito, entonces creamos una ruta a partir de ellos. El parámetro
radius
funciona de manera similar al parámetrobufferRadius
desde antes y define cuánto puede alejarse un objeto de la ruta creada. - Creamos dos objetos
GKGoal
, uno para seguir el camino y otro para permanecer en el camino. El parámetromaxPredictionTime
permite calcular de la mejor manera posible si algo va a interrumpir el objeto de seguir/permanecer en esa ruta particular. - Por último, creamos un nuevo comportamiento con estos dos objetivos y lo asignamos al agente.
También observarás que eliminamos los nodos que creamos del gráfico una vez que hemos terminado con ellos. Esta es una buena práctica que debes seguir, ya que garantiza que los nodos que has creado no interfieran con ningún otro cálculo de pathfinding más adelante.
Crea y ejecuta tu aplicación por última vez, y verás dos puntos aparecer muy cerca de ti y comenzar a moverte hacia ti. Es posible que debas ejecutar el juego varias veces si ambos aparecen como puntos verdes.
¡Importante!
En este tutorial, utilizamos la función pathfinding de GameplayKit para permitir que los puntos enemigos se dirijan al punto del jugador alrededor de los obstáculos. Ten en cuenta que esto fue solo para un ejemplo práctico de pathfinding.
Para un juego de producción real, sería mejor implementar esta funcionalidad combinando el objetivo de segmentación de jugadores de este tutorial con un objetivo de evitar obstáculos creado con el método de conveniencia init (toAvoidObstacles:maxPredictionTime:)
, que puedes leer más sobre en la Referencia de Clase GKGoal
.
Conclusión
En este tutorial, te mostré cómo puedes utilizar agentes, objetivos y comportamientos en juegos que tienen una estructura de componente de entidad. Si bien solo creamos tres objetivos en este tutorial, hay muchos más disponibles para ti, sobre los cuales puedes obtener más información en la Referencia de clase de GKGoal
.
También te mostré cómo implementar algunos pathfinding avanzados en tu juego creando un gráfico, un conjunto de obstáculos y objetivos para seguir estos caminos.
Como puedes ver, hay una gran cantidad de funcionalidades disponibles a través del framework GameplayKit. En la tercera y última parte de esta serie, te enseñaré sobre los generadores de valores aleatorios de GameplayKit y cómo crear tu propio sistema de reglas para introducir algo de lógica difusa en tu juego.
Como siempre, asegúrate de dejar tus comentarios y opiniones a continuación.