Статьи

Введение в GameplayKit: часть 2

Это вторая часть введения в GameplayKit . Если вы еще не прошли первую часть , я рекомендую сначала прочитать этот учебник, прежде чем продолжить этот.

В этом уроке я расскажу вам еще о двух функциях платформы GameplayKit, которыми вы можете воспользоваться:

  • агенты, цели и поведение
  • Найти путь

Используя агентов, цели и поведение, мы собираемся встроить некоторый базовый искусственный интеллект (ИИ) в игру, которую мы начали в первой части этой серии . ИИ позволит нашим красным и желтым точкам противника нацеливаться и двигаться в направлении нашей синей точки игрока. Мы также собираемся реализовать поиск пути, чтобы расширить этот ИИ для навигации по препятствиям.

Для этого урока вы можете использовать свою копию завершенного проекта из первой части этой серии или загрузить свежую копию исходного кода с GitHub .

В GameplayKit агенты, цели и поведение используются в сочетании друг с другом, чтобы определить, как различные объекты движутся по отношению друг к другу по всей вашей сцене. Для отдельного объекта (или SKShapeNode в нашей игре) вы начинаете с создания агента , представленного классом GKAgent . Однако для 2D-игр, таких как наша, нам нужно использовать конкретный класс GKAgent2D .

Класс GKAgent является подклассом GKComponent . Это означает, что ваша игра должна использовать структуру на основе сущностей и компонентов, как я показал вам в первом уроке этой серии .

Агенты представляют позицию, размер и скорость объекта. Затем вы добавляете поведение , представленное классом GKBehaviour , к этому агенту. Наконец, вы создаете набор целей , представленных классом GKGoal , и добавляете их к объекту поведения. Цели можно использовать для создания множества различных игровых элементов, например:

  • двигаясь к агенту
  • отходя от агента
  • группировка близко друг к другу с другими агентами
  • бродить вокруг конкретной позиции

Ваш объект поведения отслеживает и вычисляет все цели, которые вы добавляете к нему, а затем передает эти данные обратно агенту. Посмотрим, как это работает на практике.

Откройте ваш проект Xcode и перейдите к PlayerNode.swift . Сначала нам нужно убедиться, что класс PlayerNode соответствует протоколу GKAgentDelegate .

1
2
class PlayerNode: SKShapeNode, GKAgentDelegate {

Затем добавьте следующий блок кода в класс PlayerNode .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
var agent = GKAgent2D()
 
// MARK: Agent Delegate
func agentWillUpdate(agent: GKAgent) {
    if let agent2D = agent as?
        agent2D.position = float2(Float(position.x), Float(position.y))
    }
}
 
func agentDidUpdate(agent: GKAgent) {
    if let agent2D = agent as?
        self.position = CGPoint(x: CGFloat(agent2D.position.x), y: CGFloat(agent2D.position.y))
    }
}

Мы начнем с добавления свойства в класс PlayerNode чтобы у нас всегда была ссылка на объект агента текущего игрока. Далее мы реализуем два метода протокола GKAgentDelegate . Реализуя эти методы, мы гарантируем, что отображаемая на экране точка игрока всегда будет отражать изменения, которые вносит GameplayKit.

Метод agentWillUpdate(_:) вызывается непосредственно перед тем, как GameplayKit просматривает поведение и цели этого агента, чтобы определить, куда он должен двигаться. Аналогично, метод agentDidUpdate(_:) вызывается сразу после того, как GameplayKit завершил этот процесс.

Наша реализация этих двух методов гарантирует, что узел, который мы видим на экране, отражает изменения, внесенные GameplayKit, и что GameplayKit использует последнюю позицию узла при выполнении своих вычислений.

Затем откройте ContactNode.swift и замените содержимое файла следующей реализацией:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
import UIKit
import SpriteKit
import GameplayKit
 
class ContactNode: SKShapeNode, GKAgentDelegate {
         
    var agent = GKAgent2D()
     
    // MARK: Agent Delegate
    func agentWillUpdate(agent: GKAgent) {
        if let agent2D = agent as?
            agent2D.position = float2(Float(position.x), Float(position.y))
        }
    }
     
    func agentDidUpdate(agent: GKAgent) {
        if let agent2D = agent as?
            self.position = CGPoint(x: CGFloat(agent2D.position.x), y: CGFloat(agent2D.position.y))
        }
    }
}

GKAgentDelegate протокол ContactNode классе ContactNode , мы позволяем всем остальным точкам в нашей игре быть в курсе GameplayKit, а также нашей игровой точки.

Пришло время настроить поведение и цели. Чтобы сделать это, нам нужно позаботиться о трех вещах:

  • Добавьте агент узла игрока к его сущности и установите его делегат.
  • Настройте агентов, поведение и цели для всех наших вражеских точек.
  • Обновите все эти агенты в нужное время.

Во-первых, откройте GameScene.swift и, в конце метода didMoveToView(_:) , добавьте следующие две строки кода:

1
2
playerNode.entity.addComponent(playerNode.agent)
playerNode.agent.delegate = playerNode

С помощью этих двух строк кода мы добавляем агент в качестве компонента и устанавливаем делегат агента как сам узел.

Затем замените реализацию метода initialSpawn следующей реализацией:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
func initialSpawn() {
    for point in self.spawnPoints {
        let respawnFactor = arc4random() % 3 // Will produce a value between 0 and 2 (inclusive)
         
        var node: SKShapeNode?
         
        switch respawnFactor {
        case 0:
            node = PointsNode(circleOfRadius: 25)
            node!.physicsBody = SKPhysicsBody(circleOfRadius: 25)
            node!.fillColor = UIColor.greenColor()
        case 1:
            node = RedEnemyNode(circleOfRadius: 75)
            node!.physicsBody = SKPhysicsBody(circleOfRadius: 75)
            node!.fillColor = UIColor.redColor()
        case 2:
            node = YellowEnemyNode(circleOfRadius: 50)
            node!.physicsBody = SKPhysicsBody(circleOfRadius: 50)
            node!.fillColor = UIColor.yellowColor()
        default:
            break
        }
         
        if let entity = node?.valueForKey(«entity») as?
            let agent = node?.valueForKey(«agent») as?
 
            entity.addComponent(agent)
            agent.delegate = node as?
            agent.position = float2(x: Float(point.x), y: Float(point.y))
            agents.append(agent)
                 
            let behavior = GKBehavior(goal: GKGoal(toSeekAgent: playerNode.agent), weight: 1.0)
            agent.behavior = behavior
                 
            agent.mass = 0.01
            agent.maxSpeed = 50
            agent.maxAcceleration = 1000
        }
         
        node!.position = point
        node!.strokeColor = UIColor.clearColor()
        node!.physicsBody!.contactTestBitMask = 1
        self.addChild(node!)
    }
}

Самый важный код, который мы добавили, находится в операторе if который следует за оператором switch . Давайте рассмотрим этот код построчно:

  • Сначала мы добавляем агент к объекту как компонент и настраиваем его делегат.
  • Далее мы назначаем позицию агента и добавляем агент в сохраненный массив agents . Мы добавим это свойство в класс GameScene в GameScene время.
  • Затем мы создаем объект GKBehavior с одним GKGoal чтобы нацелиться на агента текущего игрока. weight параметр в этом инициализаторе используется для определения того, какие цели должны иметь приоритет над другими. Например, представьте, что у вас есть цель нацелиться на конкретного агента и цель отойти от другого агента, но вы хотите, чтобы цель нацеливания имела преимущество. В этом случае вы можете присвоить цели прицеливания вес 1 а удаленной цели — 0.5 . Это поведение затем присваивается агенту вражеского узла.
  • Наконец, мы настраиваем свойства mass , maxSpeed и maxAcceleration агента. Они влияют на то, как быстро объекты могут двигаться и поворачиваться. Не стесняйтесь поиграть с этими значениями и посмотреть, как это влияет на движение точек врага.

Затем добавьте следующие два свойства в класс GameScene :

1
2
var agents: [GKAgent2D] = []
var lastUpdateTime: CFTimeInterval = 0.0

Массив agents будет использоваться для хранения ссылки на вражеских агентов на сцене. Свойство lastUpdateTime будет использоваться для расчета времени, прошедшего с момента последнего обновления сцены.

Наконец, замените реализацию метода update(_:) класса GameScene на следующую реализацию:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
override func update(currentTime: CFTimeInterval) {
    /* Called before each frame is rendered */
    self.camera?.position = playerNode.position
     
    if self.lastUpdateTime == 0 {
        lastUpdateTime = currentTime
    }
     
    let delta = currentTime — lastUpdateTime
    lastUpdateTime = currentTime
     
    playerNode.agent.updateWithDeltaTime(delta)
     
    for agent in agents {
        agent.updateWithDeltaTime(delta)
    }
}

В методе update(_:) мы вычисляем время, прошедшее с момента последнего обновления сцены, а затем обновляем агентов с этим значением.

Создайте и запустите свое приложение, и начните перемещаться по сцене. Вы увидите, что вражеские точки будут медленно двигаться к вам.

Ориентация на врагов

Как вы можете видеть, в то время как вражеские точки нацеливаются на текущего игрока, они не перемещаются вокруг белых барьеров, вместо этого они пытаются пройти через них. Давайте сделаем врагов немного умнее с поиском пути.

С каркасом GameplayKit вы можете добавить сложный поиск пути к своей игре, комбинируя физические тела с классами и методами GameplayKit. Для нашей игры мы собираемся настроить ее так, чтобы вражеские точки нацеливались на точку игрока и в то же время перемещались вокруг препятствий.

Поиск пути в GameplayKit начинается с создания графика вашей сцены. Этот график представляет собой набор отдельных местоположений, также называемых узлами , и связей между этими местоположениями. Эти соединения определяют, как конкретный объект может перемещаться из одного места в другое. График может моделировать доступные пути в вашей сцене одним из трех способов:

  • Непрерывное пространство, содержащее препятствия. Эта графовая модель позволяет плавно обходить препятствия из одного места в другое. Для этой модели класс GKObstacleGraph используется для графа, класс GKPolygonObstacle для препятствий и класс GKGraphNode2D для узлов (местоположений).
  • Простая двумерная сетка. В этом случае допустимыми могут быть только местоположения с целочисленными координатами. Эта графическая модель полезна, когда ваша сцена имеет четкую сетку и вам не нужны гладкие контуры. При использовании этой модели объекты могут перемещаться только горизонтально или вертикально в одном направлении одновременно. Для этой модели класс GKGridGraph используется для графа, а класс GKGridGraphNode для узлов.
  • Набор местоположений и связей между ними: это наиболее общая модель графа, которая рекомендуется для случаев, когда объекты перемещаются между различными пространствами, но их конкретное расположение в этом пространстве не является существенным для игрового процесса. Для этой модели класс GKGraph используется для графа, а класс GKGraphNode для узлов.

Поскольку мы хотим, чтобы точка игрока в нашей игре перемещалась вокруг белых барьеров, мы будем использовать класс GKObstacleGraph для создания графика нашей сцены. Для начала замените свойство GameScene классе GameScene следующим:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
let spawnPoints = [
        CGPoint(x: 245, y: 3900),
        CGPoint(x: 700, y: 3500),
        CGPoint(x: 1250, y: 1500),
        CGPoint(x: 1200, y: 1950),
        CGPoint(x: 1200, y: 2450),
        CGPoint(x: 1200, y: 2950),
        CGPoint(x: 1200, y: 3400),
        CGPoint(x: 2550, y: 2350),
        CGPoint(x: 2500, y: 3100),
        CGPoint(x: 3000, y: 2400),
        CGPoint(x: 2048, y: 2400),
        CGPoint(x: 2200, y: 2200)
    ]
var graph: GKObstacleGraph!

Массив spawnPoints содержит некоторые измененные места появления для целей этого урока. Это потому, что в настоящее время GameplayKit может рассчитывать только пути между объектами, которые находятся относительно близко друг к другу.

Из-за большого расстояния по умолчанию между точками в этой игре необходимо добавить пару новых точек появления, чтобы проиллюстрировать поиск пути. Обратите внимание, что мы также объявляем свойство graph типа GKObstacleGraph чтобы сохранить ссылку на график, который мы создадим.

Затем добавьте следующие две строки кода в начале метода didMoveToView(_:) :

1
2
let obstacles = SKNode.obstaclesFromNodePhysicsBodies(self.children)
graph = GKObstacleGraph(obstacles: obstacles, bufferRadius: 0.0)

В первой строке мы создаем массив препятствий из физических тел на сцене. Затем мы создаем объект графа, используя эти препятствия. Параметр bufferRadius в этом инициализаторе можно использовать, чтобы объекты не попадали на определенное расстояние от этих препятствий. Эти строки необходимо добавить в начале метода didMoveToView(_:) , потому что график, который мы создаем, необходим к моменту initialSpawn метода initialSpawn .

Наконец, замените метод initialSpawn следующей реализацией:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
func initialSpawn() {
    let endNode = GKGraphNode2D(point: float2(x: 2048.0, y: 2048.0))
    self.graph.connectNodeUsingObstacles(endNode)
     
    for point in self.spawnPoints {
        let respawnFactor = arc4random() % 3 // Will produce a value between 0 and 2 (inclusive)
         
        var node: SKShapeNode?
         
        switch respawnFactor {
        case 0:
            node = PointsNode(circleOfRadius: 25)
            node!.physicsBody = SKPhysicsBody(circleOfRadius: 25)
            node!.fillColor = UIColor.greenColor()
        case 1:
            node = RedEnemyNode(circleOfRadius: 75)
            node!.physicsBody = SKPhysicsBody(circleOfRadius: 75)
            node!.fillColor = UIColor.redColor()
        case 2:
            node = YellowEnemyNode(circleOfRadius: 50)
            node!.physicsBody = SKPhysicsBody(circleOfRadius: 50)
            node!.fillColor = UIColor.yellowColor()
        default:
            break
        }
         
        if let entity = node?.valueForKey(«entity») as?
            let agent = node?.valueForKey(«agent») as?
                 
            entity.addComponent(agent)
            agent.delegate = node as?
            agent.position = float2(x: Float(point.x), y: Float(point.y))
            agents.append(agent)
             
            /*let behavior = GKBehavior(goal: GKGoal(toSeekAgent: playerNode.agent), weight: 1.0)
            agent.behavior = behavior*/
         
            /*** BEGIN PATHFINDING ***/
            let startNode = GKGraphNode2D(point: agent.position)
            self.graph.connectNodeUsingObstacles(startNode)
             
            let pathNodes = self.graph.findPathFromNode(startNode, toNode: endNode) as!
             
            if !pathNodes.isEmpty {
                let path = GKPath(graphNodes: pathNodes, radius: 1.0)
                 
                let followPath = GKGoal(toFollowPath: path, maxPredictionTime: 1.0, forward: true)
                let stayOnPath = GKGoal(toStayOnPath: path, maxPredictionTime: 1.0)
                 
                let behavior = GKBehavior(goals: [followPath, stayOnPath])
                agent.behavior = behavior
            }
             
            self.graph.removeNodes([startNode])
            /*** END PATHFINDING ***/
             
            agent.mass = 0.01
            agent.maxSpeed = 50
            agent.maxAcceleration = 1000
        }
         
        node!.position = point
        node!.strokeColor = UIColor.clearColor()
        node!.physicsBody!.contactTestBitMask = 1
        self.addChild(node!)
    }
     
    self.graph.removeNodes([endNode])
}

Мы начинаем метод с создания объекта GKGraphNode2D с координатами появления игрока по умолчанию. Далее мы подключаем этот узел к графу, чтобы его можно было использовать при поиске путей.

Большая часть метода initialSpawn остается неизменной. Я добавил несколько комментариев, чтобы показать вам, где находится часть кода для поиска пути в первом операторе if . Давайте пройдемся по этому коду шаг за шагом:

  • Мы создаем еще GKGraphNode2D экземпляр GKGraphNode2D и подключаем его к графику.
  • Мы создаем серию узлов, которые составляют путь, вызывая метод findPathFromNode(_:toNode:) на нашем графике.
  • Если серия узлов пути была успешно создана, мы затем создаем путь из них. Параметр radius работает аналогично параметру bufferRadius представленному ранее, и определяет, насколько объект может отойти от созданного пути.
  • Мы создаем два объекта GKGoal , один для следования по пути и другой для того, чтобы оставаться на пути. Параметр maxPredictionTime позволяет цели как можно раньше рассчитать, не собирается ли что-либо maxPredictionTime объекту следовать или остаться на этом конкретном пути.
  • Наконец, мы создаем новое поведение с этими двумя целями и назначаем его агенту.

Вы также заметите, что мы удаляем узлы, которые мы создаем из графа, как только мы закончим с ними. Это хорошая практика, которой нужно следовать, поскольку она гарантирует, что созданные вами узлы не будут мешать каким-либо другим вычислениям поиска пути в дальнейшем.

Создайте и запустите приложение в последний раз, и вы увидите, что две точки появляются очень близко к вам и начинают двигаться к вам. Возможно, вам придется запускать игру несколько раз, если они оба появляются как зеленые точки.

Поиск врагов

Важный!

В этом уроке мы использовали функцию нахождения пути GameplayKit, чтобы позволить точкам противника нацеливать точку игрока вокруг препятствий. Обратите внимание, что это было только для практического примера поиска пути.

Для реальной производственной игры было бы лучше реализовать эту функциональность, сочетая цель нацеливания игрока из предыдущего урока с целью избежать препятствий, созданной с помощью удобного метода init(toAvoidObstacles:maxPredictionTime:) , о котором вы можете прочитать больше о в GKGoal классов GKGoal .

В этом уроке я показал вам, как вы можете использовать агентов, цели и поведение в играх, которые имеют структуру сущность-компонент. Хотя в этом учебном пособии мы создали только три цели, вам доступно еще много других, о которых вы можете прочитать в Справочнике по GKGoal .

Я также показал вам, как реализовать в своей игре какой-то продвинутый поиск пути, создав график, набор препятствий и цели, чтобы следовать этим путям.

Как вы можете видеть, существует множество функциональных возможностей, доступных вам через каркас GameplayKit. В третьей и последней части этой серии я расскажу вам о генераторах случайных значений GameplayKit и о том, как создать свою собственную систему правил, чтобы добавить нечеткую логику в вашу игру.

Как всегда, пожалуйста, оставляйте свои комментарии и отзывы ниже.