Статьи

SpriteKit From Scratch: физика и столкновения

В этом уроке, третьей части серии SpriteKit From Scratch , мы подробно рассмотрим функциональность симуляции физики SpriteKit и то, как ее можно использовать в ваших 2D играх SpriteKit.

Это руководство требует, чтобы вы работали с Xcode 7.3 или выше, который включает в себя Swift 2.2 и iOS 9.3, tvOS 9.2 и OS X 10.11.4 SDK.

Чтобы продолжить, вы можете использовать проект, который вы создали в предыдущем руководстве, или загрузить свежую копию с GitHub .

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

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

Для игры в стиле сверху, которую мы создаем в этой серии, мы хотим изменить значение силы тяжести по умолчанию, предоставляемое SpriteKit. Гравитация по умолчанию предназначена для игры в виде спереди со значением (0, -9,8), которое имитирует гравитацию Земли, то есть 0 горизонтальное ускорение и ускорение вниз 9,8 м / с² . Для нашей игры нам нужна нулевая вертикальная гравитация, чтобы машина не начинала ускоряться вниз, как только мы установим ее физические свойства.

Откройте MainScene.sks и щелкните на сером фоне, чтобы выбрать сцену. Затем откройте инспектор атрибутов и измените Gravity, чтобы компоненты X и Y были установлены на 0 .

Нулевая гравитация сцены

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

Чтобы игра обнаруживала столкновения между объектами, нам нужно установить свойство contactDelegate мира физики contactDelegate . Этот делегат может быть любым объектом, который соответствует SKPhysicsContactDelegate протокол. Этот протокол определяет два метода, didBeginContact(_:) и didEndContact(_:) . Вы можете использовать эти методы для выполнения действий, основанных на объектах, которые сталкиваются в сцене.

Чтобы сохранить наш код вместе, мы собираемся сделать MainScene свой собственный контактный делегат. Откройте MainScene.swift и отредактируйте определение класса MainScene чтобы оно соответствовало протоколу SKPhysicsContactDelegate .

1
2
3
4
5
6
7
8
import UIKit
import SpriteKit
 
class MainScene: SKScene, SKPhysicsContactDelegate {
 
    …
 
}

В didMoveToView(_:) мы устанавливаем экземпляр MainScene как контактный делегат свойства physicsWorld .

1
2
3
4
5
6
override func didMoveToView(view: SKView) {
 
    …
     
    physicsWorld.contactDelegate = self
}

Мы реализуем методы протокола SKPhysicsContactDelegate позже. Сначала нам нужно настроить физические свойства узлов в сцене.

Любой узел в SpriteKit, для которого вы хотите каким-либо образом симулировать физику, должен иметь уникальный объект SKPhysicsBody . Физические тела содержат несколько свойств, в том числе:

  • масса
  • плотность
  • площадь
  • трение
  • скорость

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

В физических телах эти категории представлены свойствами categoryBitMask и collisionBitMask , которым по умолчанию присваивается значение 0xFFFFFFFF . Это означает, что все узлы принадлежат всем категориям. Важно отметить, что в этом значении каждая шестнадцатеричная цифра F является сокращенной формой и представляет число 15 в двоичных цифрах ( 1111 ), каждая из которых соответствует одной из 32 категорий, которые вы можете использовать.

Когда два узла сталкиваются, логическая операция AND выполняется на collisionBitMask и categoryBitMask первого и второго тела соответственно. Если результатом является ненулевое значение, тогда SpriteKit выполняет моделирование на двух узлах, к которым принадлежат тела.

Обратите внимание, что это вычисление AND выполняется дважды, когда два тела меняются местами. Например:

  • Расчет 1: bodyA.collisionBitMask & bodyB.categoryBitMask
  • Расчет 2: bodyB.collisionBitMask & bodyA.categoryBitMask

Если вы не знаете, как работает оператор AND , то вот очень простой пример, написанный на Swift:

1
2
3
4
let mask1 = 0x000000FF
let mask2 = 0x000000F0
 
let result = mask1 & mask2 // result = 0x000000F0

Оператор AND определяет, какие части битовых масок являются одинаковыми, и возвращает новое значение битовой маски, содержащее совпадающие части.

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

Для того чтобы эти методы выполнялись, вы должны указать contactTestBitMask для каждого тела, которое создает ненулевое значение, когда на них действует оператор AND . Для всех физических тел эта битовая маска имеет значение по умолчанию 0x00000000, что означает, что вы не будете уведомлены о каких-либо столкновениях, в которых участвует физическое тело.

Физические тела, включая их различные битовые маски, можно настроить в редакторе сцены Xcode. Откройте MainScene.sks , выберите автомобиль и откройте инспектор атрибутов справа. Прокрутите вниз до раздела определения физики .

Раздел определения физики

Поскольку для параметра « Тип тела» установлено значение « Нет» , ни одно из свойств, связанных с физикой, не отображается. Чтобы изменить это, нам нужно установить Body Type в значение, отличное от None . Доступны три типа телосложения:

  • ограничивающий прямоугольник
  • ограничивающий круг
  • альфа-маска
Физика Типы телосложения Th

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

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

Тип корпуса ограничивающего круга

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

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

Альфа-маска Тип кузова

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

По-прежнему в инспекторе атрибутов вы должны увидеть дополнительные параметры, доступные для вас в разделе « Определение физики ». Единственное свойство, которое нам нужно изменить, — это маска контакта . Измените это на значение 1 .

Контактная маска 1

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

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

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

1
import GameplayKit

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

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
func spawnObstacle(timer: NSTimer) {
    if player.hidden {
        timer.invalidate()
        return
    }
     
    let spriteGenerator = GKShuffledDistribution(lowestValue: 1, highestValue: 2)
    let obstacle = SKSpriteNode(imageNamed: «Obstacle \(spriteGenerator.nextInt())»)
    obstacle.xScale = 0.3
    obstacle.yScale = 0.3
     
    let physicsBody = SKPhysicsBody(circleOfRadius: 15)
    physicsBody.contactTestBitMask = 0x00000001
    physicsBody.pinned = true
    physicsBody.allowsRotation = false
    obstacle.physicsBody = physicsBody
     
    let center = size.width/2.0, difference = CGFloat(85.0)
    var x: CGFloat = 0
     
    let laneGenerator = GKShuffledDistribution(lowestValue: 1, highestValue: 3)
    switch laneGenerator.nextInt() {
    case 1:
        x = center — difference
    case 2:
        x = center
    case 3:
        x = center + difference
    default:
        fatalError(«Number outside of [1, 3] generated»)
    }
     
    obstacle.position = CGPoint(x: x, y: (player.position.y + 800))
    addChild(obstacle)
}

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

Мы получаем случайное число от 1 до 2 и используем его для создания узла спрайта с одним из двух спрайтов препятствий, доступных в проекте. Затем мы изменяем масштаб препятствий так, чтобы они были достаточно малы, чтобы машина могла маневрировать вокруг.

Затем мы создаем физическое тело для этого нового препятствия с кругом с радиусом 15 и маской контакта 0x00000001 . Мы устанавливаем pinned значение true и allowsRotation false чтобы препятствие оставалось на месте и не двигалось. Физическое тело тогда назначено препятствию.

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

Обратите внимание, что горизонтальная разница 85 , используемая в spawnObstacle(_:) , отличается от той, которая используется при перемещении автомобиля, 70 . Мы делаем это для того, чтобы автомобиль мог перемещаться между препятствиями.

Имея логику появления препятствий, мы можем добавить следующий код в конец метода didMoveToView(_:) .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
override func didMoveToView(view: SKView) {
     
    …
 
    let timer = NSTimer(timeInterval: 3.0, target: self, selector: #selector(spawnInObstacle(_:)), userInfo: nil, repeats: true)
    NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
     
    let camera = SKCameraNode()
    self.camera = camera
    camera.position = CGPoint(x: center, y: player.position.y + 200)
    let moveForward = SKAction.moveBy(CGVectorMake(0, 100), duration: 1.0)
    camera.runAction(SKAction.repeatActionForever(moveForward))
    addChild(camera)
     
    player.xScale = 0.4;
}

Мы создаем таймер для выполнения spawnObstacle(_:) каждые три секунды и добавляем его в основной цикл выполнения. Мы также создаем SKCameraNode будет действовать как камера для сцены, и назначаем его свойству camera этой сцены. Это приводит к визуализации сцены с точки зрения этого узла камеры. Обратите внимание, что камера расположена горизонтально и немного выше автомобиля.

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

Последняя часть для обнаружения столкновений — это реализация одного из SKPhysicsContactDelegate протокола SKPhysicsContactDelegate . В нашем случае мы собираемся реализовать метод didBeginContact(_:) поскольку мы хотим получать уведомления, как только автомобиль сталкивается с препятствием. Добавьте следующий метод в класс MainScene .

1
2
3
4
5
6
7
func didBeginContact(contact: SKPhysicsContact) {
    if contact.bodyA.node == player ||
        player.hidden = true
        player.removeAllActions()
        camera?.removeAllActions()
    }
}

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

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

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

Сцена после столкновения

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

В следующем руководстве по SpriteKit From Scratch мы рассмотрим более продвинутую визуальную функциональность в SpriteKit, включая системы частиц, источники света и фильтры.

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