Advertisement
  1. Code
  2. Xcode

Una introducción a GameplayKit: Parte 2

Scroll to top
Read Time: 14 min
This post is part of a series called An Introduction to GameplayKit.
An Introduction to GameplayKit: Part 1
An Introduction to GameplayKit: Part 3

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 clase GameScene en un momento.
  • Luego creamos un objeto GKBehavior con un solo GKGoal para apuntar al agente del jugador actual. El parámetro weight 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 de 1 a la meta de segmentación y un peso de 0,5 a la meta de alejamiento. Este comportamiento se asigna al agente del nodo enemigo.
  • Por último, configuramos las propiedades de massmaxSpeed y maxAcceleration . 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.

Targeting enemiesTargeting enemiesTargeting enemies

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 clase GKPolygonObstacle para obstáculos y la clase GKGraphNode2D 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 clase GKGridGraphNode 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 clase GKGraphNode 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ámetro bufferRadius 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ámetro maxPredictionTime 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.

Pathfinding enemiesPathfinding enemiesPathfinding enemies

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

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.