Вступление
Наряду со всеми новыми функциями и фреймворками в 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, использованной в этом руководстве.
1. Начало работы
Сначала вам нужно скачать стартовый проект для этой серии уроков с GitHub . Как только вы это сделаете, откройте проект в Xcode и запустите его на iOS Simulator или на вашем устройстве.
Вы увидите, что это очень простая игра, в которой вы управляете синей точкой и перемещаетесь по карте. Когда вы сталкиваетесь с красной точкой, вы теряете две точки. Когда вы сталкиваетесь с зеленой точкой, вы получаете одно очко. Когда вы сталкиваетесь с желтой точкой, ваша собственная точка замораживается на пару секунд.
На данном этапе это очень простая игра, но на протяжении всей серии уроков и с помощью GameplayKit мы собираемся добавить гораздо больше функциональности и элементов игрового процесса.
2. Объекты и компоненты
Первым важным аспектом новой платформы 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(_:)
. Позже вы добавите этот код обратно, но только когда ваша синяя точка войдет в неуязвимое состояние.
3. Государственные машины
В 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. ИИ позволит вражеским точкам нацеливаться на игрока и найти лучший путь для достижения игрока.
Как всегда, если у вас есть какие-либо комментарии или вопросы, оставьте их в комментариях ниже.