Advertisement
  1. Code
  2. Xcode

Una introducción a GameplayKit: Parte 3

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

Spanish (Español) translation by CYC (you can also view the original English article)

Esta es la tercera parte de una introducción a GameplayKit Si todavía no has revisado la primera parte y la segunda parte, te recomiendo que leas esos tutoriales antes de continuar con este.

Introducción

En este tercer y último tutorial, voy a enseñarte acerca de dos características más que puedes usar en tus propios juegos:

  • generadores de valor aleatorio
  • sistemas de reglas

En este tutorial, primero usaremos uno de los generadores de valores aleatorios de GameplayKit para optimizar nuestro algoritmo inicial de desove enemigo. Luego implementaremos un sistema de reglas básico en combinación con otra distribución aleatoria para manejar el comportamiento de reaparición de los enemigos.

Para este tutorial, puedes usar tu copia del proyecto completo del segundo tutorial o descargar una copia nueva del código fuente de GitHub.

1. Generadores de valores aleatorios

Se pueden generar valores aleatorios en GameplayKit utilizando cualquier clase que se ajuste al protocolo GKRandom. GameplayKit proporciona cinco clases que se ajustan a este protocolo. Estas clases contienen tres fuentes aleatorias y dos distribuciones aleatorias. La principal diferencia entre las fuentes aleatorias y las distribuciones aleatorias es que las distribuciones usan una fuente aleatoria para producir valores dentro de un rango específico y pueden manipular la salida del valor aleatorio de varias otras formas.

Las clases mencionadas anteriormente son proporcionadas por el Framework para que puedas encontrar el equilibrio correcto entre rendimiento y aleatoriedad para tu juego. Algunos algoritmos de generación de valor aleatorio son más complejos que otros y, en consecuencia, afectan el rendimiento.

Por ejemplo, si necesitas un número aleatorio generado en cada cuadro (sesenta veces por segundo), entonces sería mejor utilizar uno de los algoritmos más rápidos. Por el contrario, si solo generas con poca frecuencia un valor aleatorio, podrías usar un algoritmo más complejo para obtener mejores resultados.

Las tres clases de fuentes aleatorias provistas por el framework GameplayKit son GKARC4RandomSource, GKLinearCongruentialRandomSource y GKMersenneTwisterRandomSource.

GKARC4RandomSource

Esta clase usa el algoritmo ARC4 y es adecuada para la mayoría de los propósitos. Este algoritmo funciona produciendo una serie de números aleatorios basados en una semilla. Puedes inicializar un GKARC4RandomSource con una semilla específica si necesitas replicar el comportamiento aleatorio de otra parte de tu juego. El seed de una fuente existente se puede recuperar de su propiedad inicial de solo lectura.

GKLinearCongruentialRandomSource

Esta clase de fuente aleatoria usa el algoritmo de generador congruente lineal básico. Este algoritmo es más eficiente y funciona mejor que el algoritmo ARC4, pero también genera valores que son menos aleatorios. Puedes recuperar una semilla del objeto GKLinearCongruentialRandomSource y crear una nueva fuente con ella de la misma manera que un objeto GKARC4RandomSource.

GKMersenneTwisterRandomSource

Esta clase usa el algoritmo Mersenne Twister y genera los resultados más aleatorios, pero también es el menos eficiente. Al igual que las otras dos clases de fuente aleatorias, puedes recuperar una semilla del objeto GKMersenneTwisterRandomSource y usarla para crear una nueva fuente.

Las dos clases de distribución aleatoria en GameplayKit son GKGaussianDistribution y GKShuffledDistribution.

GKGaussianDistribution

Este tipo de distribución asegura que los valores aleatorios generados sigan una distribución gaussiana, también conocida como distribución normal. Esto significa que la mayoría de los valores generados caerán en el medio del rango que especifiques.

Por ejemplo, si configuras un objeto GKGaussianDistribution con un valor mínimo de 1, un valor máximo de 10 y una desviación estándar de 1, aproximadamente el 69% de los resultados serían 4, 5 o 6. Explicaré esta distribución con más detalle cuando agreguemos uno a nuestro juego más adelante en este tutorial.

GKShuffledDistribution

Esta clase se puede usar para garantizar que los valores aleatorios se distribuyan uniformemente en el rango especificado. Por ejemplo, si generas valores entre 1 y 10, y se genera un 4, no se generarán otros 4 hasta que todos los demás números entre 1 y 10 también se hayan generado.

Ahora es el momento de poner todo esto en práctica. Vamos a agregar dos distribuciones aleatorias a nuestro juego. Abre tu proyecto en Xcode y ve a GameScene.swift. La primera distribución aleatoria que agregaremos es GKGaussianDistribution. Más tarde, también agregaremos GKShuffledDistribution. Agrega las siguientes dos propiedades a la clase GameScene.

1
var initialSpawnDistribution = GKGaussianDistribution(randomSource: GKARC4RandomSource(), lowestValue: 0, highestValue: 2)
2
var respawnDistribution = GKShuffledDistribution(randomSource: GKARC4RandomSource(), lowestValue: 0, highestValue: 2)

En este fragmento, creamos dos distribuciones con un valor mínimo de 0 y un valor máximo de 2. Para GKGaussianDistribution, la media y la desviación se calculan automáticamente según las siguientes ecuaciones:

  • mean = (maximum - minimum) / 2
  • deviation = (maximum - minimum) / 6

La media de una distribución Gaussiana es su punto medio y la desviación se usa para calcular qué porcentaje de valores debe estar dentro de un cierto rango de la media. El porcentaje de valores dentro de un cierto rango es:

  • 68.27% dentro de 1 desviación de la media
  • 95% dentro de 2 desviaciones de la media
  • 100% dentro de 3 desviaciones de la media

Esto significa que aproximadamente el 69% de los valores generados debe ser igual a 1. Esto dará como resultado más puntos rojos en proporción a los puntos verdes y amarillos. Para que esto funcione, debemos actualizar el método initialSpawn.

En el ciclo for, reemplaza la siguiente línea:

1
let respawnFactor = arc4random() % 3  //  Will produce a value between 0 and 2 (inclusive)

con lo siguiente:

1
let respawnFactor = self.initialSpawnDistribution.nextInt()

El método nextInt se puede invocar en cualquier objeto que cumpla con el protocolo GKRandom y devolverá un valor aleatorio según la fuente y, si corresponde, la distribución que estás utilizando....

Compila y ejecuta tu aplicación, y muevete por el mapa. Deberías ver muchos más puntos rojos en comparación con los puntos verdes y amarillos.

Large proportion of red dotsLarge proportion of red dotsLarge proportion of red dots

La segunda distribución aleatoria que usaremos en el juego entrará en juego al manejar el comportamiento de reaparición basado en el sistema de reglas.

2. Sistemas de reglas

Los sistemas de reglas GameplayKit se utilizan para organizar mejor la lógica condicional dentro de tu juego y también para introducir la lógica difusa. Al introducir la lógica difusa, puedes hacer que las entidades dentro de tu juego tomen decisiones basadas en un rango de diferentes reglas y variables, como la salud del jugador, el recuento de enemigos actuales y la distancia al enemigo. Esto puede ser muy ventajoso si se compara con declaraciones simples if y switch.

Los sistemas de reglas, representados por la clase GKRuleSystem, tienen tres partes clave para ellos:

  • Agenda. Este es el conjunto de reglas que se han agregado al sistema de reglas. Por defecto, estas reglas se evalúan en el orden en que se agregan al sistema de reglas. Puedes cambiar la propiedad salience de cualquier regla para especificar cuándo deseas que se evalúe.
  • State Information. La propiedad state de un objeto GKRuleSystem es un diccionario, al que puedes agregar cualquier información, incluidos los tipos de objetos personalizados. Esta información puede ser utilizada por las reglas del sistema de reglas al devolver el resultado.
  • Facts. Los facts dentro de un sistema de reglas representan las conclusiones extraídas de la evaluación de las reglas. Un fact también puede ser representado por cualquier tipo de objeto dentro de tu juego. Cada hecho también tiene una calificación de membresía correspondiente, que es un valor entre 0.0 y 1.0. Esta calificación de membresía representa la inclusión o presencia del hecho dentro del sistema de reglas.

Las reglas mismas, representadas por la clase GKRule, tienen dos componentes principales:

  • Predicate. Esta parte de la regla devuelve un valor booleano, que indica si se cumplieron o no los requisitos de la regla. El predicado de una regla se puede crear utilizando un objeto NSPredicate o, como haremos en este tutorial, un bloque de código.
  • Action. Cuando el predicado de la regla devuelve true, su acción se ejecuta. Esta acción es un bloque de código donde puedes realizar cualquier lógica si se cumplen los requisitos de la regla. Aquí es donde generalmente afirmas (agregas) o retractas (eliminas) facts dentro del sistema de reglas principal.

Veamos cómo funciona todo esto en la práctica. Para nuestro sistema de reglas, vamos a crear tres reglas que vean:

  • la distancia desde el punto de generación al jugador. Si este valor es relativamente pequeño, haremos que sea más probable que el juego engendre enemigos rojos.
  • el recuento de nodos actual de la escena. Si esto es demasiado alto, no queremos que se agreguen más puntos a la escena.
  • si un punto ya está presente o no en el punto de generación. Si no hay, entonces queremos proceder a engendrar un punto aquí.

Primero, agrega la siguiente propiedad a la clase GameScene:

1
var ruleSystem = GKRuleSystem()

A continuación, agrega el siguiente fragmento de código al método didMoveToView(_ :):

1
let playerDistanceRule = GKRule(blockPredicate: { (system: GKRuleSystem) -> Bool in
2
    if let value = system.state["spawnPoint"] as? NSValue {
3
        let point = value.CGPointValue()
4
        
5
        let xDistance = abs(point.x - self.playerNode.position.x)
6
        let yDistance = abs(point.y - self.playerNode.position.y)
7
        let totalDistance = sqrt((xDistance*xDistance) + (yDistance*yDistance))
8
        
9
        if totalDistance <= 200 {
10
            return true
11
        } else {
12
            return false
13
        }
14
    } else {
15
        return false
16
    }
17
}) { (system: GKRuleSystem) -> Void in
18
    system.assertFact("spawnEnemy")
19
}
20
21
let nodeCountRule = GKRule(blockPredicate: { (system: GKRuleSystem) -> Bool in
22
    if self.children.count <= 50 {
23
        return true
24
    } else {
25
        return false
26
    }
27
}) { (system: GKRuleSystem) -> Void in
28
    system.assertFact("shouldSpawn", grade: 0.5)
29
}
30
31
let nodePresentRule = GKRule(blockPredicate: { (system: GKRuleSystem) -> Bool in
32
    if let value = system.state["spawnPoint"] as? NSValue where self.nodesAtPoint(value.CGPointValue()).count == 0 {
33
        return true
34
    } else {
35
        return false
36
    }
37
}) { (system: GKRuleSystem) -> Void in
38
    let grade = system.gradeForFact("shouldSpawn")
39
    system.assertFact("shouldSpawn", grade: (grade + 0.5))
40
}
41
42
self.ruleSystem.addRulesFromArray([playerDistanceRule, nodeCountRule, nodePresentRule])

Con este código, creamos tres objetos GKRule y los agregamos al sistema de reglas. Las reglas afirman un hecho particular dentro de tu bloque de acción. Si no proporcionas un valor de grado y simplemente llamas al método assertFact(_ :), como hacemos con el playerDistanceRule, se da una calificación predeterminada de 1.0.

Notarás que para el nodeCountRule solo afirmamos el hecho "shouldSpawn" con una calificación de 0.5. El nodePresentRule luego afirma este mismo hecho y agrega un valor de grado de 0.5. Esto se hace para que cuando verifiquemos el hecho más adelante, un valor de calificación de 1.0 signifique que ambas reglas hayan sido satisfechas.

También verás que tanto el playerDistanceRule como nodePresentRule acceden al valor "spawnPoint" del diccionario de estado del sistema de reglas. Asignaremos este valor antes de evaluar el sistema de reglas.

Finalmente, encuentra y reemplaza el método de reaparición respawn en la clase GameScene con la siguiente implementación:

1
func respawn() {
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
        self.ruleSystem.reset()
7
        self.ruleSystem.state["spawnPoint"] = NSValue(CGPoint: point)
8
        self.ruleSystem.evaluate()
9
        
10
        if self.ruleSystem.gradeForFact("shouldSpawn") == 1.0 {
11
            var respawnFactor = self.respawnDistribution.nextInt()
12
            
13
            if self.ruleSystem.gradeForFact("spawnEnemy") == 1.0 {
14
                respawnFactor = self.initialSpawnDistribution.nextInt()
15
            }
16
            
17
            var node: SKShapeNode? = nil
18
            
19
            switch respawnFactor {
20
            case 0:
21
                node = PointsNode(circleOfRadius: 25)
22
                node!.physicsBody = SKPhysicsBody(circleOfRadius: 25)
23
                node!.fillColor = UIColor.greenColor()
24
            case 1:
25
                node = RedEnemyNode(circleOfRadius: 75)
26
                node!.physicsBody = SKPhysicsBody(circleOfRadius: 75)
27
                node!.fillColor = UIColor.redColor()
28
            case 2:
29
                node = YellowEnemyNode(circleOfRadius: 50)
30
                node!.physicsBody = SKPhysicsBody(circleOfRadius: 50)
31
                node!.fillColor = UIColor.yellowColor()
32
            default:
33
                break
34
            }
35
            
36
            if let entity = node?.valueForKey("entity") as? GKEntity,
37
                let agent = node?.valueForKey("agent") as? GKAgent2D where respawnFactor != 0 {
38
                    
39
                entity.addComponent(agent)
40
                agent.delegate = node as? ContactNode
41
                agent.position = float2(x: Float(point.x), y: Float(point.y))
42
                agents.append(agent)
43
                
44
                let startNode = GKGraphNode2D(point: agent.position)
45
                self.graph.connectNodeUsingObstacles(startNode)
46
                
47
                let pathNodes = self.graph.findPathFromNode(startNode, toNode: endNode) as! [GKGraphNode2D]
48
                
49
                if !pathNodes.isEmpty {
50
                    let path = GKPath(graphNodes: pathNodes, radius: 1.0)
51
                    
52
                    let followPath = GKGoal(toFollowPath: path, maxPredictionTime: 1.0, forward: true)
53
                    let stayOnPath = GKGoal(toStayOnPath: path, maxPredictionTime: 1.0)
54
                    
55
                    let behavior = GKBehavior(goals: [followPath, stayOnPath])
56
                    agent.behavior = behavior
57
                }
58
                
59
                self.graph.removeNodes([startNode])
60
                
61
                agent.mass = 0.01
62
                agent.maxSpeed = 50
63
                agent.maxAcceleration = 1000
64
            }
65
            
66
            node!.position = point
67
            node!.strokeColor = UIColor.clearColor()
68
            node!.physicsBody!.contactTestBitMask = 1
69
            self.addChild(node!)
70
        }
71
    }
72
    
73
    self.graph.removeNodes([endNode])
74
}

Este método se llamará una vez por segundo y es muy similar al método initialSpawn. Hay una serie de diferencias importantes en el ciclo for.

  • Primero reiniciamos el sistema de reglas llamando a su método reset. Esto debe hacerse cuando un sistema de reglas se evalúa secuencialmente. Esto elimina todos los hechos afirmados y los datos relacionados para garantizar que no quede información de la evaluación anterior que pueda interferir con la siguiente.
  • Luego asignamos el punto de generación al diccionario state del sistema de reglas. Usamos un objeto NSValue, porque el tipo de datos CGPoint no se ajusta al protocolo AnyObject de Swift y no se puede asignar a esta propiedad NSMutableDictionary.
  • Evaluamos el sistema de reglas llamando a su método evaluate.
  • Luego recuperamos la calificación de membresía del sistema de reglas para el fact "shouldSpawn". Si esto es igual a 1, continuamos reapareciendo el punto.
  • Finalmente, verificamos la calificación del sistema de reglas para el hecho de "spawnEnemy" y, si es igual a 1, usamos el generador aleatorio distribuido normalmente para crear nuestro spawnFactor.

El resto del método de reaparición es el mismo que el método initialSpawn. Compila y ejecuta tu juego una última vez. Incluso sin moverse, verás nuevos puntos engendrarse cuando se cumplan las condiciones necesarias.

Respawning dotsRespawning dotsRespawning dots

Conclusión

En esta serie de GameplayKit, has aprendido mucho. Resumamos brevemente lo que hemos cubierto.

  • Entidades y componentes
  • Máquinas de estado
  • Agentes, objetivos y comportamientos
  • Pathfinding o Búsqueda de ruta
  • Generadores de valores aleatorios
  • Sistemas de reglas

GameplayKit es una adición importante para iOS 9 y OS X El Capitan. Elimina muchas de las complejidades del desarrollo del juego. Espero que esta serie te haya motivado a experimentar más con el framework y a descubrir de qué eres capaz.

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.