Swift 游戏开发(二)

原文:zh.annas-archive.org/md5/d0174909c3206c7de05881da7a3ac0c9

译者:飞龙

协议:CC BY-NC-SA 4.0

第七章. 实现碰撞事件

到目前为止,我们让 SpriteKit 物理模拟检测和处理游戏对象之间的碰撞。你已经看到,当皮埃尔企鹅飞入敌人或金币时,它会将它们发送到太空。这是因为物理模拟自动监控碰撞并设置每个碰撞物体的碰撞后轨迹和速度。在本章中,当两个物体接触时,我们将添加自己的游戏逻辑:从敌人那里受到伤害、在接触星星后给予玩家无敌状态,以及玩家收集金币时跟踪分数。随着游戏机制变得生动,游戏将变得更加有趣。

本章包括以下主题:

  • 学习 SpriteKit 碰撞词汇

  • 将接触事件添加到我们的游戏中

  • 玩家健康和伤害

  • 收集金币

  • 提升星级逻辑

学习 SpriteKit 碰撞词汇

SpriteKit 使用一些独特概念和术语来描述物理事件。如果你现在熟悉这些术语,那么在章节后面的实现步骤中理解起来会更容易。

碰撞与接触

当物理体在相同空间中聚集时,有两种类型的交互:

  • 碰撞是物理模拟在物体接触后对物体进行数学分析和重新定位。碰撞包括物体之间所有的自动物理交互:防止重叠、弹开、空中旋转和传递动量。默认情况下,物理体与场景中的其他所有物理体发生碰撞;到目前为止,我们在游戏中已经见证了这种自动碰撞行为。

  • 当两个物体接触时,也会发生接触事件。接触事件允许我们在两个物体接触时,将自定义游戏逻辑连接进去。接触事件本身不会产生任何变化;它们只为我们提供了执行自己代码的机会。例如,当玩家或玩家遇到敌人时,我们将使用接触事件来分配伤害。默认情况下没有接触事件;在本章中,我们将手动配置接触。

小贴士

物理体默认情况下与场景中的其他所有物体发生碰撞,但你可以配置特定的物体以忽略碰撞并相互穿过而不产生任何物理反应。

此外,碰撞和接触是独立的;你可以禁用两种类型物体之间的物理碰撞,并在物体穿过彼此时仍然使用接触事件来执行自定义代码。

物理类别掩码

你可以为游戏中的每个物理体分配物理类别。这些类别允许你指定应该发生碰撞的物体、应该接触的物体以及应该无事件地相互穿过的物体。当两个物体尝试共享同一空间时,物理模拟将比较每个物体的类别并测试是否应该触发碰撞或接触事件。

注意

我们的游戏将包括企鹅、地面、金币和敌人的物理类别。

物理类别以 32 位掩码存储,这使得物理模拟可以通过处理器高效的位操作执行这些测试。虽然理解位操作不是使用物理类别所必需的,但如果您有兴趣扩展您的知识,这是一个很好的阅读主题。如果您感兴趣,可以尝试在互联网上搜索 swift bitwise operations

每个物理物体都有三个属性,您可以使用这些属性来控制游戏中的碰撞。让我们先简单总结每个属性,然后再深入探讨:

  • categoryBitMask:物理物体的物理类别

  • collisionBitMask:与这些物理类别发生碰撞

  • contactTestBitMask:与这些物理类别接触

categoryBitMask 属性存储了物体当前的物理类别。默认值是 0xFFFFFFFF,相当于所有类别。这意味着默认情况下,每个物理物体都属于所有物理类别。

collisionBitMask 属性指定了物体应该与之碰撞的物理类别,防止两个物体共享相同的空间。起始值是 0xFFFFFFFF,或所有位都设置,意味着默认情况下,物体将与每个类别发生碰撞。当一个物体开始与另一个物体重叠时,物理模拟将比较每个物体的 collisionBitMask 与另一个物体的 categoryBitMask。如果匹配,则发生碰撞。请注意,这个测试是双向的;每个物体可以独立参与或忽略碰撞。

contactTestBitMask 属性与碰撞属性的工作方式相同,但指定了接触事件而不是碰撞的类别。默认值是 0x00000000,或没有设置位,意味着默认情况下,物体不会与任何物体接触。

这是一个复杂的话题。如果你还没有完全理解这个主题,可以继续前进。将类别掩码实现到我们的游戏中将帮助你学习。

在 Swift 中使用类别掩码

苹果的冒险游戏演示提供了在 Swift 中使用位掩码的良好实现。我们将遵循他们的例子,并使用 enum 来存储我们的类别作为 UInt32 值,并以易于阅读的方式编写这些位掩码。以下是一个理论战争游戏的物理类别 enum 的示例:

enum PhysicsCategory:UInt32 {
    case playerTank = 1
    case enemyTanks = 2
    case missiles = 4
    case bullets = 8
    case buildings = 16
}

对于每个后续组,双倍其值非常重要;这是创建适当的位掩码以进行物理模拟的必要步骤。例如,如果我们添加 fighterJets,则值需要是 32。始终记得双倍后续值以创建独特的位掩码,以便在物理测试中按预期执行。

小贴士

位掩码是 CPU 可以非常快速比较的二进制值,以检查是否匹配。您不需要理解位运算符来完成此材料,但如果您已经熟悉并且好奇,这种加倍方法之所以有效,是因为 2 等同于 1 << 1(二进制:10),4 等同于 1 << 2(二进制:100),8 等同于 1 << 3(二进制:1000),依此类推。我们选择手动加倍,因为 enum 值必须是字面量,这些值对人类来说更容易阅读。

为我们的游戏添加接触事件

现在您已经熟悉了 SpriteKit 的物理概念,我们可以进入 Xcode 为我们的企鹅游戏实现物理类别和接触逻辑。我们将首先添加我们的物理类别。

设置物理类别

要创建我们的物理类别,请打开您的 GameScene.swift 文件,并在 GameScene 类外部底部输入以下代码:

enum PhysicsCategory:UInt32 {
    case penguin = 1
    case damagedPenguin = 2
    case ground = 4
    case enemy = 8
    case coin = 16
    case powerup = 32
}

注意我们是如何像之前的例子那样将每个后续值翻倍的。我们还为我们的企鹅在受到伤害后使用创建了一个额外的类别。我们将使用 damagedPenguin 物理类别,以便企鹅在受到伤害后几秒钟内能够穿过敌人。

将类别分配给游戏对象

现在我们有了物理类别,我们需要回到现有的游戏对象并将类别分配给物理体。我们将从 Player 类开始。

玩家

打开 Player.swift 并在 spawn 函数底部添加以下代码:

self.physicsBody?.categoryBitMask =
    PhysicsCategory.penguin.rawValue
self.physicsBody?.contactTestBitMask =
    PhysicsCategory.enemy.rawValue |
    PhysicsCategory.ground.rawValue |
    PhysicsCategory.powerup.rawValue |
    PhysicsCategory.coin.rawValue

我们将企鹅物理类别分配给 Player 物理体,并使用 contactTestBitMask 属性设置与敌人、地面、提升和金币的接触测试。

此外,请注意我们如何使用 enum 值的 rawValue 属性。当您使用物理类别位掩码时,您将需要使用 rawValue 属性。

地面

接下来,让我们为 Ground 类分配物理类别。打开 Ground.swift,并在 spawn 函数底部添加以下代码:

self.physicsBody?.categoryBitMask =
    PhysicsCategory.ground.rawValue

我们需要做的只是将地面位掩码分配给 Ground 类的物理体,因为它默认情况下已经与所有物体发生碰撞。

星星提升

打开 Star.swift 并在 spawn 函数底部添加以下代码:

self.physicsBody?.categoryBitMask =
    PhysicsCategory.powerup.rawValue

这将功率提升物理类别分配给 Star 类。

敌人

Bat.swiftBee.swiftBlade.swiftGhost.swiftMadFly.swift 中执行此相同操作。在它们的 spawn 函数内部添加以下代码:

self.physicsBody?.categoryBitMask = PhysicsCategory.enemy.rawValue
self.physicsBody?.collisionBitMask =
    ~PhysicsCategory.damagedPenguin.rawValue

我们使用位运算的 NOT 操作符 (~) 从与敌人的碰撞中移除 damagedPenguin 物理类别。敌人将与所有类别发生碰撞,除了 damagedPenguin 物理类别。这允许我们在想要企鹅忽略敌人碰撞并直接穿过时,将企鹅的类别更改为 damagedPenguin 值。

金币

最后,我们将添加金币的物理类别。我们不希望金币与其他游戏对象发生碰撞,但我们仍然想要监控接触事件。打开 Coin.swift 文件,在 spawn 函数的底部添加以下代码:

self.physicsBody?.categoryBitMask = PhysicsCategory.coin.rawValue
self.physicsBody?.collisionBitMask = 0

准备 GameScene 以处理接触事件

现在我们已经为游戏对象分配了物理类别,我们可以在 GameScene 类中监控接触事件。按照以下步骤连接 GameScene 类:

  1. 首先,我们需要告诉 GameScene 类实现 SKPhysicsContactDelegate 协议。SpriteKit 就可以在接触事件发生时通知 GameScene 类。将 GameScene 类声明行修改为如下所示:

    class GameScene: SKScene, SKPhysicsContactDelegate {
    
  2. 我们将通过将 GameScene physicsWorld contactDelegate 属性设置为 GameScene 类来告诉 SpriteKit 通知 GameScene 类接触事件。在 GameScene didMoveToView 函数的底部添加以下行:

    self.physicsWorld.contactDelegate = self
    
  3. SKPhysicsContactDelegate 定义了一个 didBeginContact 函数,当发生接触时将会触发。我们现在可以在 GameScene 类中实现这个 didBeginContact 函数。在 GameScene 类中创建一个新的函数,命名为 didBeginContact,如下面的代码所示:

    func didBeginContact(contact: SKPhysicsContact) {
        // Each contact has two bodies; we do not know which is which.
        // We will find the penguin body, then use
        // the other body to determine the type of contact.
        let otherBody:SKPhysicsBody
        // Combine the two penguin physics categories into one
        // bitmask using the bitwise OR operator |
        let penguinMask = PhysicsCategory.penguin.rawValue |
            PhysicsCategory.damagedPenguin.rawValue
        // Use the bitwise AND operator & to find the penguin.
        // This returns a positive number if body A's category
        // is the same as either the penguin or damagedPenguin:
        if (contact.bodyA.categoryBitMask & penguinMask) > 0 {
            // bodyA is the penguin, we will test bodyB: 
            otherBody = contact.bodyB
        }
        else {
            // bodyB is the penguin, we will test bodyA:
            otherBody = contact.bodyA
        }
        // Find the type of contact:
        switch otherBody.categoryBitMask {
        case PhysicsCategory.ground.rawValue:
            println("hit the ground")
        case PhysicsCategory.enemy.rawValue:
            println("take damage")
        case PhysicsCategory.coin.rawValue:
            println("collect a coin")
        case PhysicsCategory.powerup.rawValue:
            println("start the power-up")
        default:
            println("Contact with no game logic")
        }
    }
    

这个函数将作为我们接触事件的中心枢纽。当我们的各种接触事件发生时,我们将向控制台打印信息,以测试我们的代码是否正常工作。

查看控制台输出

您可以使用 println 函数将信息写入控制台,这对于调试非常有用。如果您尚未在 Xcode 中使用控制台,请按照以下简单步骤查看它:

  1. 在 Xcode 的右上角,确保调试区域已开启,如图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_07_01.jpg

  2. 在 Xcode 的右下角,确保控制台已开启,如图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_07_02.jpg

测试我们的接触代码

现在您可以看到控制台输出了,运行项目。当您将皮埃尔飞入各种游戏对象时,应该会在控制台中看到我们的 println 字符串。您的控制台应该看起来像这样:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_07_03.jpg

恭喜——如果您在控制台中看到了接触输出,您已经完成了我们接触系统的结构。

您可能会注意到飞入金币会产生奇怪的碰撞行为,我们将在本章后面增强这一点。接下来,我们将为每种接触类型添加游戏逻辑。

检查点 7-A

要下载到这一点的项目,请访问此 URL:

www.thinkingswiftly.com/game-development-with-swift/chapter-7

玩家生命值和伤害

首个自定义接触逻辑是玩家伤害。我们将为玩家分配健康点数,并在受伤时扣除。当玩家耗尽健康点数时,游戏结束。这是我们游戏玩法的基础机制之一。按照以下步骤实现健康逻辑:

  1. Player.swift 文件中,向 Player 类添加六个新属性:

    // The player will be able to take 3 hits before game over:
    var health:Int = 3
    // Keep track of when the player is invulnerable:
    var invulnerable = false
    // Keep track of when the player is newly damaged:
    var damaged = false
    // We will create animations to run when the player takes
    // damage or dies. Add these properties to store them:
    var damageAnimation = SKAction()
    var dieAnimation = SKAction()
    // We want to stop forward velocity if the player dies,
    // so we will now store forward velocity as a property:
    var forwardVelocity:CGFloat = 200
    
  2. update 函数中,更改移动玩家通过世界的代码以使用新的 forwardVelocity 属性:

    // Set a constant velocity to the right:
    self.physicsBody?.velocity.dx = self.forwardVelocity
    
    
  3. startFlapping 函数的非常开始处添加此行,以防止玩家在死亡时飞得更高:

    if self.health <= 0 { return }
    
  4. stopFlapping 函数的非常开始处添加相同的行,以防止在死亡后运行飞翔动画:

    if self.health <= 0 { return }
    
  5. Player 类添加一个名为 die 的新函数:

    func die() {
        // Make sure the player is fully visible:
        self.alpha = 1
        // Remove all animations:
        self.removeAllActions()
        // Run the die animation:
        self.runAction(self.dieAnimation)
        // Prevent any further upward movement:
        self.flapping = false
        // Stop forward movement:
        self.forwardVelocity = 0
    }
    
  6. Player 类添加一个名为 takeDamage 的新函数:

    func takeDamage() {
        // If invulnerable or damaged, return:
        if self.invulnerable || self.damaged { return }
    
        // Remove one from our health pool
        self.health--
        if self.health == 0 {
            // If we are out of health, run the die function:
            die()
        }
        else {
            // Run the take damage animation:
            self.runAction(self.damageAnimation)
        }
    }
    
  7. 打开 GameScene.swift 文件。在 didBeginContact 函数中,更新与敌人接触时触发的 switch 案例:

    case PhysicsCategory.enemy.rawValue:
        println("take damage")
     player.takeDamage()
    
    
  8. 当我们撞击地面时,我们也会受到伤害。以相同的方式更新地面情况:

    case PhysicsCategory.ground.rawValue:
        println("hit the ground")
     player.takeDamage()
    
    

干得好——让我们测试我们的代码以确保一切正常工作。运行项目并撞击一些敌人。你可以在控制台输出的打印内容中检查一切是否正常工作。受到三次伤害后,企鹅应该掉到地上并变得无反应。

注意

你可能会注意到,玩家在玩游戏时无法知道他们剩余多少健康点数。我们将在下一章中向场景添加一个健康计。

接下来,当玩家受到伤害和游戏结束时,我们将通过新的动画增强游戏的感受。

受伤和游戏结束动画

当玩家受到敌人打击时,我们将使用 SKAction 序列创建有趣的动画。通过组合动作,我们将在玩家击中敌人后提供一个受伤状态下的临时安全。我们将展示一个逐渐脉冲然后随着安全状态开始减弱而加速的淡入淡出动画。

受伤动画

要添加新动画,请将此代码添加到 Player 类的 createAnimations 函数底部:

// --- Create the taking damage animation ---
let damageStart = SKAction.runBlock {
    // Allow the penguin to pass through enemies:
    self.physicsBody?.categoryBitMask =
        PhysicsCategory.damagedPenguin.rawValue
    // Use the bitwise NOT operator ~ to remove
    // enemies from the collision test:
    self.physicsBody?.collisionBitMask =
        ~PhysicsCategory.enemy.rawValue
}
// Create an opacity pulse, slow at first and fast at the end:
let slowFade = SKAction.sequence([
    SKAction.fadeAlphaTo(0.3, duration: 0.35),
    SKAction.fadeAlphaTo(0.7, duration: 0.35)
    ])
let fastFade = SKAction.sequence([
    SKAction.fadeAlphaTo(0.3, duration: 0.2),
    SKAction.fadeAlphaTo(0.7, duration: 0.2)
    ])
let fadeOutAndIn = SKAction.sequence([
    SKAction.repeatAction(slowFade, count: 2),
    SKAction.repeatAction(fastFade, count: 5),
    SKAction.fadeAlphaTo(1, duration: 0.15)
    ])
// Return the penguin to normal:
let damageEnd = SKAction.runBlock {
    self.physicsBody?.categoryBitMask =
        PhysicsCategory.penguin.rawValue
    // Collide with everything again:
    self.physicsBody?.collisionBitMask = 0xFFFFFFFF
    // Turn off the newly damaged flag:
    self.damaged = false
}
// Store the whole sequence in the damageAnimation property:
self.damageAnimation = SKAction.sequence([
    damageStart,
    fadeOutAndIn,
    damageEnd
    ])

接下来,更新 takeDamage 函数,在受到打击后立即标记玩家为受伤。你刚刚创建的受伤动画将在完成后关闭受伤标记。在此更改后,takeDamage 函数的前四行应该看起来像这样(新代码用粗体表示):

// If invulnerable or damaged, return out of the function:
if self.invulnerable || self.damaged { return }
// Set the damaged state to true after being hit:
self.damaged = true

运行项目。在受到伤害后,你的企鹅应该逐渐消失并能够穿过敌人,如图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_07_04.jpg

我们开始看到我们辛勤工作的良好成果。注意企鹅在无敌状态下可以穿过敌人,但仍然与金币、星星和地面发生碰撞。接下来,我们将添加一个游戏结束动画。

游戏结束动画

当企鹅的生命值耗尽时,我们将创建一个有趣且夸张的死亡动画。当皮埃尔失去最后一点生命值时,他将悬挂在空中,放大体型,翻转到背部,然后最终跌落到地面。为了实现这个动画,在 Player 类的 createAnimations 函数底部添加以下代码:

/* --- Create the death animation --- */
let startDie = SKAction.runBlock {
    // Switch to the death texture with X eyes:
    self.texture =
        self.textureAtlas.textureNamed("pierre-dead.png")
    // Suspend the penguin in space:
    self.physicsBody?.affectedByGravity = false
    // Stop any movement:
    self.physicsBody?.velocity = CGVector(dx: 0, dy: 0)
    // Make the penguin pass through everything except the ground:
    self.physicsBody?.collisionBitMask =
        PhysicsCategory.ground.rawValue
}

let endDie = SKAction.runBlock {
    // Turn gravity back on:
    self.physicsBody?.affectedByGravity = true
}

self.dieAnimation = SKAction.sequence([
    startDie,
    // Scale the penguin bigger:
    SKAction.scaleTo(1.3, duration: 0.5),
    // Use the waitForDuration action to provide a short pause:
    SKAction.waitForDuration(0.5),
    // Rotate the penguin on to his back:
    SKAction.rotateToAngle(3, duration: 1.5),
    SKAction.waitForDuration(0.5),
    endDie
])

运行项目并与三个敌人碰撞。你会看到如截图所示的喜剧死亡动画播放:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_07_05.jpg

可怜的皮埃尔企鹅!你很好地实现了伤害和死亡动画。接下来,我们将处理硬币接触事件上的硬币收集。

收集硬币

作为玩家的主要目标之一,收集硬币应该是我们游戏中最令人愉快的方面之一。当玩家接触硬币时,我们将创建一个奖励动画。按照以下步骤实现硬币收集:

  1. GameScene.swift 中,向 GameScene 类添加一个新属性:

    var coinsCollected = 0
    
  2. Coin.swift 中,向 Coin 类添加一个名为 collect 的新函数:

    func collect() {
        // Prevent further contact:
        self.physicsBody?.categoryBitMask = 0
        // Fade out, move up, and scale up the coin:
        let collectAnimation = SKAction.group([
            SKAction.fadeAlphaTo(0, duration: 0.2),
            SKAction.scaleTo(1.5, duration: 0.2),
            SKAction.moveBy(CGVector(dx: 0, dy: 25), duration: 0.2)
        ])
        // After fading it out, move the coin out of the way
        // and reset it to initial values until the encounter
        // system re-uses it:
        let resetAfterCollected = SKAction.runBlock {
            self.position.y = 5000
            self.alpha = 1
            self.xScale = 1
            self.yScale = 1
            self.physicsBody?.categoryBitMask =
                PhysicsCategory.coin.rawValue
        }
        // Combine the actions into a sequence:
        let collectSequence = SKAction.sequence([
            collectAnimation,
            resetAfterCollected
        ])
        // Run the collect animation:
        self.runAction(collectSequence)
    }
    
  3. GameScene.swift 中,在 didBeginContact 函数的硬币接触情况下调用新的 collect 函数:

    case PhysicsCategory.coin.rawValue:
        // Try to cast the otherBody's node as a Coin:
        if let coin = otherBody.node as? Coin {
            // Invoke the collect animation:
            coin.collect()
            // Add the value of the coin to our counter:
            self.coinsCollected += coin.value
            println(self.coinsCollected)
        }
    

干得好!运行项目并尝试收集一些硬币。你会看到硬币执行它们的收集动画。游戏将跟踪你收集的硬币数量,并将数字打印到控制台。玩家目前还看不到这个数字;我们将在下一章中在游戏屏幕上添加一个文本计数器。接下来,我们将实现升级星的游戏逻辑。

升级星逻辑

当玩家接触星星时,我们将授予玩家短暂的不可伤害状态,并给予玩家极大的速度以通过遭遇。按照以下步骤实现升级:

  1. Player.swift 中,向 Player 类添加一个新函数,如下所示:

    func starPower() {
        // Remove any existing star power-up animation, if
        // the player is already under the power of star
        self.removeActionForKey("starPower")
        // Grant great forward speed:
        self.forwardVelocity = 400
        // Make the player invulnerable:
        self.invulnerable = true
        // Create a sequence to scale the player larger,
        // wait 8 seconds, then scale back down and turn off
        // invulnerability, returning the player to normal: 
        let starSequence = SKAction.sequence([
            SKAction.scaleTo(1.5, duration: 0.3),
            SKAction.waitForDuration(8),
            SKAction.scaleTo(1, duration: 1),
            SKAction.runBlock {
                self.forwardVelocity = 200
                self.invulnerable = false
            }
        ])
        // Execute the sequence:
        self.runAction(starSequence, withKey: "starPower")
    }
    
  2. GameScene 类的 didBeginContact 函数中,在升级情况下调用新的函数:

    case PhysicsCategory.powerup.rawValue:
        player.starPower()
    

你可能会发现增加星星升级的生成率来测试很有帮助。记住,我们在 GameScenedidSimulatePhysics 函数中生成一个随机数,以确定星星生成的频率。为了更频繁地生成星星,注释掉生成随机数的行,并用硬编码的 0 替换它,如下所示(新代码用粗体标出):

//let starRoll = Int(arc4random_uniform(10))
let starRoll = 0
if starRoll == 0 {

太好了,现在测试星星升级会很容易。运行项目并找到一个星星。企鹅应该放大体型并开始向前冲,在经过时吹飞敌人,如截图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_07_06.jpg

在你继续之前,记得将星星生成代码改回随机数,否则星星会生成得太频繁。

检查点 7-B

我们在本章中取得了巨大的进步。要下载到这一点的项目,请访问此网址:

www.thinkingswiftly.com/game-development-with-swift/chapter-7

摘要

我们的小企鹅游戏看起来棒极了!你通过实现精灵接触事件,让核心机制变得生动起来。你学习了 SpriteKit 如何处理碰撞和接触,使用了位掩码为不同类型的精灵分配碰撞类别,在我们的企鹅游戏中搭建了一个接触系统,并添加了自定义游戏逻辑,包括受到伤害、收集金币和获得星级增强。

到目前为止,我们已经有一个可玩的游戏了;下一步是添加润色、菜单和功能,使游戏脱颖而出。我们将在第八章“Polishing to a Shine – HUD, Parallax Backgrounds, Particles, and More”中,通过添加 HUD、背景图像、粒子发射器等,让我们的游戏更加闪耀。第八章。

第八章. 精益求精——HUD、垂直背景、粒子效果等

我们的核心游戏机制已经就绪;现在我们可以提高整体的用户体验。我们将把重点转向使我们的游戏更加出色的非游戏功能。首先,我们将添加一个抬头显示(HUD)来显示玩家的生命值和金币计数。然后,我们将实现多层垂直背景,为游戏世界增加深度和沉浸感。我们还将探索 SpriteKit 的粒子系统,并使用粒子发射器为游戏增加制作价值。这些步骤的结合将增加游戏体验的乐趣,邀请玩家更深入地进入游戏世界,并给我们的应用带来专业、精致的感觉。

本章包括以下内容:

  • 添加 HUD

  • 垂直背景层

  • 使用粒子系统

  • 游戏开始时提供安全保障

添加抬头显示

我们的游戏需要一个 HUD 来显示玩家的当前生命值和金币分数。我们可以用心形来表示生命值——就像过去的一些经典游戏一样——并使用SKLabelNode在屏幕上绘制文本以显示收集到的金币数量。

我们将把 HUD 附加到场景本身,而不是world节点,因为它不会随着玩家向前飞行而移动。我们不希望阻挡玩家对右侧即将到来的障碍物的视线,因此我们将 HUD 元素放置在屏幕的左上角。

当我们完成时,我们的 HUD 将看起来像这样(在玩家收集了 110 个金币并受到一点伤害后):

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_08_01.jpg

要实现 HUD,请按照以下步骤操作:

  1. 首先,我们需要将 HUD 艺术资源添加到游戏中。在资源包中找到HUD.atlas纹理图集并将其添加到项目中。

  2. 接下来,我们将创建一个HUD类来处理所有的 HUD 逻辑。向项目中添加一个新的 Swift 文件HUD.swift,并添加以下代码以开始对HUD类的工作:

    import SpriteKit
    
    class HUD: SKNode {
        var textureAtlas:SKTextureAtlas = 
            SKTextureAtlas(named:"hud.atlas")
        // An array to keep track of the hearts:
        var heartNodes:[SKSpriteNode] = []
        // An SKLabelNode to print the coin score:
        let coinCountText = SKLabelNode(text: "000000")
    }
    
  3. 我们需要一个初始化风格的函数来为每个心形创建一个新的SKSpriteNode,并为金币计数配置新的SKLabelNode。向HUD类添加一个名为createHudNodes的函数,如下所示:

    func createHudNodes(screenSize:CGSize) {
        // --- Create the coin counter ---
        // First, create and position a bronze coin icon:
        let coinTextureAtlas:SKTextureAtlas = 
            SKTextureAtlas(named:"goods.atlas")
        let coinIcon = SKSpriteNode(texture: 
            coinTextureAtlas.textureNamed("coin-bronze.png"))
        // Size and position the coin icon:
        let coinYPos = screenSize.height - 23
        coinIcon.size = CGSize(width: 26, height: 26)
        coinIcon.position = CGPoint(x: 23, y: coinYPos)
        // Configure the coin text label:
        coinCountText.fontName = "AvenirNext-HeavyItalic"
        coinCountText.position = CGPoint(x: 41, y: coinYPos)
        // These two properties allow you to align the text
        // relative to the SKLabelNode's position:
        coinCountText.horizontalAlignmentMode = 
            SKLabelHorizontalAlignmentMode.Left
        coinCountText.verticalAlignmentMode = 
            SKLabelVerticalAlignmentMode.Center
        // Add the text label and coin icon to the HUD:
        self.addChild(coinCountText)
        self.addChild(coinIcon)
    
        // Create three heart nodes for the life meter:
        for var index = 0; index < 3; ++index {
            let newHeartNode = SKSpriteNode(texture: 
                textureAtlas.textureNamed("heart-full.png"))
            newHeartNode.size = CGSize(width: 46, height: 40)
            // Position the hearts below the coin counter:
            let xPos = CGFloat(index * 60 + 33)
            let yPos = screenSize.height - 66
            newHeartNode.position = CGPoint(x: xPos, y: yPos)
            // Keep track of nodes in an array property:
            heartNodes.append(newHeartNode)
            // Add the heart nodes to the HUD:
            self.addChild(newHeartNode)
        }
    }
    
  4. 我们还需要一个函数,使得GameScene类可以调用以更新金币计数标签。向HUD类添加一个名为setCoinCountDisplay的新函数,如下所示:

    func setCoinCountDisplay(newCoinCount:Int) {
        // We can use the NSNumberFormatter class to pad
        // leading 0's onto the coin count:
        let formatter = NSNumberFormatter()
        formatter.minimumIntegerDigits = 6
        if let coinStr = formatter.stringFromNumber(newCoinCount) {
            // Update the label node with the new coin count:
            coinCountText.text = coinStr
        }
    }
    
  5. 我们还需要一个函数来更新玩家生命值变化时的心形图形。向HUD类添加一个名为setHealthDisplay的新函数,如下所示:

    func setHealthDisplay(newHealth:Int) {
        // Create a fade SKAction to fade out any lost hearts:
        let fadeAction = SKAction.fadeAlphaTo(0.2, 
            duration: 0.3)
        // Loop through each heart and update its status:
        for var index = 0; index < heartNodes.count; ++index {
            if index < newHealth {
                // This heart should be full red:
                heartNodes[index].alpha = 1
            }
            else {
                // This heart should be faded:
                heartNodes[index].runAction(fadeAction)
            }
        }
    }
    
  6. 我们的HUD类已经完成。接下来,我们将在GameScene类中连接它。打开GameScene.swift文件,并向GameScene类添加一个新属性,实例化HUD类的一个实例:

    let hud = HUD()
    
  7. 我们需要将HUD节点放置到场景中,位于其他游戏对象之上。在GameScene didMoveToView函数的底部添加以下代码:

    // Create the HUD's child nodes:
    hud.createHudNodes(self.size)
    // Add the HUD to the scene:
    self.addChild(hud)
    // Position the HUD above any other game element
    hud.zPosition = 50
    
  8. 我们准备向 HUD 发送健康和金币更新。首先,当玩家受到伤害时,我们将使用健康更新来更新 HUD。在 GameScene didBeginContact 函数内部,找到玩家受到伤害的接触情况——当他或她接触地面或敌人时——并添加以下(粗体)新代码,以向 HUD 发送健康更新:

    case PhysicsCategory.ground.rawValue:
        player.takeDamage()
     hud.setHealthDisplay(player.health)
    case PhysicsCategory.enemy.rawValue:
        player.takeDamage()
     hud.setHealthDisplay(player.health)
    
    
  9. 最后,每当玩家收集到一个金币时,我们将更新 HUD。找到玩家接触金币的接触情况,并调用 HUD setCoinCountDisplay 函数(粗体新代码)如下:

    case PhysicsCategory.coin.rawValue:
        // Try to cast the otherBody's node as a Coin:
        if let coin = otherBody.node as? Coin {
            coin.collect()
            self.coinsCollected += coin.value
     hud.setCoinCountDisplay(self.coinsCollected)
        }
    
  10. 运行项目,你应该会看到你的金币计数器和健康仪表出现在左上角,如这个截图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_08_02.jpg

干得好!我们的 HUD 已经完成。接下来,我们将构建我们的背景层。

视差背景层

通过绘制单独的背景层并将它们以不同的速度移动过相机,视差为你的游戏增加了深度感。非常慢的背景给人一种距离感,而快速移动的背景看起来似乎非常接近玩家。我们可以通过用越来越不饱和的颜色绘制远处的物体来增强效果。

在我们的游戏中,我们将通过将背景附加到世界并随着世界向左移动而缓慢地将背景推向右侧来实现视差效果。当世界向左移动(带着背景一起移动)时,我们将背景的 x 位置向右移动,以便总移动距离小于正常游戏对象。结果将是背景层看起来比游戏中的其他部分移动得更慢,因此看起来更远。

此外,每个背景将只有 3000 点宽,但将在精确的间隔内向前跳跃以无缝循环,类似于 Ground 类。

添加背景资源

首先,按照以下步骤添加艺术作品:

  1. 在 Xcode 中打开你项目中的 Images.xcassets 文件。

  2. 在提供的游戏资源中,在 Backgrounds 文件夹中找到四个背景图像。

  3. 将四个背景拖放到 Images.xcassets 文件的左侧面板中。

你应该会看到背景如这里所示出现在左侧面板中:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_08_03.jpg

实现背景类

我们需要一个新类来管理视差和无缝循环的重定位逻辑。我们可以为每个背景层实例化一个 Background 类的新实例。要创建 Background 类,请将一个新的 Swift 文件 Background.swift 添加到你的项目中,使用以下代码:

import SpriteKit

class Background: SKSpriteNode {
    // movementMultiplier will store a float from 0-1 to indicate
    // how fast the background should move past.
    // 0 is full adjustment, no movement as the world goes past
    // 1 is no adjustment, background passes at normal speed
    var movementMultiplier = CGFloat(0)
    // jumpAdjustment will store how many points of x position
    // this background has jumped forward, useful for calculating
    // future seamless jump points:
    var jumpAdjustment = CGFloat(0)
    // A constant for background node size:
    let backgroundSize = CGSize(width: 1000, height: 1000)

    func spawn(parentNode:SKNode, imageName:String, 
        zPosition:CGFloat, movementMultiplier:CGFloat) {
        // Position from the bottom left:
        self.anchorPoint = CGPointZero
        // Start backgrounds at the top of the ground (y: 30)
        self.position = CGPoint(x: 0, y: 30)
        // Control the order of the backgrounds with zPosition:
        self.zPosition = zPosition
        // Store the movement multiplier:
        self.movementMultiplier = movementMultiplier
        // Add the background to the parentNode:
        parentNode.addChild(self)

        // Build three child node instances of the texture,
        // Looping from -1 to 1 so the backgrounds cover both
        // forward and behind the player at position zero.
        // closed range operator: "..." includes both endpoints:
        for i in -1...1 {
            let newBGNode = SKSpriteNode(imageNamed: imageName)
            // Set the size for this node from constant: 
            newBGNode.size = backgroundSize
            // Position these nodes by their lower left corner:
            newBGNode.anchorPoint = CGPointZero
            // Position this background node:
            newBGNode.position = CGPoint(
                x: i * Int(backgroundSize.width), y: 0)
            // Add the node to the Background:
            self.addChild(newBGNode)
        }
    }

    // We will call updatePosition every frame to
    // reposition the background:
    func updatePosition(playerProgress:CGFloat) {
        // Calculate a position adjustment after loops and 
        // parallax multiplier:
        let adjustedPosition = jumpAdjustment + playerProgress * 
            (1 - movementMultiplier)
        // Check if we need to jump the background forward:
        if playerProgress - adjustedPosition > 
            backgroundSize.width {
            jumpAdjustment += backgroundSize.width
        }
        // Adjust this background forward as the world 
        // moves back so the background appears slower:
        self.position.x = adjustedPosition
    }
}

在 GameScene 类中连接背景

我们需要在GameScene类中添加三个代码修改来连接我们的背景。首先,我们将创建一个数组来跟踪背景。接下来,当场景开始时,我们将生成背景。最后,我们可以从GameScene didSimulatePhsyics函数中调用Background类的updatePosition函数,以便在每一帧之前重新定位背景。按照以下步骤连接背景:

  1. GameScene类本身上创建一个新的数组属性来存储我们的背景,如下所示:

    var backgrounds:[Background] = []
    
  2. didMoveToView函数的底部,实例化和生成我们的四个背景:

    // Instantiate four Backgrounds to the backgrounds array:
    for i in 0...3 {
        backgrounds.append(Background())
    }
    // Spawn the new backgrounds:
    backgrounds[0].spawn(world, imageName: "Background-1", zPosition: -5, movementMultiplier: 0.75)
    backgrounds[1].spawn(world, imageName: "Background-2", zPosition: -10, movementMultiplier: 0.5)
    backgrounds[2].spawn(world, imageName: "Background-3", zPosition: -15, movementMultiplier: 0.2)
    backgrounds[3].spawn(world, imageName: "Background-4", zPosition: -20, movementMultiplier: 0.1)
    
  3. 最后,在didSimulatePhysics函数的底部添加以下代码,以便在每一帧之前重新定位背景:

    // Position the backgrounds:
    for background in self.backgrounds {
        background.updatePosition(playerProgress)
    }
    
  4. 运行项目。您应该看到四个背景图像作为单独的层在动作后面移动,并带有视差效果。此截图显示了背景在您的游戏中应该出现的样子:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_08_04.jpg

小贴士

如果您正在使用 iOS 模拟器测试您的游戏,在向游戏中添加这些大型背景纹理后,帧率降低是正常的。游戏仍然可以在 iOS 设备上良好运行。

太棒了!你已经成功实现了你的背景系统。背景让皮埃尔的企鹅世界感觉更加完整,增加了游戏的沉浸感。接下来,我们将使用粒子发射器在皮埃尔飞行时在其身后添加一条轨迹——这是一个有趣的添加,有助于玩家掌握控制。

检查点 8-A

要下载到这一点的项目,请访问此 URL:

www.thinkingswiftly.com/game-development-with-swift/chapter-8

利用 SpriteKit 的粒子系统

SpriteKit 包含一个强大的粒子系统,这使得向游戏中添加令人兴奋的图形变得容易。粒子发射器节点创建了许多图像的小实例,这些实例组合在一起创建了一个看起来很棒的效果。您可以使用发射器节点生成雪、火、火花、爆炸、魔法和其他有用的效果,这些效果在其他情况下可能需要大量的工作。

对于我们的游戏,您将学习如何使用发射器节点在皮埃尔企鹅飞行时在其身后创建一条小点轨迹,这使得玩家更容易了解他们的点击如何影响皮埃尔的飞行路径。

当我们完成时,皮埃尔的轨迹将看起来像这样:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_08_05.jpg

添加圆形粒子资源

每个粒子系统都会发射单个图像的多个版本,以创建累积的粒子效果。在我们的例子中,这个图像是一个简单的圆圈。要将圆圈图像添加到游戏中,请按照以下步骤操作:

  1. 在 Xcode 中打开Images.xcassets文件。

  2. 在提供的游戏资源中找到Particles文件夹中的dot.png图像。

  3. 将图像文件拖放到Images.xcassets左侧面板中。

创建 SpriteKit 粒子文件

Xcode 提供了一个出色的 UI 用于创建和编辑粒子系统。要使用 UI,我们将向我们的项目添加一个新的SpriteKit 粒子文件。按照以下步骤添加新文件:

  1. 首先,向你的项目添加一个新文件,并定位到SpriteKit 粒子文件类型。你可以在这个资源类别下找到这个模板,如图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_08_06.jpg

  2. 在以下提示中,选择雪花作为粒子模板

  3. 将文件命名为 PierrePath.sks 并点击创建以将新文件添加到你的项目中。

Xcode 将在主框架中打开新的粒子发射器,它应该看起来像这样:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_08_07.jpg

在 Xcode 的粒子编辑器中预览雪花模板

小贴士

在撰写本文时,Xcode 的粒子编辑器仍然有些古怪。如果你在中间看不到白色雪花粒子效果,请尝试在深灰色中心区域点击任何位置以重新定位粒子发射器 - 有时它不会从预期的位置开始。

这对于测试不与旧粒子重叠的设置更改也很有用。只需在编辑器中点击任何位置即可重新定位发射器。

确保你打开了右侧侧边栏,通过在 Xcode 右上角点亮工具按钮,如图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_08_08.jpg

你可以使用工具栏编辑粒子发射器的动画质量。你可以编辑几个属性:粒子的数量、粒子的寿命、粒子移动的速度、粒子缩放的大小,等等。这是一个非常棒的工具,因为你可以立即看到你的更改的反馈。请随意通过更改粒子属性进行实验。

配置路径粒子设置

要创建 Pierre 的点状轨迹,更新你的粒子设置以匹配此截图所示的设置:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_08_09.jpg

当你的编辑器显示一个没有明显运动的微小白色圆圈时,你就有了正确的设置。

将粒子发射器添加到游戏中

我们将把新的发射器连接到 Player 节点,这样发射器就会在玩家飞行的任何地方创建新的白色圆圈。我们可以轻松地在代码中引用编辑器中刚刚创建的发射器设计。打开 Player.swift 并在 spawn 函数底部添加以下代码:

// Instantiate a SKEmitterNode with the PierrePath design:
let dotEmitter = SKEmitterNode(fileNamed: "PierrePath.sks")
// Place the particle zPosition behind the penguin:
dotEmitter.particleZPosition = -1
// By adding the emitter node to the player, the emitter will move 
// with the penguin and emit new dots wherever the player moves
self.addChild(dotEmitter)
// However, the particles themselves should attach to the world,
// so they trail behind as the player moves forward.
// (Note that self.parent refers to the world node)
dotEmitter.targetNode = self.parent

运行项目。你应该会看到 Pierre 背后拖曳的白色点,如图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_08_10.jpg

干得好。现在玩家可以看到他们飞行的路径,这既有趣又有教育意义。点的反馈将帮助玩家学习控制系统的灵敏度,从而更快地掌握游戏。

这只是你可以使用粒子发射节点创建的许多特殊效果之一。现在你知道了如何创建、编辑和在世界中放置粒子发射器,你可以探索其他创造性的可能性。其他有趣的想法包括皮埃尔碰到敌人时产生的火花,或者在背景中轻轻飘落的雪花。

游戏开始时提供安全保障

你可能已经注意到,当你启动游戏时,皮埃尔企鹅会迅速跌落到地上,这并不是很有趣。相反,我们可以在游戏开始时将皮埃尔发射到一个优雅的循环弧线,给玩家一个准备飞行的时间。要做到这一点,打开Player.swift文件,在spawn函数的底部添加以下代码:

// Grant a momentary reprieve from gravity:
self.physicsBody?.affectedByGravity = false
// Add some slight upward velocity:
self.physicsBody?.velocity.dy = 50
// Create a SKAction to start gravity after a small delay:
let startGravitySequence = SKAction.sequence([
    SKAction.waitForDuration(0.6),
    SKAction.runBlock {
        self.physicsBody?.affectedByGravity = true
    }])
self.runAction(startGravitySequence)

检查点 8-B

下载到目前这个阶段的项目,请访问以下网址:

www.thinkingswiftly.com/game-development-with-swift/chapter-8

摘要

在本章中,我们将游戏世界栩栩如生。我们绘制了一个 HUD 来显示玩家的剩余生命值和金币分数,添加了透视背景以增加世界的深度和沉浸感,并学会了如何使用粒子发射器在我们的游戏中创建特殊图形。此外,我们在每次飞行开始时,在重力将我们的英雄拖下来之前添加了一个小的延迟。我们的游戏既有趣又看起来很棒!

接下来,我们需要一个菜单,这样我们就可以在不需要重新构建项目或手动关闭应用程序的情况下重新启动游戏。在第九章《添加菜单和声音》中,我们将设计一个开始菜单,当玩家死亡时添加一个重试按钮,并播放声音和音乐以创造更深入的游戏体验。

第九章:添加菜单和音效

容易忽视菜单设计,但菜单提供了游戏给玩家的第一印象。当正确使用时,你的菜单可以加强游戏的品牌,并在动作之间提供愉快的休息,从而让玩家留在游戏中。在本章中,我们将添加两个菜单:一个在游戏开始时显示的主菜单,以及一个在玩家输掉游戏时出现的重试菜单。

同样,沉浸式音效对于一款优秀的游戏至关重要。音效是支持游戏世界氛围和强调关键游戏机制(如收集金币和受到伤害)的机会。此外,每个有趣的游戏都值得有上瘾的背景音乐!我们将在本章中添加背景音乐和音效,以完成游戏世界的氛围。

本章包括以下主题:

  • 构建主菜单场景

  • 添加重新开始游戏菜单

  • 使用AVAudio添加音乐

  • 使用SKAction播放音效

构建主菜单

我们可以使用 SpriteKit 组件来构建我们的主菜单。我们将在新文件中创建一个新的场景用于主菜单,然后使用代码放置背景精灵节点、标志文本节点和按钮精灵节点。让我们首先将菜单场景添加到项目中,并构建出节点。

创建菜单场景和菜单节点

要创建菜单场景,请按照以下步骤操作:

  1. 我们将为菜单使用一个新的背景图片。让我们将其添加到我们的项目中。

    1. 在资源包的Backgrounds文件夹中定位Background-menu.png

    2. 在 Xcode 中打开Images.xcassets,然后将Background-menu.png拖放到Images.xcassets中,使其在项目中可用。

  2. 在项目中添加一个名为MenuScene.swift的新 Swift 文件。

  3. 添加以下代码以创建MenuScene场景类:

    import SpriteKit
    
    class MenuScene: SKScene {
        // Grab the HUD texture atlas:
        let textureAtlas:SKTextureAtlas = 
            SKTextureAtlas(named:"hud.atlas")
        // Instantiate a sprite node for the start button
        // (we'll use this in a moment):
        let startButton = SKSpriteNode()
    
        override func didMoveToView(view: SKView) {
        }
    }
    
  4. 接下来,我们需要配置一些场景属性。在新的场景的didMoveToView函数内部添加以下代码:

    // Position nodes from the center of the scene:
    self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
    // Set a sky-blue background color:
    self.backgroundColor = UIColor(red: 0.4, green: 0.6,
        blue: 0.95, alpha: 1.0) 
    // Add the background image:
    let backgroundImage = SKSpriteNode(imageNamed: "Background-menu")
    backgroundImage.size = CGSize(width: 1024, height: 768)
    self.addChild(backgroundImage)
    
  5. 我们需要在菜单的顶部附近绘制游戏名称。在didMoveToView函数的底部添加以下代码来绘制“皮埃尔企鹅逃离南极”:

    // Draw the name of the game:
    let logoText = SKLabelNode(fontNamed: "AvenirNext-Heavy")
    logoText.text = "Pierre Penguin"
    logoText.position = CGPoint(x: 0, y: 100)
    logoText.fontSize = 60
    self.addChild(logoText)
    // Add another line below:
    let logoTextBottom = SKLabelNode(fontNamed: "AvenirNext-Heavy")
    logoTextBottom.text = "Escapes the Antarctic"
    logoTextBottom.position = CGPoint(x: 0, y: 50)
    logoTextBottom.fontSize = 40
    self.addChild(logoTextBottom)
    
  6. 现在我们将添加开始按钮。开始按钮是由按钮图形的SKSpriteNode和“开始游戏”文本的SKLabelNode组合而成。在didMoveToView函数的底部添加以下代码以创建按钮:

    // Build the start game button:
    startButton.texture = textureAtlas.textureNamed("button.png")
    startButton.size = CGSize(width: 295, height: 76)
    // Name the start node for touch detection:
    startButton.name = "StartBtn"
    startButton.position = CGPoint(x: 0, y: -20)
    self.addChild(startButton)
    
    // Add text to the start button:
    let startText = SKLabelNode(fontNamed: 
        "AvenirNext-HeavyItalic")
    startText.text = "START GAME"
    startText.verticalAlignmentMode = .Center
    startText.position = CGPoint(x: 0, y: 2)
    startText.fontSize = 40
    // Name the text node for touch detection:
    startText.name = "StartBtn"
    startButton.addChild(startText)
    
  7. 最后,我们将使开始按钮进行脉冲式进出,以增加菜单的动感和兴奋感。在didMoveToView函数的底部添加以下代码以淡入淡出按钮:

    // Pulse the start button in and out gently:
    let pulseAction = SKAction.sequence([
        SKAction.fadeAlphaTo(0.7, duration: 0.9),
        SKAction.fadeAlphaTo(1, duration: 0.9),
        ])
    startButton.runAction( SKAction.repeatActionForever(pulseAction))
    

干得好!我们已经创建了MenuScene类,并添加了构建菜单所需的所有节点。接下来,我们将更新我们的应用,使其从菜单开始,而不是直接跳转到GameScene类。

游戏开始时启动主菜单

到目前为止,我们的应用每次启动时都会直接跳转到GameScene类。现在我们将更新我们的视图控制器,使其从MenuScene类开始。按照以下步骤在游戏开始时启动菜单:

  1. 打开GameViewController.swift文件,找到viewWillLayoutSubviews函数。

  2. 将整个viewWillLayoutSubviews函数替换为以下代码:

    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
    
        // Build the menu scene:
        let menuScene = MenuScene()
        let skView = self.view as! SKView
        // Ignore drawing order of child nodes
        // (This increases performance)
        skView.ignoresSiblingOrder = true
        // Size our scene to fit the view exactly:
        menuScene.size = view.bounds.size
        // Show the menu:
        skView.presentScene(menuScene)
    }
    

运行项目,你应该会看到应用以你的新主菜单启动,这看起来就像下面的截图:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_09_01.jpg

了不起的工作!接下来,我们将连接START GAME按钮以过渡到GameScene类。

连接 START GAME 按钮

就像在GameScene中一样,我们将在MenuScene类中添加一个touchesBegan函数来捕捉START GAME按钮的触摸。要实现touchesBegan,打开MenuScene.swift文件,在类的底部添加一个名为touchesBegan的新函数,如下所示:

override func touchesBegan(touches: Set<NSObject>, withEvent 
    event: UIEvent) {
    for touch in (touches as! Set<UITouch>) {
        let location = touch.locationInNode(self)
        let nodeTouched = nodeAtPoint(location)

        if nodeTouched.name == "StartBtn" {
            // Player touched the start text or button node
            // Switch to an instance of the GameScene:
            self.view?.presentScene(GameScene(size: self.size))
        }
    }
}

运行项目并点击开始按钮。游戏应该切换到GameScene类,游戏玩法将开始。恭喜你,你已经在 SpriteKit 中成功实现了你的第一个主菜单。接下来,我们将添加一个简单的重启菜单,当玩家死亡时,它将显示在GameScene上方。

添加重启游戏菜单

重启菜单的实现甚至更简单。我们不需要创建一个新的场景,而是可以通过扩展现有的HUD类来在游戏结束时显示重启按钮。我们还将包括一个更小的按钮,用于将玩家返回到主菜单。此菜单将显示在动作上方,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_09_02.jpg

扩展 HUD

首先,我们需要在HUD类中创建和绘制我们新的按钮节点。按照以下步骤添加节点:

  1. 打开HUD.swift文件,并在HUD类中添加两个新属性,如下所示:

    let restartButton = SKSpriteNode()
    let menuButton = SKSpriteNode()
    
  2. createHudNodes函数的底部添加以下代码:

    // Add the restart and menu button textures to the nodes:
    restartButton.texture = 
        textureAtlas.textureNamed("button-restart.png")
    menuButton.texture = 
        textureAtlas.textureNamed("button-menu.png")
    // Assign node names to the buttons:
    restartButton.name = "restartGame"
    menuButton.name = "returnToMenu"
    // Position the button node:
    let centerOfHud = CGPoint(x: screenSize.width / 2,
        y: screenSize.height / 2)
    restartButton.position = centerOfHud
    menuButton.position = 
        CGPoint(x: centerOfHud.x - 140, y: centerOfHud.y)
    // Size the button nodes:
    restartButton.size = CGSize(width: 140, height: 140)
    menuButton.size = CGSize(width: 70, height: 70)
    
  3. 我们故意没有将这些节点作为HUD的子节点添加,所以它们将不会出现在屏幕上,直到我们准备好。接下来,我们将添加一个使按钮出现的函数。我们将从这个GameScene类中调用此函数,当玩家死亡时。在HUD类中添加一个名为showButtons的函数,如下所示:

    func showButtons() {
        // Set the button alpha to 0:
        restartButton.alpha = 0
        menuButton.alpha = 0
        // Add the button nodes to the HUD:
        self.addChild(restartButton)
        self.addChild(menuButton)
        // Fade in the buttons:
        let fadeAnimation = 
            SKAction.fadeAlphaTo(1, duration: 0.4)
        restartButton.runAction(fadeAnimation)
        menuButton.runAction(fadeAnimation)
    }
    

为游戏结束连接 GameScene

我们需要告诉HUD类,当玩家耗尽生命值时,显示重启和主菜单按钮。打开GameScene.swift文件,并在GameScene类中添加一个名为gameOver的新函数,如下所示:

func gameOver() {
    // Show the restart and main menu buttons:
    hud.showButtons()
}

到此为止,我们将在下一章中添加到gameOver函数,当我们实现高分系统时。

当玩家死亡时通知 GameScene 类

到目前为止,GameScene类对玩家是生是死一无所知。我们需要改变这一点,以便使用我们新的gameOver函数。打开Player.swift文件,找到die函数,并在函数底部添加以下代码:

// Alert the GameScene:
if let gameScene = self.parent?.parent as? GameScene {
    gameScene.gameOver()
}

我们通过遍历节点树来访问GameScenePlayer节点的父节点是world节点,world节点的父节点是GameScene类。

运行项目并死亡。你应该会看到死亡后出现两个新按钮,如下所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_09_03.jpg

干得好。按钮显示正确,但当我们点击它们时,还没有任何反应。为了完成我们的重启菜单,我们只需在GameScene类的touchesBegan函数中实现两个新按钮的触摸事件。

实现重启菜单的触摸事件

现在我们已经显示了按钮,我们可以在GameScene类中添加类似于MenuScene类中START GAME按钮的触摸事件。

要添加触摸事件,打开GameScene.swift文件,找到touchesBegan函数。我们将在for循环的底部添加重启菜单的代码。以下代码包含了整个touchesBegan函数,其中新添加的内容用粗体表示:

override func touchesBegan(touches: Set<NSObject>, withEvent 
    event: UIEvent) {
    player.startFlapping()

    for touch in (touches as! Set<UITouch>) {
        let location = touch.locationInNode(self)
        let nodeTouched = nodeAtPoint(location)

        if let gameSprite = nodeTouched as? GameSprite {
            gameSprite.onTap()
        }

 // Check for HUD buttons:
 if nodeTouched.name == "restartGame" {
 // Transition to a new version of the GameScene
 // to restart the game:
 self.view?.presentScene(
 GameScene(size: self.size),
 transition: .crossFadeWithDuration(0.6))
 }
 else if nodeTouched.name == "returnToMenu" {
 // Transition to the main menu scene:
 self.view?.presentScene(
 MenuScene(size: self.size),
 transition: .crossFadeWithDuration(0.6))
 }
    }
}

为了测试你的新菜单,运行项目并故意耗尽生命值。现在,当你死亡时,你应该能够开始新游戏,或者通过点击菜单按钮返回主菜单。太棒了!你已经完成了每个游戏所需的基本两个菜单。

这些简单的步骤对于游戏的总体完成度有很大帮助,企鹅游戏看起来非常棒。接下来,我们将添加事件音效和音乐,以完成游戏世界。

检查点 9-A

在此 URL 下载到这一点的项目:

www.thinkingswiftly.com/game-development-with-swift/chapter-9

添加音乐和音效

SpriteKit 和 Swift 使得在我们的游戏中播放声音变得非常容易。我们可以像拖放图像资源一样将声音文件拖入项目中,并通过SKAction playSoundFileNamed触发播放。

我们还可以使用AVFoundation框架中的AVAudio类进行更精确的音频控制。我们将使用AVAudio来播放我们的背景音乐。

将音效资源添加到游戏中

Assets文件夹中找到Sound目录,并通过将其拖放到项目导航器中将其添加到项目中。你应该能看到Sound文件夹像其他资源一样出现在你的项目中。

播放背景音乐

首先,我们将添加背景音乐。我们希望音乐无论玩家当前查看哪个场景都能播放,因此我们将从视图控制器本身播放音乐。要播放音乐,请按照以下步骤操作:

  1. 打开GameViewController.swift文件,在现有导入语句下方添加以下import语句,以便我们访问AVFoundation类:

    import AVFoundation
    
  2. GameViewController类中,添加以下属性以存储我们的AVAudioPlayer

    var musicPlayer = AVAudioPlayer()
    
  3. viewWillLayoutSubviews函数的底部,添加以下代码以播放和循环音乐:

    // Start the background music:
    let musicUrl = NSBundle.mainBundle().URLForResource(
        "Sound/BackgroundMusic.m4a", withExtension: nil)
    if let url = musicUrl {
        musicPlayer = 
            AVAudioPlayer(contentsOfURL: url, error: nil)
        musicPlayer.numberOfLoops = -1
        musicPlayer.prepareToPlay()
        musicPlayer.play()
    }
    

运行项目。当应用启动时,你应该立即听到背景音乐。当你从主菜单移动到游戏界面再返回时,音乐应该持续播放。

播放音效

播放简单的声音甚至更容易。我们将使用 SKAction 对象在特定事件上播放声音,例如捡起硬币或开始游戏。

将硬币音效添加到 Coin 类

首先,我们将在玩家每次捡到硬币时添加一个快乐的声音。要添加硬币音效,请按照以下步骤操作:

  1. 打开 Coin.swift 文件,并在 Coin 类中添加一个新属性以缓存硬币声音动作:

    let coinSound = 
        SKAction.playSoundFileNamed("Sound/Coin.aif", 
            waitForCompletion: false)
    
  2. 定位 collect 函数,并在函数底部添加以下行以播放声音:

    // Play the coin sound:
    self.runAction(coinSound)
    

这就是每次玩家捡到硬币时播放硬币声音所需做的全部工作。如果您喜欢,现在可以运行项目来测试它。

小贴士

为了避免基于内存的崩溃,缓存每个 playSoundFileNamed 动作对象并在每次想要播放声音时重新运行相同的对象非常重要,而不是为每次播放创建一个新的 SKAction 对象实例。

将增强效果和受伤声音效果添加到 Player 类

当玩家找到星星增强效果时,我们将播放一个令人兴奋的声音,当玩家受到伤害时,我们将播放一个受伤的声音。按照以下步骤实现声音:

  1. 打开 Player.swift 文件,并在 Player 类中添加两个新属性以缓存音效:

    let powerupSound = 
        SKAction.playSoundFileNamed("Sound/Powerup.aif",     
            waitForCompletion: false)
    let hurtSound = 
        SKAction.playSoundFileNamed("Sound/Hurt.aif",
            waitForCompletion: false)
    
  2. 定位 takeDamage 函数,并在底部添加以下行:

    // Play the hurt sound:
    self.runAction(hurtSound)
    
  3. 找到 starPower 函数,并在底部添加以下行:

    // Play the powerup sound:
    self.runAction(powerupSound)
    

游戏开始时播放声音

最后,我们将在游戏开始时播放一个声音。按照以下步骤播放此声音:

  1. 打开 GameScene.swift 文件。我们将从 didMoveToView 函数播放这个声音效果。通常,在属性中缓存声音动作是至关重要的,但我们不需要缓存游戏开始的声音,因为我们将在每个场景加载时只播放一次。

  2. GameScene didMoveToView 函数的底部添加以下行:

    // Play the start sound:
    self.runAction(SKAction.playSoundFileNamed("Sound/StartGame.aif", 
        waitForCompletion: false))
    

太好了——我们已经为我们的游戏添加了所有音效。现在您可以运行项目来测试每个声音。

检查点 9-B

在此 URL 下载我到目前为止的项目:

www.thinkingswiftly.com/game-development-with-swift/chapter-9

摘要

在本章中,我们朝着完成游戏迈出了重要的一步。我们学会了在 SpriteKit 中创建菜单,将主菜单添加到游戏中,并在玩家健康耗尽时提供了重新开始游戏的方式。然后,我们通过吸引人的背景音乐和及时的声音效果增强了游戏体验。现在游戏感觉已经完成;我们几乎准备好发布我们的游戏了。

最后一步:我们将探索与 Apple Game Center 的集成,以在 第十章 中跟踪高分和成就,与 Game Center 集成。Game Center 集成鼓励玩家继续玩游戏并提高分数。他们可以看到自己的最佳分数,查看全球最高分,并挑战朋友打破他们的最佳成绩。

第十章。与 Game Center 集成

苹果提供了一个名为Game Center的在线社交游戏网络。您的玩家可以分享高分、追踪成就、挑战朋友,并通过 Game Center 开始多人游戏的匹配。在本章中,我们将使用苹果的 iTunes Connect 网站来注册我们的应用,然后我们可以将其与 Game Center 集成,以在我们的游戏中添加排行榜和成就。

小贴士

您需要一个有效的 Apple 开发者账户(每年 99 美元),才能将您的应用注册到苹果,使用带有 Game Center 的 iTunes Connect 网站,并将您的游戏发布到 App Store。

本章包括以下主题:

  • 使用 iTunes Connect 注册应用

  • 在我们的应用中验证玩家的 Game Center 账户

  • MenuScene类打开 Game Center

  • 添加排行榜

  • 创建和授予成就

使用 iTunes Connect 注册应用

由于苹果将在他们的集中式服务器上存储我们的高分和成就,我们需要通知苹果我们需要为我们的应用提供 Game Center。第一步是在 iTunes Connect 网站上为我们的应用创建一个记录。按照以下步骤创建 iTunes Connect 记录:

  1. 在网页浏览器中,导航到itunesconnect.apple.com

  2. 使用您的 Apple 开发者账户信息登录。

  3. 当您到达iTunes Connect仪表板时,点击My Apps图标。

  4. 在左上角,点击**+符号并选择New iOS App**,如图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_10_01.jpg

  5. 在随后的对话框中,找到底部链接,上面写着在开发者门户上注册新的 bundle ID。点击此链接为您应用创建一个 bundle ID。

  6. 您将到达一个标题为Registering an App ID的页面。这个页面一开始可能看起来令人不知所措,但您只需要填写两个字段。首先,在App Description部分输入您应用的名称。

  7. 滚动到App ID Suffix部分。请确保选择Explicit App ID,然后从您的 Xcode 项目设置中输入Bundle ID,如图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_10_02.jpg

  8. 滚动到App Services部分,并确保 Game Center 选项已经被勾选。

  9. 在页面底部,点击Continue。然后在随后的确认页面上点击Submit

  10. 您现在可以关闭此标签页,返回到 iTunes Connect,从新 iOS 应用屏幕上您离开的地方继续。

    小贴士

    您刚刚创建的 bundle ID 可能需要一段时间才能在 iTunes Connect 中显示。如果发生这种情况,请休息一下,几分钟后再次尝试。

  11. 输入您的应用的名称主要语言版本SKU(对公众不可见)。然后选择您刚刚创建的Bundle ID,如图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_10_03.jpg

  12. 在右下角点击创建。现在您应该在 iTunes Connect 中看到您应用的概览,它看起来可能像以下截图:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_10_05.jpg

恭喜,我们不仅更接近配置 Game Center,我们还迈出了将我们的应用提交到应用商店的第一步!

配置 Game Center

现在我们有了 iTunes Connect 应用记录,我们可以告诉苹果我们如何在游戏中使用 Game Center。按照以下步骤配置 Game Center:

  1. 在您的应用页面上,点击顶部导航中的 Game Center 链接。

  2. 选择为单款游戏启用,如图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_10_06.jpg

  3. 您将看到一个屏幕,允许您为您的游戏创建新的排行榜和成就。太好了!我们将在本章的后面使用这个页面。

我们已通知苹果我们希望在游戏中使用 Game Center。接下来,我们需要创建一个用于测试目的的沙盒用户账户。

创建测试用户

在应用开发期间,Game Center 使用独立的测试服务器,因此在我们测试时无法使用我们的真实 Apple ID 登录到 Game Center。相反,我们将在 iTunes Connect 中创建一个沙盒账户。

iOS 开发者库的网站指出:“始终为测试 Game Center 中的游戏创建新的测试账户。切勿使用现有的 Apple ID。”

按照以下步骤创建用于测试的 Game Center 沙盒账户:

  1. iTunes Connect中,使用左上角的下拉菜单选择用户和角色,如图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_10_07.jpg

  2. 一旦您进入用户和角色页面,请点击屏幕顶部的导航栏中的沙盒测试者

  3. 沙盒测试者页面上的指示,点击**+**图标添加新用户。

  4. 按照您的喜好填写测试用户的详细信息。以下是我如何填写我的测试用户信息的:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_10_08.jpg

  5. 点击保存按钮创建新用户。

小贴士

确保将您的实时 Apple ID 和沙盒账户分开。如果您使用沙盒账户登录到实时 Game Center 应用,该账户将失效。

太好了!这就是我们开始将 Game Center 集成到游戏中的所有所需步骤。下一步是将 Game Center 与我们的游戏代码集成。我们将从玩家打开我们的应用时验证他们的 Game Center 账户开始。

验证玩家的 Game Center 账户

当我们的应用启动时,我们将检查玩家是否已经登录到他们的 Game Center 账户。如果没有,我们将给他们一个登录的机会。稍后,当我们想要提交高分或成就时,我们可以使用应用启动时收集到的验证信息,而不是打断他们的游戏会话来收集他们的 Game Center 信息。

按照以下步骤在应用启动时验证玩家的 Game Center 账户:

  1. 我们将在GameViewController类中工作,所以请在 Xcode 中打开GameViewController.swift文件。

  2. 在文件顶部添加一个新的import语句,以便我们可以使用GameKit框架:

    import GameKit
    
  3. GameViewController类中,添加一个名为authenticateLocalPlayer的新函数,代码如下:

    // (We pass in the menuScene instance so we can create a  
    // leaderboard button in the menu when the player is
    // authenticated with Game Center)
    func authenticateLocalPlayer(menuScene:MenuScene) {
        // Create a new Game Center localPlayer instance:
        let localPlayer = GKLocalPlayer.localPlayer();
        // Create a function to check if they authenticated
        // or show them the log in screen:
        localPlayer.authenticateHandler = {
            (viewController : UIViewController!,error : NSError!) -> Void in
            if viewController != nil {
                // They are not logged in, show the log in: self.presentViewController(viewController, animated: true, completion: nil)
            }
            else if localPlayer.authenticated {
                // They authenticated successfully!
                // We will be back later to create a // leaderboard button in the MenuScene
            }
            else {
                // Not able to authenticate, skip Game Center 
            }
        }
    }
    
  4. GameViewController类的viewWillLayoutSubviews函数底部,添加对新创建的authenticateLocalPlayer函数的调用:

    authenticateLocalPlayer(menuScene)
    

运行你的项目。你应该会看到游戏中心动画进入,请求你的凭证,如下所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_10_13.jpg

太好了!记得使用你的沙盒账户。第一次登录时,游戏中心将询问一些额外的问题来设置你的账户。一旦完成游戏中心的表格,你应该返回到主菜单,一个小横幅从屏幕顶部动画进入和退出,告诉你你已经登录。横幅看起来像这样:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_10_14.jpg

如果你看到这个欢迎回来横幅,你就成功实现了游戏中心认证代码。接下来,我们将在菜单中添加一个排行榜按钮,以便玩家可以在我们的应用中查看他们的进度。

在我们的游戏中打开游戏中心

如果用户已验证,我们将在MenuScene类中添加一个按钮,以便他们可以在我们的游戏中打开排行榜并查看成就。或者,玩家始终可以使用 iOS 中的游戏中心应用查看他们的进度。

按照以下步骤在菜单场景中创建排行榜按钮:

  1. 在 Xcode 中打开MenuScene.swift文件。

  2. 在文件顶部添加一个新的import语句,以便我们可以使用GameKit框架:

    import GameKit
    
  3. 更新声明MenuScene类的行,以便我们的类采用GKGameCenterControllerDelegate协议。这允许游戏中心屏幕在玩家关闭游戏中心时通知我们的场景:

    class MenuScene: SKScene, GKGameCenterControllerDelegate {
    
  4. 我们需要一个函数来创建排行榜按钮并将其添加到场景中。当游戏中心验证玩家后,我们将调用此函数。在MenuScene类中添加一个名为createLeaderboardButton的新函数,如下所示:

    func createLeaderboardButton() {
        // Add some text to open the leaderboard
        let leaderboardText = SKLabelNode(fontNamed: "AvenirNext")
        leaderboardText.text = "Leaderboard"
        leaderboardText.name = "LeaderboardBtn"
        leaderboardText.position = CGPoint(x: 0, y: -100)
        leaderboardText.fontSize = 20
        self.addChild(leaderboardText)
    }
    
  5. 如果玩家已经通过游戏中心进行了验证,我们将从didMoveToView函数中调用我们的createLeaderboardButton函数。这为在游戏后返回主菜单的玩家创建按钮。将以下代码添加到didMoveToView函数的底部:

    // If they're logged in, create the leaderboard button
    // (This will only apply to players returning to the menu)
    if GKLocalPlayer.localPlayer().authenticated {
        createLeaderboardButton()
    }
    
  6. 接下来,我们将创建一个实际打开游戏中心的函数。添加一个名为showLeaderboard的新函数,如下所示:

    func showLeaderboard() {
        // A new instance of a game center view controller:
        let gameCenter = GKGameCenterViewController()
        // Set this scene as the delegate (helps enable the // done button in the game center)
        gameCenter.gameCenterDelegate = self
        // Show the leaderboards when the game center opens:
        gameCenter.viewState = 
            GKGameCenterViewControllerState.Leaderboards
        // Find the current view controller:
        if let gameViewController = 
            self.view?.window?.rootViewController {
            // Display the new Game Center view controller:
            gameViewController.showViewController(gameCenter, sender: self)
            gameViewController.navigationController?
               .pushViewController(gameCenter, animated: true)
        }
    }
    
  7. 我们需要添加另一个函数以遵守GKGameCenterControllerDelegate协议。这个函数名为gameCenterViewDidFinish,当玩家在游戏中心点击完成按钮时,游戏中心将调用它。将此函数添加到MenuScene类中,如下所示:

    // This hides the game center when the user taps 'done'
    func gameCenterViewControllerDidFinish (gameCenterViewController: 
            GKGameCenterViewController!) {
        gameCenterViewController.dismissViewControllerAnimated( true, completion: nil)
    }
    
  8. 为了完成MenuScene代码,我们需要在touchesBegan函数中检查我们的排行榜按钮的点击,以调用showLeaderboard。按照以下方式更新touchesBegan函数的if块(粗体为新代码):

    if nodeTouched.name == "StartBtn" { 
        self.view?.presentScene(GameScene(size: self.size))
    }
    else if nodeTouched.name == "LeaderboardBtn" {
     showLeaderboard()
    }
    
    
  9. 接下来,打开GameViewController.swift并定位到authenticateLocalPlayer函数。

  10. 更新玩家成功认证后调用的代码块,以在MenuScene类中调用我们的新createLeaderboardButton函数。这为新认证的人创建排行榜按钮,当他们开始应用程序时。以下是代码示例(粗体为新代码):

    else if localPlayer.authenticated {
        // They authenticated successfully
     menuScene.createLeaderboardButton()
    }
    

干得好。运行项目,您应该在游戏中心认证后看到菜单中出现排行榜按钮,如图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_10_15.jpg

太棒了——如果您点击排行榜文本,游戏中心将在游戏中打开。现在您的玩家可以直接从您的游戏中查看排行榜和成就。接下来,我们将在 iTunes Connect 中创建一个排行榜和一个成就来填充游戏中心。

检查点 10-A

要下载到目前为止的项目,请访问此 URL:

www.thinkingswiftly.com/game-development-with-swift/chapter-10

添加高分排行榜

我们将在玩家完成每场比赛后将其分数提交到 Game Center 服务器。第一步是在 iTunes Connect 上注册一个新的排行榜。

在 iTunes Connect 中创建新的排行榜

首先,我们将在 iTunes Connect 中创建我们的排行榜。然后我们可以从我们的代码中连接到这个排行榜并发送新的分数。按照以下步骤在 iTunes Connect 中创建排行榜记录:

  1. 重新登录到 iTunes Connect,并导航到您的应用程序的游戏中心页面。

  2. 定位并点击显示添加排行榜的按钮。

  3. 下一页会询问您想创建哪种类型的排行榜。选择单个排行榜

  4. 填写您排行榜的信息。您可以在此处参考我的示例:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_10_09.jpg

    让我们看看每个字段:

    • 参考名称是 iTunes Connect 中排行榜列表的内部使用名称

    • 排行榜 ID是我们将在代码中引用的唯一标识符

    • 分数格式类型描述了您将要传递的数据类型(通常用于高分的是整数数据)

    • 正常排行榜使用分数提交类型最佳分数排序顺序从高到低

    • 分数范围是一种反作弊措施,您可以使用它来阻止明显虚假的分数出现在排行榜上

  5. 接下来,点击添加语言按钮。您将在此屏幕上选择排行榜的名称和分数格式。这些字段大部分是自我解释的,但您可以在此处参考我的示例:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_10_10.jpg

  6. 点击保存两次(一次用于语言对话框,一次用于排行榜屏幕)。

你应该回到了游戏中心页面,你的新排行榜在排行榜部分列出。接下来,我们将从我们的游戏代码中推送新的分数到排行榜。

从代码更新排行榜

从代码向排行榜发送新分数很简单。按照以下步骤,每次游戏结束时发送收集到的金币数量到排行榜:

  1. 在 Xcode 中打开GameScene.swift

  2. 在顶部添加一个import语句,这样我们就可以在这个文件中使用GameKit框架:

    import GameKit
    
  3. GameScene类中添加一个名为updateLeaderboard的新函数,如下所示:

    func updateLeaderboard() {
        if GKLocalPlayer.localPlayer().authenticated {
            // Create a new score object, with our leaderboard: 
            let score = GKScore(leaderboardIdentifier: 
                "pierre_penguin_coins")
            // Set the score value to our coin score:
            score.value = Int64(self.coinsCollected)
            // Report the score (wrap the score in an array)
            GKScore.reportScores([score], withCompletionHandler: 
                {(error : NSError!) -> Void in
                // The error handler was used more in previous 
                // versions of iOS, it would be unusual to // receive an error now:
                    if error != nil {
                        println(error)
                    }
            })
        }
    }
    
  4. GameScene类的GameOver函数中,调用新的updateLeaderboard函数:

    // Push their score to the leaderboard:
    updateLeaderboard()
    

运行项目并玩一个游戏,将测试金币分数发送到排行榜。然后,点击返回菜单场景并点击排行榜按钮,在游戏中打开 Game Center。你应该会看到你的第一个分数出现在排行榜上!它看起来可能像这样:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_10_16.jpg

干得好——你已经实现了你的第一个 Game Center 排行榜。接下来,我们将遵循一系列类似的步骤来创建一个收集 500 个金币的成就。

添加成就

成就为你的游戏增添了第二层乐趣,并创造了重玩价值。为了展示一个 Game Center 成就,我们将添加一个收集 500 个金币而不死奖励。

在 iTunes Connect 中创建新的成就

就像排行榜一样,我们首先需要为我们的成就创建一个 iTunes Connect 记录。按照以下步骤创建记录:

  1. 登录 iTunes Connect 并导航到你的应用的 Game Center 页面。

  2. 在排行榜列表下方,找到并点击添加成就按钮。

  3. 填写你成就的信息。以下是我的值:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_10_11.jpg

    让我们看看每个字段:

    • 参考名称是 iTunes Connect 将用于内部引用成就的名称

    • 成就 ID是我们将在代码中引用此成就的唯一标识符

    • 你可以为每个成就分配一个点数,这样玩家在收集新的成就时可以获得更多的成就点数

    • 隐藏可多次获得是显而易见的,但你可以在右侧使用问号按钮获取来自苹果的更多信息

  4. 点击添加语言按钮。我们将命名成就并给出描述,就像排行榜过程一样。此外,成就还需要一个图片。图片尺寸可以是 512x512 或 1024x1024。你可以在我们的Assets包下载中找到我使用的图片,在Extras文件夹中,gold-medal.png。以下是我的值:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_10_12.jpg

  5. 点击 保存 两次(一次用于语言对话框,一次用于成就屏幕)。

太棒了,你应该已经回到了你应用的首页,在 成就 部分列出了你的新成就。接下来,我们将把这个成就集成到游戏中。

从代码更新成就

就像发送排行榜更新一样,我们可以从 GameScene 发送成就更新到 Game Center。按照以下步骤集成我们的 500 枚金币成就:

  1. 在 Xcode 中打开 GameScene.swift 文件。

  2. 如果你跳过了排行榜部分,你需要在文件的顶部添加一个新的 import 语句,这样我们就可以使用 GameKit。如果你已经实现了排行榜,你可以跳过这一步:

    import GameKit
    
  3. GameScene 类中添加一个名为 checkForAchievements 的新函数,如下所示:

    func checkForAchievements() {
        if GKLocalPlayer.localPlayer().authenticated {
            // Check if they earned 500 coins in this game:
            if self.coinsCollected >= 500 {
                let achieve = GKAchievement(identifier: "500_coins")
                // Show a notification that they earned it:
                achieve.showsCompletionBanner = true
                achieve.percentComplete = 100
                // Report the achievement! GKAchievement.reportAchievements([achieve], withCompletionHandler: 
                        {(error:NSError!) -> Void in
                            if error != nil {
                                println(error)
                            }
                })
            }
        }
    }
    
  4. gameOver 函数的底部,调用新的 checkForAchievements 函数:

    // Check if they earned the achievement:
    checkForAchievements()
    

运行项目,如果你敢的话,完成 500 枚金币飞越。当你的游戏结束时,你应该会看到一个横幅宣布你的新成就征服,如下所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_10_17.jpg

干得好!你已经将 Game Center 排行榜和成就集成到了你的游戏中。

检查点 10-B

要下载到这一点的我的项目,请访问此 URL:

www.thinkingswiftly.com/game-development-with-swift/chapter-10

摘要

与 Game Center 集成是为你玩家提供的优秀功能。在本章中,我们学习了如何为我们的应用创建一个 iTunes Connect 记录,在我们的代码中验证 Game Center 用户,在 iTunes Connect 上创建新的排行榜和成就,然后在我们游戏中集成这些排行榜和成就。我们已经取得了很大的进步!

我们正式完成了游戏本身的工作。在下一章中,我们将为应用发布做准备,上传代码供苹果审核,并回顾我们在创建伟大游戏过程中所学到的知识。一切都在顺利进行,我们准备迈出最后一步,发布我们的游戏。恭喜!

第十一章。发布它!为 App Store 和发布做准备

多么伟大的旅程!我们已经走过了使用 Swift 进行游戏开发过程的每一个环节,我们终于准备好将我们的辛勤工作与世界分享了。我们需要通过完成与之相关的资产来为项目的发布做准备:各种应用图标、启动屏幕和 App Store 的截图。然后,我们将在 iTunes Connect 中填写我们应用的描述和信息。最后,我们将使用 Xcode 上传生产归档构建并提交给苹果的审查流程。我们离在 App Store 上看到我们的游戏越来越近了!

虽然我可以向你展示你可以使用的提交应用的通用路径,但随着苹果更新 iTunes Connect,这个过程一直在变化。此外,每个应用都有其独特的方面,可能需要在本章中我展示的路径上进行调整。我鼓励你浏览 iOS 开发者库中的苹果官方文档,并参考 Stack Overflow 以获取更新的答案。你可以通过浏览到developer.apple.com/library/ios来定位 iOS 开发者库。

本章包括以下主题:

  • 完成资产:应用图标和启动屏幕

  • 完成 iTunes Connect 信息

  • 配置定价

  • 从 Xcode 上传我们的项目

  • 在 iTunes Connect 中提交审查

完成资产

在我们能够发布我们的游戏之前,我们需要一些外围资产。我们将创建一组应用图标,重新设计启动屏幕,并为我们在 App Store 预览中支持的每个设备拍摄截图。

添加应用图标

我们的应用需要多个尺寸的应用图标才能在 App Store 和我们所支持的各个 iOS 设备上正确显示。你可以在提供的资产包中的Icon文件夹中找到一个示例图标集。

小贴士

你应该设计一个宽度为 1024 像素、高度为 1024 像素的图标,然后将其缩小以适应其他变体。确保检查每个变体,以确保调整大小后看起来仍然很好。你将在本章后面将这个大尺寸直接上传到 iTunes Connect。

将图标集成到项目中的最佳方式是使用与新建项目一起提供的预配置为应用图标的Images.xcassets资产包。我们将拖放我们的图标到这个文件中,将它们引入到项目中。

按照以下步骤将我们的图标添加到项目中:

  1. 在 Xcode 中,打开Images.xcassets文件,在左侧面板中找到AppIcon图像集。

  2. 将资产包中的图像拖放到相应的图标槽中。你可以将文件作为组拖放,Xcode 将处理它们到正确的槽中。你可以忽略设置图标的图标槽,因为我们的应用不与 iOS 设置集成。当你完成时,你的图标图像集将看起来像这样:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_11_01.jpg

  3. 通过点击项目导航器中的你的项目进入你的常规项目设置。找到应用图标源设置,并确保它设置为AppIcon以使用图像包,如图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_11_02.jpg

我们在 Xcode 中添加图标已经完成。稍后我们还需要上传一些更多尺寸的图标到 iTunes Connect。你可以在真实设备上运行你的项目来查看你的新图标效果。

设计启动屏幕

当用户在设备上点击你的图标时,iOS 会以极快的速度显示你的应用的启动屏幕作为一个简单的预览。这给人一种应用几乎立即加载的错觉。玩家会立即从他们的点击中获得反馈,而你的应用实际上在后台加载。这不是添加徽标、品牌或任何信息的地方。目标是创建一个非常简单的屏幕,看起来像你的应用在内容放置之前的样子。对于 Pierre Penguin,我们将实现一个简单的空白天空蓝色背景,看起来像主菜单在没有内容之前的样子。

按照以下步骤设置你的天空蓝色启动屏幕:

  1. 在 Xcode 中打开LaunchScreen.xib文件。你将在界面构建器中看到启动屏幕打开。

  2. 选择每个现有的文本元素,并使用键盘上的删除键删除每个元素。

  3. 通过点击启动屏幕的空白区域中的任何位置来选择整个框架。

  4. 确保你打开了 Xcode 右侧的实用工具栏,并打开属性检查器,如图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_11_03.jpg

  5. 在右侧栏中找到背景颜色设置,然后点击现有的白色颜色选项以打开颜色选择窗口。

  6. 选择颜色滑块标签,并输入 RGB 值102153242,如图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_11_04.jpg

  7. 你应该看到整个框架从我们的游戏中变成了天空蓝色。

  8. 接下来,通过点击项目导航器中的项目名称来输入你的常规设置。就像之前为应用图标所做的,确保启动屏幕文件设置为LaunchScreenhttps://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_11_05.jpg

完美!当我们运行我们的应用时,我们会立即看到天空蓝色,这为从主屏幕到我们完全加载的应用提供了一个更平滑的过渡。

为每个支持的设备截图

有趣的截图会让你的游戏在 App Store 中脱颖而出。我为 Pierre Penguin 在资源包的Screenshots文件夹中创建了一些示例截图。你需要为每个你想要支持的 iOS 设备创建单独的截图。

截图必须是 JPG 或 PNG 文件。你可以使用我的示例截图作为每个截图尺寸的模板,或者按照以下表格:

设备尺寸全屏游戏的截图尺寸
3.5"(必需)960x640 像素
4"(必需)1136x640 像素
4.7"1334x750 像素
5.5"2208x1242 像素
iPad(所有版本)2048x1536 像素

一旦您的截图准备就绪,您就可以在 iTunes Connect 中最终确定您的游戏设置。我们将在下一部分完成 iTunes Connect 的详细信息。

最终确定 iTunes Connect 信息

iTunes Connect 控制我们在 App Store 中的应用详情。我们将使用 iTunes Connect 为我们的游戏创建描述,添加我们希望在 App Store 中显示的截图,并配置我们的定价信息和项目设置。

按照以下步骤填写您的 iTunes Connect 信息:

  1. 在您的网络浏览器中打开 iTunes Connect 网站。浏览到我的应用部分,然后点击您的游戏。iTunes Connect 将带您到您游戏页面的版本标签。

  2. 我们将从截图开始。将每个设备截图拖放到应用视频预览和截图部分的相应槽位中,如图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_11_06.jpg

  3. 滚动并填写下一部分的信息:名称描述关键词和相关的 URL。这些字段是自我解释的,但您始终可以点击灰色问号圆圈以获取苹果的详细信息。

    小贴士

    关于关键词:如果您有强大、准确的关键词,用户将更容易找到您的应用。尝试使用您认为人们可能会在 App Store 中输入以引导他们到您的游戏的短语。您限制在 100 个字符内,因此请省略关键词之间的空格。

  4. 接下来,滚动到通用应用信息部分。在这里,您将上传您的应用图标,输入版本号(1.0),选择您的应用的 App Store 类别(游戏),并提供您的地址信息。如果您需要关于这些字段中的任何进一步的详细信息,请再次点击灰色问号圆圈。

  5. 滚动并找到Game Center,然后翻转滑块到开启位置。您需要通过点击蓝色加号图标添加排行榜和成就,如图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_11_07.jpg

  6. 最后,滚动到应用审核信息部分,并再次填写您的联系信息。这是为了在苹果员工需要更多信息时审核您的应用。您还可以选择在审核通过后是否希望游戏自动发布到 App Store,或者等待您手动发布以配合您的营销活动。

  7. 点击右上角的保存

配置价格

皮埃尔企鹅将免费供所有人游玩,但您可以为您的游戏选择许多定价策略。

小贴士

苹果不断更新 iTunes Connect,我预计价格部分很快将进行重大更新。您的体验可能与这些步骤不完全一致。

按照以下步骤设置您游戏的价格:

  1. 在您的游戏的 iTunes Connect 页面上,点击顶部导航栏中的价格标签。

  2. 选择 可用日期价格层级和教育折扣。以下是我的设置供您参考:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_11_08.jpg

  3. 在右下角点击 保存

完美!我们的 iTunes Connect 信息已完整并准备好提交到 App Store 审查流程。现在我们只需在 Xcode 中最终确定并上传我们的构建版本。

小贴士

如果您想对您的游戏收费,那么您需要填写在 iTunes Connect 的 协议、税务和银行 部分找到的合同和银行信息。

从 Xcode 上传我们的项目

接下来,我们将创建我们游戏的最终构建版本,验证它是否包含 App Store 所需的所有内容,并将捆绑包上传到 iTunes Connect。

首先,我们将为我们的游戏创建部署存档。当您对项目满意时,使用 产品 菜单并选择 存档…,如图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_11_09.jpg

一旦流程完成,Xcode 将打开您的存档列表。从这里,您可以验证您的应用程序,以确保它包含所有必需的资产和配置文件,以便在 App Store 上发布。按照以下步骤验证您的应用程序并将其上传到 iTunes Connect:

  1. 点击以下截图所示的 验证 按钮,以验证您的应用程序。https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_11_10.jpg

  2. 下一个屏幕将要求您为您的应用程序选择一个开发团队。如果您是独立开发者,您只需选择自己的名字,如图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_11_11.jpg

  3. Xcode 将为您创建一个分发配置文件,然后带您进入一个摘要屏幕。只需点击 验证 按钮:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_11_12.jpg

  4. Xcode 将继续验证一切是否为 App Store 准备就绪,这可能需要几分钟。完成后,您应该会看到一个成功消息,如图所示。如果您收到任何错误,您可能缺少 App Store 所需的资产或配置文件。阅读并回复错误消息,并参考 iOS 开发者库、网络搜索或 Stack Overflow 以获取进一步的帮助。https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_11_13.jpg

  5. 点击 完成,然后点击蓝色 提交到 App Store 按钮将存档上传到 iTunes Connect,如图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_11_14.jpg

  6. 您需要再次点击通过验证步骤,然后最终点击 提交。Xcode 将然后将您的应用程序上传到 iTunes Connect 并显示另一个成功消息。

恭喜!您已成功将您的应用程序上传到 Apple。我们几乎完成了应用程序的提交。接下来,我们将返回到 iTunes Connect,将我们的应用程序推送到审查和批准流程。

在 iTunes Connect 中提交审查

我们已经完成了项目的准备工作,我们准备好将我们的辛勤工作提交给苹果的审核流程。按照以下步骤提交你的应用到苹果:

  1. 返回到 iTunes Connect 网站,浏览到你的游戏页面(在版本标签页)。

  2. 滚动到构建部分,并在提交你的应用之前选择点击+添加构建https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_11_15.jpg

  3. 使用单选按钮选择你刚刚上传的归档,然后点击完成,如下所示。上传的构建显示在这个列表中可能需要几分钟(有时是几个小时):https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_11_16.jpg

  4. 在右上角点击保存,然后提交审核按钮应该会亮起蓝色:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_11_17.jpg

  5. 点击提交审核,iTunes Connect 将显示包含关于你的游戏的三个最终问题的提交审核页面。苹果想知道你的应用是否使用了加密、第三方内容或广告。我为 Pierre Penguin 回答了所有三个问题都是否定的。准确回答这些问题非常重要,所以如果你不确定如何进行,请在 iTunes Connect 中使用问号图标获取更多信息。

  6. 在你回答了提交审核问题后,点击右上角的提交。这是提交过程的最后一步。

如果你的应用提交成功,iTunes Connect 将返回到你的应用页面的版本标签页。你会看到应用状态变为等待审核,如下所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_11_18.jpg

太棒了!我们已经将我们的游戏提交给了苹果。审核过程通常需要 7-14 天。如果你的游戏没有获得批准就返回,不要气馁,苹果通常要求开发者纠正小问题并重新提交他们的应用。你正在走向在 App Store 看到你的游戏!

摘要

许多独立开发者都挣扎于发布游戏的最后一步。如果你准备好发布一款游戏,你做得很好!在本章中,我们创建了应用图标和我们的启动屏幕,在 iTunes Connect 中最终确定了我们的 App Store 营销信息,使用 Xcode 归档并上传了我们的游戏,并将我们的游戏提交给苹果进行审核。你现在应该对自己的能力有信心,能够将你的游戏发布到 App Store。

在本书的过程中,我们取得了巨大的成就:我们从一个新的项目模板开始,组装了一个完整的 Swift 游戏,直到发布。当我们各自走向不同的道路时,我祝愿你在未来的游戏开发事业中取得巨大成功。我希望你现在对自己的能力有信心,能够用 Swift 开始自己的游戏项目。我期待在 App Store 看到你的作品!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值