如何在 Swift 3 中构建 SpriteKit 游戏(第 3 部分)

已发表: 2022-03-10
快速总结↬你有没有想过创建一个 SpriteKit 游戏需要什么? 按钮看起来比它们应该做的任务更大吗? 有没有想过如何在游戏中保留设置? 自从引入 SpriteKit 以来,iOS 上的游戏制作从未如此简单。 在这个由三部分组成的系列的第三部分中,我们将完成我们的 RainCat 游戏并完成对 SpriteKit 的介绍。 如果您错过了上一课,您可以通过在 GitHub 上获取代码来赶上进度。 请记住,本教程需要 Xcode 8 和 Swift 3。

你有没有想过创建一个 SpriteKit 游戏需要什么? 按钮看起来比它们应该做的任务更大吗? 有没有想过如何在游戏中保留设置? 自从引入 SpriteKit 以来,iOS 上的游戏制作从未如此简单。 在这个由三部分组成的系列的第三部分中,我们将完成我们的 RainCat 游戏并完成对 SpriteKit 的介绍。

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

SmashingMag 的进一步阅读:链接

  • 游戏化和用户体验:用户赢或输的地方
  • 有趣的用户体验设计:打造更好的游戏
  • 结合用户体验设计和心理学来改变用户行为
雨猫,第 3 课
雨猫,第 3 课
跳跃后更多! 继续往下看↓

这是我们 RainCat 之旅的第三课。 在上一课中,我们度过了漫长的一天,学习了一些简单的动画、猫的行为、快速的音效和背景音乐。

今天我们将重点关注以下内容:

  • 用于评分的平视显示器(HUD);
  • 主菜单——带按钮;
  • 静音选项;
  • 游戏退出选项。

更多资产

最后一课的资源可在 GitHub 上找到。 就像我们在之前的课程中所做的那样,再次将图像拖到Assets.xcassets中。

小心!

我们需要一种方法来保持得分。 为此,我们可以创建一个平视显示器 (HUD)。 这将非常简单; 它将是一个包含分数和退出游戏按钮的SKNode 。 现在,我们将只关注分数。 我们将使用的字体是 Pixel Digivolve,您可以从 Dafont.com 获得。 与使用不属于您的图像或声音一样,请在使用之前阅读字体的许可证。 这个声明它是免费供个人使用的,但如果你真的喜欢这种字体,你可以从页面捐赠给作者。 你不能总是自己做所有的事情,所以回馈那些一路帮助你的人是件好事。

接下来,我们需要将自定义字体添加到项目中。 这个过程第一次可能会很棘手。

下载字体并将其移动到项目文件夹中的“字体”文件夹下。 我们在之前的课程中已经做过几次了,所以我们将更快地完成这个过程。 将一个名为Fonts的组添加到项目中,并添加Pixel digivolve.otf文件。

现在是棘手的部分。 如果您错过了这部分,您可能无法使用该字体。 我们需要将它添加到我们的Info.plist文件中。 该文件位于 Xcode 的左侧窗格中。 单击它,您将看到属性列表(或plist )。 右键单击列表,然后单击“添加行”。

添加行
plist添加一行。

当新行出现时,输入以下内容:

 Fonts provided by application

然后,在Item 0下,我们需要添加我们的字体名称。 plist应如下所示:

像素digivolve.otf
字体添加到plist成功!

字体应该可以使用了! 我们应该做一个快速测试以确保它按预期工作。 移动到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)

它有效吗?

你好世界!
测试我们的SKLabelNode 。 不好了! “Hello World”标签又回来了。

如果它有效,那么你已经正确地完成了一切。 如果不是,那就有问题了。 Code With Chris 有更深入的故障排除指南,但请注意,它适用于旧版本的 Swift,因此您必须进行一些小的调整才能将其升级到 Swift 3。

现在我们可以加载自定义字体,我们可以从 HUD 开始。 删除“Hello World”标签,因为我们只使用它来确保我们的字体加载。 HUD 将是一个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()中都被调用。

连接 HUD

我们需要测试我们的 HUD 是否正常工作。 移动到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解锁!
HUD解锁!

您应该看到 HUD 正在运行。 跑来跑去收集一些食物。 第一次收集食物时,您应该会看到分数变黄并扩大规模。 当你看到这种情况发生时,让猫受到打击。 如果分数重置,那么你就会知道你在正确的轨道上!

高分!
有史以来的最高分(……在撰写本文时)!

下一个场景

没错,我们正在转移到另一个场景! 事实上,完成后,这将是我们应用程序的第一个屏幕。 在你做任何其他事情之前,打开Constants.swift并将以下行添加到文件的底部——我们将使用它来检索和保存高分:

 let ScoreKey = "RAINCAT_HIGHSCORE"

创建新场景,将其放在“Scenes”文件夹下,并将其命名为MenuScene.swift 。 在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类,但由于它们足够不同,我们不需要为它们创建新类。 这是您构建自己的游戏时的重要提示:当事情变得复杂时,您需要能够确定在哪里停止和重构代码。 一旦您在游戏中添加了三个或四个以上的按钮,可能是时候停止并将菜单按钮的代码重构到它自己的类中了。

上面的代码没有做任何特别的事情; 它正在设置四个精灵的位置。 我们还设置了场景的背景颜色,以便整个背景都是正确的值。 从 Xcode 的 HEX 字符串生成颜色代码的好工具是 UI Color。 上面的代码还为我们的按钮状态设置纹理。 开始游戏的按钮有正常状态和按下状态,而声音按钮是切换按钮。 为了简化切换,我们将在用户按下时更改声音按钮的 alpha 值。 我们也在拉取和设置高分SKLabelNode

我们的MenuScene看起来不错。 现在我们需要在应用加载时显示场景。 移动到GameViewController.swift并找到以下行:

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

用这个替换它:

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

这个小改动将默认加载MenuScene ,而不是GameScene

我们的新场景!
我们的新场景! 注意每秒 1.0 帧:没有任何东西在移动,所以不需要更新任何东西。

按钮状态

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的高亮状态将精灵的 alpha 值设置为 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中,我们需要添加一个键以在静音打开时持久化。 添加以下行:

 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)

上面的示例使用三元运算符来设置正确的纹理。

现在音乐已经连接,我们可以移动到CatSprite.swift以在静音打开时禁用猫喵喵叫。 在hitByRain()中,我们可以在删除 walk 动作后添加以下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引用,以及我们将为按钮状态设置的纹理。 为了确保我们在尝试退出时不会无意中更新伞,我们需要一个变量来告诉 HUD(和游戏场景)我们正在与退出按钮而不是伞进行交互。 在showingHighScore布尔变量下面添加以下代码:

 private(set) var quitButtonPressed = false

同样,这是一个只有HudNode可以设置但其他类可以检查的变量。 现在我们的变量已经设置好了,我们可以将按钮添加到 HUD 中。 将以下代码添加到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中,但还不能点击。

退出按钮
请注意我们 HUD 中的新退出按钮。

现在按钮已经定位,我们需要能够与它进行交互。 现在,我们在GameScene中进行交互的唯一地方是我们与umbrellaSprite精灵交互的时候。 在我们的示例中,HUD 将优先于雨伞,因此用户不必为了退出而将雨伞移开。 我们可以在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) } }

在这里,每种方法都以几乎相同的方式处理所有内容。 我们告诉 HUD 用户已经与场景进行了交互。 然后,我们检查退出按钮当前是否正在捕获触摸。 如果不是,那么我们移动伞。 我们还添加了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()中,我们可以用以下代码替换我们的 HUD 设置:

 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时。 其原因是正在发生保留周期。 场景持有对 HUD 的引用,而 HUD 持有对场景的引用。 因为对这两个对象都有引用,所以在垃圾收集时两者都不会被丢弃。 在这种情况下,每次我们进入和离开GameScene时,都会创建另一个实例并且永远不会释放。 这对性能不利,应用程序最终会耗尽内存。 有很多方法可以避免这种情况,但在我们的例子中,我们可以从 HUD 中删除对GameScene的引用,一旦我们返回MenuScene ,场景和 HUD 将被终止。 Krakendev 对引用类型以及如何避免这些循环有更深入的解释。

现在,移动到GameViewController.swift ,删除或注释掉以下三行代码:

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

排除了调试数据后,游戏看起来非常棒! 恭喜:我们目前处于测试阶段! 从今天开始在 GitHub 上查看最终代码。

最后的想法

这是一个由三部分组成的教程的最后一课,如果你做到了这一步,你就在你的游戏上做了很多工作。 在本教程中,您从一个完全没有任何内容的场景变成了一个完整的游戏。 恭喜! 在第一课中,我们添加了地板、雨滴、背景和雨伞精灵。 我们还玩弄了物理,并确保我们的雨滴不会堆积。 我们从碰撞检测开始,并致力于剔除节点,这样我们就不会耗尽内存。 我们还通过允许雨伞向用户在屏幕上触摸的位置移动来添加一些用户交互。

在第二课中,我们添加了猫和食物,以及它们各自的自定义生成方法。 我们更新了碰撞检测以允许猫和食物精灵。 我们还研究了猫的运动。 这只猫获得了一个目标:吃掉所有可用的食物。 我们为猫添加了简单的动画,并在猫和雨之间添加了自定义交互。 最后,我们添加了音效和音乐,让它感觉像是一个完整的游戏。

在最后一课中,我们创建了一个平视显示器来保存我们的乐谱标签,以及我们的退出按钮。 我们处理了跨节点的操作,并允许用户通过来自 HUD 节点的回调退出。 我们还添加了另一个场景,用户可以启动并在单击退出按钮后返回。 我们处理了启动游戏和控制游戏声音的过程。

从这往哪儿走

我们投入了很多时间来做到这一点,但是这款游戏还有很多工作要做。 RainCat 仍在继续开发,并在 App Store 中提供。 以下是需要和需要添加的列表。 一些项目已添加,而其他项目仍在等待中:

  • 添加图标和启动画面。
  • 完成主菜单(为教程简化)。
  • 修复错误,包括流氓雨滴和多个食物产卵。
  • 重构和优化代码。
  • 根据分数更改游戏的调色板。
  • 根据分数更新难度。
  • 当食物就在猫的正上方时,为猫设置动画。
  • 集成游戏中心。
  • 给予信用(包括对音乐曲目的适当信用)。

在 GitHub 上跟踪,因为这些更改将在未来进行。 如果您对代码有任何疑问,请随时通过 [email protected] 给我们留言,我们可以进行讨论。 如果某些主题得到足够的关注,也许我们可以写另一篇讨论该主题的文章。

谢谢!

我要感谢所有在创建游戏和开发相关文章的过程中提供帮助的人。

  • Cathryn Rowe 用于最初的艺术、设计和编辑,以及在我们的车库中发布文章。
  • Morgan Wheaton 对于最终的菜单设计和调色板(一旦我真正实现了这些功能,它们看起来会很棒——敬请期待)。
  • Nikki Clark 对于文章中令人敬畏的标题和分隔符以及编辑文章的帮助。
  • Laura Levisay 感谢文章中所有精彩的 GIF,以及向我发送可爱的猫咪 GIF 以获得精神上的支持。
  • 汤姆·哈德森(Tom Hudson)帮助编辑文章,如果没有他,这个系列根本就不会制作。
  • Lani DeGuire 寻求帮助编辑文章,这是一项繁重的工作。
  • Jeff Moon 帮助编辑第三课和乒乓球。 很多乒乓球。
  • Tom Nelson 帮助确保教程按预期工作。

说真的,花了很多人来为这篇文章准备好一切并将其发布到商店。

也感谢所有读到这句话的人。