Статьи

Основы SpriteKit: действия и физика

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

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

  1. Динамический объем имитирует объекты с объемом и массой. На эти объекты влияют силы и столкновения в мире физики (например, самолет в предыдущих примерах).
  2. На статический объем не влияют силы и столкновения. Однако, поскольку он сам имеет объем, другие тела могут отскакивать и взаимодействовать с ним. Вы устанавливаете свойство isDynamic физического тела в false, чтобы создать статический том. Эти объемы никогда не перемещаются физическим движком. Мы видели это в действии ранее с примером шесть, где самолет взаимодействовал с прямоугольником, но на прямоугольник не влиял самолет или гравитация. Чтобы понять, что я имею в виду, вернитесь к примеру 6 и удалите строку кода, которая устанавливает rectangle.physicsBody?.isDynamic = false .
  3. Третий тип физического тела — это ребро , которое представляет собой статическое тело без объема. Мы видели этот тип тела в действии с контуром края, который мы создали вокруг сцены во всех предыдущих примерах. Края взаимодействуют с другими основанными на объеме телами, но никогда с другим ребром.

В категориях используется 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!

  • стриж
    Запрограммируйте игру с боковой прокруткой с помощью Swift и SpriteKit
    Дерек Дженсен
  • IOS
    Идите дальше со Swift: анимация, работа в сети и пользовательские элементы управления
    Маркус Мюльбергер