Мы продолжаем учебный курс по созданию игры для iPhone на Sprite Kit и Swift. В первой части у нас получилось создать игровую сцену на которой находится наш герой, и даже заставить его подчиняться законам гравитации. В этом уроке мы научим гороя взаимодействовать с внешним миром, ловить “звезды” и подпрыгивать на платформах…
“Нажмите, чтобы начать”
Оригинал статьи на английском языке можно прочитать здесь.
Теперь, когда гравитация в игре работает, нужно сделать физическое тело героя неподвижным до тех пор, пока пользователь не решит начать игру. Внутри файла GameScene.swift найдите строку в методе createPlayer, которая устанавливает свойство dynamic физическому телу объекта playerNode. Измените его значение на false, так как показано здесь:
playerNode.physicsBody?.dynamic = false |
Мы собираемся написать код, который позволит пользователю начать игру нажав на экран. И об этом его нужно предупредить какой-нибудь надписью. Например: “Tap to start” (Нажмите, чтобы начать). Давайте разместим эту инструкцию на экране.
Поскольку инструкции будут располагаться в слое HUD, создайте его первым. В методе init(size:) сразу после места, где вы добавили узел переднего плана на сцену, вставьте следующий код:
// HUD hudNode = SKNode() addChild(hudNode) |
Графические ресурсы включают в себя изображение, на которое нужно нажать для начала игры. По этому добавьте следующую переменную к свойствам в верхней часть GameScene.swift:
// Tap To Start node let tapToStartNode = SKSpriteNode(imageNamed: "TapToStart") |
Чтобы отобразить созданный узел, добавьте следующий код в метод init(size:) сразу после строки, которая добавляет узел героя:
// Tap to Start tapToStartNode.position = CGPoint(x: self.size.width / 2, y: 180.0) hudNode.addChild(tapToStartNode) |
Постройте и запустите, чтобы увидеть надпись “Tap to start” чуть выше спрайта героя:
Для обработки касаний и для начала игры, добавьте следующий метод в класс GameScene.swift:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) { // 1 // If we're already playing, ignore touches if player.physicsBody!.dynamic { return } // 2 // Remove the Tap to Start node tapToStartNode.removeFromParent() // 3 // Start the player by putting them into the physics simulation player.physicsBody?.dynamic = true // 4 player.physicsBody?.applyImpulse(CGVector(dx: 0.0, dy: 20.0)) } |
Давайте рассмотрим этот метод детально:
1. Мы должны убедится в том, что узел героя стал динамическим. Если это так, то просто игнорируем событие касания.
2. Удаляем узел с надписью “Tap to start”.
3. Делаем физическое тело узла героя динамическим для того, чтобы физический движок мог влиять на него.
4. Сообщаем узлу героя начальный импульс, направленный вверх, чтобы он начал движение.
Постройте и запустите. При нажатии на экран, надпись «Tab to start» исчезнет, а спрайт героя подбросится вверх, но ненадолго – сила тяжести берет своё.
Игровые объекты: дотянуться до звезд!
Чтобы вашему герою было чем заняться, кроме как просто прыгать вверх, самое время добавить “звезды”. Звезды играют важную роль в Uber Jump: именно их игрок должен собирать для перехода на следующий уровень.
Для начала вам достаточно будет создать в игре одну звезду и заставить её полностью функционировать, а в следующей части урока мы завершим уже весь уровень.
По сценарию игры Uber Jump, как и Mega Jump, когда спрайт героя поднимается на определенное расстояние выше звезды, платформы или любого другого объекта, этот объект удаляется со сцены. Так как и узлы и звезды и платформы попадают под это условие, имеет смысл создать подкласс SKNode для всех игровых объектов.
Создайте новый класс Cocoa Touch Class с названием GameObjectNode и сделайте его подклассом SKNode. Убедитесь, что в качестве языка установлен Swift.
Класс GameObjectNode будет включать в себя следующий функционал:
1. Удаление из сцены того объекта, от которого узел героя отдалился на расстояние, больше чем заданное.
2. Обнаружение столкновений между узлом героя и объектом. Этот метод вернет булевое значение, которое будет уведомлять, произошло ли пересечение героя с каким-либо объектом игры, которое приведет к необходимости обновления слоя HUD. Например, если игрок набрал очки.
Замените код в GameObjectNode.swift на следующий:
import SpriteKit class GameObjectNode: SKNode { func collisionWithPlayer(player: SKNode) -> Bool { return false } func checkNodeRemoval(playerY: CGFloat) { if playerY > self.position.y + 300.0 { self.removeFromParent() } } } |
Мы вызываем метод collisionWithPlayer всякий раз, когда узел героя сталкивается с объектом, а также вызываем checkNodeRemoval в каждом кадре, чтоб была возможность сразу удалить ненужный узел со сцены.
В классе GameObjectNode метод collisionWithPlayer представляет из себя просто заглушку. Мы зададим полный метод отдельно в каждом из подклассов, которые будут созданы для ваших игровых объектов.
Метод checkNodeRemoval проверяет, переместился ли узел героя на расстояние более чем 300 точек от этого узла. Если это так, то метод удаляет узел из его родительского узла и, таким образом, убирает его со сцены.
Класс звезды
Теперь, когда у нас есть базовый класс для интерактивных узлов игры, мы можем создать класс для звезд. Чтобы не усложнять, добавим все подклассы GameObjectNode сразу в файл GameObjectNode.swift. Добавьте следующий код после класса GameObjectNode.
class StarNode: GameObjectNode { override func collisionWithPlayer(player: SKNode) -> Bool { // Boost the player up player.physicsBody?.velocity = CGVector(dx: player.physicsBody!.velocity.dx, dy: 400.0) // Remove this Star self.removeFromParent() // The HUD needs updating to show the new stars and score return true } } |
Столкновение со звездой подбрасывает узел героя вверх по оси ординат. Вы можете спросить: “почему для этого мы не используем силу или импульс физического движка?”
Если бы мы задействовали силу или импульс, нужный эффект был бы не всегда. Например, если узел героя двигается вниз экрана и врезается в звезду, то сила воздействия на героя будет гораздо меньше, чем когда он двигается вверх.
Следующая диаграмма очень наглядно это показывает:
Решение данной проблемы заключается в непосредственном изменении скорости узла игрока. Как известно, вектор скорости складывается из горизонтальной и вертикальной составляющих.
Скорость по оси X не должна изменяться, так как на неё влияет только акселерометр, который мы реализуем позже. В вышеуказанном способе при столкновении мы устанавливаем фиксированное значение скорости вертикальной проекции – 400. Чтобы столкновение имело одинаковые последствия независимо от того, что делал игрок перед тем как столкнуться со звездой.
Откройте GameScene.swift и добавьте следующий метод:
func createStarAtPosition(position: CGPoint) -> StarNode { // 1 let node = StarNode() let thePosition = CGPoint(x: position.x * scaleFactor, y: position.y) node.position = thePosition node.name = "NODE_STAR" // 2 var sprite: SKSpriteNode sprite = SKSpriteNode(imageNamed: "Star") node.addChild(sprite) // 3 node.physicsBody = SKPhysicsBody(circleOfRadius: sprite.size.width / 2) // 4 node.physicsBody?.dynamic = false return node } |
Код, представленный выше, должен быть вам уже знаком:
1. Мы создаем экземпляр класса StarNode и устанавливаем его позицию.
2. Затем добавляем графику звезды, используя SKSpriteNode.
3. Добавляем к узлу физическое тело в форме круга, которое нам понадобится для обнаружения столкновений с другими объектами игры.
4. Наконец, мы делаем физическое тело статическим, потому что не хотим, чтобы гравитация или любые другие физические симуляции влияли на звезды.
Теперь добавьте следующий код в метод init(size:), сразу перед тем местом, где мы создали узел героя. Мы хотим, чтобы звезды размещались позади игрока, но тоже находились на узле переднего плана, поэтому мы должны добавить их раньше, чем узел игрока.
// Add a star let star = createStarAtPosition(CGPoint(x: 160, y: 220)) foregroundNode.addChild(star) |
Постройте и запустите игру. Нажмите на кнопку начала игры и посмотрите, как спрайт героя будет сталкиваться со звездой.
Наш Ульта прыгун ударился головой об звезду. Это было не по плану! Как вы думаете, почему так произошло? Попробуйте догадаться.
А вот решение:
Физический движок обрабатывает столкновения между героем и узлами звезд. Физическое тело узла героя пересекает физическое тело узла звезды, которое является статическим и, следовательно, неподвижным. Узел звезды останавливает движение узла игрока.
Обнаружение столкновений и битовые маски
Для обнаружения столкновений между героем и узлами звезд, нам нужно получить событие столкновения и вызвать метод collisionWithPlayer класса GameObjectNode.
Самое время рассмотреть тему пересечения битовых масок в Sprite Kit.
Для сообщения физическим телам информации о столкновениях существуют три свойства, связанные с битовыми масками, которые вы можете использовать, чтобы определить способ взаимодействия физического тела с другими физическими телами игры.
1. categoryBitMask определяет категорию столкновения, к которой относится физическое тело.
2. collisionBitMask устанавливает категорию столкновения тел, с которыми данное тело будет пересекаться. Здесь слово “пересекаться” означает, что объекты будут именно сталкиваться друг с другом. Например, играя в качестве стрелка от третьего лица, вам будет нужно, чтобы спрайт героя сталкивался с вражескими спрайтами, но проходил сквозь спрайты других игроков.
3. contactTestBitMask сообщает движку Sprite Kit, чтобы он уведомлял вас, когда данное физическое тело вступает в контакт с физическими телами, принадлежащими к одной из указанных вами категорий. Например, в нашей игре мы хотим, чтобы Sprite Kit сообщал нам, когда спрайт игрока касается звезды или платформы. Используя правильные сочетания настроек для contactTestBitMask и collisionBitMask, можно запрограммировать Sprite Kit так, чтобы объекты проходили друг сквозь друга, а он уведомлял вас, когда это происходит. Таким образом мы можем инициировать события.
Первое, что нужно сделать, это задать свои категории. Откройте GameObjectNode.swift и добавьте следующую структуру над определениями классов:
struct CollisionCategoryBitmask { static let Player: UInt32 = 0x00 static let Star: UInt32 = 0x01 static let Platform: UInt32 = 0x02 } |
Вернитесь в GameScene.swift. Чтобы задать поведение героя во время столкновений, добавьте следующий код в нижнюю часть метода createPlayer, сразу перед оператором return:
// 1 playerNode.physicsBody?.usesPreciseCollisionDetection = true // 2 playerNode.physicsBody?.categoryBitMask = CollisionCategoryBitmask.Player // 3 playerNode.physicsBody?.collisionBitMask = 0 // 4 playerNode.physicsBody?.contactTestBitMask = CollisionCategoryBitmask.Star | CollisionCategoryBitmask.Platform |
Давайте повнимательней рассмотрим этот блока кода:
1. Так как эта игра на быстрое движение, Sprite Kit должен использовать точное обнаружение столкновений для физического тела узла героя. Ведь весь сюжет Uber Jump строится на этих столкновениях. Поэтому нам нужно, чтобы они фиксировались настолько точно, насколько возможно! (будет задействовано несколько циклов процессора)
2. Мы назначаем категорию битовой маски физического тела. Она относится к категории CollisionCategoryPlayer.
3. Приравняв collisionBitMask к нулю, мы сообщаем Sprite Kit, что не хотим, чтобы он имитировал какие-либо столкновения узла игрока. Это потому, что мы собираемся обрабатывать эти столкновения сами!
4. Мы сообщаем Sprite Kit, что хотим получать уведомления, когда узел героя будет касаться каких-либо звезд или платформ.
Теперь настроим узел звезды. Добавьте следующий код в нижней части метода createStarAtPosition перед оператором return:
node.physicsBody?.categoryBitMask = CollisionCategoryBitmask.Star node.physicsBody?.collisionBitMask = 0 |
Здесь всё аналогично настройке узла героя. Мы назначаем категорию звезды и обнуляем свойство collisionBitMask, поэтому она не будет ни с чем сталкиваться. Однако здесь мы не назначаем contactTestBitMask, это значит, что Sprite Kit не будет уведомлять нас, когда какой-либо объект будет касаться звезды. Мы ведь уже поручили Sprite Kit отправлять уведомления при соприкосновении героя со звездой, а кроме звезды он ни с чем соприкасаться не будет, поэтому нет никакой необходимости отправлять уведомления со стороны звезды.
Sprite Kit посылает уведомления о контактах выбранных нами узлов, вызывая метод didBeginContact делегата SKPhysicsContactDelegate. В качестве делегата физического мира установите саму сцену, добавив протокол SKPhysicsContactDelegate к определению класса GameScene. Это должно выглядеть примерно так:
class GameScene: SKScene, SKPhysicsContactDelegate { // Layered Nodes ... |
Теперь назначьте сцену делегатом для получения контактных уведомлений, добавив следующую строку в init(size:), сразу после строки, которая устанавливает гравитацию:
// Set contact delegate physicsWorld.contactDelegate = self |
И, наконец, добавьте следующий метод в GameScene.swift для обработки событий столкновений:
func didBeginContact(contact: SKPhysicsContact) { // 1 var updateHUD = false // 2 let whichNode = (contact.bodyA.node != player) ? contact.bodyA.node : contact.bodyB.node let other = whichNode as GameObjectNode // 3 updateHUD = other.collisionWithPlayer(player) // Update the HUD if necessary if updateHUD { // 4 TODO: Update HUD in Part 2 } } |
Давайте рассмотрим этот код поподробнее:
1. Мы инициализируем флаг updateHUD, который будем использовать в конце метода, чтобы обновлять HUD во время этих столкновений и подсчитывать очки.
2. SKPhysicsContact не гарантирует, какое физическое тело выступит в роли bodyA, а какое в роли bodyB. Но мы знаем, что все столкновения в этой игре будут между узлом героя и узлом GameObjectNode. Поэтому эта строка определяет, какой из них не является узлом героя.
3. После того, как мы определили, какой объект не является узлом героя, мы вызываем метод collisionWithPlayer: в классе GameObjectNode.
4. Здесь мы обновляем HUD, если это требуется. Реализацию HUD мы будем делать во второй части урока, поэтому здесь кроме комментария пока ничего нет.
Постройте и запустите игру. Нажмите, чтобы начать. При столкновении звезда подбрасывает спрайт героя на определенную высоту, а затем удаляется со сцены. Хорошая работа!
Ну что, было проделано много тяжелой работы! Возьмем заслуженный перерыв. В следующем разделе нам предстоит добавить новый типы звезды uber star, а также звуковой эффект.
Примечание: Если вы хотите узнать больше об определении контактов и столкновений в Sprite Kit, то изучите руководство “iOS Games by Tutorials“, в котором содержится три больших главы по физике движка.
Несколько видов звезд
Uber Jump будет содержать два вида звезд: те, которые добавляют одно очко и специальные звезды, добавляющие сразу пять очков. Звезда каждого типа будет иметь свою графику. Идентифицировать тип звезды нам поможет коллекция в классе StarNode.
В верхнюю часть GameObjectNode.swift добавьте следующую коллекцию:
enum StarType: Int { case Normal = 0 case Special } |
Для хранения типа звезды, добавьте следующее свойство в верхнюю часть класса StarNode:
var starType: StarType! |
Теперь при создании звезды нам нужно будет указывать её тип, поэтому в GameScene.swift добавьте параметр starType к сигнатуре метода createStarAtPosition:, чтобы это выглядело так:
func createStarAtPosition(position: CGPoint, ofType type: StarType) -> StarNode { |
Внутри createStarAtPosition(position: ofType:), замените три строчки кода (которые создают и добавляют SKSpriteNode) на следующие:
node.starType = type var sprite: SKSpriteNode if type == .Special { sprite = SKSpriteNode(imageNamed: "StarSpecial") } else { sprite = SKSpriteNode(imageNamed: "Star") } node.addChild(sprite) |
Сначала мы установили тип звезды. Затем, проверяем тип при создании спрайта, чтобы добавить соответствующую картинку.
Осталось только указать тип звезды при её создании. В методе init(size:) класса GameScene.swift найдите строку, в которой вызываем createStarAtPosition и измените его следующим образом:
let star = createStarAtPosition(CGPoint(x: 160, y: 220), ofType: .Special) |
Назначаем нашей звезде тип StarType.Special.
Постройте запустите. Наша звезда стала розовой! Позже мы добавим систему подсчета очков и различия в типах звезд станут более понятными.
Звон! Добавление звукового эффекта
Было бы неплохо, если бы столкновение героя со звездой сопровождалось звуковым сигналом. Скачайте звуковой эффект для звезды отсюда и перетащите файл в наш проект Xcode. Убедитесь, что указана опция “Destination: Copy items if needed” и что выбран таргет UberJump.
Для воспроизведения звуков в Sprite Kit используется SKAction. Откройте GameObjectNode.swift и добавьте следующее свойство класса выше StarNode:
let starSound = SKAction.playSoundFileNamed("StarPing.wav", waitForCompletion: false) |
Теперь осталось только воспроизвести звуковой эффект во время столкновения героя со звездой.
В методе collisionWithPlayer() класса StarNode замените self.removeFromParent() на:
// Play sound runAction(starSound, completion: { // Remove this Star self.removeFromParent() }) |
Код запускает SKAction, воспроизводит звуковой файл и удаляет звезду, когда действие закончено.
Постройте и запустите. Как только спрайт героя столкнётся со звездой, вы услышите звенящий звук.
Игровые объекты: платформы
Наш следующая задача – добавление платформы. Мы создадим для платформ новый подкласс класса GameObjectNode. Как и в случае со звездами, нам понадобятся два типа платформ: платформы одного типа должны быть неразрушимыми, а другие должны исчезать в тот момент, когда герой спрыгивает с них.
В GameObjectNode.swift добавьте следующую коллекцию над определением класса GameObjectNode, чтобы задать два типа платформ:
enum PlatformType: Int { case Normal = 0 case Break } |
Далее нам нужно создать класс PlatformNode. Добавьте следующий код в GameObjectNode.swift:
class PlatformNode: GameObjectNode { var platformType: PlatformType! override func collisionWithPlayer(player: SKNode) -> Bool { // 1 // Only bounce the player if he's falling if player.physicsBody?.velocity.dy < 0 { // 2 player.physicsBody?.velocity = CGVector(dx: player.physicsBody!.velocity.dx, dy: 250.0) // 3 // Remove if it is a Break type platform if platformType == .Break { self.removeFromParent() } } // 4 // No stars for platforms return false } } |
Давайте подробней остановим своё внимание на этом коде:
1. В соответствии со сценарием игры, узел героя должен отскакивать от платформы только при падении, т.е тогда, когда он имеет отрицательное значение dy своей скорости (свойство velocity). Эта проверка также позволяет узлу героя не пересекаться с платформами, двигаясь вверх по экрану.
2. Мы сообщаем узлу героя скорость, направленную вверх, чтобы он отскочил от платформы. Это делается также, как раньше мы это сделали для звезд, но с меньшим значением. Звезды ведь круче, не так ли?
3. Если платформа относится к типу platformType.Break, то мы удаляем её со сцены.
4. Ультра прыгун не получает очки ни когда спрыгивает с платформы, ни когда запрыгивает на нее, поэтому нет никакой необходимости обновлять HUD.
Для добавления платформы для сцену откройте GameScene.swift и впишите следующий метод:
func createPlatformAtPosition(position: CGPoint, ofType type: PlatformType) -> PlatformNode { // 1 let node = PlatformNode() let thePosition = CGPoint(x: position.x * scaleFactor, y: position.y) node.position = thePosition node.name = "NODE_PLATFORM" node.platformType = type // 2 var sprite: SKSpriteNode if type == .Break { sprite = SKSpriteNode(imageNamed: "PlatformBreak") } else { sprite = SKSpriteNode(imageNamed: "Platform") } node.addChild(sprite) // 3 node.physicsBody = SKPhysicsBody(rectangleOfSize: sprite.size) node.physicsBody?.dynamic = false node.physicsBody?.categoryBitMask = CollisionCategoryBitmask.Platform node.physicsBody?.collisionBitMask = 0 return node } |
Этот метод очень похож на createStarAtPosition (_: OfType : ), но обратите внимание на следующие основные моменты:
1. Мы создаем экземпляр PlatformNode и задаем его позицию, имя и тип.
2. Выбираем соответствующее изображение для SKSpriteNode в зависимости от типа платформы.
3. Создаем физику платформы, в том числе тип столкновений.
Теперь, чтобы добавить платформу, вставьте следующий код в метод init(size:), сразу перед строчкой, которая создает звезду:
// Add a platform let platform = createPlatformAtPosition(CGPoint(x: 160, y: 320), ofType: .Normal) foregroundNode.addChild(platform) |
Постройте и запустите игру. Нажмите, чтобы начать и посмотрите, как спрайт героя зарядится энергией от звезды, а затем подпрыгнет на платформе!
Куда идти дальше?
Отлично! Как вы уже поняли, создание игры подобной Mega Jump – это не так уж и сложно. С помощью Uber Jump мы изучили все базовые основы, которые сможем использовать далее, на пути к созданию отличной игры.
В конце статьи вы можете скачать проект Xcode содержащий всё, что мы изучали в этом курсе.
В следующей части мы задействуем акселерометр для управления перемещением героя по оси Х. А также будем загружать ряд свойств из внешнего файла plist и добавим систему подсчёта очков. Много всего интересного ещё предстоит.
Если у вас есть какие-либо вопросы, мысли или предложения для будущих уроков, пожалуйста, оставьте ниже свой комментарий.
Продолжение через неделю….
Скачать Исходник Xcode.
Скачать графические ресурсы для урока.
|
|