Swift-游戏开发-全-
Swift 游戏开发(全)
原文:
zh.annas-archive.org/md5/d0174909c3206c7de05881da7a3ac0c9译者:飞龙
前言
现在从未有过更好的时机成为游戏开发者。App Store 为你提供了一个独特的机会,将你的想法传播给庞大的受众。现在,Swift 已经到来,增强了我们的工具集,并提供了更流畅的开发体验。Swift 虽然是新的,但已经被誉为一种优秀、设计精良的语言。无论你是游戏开发的新手还是想增加你的专业知识,我相信你会喜欢用 Swift 制作游戏。
我写这本书的目标是分享 Swift 和 SpriteKit 的基础知识。我们将通过一个完整的示例游戏来学习 Swift 开发过程的每一步。一旦你完成这本书,你将能够轻松地设计并发布自己的游戏想法到 App Store,从开始到结束。
请提出任何问题并与我们分享你的游戏创作:
电子邮件:<stephen@thinkingswiftly.com>
Twitter: @sdothaney
第一章探讨了 Swift 的最佳特性。让我们开始吧!
本书涵盖的内容
第一章,使用 Swift 设计游戏,介绍了 Swift 的最佳特性,帮助你设置开发环境,并启动你的第一个 SpriteKit 项目。
第二章,精灵、摄像机、动作!,教你使用 Swift 绘制和动画的基础知识。你将绘制精灵,将纹理导入到你的项目中,并将摄像机对准主要角色。
第三章,混合物理,涵盖了物理模拟的基本原理:物理体、冲量、力、重力、碰撞等。
第四章,添加控制,探讨了移动游戏控制的多种方法:设备倾斜和触摸输入。我们还将改进示例游戏中的摄像机和核心玩法。
第五章,生成敌人、金币和道具,介绍了我们将在示例游戏中使用的角色阵容,并展示了如何为每种 NPC 类型创建自定义类。
第六章,生成一个永不结束的世界,探讨了 SpriteKit 场景编辑器,为示例游戏构建遭遇,并创建了一个无限循环遭遇的系统。
第七章,实现碰撞事件,深入探讨了高级物理模拟主题,并在精灵碰撞时添加自定义事件。
第八章,精益求精 – HUD、视差背景、粒子等,添加了使每个优秀游戏发光的额外功能。创建视差背景,了解 SpriteKit 的粒子发射器,并将抬头显示叠加到你的游戏中。
第九章, 添加菜单和声音,构建了一个基本的菜单系统,并说明了在您的游戏中播放声音的两种方法。
第十章, 与游戏中心集成,将我们的示例游戏链接到苹果游戏中心,用于排行榜、成就和友好挑战。
第十一章, 发布!准备 App Store 和发布,涵盖了打包您的游戏并将其提交到 App Store 的基本要素。
您需要为本书准备什么
本书使用 Xcode IDE 版本 6.3.2(Swift 1.2)。如果您使用的是 Xcode 的不同版本,您可能会遇到语法差异;苹果公司不断升级 Swift 的语法。
访问developer.apple.com/xcode下载 Xcode。
您需要一个苹果开发者账户来将您的应用程序集成到游戏中心,并将您的游戏提交到 App Store。
本书面向对象
如果您想使用 Swift 创建和发布有趣的 iOS 游戏,那么这本书就是为您准备的。您应该熟悉基本编程概念,如类、类型和函数。然而,不需要先前的游戏开发或苹果生态系统经验。此外,有经验的游戏程序员会发现这本书在过渡到使用 Swift 进行游戏开发时很有用。
惯例
在这本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名如下所示:"游戏在切换到这个场景时调用didMoveToView函数。"
代码块如下设置:
let mySprite = SKSpriteNode(color: UIColor.blueColor(), size:
CGSize(width: 50, height: 50))
mySprite.position = CGPoint(x: 300, y: 300)
self.addChild(mySprite)
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
// Find the width of one-third of the children nodes
jumpWidth = tileSize.width * floor(tileCount / 3)
}
新术语和重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:"在左侧窗格中选择iOS | 应用程序,在右侧窗格中选择游戏。"
注意
警告或重要注意事项如下所示。
小贴士
小贴士和技巧看起来像这样。
读者反馈
我们欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或不喜欢的地方。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。
要向我们发送一般反馈,请简单地发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及本书的标题。
如果您在某个领域有专业知识,并且您对撰写或参与一本书籍感兴趣,请参阅我们的作者指南,网址为www.packtpub.com/authors。
客户支持
现在您是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从www.packtpub.com下载示例代码文件,适用于您购买的所有 Packt Publishing 书籍。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
此外,每个章节都提供了检查点链接,您可以使用这些链接下载到该点的示例项目。
下载本书的颜色图像
我们还为您提供了一个包含本书中使用的截图/图表的颜色图像的 PDF 文件。这些颜色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/0531OT_ColorImages.pdf下载此文件。
勘误
尽管我们已经尽最大努力确保内容的准确性,错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者感到沮丧,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。
盗版
互联网上版权材料的盗版是一个持续存在的问题,所有媒体都存在。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何形式的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过<copyright@packtpub.com>与我们联系,并提供疑似盗版材料的链接。
我们感谢您在保护我们的作者和我们为您提供有价值内容的能力方面的帮助。
询问
如果您对本书的任何方面有问题,您可以通过<questions@packtpub.com>联系我们,我们将尽力解决问题。
第一章:使用 Swift 设计游戏
苹果的新语言对于游戏开发者来说来得正是时候。Swift有独特的机遇成为特别的东西;一个革命性的工具,用于应用开发者。Swift 是开发者进入苹果生态系统中创建下一个大型游戏的门户。我们刚刚开始探索移动游戏的奇妙潜力,Swift 是我们工具集现代化的需要。Swift 快速、安全、现代,对来自其他语言的开发者有吸引力。无论您是苹果世界的初学者,还是Objective-C的老手,我相信您会喜欢用 Swift 制作游戏。
注意
苹果的网站表示:“Swift 是 C 和 Objective-C 语言的继承者。”
我在这本书中的目标是逐步引导您创建 iPhone 和 iPad 的 2D 游戏。我们将从安装必要的软件开始,逐步完成游戏开发的每一层,最终将我们的新游戏发布到 App Store。
我们在旅途中也会有一些乐趣!我们的目标是创建一款以一只壮丽的飞企鹅皮埃尔为主角的无限飞行游戏。什么是无限飞行游戏?想象一下像 iCopter、Flappy Bird、鲸鱼之旅、喷射背包冒险等游戏——这个列表相当长。
无限飞行游戏在 App Store 上很受欢迎,这个类型需要我们涵盖 2D 游戏设计的许多可重用组件;我将向您展示如何修改我们的机制以创建多种不同的游戏风格。我的希望是,我们的演示项目将成为您自己创意作品的模板。不久,您将能够使用我们共同探索的技术发布您自己的游戏想法。
本章包含以下主题:
-
为什么你会喜欢 Swift
-
您将在本书中学到什么
-
设置您的开发环境
-
创建您的第一个 Swift 游戏
为什么你会喜欢 Swift
作为一种现代编程语言,Swift 受益于编程社区的集体经验;它结合了其他语言的最佳部分,避免了不良的设计决策。以下是我最喜欢的几个 Swift 特性。
美观的语法
Swift 的语法现代且易于接近,无论您现有的编程经验如何。苹果在语法和结构之间取得了平衡,使 Swift 简洁易读。
互操作性
Swift 可以直接集成到您现有的项目中,并与您的 Objective-C 代码并行运行。
强类型
Swift 是一种强类型语言。这意味着编译器将在编译时捕获更多错误——而不是当您的用户在玩游戏时!编译器会期望您的变量属于某种类型(int、string等),如果您尝试分配不同类型的值,则会抛出编译时错误。虽然如果您来自弱类型语言,这可能看起来很严格,但增加的结构会导致更安全、更可靠的代码。
智能类型推断
为了使事情更简单,类型推断将自动检测变量和常量的类型,基于它们的初始值。您不需要显式声明变量的类型。Swift 足够智能,可以在大多数表达式中推断变量类型。
自动内存管理
如苹果 Swift 开发者指南所述,“在 Swift 中,内存管理就是如此简单。”Swift 使用一种称为自动引用计数(您将看到它被称为ARC)的方法来管理游戏内存的使用。除了少数边缘情况外,您可以依赖 Swift 安全地清理并关闭灯光。
一个公平的竞争环境
我最喜欢的 Swift 特性之一是它如何迅速地被主流接受。我们都在共同学习和成长,有巨大的机会开辟新的领域。
Swift 有什么缺点吗?
Swift 是一种非常有趣的语言,但在开始新项目时,我们应该考虑这两个问题。
资源较少
由于 Swift 的历史较短,通过互联网搜索找到常见问题的答案确实更加困难。Objective-C 在 Stack Overflow 等有用的论坛上有多年讨论和答案。随着 Swift 社区的持续发展,这个问题每天都在改善。
操作系统兼容性
Swift 项目将在 iOS7 及以上版本和 OSX 10.9 及以上版本上运行。如果罕见地需要针对运行较旧操作系统的设备,Swift 不是正确的选择。
前置条件
我将努力使这篇文本对所有技能水平的人都容易理解:
-
我假设您作为语言对 Swift 是全新的
-
本书不需要先前的游戏开发经验,尽管它会有所帮助
-
我假设您对常见的编程概念有基本的理解
您将在本书中学到什么
到这本书的结尾,您将能够创建和发布自己的 iOS 游戏。您将知道如何结合我们学到的技术来创建自己的游戏风格,并且您将准备好在 2D 游戏设计的基础上深入更高级的主题。
拥抱 SpriteKit
SpriteKit是 Apple 的 2D 游戏开发框架,也是 iOS 游戏设计的主要工具。SpriteKit 将处理我们的图形渲染、物理和声音播放的机制。就游戏开发框架而言,SpriteKit 是一个极好的选择。它是 Apple 构建和支持的,因此与 Xcode 和 iOS 完美集成。您将学会熟练使用 SpriteKit——在我们的演示游戏中,我们将独家使用它。
我们将学习如何使用 SpriteKit 来驱动我们游戏的核心机制:
-
为我们的玩家、敌人和道具添加动画
-
绘制并移动侧边滚动环境
-
播放声音和音乐
-
应用类似物理的重力和冲量进行移动
-
处理游戏对象之间的碰撞
对玩家输入做出反应
移动游戏中的控制方案必须富有创意。移动硬件迫使我们模拟传统的控制器输入,例如方向垫和屏幕上的多个按钮。这占用了宝贵的可见区域,并且与物理设备相比,提供的精度和反馈更少。许多游戏只使用单一输入方式;在屏幕上的任何地方轻触一次。我们将学习如何充分利用移动输入,并通过感应设备运动和倾斜来探索新的控制形式。
结构化你的游戏代码
编写易于重用和修改的代码,以便随着你的游戏设计不可避免地变化,这是非常重要的。你将在开发和测试你的游戏时经常发现机械改进,并且你会感谢自己有一个干净的工作环境。尽管有许多方法可以接近这个主题,但我们将探索一些最佳实践来构建一个有组织的系统。
构建 UI/菜单/关卡
我们将学习如何在我们的游戏中通过菜单屏幕切换场景。在我们构建演示游戏的过程中,我们将涵盖用户体验设计和菜单布局的基础知识。
与 Game Center 集成
Game Center是苹果内置的社交游戏网络。你的游戏可以与 Game Center 集成,以存储和分享高分和成就。我们将学习如何注册 Game Center,将其集成到我们的代码中,并创建一个有趣的成就系统。
最大化乐趣
如果你像我一样,你脑海中会有数十个游戏想法。想法来得容易,但设计有趣的游戏玩法却很难!在你看到你的设计付诸实践后,发现你的想法需要游戏玩法增强是很常见的。我们将探讨如何避免死胡同,并确保你的项目能够顺利到达终点线。此外,我将分享我的技巧和窍门,以确保你的游戏能给你的玩家带来快乐。
冲刺终点线
创作一款游戏是你将珍藏的记忆。分享你的辛勤工作只会让满足感更甜。一旦我们的游戏经过打磨并准备好公开消费,我们将一起导航 App Store 提交流程。你将结束游戏,对自己使用 Swift 创建游戏并将其带到 App Store 的能力充满信心。
进一步研究
我将专注于 iOS 优秀游戏设计中的机制和编程。一些次要主题超出了本书的范围。
游戏营销和盈利
成功推广和营销你的游戏是一项重要的工作,但本文重点在于游戏开发机制和 Swift 代码。如果你对从你的游戏中赚钱感兴趣,我强烈建议你研究在独立游戏社区中推广自己的最佳方式,并在游戏发布前就开始营销你的游戏。
为 OSX 桌面制作游戏
我们将专注于 iOS。你也可以使用这本书中的技术来在 OSX 上进行游戏开发,但你可能需要研究发布和环境差异。
设置你的开发环境
学习新的开发环境可能会成为障碍。幸运的是,苹果为 iOS 开发者提供了一些出色的工具。我们将从安装 Xcode 开始我们的旅程。
介绍 Xcode
Xcode 是苹果公司的 集成开发环境(IDE)。您需要 Xcode 来创建您的游戏项目、编写和调试您的代码,以及为 App Store 构建您的项目。Xcode 还附带了一个 iOS 模拟器,可以在您的计算机上虚拟化 iPhone 和 iPad 来测试您的游戏。
注意
苹果公司将 Xcode 赞誉为“一个构建 Mac、iPhone 和 iPad 上令人惊叹应用的极具生产力的环境。”
要安装 Xcode,请在 App Store 中搜索 xcode 或访问 developer.apple.com 并点击 Xcode 图标。请注意您正在安装的 Xcode 版本。在撰写本文时,Xcode 的当前版本是 6.3.2。Swift 正在不断发展,每个新的 Xcode 版本都会为 Swift 带来语法变化。为了获得本书中代码的最佳体验,请使用 Xcode 6.3.x(Swift 版本 1.2)。
注意
苹果公司在 2015 年的 WWDC 上宣布了 Xcode 7 和 Swift 2,但在撰写本文时它仍然处于测试版。看起来会有一些小的语法变化。本书中的知识和技巧仍然适用。
Xcode 执行常见的 IDE 功能,以帮助您编写更好、更快的代码。如果您以前使用过 IDE,那么您可能熟悉自动完成、实时错误突出显示、运行和调试项目,以及使用项目管理面板创建和组织您的文件。然而,任何新的程序在开始时都可能显得令人不知所措。在接下来的几页中,我们将介绍一些常见的界面功能。我还发现 YouTube 上的教程视频特别有帮助,如果您遇到困难的话。
创建我们的第一个 Swift 游戏
您已经安装了 Xcode 吗?让我们直奔主题,看看模拟器中的一些游戏代码的实际效果!
-
我们需要创建一个新的项目。启动 Xcode 并导航到 文件 | 新建 | 项目。您将看到一个屏幕,要求您选择您新项目的模板。在左侧面板中选择 iOS | 应用程序,在右侧面板中选择 游戏。它应该看起来像这样:
![创建我们的第一个 Swift 游戏]()
-
一旦你选择了游戏,点击下一步。接下来的屏幕会要求我们输入一些关于我们项目的基本信息。不要担心;我们很快就会进入有趣的部分。对于我们的演示游戏,我们将创建一个侧滚无限飞行的游戏,特色是一只惊人的飞企鹅,名叫皮埃尔。我打算把这个游戏命名为
皮埃尔企鹅逃离南极洲,但你可以自由地给你的项目起任何名字。现在,名字并不重要。当你创建自己的游戏并发布时,你将想要选择一个有意义的产品名称和组织标识符。按照惯例,你的组织标识符应该遵循反向域名风格。我将使用com.ThinkingSwiftly,如下面的截图所示。 -
在填写完名称字段后,请确保选择Swift作为语言,SpriteKit作为游戏技术,以及通用作为设备。以下是我的设置:
![创建我们的第一个 Swift 游戏]()
-
点击下一步,你将看到最后的对话框。保存你的新项目。在电脑上选择一个位置并点击下一步。我们进来了!Xcode 已经用基本的 SpriteKit 模板预先填充了我们的项目。
导航我们的项目
现在我们已经创建了项目,你会在 Xcode 的左侧看到项目导航器。你将使用项目导航器来添加、删除、重命名文件,以及通常组织你的项目。你可能注意到 Xcode 在我们的新项目中创建了很多文件。我们会慢慢来;不要因为还不知道每个文件的作用而感到压力,但如果你好奇,可以自由地探索它们:

探索 SpriteKit 演示
使用项目导航器打开名为GameScene.swift的文件。Xcode 创建了GameScene.swift来存储我们新游戏的基本场景。
什么是场景?SpriteKit 使用场景的概念来封装游戏中的每个独特区域。想象一下电影中的场景;我们将为主菜单创建一个场景,为游戏结束屏幕创建一个场景,为我们的游戏中的每个关卡创建一个场景,等等。如果你在游戏的主菜单上点击“播放”,你会从菜单场景移动到 1 级场景。
小贴士
SpriteKit 在其类名前缀字母“SK”;因此,场景类是SKScene。
你会看到在这个场景中已经有一些代码了。SpriteKit 项目模板自带一个非常小的演示。让我们快速看一下这个演示代码,并使用它来测试 iOS 模拟器。
注意
请在此阶段不要担心理解演示代码。你的重点应该是学习开发环境。
在 Xcode 窗口的顶部寻找运行工具栏。它看起来可能像这样:

选择你偏好的 iOS 设备进行模拟,使用最右侧的下拉菜单。你应该模拟哪个 iOS 设备?你可以自由选择你喜欢的设备。在这本书的截图里,我会使用 iPhone 6,所以如果你想你的结果与我的图片完全匹配,请选择iPhone 6。
注意
很遗憾,你可能会在模拟器中看到你的游戏表现不佳。SpriteKit 在 iOS 模拟器中表现出的 FPS 很低。一旦我们的游戏变得相对复杂,我们甚至在高性能的电脑上也会看到我们的 FPS 下降。模拟器会帮你度过难关,但如果你能插入一个物理设备进行测试,那就更好了。
是时候看看 SpriteKit 的实际效果了!按下灰色播放箭头(方便的运行键盘快捷键:command + r)。Xcode 将构建项目并启动模拟器。模拟器在一个新窗口中启动,所以请确保将其带到前台。你应该看到一个灰色背景和粉笔白色的文本:Hello, World。在灰色背景上点击。
你会在你点击的任何地方看到旋转的战斗机生成:

我可能对战斗机有点过度了……
如果你已经走到这一步,恭喜你!你已经成功安装并配置了你制作第一个 Swift 游戏所需的一切。
一旦你生成了足够的战斗机,你可以关闭模拟器并返回 Xcode。注意:你可以使用键盘命令command + q退出模拟器,或者在 Xcode 中按停止按钮。如果你使用停止按钮,模拟器将保持打开状态,并更快地启动你的下一个构建。
检查演示代码
让我们快速探索一下演示代码。现在不必担心理解一切;我们将在稍后深入探讨每个元素。目前,我希望你能够适应开发环境,并在过程中学到一些东西。如果你遇到了困难,继续前进!实际上,在清除 SpriteKit 演示并开始我们自己的游戏后,下一章的内容将会变得简单。
确保你在 Xcode 中打开了GameScene.swift文件。
GameScene类实现了三个函数。让我们来检查这些函数。你可以随意阅读每个函数内部的代码,但我并不期望你立刻就能理解具体的代码。
-
游戏在切换到
GameScene时,会调用didMoveToView函数。你可以把它想象成场景的初始化或主函数。SpriteKit 演示使用它来在屏幕上绘制Hello World文本。 -
touchesBegan函数处理 iOS 设备屏幕上的用户触摸输入。SpriteKit 演示使用这个函数来生成战斗机图形,并将其设置在我们触摸屏幕的任何地方旋转。 -
update函数会在屏幕上绘制每一帧时运行一次。SpriteKit 演示没有使用这个函数,但我们可能以后会有理由实现它。
清理
我希望你已经吸收了一些 Swift 语法,并对 Swift 和 SpriteKit 有了一个大致的了解。现在是时候为我们的游戏腾出空间了;让我们把所有的示例代码都清理掉!我们想要保留一点模板代码,但我们可以删除函数内部的大部分内容。为了清楚起见,我不期望你现在就能理解这段代码。这只是一个开始我们旅程的必要步骤!请从你的 GameScene.swift 文件中删除行,直到它看起来像以下代码:
import SpriteKit
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
}
}
一旦你的 GameScene.swift 看起来像前面的代码,你就可以继续前进到第二章,精灵、相机、动作!了。现在真正的乐趣开始了!
摘要
你已经取得了很大的进步。你获得了 Swift 的第一次实践经验,安装并配置了你的开发环境,成功地将代码启动到 iOS 模拟器中,并为你的游戏项目做好了第一步准备。做得好!
我们已经看到了足够的“Hello World”演示——你准备好在自己的游戏屏幕上绘制自己的图形了吗?在第二章,精灵、相机、动作!中,我们将使用精灵、纹理、颜色和动画。
第二章:精灵、相机、动作!
使用 SpriteKit 绘制非常简单。我们可以自由地专注于构建出色的游戏体验,同时 SpriteKit 执行游戏循环的机械工作。要在屏幕上绘制一个项目,我们创建一个 SpriteKit 节点的新的实例。这些节点很简单;我们为每个要绘制的项目将子节点附加到场景或现有节点上。精灵、粒子发射器和文本标签在 SpriteKit 中都被视为节点。
注意
游戏循环是一个常用的游戏设计模式,用于每秒多次更新游戏,并在硬件快或慢的情况下保持相同的游戏速度。
SpriteKit 会自动将新节点连接到游戏循环中。随着你对 SpriteKit 的熟练,你可能希望进一步探索游戏循环以了解“幕后”发生了什么。
本章包括以下主题:
-
准备你的项目
-
绘制你的第一个精灵
-
动画:移动、缩放和旋转
-
与纹理一起工作
-
将艺术作品组织到纹理图集中
-
在精灵上居中相机
磨尖我们的铅笔
在我们开始绘制之前,有四个快速事项需要注意:
-
由于我们将设计我们的游戏以使用横幅屏幕方向,我们将完全禁用纵向视图:
-
在 Xcode 中打开你的游戏项目后,在项目导航器中选择整体项目文件夹(最顶部的项目)。
-
你将在 Xcode 的主框架中看到你的项目设置。在部署信息下,找到设备方向部分。
-
取消选择纵向选项,如图所示:
![磨尖我们的铅笔]()
-
-
SpriteKit 模板为在场景中排列精灵生成一个视觉布局文件。我们不需要它;在探索关卡设计时,我们将使用 SpriteKit 视觉编辑器。要删除这个额外的文件:
-
在项目导航器中右键单击
GameScene.sks并选择删除。 -
在对话框窗口中选择移动到废纸篓。
-
-
我们需要调整场景大小以适应新的横幅视图。按照以下步骤调整场景大小:
-
从项目导航器打开
GameViewController.swift并定位到GameViewController类中的viewDidLoad函数。viewDidLoad函数将在游戏意识到它处于横幅视图之前触发,因此我们需要使用在启动过程中较晚触发的函数。完全删除viewDidLoad,移除其所有代码。 -
将
viewDidLoad替换为名为viewWillLayoutSubviews的新函数。现在不必担心理解每一行;我们只是在配置项目。为viewWillLayoutSubviews使用以下代码:override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() // Create our scene: let scene = GameScene() // Configure the view: let skView = self.view as! SKView skView.showsFPS = true skView.showsNodeCount = true skView.ignoresSiblingOrder = true scene.scaleMode = .AspectFill // size our scene to fit the view exactly: scene.size = view.bounds.size // Show the new scene: skView.presentScene(scene) } -
最后,在
GameViewController.swift中找到supportedInterfaceOrientations函数并将其缩减到以下代码:override func supportedInterfaceOrientations() -> Int { return Int( UIInterfaceOrientationMask.Landscape.rawValue); }小贴士
下载示例代码
你可以从你购买的所有 Packt 出版物书籍的账户中下载示例代码文件。
www.packtpub.com。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。此外,每个章节还提供了检查点链接,你可以使用这些链接下载到该点的示例项目。
-
-
我们应该再次确认我们已经准备好继续前进。尝试使用工具栏上的播放按钮或command + r键盘快捷键在模拟器中运行我们的清洁项目。加载后,模拟器应该切换到横幅视图,背景为空白灰色(并在右下角显示节点和 FPS 计数器)。如果项目无法运行,或者你仍然看到“Hello World”,你需要从第一章的结尾,使用 Swift 设计游戏,重新追踪你的步骤,以完成你的项目准备。
检查点 2- A
如果你想要下载到这一点的我的项目,你可以从以下网址下载:www.thinkingswiftly.com/game-development-with-swift/chapter-2
绘制你的第一个精灵
是时候编写一些游戏代码了——太棒了!打开你的 GameScene.swift 文件,找到 didMoveToView 函数。回想一下,这个函数每次游戏切换到这个场景时都会触发。我们将使用这个函数来熟悉 SKSpriteNode 类。你将在游戏中广泛使用 SKSpriteNode,无论何时你想添加一个新的 2D 图形实体。
注意
“精灵”一词指的是在屏幕上独立于背景移动的 2D 图形或动画。随着时间的推移,这个术语已经发展到指代 2D 游戏中屏幕上的任何游戏对象。我们将在本章中创建并绘制你的第一个精灵:一只快乐的小蜜蜂。
构建 SKSpriteNode 类
让我们先在屏幕上画一个蓝色方块。SKSpriteNode 类可以绘制纹理图形和实色块。在花费时间在艺术品上之前,用色块原型化你的新游戏想法通常很有帮助。要绘制蓝色方块,向游戏中添加一个 SKSpriteNode 实例:
override func didMoveToView(view: SKView) {
// Instantiate a constant, mySprite, instance of SKSpriteNode
// The SKSpriteNode constructor can set color and size
// Note: UIColor is a UIKit class with built-in color presets
// Note: CGSize is a type we use to set node sizes
let mySprite = SKSpriteNode(color: UIColor.blueColor(), size:
CGSize(width: 50, height: 50))
// Assign our sprite a position in points, relative to its
// parent node (in this case, the scene)
mySprite.position = CGPoint(x: 300, y: 300)
// Finally, we need to add our sprite node into the node tree.
// Call the SKScene's addChild function to add the node
// Note: In Swift, 'self' is an automatic property
// on any type instance, exactly equal to the instance itself
// So in this instance, it refers to the GameScene instance
self.addChild(mySprite)
}
好吧,运行项目。你应该在模拟器中看到一个类似的小蓝色方块出现:

小贴士
Swift 允许你将变量定义为常量,它只能被赋予一次值。为了最佳性能,尽可能使用 let 来声明常量。当你需要在代码中稍后更改值时,使用 var 声明变量。
将动画添加到你的工具包中
在我们深入精灵理论之前,我们应该用我们的蓝色正方形玩得开心一些。SpriteKit 使用动作对象在屏幕上移动精灵。考虑以下示例:如果我们的目标是移动正方形穿过屏幕,我们必须首先创建一个新的动作对象来描述动画。然后,我们指示我们的精灵节点执行该动作。我将在本章中用许多示例来说明这个概念。现在,在didMoveToView函数中,在self.addChild(mySprite)行下方添加以下代码:
// Create a new constant for our action instance
// Use the moveTo action to provide a goal position for a node
// SpriteKit will tween to the new position over the course of the
// duration, in this case 5 seconds
let demoAction = SKAction.moveTo(CGPoint(x: 100, y: 100),
duration: 5)
// Tell our square node to execute the action!
mySprite.runAction(demoAction)
运行项目。你会看到我们的蓝色正方形滑过屏幕,向(100,100)位置移动。这个动作是可重用的;场景中的任何节点都可以执行这个动作来移动到(100,100)位置。正如你所见,当我们需要对节点属性进行动画处理时,SpriteKit 为我们做了很多繁重的工作。
小贴士
中间画,或称补间,使用引擎在起始帧和结束帧之间进行平滑动画。我们的moveTo动画是一个补间;我们提供起始帧(精灵的原始位置)和结束帧(新的目标位置)。SpriteKit 生成我们值之间的平滑过渡。
让我们尝试一些其他动作。SKAction.moveTo函数只是众多选项之一。尝试将demoAction行替换为以下代码:
let demoAction = SKAction.scaleTo(4, duration: 5)
运行项目。你会看到我们的蓝色正方形增长到原来的四倍大小。
多个动画的序列化
我们可以使用动作组和序列同时执行动作或依次执行。例如,我们可以轻松地将我们的精灵放大并旋转。删除到目前为止的所有动作代码,并用以下代码替换:
// Scale up to 4x initial scale
let demoAction1 = SKAction.scaleTo(4, duration: 5)
// Rotate 5 radians
let demoAction2 = SKAction.rotateByAngle(5, duration: 5)
// Group the actions
let actionGroup = SKAction.group([demoAction1, demoAction2])
// Execute the group!
mySprite.runAction(actionGroup)
当你运行项目时,你会看到一个旋转并变大的正方形。太棒了!如果你想按顺序运行这些动作(而不是同时运行),将SKAction.group更改为SKAction.sequence:
// Group the actions into a sequence
let actionSequence = SKAction.sequence([demoAction1, demoAction2])
// Execute the sequence!
mySprite.runAction(actionSequence)
运行代码,观察你的正方形首先变大然后旋转。很好。你不仅限于两个动作;我们可以将所需数量的动作组合或序列化。
我们到目前为止只使用了几个动作;在继续之前,你可以自由探索SKAction类并尝试不同的动作组合。
回顾你的第一个精灵
恭喜你,你已经学会了如何使用 SpriteKit 动作绘制非纹理精灵并对其进行动画处理。接下来,我们将探索一些重要的定位概念,然后为我们的精灵添加游戏艺术。在你继续之前,请确保你的didMoveToView函数与我的匹配,并且你的序列化动画正在正确触发。以下是到目前为止的我的代码:
override func didMoveToView(view: SKView) {
// Instantiate a constant, mySprite, instance of SKSpriteNode
let mySprite = SKSpriteNode(color: UIColor.blueColor(), size:
CGSize(width: 50, height: 50))
// Assign our sprite a position
mySprite.position = CGPoint(x: 300, y: 300)
// Add our sprite node into the node tree
self.addChild(mySprite)
// Scale up to 4x initial scale
let demoAction1 = SKAction.scaleTo(CGFloat(4), duration: 2)
// Rotate 5 radians
let demoAction2 = SKAction.rotateByAngle(5, duration: 2)
// Group the actions into a sequence
let actionSequence = SKAction.sequence([demoAction1,
demoAction2])
// Execute the sequence!
mySprite.runAction(actionSequence)
}
定位的故事
SpriteKit 使用点阵来定位节点。在这个网格中,场景的左下角是(0,0),X 轴向右为正方向,Y 轴向上为正方向。
类似地,在单个精灵级别上,(0,0)指的是精灵的左下角,而(1,1)指的是右上角。
与锚点对齐
每个精灵都有一个anchorPoint属性,或称为原点。anchorPoint属性允许您选择精灵的哪个部分与精灵的整体位置对齐。
注意
默认锚点为(0.5,0.5),因此新的SKSpriteNode在其位置上完美居中。
为了说明这一点,让我们检查一下我们在屏幕上刚刚绘制的蓝色方块精灵。我们的精灵宽度为 50 像素,高度为 50 像素,其位置是(300,300)。由于我们没有修改anchorPoint属性,其锚点为(0.5,0.5)。这意味着精灵将在场景网格的(300,300)位置上完美居中。我们的精灵的左侧边缘始于 275,右侧边缘终止于 325。同样,底部始于 275,顶部终止于 325。以下图表说明了我们的方块在网格上的位置:

为什么我们默认喜欢居中的精灵?您可能会认为通过将anchorPoint属性设置为(0,0)来根据元素的左下角定位元素会更简单。然而,当我们在缩放或旋转精灵时,居中行为对我们更有益:
-
当我们使用
anchorPoint属性为(0,0)缩放精灵时,它只会沿着 y 轴向上扩展并沿 x 轴向外出扩展。旋转动作会使精灵围绕其左下角进行大圆旋转。 -
默认的
anchorPoint属性为(0.5,0.5)的居中精灵在缩放时会在所有方向上等比例扩展,并且在旋转时会在原地旋转,这通常是期望的效果。
有时候您可能想要更改锚点。例如,如果您在绘制火箭船,您可能希望船围绕其圆锥形的前端旋转,而不是围绕其中心。
添加纹理和游戏艺术
您可能想为您的蓝色方块拍一张截图,以备将来欣赏。我非常喜欢回忆我完成的游戏的老截图,当时它们只是简单的彩色方块在屏幕上滑动。现在是我们超越这个阶段,并将一些有趣的艺术作品附加到我们的精灵上的时候了。
下载免费资源
我为这本书中使用的所有艺术资源提供了一个可下载的包。我建议您使用这些资源,这样您将为我们的演示游戏准备齐全。或者,如果您愿意,当然可以自由地为您的游戏创建自己的艺术作品。
这些资源来自 Kenney Game Studio 的一个杰出的公共领域资源包。我提供的是我们将用于游戏的资源包的小子集。请从以下 URL 下载游戏艺术资源:
www.thinkingswiftly.com/game-development-with-swift/assets
更出色的艺术作品
如果你喜欢这些艺术作品,你可以在kenney.itch.io/kenney-donation通过小额捐赠下载超过 16,000 个同风格的游戏资源。我与 Kenney 没有关联;我只是觉得他向独立游戏开发者发布了如此多的公共领域艺术作品令人钦佩。
作为 CC0 资源,你可以复制、修改和分发这些艺术作品,甚至用于商业目的,而无需请求许可。你可以在这里阅读完整的许可证:
creativecommons.org/publicdomain/zero/1.0/
绘制你的第一个纹理精灵
让我们使用你刚刚下载的一些图形。我们将从创建一个蜜蜂精灵开始。我们将把蜜蜂纹理添加到我们的项目中,将图像加载到SKSpriteNode类中,然后调整节点大小以在视网膜屏幕上获得最佳清晰度。
将蜜蜂图像添加到你的项目中
在我们能够在游戏中使用它们之前,我们需要将图像文件添加到我们的 Xcode 项目中。一旦添加了图像,我们就可以在代码中通过名称引用它们;SpriteKit 足够智能,能够找到并实现图形。按照以下步骤将蜜蜂图像添加到项目中:
-
在项目导航器中右键单击你的项目,然后点击将文件添加到“Pierre Penguin Escapes the Antarctic”(或你的游戏名称)。参考此截图以找到正确的菜单项:
![将蜜蜂图像添加到你的项目中]()
-
浏览你下载的资产包,并在
Enemies文件夹中找到bee.png图像。 -
选择如果需要则复制项目,然后点击添加。
你现在应该在项目导航器中看到bee.png。
使用 SKSpriteNode 加载图像
使用SKSpriteNode将图像绘制到屏幕上相当简单。首先,清除我们在GameScene.swift中的didMoveToView函数内编写的所有用于蓝色方块的代码。将didMoveToView替换为以下代码:
override func didMoveToView(view: SKView) {
// set the scene's background to a nice sky blue
// Note: UIColor uses a scale from 0 to 1 for its colors
self.backgroundColor = UIColor(red: 0.4, green: 0.6, blue:
0.95, alpha: 1.0);
// create our bee sprite node
let bee = SKSpriteNode(imageNamed: "bee.png")
// size our bee node
bee.size = CGSize(width: 100, height: 100)
// position our bee node
bee.position = CGPoint(x: 250, y: 250)
// attach our bee to the scene's node tree
self.addChild(bee)
}
运行项目并见证我们辉煌的蜜蜂——干得好!

为视网膜设计
你可能会注意到我们的蜜蜂图像相当模糊。为了利用视网膜屏幕,资源需要是其节点大小属性的两倍像素维度(对于大多数视网膜屏幕),或者 iPhone 6 Plus 的节点大小的三倍。暂时忽略高度;我们的蜜蜂节点宽度为 100 点,但 PNG 文件只有 56 像素宽。PNG 文件需要宽度为 300 像素才能在 iPhone 6 Plus 上看起来清晰,或者在 2x 视网膜设备上看起来清晰需要宽度为 200 像素。
SpriteKit 会自动调整纹理大小以适应其节点,因此一种方法是在最高的视网膜分辨率(节点大小的三倍)创建一个巨大的纹理,然后让 SpriteKit 将其调整到较低密度屏幕。然而,这会带来相当大的性能损失,并且旧设备甚至可能因为巨大的纹理而耗尽内存并崩溃。
理想资产方法
这些双倍和三倍大小的视网膜资源可能会让新的 iOS 开发者感到困惑。为了解决这个问题,Xcode 通常允许你为每个纹理提供三个图像文件。例如,我们的蜜蜂节点目前宽度为 100 点,高度为 100 点。在一个完美的世界里,你会向 Xcode 提供以下图像:
-
Bee.png(100 像素 x 100 像素) -
Bee@2x.png(200 像素 x 200 像素) -
Bee@3x.png(300 像素 x 300 像素)
然而,目前有一个问题阻止 3x 纹理与纹理图集正确工作。纹理图集将纹理组合在一起并显著提高渲染性能(我们将在下一节中实现我们的第一个纹理图集)。我希望 Apple 能在 Swift 2 中升级纹理图集以支持 3x 纹理。目前,我们需要在 iPhone 6 Plus 的纹理图集和 3x 资源之间做出选择。
我目前的解决方案
在我看来,纹理图集及其性能优势是 SpriteKit 的关键特性。我将继续使用纹理图集,为 iPhone 6 Plus 提供 2x 图像(它仍然看起来相当清晰)。这意味着在这本书中我们不会使用任何 3x 资源。
进一步简化问题,Swift 只运行在 iOS7 及以上版本。唯一运行 iOS7 的非视网膜设备是老化的 iPad 2 和第一代 iPad mini。如果你的最终游戏需要这些旧设备,你应该为你的游戏创建标准图像和 2x 图像。否则,你可以安全地忽略 Swift 的非视网膜资源。
注意
这意味着在这本书中我们只会使用双倍大小的图像。在可下载的资源包中的图像放弃了 2x 后缀,因为我们只使用这个大小。一旦 Apple 更新纹理图集以使用 3x 资源,我建议你切换到理想资源方法部分中概述的方法来为你的游戏使用。
在 SpriteKit 中使用视网膜显示
我们的蜜蜂图像说明了这一切是如何工作的:
-
由于我们设置了显式的节点大小,SpriteKit 会自动调整蜜蜂纹理的大小以适应我们 100 点宽、100 点高的节点。这种自动调整大小以适应的功能非常方便,但请注意,我们实际上略微扭曲了图像的宽高比。
-
如果我们不设置显式的大小,SpriteKit 会将节点(以点为单位)的大小调整为与纹理的维度(以像素为单位)相匹配。删除设置我们蜜蜂节点大小的行,并重新运行项目。SpriteKit 会自动保持宽高比,但较小的蜜蜂仍然模糊。这是因为我们的新节点是 56 点 x 48 点,与我们的 PNG 文件的 56 像素 x 48 像素像素维度相匹配……然而,我们的 PNG 文件需要是 112 像素 x 96 像素,才能在 2x 视网膜屏幕上以这个节点大小显示清晰图像。
-
我们无论如何都需要一个更小的蜜蜂,所以我们将调整节点的大小而不是生成更大的艺术品。将你的蜜蜂节点的
size属性设置为纹理像素分辨率的二分之一:// size our bee in points: bee.size = CGSize(width: 28, height: 24)
运行项目,你会看到一个更小、更清晰的蜜蜂,就像这个截图所示:

太棒了!这里的重要概念是要将你的艺术文件设计成节点点大小的两倍像素分辨率,以便利用 2x 视网膜屏幕,或者将点大小增加到三倍以充分利用 iPhone 6 Plus。现在我们将看看如何组织和动画多个精灵帧。
组织你的资源
如果我们像处理蜜蜂一样添加所有纹理,我们的项目导航器很快就会被图像文件淹没。幸运的是,Xcode 提供了几个解决方案。
探索 Images.xcassets
我们可以将图像存储在.xcassets文件中,并轻松地从我们的代码中引用它们。这是一个存储背景图像的好地方:
-
从项目导航器中打开
Images.xcassets。 -
目前我们不需要在这里添加任何图像,但将来,你可以直接将图像文件拖到图像列表中,或者右键单击,然后导入。
-
注意,SpriteKit 演示中的飞船图像存储在这里。我们不再需要它,所以我们可以右键单击它,然后选择删除所选项目来删除它。
将艺术作品收集到纹理图集中
我们将使用纹理图集来组织大部分游戏中的艺术资源。纹理图集通过收集相关的艺术作品来组织资源。它们还通过将每个图集中的所有图像优化为单个纹理来提高性能。SpriteKit 只需要一个绘制调用就能从同一纹理图集中渲染多个图像。此外,它们非常容易使用!按照以下步骤构建你的蜜蜂纹理图集:
-
我们需要移除旧的蜜蜂纹理。在项目导航器中右键单击
bee.png,然后选择删除,然后移动到废纸篓。 -
使用 Finder,浏览到你下载的资源包,并定位到
Enemies文件夹。 -
在
Enemies内部创建一个新的文件夹,并将其命名为bee.atlas。 -
在
Enemies中找到bee.png和bee_fly.png图像,并将它们复制到你的新bee.atlas文件夹中。现在你应该有一个名为bee.atlas的文件夹,其中包含两个蜜蜂 PNG 文件。创建新的纹理图集你所需要做的就是将相关的图像放置到一个带有.atlas后缀的新文件夹中。 -
将图集添加到你的项目中。在 Xcode 中,在项目导航器中右键单击项目文件夹,然后点击添加文件…,就像我们之前为单个蜜蜂纹理所做的那样。
-
找到
bee.atlas文件夹,并选择文件夹本身。 -
选择如果需要则复制项目,然后点击添加。
纹理图集将出现在项目导航器中。做得好;我们将蜜蜂资源组织到一个集合中,Xcode 将自动创建之前提到的性能优化。
更新我们的蜜蜂节点以使用纹理图集
我们实际上现在可以运行我们的项目,看到之前相同的蜜蜂。我们旧的蜜蜂纹理是bee.png,而一个新的bee.png存在于纹理图集中。尽管我们删除了独立的bee.png,但 SpriteKit 足够智能,能够在纹理图集中找到新的bee.png。
我们应该确保我们的纹理图集正在正常工作,并且我们已经成功删除了旧的单独的bee.png。在GameScene.swift中,将我们的SKSpriteNode实例化行更改为使用纹理图集中的新bee_fly.png图形:
// create our bee sprite
// notice the new image name: bee_fly.png
let bee = SKSpriteNode(imageNamed: "bee_fly.png")
再次运行项目。你应该看到不同的蜜蜂图像,它的翅膀比之前更低。这是蜜蜂动画的第二帧。接下来,我们将学习如何在两个帧之间进行动画,以创建一个动画精灵。
遍历纹理图集帧
我们需要学习一种额外的纹理图集技术:我们可以快速翻阅多个精灵帧,让我们的蜜蜂通过动作变得生动起来。我们现在有两个蜜蜂在飞行中的帧;如果我们在这两个帧之间切换,它应该看起来像是悬停在原地。
我们的小结点将运行一个新的SKAction在两个帧之间进行动画。更新你的didMoveToView函数以匹配我的(我移除了一些旧的注释以节省空间):
override func didMoveToView(view: SKView) {
self.backgroundColor = UIColor(red: 0.4, green: 0.6, blue:
0.95, alpha: 1.0)
// create our bee sprite
// Note: Remove all prior arguments from this line:
let bee = SKSpriteNode()
bee.position = CGPoint(x: 250, y: 250)
bee.size = CGSize(width: 28, height: 24)
self.addChild(bee)
// Find our new bee texture atlas
let beeAtlas = SKTextureAtlas(named:"bee.atlas")
// Grab the two bee frames from the texture atlas in an array
// Note: Check out the syntax explicitly declaring beeFrames
// as an array of SKTextures. This is not strictly necessary,
// but it makes the intent of the code more readable, so I
// chose to include the explicit type declaration here:
let beeFrames:[SKTexture] = [
beeAtlas.textureNamed("bee.png"),
beeAtlas.textureNamed("bee_fly.png")]
// Create a new SKAction to animate between the frames once
let flyAction = SKAction.animateWithTextures(beeFrames,
timePerFrame: 0.14)
// Create an SKAction to run the flyAction repeatedly
let beeAction = SKAction.repeatActionForever(flyAction)
// Instruct our bee to run the final repeat action:
bee.runAction(beeAction)
}
运行项目。你会看到我们的蜜蜂翅膀一上一下地拍打——酷!你已经学会了使用纹理图集进行精灵动画的基础。我们将在本书的后面使用相同的技巧创建越来越复杂的动画。现在,给自己鼓掌。结果可能看起来很简单,但你已经解锁了通往你的第一个 SpriteKit 游戏的主要构建块!
将所有这些整合在一起
首先,我们学习了如何使用动作来移动、缩放和旋转我们的精灵。然后,我们探索了通过多个帧进行动画,让我们的精灵栩栩如生。现在,让我们将这些技术结合起来,让我们的蜜蜂在屏幕上飞来飞去,每次转弯时翻转纹理。
在didMoveToView函数的底部添加此代码,在bee.runAction(beeAction)行之下:
// Set up new actions to move our bee back and forth:
let pathLeft = SKAction.moveByX(-200, y: -10, duration: 2)
let pathRight = SKAction.moveByX(200, y: 10, duration: 2)
// These two scaleXTo actions flip the texture back and forth
// We will use these to turn the bee to face left and right
let flipTextureNegative = SKAction.scaleXTo(-1, duration: 0)
let flipTexturePositive = SKAction.scaleXTo(1, duration: 0)
// Combine actions into a cohesive flight sequence for our bee
let flightOfTheBee = SKAction.sequence([pathLeft,
flipTextureNegative, pathRight, flipTexturePositive])
// Last, create a looping action that will repeat forever
let neverEndingFlight =
SKAction.repeatActionForever(flightOfTheBee)
// Tell our bee to run the flight path, and away it goes!
bee.runAction(neverEndingFlight)
运行项目。你会看到蜜蜂在飞来飞去,拍打翅膀。你正式学会了 SpriteKit 中的动画基础!我们将在此基础上构建,为我们的玩家创建一个丰富的动画游戏世界。
将相机中心对准精灵
游戏通常需要相机跟随玩家精灵在空间中的移动。我们确实希望我们的企鹅角色皮埃尔有这种行为,我们很快就会将其添加到游戏中。由于 SpriteKit 没有内置相机功能,我们将创建自己的结构来模拟我们想要的效果。
我们实现这一目标的一种方法是将皮埃尔保持在同一位置,并将其他每个对象移动过他。这是有效的,但在语义上可能有些混乱,并且在定位游戏对象时可能会引起错误。
创建一个新世界
我更喜欢创建一个世界节点,并将所有我们的游戏节点附加到它上(而不是直接附加到场景)。我们可以通过世界将皮埃尔向前移动,并简单地重新定位世界节点,以便皮埃尔始终位于我们设备视口的中心。所有我们的敌人、道具和结构都将作为世界节点的子节点,并且在我们滚动世界时看起来像是在屏幕上移动。
小贴士
每个精灵节点的位置始终相对于其直接父节点。当您更改节点的位置时,所有子节点都会随之移动。这对于模拟我们的相机来说是一个非常方便的行为。
此图展示了该技术的简化版本,并使用了一些虚构的数字:

您可以在以下代码块中找到我们相机功能的代码。阅读注释以获取详细说明。这只是一个快速回顾更改:
-
我们的
didMoveToView函数变得越来越拥挤。我将我们的飞行蜜蜂代码拆分到一个名为addTheFlyingBee的新函数中。稍后,我们将游戏对象,如蜜蜂,封装到它们自己的类中。 -
我在
GameScene类中创建了两个新的常量:世界节点和蜜蜂节点。 -
我更新了
didMoveToView函数。它将世界节点添加到场景的节点树中,并调用新的addTheFlyingBee函数。 -
在新的蜜蜂函数内部,我移除了蜜蜂常量,因为
GameScene现在将其声明为其自己的属性。 -
在新的蜜蜂函数内部,我们不是通过
self.addChild(bee)将蜜蜂节点添加到场景中,而是想通过world.addChild(bee)将其添加到世界中。 -
我们正在实现一个新的函数:
didSimulatePhysics。SpriteKit 在执行物理计算和调整位置后,每帧都会调用此函数。这是一个更新我们世界位置的好地方。更改世界位置的数学计算位于这个新函数中。
请更新您的整个GameScene.swift文件以匹配我的:
import SpriteKit
class GameScene: SKScene {
// Create the world as a generic SKNode
let world = SKNode()
// Create our bee node as a property of GameScene so we can
// access it throughout the class
// (Make sure to remove the old bee declaration inside the
// didMoveToView function.)
let bee = SKSpriteNode()
override func didMoveToView(view: SKView) {
self.backgroundColor = UIColor(red: 0.4, green: 0.6, blue:
0.95, alpha: 1.0)
// Add the world node as a child of the scene
self.addChild(world)
// Call the new bee function
self.addTheFlyingBee()
}
// I moved all of our bee animation code into a new function:
func addTheFlyingBee() {
// Position our bee
bee.position = CGPoint(x: 250, y: 250)
bee.size = CGSize(width: 28, height: 24)
// Notice we now attach our bee node to the world node:
world.addChild(bee)
/*
all of the same bee animation code remains here,
I am excluding it in this text for brevity
*/
}
// A new function
override func didSimulatePhysics() {
// To find the correct position, subtract half of the
// scene size from the bee's position, adjusted for any
// world scaling.
// Multiply by -1 and you have the adjustment to keep our
// sprite centered:
let worldXPos = -(bee.position.x * world.xScale -
(self.size.width / 2))
let worldYPos = -(bee.position.y * world.yScale -
(self.size.height / 2))
// Move the world so that the bee is centered in the scene
world.position = CGPoint(x: worldXPos, y: worldYPos)
}
}
运行游戏。您应该看到我们的蜜蜂直接固定在屏幕中心,每两秒翻转一次。

蜜蜂实际上正在改变位置,就像之前一样,但世界正在补偿以保持蜜蜂在屏幕中心。当我们第三章中添加更多游戏对象时,加入物理,蜜蜂看起来就像整个世界在屏幕上滚动时在飞行。
检查点 2-B
在本章中,我们对项目进行了许多更改。如果您想下载到这一点的项目,请在此处操作:
摘要
您已经获得了 SpriteKit 中精灵、节点和动作的基础知识,并且已经朝着用 Swift 制作您的第一个游戏迈出了巨大的步伐。
您已为项目配置了横幅方向,绘制了您的第一个精灵,然后让它移动、旋转和缩放。您为精灵添加了蜜蜂纹理,创建了一个图像图集,并通过飞行帧进行动画。最后,您构建了一个世界节点,以使游戏玩法始终围绕玩家进行。做得好!
在下一章中,我们将使用 SpriteKit 的物理引擎为我们的世界分配重量和重力,生成更多飞行角色,并创建地面和天空。
第三章:混合物理
SpriteKit 包含一个功能齐全的物理引擎。它易于实现且非常有用;大多数移动游戏设计都需要游戏对象之间一定程度的物理交互。在我们的游戏中,我们想知道玩家何时撞到地面、敌人或道具。物理系统可以跟踪这些碰撞,并在这些事件发生时执行我们的特定游戏代码。SpriteKit 的物理引擎还可以将重力应用于世界,使碰撞的精灵相互弹跳和旋转,并通过冲量创建逼真的运动——而且它会在屏幕上绘制每一帧之前完成所有这些。
本章包括以下主题:
-
为了保持一致性,采用协议
-
将游戏对象组织到类中
-
添加玩家的角色
-
重建
GameScene类 -
物理体和重力
-
探索物理模拟机制
-
使用冲量和力进行移动
-
将蜜蜂撞进蜜蜂中
打好基础
到目前为止,我们通过向 GameScene 类逐个添加小块代码来学习。我们应用程序的复杂性即将增加。为了构建一个复杂的游戏世界,我们需要构建可重用的类并积极组织我们的新代码。
遵循协议
首先,我们想要为每个游戏对象创建单独的类(蜜蜂类、玩家企鹅类、道具类等)。此外,我们希望所有游戏对象类都共享一组一致的属性和方法。我们可以通过创建一个 协议,即我们游戏类的蓝图来强制这种一致性。协议本身不提供任何功能,但采用该协议的每个类都必须完全遵循其规范,Xcode 才能编译项目。如果您来自 Java 或 C# 背景,协议与接口非常相似。
将新文件添加到您的项目中(在项目导航器中右键单击并选择新建文件,然后选择Swift 文件),并将其命名为 GameSprite.swift。然后,将以下代码添加到您的新文件中:
import SpriteKit
protocol GameSprite {
var textureAtlas: SKTextureAtlas { get set }
func spawn(parentNode: SKNode, position: CGPoint, size:
CGSize)
func onTap()
}
现在,任何采用 GameSprite 协议的类都必须实现一个 textureAtlas 属性、一个 spawn 函数和一个 onTap 函数。当我们用代码处理游戏对象时,我们可以安全地假设游戏对象提供了这些实现。
重新发明蜜蜂
我们的老蜜蜂工作得非常好,但我们想在世界的各个地方生成许多蜜蜂。我们将创建一个继承自 SKSpriteNode 的 Bee 类,这样我们就可以干净利落地将任意数量的蜜蜂印在世界上了。
将每个类单独分离到其自己的文件中是一种常见的约定。向您的项目中添加一个新的 Swift 文件,并将其命名为 Bee.swift。然后,添加以下代码:
import SpriteKit
// Create the new class Bee, inheriting from SKSpriteNode
// and adopting the GameSprite protocol:
class Bee: SKSpriteNode, GameSprite {
// We will store our texture atlas and bee animations as
// class wide properties.
var textureAtlas:SKTextureAtlas =
SKTextureAtlas(named:"bee.atlas")
var flyAnimation = SKAction()
// The spawn function will be used to place the bee into
// the world. Note how we set a default value for the size
// parameter, since we already know the size of a bee
func spawn(parentNode:SKNode, position: CGPoint, size: CGSize
= CGSize(width: 28, height: 24)) {
parentNode.addChild(self)
createAnimations()
self.size = size
self.position = position
self.runAction(flyAnimation)
}
// Our bee only implements one texture based animation.
// But some classes may be more complicated,
// So we break out the animation building into this function:
func createAnimations() {
let flyFrames:[SKTexture] =
[textureAtlas.textureNamed("bee.png"),
textureAtlas.textureNamed("bee_fly.png")]
let flyAction = SKAction.animateWithTextures(flyFrames,
timePerFrame: 0.14)
flyAnimation = SKAction.repeatActionForever(flyAction)
}
// onTap is not wired up yet, but we have to implement this
// function to adhere to our protocol.
// We will explore touch events in the next chapter.
func onTap() {}
}
现在可以轻松地生成我们想要的任意数量的蜜蜂。切换回 GameScene.swift,并在 didMoveToView 中添加以下代码:
// Create three new instances of the Bee class:
let bee2 = Bee()
let bee3 = Bee()
let bee4 = Bee()
// Use our spawn function to place the bees into the world:
bee2.spawn(world, position: CGPoint(x: 325, y: 325))
bee3.spawn(world, position: CGPoint(x: 200, y: 325))
bee4.spawn(world, position: CGPoint(x: 50, y: 200))
运行项目。蜜蜂,到处都是!我们的原始蜜蜂正在一群蜜蜂中来回飞行。您的模拟器应该看起来像这样:

根据你的看法,你可能觉得新蜜蜂在移动,而原始蜜蜂是静止的。我们需要添加一个参考点。接下来,我们将添加地面。
冰原
我们将在屏幕底部添加一些地面,作为玩家定位的约束和移动的参考点。我们将创建一个名为Ground的新类。首先,让我们将地面艺术纹理图集添加到我们的项目中。
另一种添加资源的方式
我们将使用不同的方法将文件添加到 Xcode 中。按照以下步骤添加新的艺术作品:
-
在 Finder 中,导航到你在第二章下载的资产包,精灵、相机、动作!,然后到
Environment文件夹。 -
你之前已经学会了如何为我们的蜜蜂创建纹理图集。我已经为我们在游戏中使用的其余艺术作品创建了纹理图集。定位
ground.atlas文件夹。 -
将此文件夹拖放到 Xcode 的项目管理器中,在项目文件夹下,如图所示:
![另一种添加资源的方式]()
-
在对话框中,确保你的设置与以下截图匹配,然后点击完成:
![另一种添加资源的方式]()
完美——你应该在项目导航器中看到地面纹理图集。
添加地面类
接下来,我们将添加地面代码。在你的项目中添加一个新的 Swift 文件,并将其命名为Ground.swift。使用以下代码:
import SpriteKit
// A new class, inheriting from SKSpriteNode and
// adhering to the GameSprite protocol.
class Ground: SKSpriteNode, GameSprite {
var textureAtlas:SKTextureAtlas =
SKTextureAtlas(named:"ground.atlas")
// Create an optional property named groundTexture to store
// the current ground texture:
var groundTexture:SKTexture?
func spawn(parentNode:SKNode, position:CGPoint, size:CGSize) {
parentNode.addChild(self)
self.size = size
self.position = position
// This is one of those unique situations where we use
// non-default anchor point. By positioning the ground by
// its top left corner, we can place it just slightly
// above the bottom of the screen, on any of screen size.
self.anchorPoint = CGPointMake(0, 1)
// Default to the ice texture:
if groundTexture == nil {
groundTexture = textureAtlas.textureNamed("ice-
tile.png");
}
// We will create child nodes to repeat the texture.
createChildren()
}
// Build child nodes to repeat the ground texture
func createChildren() {
// First, make sure we have a groundTexture value:
if let texture = groundTexture {
var tileCount:CGFloat = 0
let textureSize = texture.size()
// We will size the tiles at half the size
// of their texture for retina sharpness:
let tileSize = CGSize(width: textureSize.width / 2,
height: textureSize.height / 2)
// Build nodes until we cover the entire Ground width
while tileCount * tileSize.width < self.size.width {
let tileNode = SKSpriteNode(texture: texture)
tileNode.size = tileSize
tileNode.position.x = tileCount * tileSize.width
// Position child nodes by their upper left corner
tileNode.anchorPoint = CGPoint(x: 0, y: 1)
// Add the child texture to the ground node:
self.addChild(tileNode)
tileCount++
}
}
}
// Implement onTap to adhere to the protocol:
func onTap() {}
}
纹理平铺
为什么我们需要createChildren函数?SpriteKit 不支持内置方法来重复节点大小的纹理。相反,我们为每个纹理瓦片创建子节点,并将它们附加到父节点的宽度上。性能不是问题;只要我们将子节点附加到一个父节点上,并且所有纹理都来自同一个纹理图集,SpriteKit 就会通过一个绘制调用来处理它们。
将电线接到地面上
我们已经将地面艺术添加到项目中并创建了Ground类。最后一步是在场景中创建Ground的实例。按照以下步骤连接地面:
-
打开
GameScene.swift,并在GameScene类中添加一个新的属性以创建Ground类的实例。你可以将此放在实例化世界节点(新代码用粗体表示)的下面:let world = SKNode() let ground = Ground() -
定位
didMoveToView函数。在蜜蜂孵化线下面添加以下代码:// size and position the ground based on the screen size. // Position X: Negative one screen width. // Position Y: 100 above the bottom (remember the ground's top // left anchor point). let groundPosition = CGPoint(x: -self.size.width, y: 100) // Width: 3x the width of the screen. // Height: 0\. Our child nodes will provide the height. let groundSize = CGSize(width: self.size.width * 3, height: 0) // Spawn the ground! ground.spawn(world, position: groundPosition, size: groundSize)
运行项目。你将看到冰原出现在我们的蜜蜂下方。这个小小的改动在很大程度上有助于营造我们的中心蜜蜂正在穿越空间的感受。你的模拟器应该看起来像这样:

一只野企鹅出现了!
在我们开始物理课程之前,还需要构建一个类:Player类!是时候用指定的玩家节点替换移动的蜜蜂了。
首先,我们将添加我们的企鹅艺术纹理图集。到现在为止,你应该熟悉通过项目导航器添加文件。像之前添加地面资产一样添加皮埃尔的美术。我将皮埃尔的纹理图集命名为 pierre.atlas。你可以在资产包中找到它,在 Pierre 文件夹内。
一旦你将皮埃尔的纹理图集添加到项目中,你就可以创建 Player 类。在你的项目中添加一个新的 Swift 文件,并将其命名为 Player.swift。然后添加以下代码:
import SpriteKit
class Player : SKSpriteNode, GameSprite {
var textureAtlas:SKTextureAtlas =
SKTextureAtlas(named:"pierre.atlas")
// Pierre has multiple animations. Right now we will
// create an animation for flying up, and one for going down:
var flyAnimation = SKAction()
var soarAnimation = SKAction()
func spawn(parentNode:SKNode, position: CGPoint,
size:CGSize = CGSize(width: 64, height: 64)) {
parentNode.addChild(self)
createAnimations()
self.size = size
self.position = position
// If we run an action with a key, "flapAnimation",
// we can later reference that key to remove the action.
self.runAction(flyAnimation, withKey: "flapAnimation")
}
func createAnimations() {
let rotateUpAction = SKAction.rotateToAngle(0, duration:
0.475)
rotateUpAction.timingMode = .EaseOut
let rotateDownAction = SKAction.rotateToAngle(-1,
duration: 0.8)
rotateDownAction.timingMode = .EaseIn
// Create the flying animation:
let flyFrames:[SKTexture] = [
textureAtlas.textureNamed("pierre-flying-1.png"),
textureAtlas.textureNamed("pierre-flying-2.png"),
textureAtlas.textureNamed("pierre-flying-3.png"),
textureAtlas.textureNamed("pierre-flying-4.png"),
textureAtlas.textureNamed("pierre-flying-3.png"),
textureAtlas.textureNamed("pierre-flying-2.png")
]
let flyAction = SKAction.animateWithTextures(flyFrames,
timePerFrame: 0.03)
// Group together the flying animation frames with a
// rotation up:
flyAnimation = SKAction.group([
SKAction.repeatActionForever(flyAction),
rotateUpAction
])
// Create the soaring animation, just one frame for now:
let soarFrames:[SKTexture] =
[textureAtlas.textureNamed("pierre-flying-1.png")]
let soarAction = SKAction.animateWithTextures(soarFrames,
timePerFrame: 1)
// Group the soaring animation with the rotation down:
soarAnimation = SKAction.group([
SKAction.repeatActionForever(soarAction),
rotateDownAction
])
}
func onTap() {}
}
太好了!在我们继续之前,我们需要用我们刚刚创建的新 Player 类的实例替换原始蜜蜂。按照以下步骤替换蜜蜂:
-
在
GameScene.swift文件中,靠近顶部,删除创建bee常量的行。相反,我们想要实例化一个Player实例。添加新行:let player = Player(). -
完全删除
addTheFlyingBee函数。 -
在
didMoveToView方法中,删除调用addTheFlyingBee的行。 -
在
didMoveToView方法中,在底部添加一行以生成玩家:player.spawn(world, position: CGPoint(x: 150, y: 250)) -
在下方,在
didSimulatePhysics方法中,将蜜蜂的引用替换为player的引用。回想一下,我们在 第二章 中创建了didSimulatePhysics函数,当时我们在一个节点上居中相机。
我们已经成功地将原始蜜蜂转换成了企鹅。在我们继续之前,请确保你的 GameScene 类包含了本章中我们迄今为止所做的所有更改。之后,我们将开始探索物理系统。
修复 GameScene 类
我们对我们的项目做了一些更改。幸运的是,这是之前动画代码的最后一次重大修改。向前看,我们将使用本章中构建的出色结构。到现在为止,你的 GameScene.swift 文件应该看起来像这样:
class GameScene: SKScene {
let world = SKNode()
let player = Player()
let ground = Ground()
override func didMoveToView(view: SKView) {
// Set a sky-blue background color:
self.backgroundColor = UIColor(red: 0.4, green: 0.6, blue:
0.95, alpha: 1.0)
// Add the world node as a child of the scene:
self.addChild(world)
// Spawn our physics bees:
let bee2 = Bee()
let bee3 = Bee()
let bee4 = Bee()
bee2.spawn(world, position: CGPoint(x: 325, y: 325))
bee3.spawn(world, position: CGPoint(x: 200, y: 325))
bee4.spawn(world, position: CGPoint(x: 50, y: 200))
// Spawn the ground:
let groundPosition = CGPoint(x: -self.size.width, y: 30)
let groundSize = CGSize(width: self.size.width * 3,
height: 0)
ground.spawn(world, position: groundPosition, size:
groundSize)
// Spawn the player:
player.spawn(world, position: CGPoint(x: 150, y: 250))
}
override func didSimulatePhysics() {
let worldXPos = -(player.position.x * world.xScale –
(self.size.width / 2))
let worldYPos = -(player.position.y * world.yScale –
(self.size.height / 2))
world.position = CGPoint(x: worldXPos, y: worldYPos)
}
}
运行项目。你会看到我们的新企鹅在蜜蜂附近悬浮。干得好;我们现在准备好使用所有新节点来探索物理系统。你的模拟器应该看起来像这样的截图:

探索物理系统
SpriteKit 使用 物理体 来模拟物理。我们将物理体附加到所有需要物理计算的节点上。在探索所有细节之前,我们将设置一个快速示例。
如飞般坠落
我们的蜜蜂需要成为物理模拟的一部分,因此我们将为它们的节点添加物理体。打开你的 Bee.swift 文件并定位到 spawn 函数。在函数底部添加以下代码:
// Attach a physics body, shaped like a circle
// and sized roughly to our bee.
self.physicsBody = SKPhysicsBody(circleOfRadius: size.width / 2)
向物理模拟中添加节点就这么简单。运行项目。你会看到我们的三个 Bee 实例从屏幕上掉落。它们现在受到重力的作用,默认情况下重力是开启的。
巩固地面
我们希望地面能够捕捉下落的游戏对象。我们可以给地面自己的物理体,这样物理模拟就可以阻止蜜蜂穿过它。打开您的Ground.swift文件,找到spawn函数,然后在函数底部添加以下代码:
// Draw an edge physics body along the top of the ground node.
// Note: physics body positions are relative to their nodes.
// The top left of the node is X: 0, Y: 0, given our anchor point.
// The top right of the node is X: size.width, Y: 0
let pointTopRight = CGPoint(x: size.width, y: 0)
self.physicsBody = SKPhysicsBody(edgeFromPoint: CGPointZero,
toPoint: pointTopRight)
运行项目。现在蜜蜂会迅速下落,然后一旦与地面碰撞就会停止。注意下落得更远的蜜蜂弹跳得更有力。蜜蜂着陆后,您的模拟器将看起来像这样:

检查点 3-A
到目前为止,工作做得很好。我们已经为我们的游戏添加了很多结构,并开始探索物理系统。如果您想下载到这一点的我的项目,请在此处操作:
www.thinkingswiftly.com/game-development-with-swift/chapter-3
探索物理模拟机制
让我们更详细地看看 SpriteKit 物理系统的具体细节。例如,为什么蜜蜂受到重力的作用,而地面却保持在原地?尽管我们为两个节点都附加了物理体,但实际上我们使用了两种不同的物理体样式。有三种类型的物理体,它们的行为略有不同:
-
动态物理体有体积,并且完全受系统中的力和碰撞的影响。我们将为游戏世界的绝大部分使用动态物理体:玩家、敌人、道具等。
-
静态物理体有体积但没有速度。物理模拟不会移动具有静态体的节点,但它们仍然可以与其他游戏对象发生碰撞。我们可以使用静态体来制作墙壁或障碍物。
-
边缘物理体没有体积,物理模拟永远不会移动它们。它们标记了运动边界;其他物理体永远不会越过它们。边缘可以交叉以创建小的封闭区域。
体积(动态和静态)体具有各种属性,这些属性可以修改它们对碰撞和空间移动的反应。这使我们能够创建各种逼真的物理效果。每个属性控制一个物体的物理特性的一个方面:
-
恢复系数决定了当一个物体弹入另一个物体时损失多少能量。这改变了物体的弹性。SpriteKit 在 0.0 到 1.0 的范围内测量恢复系数。默认值是 0.2。
-
摩擦描述了滑动一个物体相对于另一个物体所需的力。这个属性也使用 0.0 到 1.0 的刻度,默认值为 0.2。
-
阻尼决定了物体在空间中移动时减速的速度。你可以把阻尼想象成空气摩擦。线性阻尼决定了物体失去速度的速度,而角阻尼影响旋转。两者都从 0.0 到 1.0 测量,默认值为 0.1。
-
质量是以千克为单位的。它描述了碰撞物体推动物体的距离,并在运动中考虑动量。质量更大的物体在受到另一个物体的撞击时移动较少,并且在它们相互碰撞时会将其他物体推得更远。物理引擎会自动使用物体的质量和面积来确定 密度。或者,你可以设置密度,让物理引擎计算质量。通常设置质量更直观。
好的——教科书就到这里吧!让我们通过一些例子来巩固我们的学习。
首先,我们希望重力跳过我们的蜂。我们将手动设置它们的飞行路径。我们需要蜂成为动态物理体,以便与其他节点正确交互,但我们需要这些体忽略重力。对于这种情况,SpriteKit 提供了一个名为 affectedByGravity 的属性。打开 Bee.swift,在 spawn 函数的底部添加以下代码:
self.physicsBody?.affectedByGravity = false
小贴士
physicsBody 后面的问号是可选链。我们需要解包 physicsBody,因为它是一个可选值。如果 physicsBody 为 nil,整个语句将返回 nil(而不是触发错误)。你可以把它想象成用内联语句优雅地解包一个可选属性。
运行项目。蜂群现在应该像我们添加它们身体之前一样停留在原地。然而,SpriteKit 的物理模拟现在会影响它们;它们会对冲量和碰撞做出反应。太好了,让我们故意让蜂群相撞。
蜂遇蜂
你可能已经注意到我们在游戏世界中将 bee2 和 bee3 放在了相同的高度。我们只需要推动其中一个水平方向,以创建碰撞——完美的碰撞测试假人!我们可以使用 冲量 为外部蜂创建速度。
在 GameScene.swift 中找到 didMoveToView 函数。在所有生成代码的下方,添加这一行:
bee2.physicsBody?.applyImpulse(CGVector(dx: -3, dy: 0))
运行项目。你会看到最外层的蜂飞向中间并撞到内蜂。这会把内蜂推向左边,并减缓第一只蜂的接触速度。
用一个变量:增加质量,尝试相同的实验。在冲量行之前,添加以下代码来调整 bee2 的质量:
bee2.physicsBody?.mass = 0.2
运行项目。嗯,我们的重蜂在相同的冲量下移动不远(毕竟它是一只 200 克的蜂。)它最终撞到了内蜂,但碰撞并不令人兴奋。我们需要增加冲量来推动我们更重的蜂。将冲量行更改为使用 -15 的 dx 值:
bee2.physicsBody?.applyImpulse(CGVector(dx: -15, dy: 0))
再次运行项目。这次,我们的冲量提供了足够的能量,使重蜂以有趣的方式移动。注意重蜂在碰撞时传递给普通蜂的能量;轻蜂在接触后飞走。两只蜂都有足够的动量,最终完全滑出屏幕。你的模拟器应该看起来像这张截图,就在蜂群滑出屏幕之前:

在您继续之前,您可能希望尝试我在本章前面概述的各种物理属性。您可以创建许多碰撞变体;物理模拟只需付出很少的努力就能提供很多深度。
冲量还是力?
您有几种选项可以使用物理体移动节点:
-
冲量是对物理体速度的即时、一次性改变。在我们的测试中,冲量给了蜜蜂速度,并且它慢慢因为阻尼和碰撞而减速。冲量非常适合投射物:导弹、子弹、不高兴的鸟等等。
-
力只在一个物理计算周期内作用于速度。当我们使用力时,我们通常在每一帧之前应用它。力对于火箭船、汽车或其他持续自我推进的任何东西都很有用。
-
您还可以直接编辑物体的
velocity和angularVelocity属性。这对于设置手动速度限制很有用。
检查点 3-B
在本章中,我们对我们的项目进行了多项结构性的更改。您可以随意下载我到目前为止的项目:
摘要
在本章中,我们取得了巨大的进步。我们新的类组织将在整本书的进程中为我们提供良好的服务。我们学习了如何使用协议在类之间强制一致性,将游戏对象封装到不同的类中,并探讨了在地面节点宽度上的平铺纹理。最后,我们从GameScene中清理了一些之前的学习代码,并使用新的类系统生成所有游戏对象。
我们还将物理模拟应用于我们的游戏。我们在 SpriteKit 中强大的物理系统中只是触及了表面——我们将在第七章实现碰撞事件中深入探讨自定义碰撞事件——但我们已经获得了相当多的功能。我们探索了三种类型的物理体,并研究了您可以使用来微调游戏对象物理行为的各种物理属性。然后,我们通过让蜜蜂相互碰撞并观察结果来将所有辛勤工作付诸实践。
接下来,我们将尝试几种控制方案,并将玩家移动到游戏世界中。这是一个令人兴奋的补充;我们的项目将开始感觉像一款真正的游戏。添加控制。
第四章。添加控件
玩家通过非常有限的操作来控制移动游戏。通常,游戏只包含一个机制:在屏幕上任何地方轻触以跳跃或飞行。与此相对的是拥有数十种按钮组合的家用游戏机控制器。在如此少的动作下,保持用户通过光滑、有趣的控件保持兴趣对于游戏的成功至关重要。
在本章中,你将学习实现从应用商店中出现的几种流行控制方案。首先,我们将尝试倾斜控制;设备的物理方向将决定玩家的飞行方向。然后,我们将连接我们的精灵节点上的 onTap 事件。最后,我们将实现并完善我们游戏中飞行的简单控制方案:在屏幕上任何地方轻触以飞得更高。你可以结合这些技术,在你的未来游戏中创建独特且有趣的控件。
本章包括以下主题:
-
为
Player类进行飞行改造 -
使用 Core Motion 轮询设备移动
-
连接精灵
onTap事件 -
教导我们的企鹅学会飞行
-
改进相机
-
当玩家向前移动时循环地面
为玩家类进行飞行改造
在我们可以对玩家输入做出反应之前,我们需要执行一些快速设置任务。我们将移除一些旧的测试代码,并为 Player 类添加一个物理体。
蜜蜂饲养者
首先,清理上一章中旧的蜜蜂物理测试。打开 GameScene.swift,找到 didMoveToView,找到底部两行;一行设置了 bee2 的质量,另一行对 bee2 应用了冲量。删除这些行。
更新玩家类
我们需要为 Player 类提供一个自己的 update 函数。我们希望在 Player 中存储与玩家相关的逻辑,并且我们需要它在每一帧之前运行。
-
打开
Player.swift并在Player中添加以下函数:func update() { } -
在
GameScene.swift中,在GameScene类的底部添加以下代码:override func update(currentTime: NSTimeInterval) { player.update() }
完美。GameScene 类将在每次更新时调用 player class update 函数。
移动地面
在上一章中,我们最初将地面设置得比必要的位置更高,以确保它在所有屏幕尺寸上都能显示。现在,由于玩家将很快开始移动,将相机带到他们想去的地方,我们可以将地面移动到其最终位置。
在 GameScene.swift 中,找到定义 groundPosition 常量的行,并将 y 值从 100 更改为 30:
let groundPosition = CGPoint(x: -self.size.width, y: 30)
为玩家分配物理体
我们将使用物理力来移动屏幕上的玩家。要应用这些力,我们必须首先为玩家精灵添加一个物理体。
从纹理创建物理体形状
当游戏玩法允许时,您应该使用圆形来定义您的物理体 - 它是物理模拟中最有效的形状,并产生最高的帧率。然而,皮埃尔形状的准确性对我们游戏玩法非常重要,而圆形并不是他的形状的理想选择。相反,我们将根据他的纹理分配一种特殊的物理体类型。
苹果在 Xcode 6 中引入了使用不透明纹理像素定义物理体形状的能力。这是一个方便的补充,因为它允许我们轻松创建极其精确的精灵形状。使用这些由纹理驱动的物理体会有性能损失;使用这些纹理驱动的物理体计算成本很高。您应该谨慎使用,仅在对您最重要的精灵上使用。
要创建皮埃尔的物理体,在Player.swift的spawn函数底部添加以下代码:
// Create a physics body based on one frame of Pierre's animation.
// We will use the third frame, when his wings are tucked in,
// and use the size from the spawn function's parameters:
let bodyTexture = textureAtlas.textureNamed("pierre-flying-3.png")
self.physicsBody = SKPhysicsBody(
texture: bodyTexture,
size: size)
// Pierre will lose momentum quickly with a high linearDamping:
self.physicsBody?.linearDamping = 0.9
// Adult penguins weigh around 30kg:
self.physicsBody?.mass = 30
// Prevent Pierre from rotating:
self.physicsBody?.allowsRotation = false
运行项目后,地面看起来会上升至皮埃尔处。因为我们已经给他一个物理体,所以他现在受到重力的作用。皮埃尔实际上正在下降网格,而摄像头正在调整以保持他居中。现在这很好;稍后我们将给他飞向天空的工具。接下来,让我们学习如何根据物理设备的倾斜移动一个角色。
使用核心运动轮询设备移动
苹果提供了核心运动框架,以暴露 iOS 设备在物理空间中的精确方向信息。我们可以使用这些数据,当用户将设备倾斜到他们想要移动的方向时,在屏幕上移动我们的玩家。这种独特的输入风格为移动游戏提供了新的游戏玩法机制。
注意
您需要一台物理 iOS 设备来完成这个核心运动部分。Xcode 中的 iOS 模拟器无法模拟设备移动。然而,这部分只是一个学习练习,并不是完成我们正在构建的游戏所必需的。我们的最终游戏将不会使用核心运动。如果您无法使用物理设备进行测试,请随意跳过核心运动部分。
实现核心运动代码
检测设备方向非常简单。我们将在每次更新时检查设备位置,并给我们的玩家应用适当的力。按照以下步骤实现核心运动控制:
-
在
GameScene.swift中,靠近顶部,在import SpriteKit行下方添加一个新的import语句:import CoreMotion -
在
GameScene类中,添加一个名为motionManager的新常量,并实例化一个CMMotionManager对象:let motionManager = CMMotionManager() -
在
GameScene函数didMoveToView中,在底部添加以下代码。这会让核心运动知道我们想要轮询方向数据,因此它需要开始报告数据:self.motionManager.startAccelerometerUpdates() -
最后,在
update函数的底部添加以下代码以轮询方向,构建适当的向量,并给玩家的角色应用物理力:// Unwrap the accelerometer data optional: if let accelData = self.motionManager.accelerometerData { var forceAmount:CGFloat var movement = CGVector() // Based on the device orientation, the tilt number // can indicate opposite user desires. The // UIApplication class exposes an enum that allows // us to pull the current orientation. // We will use this opportunity to explore Swift's // switch syntax and assign the correct force for the // current orientation: Switch UIApplication.sharedApplication().statusBarOrientation { case .LandscapeLeft: // The 20,000 number is an amount that felt right // for our example, given Pierre's 30kg mass: forceAmount = 20000 case .LandscapeRight: forceAmount = -20000 default: forceAmount = 0 } // If the device is tilted more than 15% towards complete // vertical, then we want to move the Penguin: if accelData.acceleration.y > 0.15 { movement.dx = forceAmount } // Core Motion values are relative to portrait view. // Since we are in landscape, use y-values for x-axis. else if accelData.acceleration.y < -0.15 { movement.dx = -forceAmount } // Apply the force we created to the player: player.physicsBody?.applyForce(movement) }
运行项目。你可以通过倾斜你的设备到你想要移动的方向来滑动 Pierre 横越冰面。干得好——我们成功实现了我们的第一个控制系统。
小贴士
注意,当你将 Pierre 向任何方向移动得太远时,他会穿过地面。在本章的后面部分,我们将改进地面,使其不断重新定位以覆盖玩家下方的区域。
这是一个使用 Core Motion 数据进行玩家移动的简单示例;我们不会在我们的最终游戏中使用这种方法。尽管如此,你仍然可以将这个示例扩展到你自己游戏中的高级控制方案。
检查点 4-A
要下载我的项目,包括 Core Motion 代码,请访问此地址:
www.thinkingswiftly.com/game-development-with-swift/chapter-4
连接精灵的 onTap 事件
你的游戏通常会需要当玩家点击特定精灵时运行代码的能力。我喜欢实现一个包含你游戏中所有精灵的系统,这样你就可以为每个精灵添加点击事件,而无需构建额外的结构。我们已经在所有采用 GameSprite 协议的类中实现了 onTap 方法;我们还需要将场景连接起来,以便在玩家点击精灵时调用这些方法。
注意
在我们继续之前,我们需要移除 Core Motion 代码,因为我们不会在最终游戏中使用它。一旦你完成对 Core Motion 示例的探索,请按照上一节中的项目符号反向操作将其从游戏中移除。
在 GameScene 中实现 touchesBegan
SpriteKit 每次屏幕被触摸时都会调用我们场景的 touchesBegan 函数。我们将读取触摸的位置并确定该位置的精灵节点。我们可以检查被触摸的节点是否采用我们的 GameSprite 协议。如果是,这意味着它必须有一个 onTap 函数,然后我们可以调用它。将以下 touchesBegan 函数添加到 GameScene 类中——我喜欢将其放置在 didSimulatePhysics 函数下方:
override func touchesBegan(touches: Set<NSObject>, withEvent
event: UIEvent) {
for touch in (touches as! Set<UITouch>) {
// Find the location of the touch:
let location = touch.locationInNode(self)
// Locate the node at this location:
let nodeTouched = nodeAtPoint(location)
// Attempt to downcast the node to the GameSprite protocol
if let gameSprite = nodeTouched as? GameSprite {
// If this node adheres to GameSprite, call onTap:
gameSprite.onTap()
}
}
}
这就是我们连接我们在制作的游戏对象类上实现的全部 onTap 函数所需做的。当然,所有这些 onTap 函数目前都是空的;我们现在将添加一些功能来展示效果。
超乎生活
打开你的 Bee.swift 文件并定位到 onTap 函数。暂时地,当点击时我们将蜜蜂扩展到巨大的尺寸,以演示我们已经正确地连接了 onTap 函数。在蜜蜂的 onTap 函数内添加以下代码:
self.xScale = 4
self.yScale = 4
运行项目并点击蜜蜂。它们将扩展到原来的四倍大小,如下面的截图所示:

哦不——巨大的蜜蜂!这个例子表明我们的 onTap 函数是工作的。你可以从 Bee 类中移除你添加的缩放代码。我们将保留 GameScene 中的 onTap 连接代码,这样我们就可以稍后使用点击事件。
教我们的企鹅飞翔
让我们实现企鹅的控制方案。玩家可以点击屏幕上的任何位置使皮埃尔飞得更高,松开手指让他落下。我们将进行相当多的更改——如果你需要帮助,请参考本章末尾的检查点。首先修改Player类;按照以下步骤为我们的Player准备飞行:
-
在
Player.swift中,直接向Player类添加一些新属性:// Store whether we are flapping our wings or in free-fall: var flapping = false // Set a maximum upward force. // 57,000 feels good to me, adjust to taste: let maxFlappingForce:CGFloat = 57000 // Pierre should slow down when he flies too high: let maxHeight:CGFloat = 1000 -
到目前为止,皮埃尔默认一直在拍打翅膀。相反,我们想默认显示翱翔动画,只有在用户按下屏幕时才运行拍打动画。在
spawn函数中,删除运行flyAnimation的行,而是运行soarAnimation:self.runAction(soarAnimation, withKey: "soarAnimation") -
当玩家触摸屏幕时,我们在
Player类的update函数中应用向上的力。记住GameScene每帧调用一次这个update函数。在update中添加以下代码:// If flapping, apply a new force to push Pierre higher. if self.flapping { var forceToApply = maxFlappingForce // Apply less force if Pierre is above position 600 if position.y > 600 { // The higher Pierre goes, the more force we // remove. These next three lines determine the // force to subtract: let percentageOfMaxHeight = position.y / maxHeight let flappingForceSubtraction = percentageOfMaxHeight * maxFlappingForce forceToApply -= flappingForceSubtraction } // Apply the final force: self.physicsBody?.applyForce(CGVector(dx: 0, dy: forceToApply)) } // Limit Pierre's top speed as he climbs the y-axis. // This prevents him from gaining enough momentum to shoot // over our max height. We bend the physics for gameplay: if self.physicsBody?.velocity.dy > 300 { self.physicsBody?.velocity.dy = 300 } -
最后,我们将在
Player类中提供两个函数,以便其他类可以开始和停止拍打行为。当GameScene类检测到触摸输入时,将调用这些函数。将以下函数添加到Player类中:// Begin the flap animation, set flapping to true: func startFlapping() { self.removeActionForKey("soarAnimation") self.runAction(flyAnimation, withKey: "flapAnimation") self.flapping = true } // Stop the flap animation, set flapping to false: func stopFlapping() { self.removeActionForKey("flapAnimation") self.runAction(soarAnimation, withKey: "soarAnimation") self.flapping = false }
完美,我们的Player已经准备好飞翔了。现在我们将简单地从GameScene类中调用开始和停止函数。
在GameScene中监听触摸
SKScene类(GameScene从中继承)包含了一些方便的函数,我们可以使用这些函数来监控触摸输入。按照以下步骤连接GameScene类:
-
在
GameScene.swift的touchesBegan函数中,在底部添加以下代码以在用户触摸屏幕时开始Player拍打:player.startFlapping() -
在
touchesBegan下方,在GameScene类中创建两个新函数。这些函数在用户从屏幕上抬起手指或 iOS 通知中断触摸时停止拍打:override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) { player.stopFlapping() } override func touchesCancelled(touches: Set<NSObject>!, withEvent event: UIEvent) { player.stopFlapping() }
微调重力
在测试我们新的飞行代码之前,我们需要进行一项调整。默认的重力设置为-9.8 感觉太真实了。皮埃尔生活在卡通世界里;现实世界的重力有点拖沓。我们可以在GameScene类中调整重力;在didMoveToView函数的底部添加以下行:
// Set gravity
self.physicsWorld.gravity = CGVector(dx: 0, dy: -5)
展开翅膀
运行项目。点击屏幕使皮埃尔飞得更高,松开手指让他落下。玩这个动作;皮埃尔旋转向他的矢量,并在你点击和松开时积累或失去动力。太棒了!你已经成功实现了我们游戏的核心机制。花一分钟时间享受上下飞翔的感觉,就像这个截图所示:

改进相机
我们相机的代码运行良好;它跟随玩家飞行的任何地方。然而,我们可以改进相机以增强飞行体验。在本节中,我们将添加两个新功能:
-
当皮埃尔企鹅飞得更高时,放大相机,增强增加高度的感觉。
-
当玩家下降到屏幕下半部分时,暂停垂直居中。这意味着地面永远不会占据屏幕太多,当皮埃尔飞得更高,相机开始再次跟踪他时,增加了向上切割空气的感觉。
按照以下步骤实现这两个改进:
-
在
GameScene.swift中,在GameScene类中创建一个新变量以存储屏幕的中心点:var screenCenterY = CGFloat() -
在
didMoveToView函数中,使用计算出的屏幕高度的中间值设置这个新变量:// Store the vertical center of the screen: screenCenterY = self.size.height / 2 -
我们需要显著重构
didSimulatePhysics函数。删除现有的didSimulatePhysics函数,并用以下代码替换它:override func didSimulatePhysics() { var worldYPos:CGFloat = 0 // Zoom the world as the penguin flies higher if (player.position.y > screenCenterY) { let percentOfMaxHeight = (player.position.y - screenCenterY) / (player.maxHeight - screenCenterY) let scaleSubtraction = (percentOfMaxHeight > 1 ? 1 : percentOfMaxHeight) * 0.6 let newScale = 1 - scaleSubtraction world.yScale = newScale world.xScale = newScale // The player is above half the screen size // so adjust the world on the y-axis to follow: worldYPos = -(player.position.y * world.yScale - (self.size.height / 2)) } let worldXPos = -(player.position.x * world.xScale - (self.size.width / 3)) // Move the world for our adjustment: world.position = CGPoint(x: worldXPos, y: worldYPos) }
运行项目,然后飞起。随着高度的增加,世界会缩小。当飞近地面时,相机现在也允许皮埃尔潜入屏幕中心以下。以下截图说明了两种极端情况。注意顶部屏幕中的小精灵,皮埃尔飞得更高,相机也拉远。在底部画面中,当皮埃尔接近地面时,相机停止垂直跟随:

这种综合效果为游戏增添了大量光泽,并增加了飞行的乐趣。我们的飞行机制感觉很好。下一步是将皮埃尔推进世界。
推进皮埃尔前进
这种游戏风格通常以恒定速度推进世界。而不是应用力或冲量,我们可以在每次 update 中手动为皮埃尔设置一个恒定速度。打开 Player.swift 文件,并在 update 函数中添加以下代码:
// Set a constant velocity to the right:
self.physicsBody?.velocity.dx = 200
运行项目。我们的主角企鹅会穿过蜜蜂群和世界向前移动。这很好,但你很快会发现,随着皮埃尔向前移动,地面会消失,如这个截图所示:

记得我们的地面宽度只有屏幕宽度的三倍。而不是扩展地面,我们将在适时的间隔移动地面的位置。由于地面由重复的瓷砖组成,有许多机会可以无缝地跳过其位置向前移动。我们只需要找出玩家移动了正确的距离。
跟踪玩家的进度
首先,我们需要跟踪玩家飞行的距离。我们稍后会用到这个数据,用于记录高分。这很容易实现。按照以下步骤跟踪玩家飞行的距离:
-
在
GameScene.swift文件中,向GameScene类添加两个新属性:let initialPlayerPosition = CGPoint(x: 150, y: 250) var playerProgress = CGFloat() -
在
didMoveToView函数中,更新生成玩家的行,使用新的initialPlayerPosition常量而不是旧的硬编码值:// Spawn the player: player.spawn(world, position: initialPlayerPosition) -
在
didSimulatePhysics函数中,更新新的playerProgress属性以包含玩家的新距离:// Keep track of how far the player has flown playerProgress = player.position.x - initialPlayerPosition.x
完美 – 现在我们可以在 GameScene 类中随时访问玩家的进度。我们可以使用行进距离在正确的时间重新定位地面。
地面循环
有许多可能的方法来创建无限地面循环。我们将实现一个简单的解决方案,在玩家行进到大约三分之一的宽度后,将地面向前跳跃。这种方法保证了如果我们的玩家从中间三分之一开始,地面总是覆盖屏幕。
我们将在 Ground 类上创建跳跃逻辑。按照以下步骤实现无限地面:
-
打开
Ground.swift文件,并为Ground类添加两个新属性:var jumpWidth = CGFloat() // Note the instantiation value of 1 here: var jumpCount = CGFloat(1) -
在
createChildren函数中,我们找到从三分之一的孩子瓦片的总宽度,并将其作为我们的jumpWidth。每次玩家行进这段距离时,我们都需要将地面向前跳跃。你只需要在函数底部附近添加一行:在展开纹理的条件下。以下示例中,我将展示整个函数,以提供上下文,新行将以粗体显示:func createChildren() { if let texture = groundTexture { var tileCount:CGFloat = 0 let textureSize = texture.size() let tileSize = CGSize(width: textureSize.width / 2, height: textureSize.height / 2) while tileCount * tileSize.width < self.size.width { let tileNode = SKSpriteNode(texture: texture) tileNode.size = tileSize tileNode.position.x = tileCount * tileSize.width tileNode.anchorPoint = CGPoint(x: 0, y: 1) self.addChild(tileNode) tileCount++ } // Find the width of one-third of the children nodes jumpWidth = tileSize.width * floor(tileCount / 3) } } -
在
Ground类中添加一个名为checkForReposition的新函数,位于createChildren函数下方。场景将在每一帧调用此函数以检查我们是否应该将地面向前跳跃:func checkForReposition(playerProgress:CGFloat) { // The ground needs to jump forward // every time the player has moved this distance: let groundJumpPosition = jumpWidth * jumpCount if playerProgress >= groundJumpPosition { // The player has moved past the jump position! // Move the ground forward: self.position.x += jumpWidth // Add one to the jump count: jumpCount++ } } -
打开
GameScene.swift文件,并在didSimulatePhysics函数的底部添加以下行以调用Ground类的新逻辑:// Check to see if the ground should jump forward: ground.checkForReposition(playerProgress)
运行项目。当皮埃尔向前飞行时,地面看起来会无限延伸。这种循环地面是最终游戏世界的一大步。这可能看起来是为一个简单的效果而做的大量工作,但循环地面很重要,我们的方法将在任何屏幕尺寸上表现良好。干得好!
检查点 4-B
要下载到这一点的项目,请访问此地址:
www.thinkingswiftly.com/game-development-with-swift/chapter-4
摘要
在本章中,我们将技术演示转变为真实游戏的开始。我们添加了大量新的代码。你学习了如何实现三种不同的移动游戏控制方法:物理设备运动、精灵点击事件以及当屏幕被触摸时飞得更高。我们为飞行机制进行了优化,让皮埃尔飞向世界的前方。
你还学习了如何实现两个常见的移动游戏需求:地面循环和更智能的摄像头系统。这两个功能对我们的游戏产生了重大影响。
接下来,我们将为我们关卡添加更多内容。飞行已经很有趣了,但穿过前几个蜜蜂时感觉有点孤单。我们将在 第五章 生成敌人、金币和道具 中给皮埃尔企鹅一些同伴。
第五章:生成敌人、硬币和增强
游戏开发中最有趣和最具创造性的方面之一是为玩家构建可探索的游戏世界。我们的年轻项目在添加控制后开始像可玩游戏一样,下一步是构建更多内容。我们将为新的敌人、可收集的硬币和给予皮埃尔企鹅额外能量的特殊增强创建额外的类。然后我们可以开发一个系统,随着玩家的进步,逐渐生成越来越困难的这些游戏对象的模式。
本章包括以下主题:
-
添加增强星
-
新敌人——疯狂飞虫
-
另一个恐怖——蝙蝠!
-
可怕的幽灵
-
用刀守护地面
-
添加硬币
-
测试新游戏对象
介绍角色阵容
穿上你的安全帽,我们将在本章中编写大量的代码。坚持下去!结果绝对值得努力。来看看本章我们将介绍的新角色阵容:

添加增强星
许多我最喜欢的游戏在玩家捡起星星时都会赋予玩家临时无敌能力。我们将为我们的游戏添加一个高度活跃的增强星。来看看我们的星星:

定位艺术资源
您可以在资产包的Coins和Powerups文件夹中的goods.atlas纹理图集中找到增强星和硬币的艺术资源。现在将goods.atlas纹理图集添加到您的项目中。
添加 Star 类
一旦艺术资源到位,您可以在项目中创建一个名为Star.swift的新 Swift 文件;我们将继续将类组织到不同的文件中。Star类将与之前创建的Bee类相似;它将继承自SKSpriteNode并遵循我们的GameSprite协议。星星将为玩家带来很多力量,因此我们还将给它一个基于SKAction的特殊疯狂动画,使其脱颖而出。
要创建Star类,请在您的Star.swift文件中添加以下代码:
import SpriteKit
class Star: SKSpriteNode, GameSprite {
var textureAtlas:SKTextureAtlas =
SKTextureAtlas(named:"goods.atlas")
var pulseAnimation = SKAction()
func spawn(parentNode:SKNode, position: CGPoint,
size: CGSize = CGSize(width: 40, height: 38)) {
parentNode.addChild(self)
createAnimations()
self.size = size
self.position = position
self.physicsBody = SKPhysicsBody(circleOfRadius:
size.width / 2)
self.physicsBody?.affectedByGravity = false
// Since the star texture is only one frame, set it here:
self.texture =
textureAtlas.textureNamed("power-up-star.png")
self.runAction(pulseAnimation)
}
func createAnimations() {
// Scale the star smaller and fade it slightly:
let pulseOutGroup = SKAction.group([
SKAction.fadeAlphaTo(0.85, duration: 0.8),
SKAction.scaleTo(0.6, duration: 0.8),
SKAction.rotateByAngle(-0.3, duration: 0.8)
]);
// Push the star big again, and fade it back in:
let pulseInGroup = SKAction.group([
SKAction.fadeAlphaTo(1, duration: 1.5),
SKAction.scaleTo(1, duration: 1.5),
SKAction.rotateByAngle(3.5, duration: 1.5)
]);
// Combine the two into a sequence:
let pulseSequence = SKAction.sequence([pulseOutGroup,
pulseInGroup])
pulseAnimation =
SKAction.repeatActionForever(pulseSequence)
}
func onTap() {}
}
太好了!您应该已经熟悉了大部分代码,因为它与我们之前创建的一些类非常相似。让我们继续添加另一个新角色:一只烦躁的飞虫。
添加一个新敌人——疯狂飞虫
皮埃尔企鹅要实现他的目标,不仅需要躲避蜜蜂。在本章中,我们将添加一些新的敌人,首先是MadFly类。疯狂飞虫相当烦躁,正如你所见:

定位敌人资源
您可以在资产包的Enemies文件夹中的enemies.atlas纹理图集中找到我们新敌人的所有艺术资源。现在将这个纹理图集添加到您的项目中。
添加 MadFly 类
MadFly是一个简单的类;它看起来很像Bee代码。创建一个名为MadFly.swift的新 Swift 文件,并输入以下代码:
import SpriteKit
class MadFly: SKSpriteNode, GameSprite {
var textureAtlas:SKTextureAtlas =
SKTextureAtlas(named:"enemies.atlas")
var flyAnimation = SKAction()
func spawn(parentNode:SKNode, position: CGPoint,
size: CGSize = CGSize(width: 61, height: 29)) {
parentNode.addChild(self)
createAnimations()
self.size = size
self.position = position
self.runAction(flyAnimation)
self.physicsBody = SKPhysicsBody(circleOfRadius:
size.width / 2)
self.physicsBody?.affectedByGravity = false
}
func createAnimations() {
let flyFrames:[SKTexture] = [
textureAtlas.textureNamed("mad-fly-1.png"),
textureAtlas.textureNamed("mad-fly-2.png")
]
let flyAction = SKAction.animateWithTextures(flyFrames,
timePerFrame: 0.14)
flyAnimation = SKAction.repeatActionForever(flyAction)
}
func onTap() {}
}
恭喜,你已经成功实现了疯狂飞行的敌人。没有时间庆祝——继续前进,迎接蝙蝠!
另一个恐怖——蝙蝠!
我们在创建新类方面已经进入了一种相当有节奏的状态。现在,我们将添加一只蝙蝠来与蜜蜂一起群飞。蝙蝠体型小,但有一对非常锋利的獠牙:

添加Bat类
要添加Bat类,创建一个名为Bat.swift的文件,并添加以下代码:
import SpriteKit
class Bat: SKSpriteNode, GameSprite {
var textureAtlas:SKTextureAtlas =
SKTextureAtlas(named:"enemies.atlas")
var flyAnimation = SKAction()
func spawn(parentNode:SKNode, position: CGPoint,
size: CGSize = CGSize(width: 44, height: 24)) {
parentNode.addChild(self)
createAnimations()
self.size = size
self.position = position
self.runAction(flyAnimation)
self.physicsBody = SKPhysicsBody(circleOfRadius:
size.width / 2)
self.physicsBody?.affectedByGravity = false
}
func createAnimations() {
// The Bat has 4 animation textures:
let flyFrames:[SKTexture] = [
textureAtlas.textureNamed("bat-fly-1.png"),
textureAtlas.textureNamed("bat-fly-2.png"),
textureAtlas.textureNamed("bat-fly-3.png"),
textureAtlas.textureNamed("bat-fly-4.png"),
textureAtlas.textureNamed("bat-fly-3.png"),
textureAtlas.textureNamed("bat-fly-2.png")
]
let flyAction = SKAction.animateWithTextures(flyFrames,
timePerFrame: 0.06)
flyAnimation = SKAction.repeatActionForever(flyAction)
}
func onTap() {}
}
现在你已经创建了Bat类,还有两个敌人需要添加。我们将在下一个步骤中添加Ghost类。
可怕的幽灵
我们将用另一个可怕的敌人来补充蝙蝠:如这里所示的幽灵:

我们不会通过多个帧进行动画,而是使用动作来动画幽灵的单帧。
添加Ghost类
与其他类一样,在你的项目中创建一个名为Ghost.swift的新文件,然后添加以下代码:
import SpriteKit
class Ghost: SKSpriteNode, GameSprite {
var textureAtlas:SKTextureAtlas =
SKTextureAtlas(named:"enemies.atlas")
var fadeAnimation = SKAction()
func spawn(parentNode:SKNode, position: CGPoint,
size: CGSize = CGSize(width: 30, height: 44)) {
parentNode.addChild(self)
createAnimations()
self.size = size
self.position = position
self.physicsBody = SKPhysicsBody(circleOfRadius:
size.width / 2)
self.physicsBody?.affectedByGravity = false
self.texture =
textureAtlas.textureNamed("ghost-frown.png")
self.runAction(fadeAnimation)
// Start the ghost semi-transparent:
self.alpha = 0.8;
}
func createAnimations() {
// Create a fade out action group:
// The ghost becomes smaller and more transparent.
let fadeOutGroup = SKAction.group([
SKAction.fadeAlphaTo(0.3, duration: 2),
SKAction.scaleTo(0.8, duration: 2)
]);
// Create a fade in action group:
// The ghost returns to full size and transparency.
let fadeInGroup = SKAction.group([
SKAction.fadeAlphaTo(0.8, duration: 2),
SKAction.scaleTo(1, duration: 2)
]);
// Package the groups into a sequence, then a
// repeatActionForever action:
let fadeSequence = SKAction.sequence([fadeOutGroup,
fadeInGroup])
fadeAnimation = SKAction.repeatActionForever(fadeSequence)
}
func onTap() {}
}
完美。我们的幽灵准备就绪,可以行动了。我们已经添加了许多飞行敌人来追逐皮埃尔企鹅穿越天空。我们需要一个地面敌人,它可以阻止玩家在地面以上轻易地前进。接下来,我们将添加Blade类。
守护地面——添加刀片
Blade类将阻止皮埃尔飞得太低。这个敌人类将与其他我们创建的类相似,只有一个例外:我们将基于纹理生成一个物理体。我们一直在使用的物理体圆形在计算上更快,通常足以描述我们敌人的形状;Blade类需要一个更复杂的物理体,考虑到它的半圆形形状和凹凸边缘:

添加Blade类
要添加Blade类,创建一个名为Blade.swift的新文件,并添加以下代码:
import SpriteKit
class Blade: SKSpriteNode, GameSprite {
var textureAtlas:SKTextureAtlas =
SKTextureAtlas(named:"enemies.atlas")
var spinAnimation = SKAction()
func spawn(parentNode:SKNode, position: CGPoint,
size: CGSize = CGSize(width: 185, height: 92)) {
parentNode.addChild(self)
self.size = size
self.position = position
// Create a physics body shaped by the blade texture:
self.physicsBody = SKPhysicsBody(
texture: textureAtlas.textureNamed("blade-1.png"),
size: size)
self.physicsBody?.affectedByGravity = false
// No dynamic body for the blade, which never moves:
self.physicsBody?.dynamic = false
createAnimations()
self.runAction(spinAnimation)
}
func createAnimations() {
let spinFrames:[SKTexture] = [
textureAtlas.textureNamed("blade-1.png"),
textureAtlas.textureNamed("blade-2.png")
]
let spinAction = SKAction.animateWithTextures(spinFrames,
timePerFrame: 0.07)
spinAnimation = SKAction.repeatActionForever(spinAction)
}
func onTap() {}
}
恭喜,Blade类是我们游戏中需要添加的最后一个敌人。这个过程可能看起来很重复——你已经写了很多样板代码——但将我们的敌人分成各自的类可以让每个敌人实现独特的逻辑和行为。随着你的游戏变得越来越复杂,这种结构的优势将变得明显。
接下来,我们添加金币的类。
添加金币
如果有两种价值变化,金币会更有趣。我们将创建:
-
一枚铜币,价值一枚。
-
一枚金币,价值五枚。
两个金币将通过屏幕上的颜色和硬币上的面额文字来区分,如下所示:

创建金币类
我们只需要一个Coin类来创建两种面额。到目前为止,Coin类中的所有内容都应该非常熟悉。要创建Coin类,添加一个名为Coin.swift的新文件,然后输入以下代码:
import SpriteKit
class Coin: SKSpriteNode, GameSprite {
var textureAtlas:SKTextureAtlas =
SKTextureAtlas(named:"goods.atlas")
// Store a default value for the bronze coin:
var value = 1
func spawn(parentNode:SKNode, position: CGPoint,
size: CGSize = CGSize(width: 26, height: 26)) {
parentNode.addChild(self)
self.size = size
self.position = position
self.physicsBody = SKPhysicsBody(circleOfRadius:
size.width / 2)
self.physicsBody?.affectedByGravity = false
self.texture =
textureAtlas.textureNamed("coin-bronze.png")
}
// A function to transform this coin into gold!
func turnToGold() {
self.texture =
textureAtlas.textureNamed("coin-gold.png")
self.value = 5
}
func onTap() {}
}
干得好——我们已经成功添加了所有需要的游戏对象,为我们的最终游戏做好了准备!
组织项目导航器
你可能会注意到这些新类使项目导航器变得杂乱。这是清理导航器的好时机。在项目导航器中右键单击项目,并选择按类型排序,如图所示:

你的项目导航器将根据文件类型进行分段,并按字母顺序排序。这使得在需要时查找文件变得容易得多。
测试新游戏对象
是时候看到我们的辛勤工作付诸实践了。我们现在将向游戏中添加我们每个新类的一个实例。注意,我们在完成后将移除这个测试代码;你可能想给自己留下注释或额外的空间以便于移除。打开GameScene.swift并定位到生成现有蜜蜂的六行代码。在蜜蜂行之后添加此代码:
// Spawn a bat:
let bat = Bat()
bat.spawn(world, position: CGPoint(x: 400, y: 200))
// A blade:
let blade = Blade()
blade.spawn(world, position: CGPoint(x: 300, y: 76))
// A mad fly:
let madFly = MadFly()
madFly.spawn(world, position: CGPoint(x: 50, y: 50))
// A bronze coin:
let bronzeCoin = Coin()
bronzeCoin.spawn(world, position: CGPoint(x: 490, y: 250))
// A gold coin:
let goldCoin = Coin()
goldCoin.spawn(world, position: CGPoint(x: 460, y: 250))
goldCoin.turnToGold()
// A ghost!
let ghost = Ghost()
ghost.spawn(world, position: CGPoint(x: 50, y: 300))
// The powerup star:
let star = Star()
star.spawn(world, position: CGPoint(x: 250, y: 250))
你可能还希望注释掉移动皮埃尔前进的Player类行,这样摄像机就不会快速移动过你的新游戏对象。只是确保你在完成后取消注释。
一旦你准备好了,运行项目。你应该看到整个家族,如图所示:

了不起的工作!我们所有的代码都得到了回报,我们有一大批角色准备行动。
检查点 5-A
下载到这一点的项目,请访问此 URL:
www.thinkingswiftly.com/game-development-with-swift/chapter-5
准备无限飞行
在第六章《生成无限世界》中,我们将通过生成充满这些新游戏对象的战术障碍课程来构建一个无限关卡。我们需要清除所有测试对象,为这个新的关卡生成系统做好准备。一旦你准备好了,从GameScene类中移除我们刚刚添加的生成测试代码。同时,移除我们之前章节中用来生成三只蜜蜂的六行代码。
当你完成时,你的GameScene类的didMoveToView函数应该看起来像这样:
override func didMoveToView(view: SKView) {
// Set a sky-blue background color:
self.backgroundColor = UIColor(red: 0.4, green: 0.6, blue:
0.95, alpha: 1.0)
// Add the world node as a child of the scene:
self.addChild(world)
// Store the vertical center of the screen:
screenCenterY = self.size.height / 2
// Spawn the ground:
let groundPosition = CGPoint(x: -self.size.width, y: 30)
let groundSize = CGSize(width: self.size.width * 3, height: 0)
ground.spawn(world, position: groundPosition, size:
groundSize)
// Spawn the player:
player.spawn(world, position: initialPlayerPosition)
// Set gravity
self.physicsWorld.gravity = CGVector(dx: 0, dy: -5)
}
当你运行项目时,你应该只看到皮埃尔和地面,如图所示:

我们现在准备好构建我们的关卡。
概述
你在本章中为我们游戏添加了完整的角色阵容。回顾一下你所取得的成就;你添加了能量星、铜币和金币、一个幽灵、疯狂的小飞虫、蝙蝠和一把刀。你测试了所有的新类,然后移除了测试代码,以便项目为我们在下一章中将要放置的关卡生成系统做好准备。
我们在构建每个新类上投入了大量的努力。在第六章《生成无限世界》中,世界将变得生动起来,并回报我们的辛勤工作。
第六章. 生成无尽世界
无尽飞行游戏独特的挑战在于以程序方式生成丰富、有趣的游戏世界,其范围延伸到玩家可以飞行的距离。我们将首先探索 Xcode 中的关卡设计概念和工具;Apple 在 Xcode 6 中添加了一个内置关卡设计师,允许开发者在一个场景中直观地排列节点。一旦我们熟悉了 SpriteKit 关卡设计方法,我们将创建一个自定义解决方案来生成我们的世界。在本章中,您将为我们的企鹅游戏构建一个有趣的世界,并学习如何在 SpriteKit 中为任何类型的游戏设计和实现关卡。
本章包括以下主题:
-
使用 SpriteKit 场景编辑器设计关卡
-
为皮埃尔企鹅构建遭遇战
-
将场景集成到游戏中
-
为永无止境的世界循环遭遇战
-
随机添加星级提升
使用 SpriteKit 场景编辑器设计关卡
场景编辑器是 SpriteKit 的一个宝贵补充。以前,开发者被迫硬编码位置值或依赖第三方工具或自定义解决方案。现在,我们可以在 Xcode 中直接布局我们的关卡。我们可以创建节点,附加物理体和约束,创建物理场,并直接从界面中编辑属性。
随意尝试场景编辑器,熟悉其界面。要使用场景编辑器,请向您的游戏添加一个新的场景文件,然后在项目导航器中选择场景。以下是一个您可能为平台游戏构建的简单示例场景:

在这个例子中,我只是在场景中拖动并定位了彩色精灵。如果您正在制作一个简单的游戏,您可以直接在场景编辑器中绘制不需要基于纹理动画的节点。通过在编辑器中编辑物理体,您甚至可以在编辑器中创建整个基于物理的游戏,只需添加几行控制代码。
复杂的游戏需要为每个对象定制逻辑和纹理动画,因此我们将在我们的企鹅游戏中实现一个系统,该系统仅使用场景编辑器作为布局生成工具。我们将编写代码来解析编辑器中的布局数据,并将其转换为本书中创建的游戏类的完整功能版本。这样,我们将以最小的努力将游戏逻辑与数据分离。
将关卡数据与游戏逻辑分离
关卡布局是数据,最好将数据与代码分离。通过将关卡数据分离到场景文件中,您可以提高灵活性。其好处包括:
-
非技术贡献者,如艺术家和设计师,可以在不更改任何代码的情况下添加和编辑关卡。
-
迭代时间得到改善,因为您每次需要查看更改时不需要在模拟器中运行游戏。场景编辑器布局提供即时视觉反馈。
-
每个级别都在一个独特的文件中,这在使用像 Git 这样的源代码控制解决方案时避免合并冲突是理想的。
使用空节点作为占位符
场景编辑器缺乏创建可重用类的能力,也没有将您的代码类链接到场景编辑器节点的方法。相反,我们将使用空节点作为场景编辑器中的占位符,并在代码中使用我们自己的类的实例来替换它们。您经常会看到这种技术的变体。例如,苹果的 SpriteKit 冒险游戏演示使用这种技术进行其部分关卡设计。
您可以在场景编辑器中为节点分配名称,然后在代码中查询这些名称。例如,您可以在场景编辑器中创建名为Bat的空节点,然后在GameScene类中编写代码,将每个名为Bat的节点替换为我们的Bat类的实例。
为了说明这个概念,我们将为企鹅游戏创建我们的第一次相遇。
无尽飞行中的相遇
无尽飞行动作游戏会一直进行,直到玩家失败。它们没有特定的级别;相反,我们将为我们的主角企鹅设计“相遇”。我们可以通过将一次又一次的相遇连接起来,并在需要更多内容时从开始处随机回收,来创建一个无尽的世界。
以下图像说明了基本概念:

一款完成的游戏可能包含 20 个或更多的相遇,以感觉多样化且随机。在本章中,我们将创建三个相遇来充实相遇回收系统。
我们将像对待标准平台游戏或物理游戏中的单独关卡一样,在各自的场景文件中构建每个相遇。
创建我们的第一次相遇
首先,创建一个相遇文件夹组以保持我们的项目有序。在项目导航器中右键单击您的项目,创建一个名为Encounters的新组。然后,在Encounters上右键单击,并添加一个名为EncounterBats.sks的新 SpriteKit 场景文件(从iOS | 资源类别)。
Xcode 会将新的场景文件添加到您的项目中,并打开场景编辑器。您应该看到一个灰色背景和黄色边框,指示新场景的边界。场景默认宽度为 1024 点,高度为 768 点。我们应该更改这些值。如果每个相遇宽度为 1000 像素,高度为 650 点,那么将它们连接起来会更容易。
您可以轻松地在 SKNode 检查器中更改场景的大小值。在场景编辑器的右上角,确保您已通过选择最右边的图标打开 SKNode 检查器,然后更改宽度和高度,如下面的截图所示:

接下来,我们将为Bat类创建第一个占位符节点。按照以下步骤在场景编辑器中创建一个空节点:
-
你可以从对象库中拖动节点。要打开对象库,请看向场景编辑器的右下角并选择圆形图标,如图所示:
![创建我们的第一个遭遇]()
-
将一个空节点拖动到你的场景中。
-
使用右上角的 SKNode 检查器,将你的节点命名为
Bat,如图所示:![创建我们的第一个遭遇]()
你将看到蝙蝠出现在空节点上方。太好了,我们已经创建了一个占位符。我们将重复此过程,直到为皮埃尔企鹅构建一个完整的遭遇。我们不仅可以使用蝙蝠,但我们需要首先定义我们将用于标记每个节点的名称。如果你是在团队中制作游戏,你将想要事先达成一致。以下是我将用于每个游戏对象的标签:
| 游戏对象类 | 场景编辑器节点名称 |
|---|---|
Bat |
蝙蝠 |
Bee |
蜜蜂 |
Blade |
刀片 |
Coin (bronze) |
青铜币 |
Coin (gold) |
金币 |
Ghost |
幽灵 |
MadFly |
疯狂飞虫 |
随意构建你的蝙蝠遭遇。添加更多空节点,并使用标签,直到你对设计满意为止。试着想象企鹅角色在遭遇中飞翔。
在我的遭遇中,我创建了一条通过蝙蝠的简单路径,路径上布满了青铜币,以及一条在蝙蝠下方和刀片上方的更难路径,路径上布满了金币。你可以使用以下图像中的我的蝙蝠遭遇作为灵感:

将场景集成到游戏中
接下来,我们将创建一个新的类来管理我们游戏中的遭遇。将一个新的 Swift 文件添加到你的项目中,并将其命名为EncounterManager.swift。EncounterManager类将遍历我们的遭遇场景,并使用位置数据在游戏世界中创建适当的游戏对象类。在新的文件中添加以下代码:
import SpriteKit
class EncounterManager {
// Store your encounter file names:
let encounterNames:[String] = [
"EncounterBats"
]
// Each encounter is an SKNode, store an array:
var encounters:[SKNode] = []
init() {
// Loop through each encounter scene:
for encounterFileName in encounterNames {
// Create a new node for the encounter:
let encounter = SKNode()
// Load this scene file into a SKScene instance:
if let encounterScene = SKScene(fileNamed:
encounterFileName) {
// Loop through each placeholder, spawn the
// appropriate game object:
for placeholder in encounterScene.children {
if let node = placeholder as? SKNode {
switch node.name! {
case "Bat":
let bat = Bat()
bat.spawn(encounter, position:
node.position)
case "Bee":
let bee = Bee()
bee.spawn(encounter, position:
node.position)
case "Blade":
let blade = Blade()
blade.spawn(encounter, position:
node.position)
case "Ghost":
let ghost = Ghost()
ghost.spawn(encounter, position:
node.position)
case "MadFly":
let madFly = MadFly()
madFly.spawn(encounter, position:
node.position)
case "GoldCoin":
let coin = Coin()
coin.spawn(encounter, position:
node.position)
coin.turnToGold()
case "BronzeCoin":
let coin = Coin()
coin.spawn(encounter, position:
node.position)
default:
println("Name error: \(node.name)")
}
}
}
}
// Add the populated encounter node to the array:
encounters.append(encounter)
}
}
// We will call this addEncountersToWorld function from
// the GameScene to append all of the encounter nodes to the
// world node from our GameScene:
func addEncountersToWorld(world:SKNode) {
for index in 0 ... encounters.count - 1 {
// Spawn the encounters behind the action, with
// increasing height so they do not collide:
encounters[index].position = CGPoint(x: -2000, y:
index * 1000)
world.addChild(encounters[index])
}
}
}
太好了,你刚刚添加了在游戏世界中使用我们的场景文件数据的功能。接下来,按照以下步骤在GameScene类中连接EncounterManager类:
-
在
GameScene类上添加EncounterManager类的新实例作为常量:let encounterManager = EncounterManager() -
在
didMoveToView函数的底部,调用addEncountersToWorld以将每个遭遇节点作为GameScene类世界节点的子节点添加:encounterManager.addEncountersToWorld(self.world) -
由于
EncounterManager类在屏幕外生成遭遇,我们将暂时将我们的第一个遭遇直接移动到起始玩家位置以测试我们的代码。在didMoveToView函数中添加此行:encounterManager.encounters[0].position = CGPoint(x: 300, y: 0)
运行项目。你将看到皮埃尔在新的蝙蝠遭遇中飞翔。你的游戏应该看起来像以下截图:

恭喜你,你已经实现了在场景编辑器中使用占位符节点的核心功能。你可以移除在步骤 3 中添加的定位这个遭遇的行,即添加到遭遇数组中的行(加粗的新行)。接下来,我们将创建一个系统,在皮埃尔企鹅之前重新定位每个遭遇。
检查点 6-A
你可以从这个 URL 下载到这个点的我的项目:
www.thinkingswiftly.com/game-development-with-swift/chapter-6
生成无限遭遇
我们至少需要三个遭遇来无限循环并创建一个永无止境的世界;任何时候可以有任意两个在屏幕上,第三个在玩家前方。我们可以跟踪皮埃尔的进度并重新定位他前方的遭遇节点。
构建更多遭遇
在我们可以实现重新定位系统之前,我们需要构建至少两个更多的遭遇。如果你愿意,可以创建更多;系统将支持任意数量的遭遇。现在,向你的游戏中添加两个额外的场景文件:EncounterBees.sks 和 EncounterCoins.sks。你可以完全用蜜蜂、幽灵、刀片、金币和蝙蝠填充这些遭遇——享受乐趣吧!
为了获得灵感,这里是我的蜜蜂遭遇经历:

这里是我的金币遭遇:

更新 EncounterManager 类
我们必须让 EncounterManager 类了解这些新的遭遇。打开 EncounterManager.swift 文件并将新的遭遇名称添加到 encounterNames 常量中:
// Store your encounter file names:
let encounterNames:[String] = [
"EncounterBats",
"EncounterBees",
"EncounterCoins"
]
我们还需要跟踪在任意给定时间可能出现在屏幕上的遭遇。向 EncounterManager 类添加两个新属性:
var currentEncounterIndex:Int?
var previousEncounterIndex:Int?
在 SKSpriteNode 的 userData 属性中存储元数据
当皮埃尔在世界中移动时,我们将回收遭遇节点,因此我们需要在将其放置在玩家前方之前重置遭遇中的所有游戏对象的功能。否则,皮埃尔之前的遭遇之旅可能会将节点移位。
SKSpriteNode 类提供了一个名为 userData 的属性,我们可以用它来存储有关精灵的任何杂项数据。我们将使用 userData 属性来存储遭遇中每个精灵的初始位置,这样我们就可以在重新定位遭遇时重置精灵。向 EncounterManager 类添加这两个新函数:
// Store the initial positions of the children of a node:
func saveSpritePositions(node:SKNode) {
for sprite in node.children {
if let spriteNode = sprite as? SKSpriteNode {
let initialPositionValue = NSValue(CGPoint:
sprite.position)
spriteNode.userData = ["initialPosition":
initialPositionValue]
// Save the positions for children of this node:
saveSpritePositions(spriteNode)
}
}
}
// Reset all children nodes to their original position:
func resetSpritePositions(node:SKNode) {
for sprite in node.children {
if let spriteNode = sprite as? SKSpriteNode {
// Remove any linear or angular velocity:
spriteNode.physicsBody?.velocity = CGVector(dx: 0,
dy: 0)
spriteNode.physicsBody?.angularVelocity = 0
// Reset the rotation of the sprite:
spriteNode.zRotation = 0
if let initialPositionVal = spriteNode.userData?.valueForKey("initialPosition") as? NSValue {
// Reset the position of the sprite:
spriteNode.position =
initialPositionVal.CGPointValue()
}
// Reset positions on this node's children
resetSpritePositions(spriteNode)
}
}
}
我们想在 init 时调用我们的新 saveSpritePositions 函数,当我们首次生成遭遇时。更新 EncounterManager 的 init 函数,在将遭遇节点添加到遭遇数组中的行下面(加粗的新行):
// Add the populated encounter node to the encounter array:
encounters.append(encounter)
// Save initial sprite positions for this encounter:
saveSpritePositions(encounter)
最后,我们需要一个函数来重置遭遇并在玩家前方重新定位它们。向 EncounterManager 类添加这个新函数:
func placeNextEncounter(currentXPos:CGFloat) {
// Count the encounters in a random ready type (Uint32):
let encounterCount = UInt32(encounters.count)
// The game requires at least 3 encounters to function
// so exit this function if there are less than 3
if encounterCount < 3 { return }
// We need to pick an encounter that is not
// currently displayed on the screen.
var nextEncounterIndex:Int?
var trulyNew:Bool?
// The current encounter and the directly previous encounter
// can potentially be on the screen at this time.
// Pick until we get a new encounter
while trulyNew == false || trulyNew == nil {
// Pick a random encounter to set next:
nextEncounterIndex =
Int(arc4random_uniform(encounterCount))
// First, assert that this is a new encounter:
trulyNew = true
// Test if it is instead the current encounter:
if let currentIndex = currentEncounterIndex {
if (nextEncounterIndex == currentIndex) {
trulyNew = false
}
}
// Test if it is the directly previous encounter:
if let previousIndex = previousEncounterIndex {
if (nextEncounterIndex == previousIndex) {
trulyNew = false
}
}
}
// Keep track of the current encounter:
previousEncounterIndex = currentEncounterIndex
currentEncounterIndex = nextEncounterIndex
// Reset the new encounter and position it ahead of the player
let encounter = encounters[currentEncounterIndex!]
encounter.position = CGPoint(x: currentXPos + 1000, y: 0)
resetSpritePositions(encounter)
}
在 GameScene 类中连接 EncounterManager
我们将在 GameScene 类中跟踪皮埃尔的进度,并在适当的时候调用 EncounterManager 类代码。按照以下步骤连接 EncounterManager 类:
-
向
GameScene类添加一个新属性,以跟踪何时在玩家前方定位下一次遭遇。我们将从150开始,以便立即生成第一个遭遇:var nextEncounterSpawnPosition = CGFloat(150) -
接下来,我们只需在
didSimulatePhysics函数中检查玩家是否移动到这个位置。在didSimulatePhysics的底部添加此代码:// Check to see if we should set a new encounter: if player.position.x > nextEncounterSpawnPosition { encounterManager.placeNextEncounter( nextEncounterSpawnPosition) nextEncounterSpawnPosition += 1400 }
太棒了 - 我们已经添加了所有需要的功能,以在玩家前方无限循环遭遇。运行项目。你应该会看到你的遭遇无限循环在你面前。享受飞越你的辛勤工作!
在随机位置生成星力升级
我们还需要将星力升级添加到世界中。我们可以随机在每 10 次遭遇中生成一个星力,以增加一些额外的兴奋感。按照以下步骤添加星力逻辑:
-
在
GameScene类中添加Star类的新实例作为常量:let powerUpStar = Star() -
在
GameScene didMoveToView函数的任何地方调用星力的spawn函数:// Spawn the star, out of the way for now powerUpStar.spawn(world, position: CGPoint(x: -2000, y: - 2000)) -
在
GameScene didSimulatePhysics函数中,按照以下方式更新你的新遭遇代码:// Check to see if we should set a new encounter: if player.position.x > nextEncounterSpawnPosition { encounterManager.placeNextEncounter( nextEncounterSpawnPosition) nextEncounterSpawnPosition += 1400 // Each encounter has a 10% chance to spawn a star: let starRoll = Int(arc4random_uniform(10)) if starRoll == 0 { if abs(player.position.x - powerUpStar.position.x) > 1200 { // Only move the star if it is off the screen. let randomYPos = CGFloat(arc4random_uniform(400)) powerUpStar.position = CGPoint(x: nextEncounterSpawnPosition, y: randomYPos) powerUpStar.physicsBody?.angularVelocity = 0 powerUpStar.physicsBody?.velocity = CGVector(dx: 0, dy: 0) } } }
再次运行游戏,你应该会看到星力偶尔在遭遇中生成,如下面的截图所示:

检查点 6-B
要下载到这一点的我的项目,请访问此 URL:
www.thinkingswiftly.com/game-development-with-swift/chapter-6
摘要
干得好 - 我们在本章中覆盖了大量的内容。你了解了 Xcode 的新场景编辑器,学会了如何使用场景编辑器来布局占位符节点,并解释了节点数据以在游戏世界中生成游戏对象。然后,你创建了一个系统来循环我们的无尽飞行游戏中的遭遇。
恭喜你;在本章中构建的遭遇系统是我们游戏中最复杂的系统。你现在正式处于一个很好的位置来完成你的第一个 SpriteKit 游戏!
接下来,我们将探讨在游戏对象碰撞时创建自定义事件。我们将在第七章 实现碰撞事件 中添加健康、伤害、金币拾取、无敌状态等功能。
第七章. 实现碰撞事件
到目前为止,我们让 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.swift、Bee.swift、Blade.swift、Ghost.swift 和 MadFly.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 类:
-
首先,我们需要告诉
GameScene类实现SKPhysicsContactDelegate协议。SpriteKit 就可以在接触事件发生时通知GameScene类。将GameScene类声明行修改为如下所示:class GameScene: SKScene, SKPhysicsContactDelegate { -
我们将通过将
GameScene physicsWorld contactDelegate属性设置为GameScene类来告诉 SpriteKit 通知GameScene类接触事件。在GameScene didMoveToView函数的底部添加以下行:self.physicsWorld.contactDelegate = self -
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 中使用控制台,请按照以下简单步骤查看它:
-
在 Xcode 的右上角,确保调试区域已开启,如图所示:
![查看控制台输出]()
-
在 Xcode 的右下角,确保控制台已开启,如图所示:
![查看控制台输出]()
测试我们的接触代码
现在您可以看到控制台输出了,运行项目。当您将皮埃尔飞入各种游戏对象时,应该会在控制台中看到我们的 println 字符串。您的控制台应该看起来像这样:

恭喜——如果您在控制台中看到了接触输出,您已经完成了我们接触系统的结构。
您可能会注意到飞入金币会产生奇怪的碰撞行为,我们将在本章后面增强这一点。接下来,我们将为每种接触类型添加游戏逻辑。
检查点 7-A
要下载到这一点的项目,请访问此 URL:
www.thinkingswiftly.com/game-development-with-swift/chapter-7
玩家生命值和伤害
首个自定义接触逻辑是玩家伤害。我们将为玩家分配健康点数,并在受伤时扣除。当玩家耗尽健康点数时,游戏结束。这是我们游戏玩法的基础机制之一。按照以下步骤实现健康逻辑:
-
在
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 -
在
update函数中,更改移动玩家通过世界的代码以使用新的forwardVelocity属性:// Set a constant velocity to the right: self.physicsBody?.velocity.dx = self.forwardVelocity -
在
startFlapping函数的非常开始处添加此行,以防止玩家在死亡时飞得更高:if self.health <= 0 { return } -
在
stopFlapping函数的非常开始处添加相同的行,以防止在死亡后运行飞翔动画:if self.health <= 0 { return } -
向
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 } -
向
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) } } -
打开
GameScene.swift文件。在didBeginContact函数中,更新与敌人接触时触发的 switch 案例:case PhysicsCategory.enemy.rawValue: println("take damage") player.takeDamage() -
当我们撞击地面时,我们也会受到伤害。以相同的方式更新地面情况:
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
运行项目。在受到伤害后,你的企鹅应该逐渐消失并能够穿过敌人,如图所示:

我们开始看到我们辛勤工作的良好成果。注意企鹅在无敌状态下可以穿过敌人,但仍然与金币、星星和地面发生碰撞。接下来,我们将添加一个游戏结束动画。
游戏结束动画
当企鹅的生命值耗尽时,我们将创建一个有趣且夸张的死亡动画。当皮埃尔失去最后一点生命值时,他将悬挂在空中,放大体型,翻转到背部,然后最终跌落到地面。为了实现这个动画,在 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
])
运行项目并与三个敌人碰撞。你会看到如截图所示的喜剧死亡动画播放:

可怜的皮埃尔企鹅!你很好地实现了伤害和死亡动画。接下来,我们将处理硬币接触事件上的硬币收集。
收集硬币
作为玩家的主要目标之一,收集硬币应该是我们游戏中最令人愉快的方面之一。当玩家接触硬币时,我们将创建一个奖励动画。按照以下步骤实现硬币收集:
-
在
GameScene.swift中,向GameScene类添加一个新属性:var coinsCollected = 0 -
在
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) } -
在
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) }
干得好!运行项目并尝试收集一些硬币。你会看到硬币执行它们的收集动画。游戏将跟踪你收集的硬币数量,并将数字打印到控制台。玩家目前还看不到这个数字;我们将在下一章中在游戏屏幕上添加一个文本计数器。接下来,我们将实现升级星的游戏逻辑。
升级星逻辑
当玩家接触星星时,我们将授予玩家短暂的不可伤害状态,并给予玩家极大的速度以通过遭遇。按照以下步骤实现升级:
-
在
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") } -
在
GameScene类的didBeginContact函数中,在升级情况下调用新的函数:case PhysicsCategory.powerup.rawValue: player.starPower()
你可能会发现增加星星升级的生成率来测试很有帮助。记住,我们在 GameScene 的 didSimulatePhysics 函数中生成一个随机数,以确定星星生成的频率。为了更频繁地生成星星,注释掉生成随机数的行,并用硬编码的 0 替换它,如下所示(新代码用粗体标出):
//let starRoll = Int(arc4random_uniform(10))
let starRoll = 0
if starRoll == 0 {
太好了,现在测试星星升级会很容易。运行项目并找到一个星星。企鹅应该放大体型并开始向前冲,在经过时吹飞敌人,如截图所示:

在你继续之前,记得将星星生成代码改回随机数,否则星星会生成得太频繁。
检查点 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 个金币并受到一点伤害后):

要实现 HUD,请按照以下步骤操作:
-
首先,我们需要将 HUD 艺术资源添加到游戏中。在资源包中找到
HUD.atlas纹理图集并将其添加到项目中。 -
接下来,我们将创建一个
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") } -
我们需要一个初始化风格的函数来为每个心形创建一个新的
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) } } -
我们还需要一个函数,使得
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 } } -
我们还需要一个函数来更新玩家生命值变化时的心形图形。向
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) } } } -
我们的
HUD类已经完成。接下来,我们将在GameScene类中连接它。打开GameScene.swift文件,并向GameScene类添加一个新属性,实例化HUD类的一个实例:let hud = HUD() -
我们需要将
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 -
我们准备向
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) -
最后,每当玩家收集到一个金币时,我们将更新
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) } -
运行项目,你应该会看到你的金币计数器和健康仪表出现在左上角,如这个截图所示:
![添加抬头显示]()
干得好!我们的 HUD 已经完成。接下来,我们将构建我们的背景层。
视差背景层
通过绘制单独的背景层并将它们以不同的速度移动过相机,视差为你的游戏增加了深度感。非常慢的背景给人一种距离感,而快速移动的背景看起来似乎非常接近玩家。我们可以通过用越来越不饱和的颜色绘制远处的物体来增强效果。
在我们的游戏中,我们将通过将背景附加到世界并随着世界向左移动而缓慢地将背景推向右侧来实现视差效果。当世界向左移动(带着背景一起移动)时,我们将背景的 x 位置向右移动,以便总移动距离小于正常游戏对象。结果将是背景层看起来比游戏中的其他部分移动得更慢,因此看起来更远。
此外,每个背景将只有 3000 点宽,但将在精确的间隔内向前跳跃以无缝循环,类似于 Ground 类。
添加背景资源
首先,按照以下步骤添加艺术作品:
-
在 Xcode 中打开你项目中的
Images.xcassets文件。 -
在提供的游戏资源中,在
Backgrounds文件夹中找到四个背景图像。 -
将四个背景拖放到
Images.xcassets文件的左侧面板中。
你应该会看到背景如这里所示出现在左侧面板中:

实现背景类
我们需要一个新类来管理视差和无缝循环的重定位逻辑。我们可以为每个背景层实例化一个 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函数,以便在每一帧之前重新定位背景。按照以下步骤连接背景:
-
在
GameScene类本身上创建一个新的数组属性来存储我们的背景,如下所示:var backgrounds:[Background] = [] -
在
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) -
最后,在
didSimulatePhysics函数的底部添加以下代码,以便在每一帧之前重新定位背景:// Position the backgrounds: for background in self.backgrounds { background.updatePosition(playerProgress) } -
运行项目。您应该看到四个背景图像作为单独的层在动作后面移动,并带有视差效果。此截图显示了背景在您的游戏中应该出现的样子:
![在 GameScene 类中连接背景]()
小贴士
如果您正在使用 iOS 模拟器测试您的游戏,在向游戏中添加这些大型背景纹理后,帧率降低是正常的。游戏仍然可以在 iOS 设备上良好运行。
太棒了!你已经成功实现了你的背景系统。背景让皮埃尔的企鹅世界感觉更加完整,增加了游戏的沉浸感。接下来,我们将使用粒子发射器在皮埃尔飞行时在其身后添加一条轨迹——这是一个有趣的添加,有助于玩家掌握控制。
检查点 8-A
要下载到这一点的项目,请访问此 URL:
www.thinkingswiftly.com/game-development-with-swift/chapter-8
利用 SpriteKit 的粒子系统
SpriteKit 包含一个强大的粒子系统,这使得向游戏中添加令人兴奋的图形变得容易。粒子发射器节点创建了许多图像的小实例,这些实例组合在一起创建了一个看起来很棒的效果。您可以使用发射器节点生成雪、火、火花、爆炸、魔法和其他有用的效果,这些效果在其他情况下可能需要大量的工作。
对于我们的游戏,您将学习如何使用发射器节点在皮埃尔企鹅飞行时在其身后创建一条小点轨迹,这使得玩家更容易了解他们的点击如何影响皮埃尔的飞行路径。
当我们完成时,皮埃尔的轨迹将看起来像这样:

添加圆形粒子资源
每个粒子系统都会发射单个图像的多个版本,以创建累积的粒子效果。在我们的例子中,这个图像是一个简单的圆圈。要将圆圈图像添加到游戏中,请按照以下步骤操作:
-
在 Xcode 中打开
Images.xcassets文件。 -
在提供的游戏资源中找到
Particles文件夹中的dot.png图像。 -
将图像文件拖放到
Images.xcassets左侧面板中。
创建 SpriteKit 粒子文件
Xcode 提供了一个出色的 UI 用于创建和编辑粒子系统。要使用 UI,我们将向我们的项目添加一个新的SpriteKit 粒子文件。按照以下步骤添加新文件:
-
首先,向你的项目添加一个新文件,并定位到SpriteKit 粒子文件类型。你可以在这个资源类别下找到这个模板,如图所示:
![创建 SpriteKit 粒子文件]()
-
在以下提示中,选择雪花作为粒子模板。
-
将文件命名为
PierrePath.sks并点击创建以将新文件添加到你的项目中。
Xcode 将在主框架中打开新的粒子发射器,它应该看起来像这样:

在 Xcode 的粒子编辑器中预览雪花模板
小贴士
在撰写本文时,Xcode 的粒子编辑器仍然有些古怪。如果你在中间看不到白色雪花粒子效果,请尝试在深灰色中心区域点击任何位置以重新定位粒子发射器 - 有时它不会从预期的位置开始。
这对于测试不与旧粒子重叠的设置更改也很有用。只需在编辑器中点击任何位置即可重新定位发射器。
确保你打开了右侧侧边栏,通过在 Xcode 右上角点亮工具按钮,如图所示:

你可以使用工具栏编辑粒子发射器的动画质量。你可以编辑几个属性:粒子的数量、粒子的寿命、粒子移动的速度、粒子缩放的大小,等等。这是一个非常棒的工具,因为你可以立即看到你的更改的反馈。请随意通过更改粒子属性进行实验。
配置路径粒子设置
要创建 Pierre 的点状轨迹,更新你的粒子设置以匹配此截图所示的设置:

当你的编辑器显示一个没有明显运动的微小白色圆圈时,你就有了正确的设置。
将粒子发射器添加到游戏中
我们将把新的发射器连接到 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 背后拖曳的白色点,如图所示:

干得好。现在玩家可以看到他们飞行的路径,这既有趣又有教育意义。点的反馈将帮助玩家学习控制系统的灵敏度,从而更快地掌握游戏。
这只是你可以使用粒子发射节点创建的许多特殊效果之一。现在你知道了如何创建、编辑和在世界中放置粒子发射器,你可以探索其他创造性的可能性。其他有趣的想法包括皮埃尔碰到敌人时产生的火花,或者在背景中轻轻飘落的雪花。
游戏开始时提供安全保障
你可能已经注意到,当你启动游戏时,皮埃尔企鹅会迅速跌落到地上,这并不是很有趣。相反,我们可以在游戏开始时将皮埃尔发射到一个优雅的循环弧线,给玩家一个准备飞行的时间。要做到这一点,打开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 组件来构建我们的主菜单。我们将在新文件中创建一个新的场景用于主菜单,然后使用代码放置背景精灵节点、标志文本节点和按钮精灵节点。让我们首先将菜单场景添加到项目中,并构建出节点。
创建菜单场景和菜单节点
要创建菜单场景,请按照以下步骤操作:
-
我们将为菜单使用一个新的背景图片。让我们将其添加到我们的项目中。
-
在资源包的
Backgrounds文件夹中定位Background-menu.png。 -
在 Xcode 中打开
Images.xcassets,然后将Background-menu.png拖放到Images.xcassets中,使其在项目中可用。
-
-
在项目中添加一个名为
MenuScene.swift的新 Swift 文件。 -
添加以下代码以创建
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) { } } -
接下来,我们需要配置一些场景属性。在新的场景的
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) -
我们需要在菜单的顶部附近绘制游戏名称。在
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) -
现在我们将添加开始按钮。开始按钮是由按钮图形的
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) -
最后,我们将使开始按钮进行脉冲式进出,以增加菜单的动感和兴奋感。在
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类开始。按照以下步骤在游戏开始时启动菜单:
-
打开
GameViewController.swift文件,找到viewWillLayoutSubviews函数。 -
将整个
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) }
运行项目,你应该会看到应用以你的新主菜单启动,这看起来就像下面的截图:

了不起的工作!接下来,我们将连接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类来在游戏结束时显示重启按钮。我们还将包括一个更小的按钮,用于将玩家返回到主菜单。此菜单将显示在动作上方,如下面的截图所示:

扩展 HUD
首先,我们需要在HUD类中创建和绘制我们新的按钮节点。按照以下步骤添加节点:
-
打开
HUD.swift文件,并在HUD类中添加两个新属性,如下所示:let restartButton = SKSpriteNode() let menuButton = SKSpriteNode() -
在
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) -
我们故意没有将这些节点作为
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()
}
我们通过遍历节点树来访问GameScene。Player节点的父节点是world节点,world节点的父节点是GameScene类。
运行项目并死亡。你应该会看到死亡后出现两个新按钮,如下所示:

干得好。按钮显示正确,但当我们点击它们时,还没有任何反应。为了完成我们的重启菜单,我们只需在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文件夹像其他资源一样出现在你的项目中。
播放背景音乐
首先,我们将添加背景音乐。我们希望音乐无论玩家当前查看哪个场景都能播放,因此我们将从视图控制器本身播放音乐。要播放音乐,请按照以下步骤操作:
-
打开
GameViewController.swift文件,在现有导入语句下方添加以下import语句,以便我们访问AVFoundation类:import AVFoundation -
在
GameViewController类中,添加以下属性以存储我们的AVAudioPlayer:var musicPlayer = AVAudioPlayer() -
在
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 类
首先,我们将在玩家每次捡到硬币时添加一个快乐的声音。要添加硬币音效,请按照以下步骤操作:
-
打开
Coin.swift文件,并在Coin类中添加一个新属性以缓存硬币声音动作:let coinSound = SKAction.playSoundFileNamed("Sound/Coin.aif", waitForCompletion: false) -
定位
collect函数,并在函数底部添加以下行以播放声音:// Play the coin sound: self.runAction(coinSound)
这就是每次玩家捡到硬币时播放硬币声音所需做的全部工作。如果您喜欢,现在可以运行项目来测试它。
小贴士
为了避免基于内存的崩溃,缓存每个 playSoundFileNamed 动作对象并在每次想要播放声音时重新运行相同的对象非常重要,而不是为每次播放创建一个新的 SKAction 对象实例。
将增强效果和受伤声音效果添加到 Player 类
当玩家找到星星增强效果时,我们将播放一个令人兴奋的声音,当玩家受到伤害时,我们将播放一个受伤的声音。按照以下步骤实现声音:
-
打开
Player.swift文件,并在Player类中添加两个新属性以缓存音效:let powerupSound = SKAction.playSoundFileNamed("Sound/Powerup.aif", waitForCompletion: false) let hurtSound = SKAction.playSoundFileNamed("Sound/Hurt.aif", waitForCompletion: false) -
定位
takeDamage函数,并在底部添加以下行:// Play the hurt sound: self.runAction(hurtSound) -
找到
starPower函数,并在底部添加以下行:// Play the powerup sound: self.runAction(powerupSound)
游戏开始时播放声音
最后,我们将在游戏开始时播放一个声音。按照以下步骤播放此声音:
-
打开
GameScene.swift文件。我们将从didMoveToView函数播放这个声音效果。通常,在属性中缓存声音动作是至关重要的,但我们不需要缓存游戏开始的声音,因为我们将在每个场景加载时只播放一次。 -
在
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 记录:
-
在网页浏览器中,导航到
itunesconnect.apple.com。 -
使用您的 Apple 开发者账户信息登录。
-
当您到达iTunes Connect仪表板时,点击My Apps图标。
-
在左上角,点击+符号并选择New iOS App,如图所示:
![使用 iTunes Connect 注册应用]()
-
在随后的对话框中,找到底部链接,上面写着在开发者门户上注册新的 bundle ID。点击此链接为您应用创建一个 bundle ID。
-
您将到达一个标题为Registering an App ID的页面。这个页面一开始可能看起来令人不知所措,但您只需要填写两个字段。首先,在App Description部分输入您应用的名称。
-
滚动到App ID Suffix部分。请确保选择Explicit App ID,然后从您的 Xcode 项目设置中输入Bundle ID,如图所示:
![使用 iTunes Connect 注册应用]()
-
滚动到App Services部分,并确保 Game Center 选项已经被勾选。
-
在页面底部,点击Continue。然后在随后的确认页面上点击Submit。
-
您现在可以关闭此标签页,返回到 iTunes Connect,从新 iOS 应用屏幕上您离开的地方继续。
小贴士
您刚刚创建的 bundle ID 可能需要一段时间才能在 iTunes Connect 中显示。如果发生这种情况,请休息一下,几分钟后再次尝试。
-
输入您的应用的名称、主要语言、版本和SKU(对公众不可见)。然后选择您刚刚创建的Bundle ID,如图所示:
![使用 iTunes Connect 注册应用]()
-
在右下角点击创建。现在您应该在 iTunes Connect 中看到您应用的概览,它看起来可能像以下截图:
![在 iTunes Connect 中注册应用]()
恭喜,我们不仅更接近配置 Game Center,我们还迈出了将我们的应用提交到应用商店的第一步!
配置 Game Center
现在我们有了 iTunes Connect 应用记录,我们可以告诉苹果我们如何在游戏中使用 Game Center。按照以下步骤配置 Game Center:
-
在您的应用页面上,点击顶部导航中的 Game Center 链接。
-
选择为单款游戏启用,如图所示:
![配置 Game Center]()
-
您将看到一个屏幕,允许您为您的游戏创建新的排行榜和成就。太好了!我们将在本章的后面使用这个页面。
我们已通知苹果我们希望在游戏中使用 Game Center。接下来,我们需要创建一个用于测试目的的沙盒用户账户。
创建测试用户
在应用开发期间,Game Center 使用独立的测试服务器,因此在我们测试时无法使用我们的真实 Apple ID 登录到 Game Center。相反,我们将在 iTunes Connect 中创建一个沙盒账户。
iOS 开发者库的网站指出:“始终为测试 Game Center 中的游戏创建新的测试账户。切勿使用现有的 Apple ID。”
按照以下步骤创建用于测试的 Game Center 沙盒账户:
-
在iTunes Connect中,使用左上角的下拉菜单选择用户和角色,如图所示:
![创建测试用户]()
-
一旦您进入用户和角色页面,请点击屏幕顶部的导航栏中的沙盒测试者。
-
如沙盒测试者页面上的指示,点击+图标添加新用户。
-
按照您的喜好填写测试用户的详细信息。以下是我如何填写我的测试用户信息的:
![创建测试用户]()
-
点击保存按钮创建新用户。
小贴士
确保将您的实时 Apple ID 和沙盒账户分开。如果您使用沙盒账户登录到实时 Game Center 应用,该账户将失效。
太好了!这就是我们开始将 Game Center 集成到游戏中的所有所需步骤。下一步是将 Game Center 与我们的游戏代码集成。我们将从玩家打开我们的应用时验证他们的 Game Center 账户开始。
验证玩家的 Game Center 账户
当我们的应用启动时,我们将检查玩家是否已经登录到他们的 Game Center 账户。如果没有,我们将给他们一个登录的机会。稍后,当我们想要提交高分或成就时,我们可以使用应用启动时收集到的验证信息,而不是打断他们的游戏会话来收集他们的 Game Center 信息。
按照以下步骤在应用启动时验证玩家的 Game Center 账户:
-
我们将在
GameViewController类中工作,所以请在 Xcode 中打开GameViewController.swift文件。 -
在文件顶部添加一个新的
import语句,以便我们可以使用GameKit框架:import GameKit -
在
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 } } } -
在
GameViewController类的viewWillLayoutSubviews函数底部,添加对新创建的authenticateLocalPlayer函数的调用:authenticateLocalPlayer(menuScene)
运行你的项目。你应该会看到游戏中心动画进入,请求你的凭证,如下所示:

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

如果你看到这个欢迎回来横幅,你就成功实现了游戏中心认证代码。接下来,我们将在菜单中添加一个排行榜按钮,以便玩家可以在我们的应用中查看他们的进度。
在我们的游戏中打开游戏中心
如果用户已验证,我们将在MenuScene类中添加一个按钮,以便他们可以在我们的游戏中打开排行榜并查看成就。或者,玩家始终可以使用 iOS 中的游戏中心应用查看他们的进度。
按照以下步骤在菜单场景中创建排行榜按钮:
-
在 Xcode 中打开
MenuScene.swift文件。 -
在文件顶部添加一个新的
import语句,以便我们可以使用GameKit框架:import GameKit -
更新声明
MenuScene类的行,以便我们的类采用GKGameCenterControllerDelegate协议。这允许游戏中心屏幕在玩家关闭游戏中心时通知我们的场景:class MenuScene: SKScene, GKGameCenterControllerDelegate { -
我们需要一个函数来创建排行榜按钮并将其添加到场景中。当游戏中心验证玩家后,我们将调用此函数。在
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) } -
如果玩家已经通过游戏中心进行了验证,我们将从
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() } -
接下来,我们将创建一个实际打开游戏中心的函数。添加一个名为
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) } } -
我们需要添加另一个函数以遵守
GKGameCenterControllerDelegate协议。这个函数名为gameCenterViewDidFinish,当玩家在游戏中心点击完成按钮时,游戏中心将调用它。将此函数添加到MenuScene类中,如下所示:// This hides the game center when the user taps 'done' func gameCenterViewControllerDidFinish (gameCenterViewController: GKGameCenterViewController!) { gameCenterViewController.dismissViewControllerAnimated( true, completion: nil) } -
为了完成
MenuScene代码,我们需要在touchesBegan函数中检查我们的排行榜按钮的点击,以调用showLeaderboard。按照以下方式更新touchesBegan函数的if块(粗体为新代码):if nodeTouched.name == "StartBtn" { self.view?.presentScene(GameScene(size: self.size)) } else if nodeTouched.name == "LeaderboardBtn" { showLeaderboard() } -
接下来,打开
GameViewController.swift并定位到authenticateLocalPlayer函数。 -
更新玩家成功认证后调用的代码块,以在
MenuScene类中调用我们的新createLeaderboardButton函数。这为新认证的人创建排行榜按钮,当他们开始应用程序时。以下是代码示例(粗体为新代码):else if localPlayer.authenticated { // They authenticated successfully menuScene.createLeaderboardButton() }
干得好。运行项目,您应该在游戏中心认证后看到菜单中出现排行榜按钮,如图所示:

太棒了——如果您点击排行榜文本,游戏中心将在游戏中打开。现在您的玩家可以直接从您的游戏中查看排行榜和成就。接下来,我们将在 iTunes Connect 中创建一个排行榜和一个成就来填充游戏中心。
检查点 10-A
要下载到目前为止的项目,请访问此 URL:
www.thinkingswiftly.com/game-development-with-swift/chapter-10
添加高分排行榜
我们将在玩家完成每场比赛后将其分数提交到 Game Center 服务器。第一步是在 iTunes Connect 上注册一个新的排行榜。
在 iTunes Connect 中创建新的排行榜
首先,我们将在 iTunes Connect 中创建我们的排行榜。然后我们可以从我们的代码中连接到这个排行榜并发送新的分数。按照以下步骤在 iTunes Connect 中创建排行榜记录:
-
重新登录到 iTunes Connect,并导航到您的应用程序的游戏中心页面。
-
定位并点击显示添加排行榜的按钮。
-
下一页会询问您想创建哪种类型的排行榜。选择单个排行榜。
-
填写您排行榜的信息。您可以在此处参考我的示例:
![在 iTunes Connect 中创建新的排行榜]()
让我们看看每个字段:
-
参考名称是 iTunes Connect 中排行榜列表的内部使用名称
-
排行榜 ID是我们将在代码中引用的唯一标识符
-
分数格式类型描述了您将要传递的数据类型(通常用于高分的是整数数据)
-
正常排行榜使用分数提交类型为最佳分数,排序顺序为从高到低
-
分数范围是一种反作弊措施,您可以使用它来阻止明显虚假的分数出现在排行榜上
-
-
接下来,点击添加语言按钮。您将在此屏幕上选择排行榜的名称和分数格式。这些字段大部分是自我解释的,但您可以在此处参考我的示例:
![在 iTunes Connect 中创建新的排行榜]()
-
点击保存两次(一次用于语言对话框,一次用于排行榜屏幕)。
你应该回到了游戏中心页面,你的新排行榜在排行榜部分列出。接下来,我们将从我们的游戏代码中推送新的分数到排行榜。
从代码更新排行榜
从代码向排行榜发送新分数很简单。按照以下步骤,每次游戏结束时发送收集到的金币数量到排行榜:
-
在 Xcode 中打开
GameScene.swift。 -
在顶部添加一个
import语句,这样我们就可以在这个文件中使用GameKit框架:import GameKit -
在
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) } }) } } -
在
GameScene类的GameOver函数中,调用新的updateLeaderboard函数:// Push their score to the leaderboard: updateLeaderboard()
运行项目并玩一个游戏,将测试金币分数发送到排行榜。然后,点击返回菜单场景并点击排行榜按钮,在游戏中打开 Game Center。你应该会看到你的第一个分数出现在排行榜上!它看起来可能像这样:

干得好——你已经实现了你的第一个 Game Center 排行榜。接下来,我们将遵循一系列类似的步骤来创建一个收集 500 个金币的成就。
添加成就
成就为你的游戏增添了第二层乐趣,并创造了重玩价值。为了展示一个 Game Center 成就,我们将添加一个收集 500 个金币而不死奖励。
在 iTunes Connect 中创建新的成就
就像排行榜一样,我们首先需要为我们的成就创建一个 iTunes Connect 记录。按照以下步骤创建记录:
-
登录 iTunes Connect 并导航到你的应用的 Game Center 页面。
-
在排行榜列表下方,找到并点击添加成就按钮。
-
填写你成就的信息。以下是我的值:
![在 iTunes Connect 中创建新的成就]()
让我们看看每个字段:
-
参考名称是 iTunes Connect 将用于内部引用成就的名称
-
成就 ID是我们将在代码中引用此成就的唯一标识符
-
你可以为每个成就分配一个点数,这样玩家在收集新的成就时可以获得更多的成就点数
-
隐藏和可多次获得是显而易见的,但你可以在右侧使用问号按钮获取来自苹果的更多信息
-
-
点击添加语言按钮。我们将命名成就并给出描述,就像排行榜过程一样。此外,成就还需要一个图片。图片尺寸可以是 512x512 或 1024x1024。你可以在我们的Assets包下载中找到我使用的图片,在
Extras文件夹中,gold-medal.png。以下是我的值:![在 iTunes Connect 中创建新的成就]()
-
点击 保存 两次(一次用于语言对话框,一次用于成就屏幕)。
太棒了,你应该已经回到了你应用的首页,在 成就 部分列出了你的新成就。接下来,我们将把这个成就集成到游戏中。
从代码更新成就
就像发送排行榜更新一样,我们可以从 GameScene 发送成就更新到 Game Center。按照以下步骤集成我们的 500 枚金币成就:
-
在 Xcode 中打开
GameScene.swift文件。 -
如果你跳过了排行榜部分,你需要在文件的顶部添加一个新的
import语句,这样我们就可以使用GameKit。如果你已经实现了排行榜,你可以跳过这一步:import GameKit -
在
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) } }) } } } -
在
gameOver函数的底部,调用新的checkForAchievements函数:// Check if they earned the achievement: checkForAchievements()
运行项目,如果你敢的话,完成 500 枚金币飞越。当你的游戏结束时,你应该会看到一个横幅宣布你的新成就征服,如下所示:

干得好!你已经将 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资产包。我们将拖放我们的图标到这个文件中,将它们引入到项目中。
按照以下步骤将我们的图标添加到项目中:
-
在 Xcode 中,打开
Images.xcassets文件,在左侧面板中找到AppIcon图像集。 -
将资产包中的图像拖放到相应的图标槽中。你可以将文件作为组拖放,Xcode 将处理它们到正确的槽中。你可以忽略设置图标的图标槽,因为我们的应用不与 iOS 设置集成。当你完成时,你的图标图像集将看起来像这样:
![添加应用图标]()
-
通过点击项目导航器中的你的项目进入你的常规项目设置。找到应用图标源设置,并确保它设置为AppIcon以使用图像包,如图所示:
![添加应用图标]()
我们在 Xcode 中添加图标已经完成。稍后我们还需要上传一些更多尺寸的图标到 iTunes Connect。你可以在真实设备上运行你的项目来查看你的新图标效果。
设计启动屏幕
当用户在设备上点击你的图标时,iOS 会以极快的速度显示你的应用的启动屏幕作为一个简单的预览。这给人一种应用几乎立即加载的错觉。玩家会立即从他们的点击中获得反馈,而你的应用实际上在后台加载。这不是添加徽标、品牌或任何信息的地方。目标是创建一个非常简单的屏幕,看起来像你的应用在内容放置之前的样子。对于 Pierre Penguin,我们将实现一个简单的空白天空蓝色背景,看起来像主菜单在没有内容之前的样子。
按照以下步骤设置你的天空蓝色启动屏幕:
-
在 Xcode 中打开
LaunchScreen.xib文件。你将在界面构建器中看到启动屏幕打开。 -
选择每个现有的文本元素,并使用键盘上的删除键删除每个元素。
-
通过点击启动屏幕的空白区域中的任何位置来选择整个框架。
-
确保你打开了 Xcode 右侧的实用工具栏,并打开属性检查器,如图所示:
![设计启动屏幕]()
-
在右侧栏中找到背景颜色设置,然后点击现有的白色颜色选项以打开颜色选择窗口。
-
选择颜色滑块标签,并输入 RGB 值
102,153,242,如图所示:![设计启动屏幕]()
-
你应该看到整个框架从我们的游戏中变成了天空蓝色。
-
接下来,通过点击项目导航器中的项目名称来输入你的常规设置。就像之前为应用图标所做的,确保启动屏幕文件设置为LaunchScreen:
![设计启动屏幕]()
完美!当我们运行我们的应用时,我们会立即看到天空蓝色,这为从主屏幕到我们完全加载的应用提供了一个更平滑的过渡。
为每个支持的设备截图
有趣的截图会让你的游戏在 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 信息:
-
在您的网络浏览器中打开 iTunes Connect 网站。浏览到我的应用部分,然后点击您的游戏。iTunes Connect 将带您到您游戏页面的版本标签。
-
我们将从截图开始。将每个设备截图拖放到应用视频预览和截图部分的相应槽位中,如图所示:
![最终确定 iTunes Connect 信息]()
-
滚动并填写下一部分的信息:名称、描述、关键词和相关的 URL。这些字段是自我解释的,但您始终可以点击灰色问号圆圈以获取苹果的详细信息。
小贴士
关于关键词:如果您有强大、准确的关键词,用户将更容易找到您的应用。尝试使用您认为人们可能会在 App Store 中输入以引导他们到您的游戏的短语。您限制在 100 个字符内,因此请省略关键词之间的空格。
-
接下来,滚动到通用应用信息部分。在这里,您将上传您的应用图标,输入版本号(
1.0),选择您的应用的 App Store 类别(游戏),并提供您的地址信息。如果您需要关于这些字段中的任何进一步的详细信息,请再次点击灰色问号圆圈。 -
滚动并找到Game Center,然后翻转滑块到开启位置。您需要通过点击蓝色加号图标添加排行榜和成就,如图所示:
![最终确定 iTunes Connect 信息]()
-
最后,滚动到应用审核信息部分,并再次填写您的联系信息。这是为了在苹果员工需要更多信息时审核您的应用。您还可以选择在审核通过后是否希望游戏自动发布到 App Store,或者等待您手动发布以配合您的营销活动。
-
点击右上角的保存。
配置价格
皮埃尔企鹅将免费供所有人游玩,但您可以为您的游戏选择许多定价策略。
小贴士
苹果不断更新 iTunes Connect,我预计价格部分很快将进行重大更新。您的体验可能与这些步骤不完全一致。
按照以下步骤设置您游戏的价格:
-
在您的游戏的 iTunes Connect 页面上,点击顶部导航栏中的价格标签。
-
选择 可用日期、价格层级和教育折扣。以下是我的设置供您参考:
![配置定价]()
-
在右下角点击 保存。
完美!我们的 iTunes Connect 信息已完整并准备好提交到 App Store 审查流程。现在我们只需在 Xcode 中最终确定并上传我们的构建版本。
小贴士
如果您想对您的游戏收费,那么您需要填写在 iTunes Connect 的 协议、税务和银行 部分找到的合同和银行信息。
从 Xcode 上传我们的项目
接下来,我们将创建我们游戏的最终构建版本,验证它是否包含 App Store 所需的所有内容,并将捆绑包上传到 iTunes Connect。
首先,我们将为我们的游戏创建部署存档。当您对项目满意时,使用 产品 菜单并选择 存档...,如图所示:

一旦流程完成,Xcode 将打开您的存档列表。从这里,您可以验证您的应用程序,以确保它包含所有必需的资产和配置文件,以便在 App Store 上发布。按照以下步骤验证您的应用程序并将其上传到 iTunes Connect:
-
点击以下截图所示的 验证 按钮,以验证您的应用程序。
![从 Xcode 上传我们的项目]()
-
下一个屏幕将要求您为您的应用程序选择一个开发团队。如果您是独立开发者,您只需选择自己的名字,如图所示:
![从 Xcode 上传我们的项目]()
-
Xcode 将为您创建一个分发配置文件,然后带您进入一个摘要屏幕。只需点击 验证 按钮:
![从 Xcode 上传我们的项目]()
-
Xcode 将继续验证一切是否为 App Store 准备就绪,这可能需要几分钟。完成后,您应该会看到一个成功消息,如图所示。如果您收到任何错误,您可能缺少 App Store 所需的资产或配置文件。阅读并回复错误消息,并参考 iOS 开发者库、网络搜索或 Stack Overflow 以获取进一步的帮助。
![从 Xcode 上传我们的项目]()
-
点击 完成,然后点击蓝色 提交到 App Store 按钮将存档上传到 iTunes Connect,如图所示:
![从 Xcode 上传我们的项目]()
-
您需要再次点击通过验证步骤,然后最终点击 提交。Xcode 将然后将您的应用程序上传到 iTunes Connect 并显示另一个成功消息。
恭喜!您已成功将您的应用程序上传到 Apple。我们几乎完成了应用程序的提交。接下来,我们将返回到 iTunes Connect,将我们的应用程序推送到审查和批准流程。
在 iTunes Connect 中提交审查
我们已经完成了项目的准备工作,我们准备好将我们的辛勤工作提交给苹果的审核流程。按照以下步骤提交你的应用到苹果:
-
返回到 iTunes Connect 网站,浏览到你的游戏页面(在版本标签页)。
-
滚动到构建部分,并在提交你的应用之前选择点击+添加构建。
![在 iTunes Connect 中提交审核]()
-
使用单选按钮选择你刚刚上传的归档,然后点击完成,如下所示。上传的构建显示在这个列表中可能需要几分钟(有时是几个小时):
![在 iTunes Connect 中提交审核]()
-
在右上角点击保存,然后提交审核按钮应该会亮起蓝色:
![在 iTunes Connect 中提交审核]()
-
点击提交审核,iTunes Connect 将显示包含关于你的游戏的三个最终问题的提交审核页面。苹果想知道你的应用是否使用了加密、第三方内容或广告。我为 Pierre Penguin 回答了所有三个问题都是否定的。准确回答这些问题非常重要,所以如果你不确定如何进行,请在 iTunes Connect 中使用问号图标获取更多信息。
-
在你回答了提交审核问题后,点击右上角的提交。这是提交过程的最后一步。
如果你的应用提交成功,iTunes Connect 将返回到你的应用页面的版本标签页。你会看到应用状态变为等待审核,如下所示:

太棒了!我们已经将我们的游戏提交给了苹果。审核过程通常需要 7-14 天。如果你的游戏没有获得批准就返回,不要气馁,苹果通常要求开发者纠正小问题并重新提交他们的应用。你正在走向在 App Store 看到你的游戏!
摘要
许多独立开发者都挣扎于发布游戏的最后一步。如果你准备好发布一款游戏,你做得很好!在本章中,我们创建了应用图标和我们的启动屏幕,在 iTunes Connect 中最终确定了我们的 App Store 营销信息,使用 Xcode 归档并上传了我们的游戏,并将我们的游戏提交给苹果进行审核。你现在应该对自己的能力有信心,能够将你的游戏发布到 App Store。
在本书的过程中,我们取得了巨大的成就:我们从一个新的项目模板开始,组装了一个完整的 Swift 游戏,直到发布。当我们各自走向不同的道路时,我祝愿你在未来的游戏开发事业中取得巨大成功。我希望你现在对自己的能力有信心,能够用 Swift 开始自己的游戏项目。我期待在 App Store 看到你的作品!










































浙公网安备 33010602011771号