Статьи

Создание космических захватчиков с помощью Swift и Sprite Kit: реализация игрового процесса

Конечный продукт
Что вы будете создавать

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

Мы будем использовать метод 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()
}

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

Время от времени мы хотим, чтобы один из захватчиков выстрелил. В нынешнем виде захватчики в нижнем ряду настроены на запуск пули, потому что они находятся в массиве 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 , то продолжительность этого действия будет равна продолжительности воспроизведения звука, в противном случае он сразу же перейдет к следующему действию в последовательности.

Добавьте метод 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 .

Добавьте метод fireInvaderBullet ниже метода invokeInvaderFire который вы ввели на предыдущем шаге.

1
2
3
4
func fireInvaderBullet(){
    let randomInvader = invadersWhoCanFire.randomElement()
    randomInvader.fireBullet(self)
}

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

Перейдите в Файл > Создать > Файл … и выберите Файл 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 .

Со всем этим теперь мы можем стрелять пулями. Добавьте следующее в метод didMoveToView(_:) .

1
2
3
4
5
override func didMoveToView(view: SKView) {
    …
    setupPlayer()
    invokeInvaderFire()
}

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

Добавьте следующее свойство в класс 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 .

Всякий раз, когда пользователь касается экрана, мы хотим запустить пулю. Это так же просто, как вызов fireBullet для объекта player в методе touchesBegan(_:withEvent:) класса GameScene .

1
2
3
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
    player.fireBullet(self)
}

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

Чтобы определить, когда узлы сталкиваются или вступают в контакт друг с другом, мы будем использовать встроенный физический движок 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 . Мы используем битовое смещение, чтобы включить биты.

Добавьте следующий блок кода в метод 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 .

Добавьте следующее в метод 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 .

Добавьте следующее в метод 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 .

Добавьте следующее в 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
}

Мы должны настроить класс 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 .

Чтобы согласовать класс 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 .

Затем, мы проверяем каждый возможный сценарий, используя побитовый оператор & и категории столкновений, которые мы определили ранее, чтобы проверить, что устанавливает контакт. Мы записываем результат в консоль, чтобы убедиться, что все работает как надо. Если вы тестируете приложение, все контакты должны работать правильно.

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