Статьи

SpriteKit From Scratch: передовые методы и оптимизации

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

Это руководство требует, чтобы вы работали с Xcode 7.3 или выше, который включает в себя Swift 2.2 и iOS 9.3, tvOS 9.2 и OS X 10.11.4 SDK. Чтобы продолжить, вы можете использовать проект, который вы создали в предыдущем руководстве, или загрузить свежую копию с GitHub .

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

Для оптимизации использования памяти вашей игрой SpriteKit предоставляет функциональность текстурных атласов в форме класса SKTextureAtlas . Эти атласы эффективно объединяют указанные вами текстуры в одну большую текстуру, которая занимает меньше памяти, чем отдельные текстуры самостоятельно.

К счастью, Xcode может очень легко создавать текстурные атласы. Это делается в тех же каталогах ресурсов, которые используются для других изображений и ресурсов в ваших играх. Откройте ваш проект и перейдите в каталог активов Assets.xcassets . В нижней части левой боковой панели нажмите кнопку + и выберите параметр « Новый атлас спрайтов».

Создать новый атлас спрайтов

В результате новая папка добавляется в каталог активов. Нажмите на папку один раз, чтобы выбрать ее, и нажмите еще раз, чтобы переименовать ее. Назовите это Препятствия . Затем перетащите ресурсы препятствий 1 и 2 в эту папку. Вы также можете удалить пустой ресурс Sprite, который генерирует XCode, если вы хотите, но это не обязательно. По завершении ваш расширенный текстурный атлас препятствий должен выглядеть следующим образом:

Атлас текстур препятствий

Теперь пришло время использовать текстурный атлас в коде. Откройте MainScene.swift и добавьте следующее свойство к Класс MainScene . Мы инициализируем текстурный атлас, используя имя, которое мы ввели в наш каталог ресурсов.

1
let obstaclesAtlas = SKTextureAtlas(named: «Obstacles»)

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

В классе MainScene добавьте следующий код в конец метода didMoveToView(_:) :

1
2
3
4
5
6
7
8
override func didMoveToView(view: SKView) {
     
    …
     
    obstaclesAtlas.preloadWithCompletionHandler {
        // Do something once texture atlas has loaded
    }
}

Чтобы извлечь текстуру из текстурного атласа, вы используете метод textureNamed(_:) с именем, которое вы указали в каталоге ресурсов в качестве параметра. Давайте обновим метод spawnObstacle(_:) в классе 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
36
37
38
39
func spawnObstacle(timer: NSTimer) {
    if player.hidden {
        timer.invalidate()
        return
    }
 
    let spriteGenerator = GKShuffledDistribution(lowestValue: 1, highestValue: 2)
    let texture = obstaclesAtlas.textureNamed(«Obstacle \(spriteGenerator)»)
    let obstacle = SKSpriteNode(texture: texture)
    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)
     
    obstacle.lightingBitMask = 0xFFFFFFFF
    obstacle.shadowCastBitMask = 0xFFFFFFFF
}

Обратите внимание: если ваша игра использует ресурсы по требованию (ODR), вы можете легко указать один или несколько тегов для каждого атласа текстуры. После успешного доступа к правильным тегам тегов ресурсов с помощью API ODR вы можете использовать свой атлас текстуры так же, как мы использовали spawnObstacle(_:) . Вы можете прочитать больше о ресурсах по требованию в другом моем учебнике .

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

Сохранение и загрузка вашей игры обрабатываются протоколом NSCoding , которому уже соответствует класс SKScene . Реализация SpriteKit методов, требуемых этим протоколом, позволяет автоматически сохранять и загружать все детали вашей сцены. Если вы хотите, вы также можете переопределить эти методы, чтобы сохранить некоторые пользовательские данные вместе со своей сценой.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
// MARK: — NSCoding Protocol
 
required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
 
    let carHasCrashed = aDecoder.decodeBoolForKey(«carCrashed»)
 
    print(«car crashed: \(carHasCrashed)»)
}
 
override func encodeWithCoder(aCoder: NSCoder) {
    super.encodeWithCoder(aCoder)
 
    let carHasCrashed = player.hidden
    aCoder.encodeBool(carHasCrashed, forKey: «carCrashed»)
}

Если вы не знакомы с протоколом NSCoding , метод encodeWithCoder(_:) обрабатывает сохранение вашей сцены, а инициализатор с одним параметром NSCoder — загрузкой.

Затем добавьте следующий метод в класс MainScene . Метод saveScene() создает представление сцены в NSData , используя класс NSKeyedArchiver . Для простоты мы храним данные в NSUserDefaults .

1
2
3
4
func saveScene() {
    let sceneData = NSKeyedArchiver.archivedDataWithRootObject(self)
    NSUserDefaults.standardUserDefaults().setObject(sceneData, forKey: «currentScene»)
}

Затем замените реализацию didBeginContactMethod(_:) в классе MainScene следующим MainScene :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
func didBeginContact(contact: SKPhysicsContact) {
    if contact.bodyA.node == player ||
        if let explosionPath = NSBundle.mainBundle().pathForResource(«Explosion», ofType: «sks»),
            let smokePath = NSBundle.mainBundle().pathForResource(«Smoke», ofType: «sks»),
            let explosion = NSKeyedUnarchiver.unarchiveObjectWithFile(explosionPath) as?
            let smoke = NSKeyedUnarchiver.unarchiveObjectWithFile(smokePath) as?
             
            player.removeAllActions()
            player.hidden = true
            player.physicsBody?.categoryBitMask = 0
            camera?.removeAllActions()
             
            explosion.position = player.position
            smoke.position = player.position
             
            addChild(smoke)
            addChild(explosion)
             
            saveScene()
        }
    }
}

Первым изменением, внесенным в этот метод, является редактирование categoryBitMask узла игрока, а не его полное удаление со сцены. Это гарантирует, что после перезагрузки сцены, узел игрока все еще там, даже если он не виден, но повторяющиеся столкновения не обнаруживаются. Другое сделанное изменение — это вызов saveScene() который мы определили ранее после запуска пользовательской логики взрыва.

Наконец, откройте ViewController.swift и замените метод viewDidLoad() следующей реализацией:

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
override func viewDidLoad() {
    super.viewDidLoad()
 
    let skView = SKView(frame: view.frame)
 
    var scene: MainScene?
 
    if let savedSceneData = NSUserDefaults.standardUserDefaults().objectForKey(«currentScene») as?
       let savedScene = NSKeyedUnarchiver.unarchiveObjectWithData(savedSceneData) as?
        scene = savedScene
 
    } else if let url = NSBundle.mainBundle().URLForResource(«MainScene», withExtension: «sks»),
              let newSceneData = NSData(contentsOfURL: url),
              let newScene = NSKeyedUnarchiver.unarchiveObjectWithData(newSceneData) as?
        scene = newScene
    }
 
    skView.presentScene(scene)
    view.insertSubview(skView, atIndex: 0)
 
    let left = LeftLane(player: scene!.player)
    let middle = MiddleLane(player: scene!.player)
    let right = RightLane(player: scene!.player)
 
    stateMachine = LaneStateMachine(states: [left, middle, right])
    stateMachine?.enterState(MiddleLane)
}

При загрузке сцены мы сначала проверяем, сохранены ли данные в стандартном NSUserDefaults . Если это так, мы получаем эти данные и воссоздаем объект MainScene используя класс NSKeyedUnarchiver . Если нет, мы получаем URL для файла сцены, который мы создали в XCode, и загружаем данные из него аналогичным образом.

Запустите ваше приложение и столкнитесь с препятствием на вашем автомобиле. На этом этапе вы не видите разницы. Тем не менее, запустите ваше приложение еще раз, и вы должны увидеть, что ваша сцена была восстановлена ​​в точности так, как это было, когда вы только что разбили машину.

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

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

Циклы анимации выполняются следующим образом:

Сцена вызывает свой метод update(_:) . Этот метод имеет единственный параметр NSTimeInterval , который NSTimeInterval текущее системное время. Этот временной интервал может быть полезен, поскольку он позволяет рассчитать время, необходимое для рендеринга предыдущего кадра.

Если значение больше 1/60 секунды, ваша игра не будет работать со скоростью 60 кадров в секунду, к которой стремится SpriteKit. Это означает, что вам может потребоваться изменить некоторые аспекты вашей сцены (например, частицы, количество узлов), чтобы уменьшить ее сложность.

Сцена выполняет и вычисляет действия, которые вы добавили к своим узлам, и позиционирует их соответственно.

Сцена вызывает свой didEvaluateActions() . Здесь вы можете выполнить любую пользовательскую логику, прежде чем SpriteKit продолжит цикл анимации.

Сцена выполняет свои физические симуляции и соответственно меняет вашу сцену.

Сцена вызывает метод didSimulatePhysics() , который можно переопределить с помощью didEvaluateActions() .

Сцена применяет ограничения, которые вы добавили к своим узлам.

Сцена вызывает метод didApplyConstraints() , который можно переопределить.

Сцена вызывает метод didFinishUpdate() , который также можно переопределить. Это последний метод, в котором вы можете изменить свою сцену до того, как ее внешний вид для этого кадра будет завершен.

Наконец, сцена визуализирует свое содержимое и соответственно обновляет содержащий SKView .

Важно отметить, что если вы используете объект SKSceneDelegate а не пользовательский подкласс, каждый метод получает дополнительный параметр и слегка меняет свое имя. Дополнительным параметром является объект SKScene , который позволяет вам определить, к какой сцене запускается метод. Методы, определенные протоколом SKSceneDelegate , называются следующим образом:

  • update(_:forScene:)
  • didEvaluateActionsForScene(_:)
  • didSimulatePhysicsForScene(_:)
  • didApplyConstraintsForScene(_:)
  • didFinishUpdateForScene(_:)

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

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

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

Чтобы сделать рендеринг более эффективным, вы можете организовать узлы в вашей сцене в отдельные слои. Это делается с помощью свойства zPosition класса SKNode . Чем выше zPosition узла, тем «ближе» он к экрану, что означает, что он отображается поверх других узлов вашей сцены. Аналогично, узел с наименьшей zPosition в сцене появляется в самом «конце» и может перекрываться любым другим узлом.

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

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

SKAction и SKConstraint содержат большое количество правил, которые можно добавить в сцену для создания анимации. Будучи частью инфраструктуры SpriteKit, они оптимизируются настолько, насколько это возможно, и также идеально вписываются в цикл анимации SpriteKit.

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

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

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

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

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

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

В этой серии мы рассмотрели множество функций и возможностей платформы SpriteKit в iOS, tvOS и OS X. Есть и более сложные темы, выходящие за рамки этой серии, такие как пользовательские шейдеры OpenGL ES и Metal. как физика полей и суставов.

Если вы хотите узнать больше об этих темах, я рекомендую начать со Справочника по SpriteKit Framework и прочитать соответствующие классы.

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