Статьи

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

Наряду со всеми новыми функциями и фреймворками в iOS 9 и OS X El Capitan , с выпусками этого года Apple также создала совершенно новую фреймворк для разработчиков игр, GameplayKit . Благодаря существующим графическим API-интерфейсам (SpriteKit, SceneKit и Metal), позволяющим легко создавать великолепно выглядящие игры на iOS и OS X, Apple выпустила GameplayKit, чтобы упростить создание игр, которые хорошо играют . Этот новый фреймворк содержит множество классов и функций, которые можно легко использовать для добавления сложной логики в ваши игры.

В этом первом уроке я расскажу вам о двух основных аспектах платформы GameplayKt:

  • сущности и компоненты
  • конечные автоматы

Это руководство требует, чтобы вы работали с Xcode 7 на OS X Yosemite или более поздней версии . Хотя это и не обязательно, рекомендуется иметь физическое устройство под управлением iOS 9, поскольку вы получите гораздо лучшую производительность при тестировании игры на основе SpriteKit, использованной в этом руководстве.

Сначала вам нужно скачать стартовый проект для этой серии уроков с GitHub . Как только вы это сделаете, откройте проект в Xcode и запустите его на iOS Simulator или на вашем устройстве.

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

Начальная игра

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

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

Вы строите игру защиты башни с тремя основными типами башен: Огонь , Лед и Лечение . Три типа башен будут иметь некоторые общие данные, такие как здоровье, размер и сила. Ваши Огненные и Ледяные башни должны быть в состоянии нацеливаться на вражеских врагов, чтобы стрелять в них, в то время как ваша башня Хила не делает этого. Все, что нужно сделать вашей башне Исцеления, — это починить другие башни в пределах определенного радиуса, когда они получают урон.

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

  • Родительский класс Tower содержащий общие данные, такие как здоровье, размер и сила.
  • FireTower , IceTower и HealTower , которые наследуются от класса Tower .
  • В классе HealTower вас есть логика, отвечающая за лечение ваших других башен в пределах определенного радиуса.

Пока что с этой структурой все в порядке, но теперь возникает проблема, когда вам нужно реализовать способность таргетинга Огненной и Ледяной башен. Вы просто копируете и вставляете один и тот же код в классы FireTower и IceTower ? Если вам необходимо внести какие-либо изменения, вам потребуется изменить код в нескольких местах, что является утомительным и подверженным ошибкам. Кроме того, что произойдет, если вы захотите добавить новый тип башни, который также нуждается в этой функции таргетинга. Вы копируете и вставляете это в третий раз?

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

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

Теперь давайте посмотрим, как эта модель игры может быть структурирована с использованием сущностей и компонентов :

  • Мы бы создали FireTower , IceTower и HealTower . Можно создать больше объектов для любых типов башен, которые вы хотите добавить позже.
  • Мы также создали бы компонент BasicTower , который содержал бы здоровье, размер, силу и т. Д.
  • Чтобы справиться с исцелением ваших башен в пределах определенного радиуса, мы добавили бы Healing компонент.
  • Компонент Targeting будет содержать код, необходимый для нацеливания на врагов.

Используя GameplayKit и эту структуру, вы получите уникальный тип сущности для каждого типа башни в вашей игре. К каждой отдельной сущности вы можете добавить нужные вам компоненты. Например:

  • FireTower IceTower объектами FireTower и IceTower будут связаны компоненты BasicTower и Targeting .
  • Ваша сущность HealTower будет иметь компонент BasicTower и Healing .

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

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

Теперь, когда вы знакомы с концепцией структуры игровой модели на основе сущностей и компонентов, давайте создадим ее в нашей собственной игре. В инспекторе файлов Xcode найдите папку Entities в вашем проекте. Для удобства уже есть три класса сущностей для вас, но теперь вы собираетесь создать новую сущность с нуля.

Выберите « Файл»> «Создать»> «Файл …» или нажмите Command-N, чтобы создать новый класс. Обязательно выберите шаблон Cocoa Touch Class в разделе « iOS»> «Источник ». Назовите класс Player и сделайте его подклассом GKEntity .

Создание сущности игрока

Вы увидите, что сразу после открытия вашего нового файла Xcode отобразит ошибку. Чтобы это исправить, добавьте следующий оператор импорта под существующим оператором import UIKit :

1
import GameplayKit

Вернитесь к PlayerNode.swift и добавьте следующее свойство в класс PlayerNode :

1
var entity = Player()

Затем перейдите к папке « Компоненты » в вашем проекте XCode и создайте новый класс, как вы делали раньше. На этот раз назовите класс FlashingComponent и сделайте его подклассом GKComponent как показано ниже.

Создание мигающего компонента

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
import UIKit
import SpriteKit
import GameplayKit
 
class FlashingComponent: GKComponent {
     
    var nodeToFlash: SKNode!
     
    func startFlashing() {
 
        let fadeAction = SKAction.sequence([SKAction.fadeOutWithDuration(0.75), SKAction.fadeInWithDuration(0.75)])
        nodeToFlash.runAction(SKAction.repeatActionForever(fadeAction), withKey: «flash»)
         
    }
     
    deinit {
        nodeToFlash.removeActionForKey(«flash»)
    }
     
}

Реализация просто сохраняет ссылку на объект SKNode и повторяет действия постепенного SKNode и постепенного исчезновения, пока компонент активен.

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

1
2
3
4
5
// Adding Component
let flash = FlashingComponent()
flash.nodeToFlash = playerNode
flash.startFlashing()
playerNode.entity.addComponent(flash)

Мы создаем объект FlashingComponent и настраиваем его для выполнения его мигания на точке игрока. Затем последняя строка добавляет компонент к объекту, чтобы он оставался активным и работающим.

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

Исчезающая синяя точка

Прежде чем двигаться дальше, удалите код, который вы только что добавили, из didMoveToView(_:) . Позже вы добавите этот код обратно, но только когда ваша синяя точка войдет в неуязвимое состояние.

В GameplayKit конечные автоматы предоставляют вам возможность легко идентифицировать и выполнять задачи в зависимости от текущего состояния конкретного объекта. Как видно из более раннего примера защиты башни, некоторые возможные состояния для каждой башни могут включать « Active , « Disabled и « Destroyed . Одним из основных преимуществ конечных автоматов является то, что вы можете указать, в какие состояния может переходить другое состояние. Используя три приведенных выше примера состояний, используя конечный автомат, вы можете настроить конечный автомат так, чтобы:

  • башня может быть Disabled когда Active и наоборот
  • башня может стать Destroyed когда Active или Disabled
  • Башня не может стать Active или Disabled после того, как она была Destroyed

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

В папке State Machine вашего проекта создайте два новых класса. Назовите их NormalState и InvulnerableState соответственно, оба они являются подклассом класса GKState .

Создание неуязвимого государственного класса

Замените содержимое NormalState.swift следующим:

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
import UIKit
import SpriteKit
import GameplayKit
 
class NormalState: GKState {
     
    var node: PlayerNode
     
    init(withNode: PlayerNode) {
        node = withNode
    }
     
    override func isValidNextState(stateClass: AnyClass) -> Bool {
        switch stateClass {
        case is InvulnerableState.Type:
            return true
             
        default:
            return false
        }
    }
     
    override func didEnterWithPreviousState(previousState: GKState?) {
        if let _ = previousState as?
            node.entity.removeComponentForClass(FlashingComponent)
            node.runAction(SKAction.fadeInWithDuration(0.5))
        }
    }
}

Класс NormalState содержит следующее:

  • Он реализует простой инициализатор, чтобы сохранить ссылку на текущий узел игрока.
  • Он имеет реализацию для isValidNextState(_:) . Реализация этого метода возвращает логическое значение, указывающее, может ли текущий класс состояний перейти в класс состояний, предоставленный параметром метода.
  • Класс также включает реализацию метода обратного вызова didEnterWithPreviousState(_:) . В реализации метода мы проверяем, было ли предыдущее состояние состоянием InvulnerableState и, если оно истинно, удаляем мигающий компонент из сущности игрока.

Теперь откройте InvulnerableState.swift и замените его содержимое следующим:

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
import UIKit
import GameplayKit
 
class InvulnerableState: GKState {
     
    var node: PlayerNode
     
    init(withNode: PlayerNode) {
        node = withNode
    }
     
    override func isValidNextState(stateClass: AnyClass) -> Bool {
        switch stateClass {
        case is NormalState.Type:
            return true
             
        default:
            return false
        }
    }
     
    override func didEnterWithPreviousState(previousState: GKState?) {
        if let _ = previousState as?
            // Adding Component
            let flash = FlashingComponent()
            flash.nodeToFlash = node
            flash.startFlashing()
            node.entity.addComponent(flash)
        }
    }
}

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

Теперь, когда ваши классы состояний завершены, снова откройте PlayerNode.swift и добавьте следующие строки в класс PlayerNode :

1
2
3
4
5
var stateMachine: GKStateMachine!
 
func enterNormalState() {
    self.stateMachine.enterState(NormalState)
}

Этот фрагмент кода добавляет новое свойство в класс PlayerNode и реализует удобный метод для возврата в нормальное состояние.

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

1
2
playerNode.stateMachine = GKStateMachine(states: [NormalState(withNode: playerNode), InvulnerableState(withNode: playerNode)])
playerNode.stateMachine.enterState(NormalState)

В этих двух строках кода мы создаем новый GKStateMachine с двумя состояниями и NormalState ему ввести NormalState .

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
func handleContactWithNode(contact: ContactNode) {
    if contact is PointsNode {
        NSNotificationCenter.defaultCenter().postNotificationName(«updateScore», object: self, userInfo: [«score»: 1])
    }
    else if contact is RedEnemyNode && playerNode.stateMachine.currentState!
        NSNotificationCenter.defaultCenter().postNotificationName(«updateScore», object: self, userInfo: [«score»: -2])
         
        playerNode.stateMachine.enterState(InvulnerableState)
        playerNode.performSelector(«enterNormalState», withObject: nil, afterDelay: 5.0)
    }
    else if contact is YellowEnemyNode && playerNode.stateMachine.currentState!
        self.playerNode.enabled = false
    }
     
    contact.removeFromParent()
}

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

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

Вход в неуязвимое состояние

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

Я также показал вам основы конечных автоматов, включая то, как вы можете указать, в какие состояния может переходить конкретное состояние, а также выполнять код при входе в определенное состояние.

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

Как всегда, если у вас есть какие-либо комментарии или вопросы, оставьте их в комментариях ниже.