В этой серии мы узнаем, как использовать SpriteKit для создания 2D-игр для iOS. В этом посте мы узнаем о двух важных особенностях SpriteKit: действиях и физике.
Чтобы следовать этому уроку, просто загрузите прилагаемое GitHub репо . Он имеет две папки: одну для действий и одну для физики. Просто откройте любой стартовый проект в Xcode, и все готово.
действия
В большинстве игр вам нужно, чтобы узлы делали что-то вроде перемещения, масштабирования или поворота. Класс SKAction
был разработан с этой целью. В классе SKAction
есть много методов класса, которые вы можете вызывать для перемещения, масштабирования или поворота свойств узла в течение определенного периода времени.
Вы также можете воспроизводить звуки, анимировать группу текстур или запускать собственный код с SKAction
класса SKAction
. Вы можете запустить одно действие, выполнить два или более действия одно за другим в последовательности, выполнить два или более действия одновременно в составе группы и даже повторить любые действия.
движение
Давайте получим узел, перемещающийся по экрану. Введите следующее в Example1.swift.
01
02
03
04
05
06
07
08
09
10
11
|
class Example1: SKScene {
let player = SKSpriteNode(imageNamed: «player»)
override func didMove(to view: SKView) {
player.position = CGPoint(x: 200, y: player.size.height)
addChild(player)
let movePlayerAction = SKAction.moveTo(y: 900, duration: 2)
player.run(movePlayerAction)
}
}
|
Здесь мы создаем SKAction
и вызываем метод класса moveTo(y:duration:)
, который принимает в качестве параметра позицию y
для перемещения узла и duration
в секундах. Чтобы выполнить действие, вы должны вызвать метод run(_:)
узла и передать SKAction
. Если вы тестируете сейчас, вы должны увидеть, как самолет движется вверх по экрану.
Существует несколько разновидностей методов перемещения, включая move(to:duration:)
, который переместит узел на новую позицию по осям x и y, и move(by:duration:)
, который переместит узел относительно в его текущем положении. Я предлагаю вам прочитать документацию по SKAction
чтобы узнать обо всех разновидностях методов перемещения.
Завершение закрытия
Существует еще одна разновидность метода run, которая позволяет вызывать некоторый код при закрытии завершения. Введите следующий код в Example2.swift .
01
02
03
04
05
06
07
08
09
10
11
12
13
|
class Example2: SKScene {
let player = SKSpriteNode(imageNamed: «player»)
override func didMove(to view: SKView) {
player.position = CGPoint(x: 200, y: player.size.height)
addChild(player)
let movePlayerAction = SKAction.moveTo(y: 900, duration: 2)
player.run(movePlayerAction, completion: {
print(«Player Finished Moving»)
})
}
}
|
Метод run(_:completion:)
позволяет запустить блок кода, когда действие полностью завершено. Здесь мы выполняем простую инструкцию print, но код может быть настолько сложным, насколько вам нужно.
Последовательности действий
Иногда вам захочется выполнять действия одно за другим, и вы можете сделать это с помощью метода sequence(_:)
. Добавьте следующее в Example3.swift .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
class Example3: SKScene {
let player = SKSpriteNode(imageNamed: «player»)
override func didMove(to view: SKView) {
player.position = CGPoint(x: 100, y: player.size.height)
addChild(player)
let movePlayerAction = SKAction.moveTo(y: 900, duration: 2)
let scalePlayerAction = SKAction.scale(to: 3, duration: 2)
let sequenceAction = SKAction.sequence([movePlayerAction, scalePlayerAction])
player.run(sequenceAction)
}
}
|
Здесь мы создаем два SKAction
: один использует moveTo(y:duration:)
, а другой — scale(to:duration:)
, который изменяет масштаб x и y узла. Затем мы вызываем метод sequence(_:)
, который принимает в качестве параметра массив SKAction
для запуска один за другим. Если вы выполните тестирование сейчас, вы увидите, как самолет движется вверх по экрану, и как только он достигнет пункта назначения, он увеличится в три раза по сравнению с его первоначальным размером.
Сгруппированные Действия
В других случаях вы можете выполнять действия вместе как группа. Добавьте следующий код в Example4.swift .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
class Example4: SKScene {
let player = SKSpriteNode(imageNamed: «player»)
override func didMove(to view: SKView) {
player.position = CGPoint(x: 100, y: player.size.height)
addChild(player)
let movePlayerAction = SKAction.moveTo(y: 900, duration: 2)
let scalePlayerAction = SKAction.scale(to: 3, duration: 2)
let groupAction = SKAction.group([movePlayerAction, scalePlayerAction])
player.run(groupAction)
}
}
|
Здесь мы используем те же методы moveTo
и scale
что и в предыдущем примере, но мы также вызываем метод group(_:)
, который принимает в качестве параметра массив SKAction
s для запуска в одно и то же время. Если бы вы проверили сейчас, вы бы увидели, что самолет движется и масштабируется одновременно.
Обратные действия
Некоторые из этих действий можно отменить, вызвав метод reversed()
. Лучший способ выяснить, какие действия поддерживают метод reversed()
— обратиться к документации. Одним из обратимых действий является fadeOut(withDuration:)
, который приведет к исчезновению узла из-за изменения его альфа-значения. Давайте заставим самолет исчезать, а затем снова возвращаться. Добавьте следующее в Example5.swift .
01
02
03
04
05
06
07
08
09
10
11
12
13
|
class Example5: SKScene {
override func didMove(to view: SKView) {
let player = SKSpriteNode(imageNamed: «player»)
player.position = CGPoint(x: 100, y: player.size.height)
addChild(player)
let fadePlayerAction = SKAction.fadeOut(withDuration: 2)
let fadePlayerActionReversed = fadePlayerAction.reversed()
let fadePlayerSequence = SKAction.sequence([fadePlayerAction, fadePlayerActionReversed])
player.run(fadePlayerSequence)
}
}
|
Здесь мы создаем SKAction
и вызываем метод fadeOut(withDuration:)
. В следующей строке кода мы вызываем метод reversed()
, который заставит действие полностью изменить то, что он только что сделал. Протестируйте проект, и вы увидите, как самолет исчезает, а затем возвращается обратно.
Повторяющиеся действия
Если вам когда-нибудь понадобится повторить действие определенное количество раз, вы уже рассмотрели методы repeat(_:count:)
и repeatForever(_:)
. Давайте заставим самолет многократно исчезать, а затем возвращаться навсегда. Введите следующий код в Example6.swift .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
class Example6: SKScene {
let player = SKSpriteNode(imageNamed: «player»)
override func didMove(to view: SKView) {
player.position = CGPoint(x: 100, y: player.size.height)
addChild(player)
let fadePlayerAction = SKAction.fadeOut(withDuration: 2)
let fadePlayerActionReversed = fadePlayerAction.reversed()
let fadePlayerSequence = SKAction.sequence([fadePlayerAction, fadePlayerActionReversed])
let fadeOutInRepeatAction = SKAction.repeatForever(fadePlayerSequence)
player.run(fadeOutInRepeatAction)
}
}
|
Здесь мы вызываем метод repeatForever(_:)
, передавая в fadePlayerSequence
. Если вы проверите, вы увидите, что самолет затухает, а затем возвращается обратно навсегда.
Остановка действий
Много раз вам нужно будет остановить узел от выполнения его действий. Для этого вы можете использовать метод removeAllActions()
. Давайте заставим узел игрока перестать исчезать, когда мы дотрагиваемся до экрана. Добавьте следующее в Example7.swift .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
class Example7: SKScene {
let player = SKSpriteNode(imageNamed: «player»)
override func didMove(to view: SKView) {
player.position = CGPoint(x: 100, y: player.size.height)
addChild(player)
let fadePlayerAction = SKAction.fadeOut(withDuration: 2)
let fadePlayerActionReversed = fadePlayerAction.reversed()
let fadePlayerSequence = SKAction.sequence([fadePlayerAction, fadePlayerActionReversed])
let fadeOutInRepeatAction = SKAction.repeatForever(fadePlayerSequence)
player.run(fadeOutInRepeatAction)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
player.removeAllActions()
}
}
|
Если вы коснетесь экрана, на узле игрока будут удалены все действия, и он больше не будет исчезать и исчезать.
Отслеживание действий
Иногда вам нужен способ отслеживать ваши действия. Например, если вы выполняете два или более действия на узле, вы можете захотеть идентифицировать их. Вы можете сделать это, зарегистрировав ключ в узле, который представляет собой простую строку текста. Введите следующее в Example8.swift .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
class Example8: SKScene {
let player = SKSpriteNode(imageNamed: «player»)
override func didMove(to view: SKView) {
player.position = CGPoint(x: 100, y: player.size.height)
addChild(player)
let fadePlayerAction = SKAction.fadeOut(withDuration: 2)
let fadePlayerActionReversed = fadePlayerAction.reversed()
let fadePlayerSequence = SKAction.sequence([fadePlayerAction, fadePlayerActionReversed])
let fadeOutInRepeatAction = SKAction.repeatForever(fadePlayerSequence)
player.run(fadeOutInRepeatAction, withKey: «faderepeataction»)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if player.action(forKey: «faderepeataction») != nil {
player.removeAction(forKey: «faderepeataction»)
}
}
}
|
Здесь мы run(_:withKey:)
метод run(_:withKey:)
узла run(_:withKey:)
, который, как уже упоминалось, принимает простую строку текста. Внутри метода touchesBegan(_:with:)
мы action(forKey:)
чтобы убедиться, что у узла есть назначенный нами ключ. Если это так, мы вызываем .removeAction(forKey:)
, который принимает в качестве параметра ключ, который вы ранее установили.
Звуковые Действия
Много раз вы захотите сыграть звук в своей игре. Вы можете сделать это, используя метод класса playSoundFileNamed(_:waitForCompletion:)
. Введите следующее в Example9.swift .
1
2
3
4
5
6
7
8
|
class Example9: SKScene {
override func didMove(to view: SKView) {
let planeSound = SKAction.playSoundFileNamed(«planesound», waitForCompletion: false)
run(planeSound)
}
}
|
playSoundFileNamed(_:waitForCompletion:)
принимает в качестве параметров имя звукового файла без расширения и логическое значение, определяющее, будет ли действие ждать до завершения звука, прежде чем двигаться дальше.
Например, предположим, что у вас было два действия в последовательности, со звуком, являющимся первым действием. Если waitForCompletion
true
то последовательность будет ожидать завершения воспроизведения этого звука, прежде чем перейти к следующему действию в последовательности. Если вам нужно больше контроля над своими звуками, вы можете использовать SKAudioNode
. Мы не будем рассказывать о SKAudioNode
в этой серии, но вам обязательно стоит взглянуть на это в своей карьере разработчика SpriteKit.
Кадровая анимация
Многие игры требуют анимации группы изображений. animate(with:timePerFrame:)
охватывает вас в этих случаях. Введите следующее в Example10.swift .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
class Example10: SKScene {
let explosion = SKSpriteNode(imageNamed: «explosion1»)
override func didMove(to view: SKView) {
addChild(explosion)
var explosionTextures:[SKTexture] = []
for i in 1…6 {
explosionTextures.append(SKTexture(imageNamed: «explosion\(i)»))
}
let explosionAnimation = SKAction.animate(with: explosionTextures,
timePerFrame: 0.3)
explosion.run(explosionAnimation)
}
}
|
animate(with:timePerFrame:)
принимает в качестве параметра массив SKTexture
s и значение timePerFrame
которое будет указывать, сколько времени потребуется между каждым изменением текстуры. Чтобы выполнить это действие, вы вызываете метод run узла и передаете SKAction
.
Действия с пользовательским кодом
Последний тип действий, который мы рассмотрим, — это тот, который позволяет запускать пользовательский код. Это может пригодиться, когда вам нужно что-то сделать в середине ваших действий или просто нужен способ выполнить что-то, SKAction
не обеспечивает класс SKAction
. Введите следующее в Example11.swift .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
class Example11: SKScene {
override func didMove(to view: SKView) {
let runAction = SKAction.run(printToConsole)
run(runAction)
}
func printToConsole(){
print(«SKAction.run() invoked»)
}
}
|
Здесь мы вызываем метод run(_:)
сцены и передаем функцию printToConsole()
в качестве параметра. Помните, что сцены также являются узлами, поэтому вы также можете вызывать метод run(_:)
для них.
На этом мы завершаем изучение действий. С классом SKAction
вы можете многое сделать, и после прочтения этого урока я бы посоветовал вам продолжить изучение документации по SKActions
.
физика
SpriteKit предлагает надежный физический движок из коробки, с небольшими настройками. Чтобы начать, вы просто добавляете физическое тело в каждый из ваших узлов, и все готово. Физический движок построен поверх популярного движка Box2d. Однако API-интерфейс SpriteKit намного проще в использовании, чем оригинальный интерфейс Box2d.
Давайте начнем с добавления физического тела к узлу и посмотрим, что произойдет. Добавьте следующий код в Example1.swift .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
…
let player = SKSpriteNode(imageNamed: «enemy1»)
override func didMove(to view: SKView) {
…
player.physicsBody = SKPhysicsBody(circleOfRadius: player.size.width/2)
player.physicsBody?.affectedByGravity = false
player.position = CGPoint(x: size.width/2 , y: size.height — player.size.height)
addChild(player)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
…
player.physicsBody?.affectedByGravity = true
}
|
Идите и протестируйте проект сейчас. Вы увидите самолет, сидящий на вершине сцены. Как только вы нажмете на экран, самолет упадет с экрана, и он будет падать вечно. Это показывает, как просто начать использовать физику — просто добавьте физическое тело в узел, и все готово.
physicsBody
Свойство physicsBody
имеет тип SKPhysicsBody
, который будет грубым контуром формы вашего узла … или очень точным контуром формы вашего узла, в зависимости от того, какой конструктор вы используете для инициализации этого свойства.
Здесь мы использовали init(circleOfRadius:)
, который принимает в качестве параметра радиус круга. Есть несколько других инициализаторов, включая один для прямоугольника или многоугольника из CGPath. Вы даже можете использовать собственную текстуру узла, что сделало бы physicsBody
почти точным представлением узла.
Чтобы понять, что я имею в виду, обновите файл GameViewController.swift с помощью следующего кода. Я прокомментировал строку, которая будет добавлена.
01
02
03
04
05
06
07
08
09
10
11
12
|
override func viewDidLoad() {
super.viewDidLoad()
let scene = GameScene(size:CGSize(width: 768, height: 1024))
let skView = self.view as!
skView.showsFPS = false
skView.showsNodeCount = false
skView.ignoresSiblingOrder = false
skView.showsPhysics = true // THIS LINE ADDED
scene.scaleMode = .aspectFill
skView.presentScene(scene)
}
|
Теперь physicsBody
узла будет physicsBody
зеленым цветом. В обнаружении столкновений форма физики physicsBody
— это то, что оценивается. В этом примере круг вокруг плоскости будет направлять обнаружение столкновений, а это означает, что если пуля, например, попадет в наружный край круга, то это будет считаться столкновением.
Теперь добавьте следующее в Example2.swift .
1
2
3
4
5
6
|
override func didMove(to view: SKView) {
player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)
player.physicsBody?.affectedByGravity = false
player.position = CGPoint(x: size.width/2 , y: size.height — player.size.height)
addChild(player)
}
|
Здесь мы используем текстуру спрайта. Если вы сейчас протестируете проект, то увидите, что контур изменился почти на точное представление текстуры спрайта.
Сила тяжести
В предыдущих примерах мы установили для physicsBody
значение false
. Как только вы добавите физическое тело в узел, физический движок вступит во владение. В результате самолет падает сразу после запуска проекта!
Вы также можете установить гравитацию для каждого узла, как у нас здесь, или вы можете полностью отключить гравитацию. Добавьте следующее в Example3.swift .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
let player = SKSpriteNode(imageNamed: «enemy1»)
override func didMove(to view: SKView) {
…
player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)
player.position = CGPoint(x: size.width/2 , y: size.height — player.size.height)
addChild(player)
physicsWorld.gravity = CGVector(dx:0, dy: 0)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
…
physicsWorld.gravity = CGVector(dx:0 , dy: -9.8)
}
|
Мы можем установить гравитацию, используя physicsWorld
gravity
physicsWorld
. Свойство gravity
относится к типу CGVector
. Мы устанавливаем компоненты dx
и dy
на 0 , а затем при касании экрана устанавливаем свойство dy
на -9.8 . Компоненты измеряются в метрах, и по умолчанию используется значение ( 0, -9,8 ), которое представляет гравитацию Земли.
Краевые петли
В его нынешнем виде любые узлы, добавленные в сцену, просто исчезнут с экрана навсегда. Мы можем добавить контур края вокруг сцены, используя метод init(edgeLoopFrom:)
. Добавьте следующее в Example4.swift .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
let player = SKSpriteNode(imageNamed: «enemy1»)
override func didMove(to view: SKView) {
…
player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)
player.position = CGPoint(x: size.width/2 , y: size.height — player.size.height)
addChild(player)
physicsWorld.gravity = CGVector(dx:0, dy: 0)
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
…
physicsWorld.gravity = CGVector(dx:0 , dy: -9.8)
}
|
Здесь мы добавили физическое тело к самой сцене. init(edgeLoopFrom:)
принимает в качестве параметра CGRect
который определяет его ребра. Если вы проверите сейчас, вы увидите, что самолет все еще падает; однако, он взаимодействует с этим краевым циклом и больше не выпадает из сцены. Это также подпрыгивает и даже поворачивается немного на его стороне. Это сила физического движка — вы получаете все эти функции из коробки бесплатно. Писать что-то подобное самостоятельно было бы довольно сложно.
Bounciness
Мы видели, что самолет подпрыгивает и поворачивается на бок. Вы можете контролировать бодрость и позволяет ли физическое тело вращаться. Введите следующее в Example5.swift .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
let player = SKSpriteNode(imageNamed: «enemy1»)
override func didMove(to view: SKView) {
…
player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)
player.physicsBody?.restitution = 0.8
player.physicsBody?.allowsRotation = false
player.position = CGPoint(x: size.width/2 , y: size.height — player.size.height)
addChild(player)
physicsWorld.gravity = CGVector(dx:0, dy: 0)
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
…
physicsWorld.gravity = CGVector(dx:0 , dy: -9.8)
}
|
Если вы проведете тестирование сейчас, вы увидите, что player
очень бодрый, и на его настройку уходит несколько секунд. Вы также заметите, что он больше не вращается. Свойство restitution
принимает число от 0,0 (менее оживленное) до 1,0 (очень оживленное), а свойство allowsRotation
является простым логическим значением.
фрикционный
В реальном мире, когда два объекта движутся друг против друга, между ними возникает небольшое трение. Вы можете изменить величину трения, которое имеет физическое тело — это равносильно «шероховатости» тела. Это свойство должно быть между 0,0 и 1,0 . По умолчанию установлено значение 0,2 . Добавьте следующее в Example6.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
|
let player = SKSpriteNode(imageNamed: «enemy1»)
override func didMove(to view: SKView) {
…
player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)
player.physicsBody?.allowsRotation = false
player.physicsBody?.restitution = 0.0
player.name = «player»
player.position = CGPoint(x: size.width/2 , y: size.height — player.size.height)
addChild(player)
let rectangle = SKSpriteNode(color: .blue, size: CGSize(width: size.width, height: 20))
rectangle.physicsBody = SKPhysicsBody(rectangleOf: rectangle.size)
rectangle.physicsBody?.isDynamic = false
rectangle.zRotation = 3.14 * 220 / 180
rectangle.physicsBody?.friction = 0.0
rectangle.position = CGPoint(x: size.width/2, y: size.width/2)
addChild(rectangle)
physicsWorld.gravity = CGVector(dx:0, dy: 0)
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
…
physicsWorld.gravity = CGVector(dx:0 , dy: -9.8)
}
|
Здесь мы создаем прямоугольный Sprite и устанавливаем свойство friction
в его physicsBody
в 0.0 . Если вы проверите сейчас, вы увидите, что самолет очень быстро скользит по повернутому прямоугольнику. Теперь измените свойство трения на 1.0 и протестируйте снова. Вы увидите, что самолет скользит по прямоугольнику не так быстро. Это из-за трения. Если вы хотите, чтобы он двигался еще медленнее, вы можете применить больше трения к physicsBody
(помните, что по умолчанию установлено значение 0,2 ).
Плотность и масса
Есть несколько других свойств, которые вы можете изменить на физическом теле, такие как плотность и масса. Свойства density
и mass
взаимосвязаны, и когда вы меняете одно, другое автоматически пересчитывается. Когда вы впервые создаете физическое тело, свойство area
тела вычисляется и впоследствии никогда не изменяется (оно доступно только для чтения). Плотность и масса основаны на формуле mass = density * area
.
Если в сцене более одного узла, плотность и масса будут влиять на симуляцию того, как узлы отскакивают друг от друга и взаимодействуют. Представьте себе баскетбол и шар для боулинга — они примерно одинакового размера, но шар для боулинга гораздо плотнее. Когда они сталкиваются, баскетбол меняет направление и скорость намного больше, чем шар для боулинга.
Сила и Импульс
Вы можете применить силы и импульсы, чтобы переместить физическое тело. Импульс применяется сразу и только один раз. С другой стороны, сила обычно применяется для постоянного эффекта. Сила применяется с момента добавления силы до следующей обработки симуляции. Чтобы применить непрерывную силу, вам нужно будет применить ее к каждому кадру. Добавьте следующее в Example7.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
|
let player = SKSpriteNode(imageNamed: «enemy1»)
override func didMove(to view: SKView) {
…
player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)
player.physicsBody?.allowsRotation = false
player.name = «player»
player.position = CGPoint(x: size.width/2 , y: size.height — player.size.height)
addChild(player)
physicsWorld.gravity = CGVector(dx:0, dy: 0)
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
…
physicsWorld.gravity = CGVector(dx:0 , dy: -9.8)
if(touchedNode.name == «player»){
player.physicsBody?.applyImpulse(CGVector(dx:0 , dy: 100))
}
}
|
Запустите проект и подождите, пока плеер не остановится в нижней части экрана, а затем нажмите на плеер. Вы увидите, как игрок взлетает на экран и в конце концов снова останавливается внизу. Мы применяем импульс, используя метод applyImpulse(_:)
, который принимает в качестве параметра CGVector
и измеряется в ньютон-секундах.
Почему бы не попробовать обратное и добавить силу к узлу игрока? Помните, что вам нужно будет постоянно добавлять силу, чтобы получить желаемый эффект. Хорошее место для этого — метод update(_:)
сцены update(_:)
. Кроме того, вы можете попытаться увеличить свойство восстановления игрока, чтобы увидеть, как оно влияет на симуляцию.
1
2
3
|
override func update(_ currentTime: TimeInterval) {
//APPLY FORCE HERE
}
|
Обнаружение столкновения
Физический движок имеет надежную систему обнаружения столкновений и контактов. По умолчанию любые два узла с физическими телами могут сталкиваться. Вы видели это в предыдущих примерах — никакого специального кода не требовалось, чтобы объекты взаимодействовали. Однако вы можете изменить это поведение, установив «категорию» в теле физики. Затем эту категорию можно использовать для определения того, какие узлы будут сталкиваться друг с другом, а также для информирования вас, когда определенные узлы вступают в контакт.
Разница между контактом и столкновением заключается в том, что контакт используется для определения того, когда два физических тела соприкасаются друг с другом. С другой стороны, столкновение не позволяет двум физическим телам пересекаться в пространстве друг друга — когда физический движок обнаруживает столкновение, он применяет противоположные импульсы, чтобы снова разделить объекты. Мы видели столкновения в действии с игроком и контуром ребра, а также игроком и прямоугольником из предыдущих примеров.
Типы физики physicsBodies
Прежде чем мы перейдем к настройке наших категорий для физических тел, мы должны поговорить о типах physicsBodies
. Есть три:
- Динамический объем имитирует объекты с объемом и массой. На эти объекты влияют силы и столкновения в мире физики (например, самолет в предыдущих примерах).
- На статический объем не влияют силы и столкновения. Однако, поскольку он сам имеет объем, другие тела могут отскакивать и взаимодействовать с ним. Вы устанавливаете свойство
isDynamic
физического тела в false, чтобы создать статический том. Эти объемы никогда не перемещаются физическим движком. Мы видели это в действии ранее с примером шесть, где самолет взаимодействовал с прямоугольником, но на прямоугольник не влиял самолет или гравитация. Чтобы понять, что я имею в виду, вернитесь к примеру 6 и удалите строку кода, которая устанавливаетrectangle.physicsBody?.isDynamic = false
. - Третий тип физического тела — это ребро , которое представляет собой статическое тело без объема. Мы видели этот тип тела в действии с контуром края, который мы создали вокруг сцены во всех предыдущих примерах. Края взаимодействуют с другими основанными на объеме телами, но никогда с другим ребром.
В категориях используется 32- разрядное целое число с 32 отдельными флагами, которые могут быть включены или выключены. Это также означает, что вы можете иметь максимум 32 категории. Это не должно представлять проблему для большинства игр, но об этом следует помнить.
Создание категорий
Создайте новый файл Swift, перейдя в меню « Файл» > « Создать» > « Файл» и убедившись, что файл Swift выделен.
Введите PhysicsCategories в качестве имени и нажмите Создать .
Введите следующее в файл, который вы только что создали.
1
2
3
4
5
|
struct PhysicsCategories {
static let Player : UInt32 = 0x1 << 0
static let EdgeLoop : UInt32 = 0x1 << 1
static let Redball : UInt32 = 0x1 << 2
}
|
Мы используем структуру PhysicsCategories
для создания категорий для Player
, EdgeLoop
и RedBall
. Мы используем битовое смещение, чтобы включить биты.
Теперь введите следующее в Example8.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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
…
let player = SKSpriteNode(imageNamed: «enemy1»)
var dx = -20
var dy = -20
override func didMove(to view: SKView) {
player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)
player.physicsBody?.allowsRotation = false
player.physicsBody?.restitution = 0.0
player.physicsBody?.categoryBitMask = PhysicsCategories.Player
player.physicsBody?.contactTestBitMask = PhysicsCategories.RedBall
player.physicsBody?.collisionBitMask = PhysicsCategories.EdgeLoop
player.position = CGPoint(x: size.width/2 , y: size.height — player.size.height)
addChild(player)
player.physicsBody?.applyImpulse(CGVector(dx: dx, dy: dy))
let redBall = SKShapeNode(circleOfRadius: 100)
redBall.fillColor = SKColor.red
redBall.position = CGPoint(x: size.width/2, y: size.height/2)
redBall.physicsBody = SKPhysicsBody(circleOfRadius: 100)
redBall.physicsBody?.isDynamic = false
redBall.physicsBody?.categoryBitMask = PhysicsCategories.RedBall
addChild(redBall)
physicsWorld.gravity = CGVector(dx:0, dy: 0)
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
physicsWorld.contactDelegate = self
physicsBody?.categoryBitMask = PhysicsCategories.EdgeLoop
physicsBody?.contactTestBitMask = PhysicsCategories.Player
}
func didBegin(_ 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 & PhysicsCategories.Player != 0) && (secondBody.categoryBitMask & PhysicsCategories.RedBall != 0)){
print(«Player and RedBall Contact»)
}
if((firstBody.categoryBitMask & PhysicsCategories.Player != 0) && (secondBody.categoryBitMask & PhysicsCategories.EdgeLoop != 0)){
print («Player and EdgeLoop Contact»)
dx *= -1
dy *= -1
player.physicsBody?.applyImpulse(CGVector(dx: dx, dy: dy))
}
}
|
Здесь мы создаем player
как обычно и создаем две переменные dx
и dy
, которые будут использоваться в качестве компонентов CGVector
когда мы применяем импульс к player
.
Внутри didMove(to:)
мы настраиваем проигрыватель и добавляем categoryBitMask
contactBitMask
, contactBitMask
и collisionBitMask
. categoryBitMask
должна иметь смысл — это проигрыватель, поэтому мы установили для него PhysicsCategories.Player
. Нас интересует, когда игрок вступает в контакт с redBall
, поэтому мы устанавливаем contactBitMask
в contactBitMask
. И, наконец, мы хотим, чтобы он сталкивался с физическим циклом края и подвергался его влиянию, поэтому мы устанавливаем его collisionBitMask
PhysicsCategories.EdgeLoop
. Наконец, мы применяем импульс, чтобы заставить его двигаться.
На redBall
мы просто устанавливаем его categoryBitMask
redBall
. С помощью edgeLoop
мы устанавливаем его categoryBitMask
, и поскольку нас интересует, когда player
вступает с ним в контакт, мы устанавливаем его contactBitMask
.
При настройке contactBitMask
и collisionBitMask
только один из тел должен ссылаться на другой. Другими словами, вам не нужно настраивать оба тела как контактирующие или сталкивающиеся с другим.
Для edgeLoop
мы устанавливаем его для контакта с игроком. Однако вместо этого мы могли бы настроить проигрыватель для взаимодействия с edgeLoop
, используя побитовый оператор или ( |
). Используя этот оператор, вы можете настроить несколько битовых масок контактов или коллизий. Например:
1
|
player.physicsBody?.contactTestBitMask = PhysicsCategories.RedBall |
|
Чтобы иметь возможность отвечать, когда два тела вступают в контакт, вы должны реализовать протокол SKPhysicsContactDelegate
. Возможно, вы заметили это в примере кода.
1
2
3
4
|
class Example8: SKScene,SKPhysicsContactDelegate{
…
physicsWorld.contactDelegate = self
…
|
Чтобы реагировать на события контактов, вы можете реализовать didBegin(_:)
и didEnd(_:)
. Они будут вызваны, когда два объекта начали контактировать и когда они закончили контакт соответственно. Мы будем придерживаться didBegin(_:)
для этого урока.
Вот код еще раз для didBegin(_:)
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
func didBegin(_ 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 & PhysicsCategories.Player != 0) && (secondBody.categoryBitMask & PhysicsCategories.RedBall != 0)){
print(«Player and RedBall Contact»)
}
if((firstBody.categoryBitMask & PhysicsCategories.Player != 0) && (secondBody.categoryBitMask & PhysicsCategories.EdgeLoop != 0)){
print («Player and EdgeLoop Contact»)
dx *= -1
dy *= -1
player.physicsBody?.applyImpulse(CGVector(dx: dx, dy: dy))
}
}
|
Сначала мы устанавливаем две переменные firstBody
и secondBody
. Два тела в параметре contact не передаются в гарантированном порядке, поэтому мы будем использовать оператор if, чтобы определить, какое тело имеет более низкую contactBitMask
и установить для него значение firstBody
.
Теперь мы можем проверить и посмотреть, какие физические тела вступают в контакт. Мы проверяем, с какими физическими телами мы имеем дело, добавляя ( &&
) категориюBitMask тел к PhysicsCategory
s, который мы установили ранее, и если результат ненулевой, мы знаем, что у нас правильное тело.
Наконец, мы печатаем, какие органы вступают в контакт. Если это был player и edgeLoop
, мы также инвертируем свойства dx
и dy
и применяем импульс к игроку. Это заставляет игрока постоянно двигаться.
На этом мы завершаем изучение физического движка SpriteKit. Существует много того, что не было рассмотрено, например, SKPhysicsJoint . Физический движок очень устойчив, и я настоятельно рекомендую вам прочитать все его аспекты, начиная с SKPhysicBody .
Вывод
В этом посте мы узнали о действиях и физике — двух очень важных частях фреймворка SpriteKit. Мы рассмотрели много примеров, но многое еще можно сделать с помощью действий и физики, и документация — отличное место для изучения.
В следующей и последней части этой серии мы соберем все, что узнали, создав простую игру. Спасибо за чтение, и я увижу вас там!
А пока ознакомьтесь с некоторыми из наших комплексных курсов по разработке Swift и SpriteKit!