美文网首页
如何在 Swift 3 中用 SpriteKit 框架编写游戏

如何在 Swift 3 中用 SpriteKit 框架编写游戏

作者: minse | 来源:发表于2021-01-30 00:08 被阅读0次

    写在前面:这个系列文章是转载过来的,简书里之前也有人转载了,不过没有进行重新编排,图文等格式并不适用于简书,我参照原文样式重新排版了一次。

    你有没有想过要如何开始创作一款基于 SpriteKit 的游戏?按钮的开发是一个很庞大的任务吗?想过如何制作游戏的设置部分吗?随着 SpriteKit 的出现,在 iOS 上开发游戏已经变得空前的简单了。在本系列的第三部分,我们将完成 RainCat 游戏的开发以及对 SpriteKit 框架的介绍。

    如果你错过了上一课,你可以通过获取 Github 上的代码来赶上进度。请记住,本教程需要使用 Xcode 8 和 Swift 3。

    Raincat, 第三课

    这是我们 RainCat 之旅的第三课。在上节课里,我们用了很长一段时间来搞定了一些简单动画,猫的行为、音效和背景音乐。

    今天,我们将重点关注下面的内容:

    • 用指示器(HUD)显示得分;
    • 主菜单 — 带一些按钮;
    • 静音选项;
    • 退出游戏选项。

    更多的资源

    最后一节课的资源都在 GitHub 上,再次把那些图片拖进 Assets.xcassets 里,就像我们上节课做的那样。

    第一步!

    我们需要一种方式来显示得分。要做这个,我们就得创建一个指示器(HUD)。这个很简单:指示器是一个 SKNode ,它包含了分数和一个退出游戏的按钮。现在,我们先来搞定分数。我们用 Pixel Digivolve 字体来显示分数,你可以在 Dafont.com 找到它。就像之前我们使用不是我们原创的图片和音效一样,使用字体前,一定要浏览它的使用协议。这个字体声明,个人使用是免费的,但如果你真的很喜欢,你可以去作者的页面对他进行捐赠以表示支持。你不可能自己做所有的事,所以回馈那些一路帮助过你的人也是很愉快的。

    接着,我们就需要把自定义的字体添加到项目里了。如果是第一次添加,这可能是个棘手的过程。

    下载字体并把它移动到项目文件夹的 “Fonts” 文件夹里。这个过程我们上节课已经做过好几次了,所以我们加快点儿速度。在项目里创建 Fonts 组,然后把 Pixel digivolve.otf 文件加进去。

    现在棘手的部分来了。如果错过了这部分,也许你就不能使用字体了。我们需要添加它到 Info.plist 文件。这个文件在 Xcode 的左边。打开它你会看到一堆属性列表(或者叫 plist 文件)。右键点击列表,然后点 “Add Row”。

    添加一行

    在新添加的一行里,输入下面的内容:

    Fonts provided by application
    

    然后在 Item 0 下面,我们得添加字体的名字。plist 文件看起来应该像下面这样:

    Pixel digivolve.otf

    字体已经准备完毕啦!我们应该做个快速的测试,看看它能不能像预期那样使用。打开 GameScene.swift,把下面的代码加在 sceneDidLoad 函数里的上方:

    let label = SKLabelNode(fontNamed: "PixelDigivolve")
    label.text = "Hello World!"
    label.position = CGPoint(x: size.width /2, y: size.height /2)
    label.zPosition = 1000
    
    addChild(label)    
    

    一切 OK 吗?

    Hello world!

    如果字体正常,那就说明你做的完全正确。如果不正常,那就是什么地方出了问题。Code With Chris 有一篇更加深入的字体导入问题的文章,但要注意的是,这是一篇老版本 Swift 的文章,你可能需要稍稍改动一些地方来过渡到 Swift 3 。

    现在可以开始给我们的指示器加载自定义字体了。删掉 “Hello World” 标签,因为这个只是测试字体是否正常用的。指示器是一个 SKNode ,作为我们 HUD 控件的容器。这和我们在第一节课创建背景节点的过程一样。

    老样子,创建 HudNode.swift 文件,输入下面的代码:

    import SpriteKit
    
    class HudNode: SKNode {
      private let scoreKey = "RAINCAT_HIGHSCORE"
      private let scoreNode = SKLabelNode(fontNamed: "PixelDigivolve")
      private(set) var score: Int = 0
      private var highScore: Int = 0
      private var showingHighScore = false
    
      /// Set up HUD here.
      public func setup(size: CGSize) {
        let defaults = UserDefaults.standard
    
        highScore = defaults.integer(forKey: scoreKey)
    
        scoreNode.text = "\(score)"
        scoreNode.fontSize = 70
        scoreNode.position = CGPoint(x: size.width / 2, y: size.height - 100)
        scoreNode.zPosition = 1
    
        addChild(scoreNode)
      }
    
      /// Add point.
      /// - Increments the score.
      /// - Saves to user defaults.
      /// - If a high score is achieved, then enlarge the scoreNode and update the color.
      public func addPoint() {
        score += 1
    
        updateScoreboard()
    
        if score > highScore {
    
          let defaults = UserDefaults.standard
    
          defaults.set(score, forKey: scoreKey)
    
          if !showingHighScore {
            showingHighScore = true
    
            scoreNode.run(SKAction.scale(to: 1.5, duration: 0.25))
            scoreNode.fontColor = SKColor(red: 0.99, green: 0.92, blue: 0.55, alpha: 1.0)
          }
        }
      }
    
      /// Reset points.
      /// - Sets score to zero.
      /// - Updates score label.
      /// - Resets color and size to default values.
      public func resetPoints() {
        score = 0
    
        updateScoreboard()
    
        if showingHighScore {
          showingHighScore = false
    
          scoreNode.run(SKAction.scale(to: 1.0, duration: 0.25))
          scoreNode.fontColor = SKColor.white
        }
      }
    
      /// Updates the score label to show the current score.
      private func updateScoreboard() {
        scoreNode.text = "\(score)"
      }
    }
    

    在我们做其他事之前,先在 Constants.swift 文件底部把下面的这行代码加上 —— 我们用这个键来读写最高得分记录:

    let ScoreKey = "RAINCAT_HIGHSCORE"
    

    代码里,有五个关于计分板的变量,第一个实际上是个 SKLabelNode,用来表示标签。接着是用来保存当前分数的变量;再接下来是记录最高分的变量,最后一个变量是布尔类型,用来判断是否显示我们当前获得的分数(我们用这个变量来判断是否需要运行一个 SKAction 来增加计分板的比例以及把地板弄成黄色)。

    第一个函数 setup(size:) 的功能是把一切都设置好。我们就像之前那样来设置 SKLabelNodeSKNode 类没有任何默认尺寸,所以我们要创建一种方式来设置一个尺寸用于固定 scoreNode 的大小。我们还要从 UserDefaults 里面得到当前最高分。这是一种简单方便的存储少量数据的方法,不过不太安全。不过我们并不用担心示例程序的安全性,所以使用 UserDefaults 也能让很好地完成这个任务

    addPoint() 函数里面,我们增加了 score 变量的值,接着检查玩家是否得到一个更高的分数。如果是,那么我们就把分数存到 UserDefaults 里,然后检查当前是否显示最高分。如果玩家达到了一个很高的分数,我们就用动画渲染 scoreNode 的颜色和大小。

    resetPoints() 函数中,我们把当前分数设为 0。然后,我们就检查是否需要显示高的得分,如果需要的话,重置默认值的颜色和大小。

    最后还有一个小函数,叫 updateScoreboard。这个私有函数用来把分数设置到 scoreNode 的文本上。在 addPoint()resetPoints() 里用到了这个函数。

    挂上指示器

    我们得检查一下指示器是不是正常工作。到 GameScene.swift 文件,在文件的上方,foodNode 变量下边添加一行代码:

    private let hudNode = HudNode()
    

    sceneDidLoad() 函数内部的上方,添加下面两行代码:

    hudNode.setup(size: size)
    addChild(hudNode)
    

    接着,在 spawnCat() 函数,重置所有点防止猫从屏幕上掉下去。在把猫精灵加到场景的后面,加上这行代码:

    hudNode.resetPoints()
    

    接下来,在 handleCatCollision(contact:) 函数中,当猫被雨淋到时,我们也需要重置分数。在函数最后,switch 语句的 RainDropCategory 分支里,加上下面这行代码:

    hudNode.resetPoints()
    

    最后,我们得告诉计分板,什么时候用户得了分。在 handleFoodHit(contact:) 文件的最后,找到下面这几行代码:

    //TODO increment points
    print("fed cat")
    

    换成这个:

    hudNode.addPoint()
    

    以上!

    HUD unlocked!

    当来回收集食物时,你就会看到指示器的效果了。第一次收集食物的时候,你应该会看到分数变黄然后比例变大,如果你看到当猫淋到雨滴时,分数重置,那么你就是正确的!

    High Score!

    下一个场景

    没错,我们要开始下一个场景了!事实上,如果这个场景完成,它将会作为我们游戏的首屏展示。在做其他事情之前,打开 Constants.swift 然后添加下面这行代码到文件的底部 — 我们用它来检索以及保持高分:

    let ScoreKey = "RAINCAT_HIGHSCORE"
    

    创建一个新场景,把它放到 “Scenes” 文件夹里,然后命名为 MenuScene.swift。把下面的代码加进去:

    import SpriteKit
    
    class MenuScene: SKScene {
      let startButtonTexture = SKTexture(imageNamed: "button_start")
      let startButtonPressedTexture = SKTexture(imageNamed: "button_start_pressed")
      let soundButtonTexture = SKTexture(imageNamed: "speaker_on")
      let soundButtonTextureOff = SKTexture(imageNamed: "speaker_off")
    
      let logoSprite = SKSpriteNode(imageNamed: "logo")
      var startButton: SKSpriteNode! = nil
      var soundButton: SKSpriteNode! = nil
    
      let highScoreNode = SKLabelNode(fontNamed: "PixelDigivolve")
    
      var selectedButton: SKSpriteNode?
    
      override func sceneDidLoad() {
        backgroundColor = SKColor(red: 0.30, green: 0.81, blue: 0.89, alpha: 1.0)
        
        //Set up logo - sprite initialized earlier
        logoSprite.position = CGPoint(x: size.width/2, y: size.height/2 + 100)
        
        addChild(logoSprite)
        
        //Set up start button
        startButton = SKSpriteNode(texture: startButtonTexture)
        startButton.position = CGPoint(x: size.width/2, y: size.height/2 - startButton.size.height/2)
        
        addChild(startButton)
        
        let edgeMargin: CGFloat = 25
        
        //Set up sound button
        soundButton = SKSpriteNode(texture: soundButtonTexture)
        soundButton.position = CGPoint(x: size.width - soundButton.size.width/2 - edgeMargin, y: soundButton.size.height/2 + edgeMargin)
        
        addChild(soundButton)
        
        //Set up high-score node
        let defaults = UserDefaults.standard
        
        let highScore = defaults.integer(forKey: ScoreKey)
        
        highScoreNode.text = "\(highScore)"
        highScoreNode.fontSize = 90
        highScoreNode.verticalAlignmentMode = .top
        highScoreNode.position = CGPoint(x: size.width /2, y: startButton.position.y - startButton.size.height/2 - 50)
        highScoreNode.zPosition = 1
        
        addChild(highScoreNode)
      }
    }
      
    

    因为这个场景真的很简单。所以我们不会创建任何特殊的类。我们的场景将只由两个按钮组成。这两个按钮可以(或者说应该)拥有自己的 SKSpriteNodes 类,但是因为他们都不一样,所以我不会为他们创建新的类。在构建属于你自己的游戏的时候,这是很重要的一点:在事情变得复杂时,你需要有能力来判断,在哪里停下来并重构代码。一旦你添加了三个或四个以上的按钮到游戏里,那可能就是时候停下来把菜单按钮放到他们自己的类里了。

    上面的代码没做什么特别的事儿;只是设置了四个精灵的坐标。当然我们也设置了场景的背景颜色,所以整个背景的值也是正确的。UI Color 是一个从十六进制串(HEX strings)生成 Xcode 颜色代码的优秀工具。上面的代码还设置了按钮状态的纹理。开始按钮有一个正常状态和一个按下的状态,而声音按钮则是一个开关。为了让开关简单点,在玩家点击时,我们改变声音按钮上的透明度。当然我们也设置了获得高分的 SKLabelNode

    我们的 MenuScene 看起来不错。现在,在游戏加载时需要展示场景。到 GameViewController.swift 文件,找到下面这行代码:

    let sceneNode = GameScene(size: view.frame.size)
    

    把它换成这个:

    let sceneNode = MenuScene(size: view.frame.size)
    

    这个小改动会默认加载 MenuScene 场景,而不是 GameScene

    我们新的场景!

    按钮的状态

    按钮在 SpriteKit 中可能有些麻烦。有丰富的轮子可以用(我甚至还自己做了一个),但是理论上,你只需要理解这三个函数:

    • touchesBegan(_ touches: with event:)
    • touchesMoved(_ touches: with event:)
    • touchesEnded(_ touches: with event:)

    在更新伞的时候我们简单提了几句,但是现在我们需要知道接下来的几点:哪个按钮被触摸,玩家是松开按钮还是点击按钮,按钮是不是一直被按着。这个时候就需要 selectedButton 变量发挥它的作用了。在触摸开始时,我们就可以通过这个变量来捕获被按的按钮。如果他们拖拽按钮,我们就可以处理并适当的给它一些纹理。在松开按钮时,我们也可以知道他们是否还跟按钮有接触,如果有接触,那就可以提供一些相关联的动作。把下面这些代码添加到 MenuScene.swift 的底部:

      override func touchesBegan(_ touches: Set, with event: UIEvent?) {
        if let touch = touches.first {
          if selectedButton != nil {
            handleStartButtonHover(isHovering: false)
            handleSoundButtonHover(isHovering: false)
          }
    
          // Check which button was clicked (if any)
          if startButton.contains(touch.location(in: self)) {
            selectedButton = startButton
            handleStartButtonHover(isHovering: true)
          } else if soundButton.contains(touch.location(in: self)) {
            selectedButton = soundButton
            handleSoundButtonHover(isHovering: true)
          }
        }
      }
    
      override func touchesMoved(_ touches: Set, with event: UIEvent?) {
        if let touch = touches.first {
        
          // Check which button was clicked (if any)
          if selectedButton == startButton {
            handleStartButtonHover(isHovering:(startButton.contains(touch.location(in: self))))
          } else if selectedButton == soundButton {
            handleSoundButtonHover(isHovering:(soundButton.contains(touch.location(in: self))))
          }
        }
      }
    
      override func touchesEnded(_ touches: Set, with event: UIEvent?) {
        if let touch = touches.first {
        
          if selectedButton == startButton {  
            // Start button clicked
            handleStartButtonHover(isHovering: false)
            
            if (startButton.contains(touch.location(in: self))) {
              handleStartButtonClick()
            }
            
          } else if selectedButton == soundButton {
            // Sound button clicked
            handleSoundButtonHover(isHovering: false)
              
            if (soundButton.contains(touch.location(in: self))) {
              handleSoundButtonClick()
            }
          }
        }
    
        selectedButton = nil
      }
      
      /// Handles start button hover behavior
      func handleStartButtonHover(isHovering: Bool) {
        if isHovering {
          startButton.texture = startButtonPressedTexture
        } else {
          startButton.texture = startButtonTexture
        }
      }
      
      /// Handles sound button hover behavior
      func handleSoundButtonHover(isHovering: Bool) {
        if isHovering {
          soundButton.alpha =0.5
        }else{
          soundButton.alpha =1.0
        }
      }
      
      /// Stubbed out start button on click method
      func handleStartButtonClick() {
        print("start clicked")
      }
      
      /// Stubbed out sound button on click method
      func handleSoundButtonClick() {
        print("sound clicked")
      }
    

    这就是对我们两个按钮的简单处理。在 touchesBegan(_ touches: with events:) 里,我们首先检查当前是否有按钮被选中。如果我们要做这个检查,我们就要得先重置按钮到没有被按下的状态,然后,检查是否有哪个按钮被按下。如果有被按下的按钮,就显示它的高亮状态,接下来,我们就在其他两个方法里设置按钮的 selectedButton 属性以供使用。

    touchesMoved(_ touches: with events:) 方法中,我们检查最初触摸的是哪个按钮。接着,检查当前触摸是否还在 selectedButton 的边界内,如果还在,就更新按钮的状态为高亮。startButton 的高亮状态是改变按下的纹理,而 soundButton 的高亮状态是把精灵的透明度设置为 50%。

    最后,在 touchesEnded(_ touches: with event:) 方法里,我们再次检查哪个按钮被选中,如果有,接着检查这个触摸时候还在按钮的边界内,如果前面的条件都满足,那么我们根据不同的按钮调用 handleStartButtonClick() 或者 handleSoundButtonClick()

    按钮的动作

    现在,我们已经搞定了按钮的基础行为,在按钮被点击的时候,我们还需要一个触发事件。对于 startButton 来说,这个实现很容易。我们只需要在点击时展示 GameScene。在 MenuScene.swift 文件里,更新 handleStartButtonClick() 方法里面的代码:

    func handleStartButtonClick() {
      let transition = SKTransition.reveal(with: .down, duration: 0.75)
      let gameScene = GameScene(size: size)
      gameScene.scaleMode = scaleMode
      view?.presentScene(gameScene, transition: transition)
    }
    

    如果你现在运行程序,然后点击按钮,游戏就开始了!

    接着,我们需要一个静音的切换。我们已经有一个音乐管理器了,但是我们需要告诉它静音是否开启。我们需要在 Constants.swift 里添加一个 key 来持久化存储静音状态。添加下面这行代码:

    let MuteKey = "RAINCAT_MUTED"
    

    用它把一个布尔类型的值保存到 UserDefaults 里。现在这里已经设置完了,我们到 SoundManager.swift 文件中。我们在这里通过检查和设置 UserDefaults 来确定静音的开关。在文件的顶部,trackPosition 变量的下面,加上这行代码:

    private(set) var isMuted = false
    

    这个变量用于主菜单(或者其他要播放声音的地方)检查是否允许播放声音。我们给他设置一个 false 的初始值,但首先我们需要检查 UserDefaults 里,来看看玩家是怎样设置的。把 init() 方法换成下面的代码:

    private override init() {
      //This is private, so you can only have one Sound Manager ever.
      trackPosition = Int(arc4random_uniform(UInt32(SoundManager.tracks.count)))
        
      let defaults = UserDefaults.standard
        
      isMuted = defaults.bool(forKey: MuteKey)
    }
    

    做完这些,我们的 isMuted 就有默认值了,我们还需要它能够切换。在 SoundManager.swift 文件里的底部,加入这些代码:

    func toggleMute() -> Bool {
      isMuted = !isMuted
        
      let defaults = UserDefaults.standard
      defaults.set(isMuted, forKey: MuteKey)
      defaults.synchronize()
            
      if isMuted {
        audioPlayer?.stop()
      } else {
        startPlaying()
      }
          
      return isMuted
    }
    

    UserDefaults 更新时,这个方法会切换我们的静音变量,如果新的值不是静音,那音乐就会开始播放;如果新的值是静音,那音乐就不会开始。此外,我们还会停止播放当前的音乐。做完这些,我们还需要修改一下 startPlaying() 里的 if 语句。

    找到下面的代码:

    if audioPlayer == nil || audioPlayer?.isPlaying == false {
    

    换成这行:

    if !isMuted && (audioPlayer == nil || audioPlayer?.isPlaying == false) {
    

    现在,在静音被关闭时,无论是播放器没有设置,还是当前播放停止了,我们都会继续播放音乐。

    从这开始,我们就该完成 MenuScene.swift 的静音按钮了。把 handleSoundbuttonClick() 方法换成下面的代码:

    func handleSoundButtonClick() {
      if SoundManager.sharedInstance.toggleMute(){
        //Is muted
        soundButton.texture = soundButtonTextureOff
      } else {
        //Is not muted
        soundButton.texture = soundButtonTexture
      }
    }
    

    这里切换了在 SoundManager 的声音,检查结果,接着稍微改变了一下纹理,来告诉玩家音乐是否静音。我们马上就要完成了!只剩下在游戏启动时候,设置按钮的初始纹理。在 sceneDidLoad(),找到这行代码:

    soundButton = SKSpriteNode(texture: soundButtonTexture)
    

    替换成下面的:

    soundButton = SKSpriteNode(texture: SoundManager.sharedInstance.isMuted ?
    soundButtonTextureOff : soundButtonTexture)
    

    上面的例子使用了 ternary operator 来设置正确的纹理。

    音乐这部分处理已经完成了,我们到 CatSprite.swift 文件,让小猫在静音的时候不能喵喵叫。在 hitByRain() 方法,删除散步动作后,添加下面的这行 if 语句:

    if SoundManager.sharedInstance.isMuted { return }
    

    这条语句会判断游戏是否静音,如果是就返回。这样,我们就可以忽略 currentRainHitsmaxRainHits 和喵喵声的效果了。

    所有的这些都弄完之后,是时候来试试静音按钮的效果了。运行游戏,确定是否在播放音乐。关闭音乐,然后重启游戏。确定游戏还是静音的。需要注意的一点是,如果你只是开启静音并用 Xcode 重启游戏,那可能没有足够的时间来向 UserDefaults 存储静音变量。玩一下游戏,确认在静音的时候猫不会喵喵的叫。

    测试按钮效果

    退出游戏

    现在为止,我们已经弄完了主菜单的第一种按钮,我们可以通过添加按钮,来为场景处理一些棘手的业务了。一些有趣的交互可以展示出我们游戏的风格;现在,雨伞会随着玩家的触摸而移动到相应的位置。显然,在玩家要退出游戏的时候,雨伞也会移动过去,这肯定是个糟糕的用户体验,所以我们要阻止它发生。

    我们会模仿前面添加的开始按钮来实现退出按钮,其中大部分过程都不会变。改变的地方在处理触摸这部分。把你的 quit_buttonquit_button_pressed 资源放进 Assets.xcassets 文件夹里,然后把下面的代码添加到 HudNode.swift 文件中:

    private var quitButton: SKSpriteNode!
    private let quitButtonTexture = SKTexture(imageNamed: "quit_button")
    private let quitButtonPressedTexture = SKTexture(imageNamed: "quit_button_pressed")
    

    这些变量会处理我们的 quitButton 引用,并且会根据退出按钮的不同状态来设置纹理。为了确保不在退出游戏的时候,不小心更新雨伞对象,我们还需要一个变量来告诉指示器(和游戏场景),我们只是和退出按钮交互,而不是雨伞。把下面的代码添加到 showingHighScore 变量后面:

    private(set) var quitButtonPressed = false
    

    同样的,这是一个只有在 HudNode 中才能修改,而其他类只能查看的变量。现在变量已经设置好了,我们可以添加按钮到指示器了。把下面的代码添加到 setup(size:) 方法中:

    quitButton = SKSpriteNode(texture: quitButtonTexture)
    let margin: CGFloat = 15
    quitButton.position = CGPoint(x: size.width - quitButton.size.width - margin, y: size.height - quitButton.size.height - margin)
    quitButton.zPosition = 1000
      
    addChild(quitButton)
    

    上面的代码会设置退出按钮没被按下状态的纹理。我们也把它的位置设到了右上角,并且把 zPosition 的值设置的很高,来让它一直显示在最前面。如果你现在运行游戏,他就会显示在 GameScene 里,不过还不能点。

    Quit button

    现在按钮已经定位,我们还要能够和它交互。在 GameScene 中,唯一有交互的地方就是和 umbrellaSprite 的交互。在我们的例子里,指示器的优先级比伞高,所以玩家在退出时,不用特意把伞移走。我们可以在 HudNode.swift 里创建一些相同的方法来模仿 GameScene.swift 里的触摸功能。在 HudNode.swift 文件加入下面的代码:

    func touchBeganAtPoint(point: CGPoint) {
      let containsPoint = quitButton.contains(point)
    
      if quitButtonPressed && !containsPoint {
        //Cancel the last click
        quitButtonPressed = false
        quitButton.texture = quitButtonTexture
      } else if containsPoint {
        quitButton.texture = quitButtonPressedTexture
        quitButtonPressed = true
      }
    }
    
    func touchMovedToPoint(point: CGPoint) {
      if quitButtonPressed {
        if quitButton.contains(point) {
          quitButton.texture = quitButtonPressedTexture
        } else {
          quitButton.texture = quitButtonTexture
        }
      }
    }
    
    func touchEndedAtPoint(point: CGPoint) {
      if quitButton.contains(point) {
        //TODO tell the gamescene to quit the game
      }
    
      quitButton.texture = quitButtonTexture
    }
    

    上面的代码大部分和 MenuScene 创建的差不多。不同的地方是,只需要跟踪一个按钮的状态,所以我们可以在这些方法里处理所有的事情。而且,我们还知道 GameScene 里的触摸点的位置,这样就可以检查我们的按钮是否包含触摸点。

    移动到 GameScene.swift, 并用下面的代码替换 touchesBegan(_ touches with event:)touchesMoved(_ touches: with event:)

    override func touchesBegan(_ touches: Set, with event: UIEvent?) {
      let touchPoint = touches.first?.location(in: self)
    
      if let point = touchPoint {
        hudNode.touchBeganAtPoint(point: point)
    
        if !hudNode.quitButtonPressed {
          umbrellaNode.setDestination(destination: point)
        }
      }
    }
    
    override func touchesMoved(_ touches: Set, with event: UIEvent?) {
      let touchPoint = touches.first?.location(in: self)
    
      if let point = touchPoint {
        hudNode.touchMovedToPoint(point: point)
    
        if !hudNode.quitButtonPressed {
          umbrellaNode.setDestination(destination: point)
        }
      }
    }
    
    override func touchesEnded(_ touches: Set, with event: UIEvent?) {
      let touchPoint = touches.first?.location(in: self)
    
      if let point = touchPoint {
        hudNode.touchEndedAtPoint(point: point)
      }
    }
    

    这里,每个方法以几乎相同的方式处理一切。我们告诉指示器玩家和场景交互。然后,检查退出按钮当前是否在捕捉触摸。如果它没有捕捉触摸,那我们就移动伞。我们还在 touchesEnded(_ touches: with event:) 方法里添加了点击退出按钮结束的处理,但我们还是没有使用到 umbrellaSprite

    退出游戏

    我们有个按钮了,现在我们需要一种方式来作用于 GameScene。把下面这行代码添加到 HudeNode.swift 的顶部:

      var quitButtonAction: (()->())?
    

    这是一个基本的闭包,没有参数也没返回值。我们会在 GameScene.swift 文件里设置它,在点击 HudNode.swift 里的按钮时候调用。接着,我们就可以用下面的代码,来替换以前在 touchEndedAtPoint(point:) 里面创建的 TODO 部分:

    if quitButton.contains(point)&& quitButtonAction != nil {
        quitButtonAction!()
    }
    

    现在如果我们设置了 quitButtonAction 闭包,它就会在这被调用。

    要设置 quitButtonAction 闭包,我们就要到 GameScene.swift 文件里。在 sceneDidLoad() 函数,把设置指示器的代码换成下面的:

    hudNode.setup(size: size)
        
    hudNode.quitButtonAction = {
      let transition = SKTransition.reveal(with: .up, duration: 0.75)
        
      let gameScene = MenuScene(size: self.size)
      gameScene.scaleMode = self.scaleMode
        
      self.view?.presentScene(gameScene, transition: transition)
        
      self.hudNode.quitButtonAction = nil
    }
        
    addChild(hudNode)
    

    运行程序,点击开始游戏,然后点退出按钮。如果你回到了主菜单,那说明退出按钮和预期的一样。在闭包里,我们创建并初始化了一个到 MenuScene 的过渡。我们还把这个闭包设置为 HUD 的节点,当点击退出按钮时运行闭包。这里,另一行重要的代码是我们把 quitButtonAction 设为 nil。这么做的原因是有一个循环引用产生了。场景持有一个指示器的引用,而指示器也持有一个场景的引用。因为他们两个互相引用,导致在垃圾回收的时候,他们都不会被处理。这种情形下,每次我们进入和离开 GameScene 的时候,都会有一个新的实例被创建,并且从来都不释放。这对性能有严重的影响,游戏最后一定会内存爆炸。有很多种方式来避免它,但在我们这里,只是从指示器中移除对 GameScene 的引用,这样在我们回到 MenuScene 的时候,场景和指示器都会被终止。对于引用类型和如何避免循环引用,Krakendev 有一些更深的见解

    现在,到 GameViewController.swift 文件,把下面的这几行代码注掉或者删除:

    view.showsPhysics = true
    view.showsFPS = true
    view.showsNodeCount = true
    

    把调试信息去掉以后,游戏看起来真的很不错!恭喜你:我们已经现在进入 beta 版了!在 GitHub 上找到今天的最终代码。

    最后的思考

    这是三遍教程的最后一篇,如果你一直跟着到这,那你已经对你的游戏付出了很多工作。在本教程中,你把一个一无所有的场景,变成了一个完整的游戏。恭喜!在第一课里,我们添加了地面,雨滴,背景和雨伞精灵。我们还通过物理引擎来确保雨滴没有堆积在一起。我们用碰撞检测来移除节点,这样就解决了内存溢出的问题。我们也添加了一些交互来允许伞向玩家触摸屏幕的位置移动。

    第二课里,我们添加了猫和食物,为他们定制了一些不同的生成方法。我们还更新了碰撞检测,让猫精灵和食物精灵产生一些作用。我们也在猫的移动上做了一些处理。小猫有一个目的:吃掉每一个食物。我们为猫添加了简单的动画效果,还增加了猫和雨滴之间的交互。最后,我们添加了音效和背景音乐,让我们的程序看上去更像一个完整的游戏。

    在这最后的一篇教程里,我们创建了一个指示器放我们的分数标签和退出按钮。我们处理节点上的操作,并使用户能够从指示器节点的回调里退出。我们还添加了一个玩家启动游戏的场景,并可以在点击退出按钮后返回。我们还处理了开始游戏和控制游戏中的声音的过程。

    接下来做什么

    我们做到这一步用了很久,但这个游戏还有许多工作需要继续。RainCat 也会继续发展,而且它已经可以在 App Store 下载了。下面的列表是一些想要加的和需要加的功能。有一些已经加上了,还有一些待定中:

    • 添加 icon 图标和启动画面。
    • 完成主菜单(教程的是简化版)。
    • 修复 bug,包括烦人的雨滴和多重食物的生成。
    • 重构并优化代码。
    • 根据得分更改游戏的调色板。
    • 根据得分更新难度。
    • 当食物在猫的正上方,让猫有一些动作。
    • 集成 Game Center。
    • 标明出处(包括一些适当的音乐曲目)。

    请持续关注 GitHub,因为在不久的将来这些都会被实现。如果你对代码有任何的问题,随时可以在 hello@thirteen23.com 给我们留言,我们可以一起讨论它。如果问题有足够的关注,那也许我们会专门写一篇文章来探讨这些问题。

    感谢!

    我真的很感谢所有那些,在制作游戏和写文章的过程中,与之相伴的人。

    提供了游戏最初的美术,设计和编辑,并且在 Garage 发布了文章。

    提供了游戏最终菜单的设计和调色板(如果我实现了这些,效果肯定酷炫 — 敬请期待)。

    提供了文章中漂亮的标题和分割符,并且帮助编写文章。

    提供了三篇文章里所有漂亮的 GIF 图片,还很友好的把小猫的 GIF 也发给了我。

    提供了编辑文章的帮助,如果没有他,这个系列可能都不会出现。

    提供了编辑文章的帮助,这的确是一项大工程。

    提供了第三课的编辑工作和乒乓球,很多的乒乓球(译者注:这里原文就是ping-pong,译者的理解是,可能他们写代码有点累,所以打了会乒乓球。)

    正因为这些帮助,教程才会像预计的那样完成。

    认真的说,真的用了一大堆人来准备这篇文章,并发布到商店。

    也谢谢每一位读到这句话的读者,感谢。

    相关文章

      网友评论

          本文标题:如何在 Swift 3 中用 SpriteKit 框架编写游戏

          本文链接:https://www.haomeiwen.com/subject/bgsxtltx.html