В предыдущей части этой серии мы реализовали заглушки для основных классов игры. В этом уроке мы приведем в движение захватчиков, выпустим пули как для захватчиков, так и для игрока, и реализуем обнаружение столкновений. Давайте начнем.
1. Перемещение захватчиков
Мы будем использовать метод update
сцены для перемещения захватчиков. Всякий раз, когда вы хотите переместить что-то вручную, обычно используется метод update
.
Прежде чем мы сделаем это, нам нужно обновить свойство rightBounds
. Первоначально он был установлен в 0 , потому что нам нужно использовать size
сцены, чтобы установить переменную. Мы не смогли сделать это вне каких-либо методов класса, поэтому мы обновим это свойство в didMoveToView(_:)
.
1
2
3
4
5
6
|
override func didMoveToView(view: SKView) {
backgroundColor = SKColor.blackColor()
rightBounds = self.size.width — 30
setupInvaders()
setupPlayer()
}
|
Затем, moveInvaders
метод moveInvaders
ниже метода setupPlayer
который вы создали в предыдущем уроке .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
func moveInvaders(){
var changeDirection = false
enumerateChildNodesWithName(«invader») { node, stop in
let invader = node as!
let invaderHalfWidth = invader.size.width/2
invader.position.x -= CGFloat(self.invaderSpeed)
if(invader.position.x > self.rightBounds — invaderHalfWidth || invader.position.x < self.leftBounds + invaderHalfWidth){
changeDirection = true
}
}
if(changeDirection == true){
self.invaderSpeed *= -1
self.enumerateChildNodesWithName(«invader») { node, stop in
let invader = node as!
invader.position.y -= CGFloat(46)
}
changeDirection = false
}
}
|
Мы объявляем переменную changeDirection
, чтобы отслеживать, когда захватчикам нужно изменить направление, двигаясь влево или двигаясь вправо. Затем мы используем метод enumerateChildNodesWithName(usingBlock:)
, который ищет дочерние узлы и вызывает замыкание один раз для каждого соответствующего узла, который он находит с соответствующим именем «invader» . Замыкание принимает два параметра: node
— это узел, который соответствует name
а stop
— указатель на логическую переменную для завершения перечисления. Мы не будем здесь stop
, но полезно знать, для чего он используется.
Мы SKSpriteNode
node
к экземпляру SKSpriteNode
invader
которого является подклассом, получаем половину его ширины invaderHalfWidth
и обновляем его позицию. Затем мы проверяем, находится ли его position
в пределах границ, leftBounds
и rightBounds
, и, если нет, мы устанавливаем changeDirection
в true
.
Если changeDirection
имеет значение true
, мы invaderSpeed
, который изменит направление, в котором движется захватчик. Затем мы перечислим через захватчиков и обновим их позицию y. Наконец, мы устанавливаем changeDirection
обратно в false
.
Метод moveInvaders
методе update(_:)
.
1
2
3
|
override func update(currentTime: CFTimeInterval) {
moveInvaders()
}
|
Если вы тестируете приложение сейчас, вы должны увидеть, как захватчики перемещаются влево, вправо, а затем вниз, если они достигают границ, которые мы установили с обеих сторон.
2. Стреляющие пули захватчиков
Шаг 1: fireBullet
Время от времени мы хотим, чтобы один из захватчиков выстрелил. В нынешнем виде захватчики в нижнем ряду настроены на запуск пули, потому что они находятся в массиве invadersWhoCanFire.
Когда Invader получает удар от пули игрока, то захватчик на одну строку вверх и в том же столбце будет добавлен в массив invadersWhoCanFire, а получатель попадания будет удален. Таким образом, только самый нижний захватчик каждого столбца может стрелять пулями.
Добавьте метод InvaderBullet
класс InvaderBullet в InvaderBullet.swift .
1
2
3
4
5
6
7
8
9
|
func fireBullet(scene: SKScene){
let bullet = InvaderBullet(imageName: «laser»,bulletSound: nil)
bullet.position.x = self.position.x
bullet.position.y = self.position.y — self.size.height/2
scene.addChild(bullet)
let moveBulletAction = SKAction.moveTo(CGPoint(x:self.position.x,y: 0 — bullet.size.height), duration: 2.0)
let removeBulletAction = SKAction.removeFromParent()
bullet.runAction(SKAction.sequence([moveBulletAction,removeBulletAction]))
}
|
В методе fireBullet
мы создаем экземпляр InvaderBullet
, передавая «лазер» для imageName
, и поскольку мы не хотим, чтобы звук воспроизводился, мы передаем nil
для bulletSound
. Мы устанавливаем его position
таким же, как и у захватчика, с небольшим смещением относительно позиции y, и добавляем его в сцену.
Мы создаем два экземпляра moveBulletAction
, moveBulletAction
и removeBulletAction
. Действие moveBulletAction
перемещает маркер к определенной точке в течение определенной продолжительности, в то время removeBulletAction
действие removeBulletAction
удаляет его из сцены. Вызывая метод sequence(_:)
для этих действий, они будут выполняться последовательно. Вот почему я упоминал метод waitForDuration
при воспроизведении звука в предыдущей части этой серии. Если вы создаете объект SKAction
, вызывая playSoundFileNamed(_:waitForCompletion:)
и устанавливая waitForCompletion
в значение true
, то продолжительность этого действия будет равна продолжительности воспроизведения звука, в противном случае он сразу же перейдет к следующему действию в последовательности.
Шаг 2: invokeInvaderFire
Добавьте метод invokeInvaderFire
ниже других методов, которые вы создали в GameScence.swift .
1
2
3
4
5
6
7
8
9
|
func invokeInvaderFire(){
let fireBullet = SKAction.runBlock(){
self.fireInvaderBullet()
}
let waitToFireInvaderBullet = SKAction.waitForDuration(1.5)
let invaderFire = SKAction.sequence([fireBullet,waitToFireInvaderBullet])
let repeatForeverAction = SKAction.repeatActionForever(invaderFire)
runAction(repeatForeverAction)
}
|
Метод runBlock(_:)
класса SKAction
создает экземпляр SKAction
и немедленно вызывает замыкание, переданное runBlock(_:)
. В закрытии мы fireInvaderBullet
метод fireInvaderBullet
. Поскольку мы вызываем этот метод в замыкании, мы должны использовать self
для его вызова.
Затем мы создаем экземпляр waitToFireInvaderBullet
с именем waitToFireInvaderBullet
, вызывая waitForDuration(_:)
, передавая количество секунд ожидания, прежде чем двигаться дальше. Затем мы создаем экземпляр invaderFire
, вызывая метод sequence(_:)
. Этот метод принимает коллекцию действий, которые вызываются действием invaderFire
. Мы хотим, чтобы эта последовательность повторялась вечно, поэтому мы создаем действие с именем repeatForeverAction
, SKAction
объекты SKAction
для повторения и вызываем runAction
, передавая действие repeatForeverAction
. Метод runAction объявлен в классе SKNode
.
Шаг 3: fireInvaderBullet
Добавьте метод fireInvaderBullet
ниже метода invokeInvaderFire
который вы ввели на предыдущем шаге.
1
2
3
4
|
func fireInvaderBullet(){
let randomInvader = invadersWhoCanFire.randomElement()
randomInvader.fireBullet(self)
}
|
В этом методе мы вызываем метод с именем randomElement
, который возвращает случайный элемент из массива fireBullet
, а затем вызываем его метод fireBullet
. К сожалению, в структуре Array
нет встроенного метода randomElement
. Однако мы можем создать расширение Array
для предоставления этой функциональности.
Шаг 4: реализовать randomElement
Перейдите в Файл > Создать > Файл … и выберите Файл Swift . Мы делаем что-то другое, чем прежде, поэтому просто убедитесь, что вы выбираете Swift File, а не Cocoa Touch Class . Нажмите Далее и назовите файл Утилиты . Добавьте следующее в Utilities.swift .
1
2
3
4
5
6
7
8
|
import Foundation
extension Array {
func randomElement() -> T {
let index = Int(arc4random_uniform(UInt32(self.count)))
return self[index]
}
}
|
Мы расширяем структуру Array
чтобы иметь метод с именем randomElement
. Функция arc4random_uniform
возвращает число от 0 до того, что вы передаете. Поскольку Swift неявно преобразует числовые типы, мы должны выполнить преобразование самостоятельно. Наконец, мы возвращаем элемент массива по индексу index
.
Этот пример иллюстрирует, как легко добавить функциональность в структуру и классы. Вы можете узнать больше о создании расширений на языке программирования Swift .
Шаг 5: стрельба из пули
Со всем этим теперь мы можем стрелять пулями. Добавьте следующее в метод didMoveToView(_:)
.
1
2
3
4
5
|
override func didMoveToView(view: SKView) {
…
setupPlayer()
invokeInvaderFire()
}
|
Если вы тестируете приложение сейчас, каждую секунду или около того вы должны видеть, как один из захватчиков из нижнего ряда запускает пулю.
3. Пули игрока
Шаг 1: fireBullet(scene:)
Добавьте следующее свойство в класс Player
в Player.swift .
1
2
|
class Player: SKSpriteNode {
private var canFire = true
|
Мы хотим ограничить частоту, с которой игрок может выстрелить. Свойство canFire
будет использоваться для регулирования этого. Затем добавьте следующее в метод fireBullet(scene:)
в классе Player
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
func fireBullet(scene: SKScene){
if(!canFire){
return
}else{
canFire = false
let bullet = PlayerBullet(imageName: «laser»,bulletSound: «laser.mp3»)
bullet.position.x = self.position.x
bullet.position.y = self.position.y + self.size.height/2
scene.addChild(bullet)
let moveBulletAction = SKAction.moveTo(CGPoint(x:self.position.x,y:scene.size.height + bullet.size.height), duration: 1.0)
let removeBulletAction = SKAction.removeFromParent()
bullet.runAction(SKAction.sequence([moveBulletAction,removeBulletAction]))
let waitToEnableFire = SKAction.waitForDuration(0.5)
runAction(waitToEnableFire,completion:{
self.canFire = true
})
}
}
|
Сначала мы удостоверимся, что игрок может стрелять, проверив, установлено ли для canFire
значение true
. Если это не так, мы сразу же возвращаемся из метода.
Если игрок может стрелять, мы устанавливаем для canFire
значение false
чтобы они не могли немедленно выстрелить другой пулей. Затем мы PlayerBullet
экземпляр PlayerBullet
, передавая «laser» для параметра imageNamed
. Поскольку мы хотим, чтобы звук воспроизводился, когда игрок запускает пулю, мы передаем «laser.mp3» для параметра bulletSound
.
Затем мы устанавливаем положение пули и добавляем ее на экран. Следующие несколько строк такие же, как и у Invader
Метод fireBullet
в том, что мы перемещаем пулю и удаляем ее со сцены. Затем мы создаем экземпляр SKAction
, waitToEnableFire
, вызывая метод класса waitForDuration(_:)
. Наконец, мы вызываем runAction
, передавая waitToEnableFire
, и по окончании устанавливаем canFire
обратно в true
.
Шаг 2: стрельба игрока
Всякий раз, когда пользователь касается экрана, мы хотим запустить пулю. Это так же просто, как вызов fireBullet
для объекта player
в методе touchesBegan(_:withEvent:)
класса GameScene
.
1
2
3
|
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
player.fireBullet(self)
}
|
Если вы протестируете приложение сейчас, вы сможете выстрелить при нажатии на экран. Кроме того, вы должны слышать звук лазера каждый раз, когда выстреливает пуля.
4. Столкновение Категории
Чтобы определить, когда узлы сталкиваются или вступают в контакт друг с другом, мы будем использовать встроенный физический движок Sprite Kit. Однако поведение физического движка по умолчанию таково, что все сталкивается со всем, когда к ним добавлено физическое тело. Нам нужен способ отделить то, что мы хотим, взаимодействуя друг с другом, и мы можем сделать это, создавая категории, к которым принадлежат конкретные физические тела.
Вы определяете эти категории, используя битовую маску, которая использует 32-разрядное целое число с 32 отдельными флагами, которые могут быть включены или выключены. Это также означает, что вы можете иметь максимум 32 категории для своей игры. Это не должно представлять проблему для большинства игр, но об этом следует помнить.
Добавьте следующее определение структуры в класс invaderNum
объявлением invaderNum в GameScene.swift .
1
2
3
4
5
6
|
struct CollisionCategories{
static let Invader : UInt32 = 0x1 << 0
static let Player: UInt32 = 0x1 << 1
static let InvaderBullet: UInt32 = 0x1 << 2
static let PlayerBullet: UInt32 = 0x1 << 3
}
|
Мы используем структуру CollsionCategories
для создания категорий для классов Invader
, Player
, InvaderBullet
и PlayerBullet
. Мы используем битовое смещение, чтобы включить биты.
5. Столкновение Player
и InvaderBullet
Шаг 1: Настройка InvaderBullet
для столкновения
Добавьте следующий блок кода в метод init(imageName:bulletSound:)
в InvaderBullet.swift .
1
2
3
4
5
6
7
8
9
|
override init(imageName: String, bulletSound:String?){
super.init(imageName: imageName, bulletSound: bulletSound)
self.physicsBody = SKPhysicsBody(texture: self.texture, size: self.size)
self.physicsBody?.dynamic = true
self.physicsBody?.usesPreciseCollisionDetection = true
self.physicsBody?.categoryBitMask = CollisionCategories.InvaderBullet
self.physicsBody?.contactTestBitMask = CollisionCategories.Player
self.physicsBody?.collisionBitMask = 0x0
}
|
Есть несколько способов создать физическое тело. В этом примере мы используем init(texture:size:)
, который заставит обнаружение столкновений использовать форму текстуры, которую мы передаем. Есть несколько других доступных инициализаторов, которые вы можете увидеть в справочнике по классу SKPhysicsBody .
Мы могли бы легко использовать init(rectangleOfSize:)
, потому что пули имеют прямоугольную форму. В такой маленькой игре это не имеет значения. Однако имейте в виду, что использование метода init(texture:size:)
может быть дорогостоящим в вычислительном отношении, поскольку он должен вычислять точную форму текстуры. Если у вас есть объекты, которые имеют прямоугольную или круглую форму, то вы должны использовать эти типы инициализаторов, если производительность игры становится проблемой.
Чтобы обнаружение столкновений работало, по крайней мере одно из тестируемых тел должно быть помечено как динамическое. Устанавливая для свойства usesPreciseCollisionDetection
значение true
, Sprite Kit использует более точное обнаружение столкновений. Установите для этого свойства значение true
для небольших быстро движущихся тел, таких как наши пули.
Каждое тело будет принадлежать категории, и вы определяете это, устанавливая его categoryBitMask
. Поскольку это класс InvaderBullet
, мы установили для него значение CollisionCategories.InvaderBullet
.
Чтобы сообщить, когда это тело contactBitMask
контакт с другим интересующим вас contactBitMask
, вы устанавливаете contactBitMask
. Здесь мы хотим знать, когда InvaderBullet
вступил в контакт с игроком, поэтому мы используем CollisionCategories.Player
. Поскольку столкновение не должно запускать какие-либо физические силы, мы устанавливаем collisionBitMask
в 0x0
.
Шаг 2: Настройка Player
для Collsion
Добавьте следующее в метод init
в Player.swift .
01
02
03
04
05
06
07
08
09
10
11
12
|
override init() {
let texture = SKTexture(imageNamed: «player1»)
super.init(texture: texture, color: SKColor.clearColor(), size: texture.size())
self.physicsBody =
SKPhysicsBody(texture: self.texture,size:self.size)
self.physicsBody?.dynamic = true
self.physicsBody?.usesPreciseCollisionDetection = false
self.physicsBody?.categoryBitMask = CollisionCategories.Player
self.physicsBody?.contactTestBitMask = CollisionCategories.InvaderBullet |
self.physicsBody?.collisionBitMask = 0x0
animate()
}
|
Многое из этого должно быть знакомо с предыдущего шага, поэтому я не буду перефразировать его здесь. Однако есть два отличия. Один из них заключается в том, что для параметра usesPreciseCollsionDetection
установлено значение false
, которое используется по умолчанию. Важно понимать, что только одному из контактирующих тел необходимо, чтобы это свойство было установлено в значение true
(которое было маркером). Другое отличие состоит в том, что мы также хотим знать, когда игрок связывается с захватчиком. Вы можете иметь более одной категории contactBitMask
, разделяя их с помощью побитового оператора или ( |
). Кроме того, вы должны заметить, что он в основном противоположен InvaderBullet
.
6. Invader
и PlayerBullet
Столкновение
Шаг 1: Настройка Invader
для столкновения
Добавьте следующее в метод init
в Invader.swift .
01
02
03
04
05
06
07
08
09
10
11
12
|
override init() {
let texture = SKTexture(imageNamed: «invader1»)
super.init(texture: texture, color: SKColor.clearColor(), size: texture.size())
self.name = «invader»
self.physicsBody =
SKPhysicsBody(texture: self.texture, size: self.size)
self.physicsBody?.dynamic = true
self.physicsBody?.usesPreciseCollisionDetection = false
self.physicsBody?.categoryBitMask = CollisionCategories.Invader
self.physicsBody?.contactTestBitMask = CollisionCategories.PlayerBullet |
self.physicsBody?.collisionBitMask = 0x0
}
|
Это все должно иметь смысл, если вы следили за. Мы настраиваем physicsBody
, categoryBitMask
contactBitMask
и contactBitMask
.
Шаг 2: Настройка PlayerBullet
для столкновения
Добавьте следующее в init(imageName:bulletSound:)
в PlayerBullet.swift . Опять же, реализация должна быть уже знакома.
1
2
3
4
5
6
7
8
9
|
override init(imageName: String, bulletSound:String?){
super.init(imageName: imageName, bulletSound: bulletSound)
self.physicsBody = SKPhysicsBody(texture: self.texture, size: self.size)
self.physicsBody?.dynamic = true
self.physicsBody?.usesPreciseCollisionDetection = true
self.physicsBody?.categoryBitMask = CollisionCategories.PlayerBullet
self.physicsBody?.contactTestBitMask = CollisionCategories.Invader
self.physicsBody?.collisionBitMask = 0x0
}
|
7. Настройка физики для GameScene
Шаг 1: Настройка мира физики
Мы должны настроить класс GameScene
для реализации SKPhysicsContactDelegate
чтобы мы могли реагировать, когда сталкиваются два тела. Добавьте следующее, чтобы класс GameScene
соответствовал протоколу SKPhysicsContactDelegate
.
1
|
class GameScene: SKScene, SKPhysicsContactDelegate{
|
Затем мы должны установить некоторые свойства на physicsWorld
сцены. Введите следующее в верхней части didMoveToView(_:)
в GameScene.swift .
1
2
3
4
5
|
override func didMoveToView(view: SKView) {
self.physicsWorld.gravity = CGVectorMake(0, 0)
self.physicsWorld.contactDelegate = self
…
}
|
Мы устанавливаем свойство gravity
в physicsWorld
на 0, чтобы gravity
physicsWorld
на физические тела в сцене. Вы также можете сделать это для каждого отдельного тела, вместо того чтобы устанавливать для всего мира отсутствие гравитации, установив свойство disabledByGravity. Мы также установили для свойства contactDelegate
мира физики значение self
, экземпляр GameScene
.
Шаг 2: Реализация протокола SKPhysicsContactDelegate
Чтобы согласовать класс SKPhysicsContactDelegate
протоколом SKPhysicsContactDelegate
, нам нужно реализовать метод didBeginContact(_:)
. Этот метод вызывается, когда два тела вступают в контакт. Реализация метода didBeginContact(_:)
выглядит следующим образом.
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
|
func didBeginContact(contact: SKPhysicsContact) {
var firstBody: SKPhysicsBody
var secondBody: SKPhysicsBody
if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
firstBody = contact.bodyA
secondBody = contact.bodyB
} else {
firstBody = contact.bodyB
secondBody = contact.bodyA
}
if ((firstBody.categoryBitMask & CollisionCategories.Invader != 0) &&
(secondBody.categoryBitMask & CollisionCategories.PlayerBullet != 0)){
NSLog(«Invader and Player Bullet Conatact»)
}
if ((firstBody.categoryBitMask & CollisionCategories.Player != 0) &&
(secondBody.categoryBitMask & CollisionCategories.InvaderBullet != 0)) {
NSLog(«Player and Invader Bullet Contact»)
}
if ((firstBody.categoryBitMask & CollisionCategories.Invader != 0) &&
(secondBody.categoryBitMask & CollisionCategories.Player != 0)) {
NSLog(«Invader and Player Collision Contact»)
}
}
|
Сначала мы объявляем две переменные firstBody
и secondBody
. Когда два объекта вступают в контакт, мы не знаем, какое это тело. Это означает, что сначала нам нужно сделать несколько проверок, чтобы убедиться, что firstBody
— это firstBody
с более низкой categoryBitMask
.
Затем, мы проверяем каждый возможный сценарий, используя побитовый оператор &
и категории столкновений, которые мы определили ранее, чтобы проверить, что устанавливает контакт. Мы записываем результат в консоль, чтобы убедиться, что все работает как надо. Если вы тестируете приложение, все контакты должны работать правильно.
Вывод
Это был довольно длинный урок, но теперь у нас есть движущиеся захватчики, пули, запускаемые как игроком, так и захватчиками, и обнаружение контактов, работающее с использованием битовых масок контактов. Мы находимся на домашнем участке до финальной игры. В следующей и последней части этой серии у нас будет законченная игра.