Swift-IOS8-游戏开发学习指南-全-
Swift IOS8 游戏开发学习指南(全)
原文:
zh.annas-archive.org/md5/f8cbbcac477653ce38ee2a4e32c704c8译者:飞龙
前言
在 Xcode 6 中,苹果让想要学习游戏开发的人更容易接触到这项技术。无需额外的安装。制作游戏所需的所有功能都触手可及。你只需一个触摸控制就能为 iOS 设备创建一个简单的游戏,或者你可以创建一个完整的、大预算的游戏用于 OS X。你的想象力是唯一的限制。
苹果的新编程语言 Swift 是所有这些魔法的核心。编程从未如此简单。你无需在每个类之间挣扎于创建接口和实现。告别方括号。然而,它看起来和感觉上就像任何其他编程语言一样,这样你就可以立即开始行动。如果你来自 JavaScript 或 ActionScript 背景,你会感到非常自在。即使你只精通 Objective-C,也无需担心,因为所有游戏开发技术都与 Objective-C 和 Swift 兼容。
在 SpriteKit 中改进了光照和物理效果后,你无需编写额外的代码来让你的游戏栩栩如生。游戏中的场景将拥有适当的光照;你所要做的就是提供光源的位置。你可以提供一个法线贴图,它将根据光源的方向投射阴影。使用诸如 Glyph Designer、Texture Packer 和 Spine 等工具,你可以真正让你的游戏发光发亮,在技术和视觉上脱颖而出。
SceneKit 终于在 iOS 设备上出现了。因此,现在你可以轻松地创建 3D 游戏。你还可以直接在代码中创建纹理几何体、相机和光照。你甚至可以将你在最喜欢的 3D 软件中制作的场景的 COLLADA 文件导入到 SceneKit 中,并看到它们运行而无需添加任何额外的代码。借助 SceneKit 内置的物理和碰撞系统,你可以让你的游戏变得动态。最酷的是,你还可以添加一个 SpriteKit 层用于 2D GUI 和按钮界面。
如果这些还不够,苹果还引入了 Metal,这是一个仅针对苹果设备开发的新图形库。从头开始开发,它大大减少了开销的大小,并将代码更接近 GPU,这样你就可以在屏幕上添加更多内容,同时仍然保持流畅的 60 Hz 帧率。同时,苹果还赋予你使用 OpenGLES 开发游戏的自由。
既然我们的工作已经简化,那就让我们直接开始学习 Swift、SpriteKit、SceneKit 和 Metal 吧。
本书涵盖内容
第一章,入门,展示了如何下载 Xcode 并创建一个 iOS 开发者账号。你还将了解 Swift、Playground、SpriteKit、SceneKit 和 Metal 的概述,以及图形管道的基本理解。
第二章,Swift 基础,涵盖了 Swift 的基础知识,包括不同数据类型、语句、数组、字典、函数、类和可选类型。
第三章, Xcode 简介,演示了你可以使用 Xcode 创建的不同类型的应用程序。你还可以了解 Xcode 的界面,并学习如何在设备上运行应用程序。
第四章, SpriteKit 基础,解释了如何在 SpriteKit 中创建一个基本游戏,同时你将了解 SpriteKit 的基本元素,如精灵、场景和标签。我们还创建了一个基本的物理引擎,并添加了碰撞检测和得分。
第五章, 动画和粒子,展示了如何使用 SpriteKit 内置的动画和粒子系统在游戏中添加动画和粒子效果。
第六章, 音频和视差效果,涵盖了向游戏中添加背景音乐和音效。我们还添加了视差效果,使游戏更具动态感。
第七章, 高级 SpriteKit,教你关于 SpriteKit 的高级功能,如为你的游戏添加光照和物理效果。此外,它还教你如何将 Objective-C 类与 Swift 集成以实现如 Glyph Designer 和 Spine 等工具。
第八章, SceneKit,展示了如何在 SceneKit 中创建一个基本的 3D 游戏。本章教你如何将几何体、光照和相机等对象添加到游戏场景中。你还可以将 COLLADA 文件导入场景,并集成 SpriteKit 界面以添加 GUI 和按钮界面。
第九章, Metal,深入探讨了 Metal 的图形管道和着色器。它还教你如何访问设备、创建顶点和纹理缓冲区,并在屏幕上绘制图像。
第十章, 发布和分发,为你的应用程序分发做准备。你还可以在 iTunesConnect 上创建一个应用程序,本章展示了如何在 iOS App Store 上发布你的应用程序。此外,它还展示了如何创建一个 ad hoc 构建并在任何注册的设备上运行它。
你需要这本书的内容
你需要 Xcode 6 的最新版本和运行 OS X Yosemite 或至少 Maverick 版本 10.9.4 的 Mac。尽管 Xcode 中包含模拟器,但最好有一个运行 iOS 8 的 iOS 设备。对于运行 Metal,必须使用配备 A7 芯片或更高版本的设备,因为它在模拟器上无法运行。
这本书面向的对象
这本书是为想要开发 2D 和 3D 游戏并在 App Store 上发布它们的爱好者以及游戏开发者而写的。这本书也适合想要了解 Metal 图形管道和着色语言的一般图形程序员和资深开发者。
术语约定
在这本书中,您将找到许多不同风格的文本,以区分不同类型的信息。以下是一些这些风格的示例,以及它们含义的解释。
文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名如下所示:“我们可以通过使用include指令来包含其他上下文。”
代码块应如下设置:
var height = 125.3
var name = "The Dude"
var male = true
当我们希望将您的注意力引到代码块的一个特定部分时,相关的行或项目将以粗体显示:
var height = 125.3
var name = "The Dude"
var male = true
任何命令行输入或输出都应如下所示:
# cp /usr/src/asterisk-addons/configs/cdr_mysql.conf.sample
/etc/asterisk/cdr_mysql.conf
新术语和重要词汇以粗体显示。屏幕上显示的词汇,例如在菜单或对话框中,在文本中如下所示:“点击下一步按钮将您移动到下一屏幕。”
注意
警告或重要注意事项以如下框的形式出现。
小贴士
小技巧和窍门看起来像这样。
读者反馈
我们欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正从中受益的标题非常重要。
要发送一般反馈,只需发送电子邮件到<feedback@packtpub.com>,并在邮件主题中提及书名。
如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从www.packtpub.com的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
错误清单
尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现了错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误清单,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击错误提交表单链接,并输入您的错误详细信息来报告它们。一旦您的错误清单得到验证,您的提交将被接受,错误清单将被上传到我们的网站,或添加到该标题的错误部分下的现有错误清单中。任何现有错误清单都可以通过从www.packtpub.com/support中选择您的标题来查看。
海盗行为
互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过 <copyright@packtpub.com> 联系我们,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们作者以及为我们提供有价值内容的能力方面的帮助。
问题
如果您在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。
第一章. 入门
所以你想为 iOS 创建游戏?随着SpriteKit在Xcode 5中的引入,创建 iOS 二维游戏变得轻而易举。之前,甚至在考虑创建游戏之前,你必须考虑使用哪个框架来创建游戏。有那么多框架可供选择,每个都有自己的优缺点。而且,如果你想创建自己的框架呢?在这种情况下,你必须从头开始使用OpenGLES编写它,这需要编写大量的代码仅仅为了显示一个三角形。更不用说使用框架创建三维游戏了,因为大多数框架甚至不支持这一点。
苹果公司通过在 Xcode 6 中提供所有必需的工具,解决了所有这些问题和难题。在 Xcode 6 中,你的想象力才是真正的限制。你可以使用SpriteKit和SceneKit创建 2D 或 3D 游戏。如果你想创建自己的 2D 或 3D 引擎,Metal 可供使用,这使得与GPU(即图形处理单元)的通信变得容易。但如果你是资深开发者,并且已经使用 OpenGL ES 创建过游戏,不用担心!这个选项仍然存在,所以你不必局限于只使用 Metal。那么,让我们开始使用 Xcode 6 进行 iOS 8 游戏开发吧。
本章将涵盖以下主题:
-
下载并安装 Xcode
-
创建 iOS 开发者账号
-
介绍 Swift
-
介绍 Playground
-
介绍 SpriteKit
-
查看默认的 SpriteKit 项目
-
SpriteKit 的新特性
-
查看默认的 SceneKit 项目
-
理解 3D 对象
-
2D/3D 坐标系
-
探索 SceneKit
-
介绍 Metal
-
图形管线
下载并安装 Xcode
如果你有一个苹果账号,你可以打开桌面版的 App Store 并搜索 Xcode。点击安装以开始下载。对于这本书,我将使用 Xcode 6.1。到这本书出版时,可能会有 Xcode 6 的新版本。如果你想下载与我使用的相同版本,你可以创建一个免费的 Apple Developer Account,进入下载部分,并下载之前的版本。

小贴士
在下载 Xcode 之前,请确保你正在使用 Mac OS X Yosemite 10.10 或 Maverick 9.4。否则,Xcode 6.1 无法安装到你的机器上。
Xcode 是苹果公司的集成开发环境(IDE)。它是开发 iOS 或 Mac OS X 上任何类型的应用程序所必需的。它不仅仅是一个 IDE;它包含了许多工具和功能,使其成为任何开发者工具箱的重要组成部分。你可以点击 Xcode 的App Store页面上的更多按钮,查看它提供的不同工具和功能。我们将在下一章中介绍 Xcode 的一些功能,当我们介绍 Xcode 界面时。
要安装 Xcode,请点击图标下的安装按钮。然后您将被要求输入您的 Apple ID 和密码。安装将在不久后开始。一旦您点击安装按钮,下载将在启动板上开始,如下面的屏幕截图所示:

下载并安装后,您可以在启动板上看到它出现。您可以点击应用程序的图标来启动应用程序。
创建 iOS 开发者账号
要创建任何应用程序并在 iOS、Mac OS X 或 Safari 的 App Store 上发布,您需要注册开发者计划。您可以创建一个免费的开发者账户以访问某些部分,如教程和下载,但您将无法访问最新的测试软件。此外,要在您的设备上运行和测试应用程序或游戏,您需要注册此开发者计划。
由于我们确实计划为 iOS 创建一个小游戏并在 App Store 上发布,因此我们需要注册 iOS 开发者计划。我将假设我们还没有开发者账户,所以让我们首先创建一个新的开发者账户,然后我们将注册 iOS 计划。
要注册为 Apple 开发者,请访问developer.apple.com/register/index.action。将显示以下页面:

如果您已经有了 Apple ID,您可以使用它来登录。否则,点击创建 Apple ID。在这里,您将被要求填写以下标题下的信息:
-
姓名:在此处输入以下详细信息:
-
名字
-
中间名
-
姓氏
-
-
Apple ID 和密码:在这里输入您首选的 Apple ID 和密码。请注意以下几点:
-
Apple ID:您选择的任何电子邮件地址。
-
密码:密码应至少包含八个字符;它应至少包含一个小写字母、一个大写字母和一个数字;并且不应包含连续相同的字符。此外,它不应与账户名相同。
-
确认密码:在此处输入与密码字段中输入的相同密码。
-
-
安全问题:如果您忘记密码,您将需要回答一些问题以获取对账户的访问权限。您可以选择它们。因此,在选择这些问题时要小心,并记下问题和答案以供将来参考。有三个问题,每个问题都有多个选项。因此,选择一个与您最相关的问题,并选择您容易记住的答案。
到目前为止提到的所有标题都可以在这个屏幕截图中看到:
![创建 iOS 开发者账号]()
-
出生日期:请输入您的出生日期。
-
备用电子邮件地址:这是您可以通过它进行沟通的备用电子邮件地址。
前两个标题可以在以下屏幕截图中看到:
![创建 iOS 开发者账号]()
-
邮寄地址:输入您的邮寄地址。
-
首选语言:选择您最舒适的语言。如果您向苹果支持团队提出任何问题,他们将以这种语言回答您。
-
电子邮件偏好:如果您想了解最新的苹果新闻、软件更新以及有关产品和服务的最新信息,您可以在本标题下方检查两个复选框。
-
验证码:在此截图所示的框中输入验证码图像:
![创建 iOS 开发者账号]()
现在点击创建 Apple ID以生成 Apple ID。如果您一切操作都成功,恭喜!您现在是一名苹果开发者。
一旦您进入,您将看到以下屏幕:

您将在技术资源和工具部分花费大部分时间,尽管您有一个强大的开发者社区和开发者支持,您可以在社区和支持部分随时访问。
在技术资源和工具部分下,您有两个子部分:开发中心和证书、标识符和配置文件。在开发中心子部分中,您将找到适用于适当开发平台的所有技术资源。通过证书、标识符和配置文件链接,您可以生成和管理您的证书,创建配置文件,并管理 App ID 和开发设备。当我们在创建应用程序并希望在我们的设备上运行它时,我们将介绍这个部分。
让我们看看开发中心部分。如果您点击Mac,您将看到开发适用于 Yosemite 的应用程序的资源链接,例如指向最新 OS X 版本的链接。您还可以获取文章、示例代码、指南等,您可以使用这些资源开发您一直想为 Yosemite 制作的程序。您还可以获得在 WWDC 上展示的开发视频链接。
如果您点击Safari,您将看到与您之前看到的类似的布局,包括示例代码和您可以使用来开发 Safari 应用的开发者库链接。
现在点击开发中心中的iOS。您应该会看到以下屏幕:

在这里,您可以找到开发各种 iOS 应用所需的所有资源。您可以下载最新版本的 iOS SDK,或者如果您想下载较旧版本的 Xcode,您可以从下载链接中进行。
与 Mac 开发者库类似,您有一个 iOS 开发者库,其中包含如何入门、指南、示例代码、参考和发布说明的链接。在开发视频部分下,还有关于 iOS 开发的视频链接。
你可以下载 Xcode,将其安装到你的机器上,并测试你的编码技能,但如果你想在一个设备上测试你的出色应用,并将其发布到 App Store,你需要注册 iOS 开发者计划。
如我之前所说,你必须成为该计划的一部分,才能在设备上测试应用或将其发布到 App Store。在此期间,你可以在模拟器上运行你制作的 SpriteKit 和 SceneKit 应用。所以,如果你愿意,当你对模拟器上的游戏外观满意后,你可以注册。
话虽如此,模拟器只是模拟器而已。游戏在模拟器上的运行方式不应被视为游戏在实际设备上运行的实际情况。游戏在模拟器上会运行得更慢,因为 Mac 处理器执行两个任务:运行你的操作系统和运行模拟器。
因此,最好在设备上测试应用,以更好地了解它最终将在设备上如何运行,并且你测试的设备越多,效果越好。
注意
此外,需要注意的是,在撰写这本书的时候,使用 Metal 开发的应用程序/游戏无法在模拟器上运行,它们需要在配备 A7 或 A8 芯片的设备上运行。如果你尝试在模拟器上运行它们,将会出现错误。
因此,如果你已经准备好注册 iOS 开发者计划,请点击你当前所在页面右侧“加入 iOS 开发者计划”标题下的了解更多链接。
让我们注册 iOS 开发者计划。准备好你的信用卡和律师,然后点击顶部的立即注册按钮开始流程。接下来,点击继续按钮进入下一屏幕。再次点击继续,因为你已经创建了一个 Apple ID。如果你还没有创建 Apple ID,请点击创建 Apple ID并按照说明操作,然后回到这个页面,并使用你的新 Apple ID 继续。一旦点击继续,你将被重定向到一个页面,你将需要选择你是想以个人身份还是公司身份注册。
如果你以个人身份注册,你的名字将作为卖家显示在 App Store 上,并且注册时不需要提供任何文件。如果你以公司、非营利组织、合资企业、合伙企业或政府机构身份注册,你应该选择注册为组织的选项。要注册为组织,你需要额外的文件,例如税务识别号、D-U-N-S 号码(免费提供)等等。
在组织的情况下,组织的名称将显示为 App Store 上的卖家。你还可以将其他开发者作为团队的一部分添加,与个人注册不同,在个人注册中,每个账户只能添加一个个人,即注册的人。以下截图显示了你可以选择是否作为个人或公司/组织注册的屏幕:

对于这本书,我们将以个人身份注册,所以点击屏幕左下角的 个人 以进入下一页。
在下一页,你必须选择你想要注册的项目。由于我们将注册 iOS 开发者项目,我们勾选 iOS 开发者项目 左侧的复选框,然后点击 继续,如以下截图所示:

在下一页,你必须同意程序许可协议。请让你的律师过来看看。在他们确认后,点击复选框以同意已阅读协议,并且年龄已满法定年龄。点击页面底部的 我同意 以同意并进入下一页。
在这里,你必须输入你的支付信息。提供你的信用卡和账单信息,然后点击 继续。现在再次验证信息,然后点击 下单。
完成这些后,你就是一个注册的 iOS 开发者。你将得到一个 感谢 屏幕和一封电子邮件确认。订单处理可能需要最多 2 个工作日,所以在这期间,我们将提前了解一下 Swift、SpriteKit、SceneKit 和 Metal。
Swift 简介
在 Xcode 6 中,苹果引入了一种名为 Swift 的新脚本语言。如果你来自 JavaScript 或 ActionScript 背景,你会觉得自己非常熟悉。即使你已经长时间使用 Objective-C,也不要担心。你仍然可以使用 Objective-C 创建游戏。在创建任何项目之前,你将被要求选择你想要创建应用程序/游戏的编程语言。只需选择 Swift,然后你就可以开始了。
在 第二章,Swift 基础 中,你将学习如何在 Swift 中编码,我们将看到它与 Objective-C 的不同之处。在 Swift 中编码时,我们将从绝对开始,包括变量、控制语句、循环和类等高级主题。
Playground 简介
Playground 是一个你可以用来测试 Swift 代码并立即看到代码结果的文件。让我们实际创建一个新文件并看看它。
如果你从开发者门户而不是 Mac Store 下载了 Xcode,请双击 DMG 文件以打开它。一旦打开,你可以将 Xcode 应用程序拖到你的 Applications 文件夹中以安装它。
一旦安装完成,将应用程序拖入 dock,因为我们将会经常使用它。一旦您将其拖入 dock,点击它以打开。打开后,您需要同意条款和条件,因此请同意它们以继续。
您应该在 Xcode 上看到欢迎界面。在这里,您可以点击以下任何一个选项:使用游乐场开始、创建一个新的 Xcode 项目或查看现有项目。这三个选项的详细信息如下:
-
使用游乐场开始:使用游乐场,您可以测试您的 Swift 编码技能,并在使用游乐场开发应用程序或游戏之前磨练它们。
-
创建一个新的 Xcode 项目:每次您想要创建一个新的 Xcode 项目时,您都必须点击此按钮,然后您还可以选择您想要创建的项目类型。
-
查看现有项目:如果您正在使用源代码管理器(SCM)来管理代码,例如 GitHub、SVN 或 BitBucker,您可以使用此方法查看您那里的项目。
这些选项可以在以下屏幕截图中看到:

现在,点击使用游乐场开始按钮。输入文件名(我将其命名为playgroundTest),选择iOS平台,然后点击下一步,如图所示:

在下一屏,您将被询问希望在哪里创建项目文件夹。为了保持整洁,我将所有项目都保存在“文档”文件夹下的_Projects文件夹中,该文件夹以下划线(_)开头。因此,在这种情况下,我在“文档”文件夹中创建了一个新的_Projects文件夹。然后我创建了一个名为_Playground的新文件夹,选中它,并点击创建。
您将看到在“文档”文件夹的_Projects/_Playground文件夹中创建了一个名为playgroundTest的文件。一旦文件创建完成,您将看到以下窗口:

您将在左侧面板中编码,您将能够在右侧面板中看到即时结果。您可以看到左侧已经编写了一些代码,而结果在右侧显示。在这里,Hello, playground字符串被分配给一个新的变量str,而str变量的值立即在右侧屏幕上记录。
当您在第三章学习 Swift 时,我们将详细介绍有关游乐场的信息,因为我们将要进行的所有编码实践都需要游乐场文件。
探索 SpriteKit
SpriteKit 是一个首次在 iOS 7 和 Xcode 5 中引入的 2D 游戏开发框架。它主要用于创建 2D 游戏,因此对象只能在 x 和 y 坐标上放置或移动。使用 SpriteKit,您可以创建适用于 iOS 和 OS X 的 2D 游戏。
如果你使用过 Cocos2d,你将非常熟悉 SpriteKit 的架构和语法。由于游戏将主要由精灵组成,因此它被称为 SpriteKit。
由于 SpriteKit 是一个 2D 游戏开发框架,它提供了从开始到结束创建完整 2D 游戏所需的所有工具。你可以创建主菜单屏幕、游戏玩法屏幕和选项屏幕。你还可以在每个屏幕上创建按钮。当按钮被按下时,你可以在屏幕之间导航。在游戏玩法屏幕中,你可以添加玩家、敌人、用于显示分数的文本,以及使用粒子编辑器创建的烟雾和爆炸等粒子。
SpriteKit 还包括一个执行所有物理相关计算的物理引擎。你只需要将其包含在场景中,你将看到场景中的对象根据物理模拟自动相互交互。此外,SpriteKit 还包括一个自动纹理图集生成器,以更好地优化你的游戏资源。
SpriteKit 中有些类是创建任何游戏的基本构建块。
SpriteKit 的新特性
在 Xcode 6 中,SpriteKit 添加了许多新酷炫的特性:
-
图形技术:
-
着色器
-
灯光和阴影
-
-
物理模拟技术:
-
每像素物理
-
物理场
-
逆运动学
-
约束
-
-
工具和改进:
-
SpriteKit 编辑器
-
与 SceneKit 的集成
-
SpriteKit 现在包括你可以用来在游戏中创建新的有趣效果的着色器。你还可以创建光源来投射实时 2D 阴影。
SpriteKit 已经强大的物理引擎通过包含每像素物理以实现像素级碰撞检测而变得更加强大。通过添加物理场,我们可以迅速创建一个《愤怒的小鸟太空版》的克隆,并且通过逆运动学,使你的 2D 角色的关节运动看起来更加逼真。除此之外,你还可以使用约束来控制场景中任何物理对象沿任何方向或角度的运动或旋转。
SpriteKit 还包括一个新编辑器,可以用来创建一个无需编写任何代码的简单游戏。它也可以用作调试工具来检查错误。
SpriteKit 也可以与 SceneKit 一起使用。SceneKit 是苹果公司新开发的 3D 游戏开发框架。如果你想在 3D 游戏中创建 GUI 元素,例如 2D 按钮和雷达,使用 SpriteKit 可以非常容易地实现这一点。
查看默认的 SpriteKit 项目
让我们看看当我们创建一个新项目时默认创建的 SpriteKit 项目,这样你就可以理解你在创建游戏时将使用的一些术语。
现在,我们将创建一个新项目。因此,点击中间的创建新 Xcode 项目按钮。一旦点击,你将看到以下窗口:

在左侧面板中,你需要选择你想要为哪个平台创建游戏,iOS 或 OS X。由于我们将在本书中创建 iOS 游戏,我们将从 iOS 部分选择。
在应用类型中,你可以选择你想要创建的应用类型:主从应用、页面应用、单视图应用、标签应用或游戏。由于 SpriteKit、SceneKit、Metal 和 OpenGL ES 都是游戏应用的一部分,我们将选择游戏。然后点击下一步。
在选择新项目选项窗口中,你必须填写产品名称、组织名称、组织标识符、语言、游戏技术和设备字段:

上一张截图所示字段的详细信息如下:
-
产品名称:当你创建实际项目时,这个字段的输入是你正在创建的游戏名称,例如
AngryBirds、CutTheRope等等。 -
组织名称:在这里,你可以输入你为该组织开发游戏的组织名称。为了本书的目的,你可以输入任何你想要的名称。
-
组织标识符:这一点非常重要,我无法强调它的重要性。在 App Store 上,苹果将只通过这个标识符来识别你的应用。这里输入的任何内容都将转换为包标识符的值。这必须是唯一的,App Store 上的其他应用不能有相同的包名。虽然你可以随意命名,但常用的格式是公司网站反向格式加上产品名称,例如:
com.<公司名称>.<产品名称>。如果你没有公司或网站,不要担心。你只需确保你的包名是唯一的。如果不是,苹果将不会接受这个应用。在这种情况下,你可以尝试不同的包名,稍后你可以在 Xcode 中将包名更改为苹果批准的包名。
-
语言:从这个下拉列表中,你可以选择你想要用哪种语言开发游戏。你可以选择Objective-C和Swift。由于我们将在本书中使用 Swift 开发游戏,这里我们将使用Swift选项。
-
游戏技术:从这个下拉列表中,你可以选择你想要用来开发游戏的科技。你可以选择 SceneKit、SpriteKit、Metal 和 OpenGL ES。由于我们将使用 SpriteKit,请从下拉列表中选择它。
-
设备:从这个下拉列表中,你可以选择你想要为哪个设备开发游戏。如果你想为 iPhone 或 iPad 开发,你可以选择iPhone或iPad,或者如果你想游戏能在两个平台上运行,你可以选择通用。现在你可以选择iPad。
点击下一步继续。在接下来的屏幕中,你将被询问在哪里创建项目文件夹。为此,在_Projects文件夹中创建一个名为_SpriteKit的新文件夹。然后选择此文件夹,并点击创建。
你会发现spriteKitTest项目文件夹在Documents文件夹中的_Projects/_SpriteKit文件夹内被创建,如下面的截图所示:

现在双击spriteKitTest.xcodeproj文件以在 Xcode 中打开你的项目。你的项目应该会打开,如下面的截图所示:

Xcode 内置了一个模拟器,可以展示游戏或应用在指定设备上的外观。你可以通过点击窗口顶部停止按钮右侧的项目名称来选择设备,如下面的截图所示:

点击左上角的播放按钮来构建当前项目。游戏将开始编译所有代码,并自动启动模拟器。一旦模拟器启动,给它一些时间来显示以下屏幕:

嗯,它看起来不会完全像前面的截图。首先,你不会看到在Hello, World上面的飞机图像。其次,它将是纵向模式,而不是前面截图中的横向模式。
要将视图更改为横向模式,点击左侧面板中的项目名称。中间面板将改变并显示横向、纵向等选项。取消选中纵向选项,并停止并重新构建游戏。现在,每次构建游戏时,视图都会从纵向变为横向。
接下来,你如何将飞机放到屏幕上?这非常简单;要让它出现,你只需在屏幕上的任何地方点击。我在屏幕中心点击,在顶部和显示Hello, World的地方之间,这就是为什么飞机出现在那里。要创建更多飞机,你只需继续在你想出现新飞机的地方点击。你会看到,当你添加更多飞机时,屏幕右下角的数字也会改变,飞机开始旋转得越来越慢。为什么会这样?这些数字是什么?在那之前,让我们首先了解一些术语、类和函数,这些是我们需要熟悉的:
-
SKScene:这是一个用于创建场景的类,例如,主菜单屏幕、选项屏幕和游戏玩法屏幕。每个场景将包含玩家和按钮等精灵,这些精灵将填充屏幕并帮助你导航到其他屏幕。在左侧面板的项目中,你会找到一个名为GameScene.swift的文件。这是游戏构建时立即加载的场景。 -
SKSpriteNode:正如我们之前看到的,每个场景都会加载精灵或图像。要将精灵加载到屏幕上,您必须使用SKSpriteNode。在这个场景中,每次您触摸屏幕时,您都会在特定位置创建一个精灵。它获取Spaceship的图像名称。这可以在同一类的touchesBegan函数中看到。 -
SKLabelNode:为了在屏幕上显示任何类型的文本,您需要使用SKLabelNode来决定应该使用哪种字体来创建文本。您还需要提供需要在屏幕上显示的文本,以及要显示的文本的大小和位置。在这里,我们看到SKLabelNode被用来在屏幕上显示Hello, World。在myLabeltext中,您可以在引号内写下您想要显示的内容,然后重新构建以查看您输入的内容是否在屏幕上显示。 -
SKAction:这些用于在一段时间内修改节点的参数。例如,您可以在 1 秒内将一个对象放大到其大小的两倍,然后将其恢复到正常大小。或者,您可以将对象的位置改变以使其在一段时间内移动或旋转。您可以使用 SKAction 一起执行这些操作或一个接一个地执行,这里,一旦宇宙飞船被创建,就会对其运行一个动作,告诉它每秒旋转 180 度。![查看默认的 SpriteKit 项目]()
-
touchesBegan:SpriteKit 内置了可重写的函数,可以用来在屏幕上注册触摸。有四个函数称为touchesBegan、touchesMoved、touchesEnded和touchesCancelled。您可以使用这些函数组合在一起来检测手指触摸并创建自己的控制方案,如点击、滑动和双击。 -
update:update函数是 SpriteKit 提供的另一个可重写的函数,它会在整个游戏中根据您设置的调用频率反复调用。通常,update函数每秒调用 60 次。我们可以使用这个函数来更新位置、检查碰撞或更新游戏的分数。一旦场景初始化,update函数就会自动开始调用。
因此,现在,知道了所有这些,让我们回答之前提出的问题;屏幕右下角的那两个值是什么?
节点代表您添加到场景中的节点数量。这些节点可以是精灵、标签等。这就是为什么每次您在屏幕上添加新的宇宙飞船时,您都会看到节点计数增加。屏幕上添加的对象越多,所需的处理能力就越大。
节点计数旁边的数字是 FPS 或每秒帧数的计算。在 update 函数中,我们看到该函数每秒被调用 60 次。所以,每秒 60 次,屏幕被擦除并重新绘制以创建一个新的帧,每个对象的位子和旋转也将被更新。因此,如果你在屏幕上添加更多对象,处理器就必须做更多的工作来绘制屏幕上的所有图像。这就是为什么你会看到 FPS 的下降;也就是说,当你向场景中添加更多飞船时,飞船的旋转会越来越慢。此外,在这种情况下,还需要考虑新飞船也需要旋转,因此处理器必须额外计算飞船每秒需要旋转多少。
对于 SpriteKit 和 GameScene.swift 的介绍就到这里。面板上还有其他文件,我们将在下一章中深入探讨 Xcode 时进行讲解。接下来,让我们看看 SceneKit。
探索 SceneKit
SceneKit 是一个 3D 游戏开发框架。因此,它可以用来创建 iOS 和 OS X 的 3D 游戏或应用程序。它最初在 OS X 10.8 中发布,现在在 iOS 8 中可用。它是一个基于 OpenGL 和 OpenGL ES 的高级 API,可以与 SpriteKit 集成,就像我们之前看到的。
在 SceneKit 中,就像在任何 3D 游戏开发框架中一样,我们需要为场景提供相机、灯光和对象,以便场景可以从相机的视角进行渲染,并在设备的视口中进行处理和显示。

所有对象都需要通过为放置的每个对象创建一个节点来添加到节点中,无论是相机、灯光还是对象。
你可以添加预定义的对象,例如盒子、球体、环面和平面,并为其添加纹理;或者导入在 3D 程序中创建的 COLLADA 文件或 Alembic 文件,将其导出为 .dae 或 .abc 格式,并将其导入到 SceneKit 中。这些可以在 预览 中打开,这样你可以在将其导入 SceneKit 项目之前查看和检查文件。除了几何形状之外,你添加到导入场景中的动画、纹理、灯光/相机等也会被导入。你还可以使用 SceneKit 添加 3D 文本和形状到你的游戏中。
如果你将纹理分配给 COLLADA 文件中的对象,则需要将纹理映射与文件一起导入。你可以将纹理添加到原始形状、3D 文本和复杂形状中。你还可以通过在代码中将 3D 文本和形状拉伸以赋予它们深度,或者通过倒角角落来修改它们。
通过提供用于创建形状的每个顶点的位置、纹理、坐标和颜色,也可以创建自定义对象和形状。你对多边形数量有完全的自由;如果你想得到更平滑的模型,你可以将多边形分割以获得更平滑的结果——这一切都是通过代码实现的。
SceneKit,连同摄像机一起,还提供了不同的光照类型,如 环境光、泛光灯、方向光 和 聚光灯。这些可以附加到节点上,使它们易于放置和移动。
除此之外,还有一个编辑器可以用来查看你的场景以及场景中添加的所有对象。在将对象导入场景后,你还可以查看单个对象的属性,并根据需要对其进行修改。
游戏资源将由资产目录管理,这在构建时将优化资源,类似于 SpriteKit 中的纹理图集创建器。
与 SpriteKit 类似,SceneKit 有可以对对象执行的操作来动画化它们。它还有一个用于物理模拟和碰撞检测的物理引擎。与 SpriteKit 中的物理引擎一样,你可以向你的对象和场景添加关节、约束和逆运动学。
查看默认 SceneKit 项目
与创建 SpriteKit 项目的方式类似,创建一个 SceneKit 项目。创建项目时唯一的区别是,在选择下拉列表中的 游戏技术 时,选择 SceneKit 而不是 SpriteKit。将项目命名为 SceneKitTest。
一旦创建项目,双击 SceneKitTest.xcodeproj 文件。一旦打开,你应该会看到一个类似于 SpriteKit 中的项目结构。
如我们之前所做的那样,你可以点击窗口顶部的播放按钮并选择你选择的模拟器。一旦模拟器加载,你应该会看到一个类似于以下截图的窗口。我再次将视图更改为横幅,以便于操作。

在底部,你将获得比 SpriteKit 更多的调试信息。在左下角,你有一个帧率计数器,与 SpriteKit 中的相同。正如预期的那样,帧率将低于 SpriteKit 项目。这是因为我们现在正在查看 3D 模型,这包括处理器需要计算的大量顶点和多边形,导致帧率下降。这只在模拟器上;在实际设备上,帧率将达到 60。
在屏幕右下角,菱形显示绘制调用的数量,这与 SpriteKit 中的节点类似。由于屏幕上只有一个对象——战斗机,因此只有一个绘制调用。添加的对象越多,绘制调用就越高。
三角形表示构成对象的三角形数量。星号或星号表示对象中存在的顶点数量。你可以点击左侧的 + 符号来获取额外的信息,例如 动画、物理、约束、粒子、代理、渲染、GL Flush 和 2D。
3D 物体由顶点组成,这些顶点通过线条连接形成三角形或矩形形状,称为多边形。这些多边形进而形成网格,给物体赋予形状,在这个例子中是战斗机。然后,在形状上绘制 2D 图像,使其看起来像战斗机。在左侧面板中,您可以打开art.acnassets。这是游戏所有艺术资源将被存储的地方。选择它来查看网格和图像,或者放置在网格上的纹理文件。
理解 3D 物体
我已经将ship.dae COLLADA 文件导入到 3D 程序中,以展示喷气物体的网格。COLLADA 文件可以导入到任何流行的 3D 软件包中,例如 3DSMax、Maya 或 Blender。
在 SceneKit 项目中,我们看到了物体在所有纹理的荣耀中。场景中有纹理化的物体,以及相机和投射阴影的光源。在下面的图中,您可以看到物体的实际网格的线框视图。这已被提供,以便您了解 3D 多边形物体是什么,以及顶点如何组成多边形以创建 3D 物体的网格。

绿色线条是连接点到形成物体多边形表面的线条。您可以看到这些顶点是如何用来形成三角形或多边形以创建所需形状的,例如飞机的机翼、驾驶舱和机身。物体是完全空心的。然后在其上粘贴纹理,使其看起来像刚刚涂上了一层新漆。您还可以指定一个可以反射光的材料,使其看起来闪亮和反光。
在 SceneKit 中,您可以导入在 3D 软件包中创建的 3D 场景,包括纹理、光照、动画和相机,这些都将保持完整。您可以在您喜欢的 3D 软件包中创建它,并以.dae格式导出。您将能够将其导入到 SceneKit 中,就像喷气物体一样。
如果您的游戏非常简单,不需要这样复杂的形状,那么您可以通过代码在 SceneKit 中本身创建一个盒子、球体和其他原始对象和形状,并给它们任何您想要的颜色。然而,您仍然需要创建一个相机和光源,以便物体在场景中可见。
返回到模拟器,您可以通过点击并拖动视口来旋转物体,以便更好地观察物体。您也可以在屏幕上双击以将视图重置到初始状态。除了创建 3D 场景外,SceneKit 还允许触摸界面,以便玩家可以与物体交互。我们将在本书稍后深入探讨 SceneKit 时详细讨论这一点。
2D 和 3D 坐标系
在 2D 游戏开发的情况下,我们只需要关注两个坐标系。第一个是屏幕坐标系,另一个是对象坐标系。
在 2D 中,无论何时我们在屏幕上放置一个对象,我们总是想知道对象距离屏幕左下角有多远。这是因为屏幕的左下角,而不是屏幕中心,是原点。因此,如果你不改变其位置放置一个精灵,它将在屏幕的左下角创建。屏幕原点或(0, 0)位置在屏幕的左下角。如果你想将精灵放置在屏幕中心,你需要将宽度和高度的一半加到位置属性上,因为所有一切都是相对于屏幕的左下角。这被称为屏幕坐标系。
对象坐标系指的是精灵本身。精灵的中心位于对象的中心,与屏幕不同,屏幕的原点在左下角。精灵的中心被称为锚点。当你旋转一个精灵时,它将围绕其中心旋转,因为其原点在其中心。你可以通过访问其锚点属性来改变精灵的原点。

在 3D 游戏开发中,存在几个坐标系统。因此,有世界、对象、视图和屏幕坐标系统。除了被称为坐标系统外,它们也被称作空间。
在 2D 的情况下,世界坐标系与屏幕坐标系相同,但在 3D 中并非如此。在下面的图中,想象你正在你的设备上看到喷气式飞机:

世界空间的起点是红色、绿色和黄色箭头的起源处。红色箭头代表正x轴,绿色是正y轴,黄色是正z轴。世界空间的起点是0, 0, 0。
喷气式飞机放置在世界空间内。与精灵一样,对象坐标系位于对象的中心。
红色方框代表一个朝向喷气式飞机的相机。相机所在的位置被称为视图、眼睛或相机坐标系统。视图视锥体代表相机的视图限制。任何放置在此区域之外的对象都不会被相机渲染。在图形编程中,这被称为裁剪。
最后,相机所看到的一切都必须投影到设备的屏幕上。这被称为屏幕坐标系。
SceneKit 的基础
让我们简要地看看在这个项目中场景是如何创建的。打开GameViewController.swift文件。在 SceneKit 中创建场景,你有一个独立的场景创建器,这里称为SCNScene,用于创建 3D 场景。
GameViewController.swift文件负责视图。任何创建的应用都必须包含至少一个视图,以便可以在屏幕上显示。每次打开新的应用程序时,都会创建一个视图。一旦视图创建,第一个被调用的函数就是viewDidLoad函数,它开始执行其中编写的任何代码:

我们创建了一个新的SCNScene类型的场景,并加载了ship.dae对象。不需要单独加载飞机的材料;它将自动分配给飞机。由于每个场景首先需要一个相机,我们创建了一个新的相机节点并将其添加到场景中。然后,相机被定位在 3D 空间中。所以与 SpriteKit 不同,在 SpriteKit 中位置是通过x和y坐标指定的,SceneKit 需要其坐标以x、y和z来定义。
原点位于场景中心。在之前的某个截图(显示世界空间的截图)中,红色箭头表示正x轴,绿色箭头表示正y轴,黄色箭头表示正z轴。因此,相机(用红色立方体表示)放置在对象正z方向上 15 像素处。喷气式飞机对象放置在原点。
接下来,我们创建两个光源。首先创建一个泛光灯,然后创建一个环境光来照亮场景。如果您不添加任何光源,场景中将看不到任何东西。您可以尝试注释掉这些行,但由于这是添加光源到场景的地方;您将看到一个黑色屏幕。喷气式飞机仍然在那里,但由于缺少光源,您无法看到它。
然后,我们从场景中获取飞船对象并对其应用旋转动作,类似于在 SpriteKit 中执行的方式。只是现在,它被称为SCNAction而不是SKAction。此外,你在所有三个轴上提供了角度,x和z轴的值为零,并在每秒内绕y轴旋转对象。
然后,创建了一个新的sceneView变量,将其分配给当前视图,然后将当前场景分配给sceneView的场景。然后允许sceneView控制相机并显示调试信息,例如 FPS。您还应该将背景颜色设置为black。
创建了一个新函数,称为handleTap,其中处理了双击重置视图。
GameViewControllerclass类有其他函数,如shouldAutoRotate、prefersStatusBarHidden、supportInterfaceOrientation和didReceieveMemoryWarning。让我们详细看看每个函数:
-
shouldAutoRotate:此函数设置为true或false,取决于您是否希望在设备翻转时旋转视图。如果是true,视图将被旋转。 -
prefersStatusBarHidden: 如果您想隐藏状态栏,请启用此功能。如果出于某种原因需要显示状态栏,则应禁用此选项。 -
supportInterfaceOrientation: 设备将自动检查允许哪些类型的方向。在这种情况下,如果是 iPhone,则接受除了倒置方向之外的所有方向。 -
didReceiveMemoryWarning: 如果有您尚未释放的图像和其他数据,并且如果它们正在对内存造成压力,此函数将自动发出警告,表示内存不足,并将关闭应用程序。
所有这些都会相当复杂,但请放心。当我们在本书中稍后介绍 SceneKit 时,我们将将其分解为基本原理。
介绍 Metal
Metal 是苹果公司的新低级图形 API。通过它,您可以直接与 GPU 通信并执行图形以及其他计算操作。使用 Metal,您可以从头开发自己的自定义框架,创建 2D 和 3D 应用或游戏,而无需依赖现有的框架,如 SpriteKit 和 SceneKit。
与任何其他图形 API 一样,Metal 有一个具有可编程着色器的图形管道。您还可以分配内存,包括缓冲区和纹理对象。Metal 还有自己的着色器语言,用于编译和应用着色器。
但为什么我们需要 Metal,当 SceneKit 和 SpriteKit 等其他游戏开发框架已经存在时?在 SceneKit、SpriteKit 或任何其他游戏开发框架中,Metal 首先与图形库(如 OpenGL ES)交互,然后该库将信息传递到 GPU。哇!使用 Metal,您拥有直接与 GPU 通信并创建自己框架的绝对权力,这样您就可以创建一个更优化的游戏。
框架始终位于 OpenGL 或 Metal 之上,然后与图形处理器通信,如下面的图所示。使用 Metal,您可以消除一层,直接与 GPU 通信:

您也可以根据自己的需求从头开始创建自己的工具。此外,您还可以访问内存以获取处理器最大的性能。
然而,您可能会说:“哦,OpenGL ES 也提供了所有这些功能。”这是真的,但 OpenGL ES 是跨平台的,将在运行许多其他操作系统的其他设备上工作。因此,您可以想象 OpenGL ES 中有多少额外的材料是为了支持所有设备和操作系统。这就是 Metal 和 OpenGL ES 之间的区别所在。
Metal 非常具体地针对苹果设备系列和操作系统编写。实际上,在撰写本书时,它高度依赖于苹果设备和操作系统。您需要一个配备 A7 或 A8 芯片并运行 iOS 8 的苹果设备来运行用 Metal 制作的游戏。此外,如果您想在开发过程中测试游戏,您不能在模拟器上运行它,因为它不受支持。您需要一个实际设备来运行它,因此您至少需要一个 iPad Mini 2、iPad Air 或 iPhone 5S 来测试您的游戏。
使用 Metal,开发者们发现与在当前苹果设备上运行的相同 OpenGL ES 游戏相比,性能至少提高了 30%。因此,如果您计划只为最新的苹果设备创建 3D 游戏,强烈建议您开始使用 Metal 创建您的游戏。
您可以在设备上创建一个 Metal 项目并构建它来查看输出。由于需要涵盖许多概念才能开始解释屏幕上所有的代码——并且我们将在本书的后面详细讲解这些内容——因此我们最好在理解了诸如顶点缓冲区、帧缓冲区和索引缓冲区等术语的含义以及它们之间的区别之后,再回头查看代码。
如果您的设备已连接并且已从苹果开发者门户下载了开发者许可证,您可以点击播放按钮来构建应用程序。一旦项目构建完成,您将看到以下屏幕:

为了更好地理解 Metal 或 OpenGL,并欣赏渲染屏幕上甚至一个三角形所付出的努力,您需要了解所谓的图形管线。
图形管线
如果我们查看任何游戏开发框架,我们会意识到几乎每个类中至少都有两个函数。第一个函数是init或start函数,第二个函数是update函数。
init函数或启动函数在开始时只调用一次,用于初始化类的所有变量。另一方面,update函数通常每秒调用 60 次,用于更新对象的位置或检测对象之间的碰撞。
除了初始化和更新位置之外,还需要与update函数一起调用的另一个函数。就像您挡风玻璃上的雨刷一样,它需要擦除并重新绘制屏幕上的内容。这个函数被称为draw函数。在一些框架中,它是第三个函数,称为draw()。
每次调用此函数时,都称为一次绘制调用。在 SceneKit 中,我们看到了这个单词。但在 SceneKit 或 SpriteKit 中这个函数在哪里?在这两个框架中,类似于 Cocos2d 和 ActionScript,有一个称为显示列表的东西。每次使用addChild添加对象时,该对象都会被添加到显示列表中。
显示列表就像任何数组一样,你可以添加或删除对象。显示列表遍历添加到其中的所有对象,然后在屏幕上绘制所有对象。
因此,你不必担心调用 draw 函数;你只需通过调用 addChild 添加要绘制的对象。否则——也就是说,如果你不想渲染对象——你可以调用 removeChild 函数从显示列表中移除该对象,这样它就不会被绘制到屏幕上。
在屏幕上显示事物的过程被称为图形管线。这是一个逐步的过程,将提供给 GPU 的顶点数据和纹理数据转换,以便在屏幕上显示和操作对象。让我们详细看看这个过程。
这里展示了渲染管线中的阶段:

让我们详细看看每个阶段:
-
顶点:你可以提供具有每个点坐标的单独顶点,或者整个网格。在喷气式飞机的情况下,我们提供了
.dae文件。提供的顶点信息将包含 3D 空间中的 x、y 和 z 位置。除了位置信息之外,其中一些还可能包含颜色、表面法线和纹理坐标信息。所有这些信息都存储在称为缓冲区的内存空间序列中,类似于数组。顶点信息片段,如位置和颜色,作为属性被馈送到 GPU。属性可以想象为特定顶点的一组属性。因此,有位置属性、颜色属性等等。所有这些属性都提供给 GPU 以开始生成对象。
-
生成原语:位置缓冲区中提供的顶点位置现在被连接起来形成小三角形,这些三角形进而形成物体的网格。这些三角形是基于作为属性提供的顺序顶点信息形成的。
-
顶点/几何着色器:然后信息被传递到顶点着色器。顶点着色器可以通过着色器语言进行编程。这种语言类似于 C 语言。使用这种语言,我们可以在游戏循环中改变位置,使对象移动、缩放和旋转,就像我们的
update函数一样。 -
光栅化:在知道哪些顶点位于何处之后,基于由顶点创建的多边形的定位,GPU 开始绘制点之间的像素。
-
像素/片段着色器:与顶点着色器类似,在那里你可以进行顶点修改,像素着色器使你能够执行基于像素的操作。所以,既然这也是一个着色器,你知道它也是可编程的。使用像素着色器,你可以创建诸如改变纹理的颜色和透明度等效果。
-
测试和混合:这是 GPU 检查像素是否应该实际显示在屏幕上的阶段。假设当前物体前方有一个物体,当前物体应该只部分可见。GPU 只会将那些应该实际可见的像素放置在屏幕上,并忽略重叠的像素。
-
帧缓冲区:缓冲区?这又是一个内存空间吗?是的。在图像最终显示在屏幕上之前,整个图像首先存储在一个称为帧缓冲区的内存空间中。当所有计算完成,最终图像准备好在屏幕上显示时,从帧缓冲区取出的数据然后显示在屏幕上。
如果你没有完全理解这些内容,那完全没问题。当我们查看在 Metal 中创建对象并在屏幕上显示它们时,我们将实际走过这些步骤,所以不用担心。
摘要
Xcode 6 有很多基础功能和特性,我们在这章中介绍了它们。你看到了如何成为苹果开发者,以便你可以在设备上测试你将要构建的游戏。稍后,你将把游戏上传到 App Store。
我们探讨了如何为不同的游戏技术(如 SceneKit、SpriteKit 和 Metal)创建 playground 文件和项目文件夹,这些技术我们将在接下来的章节中使用。最后,我们探讨了广泛用于在任意设备上渲染屏幕上内容的渲染管线。
在下一章中,我们将探讨 Xcode 的界面以及它提供的工具和功能。
第二章。Swift 基础
好的,让我们开始。但在我们深入 Xcode 之前,你需要学习苹果的新编程语言——Swift。对于有其他语言编码经验的人来说,这看起来非常熟悉。苹果从脚本和底层语言中吸取了最好的部分和实践,并集成了它自己的几个惊人的特性,使得 Swift 对任何人来说都很容易入门,并开始编码。随着我们进一步学习,我们将看到苹果还增加了一些真正让 Swift 与其他语言相比更加出色的特性。
正如我们在第一章中看到的,我们将使用 Xcode 的 playground 来完成所有的编码。Playground 是一个非常通用的工具。每次你创建一个新文件,你都可以立即开始工作,而无需任何进一步的设置。它有一个内置的结果面板在右侧,每次你更改文件时,它都会实时编译你的代码。还有一个辅助编辑器窗口,它将给出我们在代码中做出的更改的图形表示。
在本章中,我们将从 Swift 语言的基础开始,从声明变量到条件语句、循环、数组、函数和类。那么,让我们开始吧。创建一个新的 playground 文件,你可以给它起任何你喜欢的名字,然后打开它。如果你在第一章中创建了一个文件,你将已经在_Playgrounds文件夹中有一个可以打开的文件。
本章将涵盖以下主题:
-
数据类型和运算符
-
语句
-
数组和字典
-
函数
-
类
-
可选参数
变量
如果你有一些在其他语言中编码的经验,你现在应该知道什么是变量。如果不是,那么知道变量是某种可以随时更改值的实体。这就是为什么它被称为变量。
在 Swift 中,你可以使用var关键字来定义一个变量,这与 JavaScript 类似。所以,如果我创建一个新的变量叫做age,然后我输入var age,这就足够了;变量已经定义了。
经验丰富的 C 语言程序员会注意到我在末尾遗漏了分号。嗯,Swift 中不需要分号,但如果你想使用分号,你当然可以这样做。我这样做是出于习惯,也是因为这是一个好的实践。这是因为当你用 Swift 编码了一段时间后,如果你不使用冒号,然后使用基于 C 的语言,你会在各个地方遇到错误。
但是等等!这里有一个错误。Swift 无法隐式地确定age数据类型是什么;也就是说,它是整数、浮点数、布尔值还是字符串?为了 Swift 能够隐式地分配类型,你必须初始化变量。我可以将一个人的年龄表示为一个值为10的int变量,一个值为10.0的float变量,或者一个值为Ten的string变量。根据你分配的变量类型,Swift 将知道该变量是int、float还是string类型。
我将分配一个值为 10 的 age 变量。在 结果 面板中,你会看到变量的值被打印为 10。这就是目前存储在 age 变量中的值。所以,如果你在下一行只输入 age,你将在右侧找到显示的结果。
如果你给 age 加上 1,你会看到结果为 11。这并没有改变 age 的值;只是这一行被评估并显示。如果你想将 age 的值更改为 11,你可以使用简写 age++ 命令来增加值,就像你在 Objective-C 或任何其他基于 C 的语言中做的那样。
一旦一个变量被分配了某种类型,你就不能再分配其他类型的值。所以现在,如果你尝试分配 11.0 或 Eleven,它将不会被接受,并且你会得到错误。
好的,我们看到了如何分配 int 类型的变量,但如何分配 float、string 和 bool 呢?这可以通过以下代码片段来完成:
var height = 125.3
var name = "The Dude"
var male = true
如果我不想初始化一个变量怎么办?我们可以明确地告诉 Swift 变量的类型。我们可以通过告诉 Swift 我们想要分配的变量类型来告诉 Swift 变量是某种类型。我们通过在变量名称后面添加一个冒号和类型来实现这一点,如下面的代码片段所示:
var age:Int
var height:Float
var name:String
var male:Bool
注意
除了 var 关键字之外,你可能还看到了这个名为 let 的新关键字,这在 Swift 中非常常见。如果你不希望变量的值在代码中改变,那么你应该使用 let 关键字而不是 var。这也会确保即使你意外地更改了 let 存储的值,值也会保持不变。在其他语言中,这些也被称为常量,你可以使用 const 关键字来定义它们,但在 Swift 中,你使用 let 关键字。
在前面的例子中,你知道无论发生什么,male 在他的一生中始终是男性。所以,bool male 将保持 true 并保持不变;它永远不会改变(好吧,除非这个人真的很不满意他的性别)。一旦它被设置为 true,我们就不可以后来意外地将其更改为 false。所以,语法将变为以下形式:
let age = 10
let height = 125.3
let name = "The Dude"
let male = true
再次强调,即使你没有明确给出数据类型,你仍然需要初始化变量。所以,你可能认为你没有做任何改变。结果面板仍然显示与变量为 var 时相同的值。但现在尝试改变值。例如,假设你增加了 age 变量;现在尝试这样做,你一定会得到一个错误。
所以,如果你知道一个变量的值肯定会改变,使用 var 会更好。否则,使用 let 会更安全,这样可以避免因这种原因在代码中产生大量的错误。
现在你已经了解了如何声明和初始化变量,让我们使用这些变量通过运算符来进行一些操作。
运算符
运算符用于计算机语言中执行对变量的各种操作。这可能是一个算术操作(如加法),一个比较操作来检查一个数字是否大于或小于另一个数字,一个逻辑操作来检查条件是否为真或假,或者一个算术赋值,如增加或减少一个值。现在让我们详细看看每种类型。
算术运算符
电脑最初是为了执行数值运算,如加法、减法、乘法和除法而制造的。在 Swift 中,我们也有 +,-,*,/ 和 % 等运算符。
让我们声明两个变量,a 和 b,并将它们初始化为 36 和 10。如果你想在 Swift 中进行单行初始化,你可以像以下代码片段所示那样做:
var a = 36, b = 10
你也可以使用分号来分隔它们,如下所示:
var c = 36; var d = 10
如果你想要初始化不同类型的变量,这很有用。现在让我们在变量之间使用运算符。结果应该是 46,26,360,3 和 6。注意,将一个 int 变量除以另一个 int 变量的结果是一个 int 变量,而不是像 3.6 这样的 float 变量:
a+b // 46
a-b // 26
a*b // 360
a/b // 3
a%b // 6
要得到确切的结果,我们需要对变量进行类型转换。类型转换与其他语言中的做法非常相似;你想要转换的变量类型被添加到变量前面的方括号中,如下所示:
Float(a)/Float(b) //3.5999999
比较运算符
逻辑运算符用于检查一个变量是否等于、小于、大于、小于等于或大于等于另一个变量:
a == b //false
a<b //false
a>b //true
a<=b //false
a>=b //true
逻辑运算符
与其他语言类似,你可以使用 && 符号来检查两个操作之间的逻辑 AND 条件,使用 || 来检查 OR 条件。这两个符号用于检查两个表达式是否都满足其条件,或者只有其中一个条件为真:
a==b && a > b // false
a==b || a > b // true
在前面的例子中,我们知道 a 不等于 b,但 a 大于 b。因此,前面例子中的第一个语句是 false(因为它的两部分都不是 true),但第二个语句是 true,因为至少有一个条件是 true。
算术增量/减量
正如我们在第一个例子中通过在变量末尾添加 ++ 符号来增加年龄一样,我们也可以给变量赋一个 -- 符号来减少其值。
小贴士
请记住,a++ 与 ++a 不同。它们分别被称为后增量运算符和前增量运算符。
在以下示例中,我们从初始值为 36 的变量 a 开始。
如果你执行 a++,这被称为后增量。它将首先显示结果,然后增加 a 的值。
因此,如果你执行 a++,它应该会增加 a 的值,但结果显示仍然是 36,这表明值还没有被增加。然而,在下一行,如果你要求它输出 a 的值,它将显示为 37。

如果你执行 ++a,它被称为预增量。它将首先增加值,然后显示输出。所以在这种情况下,它将在你增加 a 的值的那一行显示 37,如果你要求它在下一行显示 a 的值,它将再次显示 37,就像这样:

这只是为了以防将来你遇到代码中的任何错误,并且想知道为什么你的表达式的正确值没有显示出来。
复合操作
我们还可以执行复合操作来增加、减少、乘以或除以一个更大的数,例如:a+=10、a-=10、a*=10、a/=10 和 a%=10,这些都是 a = a+10、a = a-10、a = a*10、a = a/10 和 a = a%10 的简写方式。所以,如果你用 a+=10 替换 a = a + 10,它仍然会给出相同的结果。这样做是为了优化代码的可读性。

语句
任何编程语言中的语句有两种类型——决策语句和循环语句。
决策语句
决策语句有以下类型:if、if else、else if 和 switch。这在任何编程语言中都是非常标准的。让我们首先看看 if 语句,看看其语法与其他基于 C 的语言有何不同。
if 语句
在 Swift 中,if 语句的写法如下:
if a > b {
println("a is greater than b")
}
立即,使用 C 语言的那些人可能会说,“亵渎!!没有括号!!”是的,在 Swift 中,为了简洁和可读性,你不需要使用括号。如果你想用,你仍然可以使用它们,而且不会得到任何编译错误。
但你绝对必须将语句包含在大括号内。即使它是一行语句,这也是绝对必要的。如果你不这样做,你肯定会得到编译错误。
注意
这里有一个关于在屏幕上记录代码的小提示:当我们开始开发游戏时,我们将非常广泛地使用这个功能,以确保程序确实在执行我们想要它执行的操作。因此,我们将记录语句以检查逻辑错误。
在 Objective-C 中,我们会使用类似 NSLog(@"Print Stuff to Screen") 或 NSLog(@"My age is: %d", age) 这样的代码。在 Swift 中,这有点不同。首先,你不需要在要记录的字符串前加上 @ 符号。其次,为了打印值,我们必须使用 \() 并在括号中放入变量:
println("\(a) is greater than \(b)")

这同样适用于字符串:
let x = "Dude"
let y = "Name"
println("\(x) is my \(y)")

if else 语句
与 if 语句类似,if else 语句的写法如下所示:
if a < b {
println("\(a) is smaller than \(b)")
} else {
println("\(a) is greater than \(b)")
}
我们不是检查 a 是否大于 b,而是检查相反的情况,现在它将通过进入 else 语句来打印 30 大于 10。
else if 语句
与我们未在条件周围放置括号的if语句类似,在else if中,我们不需要在条件周围放置括号:
if a < b {
println("\(a) is smaller than \(b)")
} else if a > b {
println("\(a) is greater than \(b)")
}
因此,在这里,我们检查a是否大于b,然后打印以下语句(来自代码片段),而不是只检查else部分。
条件表达式
对于检查这样简单的语句,不需要 10 行代码,你可以使用条件表达式语句在一行内完成工作。如果你只是检查if-else,这会更受欢迎:
a > b ? a : b
在这里,我们检查a是否大于b。如果a大于b,则表达式评估为true,输出变为36。否则,输出变为10。
switch语句
Swift 中的switch语句与其他语言的switch语句略有不同。像if语句一样,变量或表达式的括号不是必需的,但这不是唯一的区别。
所有语句都需要打印某个值,或者需要检查某个条件。你不能有一个空的case;这将产生错误。其次,由于所有语句都将被评估,因此不需要在每一行的末尾使用break。在其他基于 C 的语言中,所有行都将被评估和执行,而 Swift 中的switch语句则不同,其中只有有效的case将被执行。最后,case需要是穷尽的。这意味着需要在末尾有一个默认值,以便如果没有任何case匹配,则抛出一个默认值或语句。
让我们看看switch-case的一个例子:
var speed = 30
switch speed {
case 10 : " slow "
case 20 : " moderate"
case 30 : " fast enough"
case 40 : " faster "
case 50 : " fastest "
default : " value needs to >= 10 or <= 50"
}
在这里,创建了一个名为speed的新变量。根据速度的值,系统将打印值是慢还是最快。如果speed不是一个数字,或者值不在10到50之间,将打印默认消息。因此,在这种情况下,由于值与case 30匹配,它将打印足够快。
对于情况语句的固定值,你也可以提供一个范围,使得该语句为true。例如,如果值在0到10之间,你希望输出为慢。你可以通过在第一个情况中将范围从0到10表示为0…10来实现。重要的是,你需要在值之间放置三个点,因为它们形成一个运算符。
因此,现在,如果你将速度的值更改为0到10之间的任何值,输出将变为慢。同样,范围也可以用于其他情况语句:
switch speed {
case 0...10 : " slow "
case 20 : " moderate"
case 30 : " fast enough"
case 40 : " faster "
case 50 : " fastest "
default : " value needs to >= 10 or <= 50"
}
循环语句
循环语句用于无限执行特定代码块,或者执行指定次数,或者直到满足某个条件。像任何其他语言一样,我们有while、do while和for循环,以及像 C#和 C++11 中的for each循环。
while循环
在while循环中,我们首先给出一个条件。如果条件保持true,则代码后面的块将一直执行。
对于这个例子,我们创建了两个变量,n和t。我们将n设置为1,将t设置为10。在条件中,每次代码执行时我们都会增加值:
var n = 1, t = 10
while n < t{
n++
}
再次强调,没有必要在条件周围放置括号。但你会看到,在结果面板中,除了打印了九次之外,没有其他输出。
在这里有一个加号图标,旁边还有一个图标。按下它将打开一个新面板。这被称为辅助编辑器。在其中,你会看到一个图表。这显示了循环运行期间n值的增加。它从1开始,一直增加到9,因为我们告诉循环只在n的值小于10时运行。你可以将鼠标箭头移到节点上,以了解每个节点的值。或者,还有一个滚动条可以滚动到图表。

你也可以通过在 Xcode 中转到视图 | 辅助编辑器 | 显示辅助编辑器来打开辅助编辑器:

点击值历史图标以打开图表:

do while循环
与while循环类似,do while循环的代码块在条件为true时执行。然而,在这种情况下,块至少执行一次,然后检查条件。
在这里,每次进入循环时,我们都会将n的值减 1,并检查该值是否大于0。如果是,则执行代码块。前一条代码中n的值是10,所以它从10开始,回到1。再次,这可以通过点击结果面板上的图标在图表中看到:
do{
n--
} while n > 0

小贴士
请务必确保条件会被满足。否则,它将导致无限循环,使你的系统变得不稳定。
for循环
for循环可以写成如下所示。即使没有括号,它也能正常工作:
for var i=0; i < 10 ; i++ {
i*i
}
在这里,我们不是每次都加1,而是将i的值乘以自身,在图表中得到一条曲线,范围从0到81。

for in循环
就像其他语言中的for each循环一样,Swift 中也有for in循环。它遍历循环中的每个项目,并执行代码中声明的任何内容。在这里,与其他语言不同,没有必要指定变量的类型。Swift 会根据你想要它遍历的项目列表自动理解变量类型。例如,遍历整数列表可以写成以下形式:
for l in 1...10{
l * l * l
}
在这里,尽管我们在代码中从未提到l是整型,但 Swift 根据我们在代码中给出的值列表自动理解了它。由于给出的列表类型是int,l的值自动被分配为int类型,并提供了输出:

此外,在遍历数组的索引时,我们希望从0开始,到倒数第二个值结束。当指定范围时,可以使用..<而不是…运算符来实现这一点。因此,在前面的例子中,如果我们需要从0到9,我们可以将代码重写如下:
for l in 0..<10{
l * l * l
}
这显然不仅限于数字;我们可以遍历任何数据类型。例如,我们可以像这样遍历字符串中的字符:
for c in "string"{
println("character \(c)")
}
在这里,并没有明确提到变量的类型是char,然而 Swift 能够隐式判断c应该是char类型。在控制台中,每个字符将显示如下:
character s
character t
character r
character i
character n
character g
数组
数组是一段连续的内存块,用于存储某种类型的数据。它可以是一个预定义的数据类型,如int、float、char、string或bool,或者它可以是用户定义的。此外,数组是零基的,这意味着数组中的第一个对象是0。
数组可以是var或let类型。如果我们创建一个var类型的数组,那么我们可以像在 Objective-C 中拥有MutableArray一样更改、添加或从数组中删除对象。如果你想在 Swift 中有一个ImmutableArray数组,而不是使用var,请使用let关键字。Swift 中的数组可以声明和初始化如下:
var score = [10, 8, 7, 9, 5, 2, 1, 0, 5, 6]
var daysofweek = ["Monday", "Tuesday", "Wednesday"]
现在,如果我们想要声明一个数组并在稍后初始化它,我们可以按照以下代码片段所示进行声明:
var score : [Int]
var daysofweek : [String]
在这里,你必须提供数组将要存储的数据类型。它与定义常规变量非常相似;只是这里的类型被方括号包围。现在我们可以像之前一样初始化它:
score = [10, 8, 7, 9, 5, 2, 1, 0, 5, 6]
daysofweek = ["Monday", "Tuesday", "Wednesday"]
要获取一个索引处的项目,你可以使用带有索引数字的变量名,放在方括号中:
Score[0] // output: 10
Score[5] // output: 2
遍历数组
要遍历数组,我们可以使用for in循环,就像我们在循环中看到的那样。在这里,我们不是传递范围,而是提供范围本身,并且像之前一样,Swift 会自动确定数组中的数据类型:
for myScore in score {
myScore
}
for day in daysofweek{
println("\(day)")
}
向数组中添加、删除和插入对象
要向数组中添加项目,你可以使用append方法。因此,要将值添加到score或daysofweek数组中,我们按照以下代码片段所示进行操作。这将把对象添加到数组的末尾:
score.append(10)
daysofweek.append("Thursday")
你可以通过调用以下方法来移除数组中的最后一个对象:
score.removeLast()
如果你想要删除特定索引处的对象,你可以使用以下行:
score.removeAtIndex(5)
这将移除第五个索引处的元素,在这里是2。要在一个特定的索引处插入一个项目,我们必须提供值和项目要插入的索引:
score.insert(8 ,atIndex: 5)
重要的数组函数
除了append、remove和insert等函数之外,还有其他我们将经常在游戏中使用的内置函数。第一个是count函数,它告诉我们数组中有多少个元素:
score.count
另一个函数是 isEmpty,它可以用来检查一个数组中是否包含任何项目:
score.isEmpty
字典
在其他语言中也称为映射或哈希表,字典——就像数组一样——是一种数据集合类型。然而,与数组不同——在数组中,每个元素只能通过索引号访问——在字典中,我们将提供键,通过这些键我们可以访问特定索引处的元素。键和值的值可以是 int、float、bool 或 string。键不能重复,但在字典中显然可以有重复的值。
例如,让我们看看国际足联的国家代码列表:
| Code | 国家 |
|---|---|
| AFG | 阿富汗 |
| ALB | 阿尔巴尼亚 |
| ALG | 阿尔及利亚 |
| ASA | 美属萨摩亚 |
| AND | 安道尔 |
| ANG | 安哥拉 |
| AIA | 安圭拉 |
| ATG | 安提瓜和巴布达 |
| ARG | 阿根廷 |
| ARM | 亚美尼亚 |
左侧列中的代码可以称为 键,右侧的 国家 列表示 值。所以,当我想引用阿根廷时,我可以简单地调用 ARG,Swift 会自动理解我是在引用阿根廷。这里还有一个例子:当我们说,“获取 ATG 键的值”时,程序将知道我们是在引用安提瓜和巴布达。
字典的优势在于数据不需要排序。如果我们知道键值对存在于字典中,无论它在列表中的位置如何,我们都可以通过键来获取值。
这种语法与数组类似。唯一的区别是在声明时,我们不仅要提供一个变量类型,还要提供两个类型,第一个是键的类型,第二个是值的类型。
再次强调,如果你已经知道 key 和 value 的类型,你不需要显式提供类型:
var countries = ["AFG": "Afghanistan",
"ALB": "Albania",
"ALG: Algeria"]
键值对可以是任何组合。这意味着你可以有一个 key 变量是 int 类型,而 value 是 string 类型;key 是 string 类型,而 value 也是 string 类型,如示例所示;或者 key 是 string 类型,而 value 是 int 类型;等等。一旦声明了字典键和值的类型,就不能更改或提供不同的类型。另外,如果你已经声明键将是 string 类型,就不能添加 int 类型的键。
我们还可以显式地指定键值对的数据类型,如下所示:
var states:[String: String]
这告诉我们键将是 string 类型,与这些键关联的值也将是 string 类型。如果我们想存储一组键的种群,可以按照以下方式操作:
var population: [String: Int]
向字典中添加和删除对象
要向现有字典中添加新的键值对,我们可以简单地运行以下两行代码之一:
countries["GER"] = "Germany"
countries.updateValue("Netherlands","NED")
然而,请注意,如果键已经存在,它将覆盖当前持有的值。
如果你想要删除一个键值对,你可以使用以下任一语句来删除它:
countries["ALB"] = nil
countries.removeValueForKey("AND")
现在,如果我们尝试访问键值对,我们会注意到它不仅删除了由键存储的值,还删除了键本身。
遍历字典中的项
再次强调,我们可以使用for-in循环来访问存储在每个键值对中的数据,但我们需要在括号中提供两个值,用逗号分隔键和值:
for (code, country) in countries{
println("\(code) is the code for \(country) ")
}
控制台输出将如下所示:
GER is the code for Germany
AFG is the code for Afghanistan
ALG is the code for Algeria
NED is the code for Netherlands
字典函数
与数组类似,字典也有内置函数来确定其中存在的键值对数量。它还有一个isEmpty函数,可以调用字典来了解它是否包含任何键值对:
countries.count
countries.isEmpty
函数
函数是一段用于执行特定任务的代码块。它可以用来分配一个可重用的代码块,你可以反复调用它而无需每次都重写代码来执行该任务。
简单函数
Swift 中的函数编写如下:
func someFunction(){
Println(" performing some function ")
}
因此,在函数之前,我们需要使用func关键字,后跟函数名,括号,以及大括号。
调用函数在这里与任何其他语言非常相似,即函数名后跟括号:
someFunction()
传递参数
现在,要对传递给函数的变量执行某些任务,我们首先需要将一个参数传递给函数。这可以这样做:
func printText(mtext: String){
println("Print out \(mtext)")
}
这里,我们必须在括号内提供输入变量类型。当函数期望一个字符串时,我们不能在这里提供一个整数,因为它明确地接受类型。
要执行函数,请调用函数,并在括号内包含你想要执行函数的文本:
printText("Hello Function")
应该注意的是,传递给函数的值默认是常量。这意味着即使我们没有指定mtext是变量还是常量,它仍然是一个常量。因此,你将无法在函数内对其进行任何修改。
如果你的代码需要类型是变量而不是常量,你需要在创建函数时指定这一点:
func printVarText(var mtext: String){
mtext = "Text Changed"
println("Print out \(mtext)")
}
传递多个参数
我们显然可以传递多个变量。在这里,你需要用逗号分隔变量类型,如下所示:
func calcSum(a: int, b: int){
let sum = a + b
println("The sum of the numbers is: \(sum) ")
}
这里,我们向函数传递两个数字,10和15。我们执行它们的加法,将值存储在名为sum的常量中,并通过调用以下函数将其打印到控制台:
calcSum(10, 15)
返回值
对于 C++用户来说,这可能会显得有些奇怪。同样,不习惯指针的人可能会对函数返回参数的语法感到恐慌,因为它在 Swift 中使用指针运算符。别担心!这绝对与指针无关。这只是显示这个函数将返回一个数据类型,仅此而已。
函数的编写方式与通常一样,但在括号后面指定返回类型时,我们输入破折号和大于号,然后输入返回类型。
这里,我们使用一个接受两个int值并返回int值的函数执行乘法运算。我们通过执行乘法操作计算值,将值存储在一个名为mult的常量中,然后返回该值。
函数被调用,结果存储在一个变量中,然后打印到控制台:
func mult(a: Int, b: Int) -> Int{
let mult = a * b
return mult
}
let mVal = mult(10, 20)
println("The Multiplied valued is = \(mVal)")
这里,控制台将输出乘积值——200。
默认和命名参数
如果我们想在函数的参数中指定默认值,我们当然可以这样做,如下所示:
func defMult(a: Int = 20, b: Int = 30) -> Int{
let mult = a * b
return mult
}
let dVal = defMult()
println("The Multiplied valued is = \(dVal)")
这里,我们指定20和30作为默认值。我们只需调用函数即可得到输出300。
但如果我们只想修改一个值,而保留其他值使用默认值怎么办?在这种情况下,我们将在调用函数时通过名称指定一个值。所以,如果我们想将a的值设置为80而不是默认值,我们将这样做:
println("The Multiplied valued is = \(defMult(a: 80))")
如果我们想改变两个值,我们也可以这样做,如下所示:
println("The Multiplied valued is = \(defMult(a: 80, b: 50))")
返回多个值
在 Swift 中,我们可以使用元组返回多个值。
注意
元组是可以存储两个值的变量,就像数组一样。实际上,包含三个值的变量被称为 3 元组。
元组可以这样初始化:
var person : (int, string)
单个值可以初始化,如下所示:
person.0 = 23
person.1 = "The Dude"
你也可以为变量命名以方便使用。然后你可以通过名称来访问它们以分配值,而不是使用索引:
var person2:(age:Int, name: String)
person2.age = 23
person2.name = "The Dude"
在 Swift 中,你将能够传递实际值,对变量执行一些操作,然后返回变量。当返回两个值时,我们需要提供两个值的返回类型,用逗号分隔:
func getAreaAndPerimeter(a: Int, b: Int) ->(Int, Int){
let area = a*b
let perimeter = 2a+ 2b
return(area, perimeter)
}
因此,在先前的函数中,我们接受两个整数,计算矩形的面积和周长,然后返回这两个值:
let value = getAreaAndPerimeter(40 ,80)
值存储在一个名为value的常量中,要将值输出到控制台,我们使用点操作符。它用0来获取第一个值,用1来获取第二个值:
println("Area is = \(value.0) and Perimeter is = (value.1)")
这有点麻烦,因为我们必须记住返回的第一个值是面积,第二个值是周长。但在 Swift 中,这也可以很容易地解决。就像我们传递时命名值一样,我们也可以命名返回的值。然后,而不是使用索引值,我们可以使用名称本身来访问值:
func getNamedAreaAndPerimeter(a: Int, b: Int) ->(area: Int, perimeter: Int){
let area = a * b
let perimeter = 2 * a + 2 * b
return (area, perimeter)
}
let namedvalue = getNamedAreaAndPerimeter(80 ,100)
println("Named Area is = \(namedvalue.area) and Named Perimeter is = \(namedvalue.perimeter)")
类
与 Objective-C 或 C++相比,在 Swift 中创建类相当简单。不需要像接口文件和实现文件这样的单独文件。此外,也没有使用属性关键字来定义属性。所有 Swift 文件都以.swift扩展名结尾。
属性和初始化器
在创建类时,我们必须使用class关键字:
Class Character{
var name = "The Dude"
var health = 100
}
还要注意,在结束括号之后没有分号。要实例化 character 类型的变量,可以使用以下代码行。在这种情况下,根本不需要 alloc,就像在 Objective-C 中那样:
var theDude = Character()
可以使用点操作符访问 name 和 health 属性:
theDude.name
theDude.health
要初始化变量,我们需要使用 init 函数。这是类的构造函数,它不需要 func 关键字,这在创建任何其他函数时是必需的。所以,如果你想在 init 函数中初始化 name 和 health 变量,你必须像以下代码片段中那样做。注意,在这种情况下,你需要明确为 name 和 health 变量提供变量类型:
class Character1{
var name: String
var health: Int
init(){
name = "The Dude"
health = 100
}
}
我们可以像以前一样实例化和访问类的属性。我们肯定可以创建尽可能多的自定义初始化器。这些也会使用 init 关键字,并且你可以在括号中传递变量的类型并将值分配给属性:
class Character2{
var name: String
var health: Int
init(){
name = "The Dude"
health = 100
}
init(name: String){
self.name = name
self.health = 100
}
}
注意,在这里,在将传递给属性的变量赋值时,我们执行 self.name 以告诉代码我们将传递变量的名称赋给类的 name 属性。
此外,在传递值时,我们必须提到我们将它传递给 name 参数:
var hero = Character2(name: "Hero")
hero.name
hero.health
自定义方法
我们显然可以在类中创建自定义方法。这里,假设我们想让角色在被击中后受到一些伤害。为此,我们将创建一个新的方法,如下所示。在类的结束括号之前添加此块:
func takeDamage(damage: Int){
self.health -= damage
}
类中的方法定义方式与其他任何函数一样。这里,我们定义了一个名为 takeDamage 的方法,它接受一个名为 damage 的 int 类型的变量。现在我们可以调用这个函数在之前实例化的 hero 变量上,并传递 10 以让英雄受到 10 点伤害。如果我们再次调用 health 属性,我们会看到英雄的健康值下降了 10 点:
hero.takeDamage(10)
hero.health
现在,让我们给类添加一个名为 armour 的属性,类型为 int。让玩家的初始护甲等级为 10。每次玩家受到攻击,他的护甲也会下降:
func reduceArmour(damage: Int, armour: Int){
self.health -= damage
self.armour -= armour
}
我们添加了一个名为 reduceArmour 的新函数。在其中,我们接受一个名为 damage 的额外参数,并减少类中 armour 属性的相应数量:
hero.reduceArmour(10, armour: 2)
hero.health
hero.armour
现在,当我们想要调用方法时,我们必须明确指出传递的第二个值是用于 armour 的。当我们调用 health 和 armour 属性时,我们可以看到 health 再次减少了 10 点,而英雄的护甲现在减少了 2 点。
继承
就像在其他任何我们希望重用或扩展现有类的面向对象语言中一样,Swift 也允许继承。要继承另一个类,我们需要使用冒号并在定义当前类时指定我们想要扩展的类的名称。
小贴士
Swift 不允许多重继承。
假设我们想要创建一个 Mage 类,法师有一个名为 magic 的属性,这样她就可以在普通伤害之外造成一些“魔法”伤害:
class Mage: Character2{
var magic: Int
override init(name: String){
self.magic = 100
super.init()
//self.name = name
//self.health = 60
//self.armour = 15
}
}
在这里,我们创建了一个名为 Mage 的新类,并从 Character2 类继承。我们需要使用 override 关键字来告诉代码我们正在重写 Character2 类的 init 函数,这是超类。
在 Objective-C 中,我们通常会先调用 super.init,但在 Swift 中,我们需要先初始化 magic 属性,因为这个属性在超类中不存在。因此,我们首先初始化它,然后调用 super.init 方法。
如果我们为名为 vereka 的角色创建一个新的变量,其类型为 Mage 并将其命名为 vereka,我们会看到分配的名称和其他值是 Character2 超类的,而不是我们分配的名称:
var vereka = Mage(name: "Vereka")
vereka.name
vereka.health
vereka.armour
vereka.magic
为了为新名称分配,我们必须在 Mage 类的 init 方法中调用 super.init 之后再次初始化它。因此,我们在 Mage 类中取消注释分配名称和新的 health 以及 armour 值的代码行。现在,将显示正确的 name、health、armour 和 magic 值。
访问修饰符
在 Swift 中,有可以用来封装类、属性和方法的访问修饰符。访问修饰符提供了对变量的读写访问。Swift 中既有常规的 public 和 private 访问修饰符。
当创建一个类时,如果一个变量或函数被设置为 public,那么任何类都可以访问并修改该变量,但如果它被设置为 private,那么其他类则不能访问该变量。
除了公共和私有访问修饰符之外,还有一个额外的修饰符称为 internal。如果你在 Objective-C 或 C++ 中使用过封装,你会很高兴地知道在 Swift 中公共和私有变量几乎以相同的方式工作,而 internal 替换了 protected 封装类型。
在 Swift 中,internal 规范是默认规范类型,与 C++ 不同,在 C++ 中,如果没有指定,默认规范是私有。
尽管我们没有在 Character2 类中指定访问修饰符,但我们仍然能够在 Mage 类中访问其变量和函数,因为它们是内部的,可以被子类访问:
public class myCharacter {
public var name: String
private var age:Int
var speed:Int
public init(){
self.name = "The Dude"
self.age = 100
self.speed = 20
}
}
myCharacter 类和 name 变量是公开的。age 变量是私有的,速度被假定为内部变量,因为它没有其他指定。
可选类型
可选类型是 Swift 中的新数据类型。在本章迄今为止的所有案例中,我们总是初始化了一个变量,无论是直接初始化还是在一个类的初始化器中初始化。如果我们没有初始化变量就开始使用它,我们会得到一个错误,说我们还没有初始化它。快速修复方法是将其初始化为 0 或 ""。
假设这个变量是score,我们将其值初始化为零以消除错误。现在我们尝试检索 GameCenter 中存储的最后分数。由于某种原因,没有互联网连接或 Wi-Fi,所以我们无法检索该玩家的最后score。如果我们显示分数在屏幕上,它将显示0,因为这是存储在score中的值。玩家会非常困惑和沮丧,因为他们确信在他们的最后一次尝试中分数远高于零。现在,我们如何告诉玩家我们无法检索分数是因为他们忘记支付互联网账单呢?
换句话说,我们如何告诉程序score中没有值?我们甚至不能将score等于nil,因为我们将会得到一个错误,说它不是nil类型。为此,我们在类型后加上一个问号。这意味着如果我们分配一个值,它将是int类型;否则,它应该被视为nil。
现在,在显示分数的时候,我们可以检查值是否为nil。如果是,我们可以打印一条消息说明没有互联网连接,如果score包含一个值,我们也可以打印出来:
var score:Int?
if score != nil {
println("Yay!! Your current score is \(score)")
}else{
println(" No internet! Pay your bills on time ")
}
之前的代码将从else块中打印出来,因为值没有被分配——它仍然是nil。
现在,如果我们分配一个值,比如说score = 75,它将打印这个值,但会带上可选关键字以及括号内的值。这是因为,由于我们指定的类型是可选的,它让我们知道数据类型。
为了在记录时不出现在可选关键字,我们需要“展开”可选类型。这是通过在if块中跟随score变量并使用感叹号来完成的,如代码行所示:
println("Yay!! Your current score is \(score!)")
可选关键字现在在记录分数时将消失。
你可能不会在你的游戏中使用可选参数,但如果你不得不这么做,当你看到其中一个时,至少不会感到惊讶。
摘要
在本章中,我们看到了 Swift 语言的一些基础知识。这应该让你对语言有足够的了解,以便从下一章开始,你将有一个很好的语法概念,并确切地知道代码中发生了什么。你现在也可以回顾一下第一章,看看 SpriteKit 和 SceneKit 项目文件中的代码,看看你是否能理解这些代码。
正如章节所说,这只是一个 Swift 语言的入门介绍。与其他语言相比,这个语言在某些方面非常相似,在其他方面则非常不同。在接下来的章节中,当我们遇到这些差异时,我会指出它们,这样你就能清楚地意识到错误,并知道如何绕过它们,更好地理解它们。
在下一章中,我们将深入探讨 Xcode,从 Xcode 的基础开始。
第三章。Xcode 简介
在上一章中,我们看到了 Playground 工具。这是一个简单的测试代码的工具,但如果你想要真正创建一个应用程序并在设备上运行它,或者通过 Mac 或 iOS App Store 发布和分发它,你需要创建一个 Xcode 项目。
在本章中,我们将游览 Xcode。我们将详细查看界面,创建一个非常基本的“Hello World”应用程序,并在模拟器和稍后设备上运行它。
在本章中,我们将详细介绍以下主题:
-
Xcode 应用程序类型
-
Xcode 界面
-
在设备上运行应用程序
Xcode 应用程序类型
Xcode 提供了一些预定义的应用程序类型,用于创建特定类型的应用程序。它们是主-详细应用程序、基于页面的应用程序、单视图应用程序、标签页应用程序和游戏类型,如下面的截图所示。我们已经看到了游戏类型的内容,所以让我们看看其他类型:

主-详细应用程序
如果你想要为 iOS 创建一个非常基础的应用程序类型,例如笔记应用程序,你应该选择这个模板,然后修改它以创建你的应用程序。有一个主列表(如下面的截图所示),当你点击列表上的每个项目时,它将显示列表中选定项目的详细信息。在详细视图的左上角有一个可定制的主按钮。点击它将带回到主屏幕:

你可以以类似于你在第一章中创建游戏应用程序的方式创建项目:点击应用程序类型的图标,然后选择项目的名称、位置、捆绑 ID 以及你想要为应用程序创建的平台。
基于页面的应用程序
如果你想要创建一个类似于翻页效果的书本应用程序,你需要选择基于页面的应用程序模板。你可以指定书封面,然后向页面添加内容以创建翻页应用程序。你可以用手指向左滑动以翻到下一页,或者向右滑动以返回上一页。

标签页应用程序
标签页应用程序模板将在屏幕底部有一排按钮,点击它将显示不同的视图。标签页应用程序模板类似于在你的设备上打开音乐应用时标签页的工作方式。当音乐应用打开时,显示的标签是播放列表。其他标签包括艺术家、歌曲、专辑、流派、合辑和作曲家(最后一个标签)。播放列表标签显示你存储的播放列表,但如果你点击艺术家标签,它将显示按艺术家排序的歌曲列表。如果你构建默认的应用程序,你将有两个标签:第一个和第二个。点击底部的标签将打开相应的屏幕,如下所示:

单视图应用程序
单视图应用程序模板是最基本的。如果你的应用程序不符合任何之前的应用程序模板类型,那么你可以使用这个模板来创建自己的应用程序。
对于本章,我们将从基础知识开始,因此我们将创建一个单视图应用程序。点击Xcode以打开它,然后点击创建一个新的 Xcode 项目。在iOS部分,在左侧面板中选择应用程序。然后,选择单视图应用程序并创建它。
当你创建一个项目并运行它时,模拟器将打开以显示一个空白屏幕,如下面的截图所示。当我提到单视图应用程序模板是最基本的,我并不是在开玩笑。你将获得一个单视图控制器,没有任何按钮进行导航。这里的优势是,你将看到一些苹果已经提供的用于从头开始创建应用程序的基本类,然后你可以添加自己的项目以使其成为你的应用程序。

你应该保存这个空白项目,因为我们将在下一节介绍 Xcode 界面时参考它。所以请继续;命名文件并将项目保存在 Mac 上。
Xcode 界面
如果你正在继续上一节的内容,那么请打开上一节中创建的单视图应用程序。否则,你可以创建一个新的单视图应用程序项目。由于我们将在接下来的书中大部分时间都会查看这个窗口,让我们详细了解其布局。
打开项目,你将看到以下截图所示的窗口。项目视图的顶部区域被称为工具栏。工具栏下方左侧是项目导航面板。中间下方是编辑器面板,其右侧是实用工具面板。还有一个可能默认未打开的调试面板,但稍后我们将激活它:

每个工具栏和面板中都有很多信息。让我们逐一查看。
工具栏
Xcode 项目的顶部是工具栏。在左侧的最大化按钮旁边,播放和停止按钮用于运行和停止应用程序。这些按钮旁边是方案,您可以在其中选择目标应用程序和您想要运行应用程序的设备,无论是实际设备还是模拟器。
工具栏中央是活动视图。这显示了应用程序在任何给定时间的状态。当您构建项目时,它将显示构建进度,显示构建过程的各个阶段。

在活动视图右侧,有两组三个按钮。右侧的第一组三个按钮用于根据您的需求更改编辑器面板:
-
标准编辑器:每次创建新项目时都会默认激活。此设置用于编码、调试或导航项目。我们将几乎一直使用此面板。
-
辅助编辑器:点击此按钮,将在标准编辑器右侧打开另一个面板。这被称为辅助编辑器。它将根据在标准编辑器中点击的内容提供更多信息。在以下示例中,在导航面板中,我点击了
Main.Storyboard文件。标准编辑器会改变以显示文件中的对象,而辅助编辑器则显示与在标准编辑器中选择的视图控制器关联的类的内容。![工具栏]()
-
版本编辑器:当选择时,标准编辑器再次分为两个面板,我们将能够看到和比较当前版本和之前版本代码的变化。在以下截图所示的示例中,我对
ViewController.swift文件进行了一些更改。左侧面板中突出显示了添加的注释,右侧面板显示了这些更改在文件中的位置。当使用源代码管理进行项目时,这显然非常强大。源代码管理已包含在 Xcode 中,并在创建新项目时启动。![工具栏]()
编辑器右侧的三个按钮用于隐藏和显示导航器、调试和实用工具面板。这些面板可以根据您的需求或方便性显示或隐藏。例如,为了获取前面的截图并获取更多屏幕空间,我通过点击最右侧的按钮隐藏了实用工具面板。
工具栏的所有内容就这些了。让我们接下来看看导航面板。
导航面板
工具栏左侧下方是导航面板。它有八个标签。从左到右,它们是项目导航、符号、查找、问题、测试、调试、断点和报告导航器。点击任何标签都会激活它。默认情况下,项目导航标签被选中。
项目导航标签
项目导航 标签显示了你在项目中的文件内容,包括代码文件和资源文件。稍后,如果你向游戏中添加框架,这些框架也将在此面板中显示,使导航更容易。正如我们之前看到的,你可以通过单击代码文件(如 .swift 文件)来编辑代码。在编辑器窗口中,你可以添加、删除和修改代码。如果你有图像资源文件和三维网格,这些文件也将在此处显示。通过单击图像和三维文件,你可以查看内容,但无法修改它。为此,你需要使用 Photoshop 等应用程序打开文件来编辑图像,以及使用 Maya 或 3dsmax 等应用程序来编辑三维几何形状:

你可以使用文件夹(在这里称为 组)来组织一组文件。请注意,这只是为了在 项目导航 中组织文件。在 导航 面板中创建新的组或文件夹不会在系统项目目录中创建文件夹。
符号导航标签
右侧的下一个按钮是 符号导航 标签。你可以访问项目中的所有符号,如函数、方法、属性、类、结构体、枚举和变量。

C 表示这是一个类,M 表示方法,P 表示属性。还有其他符号,但这些是你通常会遇到的最基本的符号。
因此,在 AppDelegate 类中,有六个方法和一个名为 window 的属性。
查找导航标签
查找导航 标签用于通过名称在项目中搜索短语或文件。在以下示例中,我正在尝试搜索术语 UIKit,看起来它被导入到三个类中:

你也可以选择在当前评分中搜索关键字,或者创建一个新的范围在其他地方查找。此外,通过单击 查找 下拉箭头,你可以选择在包含、匹配、以搜索词开头或以搜索词结尾的文本上查找或替换命令。

问题导航标签
当你的项目有构建错误时,错误和警告列表、计数和描述将在 问题导航 标签中显示。你可以点击列表中的条目来显示错误存在的位置和文件:

在前面的例子中,我忘记在 ViewController 类中关闭注释部分,所以在构建项目后,我得到了一个错误,告诉我 ViewController.swift 中有一个未确定的 /* 注释。在某些情况下,错误消息可能不是很明确,但至少,你会得到一些关于可能引起错误的线索。
测试导航标签
如果您在项目中使用单元测试,则测试结果将在测试导航器选项卡中显示。单元测试是一个独立的话题,不幸的是,它超出了本书的范围。要了解更多关于单元测试的信息,您可以查看 Apple 官方文档,了解更多信息,请访问developer.apple.com/library/ios/recipes/xcode_help-test_navigator/Recipe.html#//apple_ref/doc/uid/TP40013329-CH1-SW1。

调试导航器选项卡
调试导航器选项卡用于调试并显示代码的优化程度。此导航器仅在应用程序运行时才处于活动状态。当应用程序在模拟器或设备上运行时,我们可以获取有关 CPU、内存、磁盘和网络使用的相关信息。除了通用系统信息外,您还可以获得调用顺序的方法和函数的堆栈。点击方法和函数将在编辑器面板中打开它们。

运行应用程序右侧有两个按钮。左侧的第一个按钮允许您隐藏或显示仪表盘,右侧的按钮允许您选择是否要按线程、队列或UI 层次结构查看进程。
断点导航器选项卡
断点导航器选项卡显示了我们在项目中添加到文件的断点位置。断点的位置按类和方法显示。它还会显示添加断点的行号。

通过右键单击任何断点,您可以编辑、禁用、共享、删除或移动它。
报告导航器选项卡
报告导航器选项卡显示了最近构建的历史记录和项目的日志,以及时间戳:

这就完成了关于导航面板的部分。
实用工具面板
实用工具面板的顶部部分是上下文相关的,取决于在导航面板中单击了哪种类型的文件。实用工具面板的底部部分有四个选项卡,用于不同类型的库,以便将特定的库对象拖放到编辑器面板。有时,顶部部分会被面板的底部部分隐藏,但您可以拖动实用工具的底部部分来揭示其下的内容。

实用工具面板一旦我们跳过项目并逐个查看每个文件,就会更容易理解。
单视图项目
返回到项目导航器标签页。这是导航面板中从左数第一个标签。让我们看看项目导航器标签页中的文件。项目导航器标签页显示了项目根目录,以及其下的所有与项目相关的文件。让我们首先看看项目根目录。
项目根目录
当您点击标签页顶部的应用根时,您将看到编辑器面板变为显示六个标签:通用、能力、信息、构建设置、构建阶段和构建规则。大多数时候,您将关注前三个标签:通用、能力和信息。
通用
此标签页包含有关应用的基本信息。我们在第一章“入门”中简要地看过它,当我们改变游戏方向时。此标签页有五个子部分,如下截图所示:

上述截图包含以下部分:
-
标识:这显示了应用捆绑标识符、应用的版本号、应用的构建号和团队。当我们将在设备上部署应用时,我们将查看团队,因为它需要一些步骤来获取它。
-
部署信息:在这里,您可以选择部署目标的值。默认情况下,我们在这里针对的是 iOS 8.1 设备,但如果我们想让我们的应用与旧版本兼容,可以使用 7.0。然而,我们必须确保我们没有使用任何 8.1 的 API,否则应用将给出构建错误。我们可以选择我们想要的目标设备,无论是 iPhone 还是 iPad,或者制作一个通用应用。主界面文件是当应用完成加载时将被调用的第一个文件。这里的文件应该有
.storyboard扩展名,因此当应用启动时,我们在这里调用Main.storyboard文件。我们还可以在这里更改设备方向并选择状态栏样式的值,也可以通过点击复选框来隐藏状态栏。 -
应用图标和启动图像:这里提供了应用图标的源文件。在这里选择了
AppIcon文件。如果您想知道这个文件在哪里,它位于Images.xcassets文件夹中。我们将使用这个文件为不同的 iOS 版本和设备分配图标。如果您想在应用启动时显示图像,您可以创建一个资产目录文件来显示不同设备和 iOS 版本的启动图像,类似于图标。我们还可以指定一个启动屏幕文件,在应用启动时将显示该文件。在这里,LaunchScreen文件将在启动时显示。LaunchScreen文件的文件扩展名,之前是.nib格式,现在应该是.xib格式。尽管扩展名已更改,但它们仍然被称为 NIB 文件。 -
嵌入的二进制文件:这显示了项目中嵌入的任何二进制文件。
-
链接的框架和库:这显示了项目中包含的框架和库列表。
能力选项卡
这显示了应用程序使用的 Apple 服务。要使用服务,您需要在右侧将其开启。游戏通常使用 GameCenter、应用内购买和 iCloud 集成等云保存服务。

信息选项卡
信息选项卡包含有关版本号、构建号、故事板文件基本名称(这是应用程序启动时加载的故事板文件)、应用程序名称以及其他一些信息。所有这些信息都是从info.plist文件中加载的,该文件位于项目的Supporting Files文件夹中。还有一些其他内容,如文档类型、导出的 UTI、导入的 UTI 和 URL 类型,在本书的大部分内容中,你不会与之打交道。

构建设置选项卡
构建设置选项卡的基本视图显示了有关部署的信息,例如目标设备、iOS 版本、框架路径的位置、打包信息,例如info.plist文件的位置、产品名称、资产目录应用图标集名称以及其他用户定义的设置。

通过点击所有选项卡而不是基本选项卡,可以查看更详细的信息。这将提供有关支持的架构的设置、构建的位置和选项以及代码签名的信息,这些内容将在我们准备在设备上测试应用程序并将其部署到 App Store 时进行介绍。它还包括有关内核模块、链接器和编译器的进一步设置和信息。
构建规则选项卡
构建阶段选项卡显示了添加的目标依赖项、源文件列表、添加的库和添加的资产资源。

这个选项卡相当重要,因为当你想要向项目中添加框架时,你会来到这个选项卡来包含它们。此外,有时当你遇到构建错误时,你可能想要检查是否所有必需的源文件实际上都包含在编译源文件列表中,因为这可能是构建错误的原因。随着项目变大,构建错误可能会出现,因为你可能删除了一些源文件。
构建规则选项卡
你可能永远不会需要在这里更改任何内容,因为这个选项卡主要用于当你想要以特定方式编译特定文件类型时。为了定义某种文件类型的自定义过程,你只需创建一个新的构建规则。

现在您已经查看了项目根目录,您可以点击项目根目录旁边的三角形按钮来打开项目树,如果它还没有打开的话。
在创建的项目下,有三个组。第一个组与项目名称相同。第二个组包含项目测试文件的文件,因此它将始终包含项目名称,并以 Tests 后缀。第三个是 Products 组,包含 .app 文件和 Tests 文件。在大多数时间里,我们将处理第一个组的文件,即应用名称之后的组名。第一个文件夹是所有代码、资源和项目相关文件应该存在的地方。您可以创建类似于 Supported Files 文件夹的子文件夹来更好地组织项目。因此,您可以创建一个包含项目所有类的 Classes 文件夹,以及一个可以放置您的图片、图标和三维对象的资源文件夹。
让我们详细查看主要项目文件夹中的每个文件。
项目文件夹
主要项目文件夹包含 AppDelegate.swift、ViewController.swift、Main.Storyboard、Images.xcassets 和 LaunchScreen.xib 文件;以及 Supported Files 文件夹。
一旦我们打开 AppDelegate.swift 文件,我们会看到 AppDelegate 类继承自 UIResponder 和 UIApplicationDelegate 类。这两个类都是 UIKit 的组成部分。UIKit 框架是最重要的,因为它是创建和管理任何 iOS 应用所必需的。此框架提供了基本元素,如窗口和视图的创建。
UIResponder 类负责处理事件,例如检测触摸和运动事件。对于触摸事件,它有 touchesBegan、touchesMoved、touchesEnded 和 touchesCancelled,当手指触摸屏幕时会被调用。有三个运动事件:motionBegan、motionEnded 和 motionCancelled:
-
UIApplicationDelegate: 这是一个UIResponder的子类,它会在应用的生命周期中响应事件。AppDelegate类中的函数来自UIApplicationDelegate,当这些函数被应用触发时,将会被调用。 -
applicationdidFinishLaunchingWithOptions: 当应用完成启动时,会调用此函数。当应用启动时,它返回true。 -
applicationWillResignActive: 当应用刚刚变为非活动状态并即将进入后台时,会调用此方法。当你接听电话或按下主页按钮切换应用时,会触发此方法。它用于禁用计时器或暂停任何更新功能。 -
applicationDidEnterBackground: 当应用进入后台并且不再活跃时,此函数会被调用。如果应用即将被终止,您可以保存用户分数并使应用准备就绪。 -
applicationWillEnterForeground:一旦后台运行的应用程序被选中并即将再次激活,此函数将被调用。由于用户没有终止应用程序,您可以在此处恢复到之前的状态。 -
applicationDidBecomeActive:现在应用程序再次完全激活,因此您可以恢复游戏并取消暂停和恢复更新函数。 -
applicationWillTerminate:这是在应用程序完全终止之前将被调用的最后一个函数,因此您可能想要保存游戏并释放所有对象。
有一个可选变量被创建,称为 window,它是 UIWindow 类型。每个创建的应用程序都在一个窗口内部,这个窗口变量将提供对当前窗口的访问。如果需要,您可以将当前窗口分配给此变量以访问当前运行窗口的属性,例如窗口的大小。
在我们查看 ViewController.swift 之前,您需要了解 Main.Storyboard,因为 ViewController.swift 类是通过 Main.StoryBoard 调用的。
Main.storyboard 文件在应用程序完成启动后自动调用。这是 Xcode 通过标准协议自动完成的,因此您在 AppDelegate 类的 applicationDidFinishLaunching 函数中看不到它被调用。我们已经在项目根的 常规 选项卡中调用了它。现在让我们详细查看该文件并了解它做了什么。为此,您需要了解 iOS 中的故事板是什么:

故事板是 iOS 接口构建器或 UIToolkit 的一部分。它用于创建用户界面而无需编写任何代码。您可以在您的应用程序中添加按钮、文本或滑块。此外,通过点击按钮,您可以通过在当前视图和下一个视图之间创建链接来使应用程序更改视图。您可以通过创建此类视图的链来为您的应用程序创建屏幕流程。
每个故事板都需要一个 ViewController 来启动应用程序。ViewController 类似于屏幕,是应用程序构建块。您将链接故事板中的 ViewController 来开发您想要的任何应用程序的屏幕。
ViewController 与自定义类 ViewController.swift 文件相关联。这可以通过点击 实用工具 面板中的 身份检查器(从左数第三个标签)来在 实用工具 面板中看到。在自定义类中,在 class 字段中指定了 ViewController 类。我们将在稍后详细介绍 实用工具 面板。

如果您看不到 ViewController 树,您可以点击 编辑器 面板左下角的小方块图标来打开它。视图控制器场景 包含 视图控制器、第一响应者 和 退出。

通过点击 ViewController 旁边的三角形,你会看到它包含三个更多项:顶部布局指南、底部布局指南和视图。顶部和底部布局显示了视图的顶部和底部端点。顶部从电池指示图标开始,底部在视图底部结束。这些更像是指南,让你知道你需要在此范围内工作的限制。视图项是用户在 ViewController 打开时将能够看到的整个区域。
第一响应者项是你在 ViewController 中首先与之交互的对象。这将向 UIResponder 发送消息。每次你点击按钮、与滑块交互或输入文本字段时,第一响应者会提供关于你当时正在与之交互的信息。因此,如果你点击按钮,响应者会立即知道你正在点击按钮。
退出用于当你想要将用户发送到不同的 ViewController 时。这可以是用户从其中来到当前场景的先前 ViewController,或者可以是其他 ViewController。在这个阶段,只要你能理解 ViewController 是什么,这并不那么重要。
我们已经看到ViewController.swift是通过.storyboard文件中的ViewController对象被调用的。在ViewController.swift文件中,有两个函数:一个是ViewDidLoad,另一个是didReceiveMemoryWarning。在.storyboard文件中,我们看到ViewController包含视图。当视图被创建时,ViewDidLoad函数会被调用。这意味着 ViewController 可以显示视图中的任何内容,并且它准备好应对用户可能对视图进行的任何交互。
当这种情况发生时,didReceiveMemoryWarning函数会被调用。这个函数会在你有太多应用程序打开在设备上,或者如果你的程序没有释放它之前创建的对象时被调用。后者不应该有问题,因为 Swift 有自己的垃圾回收,所以你只需要担心释放内存。
Images.xcasstes将包含项目的所有图像资源。到目前为止,里面唯一的文件是AppIcon集合。在这个文件中,显示了不同 iOS 版本和分辨率的图标图像,并带有占位符。
这里是创建的游戏pizZapMania的图标集的示例。每个集合下面提到的值是基本尺寸。左上角第一个图标的基线是 29 点,因此为了创建 iOS 5 和 6 的图标,我们需要两个图标:一个1x,意味着 29 x 29 像素,另一个是这个尺寸的两倍。类似地,还需要为特定设备提到的所有基值和乘数创建图标。

当查看项目根的常规选项卡时,我们发现LaunchScreen.xib文件在应用启动时被调用。那么这个 XIB 文件是什么?

XIB 文件被称为 NIB 文件(就像.xib之前是.nib一样)或 NeXTSTEP 界面构建器。到目前为止,这个文件是基于 XML 的,n被替换为x,但它仍然被称为 NIB 文件。所以,当有人提到 NIB 文件时,他们指的是 XIB 文件。根据 Apple 的说法,故事板是前进的方向,因为它们支持多个视图控制器,而.xib文件只能有一个。由于在这种情况下,应用程序和启动屏幕都使用单个视图控制器,所以它们是相同的。
在创建项目时,Launchscreen.xib文件默认添加了两个标签。第一个标签是应用名称,放置在视图的中心,另一个放置在视图底部以显示版权信息。点击任意文本,可以看到实用工具面板像圣诞树一样亮起。让我们最后看看实用工具面板。
实用工具面板(Redux)
实用工具面板的上半部分包含六个检查器选项卡,下半部分有四个库选项卡。让我们首先逐一查看检查器选项卡。
检查器选项卡
让我们从最左边的选项卡开始,即文件检查器选项卡,然后我们将查看快速帮助检查器、身份检查器、属性检查器、大小检查器和连接检查器选项卡。需要记住的主要选项卡是属性检查器。我们只需快速浏览其他选项卡。
文件检查器选项卡
文件检查器选项卡提供了关于当前对象所附加的父文件的信息。在这里,标签附加到LaunchScreen.xib文件上,位于 Mac 上的Base.lproj目录中。

它显示了可以打开它的 Xcode 版本、构建目标版本以及文档支持的 iOS 版本。它还提供了有关源控制的详细信息,例如仓库名称、类型和分支。
快速帮助检查器选项卡
快速帮助检查器选项卡提供了所选项目的简要描述、功能和能力。在这里,由于我们选择了标签,它显示了标签的父类,即UILabel。它还提供了其他详细信息,例如可用性,它告诉我们哪些 iOS 版本支持此功能。

身份检查器选项卡
身份检查器选项卡有助于为任何对象分配和管理元数据,在这种情况下将是选中的文本标签。它还显示了附加到其上的自定义类。在这里,它期望一个继承自UILabel类的类。

属性检查器选项卡
这就是您可以编辑所选对象属性的地方。因此,在这里,我们可以更改外观、颜色、位置、搜索和文本——就像我们可以在一个 Word 文档的页面中做的那样。

大小检查器选项卡
大小检查器选项卡可以帮助您定位对象,设置对象的大小,并添加约束以进一步帮助正确定位对象。

连接检查器选项卡
如前所述,我们可以连接 ViewControllers 以创建屏幕流程,并从 ViewController 切换到下一个。在连接检查器选项卡中,我们可以看到哪些对象连接到了哪些出口。

库
该库包含四个选项卡:文件模板、代码片段、对象和媒体库。
文件模板选项卡
如果您想创建一个新文件,您可以通过导航到文件 | 新建,或者简单地将所需的文件类型拖放到项目导航器中。

在这里,如果我们想的话,可以将Swift 文件模板拖入我们的项目,一旦我们释放鼠标左键,Xcode 将要求我们保存文件。然后我们可以重命名文件,并且它将被包含在项目中。
代码片段库选项卡
这些包含可以拖放到文件中以避免重复工作的代码块集合。

对象库选项卡
在对象库中,您有预定义的对象,如文本、标签和按钮,可以将它们拖放到 ViewController 中。

媒体库选项卡
此选项卡包含您添加到库中并拖放到应用程序中的所有媒体,如电影、音频和图像。目前尚未添加任何媒体,因此此选项卡为空。

这就完成了关于实用工具面板的部分。现在我们已经涵盖了其他文件并查看了实用工具面板,让我们看看项目层次结构中的最后一个文件夹。
Supported Files文件夹/组中只包含一个info.plist文件,它包含有关应用程序的所有必要信息。它将包含有关应用程序名称、捆绑名称、应用程序版本号以及其他基本信息的详细信息,并将传递给 Xcode。大多数情况下,我们只有在将已上架的应用程序更新上传到商店时才会修改此文件,并且我们希望最新构建的版本号为 1.1 而不是 1.0。我们将在最后一章中发布应用程序到 App Store 时介绍这一点。
调试面板
还有另一个我们还没有介绍的面板,那就是调试面板。可以通过点击工具栏右侧、实用工具面板左侧的三个按钮中间的按钮来激活它。一旦点击,就会打开以下截图所示的面板。您可以再次点击它来隐藏它。

调试面板或区域有两个子部分。左侧是变量视图,右侧是控制台。您使用println函数记录的任何内容都会在这里显示。例如,在 ViewController 类的viewDidLoad函数中,我记录了以下内容:
println(" The View Is Loaded Now !!!")
当我运行应用程序时,控制台弹出了消息。所以,类似于我们在 playground 中记录信息的方式,我们可以在 Xcode 中这样做,但为了看到它,显然您需要运行应用程序——与 playground 不同。
现在我们有了这个很棒的应用,我们可以看看如何在设备上运行它。
在设备上运行应用
所有这些时间,我们都在模拟器上运行应用。在模拟器上运行应用相对容易——选择要运行应用的模拟器,点击播放按钮,就这样!然而,要在设备上运行应用,您需要执行几个步骤。
首先,我们需要获取开发者证书并安装它。因此,前往iOS 开发者门户,点击会员中心,并输入我们在第一章中创建的登录名和密码,入门:
-
在开发者计划资源下,点击证书、标识符和配置文件。
![在设备上运行应用]()
-
在iOS 应用下,点击证书。
![在设备上运行应用]()
-
在证书下,点击开发。在右上角,点击搜索按钮旁边的+号。
![在设备上运行应用]()
-
然后,页面加载完成后,点击iOS 开发,然后点击继续。然后,我们必须创建一个证书签名请求。
![在设备上运行应用]()
-
在您的 Mac 上,通过点击启动台打开密钥链。一旦打开密钥链应用,前往证书助手并点击从证书颁发机构请求证书...。
![在设备上运行应用]()
-
在弹出窗口中,输入您用于创建 Apple 开发者 ID 的电子邮件地址。然后输入一个常见的名称作为参考,选择保存到磁盘,然后点击继续以选择您希望证书下载的位置。您现在可以将其保存在桌面上,但稍后将其移动到更安全的地方。
![在设备上运行应用]()
-
返回 iOS 开发者门户并点击继续。现在,你必须上传你在上一步中保存在 Mac 上的 CSR 文件。点击选择文件,导航到下载文件的桌面,选择它,然后点击生成。
-
现在你的证书已经准备好了。点击下载以下载文件。
-
双击下载的文件进行安装。然后,点击完成。
-
现在,打开 Xcode,转到偏好设置,点击账户,然后点击查看详情。在顶部部分,应该显示 iOS 开发,这意味着证书已安装。
此证书仅用于开发。对于分发,你将需要下载一个分发证书,我们将在准备发布游戏时进行。
-
现在,将你的设备连接到 Mac。Xcode 将自动假设你将使用连接的设备进行开发并为你注册。你可以看到所有注册的设备。打开 Xcode,从顶部栏选择窗口,然后选择设备以显示所有注册的设备。你可以注册多达 100 台设备以测试你的应用程序。在设备上运行应用
-
现在,在播放按钮旁边的左上角,从我们之前用来选择模拟器的位置,我们可以选择设备。在设备上运行应用
-
最后一步:我们还需要选择团队以使应用在设备上运行。因此,转到项目导航器标签并选择项目根目录。在编辑器中,在团队部分向下滚动以选择团队。在设备上运行应用
现在,如果一切顺利,你应该能够在设备上运行应用。恭喜!!!!
摘要
在本章中,我们探讨了 Xcode 的界面。我希望你现在对不同的面板以及如何访问、激活和停用它们有了很好的了解。我故意没有涵盖文件、编辑和查看菜单,因为这些与其他应用程序有些相似。如果需要,我们将根据需要了解一些内容。
作为练习,你可以尝试更改视图的颜色,或在.storyboard文件中更改高度并输入。你还可以在 NIB 文件中对应用进行类似更改,使其更加多彩。
现在你已经了解了 Xcode 的工作原理,我们将直接进入使用 SpriteKit 进行游戏开发。一旦我们掌握了二维空间,我们将在 SceneKit 中查看三维空间。
第四章. SpriteKit 基础
在一整章的理论之后,我们终于到达了将创建游戏的一章。我相信这是你一直期待的时刻,你的手指都渴望编写一些代码并制作一个游戏。
在本章中,你将使用 SpriteKit 创建一个小型且基本的游戏。我们将看到如何创建游戏的主菜单,你将学习如何从主菜单场景切换到游戏玩法场景,所有游戏玩法代码都将在此场景中编写。
在游戏玩法场景中,我们首先添加精灵,如背景和英雄。然后,我们将创建一个小的物理引擎来让英雄移动。接着,我们将添加敌人,并移动它们。接下来,我们将让英雄和敌人互相射击。我们将检测英雄的火箭与敌人之间的碰撞,以及敌人的子弹与英雄之间的碰撞。对于英雄射出的每一个敌人,我们将得到一分,但如果任何敌人穿过屏幕的左侧,游戏将结束。如果当前最高分高于之前保存的分数,那么你的当前分数将被保存为新的最高分。一旦游戏结束,玩家可以点击按钮返回主菜单以开始游戏。我希望你很兴奋!让我们最终跳进去。
本章涵盖的主题如下:
-
SpriteKit 和 SKScene 简介
-
添加主菜单场景和游戏玩法场景
-
添加并移动英雄精灵
-
通过触摸创建交互性
-
一个简单的物理引擎
-
生成敌人
-
发射英雄火箭和敌人子弹
-
碰撞检测
-
得分和游戏结束条件
-
显示、保存和检索分数
SpriteKit 和 SKScene 简介
我们已经在 第一章,入门 中看到,如何创建 SpriteKit 项目。为了唤醒你的记忆,我将再次向你展示如何创建项目。点击 Xcode,然后点击 创建一个新的 Xcode 项目。然后,在左侧面板下,导航到 iOS,然后到 应用程序 并选择 游戏。然后点击 下一步。为新项目命名。选择语言为 Swift,游戏技术为 SpriteKit,设备为 iPad,然后点击 下一步。选择你想要创建项目文件夹的位置,然后点击 创建。
你会看到,项目结构的大部分与我们在上一章中看到的 SingleView 项目相似。我们有 GameScene.sks、GameScene.swift 和 GameViewController.swift 文件:
-
GameScene.sks: 这是一个序列化的SpriteKitScene文件。这个文件用于在无需编写代码的情况下可视地创建 SKScenes。例如,你可以拖放图片并将它们设计成按钮,点击它们时,你可以让它们执行不同的功能。但由于我们将全部使用代码编写,我们不会使用这个文件来创建游戏的界面。 -
GameScene.swift:这个类继承自 SKScene。SKScenes 是游戏的构建块。这个类在应用程序视图加载后调用。你可以创建 SKScene 文件来创建主菜单场景、游戏玩法场景、选项场景等等。实际上,我们稍后会重命名GameScene.swift文件为MainMenuScene.swift,并创建一个新的场景GamePlayScene,我们将在这里编写我们的游戏玩法代码。![SpriteKit 和 SKScene 的介绍]()
-
GameViewController.swift:这个类与我们之前章节中看到的ViewController.swift文件类似。在Main.Storyboard文件中,你会看到有一个GameViewControllerScene文件而不是ViewController,但结构非常相似。如果你点击GameViewController,你可以在Identity检查器的Utility面板下看到它调用了GameViewController类。参考前面的图示。
现在,打开GameViewController.swift文件。你会看到一些新函数和一些我们在ViewController.swift中看到的旧函数。你还会注意到已经导入了 SpriteKit。我们需要在所有想要使用其功能的类中导入 SpriteKit。所有属于 SpriteKit 的类和对象都以前缀SK开头,所以 SpriteKit 场景是SKScene,精灵是SKSpriteNode等等。
SpriteKitNode或SKNode是创建 SpriteKit 中任何内容所需的基本构建块,但与SKScene或SKSpriteNode不同,它不绘制任何可视内容。然而,SKScene和SKSpriteNode都是SKNode的子类。因此,如果SKScene是任何游戏的构建块,那么SKNode就是 SpriteKit 本身的基本构建块。详细的解释可以在第一章的 SpriteKit 部分找到,入门。
导入 SpriteKit 后,我们看到创建了一个名为unarchivedFromFile的类函数的 SKNode 扩展,它接受一个字符串并返回一个 SKNode。以下函数用于加载我们之前看到的.sks文件:
extension SKNode {
class func unarchiveFromFile(file : NSString) -> SKNode? {
if let path = NSBundle.mainBundle().pathForResource(file, ofType: "sks") {
var sceneData = NSData(contentsOfFile: path, options: .DataReadingMappedIfSafe, error: nil)!
var archiver = NSKeyedUnarchiver(forReadingWithData: sceneData)
archiver.setClass(self.classForKeyedUnarchiver(), forClassName: "SKScene")
let scene = archiver.decodeObjectForKey(NSKeyedArchiveRootObjectKey) as GameScene
archiver.finishDecoding()
return scene
} else {
return nil
}
}
}
扩展后,我们看到实际的GameViewController类。它仍然继承自UIViewController。类似于ViewController.swift文件,这里首先调用的函数是viewDidLoad,这是视图加载后立即调用的函数。调用了super.viewDidLoad函数,该函数调用父类的viewDidLoad。然后,使用之前创建的扩展加载了GameScene.sks文件。if let语句检查对象场景是否为空。如果不为空,则执行if块中的代码。
注意
extension:在 Swift 中,你可以向现有类添加功能。在前面的例子中,我们向 SKNode 类添加了一个名为 unarchivedFromFile 的新函数,该函数解档文件并返回一个 SKScene。这个函数用于在以下代码的 viewDidLoad 函数中解档 SKS 文件。
if let:这检查对象场景是否为空。如果不为空,则执行 if 块中的代码。
as:这个运算符用于向下转换 SKView,因为它实际上是 UIView 的子类。
override func viewDidLoad() {
super.viewDidLoad()
if let scene = GameScene.unarchiveFromFile("GameScene") as? GameScene {
// Configure the view.
let skView = self.view as SKView
skView.showsFPS = true
skView.showsNodeCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .AspectFill
skView.presentScene(scene)
}
}
创建了一个新变量 skView,并将当前视图通过类型转换赋值给它,因为 GameViewController 的根视图是一个 SKView。
然后,场景的 showsFPS 和 showsNodeCount 属性被设置为 true,这将显示屏幕右下角的 FPS 和 节点计数。
将 ignoreSiblingOrder 属性设置为 true,意味着如果一个或多个对象处于相同的深度,则不会在它们之间进行优先级排序,所有对象将以相同的深度绘制。
Z 坐标,或深度顺序的值决定了哪个对象在屏幕的前面,哪个对象在后面。具有最小 Z 值的对象保持在屏幕后面,而具有最高值的对象是最接近屏幕的。如果一个对象没有分配 Z 坐标值,SpriteKit 将假设该对象位于之前添加的对象之上。这就是为什么在所有游戏中,背景总是首先添加,以便它位于最低的 Z 顺序,然后添加其他对象,如英雄。如果你首先添加英雄然后添加背景,英雄将位于最低的 Z 顺序,而覆盖整个屏幕的背景图像将位于其上方。你可能认为代码或 SpriteKit 出了问题,因为英雄没有显示,只有背景。事实是英雄确实在那里,但他位于背景之后。所以,要注意 Z 顺序,因为这可能导致游戏中的错误或意外结果。
设置顺序后,我们可以设置场景的 scaleMode 属性。在这里,默认情况下它已被设置为 AspectFill。有四种模式:AspectFill、Fill、AspectFit 和 ResizeFill。
-
AspectFill:这是创建新项目时的默认模式。在这个比例中,计算了 x 和 y 缩放因子,并选择较大的缩放因子来填充视图并保持图像的宽高比。这将导致场景裁剪。让我们创建一个项目,并将角色放置在右上角和左下角,观察当我们从横屏模式切换到竖屏模式时会发生什么。
在横屏模式下,两个角色都按照应有的方式显示。一个位于屏幕左下角,另一个位于右上角,如下面的截图所示:
![SpriteKit 和 SKScene 简介]()
但在纵向模式下,它们已经超出了屏幕范围,如下面的截图所示:
![SpriteKit 和 SKScene 简介]()
-
Fill: 无论是x轴还是y轴,都会缩放以填充视图。视图是在你点击Main.Storyboard文件中的视图后显示的区域。图像的宽度和高度都会改变,以填充视图。如果我们在横屏模式下再次使用
.Fill进行相同的测试,图像看起来似乎是正常的,但在纵向模式下,两个图像分别位于它们的位置,但被压扁以适应,如下面的截图所示:![SpriteKit 和 SKScene 简介]()
-
AspectFit: 不是选择上缩放因子,而是选择下缩放因子以保持场景的宽高比。这可能会导致场景出现信封式显示,但场景中的所有内容都将显示,并且将在视图中可见。使用这种模式,在横屏模式下,一切看起来都很正常,但在纵向模式下,图像根本不是正方形的;整个场景被缩小以适应屏幕宽度。这将在屏幕的顶部和底部造成信封式显示,如下面的截图所示:
![SpriteKit 和 SKScene 简介]()
-
ResizeFill: 场景不会进行任何缩放。它只是调整大小以适应视图。图像将保持原始大小和宽高比。这里,由于宽高比和缩放保持不变,左下角的图像显示在正确的位置,但右上角的图像超出了屏幕范围,如下面的截图所示:
![SpriteKit 和 SKScene 简介]()
观察前面四种模式的截图,我们可以看到并非所有尺寸都适合。你需要调整缩放模式以最好地满足你游戏的需求。由于我们的游戏主要是为横屏模式设计的,所以我们只禁用纵向模式。因此,在主项目节点中,通过在通用选项卡中取消选中来禁用纵向模式和颠倒,如下面的截图所示:

我们还将使用ResizeFill,因为我们将为设备的 Retina 和非 Retina 资产提供单独的图像,这样就不会影响宽高比,从而得到完整的全屏图像,而不是裁剪或缩放的图像。因此,在GameViewController类中,将缩放模式更改为以下内容:
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .ResizeFill
最后,使用skView对象的presentScene函数加载并呈现场景。
当GameScene.swift文件呈现时,会调用didMoveToView函数,正如我们在第一章中看到的,入门,显示显示Hello, World!文本的SKLabelNode标签,并且每次点击屏幕时,都会调用touchesBegan函数,并在触摸位置创建一个SKSpriteNode并对其运行动作。
有三个新函数:shouldAutoRotate函数,设置为true,当设备旋转时将旋转视图;supportedInterfaceOrientation函数,检查方向并相应地对齐视图;以及prefersStatusBarHidden函数,隐藏状态栏元素,例如屏幕顶部的网络和电池指示器。您可以根据需要启用或禁用它们。
现在我们将更改GameScene以开始制作我们的游戏。
添加主菜单场景
让我们对GameScene类进行一些修改,使其成为我们的主菜单场景:
-
通过在项目导航器中选择项目层次结构中的文件来将文件重命名为
MainMenuScene.swift。 -
将文件中的类名更改为
MainMenuScene。 -
删除
didMoveToView中的所有代码行。 -
在
touchesBegan函数中,删除与添加精灵及其上运行动作相关的代码。 -
删除
update函数,因为它对于主菜单场景不是必需的。如果需要,我们将在稍后添加它。
MainMenuScene.swift文件应如下代码片段所示,因为我们已从didMoveToView函数中删除所有代码并修改了touchedBegan函数:
import SpriteKit
class MainMenuScene: SKScene {
override func didMoveToView(view: SKView) {
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
}
}
}
通过将其移动到回收站来从项目层次结构中删除GameScene.sks文件。
我们还需要对GameViewController.swift文件进行一些修改。
-
删除为 SKNode 创建的扩展。
-
删除
if let场景行以及开闭括号,因为我们将通过代码直接调用MainMenuScene类。 -
将上述行替换为
let scene = MainMenuScene(size: view.bounds.size)。SKScene 构造函数接受屏幕大小,因此我们在这里从视图的bounds.size属性中获取它。 -
将
.AspectFill更改为.ResizeFill。
文件的其他部分可以保持不变。现在viewDidLoad函数应如下所示:
override func viewDidLoad() {
super.viewDidLoad()
let scene = MainMenuScene(size: view.bounds.size)
// Configure the view.
let skView = self.view as SKView
skView.showsFPS = true
skView.showsNodeCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .ResizeFill
skView.presentScene(scene)
}
让我们开始向主菜单场景添加内容。
在MainMenuScene.swift文件中的didMoveToView函数中,我们首先添加背景图片,然后添加一个标签,用于显示游戏名称,最后添加播放按钮,点击后将启动GamePlayScene并开始游戏。
要添加背景图片,请添加以下代码:
let BG = SKSpriteNode(imageNamed: "BG")
BG.position = CGPoint(x: viewSize.width/2, y: viewSize.height/2)
self.addChild(BG)
我们创建了一个名为BG的常量变量,并将其分配给一个名为BG的图像集。然后,我们定位这个图像。为了定位图像,我们需要视图的大小。获取视图的宽度和高度非常简单。我们创建了一个名为viewSize的新常量,其类型为CGView,并将其分配给view.bounds.size。因此,在didMoveToView函数的开始处添加以下代码:
let viewSize:CGSize = view.bounds.size
我们现在可以设置BG的位置。要设置位置,我们将BG.position设置为视图宽度和高度的一半。每次我们需要分配或创建一个新的CGPoint变量时,我们必须调用 CGPoint,并在括号内提供用逗号分隔的x和y值。x值需要以x开头,然后是一个冒号,同样,y值需要以y开头,然后是一个冒号。
为了使背景显示出来,我们将在 self 上调用addChild函数,并传入创建的背景。
如果您现在运行游戏,它将产生错误,因为我们还没有为项目分配实际的图像。为此,转到本章的Resources文件夹,并将所有资产复制到桌面。这将包含本章将使用的所有资产。
现在,转到项目导航器中的Images.xcassets文件,右键单击面板并选择New Image Set,如下面的截图所示。将创建一个名为Image的新文件。选择并重命名它为BG。当我们创建背景精灵时调用BG,我们实际上是在引用这个文件。因此,如果您命名错误,代码将产生错误。

文件中有1x、2x和3x图像的占位符。由于我们正在为 iPad 制作游戏,我们只需要担心两种分辨率;1024 x 768 和 2048 x 1536 分辨率。我们的背景也是这两种分辨率之一。在Resources文件夹中,查找名为Bg.png和Bg2.png的图像文件。将Bg.png拖到1x框中,将Bg2.png拖到2x框中,如下面的截图所示:

现在您可以运行应用程序,屏幕将显示背景图像的全貌。在模拟器中,您可以选择iPad2或iPadAir,您将看到图像将填充整个屏幕,如下面的截图所示。请确保您正在横屏模式下运行。

接下来我们将添加标签以显示游戏名称。标签用于在屏幕上显示文本。在我们将背景添加到场景之后,添加以下代码以显示标签:
let myLabel = SKLabelNode(fontNamed:"Chalkduster")
myLabel.text = "Ms.TinyBazooka"
myLabel.fontSize = 65
myLabel.position = CGPoint(x: viewSize.width/2, y: viewSize.height * 0.8)
self.addChild(myLabel)
我们创建一个新的常量myLabel并调用SKLabelNode的构造函数。它需要我们想要用来创建文本的字体名称,所以我们传递Chalkduster,这是 Mac 中默认字体之一。在myLabel.text中,我们传递我们想要显示的实际文本。接下来我们分配字体的大小、位置,并将其作为子节点添加到当前类中。要创建文本,我们不需要创建图像集,但我们必须在系统中拥有该字体,因为它会自动从系统的字体目录中获取。
让我们接下来创建播放按钮。在Resources文件夹中,你可以找到playBtn.png和playBtn2.png。类似于我们为BG创建图像集的方式,通过命名文件为playBtn来为播放按钮创建一个图像集。将playBtn.png图像拖到1x,将playBtn2.png图像拖到2x。
对于Resources文件夹中的所有资产,你将找到每个文件的两个副本,一个以文件名开头,另一个以文件名结尾的2。因此,确保从现在起,常规文件名的资产分配给1x,以2结尾的文件分配给2x。
现在,适当的图像已经分配给了playBtn图像集。在添加标签代码的下方添加以下代码:
let playBtn = SKSpriteNode(imageNamed: "playBtn")
playBtn.position = CGPoint(x: viewSize.width/2, y: viewSize.height/2)
self.addChild(playBtn)
playBtn.name = "playBtn"
playBtn图像集也是一个常规的SKSpriteNode,所以类似于我们添加BG的方式,我们将playBtn图像集分配给playBtn常量。将其放置在视图的中心,然后将其添加到视图中。
除了我们通常做的事情之外,我还为playBtn常量分配了一个名称,以便在需要时可以引用它。不一定非得分配一个字符串;如果你愿意,甚至可以分配一个整数值。应该给它起一个容易记住并且能够与常量相关联的名字。
现在,如果你构建并运行项目,它应该看起来像以下截图:

接下来,我们将在touchesbegan函数中添加代码以检查是否按下了播放按钮。在touchesbegan函数中,我们首先检查屏幕上是否被触摸了任何对象,然后,如果被触摸,我们获取触摸的位置。获取位置后,我们添加以下代码:
let _node:SKNode = self.nodeAtPoint(location)
if(_node.name == "playBtn"){
let scene = GamePlayScene(size: self.size)
self.view?.presentScene(scene)
}
我们创建一个新的常量_node,其类型为SKNode,并获取触摸位置处的节点。然后我们检查被按下的节点名称是否为playBtn,如果是,则创建一个名为scene的常量,并将其分配给GameplayScene,然后像在GameViewController类中展示MainMenuScene一样展示场景。由于我们尚未创建GamePlayScene,你将得到一个错误。别担心,我们将在下一节中创建它。
self.view后面的问号检查视图是否为空。如果为空,它将给出错误,但由于视图存在,它不会给出错误。让我们创建游戏场景,这样我们就不会得到说它不存在的错误。
添加游戏场景
在此期间,我们一直在修改基础项目中已包含的文件。现在我们将在项目中创建一个新文件。在基础项目文件夹上右键单击,然后单击新建文件:

在左侧面板中,选择iOS,选择 Swift 文件,然后单击下一步。它将要求输入文件名,命名为GamePlayScene,然后单击创建。这将创建一个空的 Swift 文件。
在其中添加以下代码。这是每次创建新的场景文件时所需的基本结构:
import SpriteKit
class GamePlayScene: SKScene {
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
} // required init
override init(size: CGSize){
super.init(size: size)
} //init function
} //class end
我们首先导入 SpriteKit,然后使用class关键字创建类,并使用类的名称继承自SKScene。
然后我们有required init函数。由于超类SKScene实现了它,所以它必须包含在所有子类中。这是一个要求,所以无法避免,但我们不会用它做任何事情,因为我们将使用常规的init函数。
SKScene 的常规init函数接受视图的大小。然后我们必须确保调用super.init函数,并在其中传递视图的大小。
就这样,我们就准备好在这个类中添加一些游戏代码了。你可以在MainMenuScene.swift文件中检查是否有错误,并且代码正在正确构建。
在GamePlayScene.swift文件中,首先我们必须为viewSize创建一个全局变量。因此,在类和必需的init函数之间,添加let viewSize:CGSize!以使viewSize成为全局变量。此外,我们使用let而不是var,因为我们知道在游戏过程中视图的大小不会改变。
由于我们在这里没有初始化这个常量,我们必须在末尾使用感叹号来告诉 Swift 我们将初始化它,并且我们知道我们将初始化的类型将是CGSize。
将viewSize初始化为通过init函数传入的大小。在调用super.init之后添加以下行:
viewSize = size
添加背景和英雄
我们将首先添加背景,因此我们可以将MainMenuScene中的相同代码复制并粘贴到初始化viewSize之后的init函数中:
let BG = SKSpriteNode(imageNamed: "BG")
BG.position = CGPoint(x: viewSize.width/2, y: viewSize.height/2)
self.addChild(BG)
接下来我们将添加英雄精灵。类似于我们创建BG图像资源的方式,创建一个新的资源名为hero,并将hero.png和hero2.png分配给1x和2x槽位。
接下来,我们希望英雄也是一个全局变量,因为我们将在init函数之外引用她。所以,在创建viewSize属性之后,在类的顶部添加以下代码行:
let hero:SKSpriteNode!
接下来,在添加了BG之后的init函数中,添加以下代码:
hero = SKSpriteNode(imageNamed: "hero")
hero.position = CGPoint(x: viewSize.width/4, y: viewSize.height/2)
self.addChild(hero)
在这里,像往常一样,我们将图像集hero分配给常量,分配位置,并将其添加到场景中。
与我们定位背景的方式类似,我们定位英雄,但不是将英雄添加到屏幕中心,而是将其放置在屏幕左侧距离viewSize的四分之一处。
更新英雄的位置
接下来,让我们更新英雄的位置。让我们在场景中添加重力,这样游戏开始后她就会开始下落。为了更新她的位置,我们将使用update函数。update函数在类创建时立即被调用,并且每秒被调用 60 次。因此,将update函数添加到类中,如下所示。在它里面调用一个updateHero()函数,我们将在稍后定义这个函数:
override func update(currentTime: CFTimeInterval) {
updateHero()
}
在let hero行之后创建一个新的全局常量,称为gravity,类型为CGPoint,并将其初始化为x值为0和y值为-1,因为重力只影响负y方向:
let gravity = CGPoint(x:0.0, y: -1.0)
我们还将创建一个新的函数,称为updateHero,我们将在这个函数中编写所有更新英雄位置的代码。在update函数下创建这个函数,并且不要忘记在update函数中调用这个函数,否则英雄的位置将不会更新。
func updateHero(){
hero.position.y += gravity.y
}
在updateHero函数中,我们在每次更新中递减英雄的y位置。最终,她将穿过屏幕底部。为了让她保持在边界内,我们检查她是否即将超出屏幕,并将她放回屏幕底部的边缘。为此,在heroUpdate函数中递减她的位置下方添加以下代码:
if(hero.position.y - hero.size.height/2 <= 0){
hero.position.y = hero.size.height/2
}else if (hero.position.y + hero.size.height/2 >= viewSize.height){
hero.position.y = viewSize.height - hero.size.height/2
}
在第一个if块中,我们检查英雄的底部是否已经超过了屏幕的底部。如果是这样,那么我们将英雄的原点放置在屏幕底部距离她高度的一半处。
在else if块中,我们检查英雄的顶部是否已经超过了屏幕的顶部。如果是这样,那么我们将她放置在屏幕顶部距离她高度的一半处。
现在,如果你构建并运行游戏并按下播放,英雄将位于屏幕左侧距离的四分之一处,并且当她到达屏幕底部时将停止。

添加玩家控制
我们现在将通过使用touchesbegan函数来添加玩家控制。如果玩家点击屏幕的左侧,英雄将被推向上方,然后由于重力作用将再次开始下落,如果玩家点击屏幕的右侧,英雄将发射火箭。
为了检测触摸,在update函数下添加touchesBegan函数,如下所示:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
}
}//touchesBegan
这显然是我们之前在MainMenuScene中用来检测播放按钮触摸的相同函数。由于我们只是将要检查触摸的位置,所以我们现在不需要我们触摸的对象。
为了检测屏幕被点击的哪一侧,在for in循环中获取触摸位置的地方添加以下代码:
if(location.x < viewSize.width/2){
println("[GamePlayScene] touchedLeftSide ")
}else if(location.x > viewSize.width/2){
println("[GamePlayScene] touchedRightSide ")
}
我们检查触摸位置的x值是否小于屏幕宽度的一半。如果是这样,那么我们将打印出屏幕的左侧被触摸了;否则,我们检查触摸位置的x值是否大于屏幕宽度的一半,在这种情况下,我们可以确认屏幕的右侧被触摸了。
现在,为了将英雄推向空中,每当玩家触摸屏幕的左侧时,我们给她一个小的推力。添加一个全局变量thrust,其类型为CGPoint,并将x和y的值初始化为零,如下所示:
var thrust = CGPointZero
注意,我们使用var而不是let,因为thrust的值将在一段时间内发生变化。此外,CGPointZero只是CGPoint(x:0, y:0)的简写。它们都将做同样的事情,所以这只是方便和偏好的问题。
在touchesBegan函数中,在我们检查屏幕左侧被触摸后,立即添加以下行:
thrust.y = 15.0
在updateHero函数中,将hero.position.y += gravity.y这一行改为以下内容:
thrust.y += gravity.y
hero.position.y += thrust.y
println("Thrust Y Value: \(thrust.y)")
现在,每当触摸屏幕的左侧时,英雄将向上推 15 点,然后当她达到最高位置后,她将开始下落。按照以下方式记录thrust.y的值,以查看其工作原理:
[GamePlayScene] touchedLeftSide
Thrust Y Value: 14.0
Thrust Y Value: 13.0
Thrust Y Value: 12.0
Thrust Y Value: 11.0
Thrust Y Value: 10.0
一旦屏幕被轻触,thrust的y值,最初设置为0,将被设置为14。它不是15,因为我们由于重力而从中减去了1。然后,在每次更新时,英雄的y位置会逐渐降低,直到变为零,重力将再次开始作用并开始将英雄向下拉。
你会注意到,当英雄在屏幕底部并且你施加向上的推力时,英雄不会立即开始向上移动。为什么这一点在thrust.y的控制台输出中也是可见的。由于重力被添加到thrust.y,其值变得非常大,15 点的小推力必须克服这个值才能使英雄再次向上移动。为了解决这个问题,我们必须在英雄触摸屏幕顶部或底部时将thrust的值重置为零。因此,在updateHero函数中,在检查她是否触摸了屏幕顶部或底部之后,在设置英雄位置的if和if else块之后添加以下行:
thrust.y = 0
接下来,我们将添加当屏幕右侧被轻触时发射的火箭。为此,我们将创建一个新的泛型类,以便我们可以在以后创建敌人和敌人子弹时使用。
由于我们创建了GamePlayScene.swift文件,因此创建一个名为MovingSprite的文件,并在该文件中添加以下代码:
import SpriteKit
class MovingSprite{
let _sprite: SKSpriteNode!
let _speed : CGPoint!
init(sprite: SKSpriteNode, speed: CGPoint){
_sprite = sprite
_speed = speed
}//init
func moveSprite(){
_sprite.position.x += _speed.x
}
}//class
在这个类中,我们导入 SpriteKit 并创建类的定义。我们创建了两个全局常量来保存我们将传递给构造函数的 SKSpriteNode 和 CGPoint 对象的引用。SKSpriteNode 将保存我们将传递的精灵,而 CGPoint 将保存我们想要精灵移动的速度。
在 init 函数中,我们将传递的对象分配给本地创建的对象。我们添加了一个名为 moveSprite 的额外函数。这个函数将使用分配给它的速度移动精灵。现在这个类就到这里为止。当我们添加敌人和子弹类时,我们将重新访问并修改这个类。
为了创建火箭,在 GamePlayScene 文件中创建一个名为 addRockets 的新函数。在其中,我们添加以下代码来创建火箭:
func addRockets(){
let rocketNode: SKSpriteNode = SKSpriteNode(imageNamed: "rocket")
rocketNode.position = CGPoint(x: hero.position.x + hero.size.width/2 + rocketNode.size.width/2,y: hero.position.y - rocketNode.size.height/2)
self.addChild(rocketNode)
let speed: CGPoint = CGPoint(x: 10.0, y: 0.0)
let rocket: MovingSprite = MovingSprite(sprite: rocketNode, speed: speed)
}
在 addRockets 函数中,我们首先创建一个名为 rocketNode 的常量,其类型为 SKSpriteNode,并从 imageset 中分配一个火箭。因此,创建一个新的图像集并命名为 rocket。在 Resources 文件夹中,你可以找到 rocket.png 和 rocket2.png,你可以分别将它们分配给文件的 1x 和 2x 位置。
接下来,我们设置火箭的位置。由于我们想让火箭看起来是从火箭筒中发射出来的,而不是在玩家的位置生成,所以我们将其放置在火箭筒的前端。因此,对于 x 位置,我们获取玩家的位置,然后加上玩家宽度的一半,并加上火箭本身宽度的一半。对于 y 位置,我们获取英雄的 y 位置,并从它减去火箭高度的一半。然后我们将它添加到场景的显示列表中。
接下来,我们创建一个名为 rocket 的对象,其类型为 MovingSprite,并分配我们想要移动精灵的速度,并将我们之前创建的 rocketNode 传递进去。为了分配速度,我们创建一个新的常量 speed,其类型为 CGPoint,并将 10 和 0 分别分配给 x 和 y 的值,这样每次我们调用类的 moveSprite 函数时,位置将根据提供的值在 x 方向上更新。
在 touchedBegan 函数中,我们检查屏幕的右侧是否被点击,添加 addRocket 函数以在每次点击屏幕的右侧时创建火箭:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
if(location.x < viewSize.width/2){
println("[GamePlayScene] touchedLeftSide ")
thrust.y = 15.0
}else if(location.x > viewSize.width/2){
println("[GamePlayScene] touchedRightSide ")
addRockets()
}
}
}//touchesBegan
现在,如果你构建并运行,然后点击屏幕的右侧,火箭将被创建,但它们没有移动。为了移动火箭,我们必须将我们创建的每个火箭添加到一个数组中,并且在每个火箭上调用 moveSprite 函数来实际移动精灵。
为了更新火箭的位置,我们首先需要创建一个数组来存储所有的火箭。这个数组需要是一个全局变量,这样我们就可以轻松地访问它。因此,在类开始处添加var thrust = CGPointZero的下面,紧接着添加以下代码行。我们创建一个名为rockets的数组来存储MovingSprite类型的对象,我们使用var,因为它是一个可变数组,这意味着我们将在游戏过程中向其中添加和删除对象。
var rockets:[MovingSprite] = []
接下来,在创建更新英雄函数的下面创建一个新的函数updateGameObjects,并添加以下代码:
func updateGameObjects(){
for(var i:Int = 0; i < rockets.count; i++){
rockets[i].moveSprite()
var sprite: SKSpriteNode = rockets[i]._sprite
if((sprite.position.x - sprite.size.width/2) >
viewSize.width){
sprite.removeFromParent()
rockets.removeAtIndex(i)
}
}
}
你可能正在想,为什么我们使用for循环而不是for in循环呢?嗯,这是因为一旦火箭飞出屏幕,我们就必须删除该对象,而在 Swift 中从数组中删除对象时,我们需要被删除对象的索引,而for in循环没有这个功能。
因此,我们创建一个从0开始的常规for循环,每次通过将索引增加 1 遍历rockets数组中的每个对象。我们调用数组中的第 i 个对象的moveSprite函数。接下来,为了方便起见,我们从索引中获取spritenode,这样我们就可以对其进行一些检查。我们检查火箭精灵的左侧边缘是否超出了屏幕的宽度,如果是这样,那么我们就从其父节点中删除该精灵,即GamePlayScene,因为这是我们将在addRockets函数中添加addChild的地方。然后,我们通过调用数组的removeAtIndex函数并传入当前索引来从火箭数组中删除当前索引的对象。
最后,我们还需要将对象添加到数组中以便删除它。因此,在addRocket函数中,在函数末尾添加rockets.append(rocket)以将火箭添加到rockets数组中:
func addRockets(){
let rocketNode: SKSpriteNode =
SKSpriteNode(imageNamed: "rocket")
rocketNode.position = CGPoint(
x: hero.position.x + hero.size.width/2 +
rocketNode.size.width/2,
y: hero.position.y - rocketNode.size.height/2)
self.addChild(rocketNode)
let speed: CGPoint = CGPoint(x: 10.0, y: 0.0)
let rocket: MovingSprite =
MovingSprite(sprite: rocketNode, speed: speed)
rockets.append(rocket)
}
最后,别忘了在调用updateHero函数的下面调用updateGameObjects函数。
现在,当你构建并运行游戏时,你将能够点击屏幕的左侧来提升玩家的速度,然后点击屏幕的右侧来发射火箭。
此外,请查看屏幕右下角的节点计数。每次创建一个新的火箭时,节点计数都会增加,由于火箭一旦飞出屏幕就会被删除,因此每次从场景中删除火箭时,计数也会减少。
你还可以在update函数中添加以下代码来记录rockets.count,以检查火箭数组中有多少个火箭:
println(" rockets count: \(rockets.count)")

添加敌人
要让英雄成为英雄,我们需要反派。因此,我们现在将添加敌人。类似于我们创建addRocket函数的方式,创建一个新的函数addEnemy。
此外,在GamePlayScene类的全局变量中rocket数组之后创建一个新的数组名为enemies。这添加了一个新的数组,将管理敌人,如下所示:
var enemies:[MovingSprite] = []
现在,在你的变量声明中应该有类似以下的内容:
let viewSize:CGSize!
let hero:SKSpriteNode!
let gravity = CGPoint(x:0.0, y: -1.0)
var thrust = CGPointZero
var rockets:[MovingSprite] = []
var enemies:[MovingSprite] = []
现在,我们可以更新所有敌人,更新它们的位置并在它们离开屏幕时移除它们。
创建一个新的图像集名为enemy,并将enemy.png和enemy2.png添加到文件中。
与在火箭筒喷嘴处生成的火箭不同,敌人将从屏幕右侧生成并向屏幕左侧移动。它们也会在屏幕的不同高度生成。如果所有敌人都是从同一位置生成的,这对玩家来说不会构成挑战。因此,我们将创建一个随机数,根据这个随机数我们将决定敌人将在什么高度生成。
按照以下方式创建addEnemy函数:
func addEnemy(){
var factor = arc4random_uniform(4) + 1
var fraction = CGFloat(factor) * 0.20
var height = fraction * viewSize.height
println("enemy height: \(factor), \(fraction), \(height)")
var enemyNode:SKSpriteNode = SKSpriteNode(imageNamed: "enemy")
enemyNode.position = CGPoint(x: viewSize.width + enemyNode.size.width/2, y: height)
self.addChild(enemyNode)
enemyNode.name = "enemy"
let speed: CGPoint = CGPoint(x: -5.0, y: 0.0)
var enemy:MovingSprite = MovingSprite(sprite: enemyNode, speed: speed)
enemies.append(enemy)
}
对于创建随机数,我们使用内置函数arc4random_uniform。这个函数接受一个值并生成一个从0到小于该值的随机数。所以,在这种情况下,因为我们传递了4,它将创建一个从0到3的数字。由于我们想要一个从1到4的随机数,我们在最后将其加1。我们将这个值分配给一个名为factor的变量。
然后,我们将这个变量转换为CGFloat类型,这样我们就可以得到一个分数值。然后,将这个值乘以 0.20 并存储在一个名为fraction的新变量中。为了最终得到敌人需要生成的随机高度,我们将分数乘以视图的高度,并将其分配给一个名为height的变量。
这样,敌人将在屏幕高度的 20%、40%、60%或 80%处生成。我们不能在 0%或 100%的高度生成敌人,因为那样的话,敌人的顶部或底部部分将不可见,因为精灵的锚点位于精灵的中心。
现在,类似于我们创建火箭的方式,我们创建一个新的名为enemyNode的SKSpriteNode类型,并将敌人图像集分配给它。我们必须将敌人放置在屏幕右侧之外,因此我们获取屏幕宽度并加上敌人宽度的一半。对于高度,我们给出敌人需要生成的随机高度,并将敌人精灵添加到场景中。最后,我们将enemyNode精灵命名为enemy,因为我们稍后会用到它。
接下来,由于我们需要创建MovingSprite类的实例,并提供enemySprite节点和速度,我们将创建一个新的speed对象。由于这次我们希望敌人向负的x方向移动,我们在x方向上提供-5的值作为速度,将y值保持为0,因为我们不希望敌人向y方向移动。然后,我们创建一个新的MovingSprite对象,命名为enemy,并向其提供enemySprite和speed。最后,我们将新创建的敌人对象追加到enemies数组中,这样我们就在顶部创建了一个名为enemies的数组,类似于我们为英雄创建火箭的方式。
现在我们必须更新数组中的敌人对象,并对敌人调用moveSprite使其向负的x方向移动。我们还需要确保从父类中移除敌人精灵,然后从enemies数组中移除敌人对象。为此,我们在updateGameObjects函数中更新玩家火箭的地方添加以下代码:
for(var i:Int = 0; i < enemies.count; i++){
enemies[i].moveSprite()
var sprite: SKSpriteNode = enemies[i]._sprite
if((sprite.position.x + sprite.size.width/2) < 0){
sprite.removeFromParent()
enemies.removeAtIndex(i)
}
}//update enemies
与我们更新英雄火箭的方式类似,我们创建一个for循环,然后对所有的对象调用moveSprite函数。为了方便起见,创建一个sprite节点。现在,我们不再检查对象是否从屏幕的右侧离开,因为敌人正在移动,向负的x方向,而是检查敌人的右边缘是否超出了屏幕的左侧,如果是这样,我们就从父节点中移除精灵,然后从enemies数组中的当前索引移除对象。
由于我们已经在update函数中调用了updateGameObjects,因此不需要再次添加。但是,我们应该每隔几秒钟调用一次addEnemy函数来生成敌人。
对于实际生成敌人,我们可以使用一个动作在想要持续的时间后调用addEnemy函数。为此,在init函数中,在将英雄添加到场景的地方添加以下内容:
//spawn enemies after delay
let callFunc = SKAction.runBlock(addEnemy)
let delay = SKAction.waitForDuration(3.0)
let sequence = SKAction.sequence([callFunc,delay])
let addEnemyActiom = SKAction.repeatActionForever(sequence)
self.runAction(addEnemyActiom)
首先,我们创建一些动作。所有动作都是SKAction类型。第一个动作是runBlock,我们提供想要调用的函数,即addEnemy。我们将此动作分配给名为callFunc的let。接下来,我们创建另一个名为waitDuration的动作,将其分配为3.0,即 3 秒,并将其分配给delay。第三个动作是一个sequence。sequence动作允许你依次执行动作。因此,在这里我们首先提供callFunc,然后是delay。方括号表示序列是一个数组,因此我们可以创建一个包含我们想要调用的任意多个动作的序列,通过将其添加到数组中,然后传递给序列。在这个序列中,callEnemy函数将首先被调用,然后动作将等待 3 秒。
最后的操作是repeatActionForever动作,在这里我们传入序列动作,以便序列被反复调用。
最后,我们在当前场景上运行动作,并提供addEnemyAction,这将最终调用addEnemy函数,直到我们告诉它停止运行动作。
现在构建并运行程序,可以看到敌人从屏幕右侧出现,更新状态,然后一旦他们离开场景,就会从场景中删除。

添加敌人子弹
好的。敌人似乎携带一种卡通风格的步枪,但他似乎没有用它做任何事情。让我们让他使用它。我们将让敌人射击子弹。
创建一个名为bullet的图像集,并将bullet.png和bullet2.png分别分配给Images.xcassets中的1x和2x槽位。此外,在GamePlayScene类的顶部创建一个名为bullets的新的全局变量,类型为array,如下所示:
var bullets:[MovingSprite] = []
对于生成子弹,我们将使用movingSprite类。但我们必须对其进行一些修改,以便敌人一被创建就开始用步枪射击。
因此,打开movingSprite类,并在初始化全局变量后立即在init函数中添加以下内容。记住,我们在创建敌人精灵时给它添加了一个名称。这将在以下内容中使用:
if(_sprite.name == "enemy"){
let shootActiom = SKAction.repeatActionForever(SKAction.sequence([SKAction.runBlock(shootBullet),SKAction.waitForDuration(3.0)]))
_sprite.runAction(shootActiom)
}
在这里,我们首先检查传入的精灵名称,如果是敌人,则创建一个类似于我们在GameplayScene类中创建敌人生成的动作。区别在于,我们在这里不是为每个动作创建一个单独的变量,而是创建一个名为shootAction的单个动作,并调用其中的所有动作。
因此,基本上,我们正在调用一个名为shootBullet的函数,我们将在同一个类中创建它,该函数将每 3 秒被调用一次。
在我们创建shootAction之后,我们调用精灵上的动作,以便它能够开始调用shootAction。
我们将定义shootBullet函数如下。这可以添加在我们添加moveSprite函数的下方。
func shootBullet(){
let _gameplayScene = _sprite.parent as GamePlayScene
_gameplayScene.addBullets(_sprite.position, size: _sprite.size)
}
这个函数反过来会调用GameplayScene中的addBullets函数。由于我们在游戏场景中添加了英雄、火箭和敌人,如果我们也将子弹添加到同一个场景中会更好,因为这样在检查碰撞时更容易遍历对象。
要获取GameplayScene的实例,我们将创建一个名为_gameplayScene的局部常量,并使用SKSpriteNode的.parent属性来获取精灵被添加到的父类。由于我们在GameplayScene中添加了敌人,它将返回GameplayScene。我们仍然需要将其类型转换为GamePlayScene,所以我们使用as运算符并将其类型转换为GamePlayScene。
现在,由于我们需要正确地定位子弹,就像我们为英雄定位火箭一样,我们需要在创建子弹时提供敌人对象的位置和大小。假设如此,我们将创建一个名为 addBullets 的函数,我们将通过提供敌人精灵的位置和大小来在 GamePlayScene 中调用此函数。
现在,让我们转到 GameplayScene 并创建一个名为 addBullets 的函数,如下所示:
func addBullets(pos:CGPoint, size: CGSize){
let bulletNode: SKSpriteNode = SKSpriteNode(imageNamed: " bullet")
var newPos = CGPoint(x: pos.x - size.width/2 -
bulletNode.size.width/2,
y: pos.y - bulletNode.size.height)
bulletNode.position = newPos
self.addChild(bulletNode)
let speed: CGPoint = CGPoint(x: -10.0, y: 0.0)
let bullet: MovingSprite = MovingSprite(sprite: bulletNode, speed: speed)
bullets.append(bullet)
}
你现在应该相当熟悉在 SpriteKit 中添加对象了。就像火箭一样,我们创建一个名为 bulletNode 的 SKSpriteNode,并分配子弹图像。你知道该怎么做。
然后,我们创建一个新的位置,它将在敌人精灵的左侧端点。因此,我们取敌人的当前位置,从 x 位置减去敌人宽度的一半和 bulletNode 的一半。对于 y 位置,我们从位置 y 位置减去子弹的全高。在下一步中,我们将此位置分配给 bulletNode 的位置,然后将其添加到当前场景中。
我们创建一个新的速度变量,并分配我们想要子弹移动的速度。我们创建一个新的常量 bullet,并将 bulletNode 和速度提供给它。
需要创建一个子弹数组来追加所有创建的子弹。因此,创建一个新的名为 bullets 的数组,它接受 MovingSprite 并将其添加到类的顶部。
子弹创建后,将其追加到 bullets 数组中。
我们还需要更新子弹的位置并检查,以便在它们离开屏幕后删除它们。因此,类似于更新敌人,我们需要在 updateGameObjects 函数中添加以下代码来更新子弹:
for(var i:Int = 0; i < bullets.count; i++){
bullets[i].moveSprite()
var sprite: SKSpriteNode = bullets[i]._sprite
if((sprite.position.x + sprite.size.width/2) < 0){
sprite.removeFromParent()
bullets.removeAtIndex(i)
}
}
因此,我们遍历数组中的子弹,调用 moveSprite 函数,如果精灵已经超出屏幕的左侧,我们就从父节点中移除它,并在当前索引中移除对象。

现在我们已经拥有了进入下一阶段开发所需的所有元素,即检查英雄火箭与敌人之间的碰撞,以及敌人子弹与英雄之间的碰撞。基于碰撞,我们将进行计分和检查游戏结束条件。
碰撞检测
在二维游戏中,碰撞检测是通过精灵类的 intersectsNode 函数来完成的。我们可以检查当前精灵是否与其他精灵重叠。在当前节点的 intersectsNode 函数中,我们传入一个节点,我们想要检查它与当前节点是否发生碰撞。如果有交集,函数将返回 true,如果没有,则返回 false。
对于碰撞检查,我们首先检查敌人子弹与英雄之间的碰撞。如果发生碰撞,则游戏结束。然后我们将检查英雄火箭与敌人之间的碰撞。如果我们检测到碰撞,则必须更新分数。
此外,如果敌人超出屏幕的左侧,游戏即结束,因此在这种情况下也会调用 GameOver 函数。
对于检查碰撞,在 GameplayScene 中创建一个新的函数 checkCollision 并添加以下代码:
//Hero and Bullets
for bullet in bullets{
var sprite = bullet._sprite
if(sprite.intersectsNode(hero)){
GameOver()
}
}
由于我们不需要知道索引号,我们将仅使用 for in 循环来检查碰撞。
在这里,我们遍历 bullets 数组中的所有子弹对象。首先,我们将子弹对象的精灵分配给一个局部精灵变量。然后,我们将对该精灵调用 intersectsNode 函数以检查它是否与英雄精灵相交。如果相交,则调用 GameOver 函数。
intersectsNode 函数仅接受节点的精灵,并检查围绕该精灵的矩形是否与提供的精灵的边界框相交。如果存在重叠,则如果发生碰撞,将返回 true,否则返回 false。
在更新英雄和游戏对象之后,立即在 update 函数中调用 checkCollision 函数,如下所示:
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
updateGameObjects()
updateHero()
checkCollision()
}
接下来,让我们创建 GameOver 函数。一旦游戏结束,我们希望停止更新英雄和游戏对象,并停止检查碰撞。
此外,在类的顶部创建一个全局布尔变量 gameOver 并将其设置为 false。它应该是一个 var 而不是 let,因为我们将在 GameOver 函数中更改它:
var enemies:[MovingSprite] = []
var bullets:[MovingSprite] = []
var gameOver = false
将 GameOver 函数添加到 GamePlayScene 类中,紧接在 update 函数之后,如下所示:
func GameOver(){
gameOver = true
self.removeAllActions()
for enemy in enemies{
enemy._sprite.removeAllActions()
}
}
一旦游戏结束,我们将 gameOver 布尔值设置为 false。接下来,我们调用当前类上的 removeAllActions 以停止敌人生成,然后我们也调用屏幕上所有当前存在的敌人的函数,这样子弹就不会生成。
我们还需要在敌人超出屏幕左侧时调用 GameOver 函数,因此为了更新敌人的位置,在循环中的 if 条件中调用 GameOver 函数,如下所示:
for(var i:Int = 0; i < enemies.count; i++){
enemies[i].moveSprite()
var sprite: SKSpriteNode = enemies[i]._sprite
if((sprite.position.x + sprite.size.width/2) < 0){
sprite.removeFromParent()
enemies.removeAtIndex(i)
GameOver()
}
}//update enemies
为了在游戏结束后停止更新英雄和游戏对象,并停止检查碰撞,一旦游戏结束,将三个函数包裹在一个 if 条件语句中,如下所示,其中我们检查 gameOver 布尔值是否为 false。如果是 false,则函数将被调用,否则将跳过并不会调用函数。
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
if(!gameOver){
updateGameObjects()
updateHero()
checkCollision()
}
}
接下来,如果游戏结束,玩家不应能够发射火箭或使英雄向上推。所以,基本上,我们必须在游戏结束后禁用玩家控制。因此,在 touchesBegan 函数中,将检查屏幕被点击哪一侧的部分包裹在 if 条件中,检查是否满足游戏结束条件,如下所示:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
let _node:SKNode = self.nodeAtPoint(location)
if(!gameOver){ //if game is not over check for touches
if(location.x < viewSize.width/2){
println("[GamePlayScene] touchedLeftSide ")
thrust.y = 15.0
}else if(location.x > viewSize.width/2){
println("[GamePlayScene] touchedRightSide ")
addRockets()
}
}
}
}
记分
我们还没有完成checkCollision函数。我们仍然需要跟踪分数。为此,我们必须检查英雄的火箭与敌人的碰撞。因此,在checkCollision函数中,添加以下代码:
for(var i:Int = 0; i < rockets.count; i++){
var rocketSprite = rockets[i]._sprite
for(var j:Int = 0 ; j < enemies.count; j++){
var enemySprite = enemies[j]._sprite
if(rocketSprite.intersectsNode(enemySprite)){
enemySprite.removeFromParent()
rocketSprite.removeFromParent()
rockets.removeAtIndex(i)
enemies.removeAtIndex(j)
score++
}
}
}
我们使用for循环,因为我们在这里需要循环中对象的索引。我们遍历场景中的所有火箭,并通过遍历enemies数组来检查场景中所有敌人的碰撞。如果任何火箭与敌人发生碰撞,则从场景中移除火箭和敌人精灵节点,并从数组中移除火箭和敌人。
最后,在类的顶部创建一个新的全局变量var,命名为score,类型为int,并将其初始化为 0,如下所示。在检查碰撞后,我们增加分数 1,以跟踪分数。
var bullets:[MovingSprite] = []
var gameOver = false
var score:Int = 0
我们可以在控制台中记录分数以检查分数变量是否实际上在增加。但是,玩家如何知道他们已经获得了多少分数?
显示分数
为了显示分数,我们将使用SKLabelNode,并在分数每次更改时分配分数值。由于我们将在checkCollision函数中访问此变量,它必须是一个全局变量。因此,在顶部创建一个名为scoreLabel的变量,类型为SKLabelNode,与其他全局变量一起:
var scoreLabel: SKLabelNode!
在init函数中,在我们添加英雄后,可以添加以下行来初始化scoreLabel变量:
scoreLabel = SKLabelNode(fontNamed:"Chalkduster")
scoreLabel.text = "Score: 0"
scoreLabel.fontSize = 45
scoreLabel.position = CGPoint(x: viewSize.width/2, y: viewSize.height * 0.9)
self.addChild(scoreLabel)
我们为要使用的字体指定一个名称,即Chalkdust。然后我们分配要显示的实际文本。稍后,在checkCollision函数中,我们将根据分数变量的值更改此文本的值。我们将大小设置为45,并将scoreLabel放置在显示高度的 90%,以便它在屏幕顶部,并将其放置在屏幕宽度的中心。最后,我们将scoreLabel添加到当前场景中。
如果你现在运行游戏,我们将分配的文本将会显示,但分数不会更新。为了更新分数,我们必须更改文本并分配我们之前创建的分数变量的实际值。
因此,在我们通过checkCollision函数增加分数后,立即添加以下代码来增加标签的分数文本:
rockets.removeAtIndex(i)
enemies.removeAtIndex(j)
score++
scoreLabel.text = "Score: \(score)"
与我们将变量记录到控制台的方式类似,我们将分数值分配给字符串,然后将其传递给scoreLabel的文本属性。现在,如果我们构建并运行游戏,它应该显示当前分数,如下面的截图所示:

显示游戏结束屏幕
游戏结束后,我们必须向玩家显示GameOver!,并添加一个按钮,以便玩家可以返回主菜单。
在GameOver函数中,在调用停止所有动作的函数后添加一个名为myLabel的标签:
let myLabel = SKLabelNode(fontNamed:"Chalkduster")
myLabel.text = "GameOver!"
myLabel.fontSize = 65
myLabel.position = CGPoint(x: viewSize.width * 0.5, y: viewSize.height * 0.65)
self.addChild(myLabel)
我们添加了字体大小为65的GameOver!文本,以便玩家可以轻松看到,并将其放置在屏幕中央稍上方,为即将添加的主菜单按钮腾出空间。
添加主菜单按钮
接下来,在GameOver函数中,创建一个名为menuBtn的 SKSpriteNode,并将menuBtn的图像集传递给它。为了在images.xcassets中创建图像集,homeBtn.png和homeBtn2.png被包含在Resources文件夹中。我们将它放置在屏幕中央。我们还给它起了一个名字,以便我们可以在touchesBegan函数中引用它,以便如果它被按下,我们可以调用它的一些函数:
let menuBtn = SKSpriteNode(imageNamed: "menuBtn")
menuBtn.position = CGPoint(x: viewSize.width/2, y: viewSize.height/2)
self.addChild(menuBtn)
menuBtn.name = "menuBtn"
接下来,在touchesBegan函数中,在我们检查游戏是否结束之后,添加一个else块,并添加以下代码以替换当前场景为MainMenuScene:
else{ // else check whether main menu button is clicked
let _node:SKNode = self.nodeAtPoint(location)
if(_node.name == "menuBtn"){
let scene = MainMenuScene(size: self.size)
self.view?.presentScene(scene)
}
}

与我们制作播放按钮交互的方式类似,如果游戏结束,我们检查玩家是否触摸了屏幕。如果他点击的位置包含节点名称menuBtn,我们创建一个名为scene的本地对象,并分配MainMenuScene,然后替换当前场景为MainMenuScene。
就游戏玩法而言,我们已经完成了。现在让我们添加一个功能,允许我们保存最高分,这样我们就可以挑战玩家打破它,以增加游戏的可玩性。
保存最高分
对于保存最高分,我们可以使用NSUserDefaults属性。在这里,我们可以使用一个键并为其分配一个特定的值,设备将将其存储在其内存中,以便我们稍后可以检索。最好的事情是,我们可以检索并重写它当前存储的值,到另一个文件中。因此,这里我们将最高分存储在GamePlayScene中,稍后,在MainMenuScene中存储的键的值。
由于它是一个字典,你可以存储整数、浮点数和字符串。在这种情况下,由于最高分始终是整数,我们将为键获取并存储一个整数。这里的键是一个字符串,存储的值是一个整数。
对于检索最高分值,在GameOver函数中添加menuBtn之后添加以下代码:
var currentHighScore = NSUserDefaults.standardUserDefaults().integerForKey("tinyBazooka_highscore")
由于当前没有在键中存储值,它将返回零。
为了祝贺玩家获得新的最高分,创建一个新的SKLabelNode,在我们添加了currentHighScore之后,并称它为highScoreLabel,如下面的代码所示:
var highScoreLabel = SKLabelNode(fontNamed:"Chalkduster")
highScoreLabel.text = ""
highScoreLabel.fontSize = 45
highScoreLabel.position = CGPoint(x: viewSize.width * 0.5, y: viewSize.height * 0.30)
self.addChild(highScoreLabel)
我们创建了一个带有Chalkduster字体的标签。我们将初始文本值设置为空白,以便我们可以根据玩家是否打破最高分来稍后更改它。我们将文本高度设置为45,并将其放置在按钮下方,然后将其添加到场景中。
接下来,我们检查存储在currentHighScore中的值与当前score的值,看看currentHighScore的值是否大于当前score,如下所示:
if (score > currentHighScore) {
NSUserDefaults.standardUserDefaults().setInteger(score,
forKey: "tinyBazooka_highscore")
NSUserDefaults.standardUserDefaults().synchronize()
highScoreLabel.text = "New High Score: \(score) !"
} else {
highScoreLabel.text = "You can Do Better than \(score)"
}
如果分数高于当前最高分,那么我们调用standardUserDefaults的setInteger函数,并将新的最高分(即当前分数)和键分配给它存储。在分配键时,请确保它是唯一的。
要将数据保存到设备,我们必须调用synchronize函数。如果我们未能调用此函数,一旦关闭应用程序,数据将会丢失,所以请确保每次存储值时都调用此函数。
然后,我们通过更改文本属性来祝贺玩家获得新的高分,显示当前分数,即新的最高分。
如果玩家没有打破他当前的最高分,那么在else语句中,我们更改highScoreLabel的文本属性,显示他可以比当前分数做得更好,这样就会激励他再次玩游戏并打破之前的最高分。
在下面的屏幕截图中,我得了7分,我认为这还不错。经过一点练习,我认为我会做得更好。这也只是为了证明代码运行正常,如果当前分数高于最高分,那么它将显示新的最高分。

如果当前分数低于当前最高分,那么你会看到以下屏幕。这里我得了4分,低于保存在内存中的当前最高分7。

重置最高分计数
如果玩家想要将他们的最高分重置为零呢?我们可以通过在主菜单中添加一个按钮并重置键的值为零来实现这一点。
打开MainMenuScene.swift文件,在创建播放按钮的didMoveToView函数中,创建一个名为resetBtn的SKSpriteNode。将此按钮放置在屏幕宽度的四分之三处,并命名为resetBtn。我们将使用此名称来检查玩家是否点击了播放按钮或重置按钮。
重置按钮的资产位于Resources文件夹中,因此创建一个新的图像集resetBtn,并将resetBtn.png和resetBtn2.png分别拖放到1x和2x占位符中,如下所示:
let resetBtn = SKSpriteNode(imageNamed: "resetBtn")
resetBtn.position = CGPoint(x: viewSize.width * 0.75, y: viewSize.height/2)
self.addChild(resetBtn)
resetBtn.name = "resetBtn"
现在,在touchesBegan函数中,我们在检查playBtn时添加一个else if块,并检查是否按下了resetBtn,如下所示:
else if (_node.name == "resetBtn"){
NSUserDefaults.standardUserDefaults().setInteger(0, forKey: "tinyBazooka_highscore")
NSUserDefaults.standardUserDefaults().synchronize()
}
如果按下重置按钮,我们将通过我们获取值的键的值设置为零,并再次调用synchronize函数,以便将此值存储在系统中。
现在,作为最后一件事,当应用程序打开时,让我们向玩家展示当前的最高分,因此,在MainMenuScene类的顶部创建一个新的全局变量currentHighScoreLabel,类型为SKLabel,如下面的代码所示:
class MainMenuScene: SKScene {
var currentHighScoreLabel: SKLabelNode!
接下来,在didMoveToView函数中添加以下内容,紧接在添加重置按钮之后:
var currentHighScore = NSUserDefaults.standardUserDefaults().integerForKey("tinyBazooka_highscore")
currentHighScoreLabel = SKLabelNode(fontNamed:"Chalkduster")
currentHighScoreLabel.text = "Current High Score: \(currentHighScore)"
currentHighScoreLabel.fontSize = 45
currentHighScoreLabel.position = CGPoint(x: viewSize.width * 0.5, y: viewSize.height * 0.20)
self.addChild(currentHighScoreLabel)
我们首先获取存储的当前最高分,然后将这个值赋给文本。其余的代码与之前的代码类似,只是位置y的值乘以 0.2 而不是 0.3,因为播放按钮相当大。
我们还需要在重置最高分后更改文本,所以在我们检查是否按下重置按钮的else if块中,在添加代码重置键的值的地方,添加这里显示的突出显示的行:
else if (_node.name == "resetBtn"){
NSUserDefaults.standardUserDefaults().setInteger(0, forKey: "tinyBazooka_highscore")
NSUserDefaults.standardUserDefaults().synchronize()
var currentHighScore = NSUserDefaults.standardUserDefaults().integerForKey("tinyBazooka_highscore")
currentHighScoreLabel.text = "Current High Score: \(currentHighScore)"
}
在这里,我们再次获取存储的值,只是为了检查我们之前存储的值是否确实得到了反映。然后我们将currentHighScoreLabel的文本设置为这个值。
我们本可以将文本设置为当前最高分:0,这样它仍然可以正常工作,但这样我们就不确定键的值是否实际上被设置为零。
现在,如果你运行代码,你可以在游戏开始时看到主菜单屏幕上显示的当前最高分。你还可以验证一旦重置,最高分的值实际上被设置为零。
因此,在下面的屏幕截图中,主菜单场景再次显示了当前最高分,尽管这段代码是在达到新最高分之后添加的:

在下一张屏幕截图中,我们可以确定一旦按下重置按钮,当前最高分的值就会改变:

所以这就是这一章的全部内容。保存这个文件并保留它,因为我们在下一章还需要它。
摘要
这是一章相当长的内容,我们实际上在其中制作了一个完整的游戏。我们使用 SKScenes 创建了主菜单和游戏场景,并添加了交互式按钮来在场景之间切换。
你学习了如何将资源导入项目,将它们添加到屏幕上,并使它们移动并相互交互。你还看到了如何从屏幕上移除对象以及添加和更新分数。
最后,你看到了如何在设备上存储和检索当前最高分。然而,我们还没有完成游戏。我们还需要添加动画、粒子效果和背景音乐及音效,让游戏变得生动起来。希望你们都在期待它。
同时,你可以练习并尝试打破我的最高分。
第五章:动画与粒子
在上一章中,我们创建了一个基本游戏。在本章中,我们将使游戏更加生动,角色更加逼真。我们将添加动画,而不是仅仅静态图像。我们还将探讨 SpriteKit 内置的粒子效果创建器。
对于角色动画,我们将探讨 SpriteKit 如何创建动画,以及一个名为Texture Packer的外部工具,由 Code'n'Web 开发。我们将了解它是如何简化动画过程的。我们还将介绍一个称为精灵表的概念,这是基础性的,用于优化游戏性能。
我们首先将了解 SpriteKit 如何获取内置的精灵表生成器,使动画过程更加简单。然后我们将探讨 Texture Packer,它进一步简化了这一过程。所以,让我们看看什么是精灵表。
本章涵盖的主题如下:
-
精灵表动画
-
基本 SpriteKit 动画
-
Texture Packer
-
创建英雄精灵表
-
动画英雄
-
粒子系统
-
粒子设计师
-
创建粒子效果
-
将粒子系统添加到游戏中
精灵表动画
到目前为止,我们所做的是使用单个文件来存储玩家、敌人、火箭和子弹。每次我们创建一个新的火箭,游戏就会去获取内存,找到并检索图像。每次创建子弹时,这个过程都会重复。对于像我们这样的简单游戏来说,这没问题,但后来,当我们开始创建包含更多敌人类型和子弹的更复杂游戏时,这个过程将对设备造成很大的负担,并影响游戏性能。
为了解决这个问题,我们使用了精灵表。精灵表包含我们在游戏中将使用的所有图像,存储在一个单独的文件中,而不是在 10 个不同的内存位置存储 10 个图像。精灵表图像文件还将附带一个数据文件,其中包含精灵表中每个图像的位置和大小。在游戏开始时,精灵表图像和数据文件被一次性加载到缓存中。然后,每次调用火箭或子弹时,游戏就知道精灵表的位置,并简单地从其中加载图像。
精灵表文件需要尽可能紧凑,因此图像可能会被旋转以使其更紧凑。数据文件将跟踪这一点,并在游戏中创建帧时,将图像恢复为直立。

在游戏中进行动画时,对应动画的每一帧都将存储在数组中,并以特定的预定义速度循环。
幸运的是,在 SpriteKit 中,我们只需要提供帧。在运行时,SpriteKit 会自动创建精灵表,我们可以在游戏中使用它。我们将使用 SpriteKit 内置的精灵表创建器来创建敌人动画。稍后,我们将使用 Texture Packer 创建玩家动画。
基本 SpriteKit 动画
要创建敌人动画,我们首先必须将图像提供给 SpriteKit。由于我们必须为每个帧提供 1x 和 2x 图像,我们可以创建四个图像集;命名为 enemy1、enemy2、enemy3 和 enemy4;然后为每个集拖动 1x 和 2x。虽然以这种方式做是完全可能的,但非常繁琐。有一种更简单的方法。在命名每个帧时,对于帧的 1x 图像,您可以在文件末尾添加数字 1、2、3 和 4 以显示帧名称。对于帧的 2x 版本,您需要在文件末尾添加 @2x 以告诉 SpriteKit 此文件是原始图像版本的两倍大小。因此,对于第一帧,1x 文件将是 enemy_1.png,而帧的 2x 版本的文件名将是 enemy_1@2x.png(在这里,我使用下划线是因为我们在上一章中使用的常规敌人图像已经命名为 enemy1.png)。
@2x 位是关键字,所以请确保您不要将图像常规命名为 @2x,因为这可能会导致一些意外的结果。这种命名约定仅在您有一个图像的 1x 版本时使用。为了告诉 SpriteKit 哪个图像是 2x 版本,您需要在文件末尾添加 @2x 以表明此图像是另一个图像的两倍大小。
因此,为了创建敌人动画,我们将有四个动画帧。因此,我们将为每个帧有一个图像。此外,对于 2x 版本,我们还需要四个额外的图像,它们的大小是两倍,文件名以 @2x 结尾。
敌人动画的帧在此章节的 Resources 文件夹中提供。获取所有图像并将它们放置在桌面上的一个新文件夹中。现在,为了告诉 Xcode 必须从提供的图像创建精灵表,点击包含所有图像的文件夹,并将其重命名为 enemyAnim.atlas。这非常重要;如果您不这样做,则不会创建精灵表。文件夹应如图所示:

将文件夹拖放到项目文件夹中。当窗口打开时,如图所示,请确保已勾选 Copy items if needed 复选框,并且当前项目是目标:

点击 完成 继续操作。现在文件夹将位于项目中,我们可以开始添加代码来动画化敌人角色。当敌人被创建时,我们不会使用静态图像,而是更改 addEnemy 函数,如下所示。
首先,让我们检查我之前所说的是否正确。如果我们用第一帧的图像名称替换敌人变量,游戏应该仍然可以工作。因此,在创建敌人的地方,我们不会传递敌人图像集,而是将动画的第一帧传递给enemyNode变量。在addEnemy函数中将创建敌人节点的行更改为以下内容:
var enemyNode:SKSpriteNode = SKSpriteNode(imageNamed: "enemy_1")
这不会使角色动画化,因为它仍然只取动画的第一帧并显示它。但至少,我们可以确信帧正在正确加载。在传递名称时不需要添加扩展名,因此不需要.png。你也不需要说@2x来加载更高分辨率的图像,因为 SpriteKit 会自动将@2x的缺失视为1x分辨率,并为2x分辨率获取@2图像文件名。
现在,让我们继续加载其他帧,以便我们可以为敌人动画。在addChild(enemyNode)行之后,在addEnemy函数中添加以下代码行:
let textureAtlas = SKTextureAtlas(named: "enemyAnim.atlas")
var textureArray:[SKTexture] = []
for(var i: Int = 1; i <= 4; i++){
textureArray.append(textureAtlas.textureNamed("enemy_\(i)"))
}
let animation = SKAction.animateWithTextures(textureArray, timePerFrame: 0.2)
let animate = SKAction.repeatActionForever(animation)
enemyNode.runAction(animate)
首先,我们使用 SpriteKit 中的纹理图集类在名为textureAtlas的常量中加载精灵表。等等!什么是纹理图集?嗯,它只是精灵表的另一个名字。你可以叫它任何名字,但它们的意思是相同的。
在加载精灵表后,我们创建一个数组来存储所有纹理,即动画的帧。
由于我们需要加载四个动画帧,即enemy_1、enemy_2、enemy_3和enemy_4,我们创建一个for循环并从1迭代到4。像任何数组一样,我们将这四个文件追加到我们创建的textureArray变量中。我们使用SkTextureAtlas类的textureNamed函数为每个纹理分配。类似于我们在屏幕上记录事物或动态更改文本的方式,我们使用\()运算符提供四个文件的名字。一旦图像存储在数组中,我们创建一个动作,以便我们可以以一定的速度遍历这些帧。SKAction 的animateWithTextures函数接受一个纹理数组以及每个帧应在屏幕上显示的持续时间。因此,在这里,我们提供了存储所有纹理的textureArray变量,并给出 0.2 秒或 200 毫秒作为每个帧将显示的时间。然而,这将只运行一次动画。为了让动画反复运行,我们使用repeatForever动作。因此,我们创建一个新的常量animate,并通过传递动画将其存储在其中。最后,我们在enemyNode变量上运行这个动作。现在你可以构建并运行,以查看敌人角色正在动画化。
这一切都很不错,但有一个更简单的方法,这样我们就不必为 1x 和 2x 分辨率的游戏创建单独的帧集。此外,在创建纹理数组时,我们需要事先知道动画中需要循环多少帧。对于敌人来说,我们知道动画中有四帧,所以我们从 1 循环到 4。如果我们不知道这个信息怎么办?如果我们循环的帧数少于或超过动画的总帧数,可能会导致错误或动画看起来不流畅。
因此,为了动画化玩家,我们将使用 Texture Packer 的动画方式。
探索 Texture Packer
Texture Packer 是一款非常受欢迎的软件,被迪士尼、Zynga 和 WG Games 等行业专业人士用于创建精灵图集。您可以从 www.codeandweb.com/ 下载它。

这与我们在创建敌人动画时创建图像的方式相似。要使用 Texture Packer 创建精灵图集动画,您也必须首先在 Photoshop 或 Illustrator 中创建单个帧。我已经制作好了,每个单独帧的图像都已经准备好了。
您可以使用 Texture Packer 的试用版来跟随教程。下载时,请选择适合您操作系统的版本。幸运的是,Texture Packer 可用于所有主要操作系统,包括 Linux。
小贴士
下载 Texture Packer 后,您有三个选项:您可以点击尝试完整版一周,购买许可证,或者点击 基本版本 使用试用版。在试用版中,一些专业功能被禁用,所以我建议您尝试一周的专业功能。
点击选项后,您应该会看到以下截图所示的界面:

在这里,您可以通过点击屏幕左下角的 打开项目 按钮来打开一个现有项目,或者选择您希望创建精灵图集的框架。
如您所见,Texture Packer 支持广泛的框架和格式,包括 Cocos2d、Unity、Corona、Swift 以及更多。我们需要从列表中选择 Swift 并点击 创建项目。
注意
Texture Packer 有三个面板;让我们从左侧开始。左侧面板将显示您为创建精灵表而选定的所有图像的名称。在这里,您可以拖放单个图像或包含您的资源的完整文件夹。中间面板是一个预览窗口。它显示了图像是如何打包的。右侧面板提供了选择您希望存储打包纹理和数据文件的位置以及打包图像格式的选项。布局部分为在纹理打包器中设置单个图像提供了很多灵活性。最后,我们有不同的模式来优化精灵表。
让我们来看看右侧设置面板中的关键项:

数据
在数据下,我们定义了有关要导出数据文件的所有信息。这包括数据格式、图集包和Swift 类文件。以下是对每个字段的解释:
-
数据格式:如我们之前所见,每个导出的文件都会创建一个包含图像集合和数据文件的精灵表,该数据文件跟踪精灵表上的位置。数据格式通常取决于您选择的框架或引擎。由于我们最初选择了 SpriteKit 并选择了Swift作为语言,因此格式为
swift类型。假设我们正在使用 Objective-C 语言,那么也有一个针对该语言的单独选项。因此,在选择格式时要小心,否则如果您希望为另一种格式开发精灵表,您将不得不从头开始。 -
图集包:这是您希望导出的图像和数据文件保存的位置。因此,一旦文件发布,您将获得一个
.png图像文件和一个包含精灵表信息的.plist数据文件。 -
Swift 类文件:除了图像和数据文件外,Texture Packer 还会创建一个包含信息的辅助类,这些信息将与图像和数据文件一起导入,以使动画代码更加简单。
纹理
在纹理部分,我们将指定精灵表图像的文件细节。这包括有关纹理格式、Png Opt Level和像素格式的详细信息。以下是对这些字段的解释:
-
纹理格式:默认设置为
.png,但也支持其他格式。除了 PNG 之外,您还可以使用 PVR 格式。这种格式用于数据保护,因为它更容易从常规 PNG 文件中复制数据。此外,PVR 格式提供了优越的图像压缩。但是,请注意,它只能在苹果设备上使用。 -
Png Opt Level:用于设置 PNG 文件的质量。
-
像素格式:这设置了要使用的 RGB 格式。通常,您希望将其设置为默认值。
布局
在这里,我们指定精灵表图像的布局。以下字段可以在布局部分看到:
-
最大尺寸: 您可以根据框架指定精灵图集的最大宽度和高度。通常,所有框架都允许最大为 4092 x 4092,但这主要取决于框架,因此在创建精灵图集之前,请检查框架允许的最大尺寸。
-
尺寸约束: 一些框架更喜欢精灵图集以 POT 格式(或 2 的幂)存在,即 32 x 32、64 x 64、256 x 256 等。如果是这种情况,那么您需要相应地选择。否则,您可以选择任意尺寸。
-
缩放变体: 这用于放大或缩小图像。如果您打算为不同的分辨率创建图像,例如1x、2x和3x,那么此选项允许您根据您为不同分辨率开发的游戏创建资源。此外,您无需进入图形软件,分别缩小图像并重新打包所有分辨率的图像。
-
算法: 这是用于创建精灵图集的代码逻辑,并确保图像以最有效的方式打包。在基本版本中,您必须从下拉菜单中选择基本,与专业版本不同,在专业版本中,您可以选择MaxRects。
-
多包: 如果 PNG 图像文件超过最大尺寸,则 Texture Packer 将自动为之前无法包含到精灵图集中的图像创建额外的精灵图集和数据文件。
精灵
在这里,我们指定对精灵图集中的单个精灵的任何特殊处理。本节中有一个字段:
- 裁剪模式: 移除围绕每个图像的额外透明度,使精灵图集更加紧凑,从而进一步减小文件大小。
创建英雄精灵图集
在本章节的Resources文件夹中有一个名为heroAnim的文件夹。此文件夹包含另一个文件夹,其中包含英雄空闲动画的单独帧。将heroAnim文件夹拖放到 Texture Packer 的左侧面板中。
您将在这里看到heroAnim文件夹的文件夹结构。在其下,您将看到idle文件夹,其中包含帧的单独文件。在预览窗格中,您将看到从提供的图像创建的精灵图集的预览。
在布局部分,点击缩放变体按钮。从预设的下拉列表中选择SpriteKit @2x/@1x。这将自动创建2x和1x分辨率的精灵图集,通过将图像缩小 50%并保存为1x模式。

接下来,在数据标题下,选择您想要保存精灵图集图像和数据文件的存储位置。这些文件将保存在具有.atlasc扩展名的文件夹内部的位置,并且这个文件夹将包含图像和数据文件。接下来,选择要保存类文件的位置。您可以将其保存到任何您想要的位置,但请确保您记得保存的位置,因为稍后需要用到它。
还请确保将当前文件保存在某个位置,这样如果您稍后想要对文件进行修改,您将能够轻松地打开它并进行修改。需要注意的是,如果您更改heroAnim文件夹的位置,那么引用将会丢失,您将需要重新导入图像。请将图像、精灵图集、类和 Texture Packer 文件保存在单独的文件夹中,以便所有相关数据都在同一目录下。
最后,要创建精灵图集,请点击顶部的发布精灵图集按钮。现在我们可以为英雄动画了。
为英雄动画
要在游戏中为英雄动画,将.atlasc文件夹和 Swift 类文件拖入项目。然后,在init函数中,在将英雄添加到场景后立即添加以下代码:
let heroAtlas = heroAnim()
let heroIdleAnimArray = heroAtlas.hero_Idle_()
let animaiton = SKAction.animateWithTextures(heroIdleAnimArray, timePerFrame: 0.2)
let animate = SKAction.repeatActionForever(animaiton)
hero.runAction(animate)
就这样!您可以为游戏构建和运行,以查看英雄的动画效果。
在这里,我们首先创建对在 Texture Packer 中创建的 Swift 类的引用。然后,我们创建一个名为heroIdleArray的常量,并将heroAnim.swift类中已创建的空闲动画数组分配给它。如果没有这个,我们就必须手动创建一个数组并存储帧,就像我们为敌人做的那样。接下来的三个步骤与创建敌人动画的方法完全相同。我们创建一个动画常量,传递数组和我们想要为每个帧设置的延迟,然后创建另一个动作来重复动画,最后运行英雄的动画。
因此,我们看到我们不必创建两套图像,因为 Texture Packer 为我们创建了它们,而且我们也不必为空闲动画创建数组。如果我们后来需要创建跑步、行走、跳跃或攻击动画,这一点尤为重要。我们不能每次都创建数组。实际上,在其他框架中,如果我们想要创建不同的动画,就需要这样做。使用 Texture Packer 为 SpriteKit 创建这个过程要容易得多。
让我们看看 Texture Packer 创建的 Swift 类,以便您更好地理解正在发生的事情以及数组是如何创建的。所以,打开heroAnim.swift文件:
// Sprite definitions for 'heroAnim'
// Generated with TexturePacker 3.6.0
//
// http://www.codeandweb.com/texturepacker
// ---------------------------------------
import SpriteKit
class heroAnim {
// sprite names
let HERO_IDLE_1 = "hero_Idle_1"
let HERO_IDLE_2 = "hero_Idle_2"
let HERO_IDLE_3 = "hero_Idle_3"
let HERO_IDLE_4 = "hero_Idle_4"
// load texture atlas
let textureAtlas = SKTextureAtlas(named: "heroAnim")
// individual texture objects
func hero_Idle_1() -> SKTexture { return textureAtlas.textureNamed(HERO_IDLE_1) }
func hero_Idle_2() -> SKTexture { return textureAtlas.textureNamed(HERO_IDLE_2) }
func hero_Idle_3() -> SKTexture { return textureAtlas.textureNamed(HERO_IDLE_3) }
func hero_Idle_4() -> SKTexture { return textureAtlas.textureNamed(HERO_IDLE_4) }
// texture arrays for animations
func hero_Idle_() -> [SKTexture] {
return [
hero_Idle_1(),
hero_Idle_2(),
hero_Idle_3(),
hero_Idle_4()
]
}
}
在 Texture Packer 中创建文件时,我将其命名为heroAnim,这就是为什么类的名称和我命名的一样。其次,你会注意到我们用来创建精灵图的四个图像分别命名为hero_idle_1、hero_idle_2、hero_idle_3和hero_idle_4。因此,在类的开始处,它会自动为这四个文件名创建等于字符串名称的常量。
然后,类创建了一个名为textureAtlas的SKTextureAtlas常量。这个纹理图集是从heroAnim.atlasc文件夹创建的。所以,尽管名称与类的名称相同,但这实际上是精灵图文件;不要混淆。
在获取纹理图集后,创建了四个函数来获取之前定义的常量名称中存储在纹理图集中的四个图像。然后最终创建了一个新函数,该函数将四个图像添加到数组中并返回该数组。这个函数的命名方式与我们命名用于创建动画的图像文件的方式相似。这使得在创建动画动作时知道要调用的函数名称变得方便。还要记住,所有这些都是在 Texture Packer 中自动完成的。
除了空闲动画之外,如果我们有两个用于跑步周期的图像,分别命名为heroRun1.png和heroRun2.png,那么将返回run数组的函数将被命名为heroRun()。记住这个方法的一个简单方式是将任何数字替换为开闭括号以获取函数名和帧数组。
小贴士
还有一点很重要:在为创建动画帧所需的图像命名时,确保你为所有帧使用相同的命名约定,就像命名帧的情况一样。例如,在为动画命名帧时,我通常将文件命名为name_action_number的形式。它也可以像我们命名跑步动画示例那样,命名为nameActionNumber,但请确保你保持一致性。
还很重要的一点是要确保文件的编号顺序是你想要动画播放的顺序。这是因为当播放动画时,首先播放帧 1,然后是帧 2,然后是帧 3,依此类推。如果你的图像编号不正确,动画将按此顺序播放,播放时可能会显得有些奇怪。
这些是你需要注意的唯一事项。如果你在文件命名和编号上保持一致,那么在 Texture Packer 中正确创建动画将变得非常容易。Texture Packer 为你做了大部分脏活,并且没有留下出错的空间,因为它主要为你自动化了这个过程。
现在我们来看看如何在游戏中创建粒子。
粒子系统
粒子系统是一组精灵或粒子。每个粒子系统都有一个发射器,粒子将从那里产生。粒子系统还决定了系统中粒子的行为。因此,可以说粒子是创建粒子系统的最小实体。
粒子系统的一个非常简单的例子是雨。雨是一个粒子系统,其中每一滴雨都是粒子,而云有大量的发射器,从这些发射器中产生水滴或粒子。
我们创建一个粒子系统而不是创建单个粒子,因为使用粒子系统,我们可以使用相同的粒子创建不同类型的特效。例如,我们看到了雨,这是一个粒子系统。如果我们想有另一个效果,比如水从水龙头流出呢?在这里,粒子是相同的——水滴,但雨滴的行为不同。当水从水龙头落下时,每一滴都带有力量落下,并且由单个发射器——水龙头出口——创建。因此,我们可以将粒子系统更改为只有一个发射器,并给粒子一个初始向下的力。这样,我们将有相同的粒子以不同的方式表现,而不是从头开始编码系统。
在 SpriteKit 中,就像在其他任何框架中一样,每个粒子都是一个由粒子系统控制的图像,该粒子系统有一个或多个发射器。发射器控制粒子系统的生成、移动和销毁。
为了渲染粒子系统,创建了一个SpriteKit 粒子文件(.sks)。它可以包含任何大小的粒子系统,并允许整个粒子系统旋转和缩放。
设计粒子
SpriteKit 有一个内置的粒子设计器。这个设计器有一个相当不错的用户界面,可以用来创建您自己的粒子系统。SpriteKit 还包括一些默认的粒子系统,这些系统已经内置,可以通过从下拉菜单中选择您想要用于游戏的粒子系统来创建。
要创建一个新的粒子系统,请前往文件并选择新建文件。您也可以通过在项目的根文件夹上右键单击来创建一个新的文件。在左侧的iOS面板中,选择资源,然后选择SpriteKit 粒子文件,并点击下一步。

现在,从下拉菜单中,您可以从 SpriteKit 内置的八个默认粒子系统中进行选择。您可以从波纹、火焰、萤火虫、魔法、雨、烟雾、雪和火花中进行选择:

在这个例子中,我选择了火焰。一旦您选择了所需的粒子系统,请点击下一步。我们将必须给粒子系统起一个文件名,这样我们就可以在以后创建游戏中的效果时调用它,所以我将其命名为fireParticle。

完成后,你将在项目中看到创建的fireParticle.sks文件,编辑面板中的火粒子正在熊熊燃烧。默认情况下,它位于屏幕中央。在编辑面板的任何位置单击,火粒子系统将移动到该位置。你还可以单击并移动鼠标,以查看粒子系统随着鼠标箭头移动。
你还会看到工具面板已更改,出现了一堆可更改参数的变量。你可以更改这些参数来创建自己的自定义粒子系统。
让我们逐一了解这些基本参数,以便你可以了解每个变量做什么以及如何通过更改每个参数来改变粒子系统的行为。

名称
如果我们想在代码中通过名称引用粒子系统,我们可以在这里给它一个名称,这样我们就可以稍后引用它。类似于我们给enemyNode命名以检查传递给movingSprite类的节点是否是敌人,然后根据该信息执行某些功能。
背景
背景参数设置背景颜色。更改它不会影响粒子。这纯粹是为了可见性。如果你的粒子是黑色的,你可以将背景改为白色,这样你就可以清楚地看到粒子的外观和行为。
粒子纹理
粒子纹理参数是每个粒子将显示的纹理或图像。目前使用spark.png作为纹理。如果你想的话,可以将其更改为敌人、子弹、火箭或英雄图像。
粒子
粒子参数控制粒子发射的速度以及你希望发射器发射的粒子数量。要控制速度,你可以增加或减少出生率参数。我们可以通过降低出生率来降低发射率,或者如果我们想让粒子更快地发射,我们可以增加它。要限制粒子数量,我们将最大值更改为我们希望发射器发射的粒子数量。如果我们想让发射器持续发射粒子,我们保持该值为 0。
寿命
当粒子被创建时,寿命决定了它在屏幕上停留多长时间后才被删除。在这里,每个粒子在屏幕上停留 2.5 秒。范围用于在粒子的行为中引入一些随机性。假设我们将此范围值更改为 1。那么一些粒子将在屏幕上停留 2.0 秒,而其他粒子将停留 3.0 秒,然后被删除。因此,创建的随机值是范围值的一半加上初始值。
这就是范围的工作原理:它取第一个值,然后通过从它中加上或减去范围值的一半来得到一个值,这样看起来就像每个粒子都有不同的寿命,就像生活中,不是所有粒子都以相同的方式表现。
位置范围
默认位置在屏幕中央。通过查看变量名称中的位置范围关键字,你可能已经猜到我们输入的值是范围值。在这里,x的值是55.65,这意味着当一个粒子被生成时,它将在从中心向x方向的-27.825到+27.825之间随机生成。发射点在编辑器视图中用一个小的绿色点表示。y的值是5,这意味着从中心开始,粒子将在y方向的-2.5到+2.5之间随机生成。如果你将X和Y的值改为0,你会看到所有粒子都从绿色点发射出来。
角度
角度参数决定了粒子生成的角度。由于我们希望火焰在这种情况下向上移动,所以角度大约是 90 度。你也可以将其值改为45,这样看起来就像有风吹拂着火焰。为了给初始角度添加一些随机性,范围保持在20。否则,所有粒子都将直接向上移动,这看起来非常不自然。
速度
这是粒子在创建时开始移动的速度。在这里,它们以平均速度 100 开始移动。有一个范围是 50,这意味着一些粒子将以最小速度 75 移动,而其他粒子将以最大速度 125 移动。
加速度
我们可以在x和y方向上加速一个粒子,例如,在喷气发动机或彗星的情况下。为了创建这种效果,你必须将X位置的范围改为5,并将Y加速度增加到大约500。

Alpha
这定义了每个粒子的不透明度或透明度级别。如果Alpha的值为零,则粒子将完全透明,而1表示它将完全可见。还有一个你可以指定的范围值。速度参数决定了每个粒子的Alpha值每秒变化的速率。因此,粒子一旦创建就会变得可见,然后在一段时间后,随着Alpha值的降低,它变得透明。
缩放
与Alpha类似,缩放值范围从0到1。在0时,图像完全不可见;在1时,它恢复到原始大小。因此,在0.5时,对象将是原来的一半大小;在2.0时,它在x和y方向上将是原来大小的两倍。在这里,对象的起始值为0.5,范围为0.4。因此,任何粒子的初始起始大小将在0.3和0.7之间。由于速度为-0.5,它将在生成后的一段时间内逐渐变小。
旋转
一旦创建了一个粒子,我们可以通过给它一个起始值和范围来生成随机旋转速度,使其旋转。我们还可以通过改变速度参数在一段时间内增加或减少旋转速度。
颜色混合
颜色混合用于将一种颜色与另一种颜色混合。在这里,初始因子为1,范围为0,所以我们使用分配的原始颜色。由于我们不是进行颜色混合,我们已将速度值保持为0。我们可以将速度更改为-0.125,以看到颜色逐渐混合到白色,颜色在粒子生命周期的末尾变得更浅。
颜色渐变
在这里,我们可以指定要生成的粒子的颜色。粒子纹理图像始终保持白色,这样我们就可以在代码中随时更改对象的颜色。所以在这里,尽管纹理或图像颜色是白色,但火焰的颜色是橙色的。
我们还可以在不同的生命周期阶段为粒子分配不同的颜色。

其他四个变量——混合模式、场掩码、自定义着色器和自定义着色器统一变量——与着色器和着色器编程有关,这超出了本书的范围。使用着色器,你可以为粒子创建自定义效果和行为。
如果你了解其工作原理,可以稍微玩一下混合模式,因为它与你在 Photoshop 中看到的东西非常相似。如果你了解 Photoshop,你会熟悉添加、减去、乘以、屏幕、替换和Alpha。你可以选择这些中的每一个,并查看它对粒子系统产生的影响。
现在我们需要的信息已经足够来设计我们自己的游戏粒子系统了。现在,让我们创建我们将要在游戏中使用的粒子系统。
创建粒子效果
对于游戏,我们将创建一个非常简单的爆炸粒子系统,并且每次英雄发射火箭时都会显示。我们将使用我们自己的自定义精灵图像。所以,转到该章节的资源文件夹,将smoke.png图像拖放到项目中。别忘了将其添加为副本,并确保当前游戏是目标。
现在创建一个新的粒子系统,将文件命名为explosionParticle,并将其保存到你的项目中。你可以使用火焰、烟雾或任何默认的粒子系统。这无关紧要,因为我们无论如何都会更改这些值以符合我们的规格。

在前面的截图上,你可以看到我使用默认粒子作为基础创建的粒子系统的参数。
我将其命名为explosionParticle以防我需要在代码中引用粒子系统。然后我将背景保持为默认设置,因为我仍然能够清楚地看到粒子。
我将纹理更改为之前导入的烟雾图像文件,并用它替换了默认图像。对于出生率,我将其值保持在约65;你可以根据你的需求进行更改。我将粒子数的最大值保持在12,这样在创建了 12 个粒子后,发射器将停止创建任何新的粒子。
寿命参数保持在约8,范围保持在约16。你可能会说有些粒子可能一创建就被销毁。好吧,这是真的,而且这同样适用于烟雾粒子。当枪被发射时,有些烟雾中的粒子我们甚至看不到,所以即使看起来像卡通,行为也会更真实。
位置范围参数在x方向上约为4,在y方向上约为9。这只是为了在指定的初始位置周围随机创建粒子,这样所有的粒子看起来就不会像是从同一个点出现的。
角度参数保持在0到360之间,因为我们希望烟雾粒子在它们被创建的点周围的所有方向移动。
每个粒子在开始时的速度参数保持在约48,范围约为90。这将使得一些粒子移动较慢,而其他粒子则移动得很快。事实上,一些粒子可能根本不会移动或移动得非常慢,从而使行为再次更真实。
加速度参数在X和Y方向上均保持在0,因为这不必要,但你也可以调整这些值来看看你是否喜欢效果。
由于我们希望烟雾粒子缓慢消失,我们赋予一个初始值和范围,并通过逐渐增加速度参数到-1来缓慢减少 alpha 值。由于图像的大小非常大,我将它缩放到0.25以使其更小,并赋予一个范围,使得大小是随机的。我将速度值更改为-0.125,这样每个粒子的尺寸就会在一段时间内缓慢减小。
粒子一旦被创建就需要旋转,所以我给它赋予了一个初始值和范围。我将速度参数增加,使得粒子在一段时间内旋转得更快。
由于我没有进行颜色混合,我将因子保持在1,范围和速度在0。我将粒子的颜色改为浅蓝色,使其看起来更卡通化。黑色可能会显得有点太暗,更真实。
最后,我将混合模式更改为Alpha,因为我不想使用加法混合模式。您可以对您的粒子文件进行必要的更改,然后按command + S保存文件。
将粒子系统添加到游戏中
粒子系统文件现在已准备好被调用到游戏中。因此,打开GameplayScene.swift文件,在addRocket函数中,在我们将火箭添加到场景后立即添加以下代码:
let explosionParticle = SKEmitterNode(fileNamed: "explosionParticle")
explosionParticle.position = CGPoint(x: hero.position.x + hero.size.width/2 + 10, y: hero.position.y - 5)
self.addChild(explosionParticle)
在这里,我们创建了一个名为explosionParticle的新常量,其类型为SKEmitterNode,并传递我们之前创建的粒子系统的文件名。每次您想要创建粒子系统时,都必须使用SKEmitterNode。
接下来,我们将粒子系统放置在英雄火箭筒的喷嘴处。这与我们放置英雄火箭的方式类似。最后,我们将粒子添加到场景中。
我们不必担心移除每个粒子,因为粒子系统会自动处理这一点。当每个粒子达到其生命周期的末端时,它会自动从场景中移除。
现在,您可以构建并运行游戏,但您会发现粒子系统创建粒子需要一点时间。为了使其看起来像是瞬间创建的,我们在将粒子添加到场景后,将以下突出显示的代码添加进去:
self.addChild(explosionParticle)
explosionParticle.advanceSimulationTime(0.25)
这行代码将模拟快速推进到粒子创建后的 0.25 秒,看起来就像粒子是瞬间被创建出来的。再次构建并运行游戏,以查看粒子的效果。

粒子设计师中还有一些额外的参数和属性,例如advanceSimuationTime,只能在代码中调用和修改。但最好通过设计师设计大部分粒子,然后在将其添加到场景后通过代码进行微调。
现在,如果您愿意,可以使用代码创建粒子系统,并通过代码单独定义参数,如下面的代码片段所示,这将产生相同的结果:
let explosionParticle = SKEmitterNode()
explosionParticle.particleTexture = SKTexture(imageNamed: "smoke")
explosionParticle.particleBirthRate = 65.5
explosionParticle.numParticlesToEmit = 12
explosionParticle.particleLifetime = 8.841
explosionParticle.particleLifetimeRange = 16
explosionParticle.particlePositionRange = CGVector(dx:5.0, dy: 9.0)
explosionParticle.emissionAngle = 0
explosionParticle.emissionAngleRange = 360
explosionParticle.particleSpeed = 48
explosionParticle.particleSpeedRange = 90
explosionParticle.xAcceleration = 0
explosionParticle.yAcceleration = 0
explosionParticle.particleAlpha = 1.0
explosionParticle.particleAlphaRange = 0.2
explosionParticle.particleAlphaSpeed = -1.0
explosionParticle.particleScale = 0.25
explosionParticle.particleScaleRange = 0.125
explosionParticle.particleScaleSpeed = -0.125
explosionParticle.particleRotation = 60
explosionParticle.particleRotationRange = 60
explosionParticle.particleRotationSpeed = 5.0
explosionParticle.particleColorBlendFactor = 1.0
explosionParticle.particleColorBlendFactorRange = 0
explosionParticle.particleColorBlendFactorSpeed = 0
explosionParticle.particleColor = UIColor(red: 0.455, green: 0.784, blue: 0.835, alpha: 1.0)
explosionParticle.particleBlendMode = SKBlendMode.Alpha
explosionParticle.position = CGPoint(x: hero.position.x + hero.size.width/2 + 10,
y: hero.position.y - 5);
self.addChild(explosionParticle)
explosionParticle.advanceSimulationTime(0.25)
这些确实是相同的变量和参数。唯一的区别是这里它是代码格式。显然,使用设计师创建粒子更方便。
在苹果开发者门户中,有一些额外的命令可用于粒子系统,方便您使用。如果您感兴趣,可以查看并尝试一下。
摘要
在本章中,我们学习了如何使用动作和精灵表来创建游戏中的动画。我们查看了 SpriteKit 内置的精灵表生成器,并使用了一个名为 Texture Packer 的专业工具来创建精灵表。我们还看到了使用这个专业工具生成精灵表和动画是多么容易。
除了这些,我们还看到了 SpriteKit 粒子设计器的介绍,并在游戏中创建并实现了粒子系统。
然而,游戏中仍然缺少一些东西,那就是音效和字体定制。我们将在下一章中探讨这个问题,包括如何为游戏添加最后的修饰。
第六章:音频和视差效果
在 SpriteKit 中添加背景音乐和音效非常简单,因为它只需要调用一行代码。在添加效果时需要考虑一些重要的事情;例如,在请求 SpriteKit 播放声音后,场景会改变吗?以及应该使用哪种格式的音频文件?
在本章中,我们将为游戏添加背景音乐,并为火箭发射、敌人被火箭击中、屏幕上的按钮被按下以及游戏结束时添加音效。我们还将查看一个可以用来将一个音频文件从一种格式转换为另一种格式的免费应用程序。最后,我们将为背景添加一个漂亮的滚动效果,以增加额外的乐趣。
本章涵盖了以下主题:
-
音频文件格式
-
下载和安装 Audacity
-
转换音频格式
-
添加音效
-
添加背景音乐
-
视差背景——理论
-
实现视差效果
音频文件格式
SpriteKit 允许我们使用 OSX 标准的 .caf 音频文件格式,但它也支持 .mp3 文件。理论上,你可以使用这些格式中的任何一个来在游戏中播放音频或效果。你所需做的只是更改在请求 SpriteKit 播放文件时调用的文件名。但为了优化,我们将使用 .mp3 文件格式。原因是 .mp3 文件的大小比 .caf 文件小得多。以背景音乐文件为例;.caf 文件大小为 5.2 兆字节,而 .mp3 文件仅为 475 千字节——大约是 .caf 文件大小的十分之一,玩家的体验不会有任何不同。
添加的音频文件越多,它将增加捆绑包的大小,这将极大地增加游戏的大小,并使玩家下载和玩游戏的时间更长。对于像我们这样的小型游戏,这可能不是很重要,但当你开始制作包含更多音效和背景音乐的更大游戏时,这是需要记住的事情。
虽然 .mp3 是一个非常流行的格式,但有时你可能需要将音频文件格式转换为 .mp3。在下一节中,我们将看到如何确切地做到这一点。
下载和安装 Audacity
Audacity 是一款免费的跨平台音频录制和编辑软件。你可以从 audacity.sourceforge.net/ 下载最新版本。尽管版本 2.06 表示它是为 Mac OS X 10.4 到 10.9.x 设计的,但它也可以在 Yosemite 上运行。

下载 DMG 文件,双击它,然后将 Audacity 文件夹拖到你的 应用程序 目录中。从启动台,点击 Audacity 应用程序以打开它。

您可以点击快速帮助、手册、Wiki和论坛链接来了解更多关于软件及其功能的信息。点击确定继续。
我不是声音工程师,所以我真的不是告诉您或解释每个标题做什么的合适人选。我真正能说的是,如果您对声音编程或工程有所了解,您可以用这个免费开源应用程序真正施展魔法。例如,我们可以通过将文件频率从 44 kHz 降低到 22 kHz 来减小文件大小,如下面的截图所示,或者将格式从双声道或立体声减少到单声道。
要更改频率,点击文件名称旁边的向下箭头,选择设置速率,并选择22050 Hz而不是44100 Hz:

转换音频格式
要将格式转换为 MP3,请转到文件 | 导入 | 音频。在本章的Resources文件夹中,您将找到一个名为Game Audio Files的文件夹。在这个文件夹内,还有一个名为caf的文件夹。在这个文件夹中,您将找到所有.caf格式的音频文件。选择文件夹中的所有文件,然后点击打开。现在所有文件都将导入到当前项目中。

一旦所有文件都导入,要将它们转换为 MP3 文件,您需要从lame.buanzo.org/#lameosxdl安装 lame 库。我知道它的名字很俗气,但它确实有效。下载第一个链接,上面写着适用于 Audacity 1.3.3 或更高版本。下载 DMG 文件,打开包,并安装它。

现在回到 Audacity,导航到文件 | 导出多个。以下窗口将打开:

在打开的窗口中,选择格式为MP3 文件。对于导出位置,我在Game Audio Files文件夹中创建了一个名为mp3的新文件夹。对于文件名,选择第一个选项,以便它将保持与我们要提供的文件相同的标签名称。您可以根据您的偏好勾选或取消勾选覆盖现有文件框。
小贴士
请记住,如果您勾选此框,所有具有相同名称的文件在将来都会被覆盖,所以请小心。
点击导出开始转换过程。然后点击所有正在导出的文件的确定。您将收到一条确认信息,表明所有文件都已成功转换,如下所示:

现在,如果您查看mp3文件夹,您将看到所有文件都已转换为.mp3格式。

文件准备就绪后,我们可以为游戏添加声音和效果。
添加音效
如我之前所述,在 SpriteKit 中添加声音和效果非常简单。实际上只需要添加一行代码来创建所需的音效。
首先,我们将所有文件导入到项目中。因此,选择您之前创建的文件夹中的所有.mp3文件,并将它们拖入项目中。

确保您已勾选“如有需要则复制项目”,并且您的项目已选为目标。

此外,请确保文件位于构建阶段中的“复制资源包”下,因为有时即使您已将项目选为目标,文件也可能没有被复制。如果文件不在这里,那么您将收到一个构建错误,表示找不到该文件,尽管文件确实存在于项目层次结构中。在这种情况下,点击+按钮并添加文件。

添加 fireRocket 音效
好的!所有文件都已导入,让我们在英雄发射火箭时调用fireRocket音效。在GamePlayScene.swift文件中的addRockets函数末尾,添加以下代码行:
let fireRocker = SKAction.playSoundFileNamed("fireRocket.mp3", waitForCompletion: false)
self.runAction(fireRocker)
要播放声音,您还将使用SKAction类。SKAction类有一个名为playSoundFileNamed的属性用于播放声音,它接受两个参数。第一个参数是声音文件的名称,第二个是一个布尔值,它确定动作的长度。如果是true,则动作的持续时间等于音频轨道的长度。否则,动作将立即终止。
注意,播放声音时我们必须提供包括扩展名在内的完整名称。所以,如果您在项目中使用.caf文件,并且想要调用该文件,您必须相应地替换扩展名。
如果您构建并运行游戏,您将在点击屏幕右侧时听到音效。是的,这真的很简单!
虽然这很简单,但你可能已经注意到,您第一次发射火箭时,从您轻触屏幕到音文件实际播放之间有一点点延迟。这是因为当第一次调用音效时,系统必须从内存中检索文件。系统必须为我们将要调用的每个文件都这样做。一旦初始加载完成,从下一次开始,文件就已经存储在内存中,因此可以立即调用。
添加敌人击杀音效
接下来,我们将添加一个效果,该效果将在敌人被击杀时发生。因此,在checkCollision函数中,我们检查玩家火箭与敌人之间的碰撞,在GamePlayScene.swift文件中增加分数的代码之后,添加以下代码:
let enemySound = SKAction.playSoundFileNamed("enemyKill.mp3", waitForCompletion: false)
self.runAction(enemySound)
哇哦!每次敌人被击败时效果都会开始播放。你可以通过说 self 在敌人或类本身上运行动作,这实际上并没有什么区别,只要你确保在某个东西上调用动作,否则音效不会播放。
在游戏结束时添加音效
接下来,我们将为游戏结束添加音效。因此,在 GameOver 函数中,在我们移除 self 和英雄的动作之后,添加以下代码片段中高亮显示的行。这很重要,因为如果你在之前添加它,那么动作将会立即被移除,你将坐在那里挠头,认为你已经添加了音效,但音效却没有播放:
//removing actions
self.removeAllActions()
for enemy in enemies{
enemy._sprite.removeAllActions()
}
hero.removeAllActions()
//playing one last action
let gameOverSound = SKAction.playSoundFileNamed("gameOver.mp3", waitForCompletion: false)
self.runAction(gameOverSound)
接下来,我们将为屏幕上的按钮按下时添加一个弹出音效。因此,在 touchesbegan 函数中,我们检查 menuBtn 是否被按下,在呈现场景之前添加以下代码:
let popSound = SKAction.playSoundFileNamed("pop.mp3", waitForCompletion: false)
self.runAction(popSound)
现在,运行游戏并让它结束。当你按下主页按钮时,没有声音。没有声音?!
嗯,由于我们从 GameplayScene 切换到 MainMenuScene 时场景已经改变,所以音效没有播放。
解决这个问题的简单方法是在小延迟后调用 MainMenuScene,这样在同时,音效将会播放。为此,创建一个名为 btnPressed 的函数,并将调用 MainMenuScene 的所有代码复制粘贴到其中,如下所示:
func btnPressed(){
let scene = MainMenuScene(size: self.size);
self.view?.presentScene(scene)
}
此外,删除以下行,因为它们已经在 btnPressed 函数中被调用:
let scene = MainMenuScene(size: self.size)
self.view?.presentScene(scene)
然后,紧接着我们在调用 popSound 之后,添加以下代码行,其中我们创建 SKAction。这将延迟 1 秒后调用 btnPressed 函数:
let buttonPressAction = SKAction.sequence(
[SKAction.runBlock(btnPressed), SKAction.waitForDuration(1.0)])
self.runAction(buttonPressAction)
如果你现在按下主页按钮,音效将会播放,并且一秒后场景将切换到 MainMenuScene。
与你在这里添加 popSound 的方式类似,在 MainMenuScene 中,你可以在重置和播放按钮被点击时添加音效。当按下重置按钮时,你只需调用 popSound 即可,因为它会毫无问题地播放——这是因为场景没有改变。当按下 playBtn 时,你需要使用 btnPressed 函数,因为场景会瞬间改变,音效将不会播放。所以,确保你创建一个 btnPressed 函数,并在按下播放按钮后延迟调用它,以便完全播放音效。代码如下:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
let _node:SKNode = self.nodeAtPoint(location);
if(_node.name == "playBtn"){
//println("[GameScene] play btn clicked ");
let popSound = SKAction.playSoundFileNamed("pop.mp3", waitForCompletion: true)
self.runAction(popSound)
let buttonPressAction = SKAction.repeatActionForever(SKAction.sequence([SKAction.runBlock(btnPressed),SKAction.waitForDuration(1.0)]))
self.runAction(buttonPressAction)
}
else if (_node.name == "resetBtn"){
let popSound = SKAction.playSoundFileNamed("pop.mp3", waitForCompletion: true)
self.runAction(popSound)
NSUserDefaults.standardUserDefaults().setInteger(0, forKey: "tinyBazooka_highscore")
NSUserDefaults.standardUserDefaults().synchronize()
var currentHighScore = NSUserDefaults.standardUserDefaults().integerForKey("tinyBazooka_highscore")
currentHighScoreLabel.text = "Current High Score: \(currentHighScore)";
}
}
}
添加背景音乐
我们的游戏现在已经充满了音效,但如果我们添加背景音乐将会更加精彩。由于玩家需要不断在 MainMenuSceen 和 GameplayScene 之间切换,使用 SKAction 播放音效是没有意义的,因为它不会正确播放或者每次场景切换时都会被截断。
为了这个目的,我们将不得不使用苹果的音频视觉类 AVFoundation 来帮助我们解决这个问题。正如你可能已经猜到的,AVFoundation 是一个超类,它处理音频和视频功能,所以如果你想在你游戏中播放任何视频(比如教程),你完全可以使用这个类——这就是它被创建的原因。但让我们回到将背景音乐添加到我们的游戏中。
添加音频循环
要创建音频循环,我们必须导入 AVFoundation,并且由于我们必须在游戏开始时立即播放音频轨道,让我们在 GameViewController 类中添加它。所以,在类的顶部添加以下内容,如高亮所示:
import UIKit
import SpriteKit
import AVFoundation
接下来,在创建类之后,我们包含以下高亮显示的行:
class GameViewController: UIViewController {
let bgMusic = AVAudioPlayer(contentsOfURL: NSBundle.mainBundle().URLForResource("bgMusic", withExtension: "mp3"), error: nil)
在这里,我们创建一个新的常量,称为 bgMusic。我们将其赋值为 AVAudioPlayer 函数。它接受两个参数。第一个是文件的存储位置,第二个是要显示的错误信息。
在第一个参数中,我们传递文件名和扩展名,以便可以从主包的资源位置检索文件。在 URLForResource 函数中,我们传递文件名,即 bgMusic,并提供扩展名,即 MP3 格式。这将获取之前在 Build Phases 下的 Copy Bundle Resources 中添加的 bgMusic.mp3 文件。
此外,这需要在视图加载之前检索。如果我们将此行添加到 viewDidLoad 中,则不会导致错误,但文件将无法正确播放,因此它是在 viewDidLoad 函数之外调用的。
一旦文件在 viewDidLoad 函数中加载,添加以下行以告诉文件无限循环并开始播放:
bgMusic.numberOfLoops = -1
bgMusic.play()
通过将循环次数的值设置为 -1,我们告诉它连续循环播放声音文件。如果我们设置为 0,它将只播放一次;如果设置为 1,它将播放声音文件两次;依此类推。
就这样!现在,构建并运行游戏,享受美妙的背景音乐。
现在我们已经在游戏中添加了声音,让我们也添加一个滚动背景来增加趣味性。
视差背景理论
在本节中,我们将添加一个视差或滚动背景。这是游戏中非常流行的效果,其中前景中的对象移动速度比背景中的对象快,背景移动速度较慢,从而产生深度和运动的错觉。这与过去的电影非常相似,其中英雄或主题是静止的,并表现得好像他们在骑马奔跑,背景被循环播放,以产生英雄实际上在场景中向前移动的错觉。
我们将实现一个非常简单的视差效果,其中背景中的所有对象,如树木、灌木丛和草地,将以相同的速度移动。因此,我们只需将背景图像移动并循环播放。
注意
这就是实现视差效果的方法:我们不会使用一个背景图像的精灵,而是将使用两个精灵,并在游戏开始时将它们水平相邻放置,如下面的图像所示。第一个精灵将是可见的,但第二个精灵将位于屏幕外,最初对玩家不可见。
当游戏开始时,两个精灵将以一定的速度向负x方向移动,即向屏幕左侧移动。两个精灵将以相同的速度移动。因此,一旦游戏开始,sprite1将逐渐逐渐离开屏幕,而第二个精灵将开始变得可见。
当第一个精灵完全离开屏幕时,它会迅速移动到第二个精灵的末尾,这是第二个精灵在游戏开始时的相同位置。

然后处理继续在循环中进行。两个精灵总是向屏幕的左侧移动。每个精灵在左侧离开屏幕后,它会被放置在屏幕右侧的离屏位置,并继续向左移动。
在创建用于视差滚动的资源和编码视差效果时,有几个需要注意的事项。首先,当创建用于视差效果的资源时,艺术作品需要是连续的。例如,如果你看前一个图像的中间,你会看到山脉看起来像是一个连续的山脉。尽管sprite1和sprite2是两个不同的图像,但当它们放在一起时,它们看起来像是一个单独的图像。这同样可以在山脉下面的浅绿色灌木丛中看到。灌木丛的左侧在sprite1中,而右侧在sprite2中,但当两个精灵保持相邻时,它给人一种无缝的错觉,好像它是单个灌木丛的一部分。
需要记住的第二点是图像间隙。即使你使图像无缝并且使精灵以相同的速度移动,有时你可能会在精灵之间遇到间隙。这不是一个非常常见的问题,但在某些框架中可能存在。为了应对这个问题,你可以稍微拉伸图像,使精灵相互重叠,这样对玩家来说并不非常明显。另一种方法是确保你手动将精灵放置在屏幕上精灵的末尾,并在必要时进行必要的调整,以弥合精灵之间的间隙。
这是视差滚动的理论基础。让我们在接下来的代码中看看它的实际应用。
实现视差效果
要为背景创建视差效果,我们必须创建一个类似于我们创建MovingSprite类的新类。所以,转到文件 | 新建 | 文件,创建一个名为ParallaxSprite的新 swift 文件。
在文件中,在文件顶部导入 SpriteKit 并创建一些常量。在类中,我们将只取我们想要用于视差效果的文件名。然后我们将从它创建两个名为sprite1和sprite2的精灵。我们将取一个我们想要移动精灵的速度值。然后我们将获取GameplayScene类的实例,以便我们可以将精灵添加到游戏类中。我们还将创建一个全局常量以获取视图的大小:
import Foundation
import SpriteKit
class ParallaxSprite{
let _sprite1: SKSpriteNode!
let _sprite2: SKSpriteNode!
let _speed : CGFloat = 0.0
let _viewSize:CGSize!
} //class end
接下来,我们将为类创建init函数,在这个函数中我们将获取要创建视差效果的精灵的名称、速度和游戏场景,并初始化我们在类顶部创建的常量:
init(sprite1: SKSpriteNode, sprite2: SKSpriteNode, viewSize: CGSize, speed: CGFloat){
_speed = speed
_viewSize = viewSize
_sprite1 = sprite1
_sprite1.position = CGPoint(x: _viewSize.width/2, y: _viewSize.height/2)
_sprite2 = sprite2
_sprite2.position = CGPoint(x: _sprite1.position.x + _sprite2.size.width - 2, y: _viewSize.height/2)
}//init
在init函数中,我们将速度变量初始化为传入的任何值。我们还将分配从我们在GameplayScene类中创建的全局常量中检索到的视图大小。我们还将分配传入的两个精灵名称到本地精灵变量:_sprite1和_sprite2。
_sprite1对象位于视图的中心,因此x和y位置是通过将viewSize的宽度和高度除以二得到的。
对于第二个精灵,_sprite2,高度保持在屏幕高度的一半,但就位置而言,它被放置得使得sprite2的左边缘与sprite1的右边缘重叠。因此,第二个精灵保持在sprite2的x位置等于其宽度。-2是一个小的调整因子,用于确保两个精灵重叠。这是在经过一些尝试和错误后得到的数字。
小贴士
您可以增加或减少此值以查看它的影响,并且如果您愿意,可以根据您的偏好添加更多或更少的重叠。
此外,请注意,精灵不是添加到当前类中,而是添加到GameplayScene类中。我们不能将精灵添加到当前类中,因为我们没有从SKNode或SKSpriteNode继承,所以当前类没有addChild属性。
接下来,我们定义update函数,因为我们需要持续更新sprite1和sprite2的位置。因此,在init函数之后,添加update函数:
func update(){
_sprite1.position.x += _speed
_sprite2.position.x += _speed
if((_sprite1.position.x + _sprite1.size.width/2) < 0){
_sprite1.position = CGPoint(x: _sprite2.position.x + _sprite1.size.width - 2, y: _viewSize.height/2)
}
if((_sprite2.position.x + _sprite2.size.width/2) < 0){
_sprite2.position = CGPoint(x: _sprite1.position.x + _sprite2.size.width - 2 , y: _viewSize.height/2)
}
}//update
在update函数中,我们以一定的速度增加精灵的位置,从而使精灵移动。因此,由于我们是增加而不是减少值,当我们创建这个类的实例时,我们需要记住提供一个负速度值,以便精灵向左移动,否则精灵将开始向正x方向移动。
接下来,我们检查sprite1的右边缘是否已经超过了屏幕的左端。如果精灵已经离开屏幕,我们获取sprite2的位置,将sprite1放置在精灵的末尾,并减去调整因子以避免间隙。对sprite2也执行类似的过程,但在这里我们将它放置在sprite1的末尾。
这就是 ParallaxSprite 类的内容。
要实现这个类,请转到 GameplayScene 类,并在类顶部 var score:Int = 0 之后添加一个全局常量。将名称为 scrollingBg 的 ParallaxSprite 类型输入,如下面的代码所示。别忘了最后的感叹号:
let scrollingBg:ParallaxSprite!
接下来,我们移除在 init 函数中添加的包含 BG 精灵的代码,并替换为以下这些行:
let BG1 = SKSpriteNode(imageNamed: "BG"); self.addChild(BG1);
let BG2 = SKSpriteNode(imageNamed: "BG"); self.addChild(BG2);
scrollingBg = ParallaxSprite(sprite1: BG1, sprite2: BG2, viewSize: viewSize, speed: -5.0)
我们创建了两个精灵常量,分别命名为 BG1 和 BG2,并将图像 BG 传递给它们。接下来,我们初始化 scrollingBg 类。在这个类中,我们传递了想要创建滚动效果的图像名称,即 BG1 和 BG2 精灵。我们给它一个速度 -5.0,并将当前的游戏场景实例传递给它,即 self。
接下来,我们需要调用 ParallaxSprite 类的更新函数,以便更新对象的位子,然后我们就可以拥有我们的滚动背景:
if(!gameOver){
scrollingBg.update()
updateGameObjects()
updateHero()
checkCollision()
}//gameOver check

概述
因此,在本章中,我们添加了一些音乐和效果,以配合游戏玩法。我们还探讨了音频格式的差异以及如何将一种音频格式转换为其他格式。
更重要的是,我们在游戏中添加了视差效果,使其看起来更加生动。就像背景一样,您也可以传递其他对象以创建更好的视差效果。对于免费的版权效果和音乐,您可以访问 www.freesound.org 或 www.soundbible.com。
我们将在第十章 “发布和分发” 中看到如何添加开场屏幕、添加图标、将游戏上传到 App Store 以及为游戏添加最后的修饰。在下一章中,我们将探讨 SpriteKit 的一些更高级的功能,例如物理、光照等。下一章再见!
第七章. 高级 SpriteKit
在本章中,我们将介绍 SpriteKit 中的一些高级功能,例如灯光和物理,这将使游戏开发过程更加容易,并使我们的游戏看起来更漂亮。通过灯光,我们可以创建光源,并使场景中的某些对象产生阴影并受到光的影响。使用物理引擎,我们可以使游戏对象自动受到重力和外部施加的力的作用。
我们还将探讨如何导入用 Objective-C 编写的类,这样如果你已经为 SpriteKit Objective-C 编写了一些类,我们可以轻松地将它们带入 Swift,而无需再次重写代码。
使用导入 Objective-C 类的新知识,我们将探讨诸如 Glyph Designer 和 Spine 等工具。使用 Glyph Designer,我们可以拥有比常规标签占用更少空间和处理能力的自定义字体,而使用 Spine,我们可以创建基于骨骼的动画,这是一种创建动画的更好优化方式。
本章涵盖的主题如下:
-
灯光和阴影
-
Sprite Illuminator
-
物理
-
Objective-C 在 Swift 中
-
Glyph Designer
-
骨骼动画
灯光和阴影
我们可以使用 LightNode 在 SpriteKit 中创建光源。LightNode 可以像精灵节点一样放置在场景中,通过将其添加到场景中。
要创建光源,打开 MainMenuScene 类,并在我们添加背景到场景之后添加以下内容:
let lightNode = SKLightNode()
lightNode.position = CGPoint(x: viewSize.width * 0.5, y: viewSize.height * 0.75)
lightNode.categoryBitMask = 1
lightNode.falloff = 0.25
lightNode.ambientColor = UIColor.whiteColor()
lightNode.lightColor = UIColor(red: 1.0, green: 1.0, blue: 0.0, alpha: 0.5)
lightNode.shadowColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.3)
addChild(lightNode)
与我们创建 SKSpriteNode 类似,我们在 Swift 中创建 SKLightNode 来创建灯光。我们通过在宽度上居中并放置在屏幕高度的四分之三处来定位它。
如果你现在构建并运行游戏,你会看到场景没有任何变化。这是因为我们必须明确告诉场景哪些对象应该受到光源的影响。这是通过将类别掩码位赋给光源来完成的,这样我们就可以稍后去对象那里告诉它受到特定掩码位的光源的影响。在这里,我们赋值掩码为 1。由于掩码位使用 UINT32,因此一个场景中一次可以有总共 32 个光源,因为整数中有 32 位或 4 个字节。
通过将 LightNode 的掩码类别设置为 1,我们表示这个光源的第一个位是开启的。所以,在分配类别时,你不会使用一般的整数数字来定义一个类别。因此,灯光的类别应该是 1、2、4、8、16 等等。在这里,1 表示第一个位是开启的,2 表示第二个位是开启的,4 表示第三个位是开启的,以此类推。不要使用像 1、2、3、4 等一般的数字来定义类别,因为这会导致意外的结果。
在创建光源时,我们还需要提供有关光源的其他信息,例如衰减。像所有光源一样,它在源头具有最大强度,并且你离源头越远,强度就越弱。衰减决定了光源强度下降的速度。1的值表示它永远不会失去其强度值,而0表示它会立即失去。
除了掩码和衰减之外,我们还需要提供光源的颜色、环境光和阴影的颜色。对于光和阴影的颜色,我们给出白色和黑色。注意,在阴影颜色中,我们降低了不透明度的值,否则阴影将完全黑色。对于环境光,我们将蓝色变量降低到零,因为背景中已经有足够的蓝色。然后,我们将光源添加到场景中。
要使对象实际上受到光源的影响,我们必须将该对象的lightBitMask属性分配给之前分配的光源的分类掩码。
因此,要使背景受到光源的影响,在将背景添加到场景后并在代码中添加以下内容,然后运行游戏以查看结果,如下所示图像:
BG.lightingBitMask = 1

哇!这不是很漂亮吗?这里我将字体颜色改为蓝色,以便更容易看到光源。这可以通过在myLabel中添加以下高亮行来完成:
let myLabel = SKLabelNode(fontNamed:"Chalkduster")
myLabel.text = "Ms.TinyBazooka"
myLabel.fontSize = 65
myLabel.position = CGPoint(x: viewSize.width/2, y: viewSize.height * 0.8)
myLabel.fontColor = UIColor.blueColor()
self.addChild(myLabel)
现在要投射阴影,我们只需要在你想投射阴影的对象上调用shadowCastBitMask。将光源的分类掩码分配给它,以便可以投射阴影。
我们将要求播放按钮精灵图像投射阴影,因此,在将播放按钮添加到场景后,添加以下代码:
self.addChild(playBtn)
playBtn.name = "playBtn"
playBtn.shadowCastBitMask = 1
现在,播放按钮会根据光源的位置投射阴影。
在场景中创建光源和阴影时,有一些事情需要记住。只有精灵可以在 SpriteKit 场景中投射阴影。我尝试将其应用于标签,但它无法从文本中创建阴影。
如果你正在制作一个受光源影响的对象,确保它永远不会被删除并重新添加到场景中。如果对象被移除,光源必须重新计算场景中的照明,这个过程在对象被重新添加到场景中时再次执行。这将在场景中重新计算光和阴影时引起闪烁。
注意
最好将阴影添加到你知道将来不会删除的对象上。
此外,在GameViewController中,将ignoreSiblingOrder更改为false:
skView.ignoresSiblingOrder = false
说到这里,让我们运行场景,看看阴影的效果,如下所示图像:

这很棒。但如果我们能在场景中添加一些投射阴影的移动云层,那就太好了。此外,由于云层在移动,我们可以使用视差精灵类使背景像我们在上一章中做的那样滚动。我们还可以添加一个太阳精灵并使其旋转。
为了完成本章的内容,我们将为天空创建一个单独的精灵,并将背景精灵的云层移除,因为我们将会添加一个新的云层精灵,并使用移动精灵类将其移动,使其在场景中循环。因此,前往本章的资源文件夹,进入照明目录,获取天空、太阳、云层和新的背景图像。创建名为天空、太阳和云层的图像资源。用新的背景精灵替换旧的背景精灵。
在MainMenuScene类中,首先添加天空精灵,如下所示:
let sky = SKSpriteNode(imageNamed: "sky", normalMapped: true)
sky.position = CGPoint(x: viewSize.width/2, y: viewSize.height/2)
self.addChild(sky)
然后,按照以下方式将太阳精灵添加到场景中:
let sun = SKSpriteNode(imageNamed: "sun")
sun.position = CGPoint(x: viewSize.width * 0.5,y: viewSize.height * 0.75)
addChild(sun)
sun.lightingBitMask = 1
sun.runAction(SKAction.repeatActionForever(SKAction.rotateByAngle(1, duration: 1)))
在将太阳添加到场景后,使其受到光线的影响。然后,我们在其上创建一个新的repeatActionForever变量,使其每秒旋转太阳精灵一度,永远旋转。
接下来,在MainMenuScene类的顶部添加以下全局变量:
var scrollingBg:ParallaxSprite!
var cloud:MovingSprite!
var cloudNode:SKSpriteNode!
由于我们希望在update函数中调用scrollingBg、cloudNode和云对象的更新方法,因此我们希望它们是全局变量。
现在,在didMoveToView方法中初始化类和变量。
首先按照以下方式创建cloudNode:
cloudNode = SKSpriteNode(imageNamed:"cloud");
cloudNode.position = CGPoint(x: viewSize.width/2, y: viewSize.height * 0.9)
addChild(cloudNode)
接下来,在cloudNode上初始化shadowcaste属性,并将其添加到云移动精灵变量中,以便我们稍后更新其位置:
cloudNode.shadowCastBitMask = 1
cloud = MovingSprite(sprite: cloudNode, speed: CGPointMake(-3.0, 0.0))
与背景相比,我们使云层移动得较慢,因为背景以-5.0的速度移动。如果我们让两者以相同的速度移动,那么它们将看起来像是一张图片,并且一起移动。
在此之下,添加光源。
在此之后,通过创建两个名为BG1和BG2的精灵图像来初始化scrollingBg类。将它们添加到场景中,并在两个背景精灵上初始化lightingBitMask。
接下来,通过传递背景精灵、viewsize和速度来初始化scrollingBg类,如下所示:
let BG1 = SKSpriteNode(imageNamed: "BG")
self.addChild(BG1)
let BG2 = SKSpriteNode(imageNamed: "BG")
self.addChild(BG2)
BG1.lightingBitMask = 1
BG2.lightingBitMask = 1
scrollingBg = ParallaxSprite(sprite1: BG1, sprite2: BG2, viewSize: viewSize, speed: -5.0)
接下来,我们必须更新scrollingBg、云层和云节点对象的位置。因此,添加一个更新函数并添加以下内容:
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
scrollingBg.update()
cloud.moveSprite()
if((cloudNode.position.x + cloudNode.size.width/2) < 0){
cloudNode.position.x = viewSize.width + cloudNode.size.width/2
}
}
在这里,我们调用scrollingBg和云对象的update和moveSprite函数。我们还通过将cloudNode精灵的X位置设置为屏幕右侧,一旦图像超过左侧,来更新cloudNode精灵的位置,如图所示:

我们必须注意对象添加到场景中的顺序,以确保阴影正确投射。顺序如下:
-
sky -
sun -
云(
cloudNode) -
lightNode -
scrollingBg(BG1和BG2)
这很重要,因为阴影是在光源的深度投射,而不是在对象的深度。
小贴士
在前面的图像中,尽管我们要求光线照在播放按钮上,但由于背景层在上面,我们没有看到像早期截图中的阴影。如果你将lightNode移到最顶层,你会看到播放按钮投下阴影,但即使云层在背景层后面,它们的阴影也会在背景层的树木上显现,这看起来很奇怪。因此,在哪个层添加光源是很重要的。
你还必须注意你想要光线影响的图像的大小。你会发现我没有在天空图像上启用lightBitMask属性,因为它将游戏的 FPS 降低到 45,我的 iPadMini Retina 上的一切都运行得很慢。所以我禁用了天空层的照明,只保留了运行和背景精灵的照明。但即使在 iPad3 上,这仍然会导致卡顿和减速,所以我不得不完全禁用所有图像的照明,以保持一致的 60 FPS。
添加光照和阴影非常依赖于硬件和处理器,所以在你的游戏中实现它们时,请确保在所有设备上进行足够的测试,以确保无论游戏在哪个设备上运行,都能以平滑的 FPS 运行。
这就是关于光照的所有内容;我们将转到GameplayScene类,看看物理,并了解如何实现它。当我们在本章后面查看 Glyph Designer 和 Spine 时,我们将返回到MainMenuScene。
在下一节中,我们将看到如何通过替换我们之前创建的小型物理引擎来添加物理,该引擎用于创建重力以拉下玩家或推起玩家。
Sprite Illuminator
虽然 SpriteKit 会自动添加法线图,但你也可以指定自己的法线图以获得更好的效果。要下载 Sprite Illuminator,请访问www.codeandweb.com/spriteilluminator并下载 DMG 文件。
下载 Sprite Illuminator 后,双击 DMG 文件,并将应用程序复制到应用程序文件夹。转到启动盘,启动应用程序。一旦应用程序启动,你应该会看到以下截图中的窗口:

让我们详细看看界面。
在左侧,我们有精灵面板。中间面板是预览面板,其中你可以看到你所做的所有更改的预览。右侧的面板称为工具面板。我们将进一步深入了解这些面板中的每一个。
精灵面板
在精灵面板中,你可以按如下方式修改精灵、全局光和显示模式:
-
精灵:在这里,我们可以添加或移除需要创建法线图的精灵。
-
全局光照:我们可以通过影响其
Z位置、亮度、光照颜色和环境颜色来改变全局光源的属性。这只是为了可视化目的,在游戏中,我们将在 SpriteKit 中添加光源并改变其属性。 -
显示模式:默认情况下,光照纹理模式是开启的。这会显示图像在启用光照和法线贴图时的预览。纹理模式将仅显示纹理而不显示光照和法线贴图。光照表面将显示带有光源的图像但不显示法线贴图。法线贴图模式将仅显示为图像创建的法线贴图。
预览面板
预览面板将根据你在显示模式部分选择的模式显示图像预览。
工具面板
我们将在工具面板上花费大部分时间。这是我们将为任何图像创建高度图的地方。工具面板包括效果和画笔和工具:
在效果类别下,使用斜面和浮雕工具我们可以给地图添加高度或深度。这正好与 Photoshop 中的相同工具的工作方式相同。
在画笔类别下,包含五种画笔。这些画笔包括高度、角度、结构、平滑和擦除:
-
高度:这可以添加到一个你希望看起来从图像中凸出的区域。你可以为它指定画笔大小、高度、硬度、轮廓和方向的值。
-
角度:这可以添加到一个可能以特定角度相对于光源方向的区域。这在开发等距瓦片地图时非常有用。除了指定画笔大小、不透明度和硬度的值外,我们还可以通过选择球体周围的区域来指定表面的方向。
-
结构:这个画笔用于在某个区域添加一些粗糙的纹理。你可以修改画笔的大小、密度、高度和方向。
-
平滑度:这个画笔用于平滑某个区域。可以通过滑块控制画笔大小和平滑度。
-
擦除:如果我们想擦除效果或画笔,可以使用擦除画笔。
提供了一些选择工具,可以使选择过程变得简单一些。所以,如果你想在一个特定区域应用画笔或效果,你可以使用选择工具隔离该区域,并在该区域应用效果:
-
** wand/颜色**:这个工具类似于 Photoshop 中的 wand 工具。你还可以调整容差级别。
-
多边形:你可以使用这个工具在图像的某个区域周围创建一个闭合的多边形环路。
-
移动:你可以使用这个工具移动图像。
一旦你对你的创作满意,你可以点击发布按钮,并为你的图像创建一个法线贴图。法线贴图将是一个 PNG 文件,文件名与原始文件相同,后缀为 _n。
使用精灵照明器,我为英雄精灵创建了一个法线贴图(如下截图所示)。文件和资源包含在本章的Resources文件夹中。

在Resources目录的Sprite Illuminator文件夹中,我为两种尺寸的常规图像和法线贴图创建了两个版本。我还重命名了文件以避免任何名称冲突。将这些四个文件拖入项目中。
要在GamePlayScene.swift文件中使用这些资源,注释掉添加和动画英雄精灵的旧代码,并添加以下突出显示的代码。
在这里,我们不是在图像集中加载英雄文件,而是将heroSI和heroSI_n纹理分配给英雄变量。
此外,我们将英雄的lightBitmask属性设置为1,以便光源影响它。
//hero = SKSpriteNode(imageNamed: "hero");
//hero.position = CGPoint(x: viewSize.width/4, y: viewSize.height/2)
//self.addChild(hero)
hero = SKSpriteNode(texture: SKTexture(imageNamed: "heroSI"), normalMap: SKTexture(imageNamed: "heroSI_n"));
hero.position = CGPoint(x: viewSize.width/4, y: viewSize.height/2)
self.addChild(hero)
hero.lightingBitMask = 1
/*
let heroAtlas = heroAnim()
let heroIdleAnimArray = heroAtlas.hero_Idle_()
let animaiton = SKAction.animateWithTextures(heroIdleAnimArray, timePerFrame: 0.2)
let animate = SKAction.repeatActionForever(animaiton)
hero.runAction(animate)
*/
物理
在其他框架中,你很可能会需要导入你选择的物理引擎库,例如 Box2d 或 chipmunk,并配置它以使其正常工作。你还需要编写自定义代码以使碰撞检测工作。在 SpriteKit 中,一旦创建场景,每个场景都会在后台运行物理效果。你不需要做任何事情来使其工作。因此,在游戏场景中,我们将禁用我们创建的物理引擎,并用 SpriteKit 的内置物理引擎替换它。
打开gameplayScene.swift文件,在update函数中注释掉updateHero函数。如您所记得,updateHero函数负责让英雄受到重力影响,确保英雄始终在屏幕内,并在玩家点击屏幕左侧时应用推力。使用内置的物理引擎,我们将看到我们如何让它为我们完成所有工作。
如我之前所说,物理效果已经激活,这意味着场景中已经存在一些正在作用的引力。因此,让我们让英雄受到重力的影响。
在init函数中,在我们将英雄精灵添加到场景之后,添加以下行:
hero.physicsBody = SKPhysicsBody(rectangleOfSize: hero.size)
这就是我们告诉英雄受到物理影响所需做的所有事情。任何精灵的physicsBody属性都会将一个物体分配给精灵,使其表现得像一个固体物体,这意味着现在这个精灵将具有与现实生活中任何物体相同的物理属性。它将具有密度,对摩擦力做出反应,具有弹性,并受到其他物体的作用。现在我们可以施加力或以一定的速度移动它。如果移动的物体撞击这个物体,正如牛顿所说,它会对此撞击做出反应并移动。
当我们分配一个物体时,我们可以选择给物体赋予形状。在这里,我们给物体赋予了一个与精灵相同尺寸的矩形。如果我们愿意,我们也可以给物体赋予圆形的形状,或者我们可以使用自定义形状。
如果我们现在构建并运行游戏,我们会看到英雄会直接穿过屏幕底部。这是因为没有任何东西阻止英雄通过。为了对抗这种向下运动,我们必须在场景周围创建一个物理体,以防止英雄掉落。
在init函数中,添加以下代码:
self.physicsBody = SKPhysicsBody(
edgeLoopFromRect: CGRect(x: 0,
y: 0,
width: viewSize.width,
height: viewSize.height))
之前,我们给英雄添加了一个物理体,而现在我们正在初始化场景本身的物理体属性。这里的区别在于它是一个edgeLoop类型的物理体。边环体与常规物理体不同,因为前者没有体积。所以,它没有质量、密度、摩擦等,你不能施加力或使其以速度移动。但其他物体将会受到它的影响,这意味着如果一个常规物体在移动并被edgeLoop阻挡,那么常规物体将停止移动。

在创建边环时,我们必须传递环的形状,因此这里我们提供了从屏幕左下角的原点开始的矩形形状,即(0, 0),并通过传递屏幕的宽度和高度来获取视图的大小。
现在,如果你构建并运行,你会看到英雄停止了。事实上,英雄不仅停止了,而且一旦她碰到屏幕底部,还会弹跳一下。你可以根据需要让角色有多重或弹跳,通过改变以下所示的价值来设置质量、密度、摩擦和恢复值。所有这些值范围从0到1:0表示更不弹跳、更密集、受摩擦影响等,而1则是光谱的另一端。
hero.physicsBody?.restitution = 0
hero.physicsBody?.friction = 0
hero.physicsBody?.density = 0
hero.physicsBody?.mass = 0
你也可以更改场景中的引力值,如果你想制作一个在月球上的关卡,你可以将默认引力更改为原始值的 1/6,以产生那种效果。
关于物理引擎的一个重要注意事项是它不是以像素为单位,而是以真实世界值为单位。例如,重力的默认值实际上是 9.8 米/(秒 * 秒)。实际上所有以像素为单位的值都被转换为米,SpriteKit 内部进行像素到米的转换。

注意
要有类似月球的引力,访问场景物理世界属性的引力属性,并将引力更改为 9.8 的 1/6,如下所示:
self.physicsWorld.gravity = CGVector(dx: 0, dy: -1.64)
gravity属性期望一个CGVector值,因为我们希望引力施加向下的力,所以x的值为零,y保持为从默认值-9.8的-1.64,这是月球上的引力。你可以将其改回-9.8以获得更地球的感觉。
现在,我们可以对英雄施加一个力,这样我们就可以将她推到空中。因此,在 touchesBegan 函数中,我们可以移除之前添加的将英雄推到空中的代码,并施加一个向上的物理力。但在我们施加力之前,我们必须将英雄的速度设置为零,因为英雄必须克服向下的速度才能向上移动。如果负 y 方向的速度太大,那么,无论你向上施加多少力,都会被作用在英雄身上的向下重力力所抵消。首先将英雄的向下速度设置为零,然后向上施加力。以下是一个推力代码示例:
//thrust.y = 15.0
将其替换为以下代码:
hero.physicsBody?.velocity = CGVectorMake(0, 0)
hero.physicsBody?.applyImpulse(CGVectorMake(0, 300))
在这里,我们将英雄的速度设置为 0。由于没有在 x 方向上作用的力,所以设置 Y 值为 0 或同时将 X 和 Y 值都设置为 0 都没有关系。
接下来,我们在 y 方向上施加一个 X 值为 0、Y 值为 300 的冲量。但等等!
注意
什么是这个冲量?
所有这些时间我们都在谈论施加力,而现在我们实际上是在施加一个冲量。在物理引擎中,有一个单独的属性称为力,其工作原理是,一旦你对一个物体施加力,这个力就会持续作用于物体。我们只想在点击屏幕左半部分后一次性施加力。因此,我们施加的是冲量而不是力。如果我们想在点击屏幕后让玩家继续向上移动,我们应该使用力属性而不是冲量。所以,确保你想要你的对象如何表现,并相应地对你的物理对象施加力或冲量。
就这样。现在玩家表现与我们之前使用自己的物理引擎时完全一样。运行游戏并测试它。
Objective-C 在 Swift 中
让我们看看如何将 Objective-C 类导入 Swift 并使用它们。然后我们将使用这些工具,如具有 Objective-C 实现但 Swift 中没有特定类的 Glyph Designer 和 Spine。
为了让 Objective-C 类与 Swift 兼容,你需要创建一个桥接头文件。该文件通常按照约定命名为 <ProjectName>-Bridging-Header.h,然后你需要在项目的 Build Settings 下的 Swift Compiler – Code Generation 中添加文件位置到 Objective-C Bridging Header。
注意
如果你使用这本书附带提供的代码,你可能需要修改路径以避免编译错误。
通常,如果你在 Swift 项目中创建一个新的头文件,你会看到一个窗口询问是否将当前创建的文件作为桥接头文件处理,Xcode 将自动将文件位置添加到 Build Settings 中。如果它没有弹出,我们将必须按照以下步骤进行操作,以确保 Xcode 知道在哪里查找桥接头文件,手动进行操作。

要为当前项目创建桥接头文件,请转到文件 | 新建 | 文件,然后在iOS下选择源,并选择带有“H”的头文件。将文件命名为skGame-Bridging-Header,然后点击创建。
现在,转到构建设置,在搜索框中输入bridging,如图所示。双击Objective-C 桥接头右侧,并将我们刚刚创建的项目中的桥接头文件拖放到框中。按Enter键。现在,项目知道了桥接头文件的位置。我们可以使用这个文件来调用 Objective-C 类的头文件,以便它们可以被 Swift 共享。

Glyph Designer
Glyph Designer 是一个可以用来制作游戏字体应用程序。但我们不是已经有了 SKLabel 吗?这是真的。但 SKLabel 从系统中获取字体,并在运行时将字体文件转换为图像,然后将其显示在屏幕上。所以,每当分数需要增加时,系统都需要将字体转换为图像,然后将其显示在屏幕上。这与我们之前遇到的精灵问题非常相似,我们使用了 Texture Packer 来解决这个问题。
虽然你可以使用系统字体来制作大型游戏,但使用位图字体会更好,位图字体中字母和数字已经转换为图像,而不是每次都进行转换。因此,使用 Glyph Designer,我们可以创建位图字体,并使用它来更好地优化游戏。
位图字体类似于精灵图,其中包含所有字母、数字和符号的图像,并且这个图像文件将附带一个包含符号和字母位置和大小的数据文件。每当需要在屏幕上显示字母时,数据文件将检查字母的位置,然后检索并显示在屏幕上。
可以从71squared.com/glyphdesigner下载应用程序的试用版。

下载应用程序后,你可以打开它,它将创建一个新的未命名项目。在左侧面板中,你会看到系统中所有字体列表。中心视图显示了将要创建的文件精灵图。这是一个预览窗口,会根据你做的更改动态变化。
在选择左侧要修改的字体后,你将在右侧面板中进行大部分更改。在右侧面板中,你会找到以下标题(如图所示):
-
符号信息
-
纹理图集
-
符号填充
-
符号笔画
-
符号阴影
-
包含的符号

你应该主要关注符号填充、笔画和阴影:
-
Glyph Fill:我们可以选择填充类型,即实心、渐变或图像。所以,基本上我们在这里可以选择字体的颜色。
-
Glyph Stroke:这将围绕字母创建一个新的描边效果。在这里,你可以选择描边的颜色和大小。
-
Glyph Shadow:在这里,我们可以选择阴影的颜色和方向。有两种类型的阴影:内阴影和外阴影。这种效果会给字母带来一些深度感。
一旦你对更改满意,点击 导出 按钮并选择格式。你可以选择 skf @2 版本或正常的 skf 版本来生成 2x 和 1x 分辨率的文件。

实现位图字体
确保选择 .skf 作为导出类型。这将创建一个以 .atlas 结尾的文件夹,其中包含所有字符和符号,以及与字体相关联的数据文件 .skf。
将 .atlas 文件夹和 .skf 文件拖入项目。
现在我们已经准备好在游戏中实现位图字体:
-
为了让 Glyph Designer 与 SpriteKit 兼容,我们需要使用 71Squared 创建的通用静态库。请访问
71squared.zendesk.com/hc/en-us/articles/200037472-Using-Glyph-Designer-1-8-with-Sprite-Kit,并从页面底部下载libSSBitmapFont.zip文件。 -
文件下载完成后,解压并将
SSBitmapFont.h和SSBitmapFontLableNode.h文件拖入项目。不要拖动包含文件的文件夹,只需拖动单个文件。同时,将libSSBitmapFont.a文件拖入项目。 -
前往我们之前创建的桥接头文件,并在桥接头文件中导入两个头文件
SSBitmapFont.h和SSBitmapFontLabelNode.h,如下所示:#ifndef skGame_skGame_Bridging_Header_h #define skGame_skGame_Bridging_Header_h #endif #import "SSBitmapFont.h" #import "SSBitmapFontlabelNode.h"现在,我们可以在 Swift 项目的任何地方访问这些文件。
-
为了确保一切正常工作,只需构建项目以确保没有错误弹出。
-
在
MainMenuScene类中,我们将使用位图字体来显示游戏名称,而不是使用SKLabelNode。打开MainMenuScene类。在类的顶部添加以下代码。这与我们在GameViewController类中获取bgMusic.mp3文件的方式类似。但在这里,我们将从项目的主包位置获取 SKF 文件。var bmFontFile = SSBitmapFont(file: NSBundle.mainBundle().URLForResource("skGame_font", withExtension: "skf"), error: nil) -
在这里,我们创建了一个名为
bmFontFile的新变量,并使用从 Objective-C 导入的SSBitmapFont类,从项目位置获取skGame_font字体文件的名称。除了名称外,我们还需要提供skf文件的扩展名。 -
由于我们已经保存了
bmFont文件,我们可以通过传入文本并指定位置,然后将它添加到场景中,来使用此文件创建新的文本或标签。因此,我们用以下代码替换了我们之前添加的SKLabelNode代码,以查看位图字体的实际效果:let bmFontText = bmFontFile.nodeFromString("Ms.TinyBazooka") bmFontText.position = CGPoint(x: viewSize.width/2, y: viewSize.height * 0.8) addChild(bmFontText) -
我们创建了一个新的常量
bmFontText,并使用bmFontFile的nodeFromString属性将Ms.TinyBazooka文本分配给它。我们像往常一样设置位置,并将bmFontText添加到场景中:![实现位图字体]()
您可以立即看到,位图字体比使用SKLableNode创建的文本要清晰得多。因此,毫不奇怪,如今,无论是专业公司还是独立开发者,都使用位图字体。
骨骼动画
在前面的章节中,我们看到了如何使用基于帧的动画在游戏中制作动画,其中我们导入了一系列图像,并通过循环帧来创建动画。尽管基于帧的动画很好,但制作起来可能会很繁琐。艺术家必须绘制每一帧,如果您想保持包的大小低,则不能有太多的帧。因此,动画看起来不太流畅。此外,如果您想对角色进行一些修改,那么艺术家就必须回到画板前,因为他必须通过动画的所有帧并重新绘制它们。
使用骨骼动画技术而不是为动画制作单独的帧,我们导入角色的各个部分并制作一个精灵图集,如下所示:

使用应用程序,我们定位角色的各个部分,然后创建动画。这样,我们可以从身体部分创建不同的动画,如行走、跑步、跳跃、攻击等,所有动画都导出为数据文件。
当数据文件和角色部分被带入游戏中时,数据文件将被用来放置角色的各个部分以形成角色的姿势。稍后,当我们调用动画在角色上播放时,数据文件将再次被用来动态地创建运动。让我们看看如何创建骨骼动画。
为了创建动画,我们将使用 Esoteric Software 公司的一个名为Spine的应用程序。您可以从他们的网站esotericsoftware.com/下载试用版。

试用版不允许您保存项目,但我已经包含了本章的资源项目文件,以便您可以在试用版中打开它并对其进行操作。
从网站上下载 DMG 文件后,双击它进行安装。安装完成后,点击屏幕左上角的 spine 标志创建一个新项目,如下面的截图所示。
接下来,我们必须引入角色的各个部分,以便我们可以为动画正确地摆姿势。这个阶段被称为角色设置。
在脊柱文件夹中本章的资源中,你会找到一个名为heroParts的文件夹,将此文件夹复制到桌面上。
当你打开 Spine 时,默认项目将被加载。要创建一个新项目,请点击屏幕左上角的 Spine 图标,然后选择新建项目。

在右侧的树面板上,在层次面板中,选择图像文件夹,如图所示。在底部,你现在可以选中heroParts文件夹的路径。点击浏览并指向桌面上的另一个heroParts文件夹。
所有部分都将出现在图像选项卡下。接下来,将所有部分拖动到选中的视图面板中。你可以点击单个部分,并在屏幕底部的变换面板中使用旋转和平移按钮来放置英雄的部分。如果某些部分需要放在其他部分之前,那么在层次面板中点击绘制顺序旁边的三角形,并将一个图像拖动以使其出现在任何其他对象之上或之下。
视图面板中心的加号是根节点。通过在屏幕底部的补偿面板中点击图像按钮锁定图像,将此节点移动到角色的中间。你可以通过点击它来重命名根节点。为了方便起见,将其重命名为hip。
一旦角色设置正确,它应该看起来像以下图像。参考以下截图以检查绘制顺序:

接下来我们将绘制骨骼。骨骼的工作方式与人类骨骼非常相似。你可以将角色的一个或多个部分附着到骨骼上,然后当你移动或旋转骨骼时,角色部分将相应地移动或旋转。
首先,我们将为腿部创建骨骼。要创建骨骼,请点击屏幕底部工具面板上带有骨骼图标的创建按钮。现在我们将从臀部创建一个到左脚的骨骼。左键点击臀部开始创建骨骼。在仍然按住左鼠标按钮的同时,将鼠标移动到左脚图像上。在左脚上方按下键盘上的Shift键。一旦左脚被高亮显示,释放鼠标按钮和Shift键。
现在为另一只脚重复此过程。从臀部开始,并按下Shift键,当另一只脚被高亮显示时,释放鼠标和Shift键。如果骨骼没有与脚完美对齐,这是可以的。
再次,从臀部创建一个骨骼到身体,使其更接近手的起始位置,但确保身体被突出显示,而不是手。在层次结构面板中将这个骨骼命名为body。现在,从这个新创建的骨骼的末端创建一个用于手的骨骼和另一个用于角色头部的骨骼。
你会注意到火箭筒仍然连接在臀部或根骨骼上。在层次结构面板中,将火箭筒节点移动到手骨骼下。这样,火箭筒就会随着手骨骼移动。以下截图显示了完成后的骨骼层次结构:

现在你可以点击单个骨骼,旋转它们,当你旋转骨骼时,身体部分也会随之旋转。这里展示了角色的不同姿势。
注意
确保在转置面板中点击了旋转按钮,并且图像没有被锁定。

现在,角色已经设置好,准备进行动画制作。点击视图面板上的设置按钮。你现在处于动画模式。一个新面板将在下面打开,称为Dopesheet。在层次结构面板下点击动画标题,然后在屏幕底部点击新动画。将打开一个新窗口,要求输入名称,在这里输入walk并点击确定。
除了新的Dopesheet面板外,你还会看到变换面板也略有变化。如果你点击任何骨骼,旋转、平移和缩放按钮旁边会出现一个绿色键符号,如下面的截图所示:

这些键记录了该骨骼旋转、平移和缩放按钮所做的更改。绿色表示没有记录任何更改。在 0 帧的变换面板中点击三个绿色按钮。现在,值已经被记录,因为键已经从绿色变为红色。现在,将蓝色的时间线滑块从 dopesheet 中的 0 帧移动到 5 帧。再次,键是绿色的。稍微旋转腿部,然后再次点击所有三个键使它们变为红色。
如果你将时间轴滑块在 0 帧和 5 帧之间移动,你会看到腿部来回旋转。点击播放按钮查看动画效果。点击播放按钮右侧的循环按钮以循环动画。这就是创建动画的方法。
我们将创建一个简单的行走循环动画,稍后将其导入游戏并播放动画。撤销所有操作以回到原始姿势。
为了创建行走循环,我们在第 0 帧、第 6 帧、第 12 帧、第 18 帧和第 24 帧记录了所有骨骼的位置、旋转和缩放。第 0 帧和第 24 帧的姿势相同。第 12 帧的姿势与第 0 帧相反,因为脚的位置将会互换,这意味着在第 0 帧中原本在后面的脚将移到前面,反之亦然。在第 6 帧和第 18 帧,将脚重新放在一起,并通过移动臀部/根骨来提升角色。
以下截图显示了不同帧的姿势。从左到右,第一个姿势是第 0 帧和第 24 帧的。因此,旋转腿骨分开,选择所有骨骼,并通过点击所有三个绿色关键按钮来创建关键。保持相同的姿势,将时间滑块移动到第 24 帧,然后再次点击关键按钮。
中间的图像显示了第 12 帧的姿势,此时腿已经交换。因此,再次旋转腿,并将时间滑块移动到第 12 帧,然后点击关键按钮。
右侧的图像显示了第 6 帧和第 18 帧的姿势。在这里,将腿靠近,并通过向上移动臀部骨骼来提升角色。将时间滑块移动到第 6 帧并创建关键帧。为了创建第 18 帧的关键帧,对姿势不做任何更改。只需将时间滑块移动到第 18 帧并创建一个新的关键帧即可。就这样;你的行走循环就准备好了。点击播放按钮并享受吧!
确保在点击绿色按钮记录帧时选择了所有骨骼。

现在,如果你拥有 Spine 的精华版或专业版,你可以通过点击左上角的 Spine 图标,然后点击导出来导出数据文件。
导出的数据类型将是.json类型。选择你想要导出数据文件的位置,并将其他值保留为默认值。点击导出按钮来导出 JSON 数据文件。
当你导出文件时,你会注意到数据文件被命名为 skeleton。请手动将文件重命名为player,因为每次文件默认命名为 skeleton,我们不希望这个文件在为其他角色创建 JSON 文件时被覆盖。
要为脊动画创建图像的图集,创建一个名为player.atlas的文件夹,并将heroParts文件夹中的所有角色部分复制到其中。
现在将player.atlas和player.json文件拖入项目。
为了对角色进行动画处理,我们需要 Spine 运行时。与 Glyph Designer 类似,它是用 Objective-C 编写的,但正如我们之前所做的那样,我们将导入头文件到桥接头文件中,并使 Objective-C 类在 Swift 中可访问。
要获取头文件,请访问github.com/mredig/SGG_SKSpineImport,下载 ZIP 文件并解压。从解压的文件夹中,转到SpineImporter文件夹,并将文件夹中的所有文件拖到 Swift 项目中。
在Bridging Header文件中,添加SpineImport.h文件,如下所示:
#import "SSBitmapFont.h"
#import "SSBitmapFontlabelNode.h"
#import "SpineImport.h"
在MainMenuScene类中,我们将添加玩家动画。在类的顶部,创建一个全局变量 hero,并将其分配给SGG_Spine类,如下所示:
var hero = SGG_Spine()
现在,在将BG1和BG2添加到场景之后,添加以下代码:
hero.skeletonFromFileNamed("player",
andAtlasNamed: "player",
andUseSkinNamed: nil)
hero.position = CGPoint(x: viewSize.width/4, y: viewSize.height * 0.25)
hero.xScale = 1.25; hero.yScale = 1.25;
hero.runAnimation("walk", andCount: -1)
addChild(hero)
我们在英雄的skeletonFromFile属性中提供了 JSON 数据文件的名称和图集的名称。由于我们在游戏中没有使用任何皮肤,第三个参数保持为nill。
然后,我们将英雄变量定位并稍微增加其缩放比例。
为了告诉要播放哪个动画,我们使用英雄的runAnimation属性,并分配我们在 spine 中创建的行走动画。
最后,我们将英雄添加到场景中。构建并运行游戏,查看以下截图所示的最终结果:

摘要
在本章中,您看到了如何不费吹灰之力地为我们的游戏添加光照和阴影。由于 Apple 将其包含在 SpriteKit 中,您可以确信创建效果的代码已经得到了很好的优化。在其他框架中,这个效果需要由开发者编写,并且开发者需要有良好的经验才能制作出优化的光照和阴影效果。
我们还简要介绍了 SpriteKit 的物理引擎,并用它替换了我们自制的物理引擎。在这里,我们对物理引擎的可能性几乎只是触及了皮毛。有了良好的知识和经验,我们可以制作出自己的愤怒的小鸟克隆版。
除了 SpriteKit 的光照和物理引擎之外,我们还看到了如何将 Objective-C 代码引入 Swift,并利用它实现如 Glyph Designer 和 Spine 等工具。Glyph Designer 和 Spine 都是专业工具,对于游戏开发者和设计师来说是绝对必须的。它们真的在很大程度上帮助优化和简化了游戏开发过程。
是时候向 TinyBazooka 小姐说再见了,因为在接下来的两个章节中,我们将进入 3D 游戏开发的世界。但我们在第十章(Chapter 10. 发布和分发)中会回到 SpriteKit,我们将看到如何将这款游戏发布到 App Store。
第八章. SceneKit
所以,这就是全部!最后,我们从 2D 世界过渡到 3D。使用 SceneKit,我们可以轻松地制作 3D 游戏,特别是由于 SceneKit 的语法与 SpriteKit 非常相似。
当我们说 3D 游戏时,并不意味着你可以戴上 3D 眼镜来玩游戏。在 2D 游戏中,我们主要在 x 和 y 坐标上工作。在 3D 游戏中,我们处理所有三个轴x、y和z。
此外,在 3D 游戏中,我们有不同类型的灯光可以使用。SceneKit 还内置了物理引擎,它将处理重力等力,并有助于碰撞检测。
我们还可以在 SceneKit 中使用 SpriteKit 来添加 GUI 和按钮,以便我们可以添加分数和交互性。所以,本章有很多内容要介绍。让我们开始吧。
本章涵盖的主题如下:
-
使用 SCNScene 创建场景
-
向场景添加对象
-
从外部 3D 应用程序导入场景
-
向场景添加物理效果
-
添加敌人
-
检查碰撞检测
-
添加 SpriteKit 叠加层
-
添加触摸交互
-
完成游戏循环
-
添加墙壁和地板的视差效果
-
添加粒子
使用 SCNScene 创建场景
首先,我们创建一个新的 SceneKit 项目。它与创建其他项目非常相似。但这次,请确保您从游戏技术下拉列表中选择 SceneKit。不要忘记为语言字段选择Swift。选择iPad作为设备,然后点击下一步以在所选目录中创建项目,如下面的截图所示:

创建项目后,打开它。点击GameViewController类,删除viewDidLoad函数中的所有内容,删除handleTap函数,因为我们将创建一个单独的类,并添加触摸行为。
创建一个名为GameSCNScene的新类,并导入以下头文件。从SCNScene类继承,并添加一个名为init的函数,该函数接受一个名为view的参数,其类型为SCNView:
import Foundation
import UIKit
import SceneKit
class GameSCNScene: SCNScene{
let scnView: SCNView!
let _size:CGSize!
var scene: SCNScene!
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init(currentview view: SCNView) {
super.init()
}
}
此外,创建两个新的常量scnView和_size,分别对应类型SCNView和CGSize。另外,添加一个名为scene的变量,其类型为SCNScene。
由于我们正在制作 SceneKit 游戏,我们必须获取当前视图,其类型为SCNView,类似于我们在 SpriteKit 中获取视图的方式,我们在 SpriteKit 中将当前视图类型转换为SKView。
我们创建一个_size常量来获取当前视图的大小。然后我们创建一个新的变量scene,其类型为SCNScene。SCNScene是 SceneKit 中用于创建场景的类,类似于我们使用SKScene在 SpriteKit 中创建场景的方式。
小贴士
Swift 会自动提示创建required init函数,所以我们不妨将其包含在类中。
现在,转到GameViewController类,创建一个名为gameSCNScene的全局变量,其类型为GameSCNScene,并在viewDidLoad函数中将其赋值,如下所示:
class GameViewController: UIViewController {
var gameSCNScene:GameSCNScene!
override func viewDidLoad() {
super.viewDidLoad()
let scnView = view as SCNView
gameSCNScene = GameSCNScene(currentview: scnView)
}
}// UIViewController Class
太好了!现在我们可以在GameSCNScene类中添加对象。最好将所有代码移动到单个类中,这样我们可以保持GameSceneController类整洁。
在GameSCNScene类的init函数中,在super.init函数之后添加以下代码:
scnView = view
_size = scnView.bounds.size
// retrieve the SCNView
scene = SCNScene()
scnView.scene = scene
scnView.allowsCameraControl = true
scnView.showsStatistics = true
scnView.backgroundColor = UIColor.yellowColor()
在这里,我们首先将当前视图分配给scnView常量。接下来,我们将_size常量设置为当前视图的尺寸。
接下来,我们初始化场景变量。然后,将场景分配给scnView的场景。接下来,启用allowCameraControls和showStatistics。这将使我们能够控制相机并在场景中移动它,以便更好地查看场景。此外,启用统计信息后,我们将看到游戏的表现,以确保帧率(FPS)保持稳定。
scnView的backgroundColor属性使我们能够设置视图的颜色。我将其设置为黄色,以便在场景中更容易看到对象,如以下截图所示。设置好所有这些后,我们可以运行场景。

嗯,目前还不是那么令人印象深刻。需要注意的一点是,我们还没有添加相机或灯光,但我们仍然看到了黄色的场景。这是因为虽然我们还没有向场景中添加任何内容,但 SceneKit 自动为创建的场景提供了默认的灯光和相机。
添加场景中的对象
让我们接下来向场景添加几何形状。在 SceneKit 中,我们可以轻松创建一些基本几何形状,如球体、盒子、圆锥体、环面等。让我们首先创建一个球体并将其添加到场景中。
将球体添加到场景中
在类中创建一个名为addGeometryNode的函数,并在其中添加以下代码:
func addGeometryNode(){
let sphereGeometry = SCNSphere(radius: 1.0)
sphereGeometry.firstMaterial?.diffuse.contents = UIColor.orangeColor()
let sphereNode = SCNNode(geometry: sphereGeometry)
sphereNode.position = SCNVector3Make(0.0, 0.0, 0.0)
scene.rootNode.addChildNode(sphereNode)
}
对于创建几何形状,我们使用SCNSphere类创建球体形状。我们还可以调用SCNBox、SCNCone、SCNTorus等来分别创建盒子、圆锥体或环面形状。
在创建球体时,我们必须提供一个半径作为参数,这将确定球体的大小。尽管为了放置形状,我们必须将其附加到一个节点上,这样我们就可以放置并添加它到场景中。
因此,创建一个新的常量sphereNode,其类型为SCNNode,并将球体几何形状作为参数传递。为了定位节点,我们必须使用SCNvector3Make属性通过提供x、y和z的值在 3D 空间中放置我们的对象。
最后,要将节点添加到场景中,我们必须调用scene.rootNode将sphereNode添加到场景中,这与 SpriteKit 不同,在 SpriteKit 中我们只需使用addChild将对象添加到场景中。
球体添加后,让我们运行场景。别忘了在init函数中添加self.addGeometryNode()。
我们确实添加了一个球体,那么为什么我们会看到一个圆(如以下截图所示)呢?嗯,SceneKit 使用的基灯光源只是使我们能够看到场景中的对象。如果我们想看到实际的球体,我们必须改进场景的光源。

添加光源
让我们创建一个名为 addLightSourceNode 的新函数,如下所示,以便我们可以向场景添加自定义灯光:
func addLightSourceNode(){
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = SCNLightTypeOmni
lightNode.position = SCNVector3(x: 10, y: 10, z: 10)
scene.rootNode.addChildNode(lightNode)
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = SCNLightTypeAmbient
ambientLightNode.light!.color = UIColor.darkGrayColor()
scene.rootNode.addChildNode(ambientLightNode)
}
我们可以向球体对象添加一些光源,以便看到一些深度。在这里,我们添加两种类型的光源。第一种是泛光灯。泛光灯从一个点开始,然后光线均匀地向所有方向散射。我们还添加了一个环境光源。环境光是被其他物体反射的光,例如月光。
注意
还有两种类型的灯光:方向光和聚光灯。聚光灯很容易理解,我们通常在需要将某个对象(如舞台上的歌手)吸引注意时使用它。如果你想使光线朝一个单一方向传播,例如阳光,则使用方向光。太阳离地球非常远,当我们看到光线时,光线几乎是平行的。
在创建光源时,我们创建一个名为 lightNode 的节点,其类型为 SCNNode。然后,我们将 SCNLight 分配给 lightNode 的灯光属性。我们将泛光灯类型分配给灯光的类型。我们将光源的位置分配为在所有三个 x、y 和 z 坐标上的 10。然后,我们将它添加到场景的根节点。
接下来,我们在场景中添加一个环境光。这个过程的前两个步骤与创建任何光源的步骤相同:
-
对于灯光类型,我们必须将
SCNLightTypeAmbient分配给环境类型的光源。由于我们不希望光源非常强烈,因为它会被反射,所以我们分配一个darkGrayColor给颜色。 -
最后,我们将光源添加到场景中。
没有必要将环境光源添加到场景中,但它会使场景的阴影更柔和。你可以移除环境光源以查看差异。
在 init 函数中调用 addLightSourceNode 函数。现在,构建并运行场景,以查看具有适当照明的实际球体,如下面的截图所示:

你可以在屏幕上放置一个手指并移动它来旋转相机,因为我们已经启用了相机控制。你可以使用两个手指平移相机,你可以双击以将相机重置到其原始位置和方向。
添加相机到场景
接下来,让我们向场景添加一个相机,因为默认相机非常靠近。在类中创建一个名为 addCameraNode 的新函数,并在其中添加以下代码:
func addCameraNode(){
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
scene.rootNode.addChildNode(cameraNode)
}
在这里,我们再次创建一个名为 cameraNode 的空节点。我们将 SCNCamera 分配给 cameraNode 的相机属性。接下来,我们将相机定位,使 x 和 y 值保持在零,并在 z 方向上将相机向后移动 15 个单位。然后,我们将相机添加到场景的根节点。在 init 函数的底部调用 addCameraNode。
在这个场景中,原点位于场景中心,与 SpriteKit 中场景的原点始终位于场景右下角不同。在这里,正x和y是从中心向右和向上的。正z方向朝向你。

我们没有将球体移回或缩小其大小,这纯粹是因为我们在场景中向后移动了相机。
让我们接下来创建一个地板,这样我们就可以更好地理解场景中的深度。同时,通过这种方式,我们将学习如何在场景中创建地板。
添加地板
在类中添加一个名为addFloorNode的新函数,并添加以下代码:
func addFloorNode(){
var floorNode = SCNNode()
floorNode.geometry = SCNFloor()
floorNode.position.y = -1.0
scene.rootNode.addChildNode(floorNode)
}
为了创建地板,我们创建了一个名为floorNode的变量,其类型为SCNNode。然后我们将SCNFloor分配给floorNode的几何属性。对于位置,我们将y值设置为-1,因为我们希望球体出现在地板上方。最后,像往常一样,我们将floorNode分配给场景的根节点。
在下面的屏幕截图中,我已经旋转了相机以展示整个场景的动作。在这里,我们可以看到地板是灰色的,球体在地面上投射出它的反射,我们还可以看到球体顶部左边的明亮泛光灯。

从外部 3D 应用程序导入场景
虽然我们可以通过代码添加对象、相机和灯光,但当场景中添加了很多对象时,这会变得非常繁琐和混乱。在 SceneKit 中,这个问题可以通过导入其他 3D 应用程序预先构建的场景来轻松解决。
所有 3D 应用程序,如 3D StudioMax、Maya、Cheetah 3D 和 Blender,都有导出 Collada(.dae)和 Alembic(.abc)格式场景的能力。我们可以直接将这些场景导入 SceneKit,其中包含灯光、相机和纹理化对象,无需设置场景。
在本节中,我们将导入一个 Collada 文件到场景中。在本章的资源文件夹中,你可以找到monsterScene.DAE文件。将此文件拖动到当前项目中。

除了 DAE 文件外,还需要将monster.png文件添加到项目中,否则你将只看到场景中未纹理化的怪物网格。
点击monsterScene.DAE文件。如果纹理化的怪物没有自动加载,请将monster.png文件从项目中拖动到预览窗口中的怪物网格。当鼠标悬停在怪物网格上并出现一个(+)符号时,释放鼠标按钮。现在你将能够正确地看到怪物被纹理化。
左侧的面板显示了场景中的实体。在实体下方显示场景图,右侧的视图是预览窗格。
实体显示场景中的所有对象,场景图显示了这些实体之间的关系。如果你有某些对象是其他对象的子对象,场景图将它们显示为树状结构。例如,如果你打开CATRigHub001旁边的三角形,你将看到其下的所有子对象。
你可以使用场景图来移动和旋转场景中的对象以微调你的场景。你还可以添加节点,这些节点可以通过代码访问。你可以看到我们已经在场景中有一个相机和一个聚光灯。你可以选择每个对象,并使用对象的轴点上的箭头将其移动。
你还可以通过在预览场景上点击并拖动左鼠标按钮来旋转场景以获得更好的视角。为了缩放,上下滚动鼠标滚轮。要平移,按住键盘上的Alt键,然后左键点击并拖动预览窗格。
注意
需要注意的是,在预览窗格中进行旋转、缩放和平移实际上并不会移动你的相机。相机仍然处于相同的位置和角度。要从前视图查看,再次从预览窗格的下拉列表中选择Camera001选项,视图将重置为相机视图。
在预览窗口的底部,我们可以选择通过相机或聚光灯查看视图,或者点击并拖动以旋转自由相机。如果你在场景中有多于一个相机,那么在下拉列表中你将看到Camera002、Camera003等等。
在预览面板中的视图选择下拉菜单下方,你还有一个播放按钮。如果你点击播放按钮,你可以在预览窗口中查看怪物的默认动画。
预览面板就是这样;它只是为了帮助你更好地理解场景中的对象。它绝不是 3DSMax、Maya 或 Blender 等常规 3D 软件的替代品。
你可以在场景图中创建相机、灯光和空节点,但无法添加如盒子或球体这样的几何形状。你可以添加一个空节点并将其定位在场景图中,然后在代码中添加几何形状并将其附加到该节点上。
现在我们已经了解了场景图,让我们看看我们如何在 SceneKit 中运行这个场景。
在init函数中,删除我们初始化场景的行,并添加以下行代替。同时删除我们之前添加的对象、灯光和相机。
init(currentview view:SCNView){
super.init()
scnView = view
_size = scnView.bounds.size
//retrieve the SCNView
//scene = SCNScene()
scene = SCNScene(named: "monsterScene.DAE")
scnView.scene = scene
scnView.allowsCameraControl = true
scnView.showsStatistics = true
scnView.backgroundColor = UIColor.yellowColor()
// self.addGeometryNode()
// self.addLightSourceNode()
// self.addCameraNode()
// self.addFloorNode()
//
}
构建并运行游戏以查看以下截图:

你将看到怪物在跑动,以及我们最初分配给场景的黄色背景。在导出场景时,如果你导出了动画,一旦场景在 SceneKit 中加载,动画就会自动播放。
此外,你还会注意到我们已经删除了场景中的相机和灯光。那么,为什么默认的相机和灯光没有加载到场景中呢?
这里发生的情况是,在我导出文件时,我在场景中插入了一个相机,并添加了一个聚光灯。因此,当我们将文件导入场景时,SceneKit 自动理解场景中已经存在一个相机,因此它将使用该相机作为其默认相机。同样,场景中已经添加了一个聚光灯,它被视为默认光源,并且相应地计算照明。
向场景添加对象和物理
现在我们来看看我们如何访问场景图中的每个对象,并为怪物添加重力。在本章的后面部分,我们将看到如何添加一个触摸界面,通过这个界面我们可以通过施加向上的力使英雄角色跳跃。
访问英雄对象并添加物理身体
因此,创建一个名为addColladaObjects的新函数,并在其中调用一个addHero函数。创建一个名为heroNode的全局变量,其类型为SCNNode。我们将使用此节点来访问场景中的英雄对象。在addHero函数中,添加以下代码:
init(currentview view:SCNView){
super.init()
scnView = view
_size = scnView.bounds.size
//retrieve the SCNView
//scene = SCNScene()
scene = SCNScene(named: "monster.scnassets/monsterScene.DAE")
scnView.scene = scene
scnView.allowsCameraControl = true
scnView.showsStatistics = true
scnView.backgroundColor = UIColor.yellowColor()
self.addColladaObjects()
// self.addGeometryNode()
// self.addLightSourceNode()
// self.addCameraNode()
// self.addFloorNode()
}
func addHero(){
heroNode = SCNNode()
var monsterNode = scene.rootNode.childNodeWithName(
"CATRigHub001", recursively: false)
heroNode.addChildNode(monsterNode!)
heroNode.position = SCNVector3Make(0, 0, 0)
let collisionBox = SCNBox(width: 10.0, height: 10.0,
length: 10.0, chamferRadius: 0)
heroNode.physicsBody?.physicsShape =
SCNPhysicsShape(geometry: collisionBox, options: nil)
heroNode.physicsBody = SCNPhysicsBody.dynamicBody()
heroNode.physicsBody?.mass = 20
heroNode.physicsBody?.angularVelocityFactor = SCNVector3Zero
heroNode.name = "hero"
scene.rootNode.addChildNode(heroNode)
}
首先,我们在init函数中调用addColladaObjects函数,如高亮所示。然后我们创建addHero函数。在其中我们初始化heroNode。然后,为了实际移动怪物,我们需要访问CatRibHub001节点来移动怪物。我们通过scene.rootNode.ChildWithName属性来获取访问权限。对于我们希望通过代码访问的每个对象,我们都需要使用场景的rootNode的ChildWithName属性,并传入对象的名称。
如果递归设置为true,SceneKit 将通过所有子节点来获取该对象。由于我们正在寻找的节点就在顶部,所以我们将其设置为false以节省处理时间。
我们创建了一个临时变量monsterNode。在下一步中,我们将monsterNode变量添加到heroNode中。然后我们将英雄节点的位置设置为原点。
为了使heroNode能够与场景中的其他物理身体交互,我们必须为heroNode的物理身体分配一个形状。我们可以使用怪物的网格,但形状可能计算不正确,而一个盒子比怪物的网格简单得多。为了创建一个盒子碰撞器,我们创建了一个新的盒子几何体,其宽度、高度和深度大致与怪物的尺寸相匹配。
然后,使用heroNode.physicsBody.physicsShape属性,我们为其分配了我们为其创建的collisionBox的形状。由于我们希望物体受到重力的影响,我们将物理身体类型设置为动态。稍后我们将看到其他身体类型。
由于我们希望物体对重力有高度的反应,我们将物体的质量值设置为20。在下一步中,我们将所有三个方向的angularVelocityFactor设置为0,因为我们希望当施加垂直力时,物体能够直接上下移动。如果我们不这样做,物体将会翻来覆去。
我们还将名称hero分配给怪物,以检查碰撞的对象是否是英雄。这在我们检查与其他物体的碰撞时将非常有用。
最后,我们将heroNode添加到场景中。
将addColladaObjects添加到init函数中,如果你还没有做的话,请注释或删除self.addGeometryNode、self.addLightSourceNode、self.addCameraNode和self.addFloorNode函数。然后,运行游戏以查看怪物缓缓下落。
我们将在怪物正下方创建一小块地面,这样它就不会掉下来。
添加地面
创建一个名为addGround的新函数,并添加以下内容:
func addGround(){
let groundBox = SCNBox(width: 10, height: 2,
length: 10, chamferRadius: 0)
let groundNode = SCNNode(geometry: groundBox)
groundNode.position = SCNVector3Make(0, -1.01, 0)
groundNode.physicsBody = SCNPhysicsBody.staticBody()
groundNode.physicsBody?.restitution = 0.0
scene.rootNode.addChildNode(groundNode)
}
我们创建了一个名为groundBox的新常量,其类型为SCNBox,宽度为10,长度为10,高度为2。倒角是盒子边缘的圆角。由于我们不想对角落进行任何圆角处理,所以它被设置为0。
接下来,我们创建一个名为groundNode的SCNNode,并将groundBox分配给它。我们将其放置在原点稍下方。由于盒子的高度为2,我们将其放置在-1.01处,这样当怪物静止在地面上时,heroNode将是(0, 0, 0)。
接下来,我们分配一个静态类型的物理体。由于我们不想让英雄在掉到地面上时弹起,我们将恢复系数设置为0。最后,我们将地面添加到场景的根节点。
我们将这个物体设置为静态而不是动态的原因是,动态物体会受到重力和其他力的作用,而静态物体则不会。因此,在这个场景中,尽管重力向下作用,英雄会掉落,但groundBox不会,因为它是一个静态物体。
你会看到物理语法与 SpriteKit 非常相似,包括静态物体和动态物体、重力等。而且,与 SpriteKit 一样,当我们运行场景时,物理模拟会自动开启。
在addColladaObjects函数中添加addGround函数,并运行游戏以查看怪物受到重力影响并在接触地面后停止。

添加敌人节点
要在 SceneKit 中检查碰撞,我们可以检查英雄和地面之间的碰撞。但让我们让它更有趣,并学习一种新的物体类型:运动学物体。
为了这个目的,我们将创建一个新的名为enemy的盒子,并使其移动并与英雄碰撞。创建一个新的全局SCNNode,命名为enemyNode,如下所示:
let scnView: SCNView!
let _size:CGSize!
var scene: SCNScene!
var heroNode:SCNNode!
var enemyNode:SCNNode!
此外,在类中创建一个新的函数addEnemy,并在其中添加以下内容:
func addEnemy(){
let geo = SCNBox(width: 4.0,
height: 4.0,
length: 4.0,
chamferRadius: 0.0)
geo.firstMaterial?.diffuse.contents = UIColor.yellowColor()
enemyNode = SCNNode(geometry: geo)
enemyNode.position = SCNVector3Make(0, 20.0 , 60.0)
enemyNode.physicsBody = SCNPhysicsBody.kinematicBody()
scene.rootNode.addChildNode(enemyNode)
enemyNode.name = "enemy"
}
这里没有什么特别的地方!就像添加groundNode时,我们创建了一个所有边长为四单位的立方体。我们还为其材质添加了黄色。然后在函数中初始化enemyNode。我们将节点沿x、y和z轴定位。将身体类型设置为运动学而非静态或动态。然后我们将身体添加到场景中,并最终将enemyNode命名为enemy,这是我们检查碰撞时需要的。在我们忘记之前,在调用addHero函数之后,在addColladaObjects函数中调用addEnemy函数。
注意
运动学身体与其他身体类型的区别在于,就像静态一样,外部力不能作用于身体,但我们可以对运动学身体施加力来移动它。
在静态身体的情况下,我们看到了它不受重力影响,即使我们对其施加力,身体也不会移动。
在这里,我们不会应用任何力来移动敌人方块,而只是像我们在 SpriteKit 游戏中移动敌人一样移动对象。所以,这就像是制作同样的游戏,但是在 3D 而不是 2D 中,这样你就可以看到,尽管我们有第三维度,但游戏开发的原则同样适用于两者。
为了移动敌人,我们需要为敌人添加一个update函数。因此,让我们通过创建一个updateEnemy函数并将其添加到其中来将其添加到场景中:
func updateEnemy(){
enemyNode.position.z += -0.9
if((enemyNode.position.z - 5.0) < -40){
var factor = arc4random_uniform(2) + 1
if( factor == 1 ){
enemyNode.position = SCNVector3Make(0, 2.0 , 60.0)
}else{
enemyNode.position = SCNVector3Make(0, 15.0 , 60.0)
}
}
}
在update函数中,类似于我们在 SpriteKit 游戏中移动敌人的方式,我们将敌人节点的Z位置增加 0.9。不同的是,我们是在移动z方向。
当敌人超过z方向的-40时,我们重置敌人的位置。为了给玩家创造额外的挑战,当敌人重置时,会随机选择1和2之间的一个数字。如果是1,则敌人放置得更靠近地面,否则它放置在离地面 15 个单位的位置。
之后,我们将为英雄添加一个跳跃机制。因此,当敌人靠近地面时,英雄必须跳过敌人盒子,但如果敌人以高度生成,英雄则不应该跳跃。如果他跳跃并击中敌人盒子,那么游戏就结束了。之后我们还将添加一个计分机制来记录分数。
为了更新敌人,我们实际上需要一个更新函数来添加enemyUpdate函数,以便敌人移动并且其位置重置。因此,在类中创建一个名为update的函数,并在其中调用updateEnemy函数,如下所示:
func update(){
updateEnemy()
}
更新场景中的对象
在 SceneKit 中,有一个在场景渲染后调用的更新函数。我们将使用此函数来调用我们游戏的update函数。在GameViewController类中,添加以下函数:
func renderer(aRenderer: SCNSceneRenderer, updateAtTime time: NSTimeInterval) {
gameSCNScene.update()
}
要调用rendererUpdateAtTime函数,GameViewController类需要继承自SCNSceneRendererDelegate。因此,在创建类的地方,添加以下代码:
class GameViewController: UIViewController, SCNSceneRendererDelegate {
接下来,在viewDidLoad函数中,将当前代理设置为 self,如下所示:
let scnView = view as SCNView
scnView.delegate = self
注意
代理是代码设计模式的一部分。使用代理,一个类可以让另一个类获得访问权限并执行其部分职责。在这里,SceneRenderer通过将代理设置为 self 来将scnView委托出去。
rendererUpdateAtTime函数是一个系统函数,在场景中所有对象渲染完毕后被调用。因此,一旦场景渲染完毕,场景中的对象就可以更新,否则可能会导致伪影。
现在,如果我们构建并运行游戏,我们会看到enemyBox被更新。
但存在问题,当盒子击中英雄时,英雄从他的祭坛上被撞飞。这是因为,首先,英雄是一个动态体,所以外部力会影响他。其次,尽管我们手动移动盒子而没有施加任何力,但我们仍在移动盒子,并且 SceneKit 计算了一些惯性力,所以一旦盒子击中英雄,能量就转移到英雄身上,它就像施加在英雄身上的外部力一样,使英雄开始移动。
由于我们使用heroNode.physicsBody?.angularVelocityFactor = SCNVector3Zero限制了英雄的旋转,当盒子击中他时,他不会旋转。如果我们注释或删除该行,英雄会因为盒子的撞击而旋转。

当我们检查碰撞时,我们将修复这个问题。当发生碰撞时,我们将英雄和盒子的位置重置到它们的初始位置。因此,让我们接下来看看如何检查碰撞。
检查物体之间的接触
SceneKit 的物理引擎具有内置的函数,当启用物理时检查物体之间的接触。当两个物体即将接触时,会触发接触。
对于检查接触,我们必须使用physicsWorld的didBeginContact函数;因此,将以下代码添加到类中。同时,我们必须继承自SCNPhysicsContactDelegate并将GameSCNScene类设置为接触代理。
注意
类似于在GameViewController中调用RenderDelegate,为了能够通过物理引擎在提供的GameSCNScene中接收“接触”事件并能够操作它们,GameSCNScene类继承自SCNPhysicsContactDelegate,并且场景的contactDelegate被设置为self。
因此,在类的顶部,继承自SCNPhysicsContactDelegate:
class GameSCNScene: SCNScene,SCNPhysicsContactDelegate{
并且,在init函数中,添加以下内容以设置当前类为接触代理:
scene.physicsWorld.contactDelegate = self
接下来,将didBeginContact函数添加到类中,如下所示:
func physicsWorld(world: SCNPhysicsWorld, didBeginContact contact: SCNPhysicsContact) {
if( (contact.nodeA.name == "hero" &&
contact.nodeB.name == "enemy") )
{
contact.nodeA.physicsBody?.velocity = SCNVector3Zero
println("contact")
}
}
当两个物理对象发生碰撞时,节点存储在contact变量中。由于我们已经为我们的物理体对象命名,我们检查第一个物体是否是英雄的,而英雄碰撞的另一个物体是否是敌人的。如果是,我们将打印出contact并也将第一个物体的速度设置为零。
由于我们的游戏规模较小,我们可以推测身体A将是英雄,而身体B将是敌人。在更大型的游戏中,每秒发生许多碰撞,可能很难确定哪个是身体A,哪个是身体B。在这种情况下,我们将不得不检查两种情况,即身体A是否是敌人还是英雄,以及身体B的相反情况,然后做出必要的结论。
因此,现在我们已经添加了英雄和敌人,并且我们设置了碰撞。接下来,我们将看到如何在我们的 3D 场景上实现 2D 覆盖层,以便我们可以显示按钮和得分,并为我们的简单游戏添加游戏结束条件。
添加 SpriteKit 覆盖层
为了显示游戏得分和按钮,我们将添加一个 2D SpriteKit 层。为了添加覆盖层,创建一个名为OverlaySKscene的类。在这个类中,添加以下内容:
import SpriteKit
class OverlaySKScene: SKScene {
let _gameScene: GameSCNScene!
let myLabel: SKLabelNode!
var gameOverLabel: SKLabelNode!
var jumpBtn: SKSpriteNode!
var playBtn: SKSpriteNode!
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init(size: CGSize, gameScene: GameSCNScene){
super.init(size: size)
}
}
为了导入 SpriteKit,我们必须创建一个 SpriteKit 的子类。创建全局变量GameSCNScene、SKLabelNodes和SpriteNodes类型。在这里,我们创建了两个LabelNodes:一个用于显示得分,另一个用于显示“游戏结束”文本。我们还创建了两个spriteNodes:一个用于播放按钮,另一个用于跳跃按钮。
我们添加所需的init函数和默认的init函数。默认的init将接受场景的大小和GameSCNScene类的引用作为参数。
在init函数中,我们初始化超类。
添加标签和按钮
接下来,在init函数中,添加以下代码:
_gameScene = gameScene
myLabel = SKLabelNode(fontNamed:"Chalkduster")
myLabel.text = "Score: 0";
myLabel.fontColor = UIColor.whiteColor()
myLabel.fontSize = 65;
myLabel.setScale(1.0)
myLabel.position = CGPointMake(size.width * 0.5, size.height * 0.9)
self.addChild(myLabel)
gameOverLabel = SKLabelNode(fontNamed:"Chalkduster")
gameOverLabel.text = "GAMEOVER";
gameOverLabel.fontSize = 100;
gameOverLabel.setScale(1.0)
gameOverLabel.position = CGPointMake(size.width * 0.5, size.height * 0.5)
gameOverLabel.fontColor = UIColor.whiteColor()
self.addChild(gameOverLabel)
gameOverLabel.hidden = true
playBtn = SKSpriteNode(imageNamed: "playBtn")
playBtn.position = CGPoint(x: size.width * 0.15, y: size.height * 0.2)
self.addChild(playBtn)
playBtn.name = "playBtn"
jumpBtn = SKSpriteNode(imageNamed: "jumpBtn")
jumpBtn.position = CGPoint(x: size.width * 0.9, y: size.height * 0.15)
self.addChild(jumpBtn)
jumpBtn.name = "jumpBtn"
jumpBtn.hidden = true
在init函数中,首先,我们将传递给_gameScene属性的gameScene设置为gameScene。
接下来,我们初始化scoreLabel和gameOverLabel。我们设置text、color、textsize和position的值,并将其添加到场景中。在gameOverLabel中,我们将hidden设置为true,因为我们只想在游戏结束后显示文本。
然后,我们初始化我们在 SpriteKit 中制作的播放和跳跃按钮。我们将跳跃按钮精灵设置为隐藏,因为我们希望它在播放按钮被点击并开始游戏时才显示。跳跃和播放按钮的图像在章节的资源文件夹中提供。
添加触摸交互性
为了添加触摸交互性,我们将使用与我们在 SpriteKit 中一直使用的类似方式的touchesBegan函数。因此,我们获取触摸位置和触摸下精灵的名称。如果精灵名称是jumpBtn且gameOver布尔值为false,则我们在gameScene类中调用heroJump函数。如果gameOver为true并且如果播放按钮被点击,则我们在 SceneKit 类中调用startGame函数。
因此,添加以下函数以检测触摸:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
let _node:SKNode = self.nodeAtPoint(location);
if(_gameScene.gameOver == false){
if(_node.name == "jumpBtn"){
_gameScene.heroJump()
}
}else{
if(_node.name == "playBtn"){
_gameScene.startGame()
}
}
}
}
这就是 SpriteKit 类的全部内容。我们将在 SceneKit 类中添加gameOver布尔值、heroJump和startGame函数。代码在创建之前会显示一些错误,所以现在请忽略这些错误。
完成游戏循环
返回 SceneKit 类,并在类顶部导入 SpriteKit,如下所示:
import UIKit
import SceneKit
import SpriteKit
此外,创建一个名为 skScene 的全局变量,类型为 OverlaySKScene。在 SceneKit 类中添加一个名为 addSpriteKitOverlay 的新函数,并添加以下代码:
func addSpriteKitOverlay(){
skScene = OverlaySKScene(size: _size, gameScene: self)
scnView.overlaySKScene = skScene
skScene.scaleMode = SKSceneScaleMode.ResizeFill
}
在这里,我们初始化之前创建的 skScene 全局变量,并传入当前场景的大小和当前 SceneKit 类。接下来,我们将 skScene 类分配给 scnView 的 overlaySKScene 属性。最后,我们将 skScene 变量的 scaleMode 设置为 SKSceneScaleMode.ResizeFill 类型。
最后,在 addColladaObjects 函数中调用 addSpriteKitOverlay 函数,如下所示:
func addColladaObjects(){
addHero()
addGround()
addEnemy()
addSpriteKitOverlay()
}
使英雄跳跃
我们仍然需要向类中添加布尔值和函数,以便我们的游戏能够运行。因此,在类顶部创建一个名为 gameOver 的全局变量,并将其设置为 true。
接下来,创建一个名为 heroJump 的新函数,如下所示:
func heroJump(){
heroNode.physicsBody?.applyForce(SCNVector3Make(0, 1400, 0), impulse: true)
}
在这里,我们对 heroNode 应用一个向上的力 1400 单位在 y 方向上。
接下来,创建 gameStart 函数,如下所示,并将其添加到类中:
func startGame(){
gameOver = false
skScene.jumpBtn.hidden = false
skScene.myLabel.hidden = false
skScene.playBtn.hidden = true
skScene.gameOverLabel.hidden = true
score = 0
skScene.myLabel.text = "Score: \(score)"
}
将 gameOver 布尔值设置为 false。我们将跳跃按钮和 scoreLabel 设置为可见,并隐藏播放按钮和 gameOverlabel。
为了跟踪和显示得分,我们需要一个得分变量,因此创建一个名为 score 的全局变量,类型为 int,并在类顶部将其初始化为 0。同样,在 startGame 函数中,将值设置为 0,以便每次函数被调用时值都会重置。此外,我们将 scoreLabel 文本设置为反映游戏开始时的当前得分。
对于游戏中的得分,每次敌人方块超出屏幕并重置时,我们将增加分数。如果方块击中英雄,则游戏结束。
因此,在 enemyUpdate 函数中,在我们检查敌人的 Z 位置是否小于 -40 后,添加以下突出显示的行以更新得分和 scoreLabel 文本:
func updateEnemy(){
enemyNode.position.z += -0.9
if((enemyNode.position.z - 5.0) < -40){
var factor = arc4random_uniform(2) + 1
if( factor == 1 ){
enemyNode.position = SCNVector3Make(0, 2.0 , 60.0)
}else{
enemyNode.position = SCNVector3Make(0, 15.0 , 60.0)
}
score++
skScene.myLabel.text = "Score: \(score)"
}
}
设置游戏结束条件
在 didBeginContact 函数中,在我们将玩家的速度重置为零后,添加以下代码:
gameOver = true
GameOver()
在这里,我们将 gameOver 设置为 true 并调用一个 GameOver 函数,我们将设置标签和按钮的可见性。因此,向 SceneKit 类添加一个名为 GameOver 的新函数,如下所示:
func GameOver(){
skScene.jumpBtn.hidden = true
skScene.playBtn.hidden = false
skScene.gameOverLabel.hidden = false
enemyNode.position = SCNVector3Make(0, 2.0 , 60.0)
heroNode.position = SCNVector3Make(0, 0, 0)
}
在这里,一旦游戏结束,我们将隐藏 jumpButton 并显示 playButton 和 gameOverLabel。然后我们将敌人英雄的位置重置到初始状态。
接下来,我们必须确保 enemyUpdate 函数仅在 gameOver 为 false 时被调用。在 update 函数中,将 enemyUpdate 函数包含在一个 if 语句中,如下所示:
if(!gameOver){
updateEnemy()
}
最后,我们必须调整场景中的重力,否则英雄会被抛到空中,因为当前的重力非常低。在 addColladaObjects 函数中,在函数末尾添加以下行:
scene.physicsWorld.gravity = SCNVector3Make(0, -300, 0)
现在我们的游戏循环已经准备好了。如果你按下播放按钮,游戏将开始,跳跃按钮将可见,并且当按下时英雄将跳跃。每次英雄成功避开敌人方块时,分数都会增加,如果英雄碰到敌人方块,游戏将结束。游戏结束后,播放按钮将可见,跳跃按钮将隐藏。再次轻触播放按钮将重置一切,游戏将再次开始。
修复跳跃
尽管如此,仍然有一个问题。你可以一直按跳跃按钮,英雄会一直向上跳。我们不想这样。我们希望英雄只有在落地时才能跳跃。为此,我们将添加一个小计数器,并在英雄在空中时禁用跳跃。
为了做到这一点,添加一个名为jumpCounter的新全局变量,其类型为int,并将其初始化为0。在类的update函数中添加以下内容:
jumpCounter--
if(jumpCounter < 0 ){
jumpCounter = 0
}
在这里,我们减少jumpCounter的值,一旦它小于零,我们就将其值设置为0。
接下来,在heroJump函数中,将我们对英雄施加跳跃力量的部分放在以下 if 条件中:
if(jumpCounter == 0){
heroNode.physicsBody?.applyForce(SCNVector3Make(0,
1400,
0),
impulse: true)
jumpCounter = 25
}
现在,英雄只有在jumpCounter等于 0 时才会跳跃。如果它等于0,那么就会施加力量,并将计数器设置为25。
注意
这个数字是通过试错得出的,以确保怪物在空中时不会按下跳跃按钮。
在update函数中,我们减少这个值,直到那时不能施加力量。一旦jumpCounter再次设置为0,英雄就可以再次跳跃。
因此,最终,我们可以运行并测试游戏。确保在addColladaObjects函数中调用addSpriteKitOverlay函数。

添加墙壁和地板视差
没有视差效果的游戏是什么?在 SpriteKit 中,我们使用精灵添加了视差,而在 SceneKit 中,我们将使用平面将其添加到场景中。除了添加视差之外,我们还将了解如何向平面添加漫反射、法线和高光贴图。我们还将学习这些术语的含义。
所以,像往常一样,我们创建一个新的函数,在其中我们将添加所有这些平面。按照以下方式添加四个全局SCNNodes:
var parallaxWallNode1: SCNNode!
var parallaxWallNode2: SCNNode!
var parallaxFloorNode1: SCNNode!
var parallaxFloorNode2: SCNNode!
还要向场景添加一个名为addWallandFloorParallax的函数,如下所示:
func addWallandFloorParallax(){
//Preparing Wall geometry
let wallGeometry = SCNPlane(width: 250, height: 120)
wallGeometry.firstMaterial?.diffuse.contents = "monster.scnassets/wall.png"
wallGeometry.firstMaterial?.diffuse.wrapS = SCNWrapMode.Repeat
wallGeometry.firstMaterial?.diffuse.wrapT = SCNWrapMode.Repeat
wallGeometry.firstMaterial?.diffuse.mipFilter = SCNFilterMode.Linear
wallGeometry.firstMaterial?.diffuse.contentsTransform = SCNMatrix4MakeScale(6.25, 3.0, 1.0)
wallGeometry.firstMaterial?.normal.contents = "monster.scnassets/wall_NRM.png"
wallGeometry.firstMaterial?.normal.wrapS = SCNWrapMode.Repeat
wallGeometry.firstMaterial?.normal.wrapT = SCNWrapMode.Repeat
wallGeometry.firstMaterial?.normal.mipFilter = SCNFilterMode.Linear
wallGeometry.firstMaterial?.normal.contentsTransform = SCNMatrix4MakeScale(6.25, 3.0, 1.0)
wallGeometry.firstMaterial?.specular.contents = "monster.scnassets/wall_SPEC.png"
wallGeometry.firstMaterial?.specular.wrapS = SCNWrapMode.Repeat
wallGeometry.firstMaterial?.specular.wrapT = SCNWrapMode.Repeat
wallGeometry.firstMaterial?.specular.mipFilter = SCNFilterMode.Linear
wallGeometry.firstMaterial?.specular.contentsTransform = SCNMatrix4MakeScale(6.25, 3.0, 1.0)
wallGeometry.firstMaterial?.locksAmbientWithDiffuse = true
//Preparing floor geometry
let floorGeometry = SCNPlane(width: 120, height: 250)
floorGeometry.firstMaterial?.diffuse.contents = "monster.scnassets/floor.png"
floorGeometry.firstMaterial?.diffuse.wrapS = SCNWrapMode.Repeat
floorGeometry.firstMaterial?.diffuse.wrapT = SCNWrapMode.Repeat
floorGeometry.firstMaterial?.diffuse.mipFilter = SCNFilterMode.Linear
floorGeometry.firstMaterial?.diffuse.contentsTransform = SCNMatrix4MakeScale(12.0, 25, 1.0)
floorGeometry.firstMaterial?.normal.contents = "monster.scnassets/floor_NRM.png"
floorGeometry.firstMaterial?.normal.wrapS = SCNWrapMode.Repeat
floorGeometry.firstMaterial?.normal.wrapT = SCNWrapMode.Repeat
floorGeometry.firstMaterial?.normal.mipFilter = SCNFilterMode.Linear
floorGeometry.firstMaterial?.normal.contentsTransform = SCNMatrix4MakeScale(24.0, 50, 1.0)
floorGeometry.firstMaterial?.specular.contents = "monster.scnassets/floor_SPEC.png"
floorGeometry.firstMaterial?.specular.wrapS = SCNWrapMode.Repeat
floorGeometry.firstMaterial?.specular.wrapT = SCNWrapMode.Repeat
floorGeometry.firstMaterial?.specular.mipFilter = SCNFilterMode.Linear
floorGeometry.firstMaterial?.specular.contentsTransform = SCNMatrix4MakeScale(24.0, 50, 1.0)
floorGeometry.firstMaterial?.locksAmbientWithDiffuse = true
//assign wall geometry to wall nodes
parallaxWallNode1 = SCNNode(geometry: wallGeometry)
parallaxWallNode1.rotation = SCNVector4Make(0, 1, 0, Float(-M_PI / 2))
parallaxWallNode1.position = SCNVector3Make(15, 0, 0)
scene.rootNode.addChildNode(parallaxWallNode1)
parallaxWallNode2 = SCNNode(geometry: wallGeometry)
parallaxWallNode2.rotation = SCNVector4Make(0, 1, 0, Float(-M_PI / 2))
parallaxWallNode2.position = SCNVector3Make(15, 0, 250)
scene.rootNode.addChildNode(parallaxWallNode2)
//assign floor geometry to floor nodes
parallaxFloorNode1 = SCNNode(geometry: floorGeometry)
parallaxFloorNode1.rotation = SCNVector4Make(0, 1, 0, Float(-M_PI / 2))
parallaxFloorNode1.rotation = SCNVector4Make(1, 0, 0, Float(-M_PI / 2))
parallaxFloorNode1.position = SCNVector3Make(15, 0, 0)
scene.rootNode.addChildNode(parallaxFloorNode1)
parallaxFloorNode2 = SCNNode(geometry: floorGeometry)
parallaxFloorNode2.rotation = SCNVector4Make(0, 1, 0, Float(-M_PI / 2))
parallaxFloorNode2.rotation = SCNVector4Make(1, 0, 0, Float(-M_PI / 2))
parallaxFloorNode2.position = SCNVector3Make(15, 0, 250)
scene.rootNode.addChildNode(parallaxFloorNode2)
}
OMG!!这是一大堆代码。但别慌。我们将系统地通过它。只需看看代码中写着准备墙壁几何形状的地方。首先,我们将看看墙壁几何形状是如何设置的,然后一旦你理解了它,我们将看看如何设置地板几何形状。
我们创建了一个名为wallGeometry的新常量,并将其分配给SCNPlane。SCNPlane和SCNFloor之间的区别在于,在这里我们可以设置平面的尺寸。所以,非常简单,我们将平面的宽度和高度设置为250乘以120单位。
接下来,我们给平面分配一个材质。到目前为止,我们只看到了如何在 SceneKit 中给对象分配颜色。这里我们给平面分配了三种类型的贴图。第一种是漫反射。
注意
漫反射材质是你想要粘贴到平面上的图像或纹理。
文本纹理就像墙上的壁纸。想象一下未粉刷的墙面;现在你可以选择给墙面涂色或者贴上壁纸。在数字世界中添加油漆是通过应用颜色来完成的,就像我们在enemyblock上所做的那样,我们给它分配了一个黄色的漫反射颜色。要在数字世界中应用壁纸,我们使用纹理或图像。在这里,我们将wall.png图像应用到墙面平面几何上。
注意到,从宽度和高度的角度来看,墙面平面相当大。如果我们让它保持原样,wall.png图像将不得不拉伸以适应墙面的尺寸。所以,我们使用wrapt和wraps函数,使得墙面纹理在平面的x和y方向上重复,而不拉伸墙面纹理。所以这就是在接下来的两行中发生的事情。我们只是在两个方向上重复墙面纹理,直到它填满整个平面。
在下一步中,我们将mipFiler分配给linear。过滤器将决定需要添加多少细节到纹理中。如果相机足够远,它将生成一个低分辨率的纹理以减少 CPU 的负担。如果相机靠近,则将创建一个更高分辨率的图像,以便纹理的所有细节都可见。这纯粹是用于优化目的。线性过滤器模式是过滤器最基本的一种模式。还有其他模式,如双线性、三线性等,将提供更好的结果,但计算成本较高。对于我们的目的,线性过滤就足够了。你可以通过更改代码并在设备上运行游戏来看到差异。
对于最后的漫反射,我们根据纹理在平面上看起来有多大或有多小来缩小图像本身的纹理。在缩放时,我们将其缩放到与几何尺寸相同的比例。所以在这里的x和y平面上,我们将其缩小到几何宽度和高度的 1/40 倍。由于我们不在z方向上缩放,我们将其保持在1。
以下就是墙面的漫反射贴图图像:

现在,同样的五个步骤也重复用于法线贴图和镜面反射贴图。我们看到为了给平面添加壁纸,我们必须使用漫反射贴图。现在,如果这个壁纸有一些凹凸和孔洞怎么办?不是所有的墙面都这么光滑。所以,为了给壁纸添加这种粗糙感,我们使用所谓的法线贴图。以下就是法线贴图的图像:

这基本上计算了光线一旦击中正常表面后应该如何行为。根据光线的方向以及正常图中是否有凹坑或孔洞,照明将自动计算。所有这些操作都使用正常图来完成。正常图不过是一张图片。代码将从这张图片中获取信息以创建所需的效果。因此,我们分配一个名为 wall_NRM.png 的正常图,并将其作为内容。接下来的四个步骤与我们对漫反射图所做的步骤非常相似。
接下来,让我们看看高光图。这张图将决定纹理的哪些部分是闪亮的,哪些部分不是。想象一下,如果你的壁纸是由不锈钢制成的。为了产生这种效果,我们分配高光图,并使用 wall_SPEC.png 文件来添加它。接下来的四个步骤与之前相同,但现在我们分别对高光图进行操作。高光图的图像如下:

对于地板几何形状,也重复相同的步骤,只是这次我们翻转了宽度和高度值。
一旦地板和墙壁的几何形状准备就绪,我们将它们分配给墙壁和地板节点。
对于墙壁节点,我们将几何形状分配给节点。然后我们旋转节点,使墙壁垂直。我们在 y 方向上旋转节点 -90 度。然后我们将第一面墙放置在 (15, 0, 0),然后将其添加到场景中。我们将第二面墙节点放置在 (15, 0, 250),这将使其与第一个平面相邻。
对于地板节点,我们遵循类似的过程,但在这里我们必须旋转两次才能使其与地面水平。
在我们的墙壁和地板节点准备就绪后,我们可以更新平面的位置以创建透视效果。因此,在 update 函数中,添加以下代码:
parallaxWallNode1.position.z += -0.5
parallaxWallNode2.position.z += -0.5
parallaxFloorNode1.position.z += -0.5
parallaxFloorNode2.position.z += -0.5
if((parallaxWallNode1.position.z + 250) <= 0){
self.parallaxWallNode1.position = SCNVector3Make(15, 0, 250)
}
if((parallaxWallNode2.position.z + 250) <= 0){
self.parallaxWallNode2.position = SCNVector3Make(15, 0, 250)
}
if((parallaxFloorNode1.position.z + 250) <= 0){
self.parallaxFloorNode1.position = SCNVector3Make(15, 0, 250)
}
if((parallaxFloorNode2.position.z + 250) <= 0){
self.parallaxFloorNode2.position = SCNVector3Make(15, 0, 250)
}
现在对你来说这应该非常熟悉了。就像我们在 SpriteKit 游戏中更新和重置背景精灵一样,我们现在将更新四个节点的位置,然后如果它们在 z 方向上超过 250 个单位,我们将重置所有节点的位置。
将 addWallandFloorParallax 函数添加到 addColladaObjects 函数中。
对于这个场景,我还使用场景图添加了一个环境光节点,否则场景看起来非常暗。转到 monsterScene.DAE 文件,在场景图中点击加号并添加一个新节点。然后,右键单击节点并选择 addLight。在右侧面板中,选择 Attributes 检查器,在 Type 下选择 Ambient。在 Type 下方选择 Color 并选择深蓝色或紫色。现在运行游戏,你应该会看到以下截图所示的结果:

添加粒子
作为锦上添花的部分,我们将包括一个雨粒子效果。要在 SceneKit 中创建粒子效果,请转到 文件 | 新建,在 资源 下选择 SceneKit 粒子系统。点击 下一步,在下一屏幕上,从 粒子系统模板 下拉列表中选择 Rain。点击 下一步,并为文件命名。我将其命名为 rain。因此,现在你将在项目中拥有 rain.scnp 和 spark.png 文件。
为了更好地定位粒子,前往场景图并创建一个名为 particleNode 的节点,并将节点平移和旋转,使其指向英雄。
在类中创建一个名为 addRainParticle 的新函数,并添加以下代码:
func addRainParticle(){
let rain = SCNParticleSystem(named: "rain", inDirectory: nil)
var particleEmitterNode = SCNNode()
particleEmitterNode = scene.rootNode.childNodeWithName("particleNode", recursively: true)!
particleEmitterNode.addParticleSystem(rain)
scene.rootNode.addChildNode(particleEmitterNode)
rain.warmupDuration = 10
}
我们创建了一个名为 rain 的新常量,并将其分配给 SCNParticleSystem,并在其中提供了我们创建的 rain 粒子系统。
创建了一个新的 SCNNode 被称为 particleEmitterNode,并将我们在场景图中创建的 particleNode 分配给它。然后我们将雨粒子系统分配给它。然后我们将 particleEmitterNode 添加到场景中。
我们使用粒子系统的 warmupDuration 并将其分配一个值为 10 的值。这样做是为了当游戏开始时,雨粒子效果被快速播放,看起来就像已经开始下雨了。
你可以选择 rain.scnp 文件并更改参数以更好地满足你的需求。构建并运行以查看我们完成的 SceneKit 游戏。在 addColladaObjects 函数的末尾调用 addRainParticle 函数。

小贴士
将音频添加到游戏中与我们将主旋律添加到 SpriteKit 游戏中的方式完全相同。所以,我将留给你大家去实验,找出如何将音频添加到场景中。
此外,我不想再次重复如何将资产导入到游戏中,因为我们已经看到如何做这件事超过四章了。但请确保在调用文件时提供正确的文件夹位置。如果不这样做,则资产将无法正确检索,导致构建错误。
概述
在本章中,我们看到了如何在 SceneKit 中制作 3D 游戏。从制作简单的几何形状到地板,我们创建了一个完整的游戏,包括完整的游戏循环。我们已将已在 3D 软件包中创建并带有动画的场景添加到 SceneKit 中。由于它已经是 3D 场景的一部分,因此我们无需添加相机或光源。
我们将 COLLADA 对象导入到场景中,并看到了如何通过代码访问对象。我们在场景中添加了敌人并应用了物理。我们使用 SceneKit 的物理引擎来计算碰撞,并也对英雄对象应用了力。
此外,你还看到了如何将 SpriteKit 集成到 SceneKit 中以在场景中显示分数和按钮。我们还使用了 SpriteKit 的 touchBegan 函数来检测屏幕上的触摸,并创建了播放和跳跃按钮。
场景中还添加了视差滚动效果,使用的是平面。此外,你还看到了不同类型的贴图,如漫反射贴图、法线贴图和镜面反射贴图,以及它们各自的功能。最后,我们还向场景中添加了一个雨粒子系统。
在下一章中,你将更深入地学习图形编程,并了解如何使用 Metal 图形库将对象实际显示在屏幕上。
第九章. 金属
在我们开始创建游戏之前,您应该了解如何在屏幕上显示内容。这总是被理所当然地认为,因为所有框架都有一个名为sprite的类,我们只需提供一个.png或.jpg文件,并说addChild,然后“砰”的一声,图像就出现在屏幕上。此外,仅通过一些简单的函数,如移动、缩放和旋转,我们甚至可以变换精灵的位置、大小和旋转。实际上,这个sprite类只是为了在屏幕上显示图像而做了一大堆工作。
在本章中,我们将探讨 Metal——苹果公司开发的一个新的图形库。这个图形库将帮助我们显示屏幕上的对象。它是一个通信工具,与处理器、内存、图形处理单元(GPU)和屏幕进行通信。
如果您来自 DirectX 或 OpenGL 的背景,您会发现显示内容到屏幕上的过程,也就是所谓的图形管线,在 Metal 中非常相似。Metal 的图形管线可以使用着色语言进行编程,该语言以 C++11 为基础。我们将在本章中详细介绍它。让我们开始使用 Metal 吧!
本章我们将涵盖以下主题:
-
概述
-
图形管线和着色器
-
基本的 Metal 项目
-
着色四边形项目
-
纹理四边形项目
概述
Metal 是一个用于在屏幕上显示任何内容的图形 API。Metal 非常特定于 iOS 8 及以上版本;此外,它只能在配备 A7 芯片及以上的设备上运行。据说它的运行速度比 OpenGLES 快十倍。OpenGLES 是另一个图形库。与 Metal 不同,OpenGLES 是开源的,并且可以在跨平台上工作。这里的权衡是,您可以使其运行速度快十倍,这意味着您可以在屏幕上添加更多的粒子和其他对象,然而,您无法在其他操作系统(如 Android 和 Windows Phone)上运行您的游戏。使用 OpenGLES 开发的游戏可以在其他设备上运行,只需对代码进行少量修改。实际上,SpriteKit 和 SceneKit 就是使用 OpenGLES 开发的。那么,您是想制作一个屏幕上有更多对象的游戏,还是更希望您的游戏可以在其他平台上运行?选择权在您手中。
在 Metal 中,要在屏幕上渲染任何内容,您必须分两个阶段进行。第一个阶段是准备或初始化阶段,下一个阶段是绘制阶段。在准备阶段,我们首先获取对 GPU 的访问权限,准备资源,如顶点和缓冲区,并准备渲染管线和您希望对象渲染到的视图。准备阶段完成后,我们就可以进入“绘制”阶段,实际绘制图像。
图形管线和着色器
让我们详细看看这些阶段。我们首先看看准备阶段。
准备/初始化阶段
此阶段包括以下步骤:
-
获取设备。
-
命令队列。
-
资源。
-
渲染管线。
-
查看视图。
我们将逐个查看每个步骤。
获取设备
首先,我们必须获取将负责渲染我们的对象的设备。这将让我们了解 GPU 在性能和功能方面的能力。在 Metal 中,它将基本上告诉我们我们在哪个设备上运行游戏,即是否在 iPhone、iPad 或 OS X 上运行。它还会告诉我们设备的哪个版本,以及它是否是 iPhone 6、5、4 或其他任何设备。
命令队列
一旦我们知道了正在处理的设备,我们就可以获取队列中的下一个命令。信息以异步方式从 CPU 发送到 GPU。这意味着 CPU 和 GPU 不会在特定时间处理相同的项。当 CPU 完成计算后,信息被传递给 GPU。如果 GPU 正忙,信息必须等待,直到 CPU 处理下一个进程。为此,我们需要一个队列。所以基本上,GPU 的命令在执行之前会等待在队列中。因此,在这个步骤中,我们获取队列中的下一个空闲槽位,以便信息可以从 CPU 传递到 GPU。
资源
在这个步骤中,我们准备顶点,这是我们想要传递给 GPU 的信息。我们将声明每个顶点具有的属性(例如,在基本层面上每个顶点的坐标)。我们还可以提供其他信息,例如每个坐标的颜色。我们还需要将这些数据存储在内存中,以便在命令执行时,GPU 可以检索信息。信息存储在缓冲区中。对于顶点的每个属性,都会创建一个缓冲区。正如我们在第一章中看到的,缓冲区不过是内存中的一个位置。
在下面的屏幕截图中,我们可以看到指定了四个坐标a、b、c和d,每个都有自己的顶点和颜色属性:

渲染管道
这个步骤被分解为两个步骤。在第一步中,你必须创建一个描述符。描述符是初始化管道的地方。我们将告诉像素在光栅化图像时使用哪种格式。我们还将在这里传递我们的顶点着色器和像素着色器函数。一旦描述符准备就绪,我们就可以将其传递给渲染管道状态。现在,状态包含管道所需的所有信息,以便可以轻松传递。我们将在稍后的部分查看着色器,因为它们在实际操作中更容易理解。
注意
从第一章,入门中回顾以下内容:
-
顶点/几何着色器:信息随后被传递到顶点着色器。顶点着色器可以通过着色器语言进行编程。这种语言类似于 C。使用这种语言,我们可以改变位置,使对象移动、缩放或旋转,就像你在游戏循环中的
update函数中做的那样。 -
像素/片段着色器:与顶点着色器类似,在顶点着色器中你可以进行顶点修改,像素着色器将使你能够进行基于像素的操作。由于这是一个着色器,你知道这也是可编程的。使用像素着色器,你可以创建诸如更改纹理的颜色和透明度等效果。
视图
最后一步是设置和准备视图。在视图阶段,我们实际上需要访问将要绘制对象的视图所附加的层。层就像一个准备绘制的空白画布。我们获取视图的层,以便我们可以在其上继续绘制和擦除。
我们已经完成了初始化阶段;接下来,我们将看到实际开始在屏幕上绘制内容的步骤。
绘制阶段
在这里,我们将最终绘制我们在第一阶段发送的顶点。这一阶段也有一些需要遵循的步骤:
-
开始渲染过程。
-
获取命令缓冲区。
-
绘制。
-
提交命令缓冲区。
开始渲染过程
我们为绘制对象准备层。我们将层分配给要绘制的对象,并使用默认颜色清除表面。
获取命令缓冲区
命令缓冲区是存储渲染命令的地方。我们需要获取对命令缓冲区的访问权限以执行命令。
绘制
最后,绘制发生在层上。这是通过一个渲染命令编码器完成的,它将命令缓冲区中的代码编码成机器语言以渲染图像。我们传递管道状态和顶点缓冲区,然后绘制图像。这一整个过程是在屏幕外完成的。
提交命令缓冲区
这是最终阶段,图像渲染已经完成,图像准备在屏幕上显示。
现在我们已经拥有了所有这些理论知识,让我们将其付诸实践,看看我们如何在屏幕上显示可以观看的内容。
基本 Metal 项目
在这个第一个项目中,我们将创建一个基本的三角形并在设备上显示它。
创建一个新的 Xcode 项目。选择项目技术为 Metal,语言为 Swift:

选择保存项目的位置。在 GameViewController.swift 文件中,我们删除 viewDidLoad 函数的所有内容,以便我们可以从绝对基础开始。
如概述中所述,我们必须做的第一件事是获取应用程序将要工作的设备。添加以下行:
//get device
let device: MTLDevice = MTLCreateSystemDefaultDevice()
我们创建了一个名为 device 的新常量,其类型为 MTLDevice,并将其分配给 MTLCreateSytemDefaultDevice。因此,我们现在可以直接访问设备。
接下来,我们必须为设备创建 commandQueue:
//Create Command Queue
var commandQueue: MTLCommandQueue = device.newCommandQueue()
我们从设备获取命令并将其分配给一个名为 commandQueue 的新变量。
在下一步中,我们准备资源,如顶点信息和顶点缓冲区。要创建顶点定义,在文件顶部创建一个名为vertexArray的数组,如图下代码所示:
let vertexArray:[Float] = [
0.0, 0.75,
-0.75, -0.75,
0.75, -0.75]
假设屏幕的形状是一个矩形,其宽度和高度各为 2 个像素,屏幕中心是原点。因此,在前面的数组中,第 0 个、第 2 个和第 4 个值是x坐标,而第 1 个、第 3 个和第 5 个项是对应的y坐标。
在这里,我们传递了三对x和y值来绘制一个三角形。对于第一个值,x位于原点,y在屏幕中心的y方向上为.75。接下来的两个坐标位于原点的左下角和右下角。
因此,我们现在已经有了顶点。接下来,我们必须创建一个顶点缓冲区,以便我们可以将其存储在其中。创建一个新的变量vertexBuffer,类型为MTLBuffer,如图所示,并将我们创建的vertexArray变量及其数组大小和nil作为options赋值。

var vertexBuffer: MTLBuffer! = device.newBufferWithBytes(vertexArray,
length: vertexArray.count * sizeofValue(vertexArray[0]),
options: nil)
接下来,我们必须创建我们的顶点着色器和片段着色器。
着色器是运行时编译的小段代码。有两种类型的着色器:顶点和片段:
-
顶点着色器:这允许我们在游戏代码外部执行顶点操作。通过顶点操作,我们指的是移动、旋转和转换每个顶点,从而整个对象。这是基本级别;我们可以通过使用顶点着色器执行更复杂的操作。顶点着色器的调用次数与我们传递的坐标数量相同。因此,在这种情况下,它将被调用三次。
-
片段着色器:这些着色器可以用于在像素级别进行操作。它们可以用来实现各种效果,如模糊、像素化、赛博着色等。与顶点着色器不同,像素或片段着色器可以根据需要多次调用,以填充三角形内的颜色或纹理空间。
让我们看看如何编写我们的第一个着色器文件。首先,我们将编写一个顶点着色器。在项目文件中,你将已经有一个名为Shaders.metal的新文件。这是 Metal 的着色器文件,其中你将编写你的顶点着色器和像素着色器。
拥有 OpenGL 或 DirectX 背景知识的人可能会想知道其他文件在哪里。这是 Metal 与其他着色语言之间的一大区别。Metal 只使用一个文件,在这个文件中,你可以编写所有的着色器。每个着色器不是一个文件,而是一个函数。因此,当我们将在管道描述符中传递着色器时,我们不会给出着色器的文件名,而是给出函数名。
你可以创建多个着色器文件,在一个文件中编写顶点着色器,在另一个文件中编写像素着色器,或者反之亦然。Metal 实际上并不关心你将什么放在哪个文件中,只要确保你调用的是正确的函数。
要创建额外的金属着色器文件,你可以转到 文件 | 新建 | 文件 | 源文件 并选择 Metal 文件:

在 Shaders.metal 文件中,删除所有内容并添加以下代码,因为我们将从基础开始:
#include <metal_stdlib>
using namespace metal;
vertex float4 myVertexShader(const device float2 * vertex_array [[ buffer(0) ]],
uint vid [[ vertex_id ]]) {
return float4(vertex_array[vid],0,1);
}
fragment float4 myFragmentShader() {
return float4(1.0, 0.0, 1.0, 1.0);
}
在顶部,我们包含金属标准库并使用命名空间 metal。对于有 C++ 经验的人来说,金属着色器语言使用的是 C++11 的修改版,会感到很熟悉。
随后的第一个函数是顶点着色器函数。着色器函数以 vertex 或 fragment 关键字开头,以表示该函数是顶点着色器还是片段着色器。
因此,在顶点着色器中,函数具有 vertex 关键字,并返回一个 float4。一个 float 4 就像是一个包含四个 float 值的结构:x、y、z 和 w;或者 r、g、b 和 a。
注意
着色器也有它们自己的数据类型,例如 float、float2、float3 和 float4 或 int、int2、int3 和 int4。由于着色器通常处理顶点或颜色,这些是具有 x、y 和 z 值的 float3 以及具有 r、g、b 和 a 值的 float4。
你也可以对这些值执行数学运算。例如,如果你有两个名为 vert1 和 vert2 的 float3 变量,并且将 vert1 和 vert2 相乘,那么生成的结果 vert3 将使用 x 值相乘来创建一个新的 x 值。同样,y 和 z 值将与 vert1 和 vert2 相乘,以创建具有新 x、y 和 z 值的 vert3。
在返回类型之后,我们指定函数的名称。
函数接受两个属性。双矩形括号表示它是一个属性。属性就像属性一样。在这里,我们通过我们创建的缓冲区传递 vertexArray。在属性中,我们通过索引 0 的缓冲区传递顶点数组。稍后,你会看到我们为 vertexBuffer 分配一个索引值,它指的是这里的 0 位置,这样着色器就知道哪个缓冲区是顶点缓冲区。
函数接受的下一个属性是顶点 ID。这会根据我们传递的顶点数量自动生成。我们传递三对 x 和 y 坐标,因此将为这个生成三个顶点 ID。
接下来,在函数中,它为每个顶点 ID 返回一个float4顶点。由于我们必须返回一个 float 4,我们在末尾添加了额外的0和1。你可能想知道我们如何返回四个值,而实际上只返回了三个值:vertex_array[vid]、0和1。在着色器语言中,你可以在单个变量中将x和y组合在一起。在这里,vertex_array[vid]是一个变量,但实际上它包含两个对象,即该坐标的x和y值。
我们首先为片段着色器创建函数。与顶点着色器类似,我们首先指定着色器类型,然后是返回类型,接着提供函数的名称。目前我们还没有向函数传递任何内容。它确实返回一个float4值。由于它是一个片段着色器,而片段着色器用于像素操作,这里的四个值是颜色的 RGBA 值。因此,我们将绘制的三角形将是紫色的。如果你想让所有四个颜色值相同,我们可以执行以下操作:
return float4(0.56);
这将返回所有 RGBA 值都等于0.56。因此,三角形将是灰色的,并且由于 alpha a的值也是0.56,它将是透明的。这种写值的方式看起来非常奇怪,因为我们通常不遵循常规数学中的这种做法,但通过不断使用,你会习惯它,实际上你会欣赏它,因为它对编写着色器来说更加方便。
注意
片段着色器也可以称为像素着色器,因为大部分情况下它们是相同的,但确保在创建像素着色器函数时使用fragment关键字,否则 Metal 无法理解你在说什么。
因此,我们已经完成了着色器文件;现在让我们继续在GameViewController.swift文件中的常规代码。
我们已经将着色器函数添加到我们的着色器库中。一旦着色器被编译,它就会被添加到shader库中,以便以后可以检索它,从而节省再次编译着色器的努力。
从设备获取库并将着色器函数添加到其中。我们还创建了新的常量,用于从设备获取顶点和着色器函数,这些将在渲染描述符中需要,如下面的代码所示:
//library - collection of functions that can be retrieved by name
let defaultLibrary = device.newDefaultLibrary()
let newVertexFunction = defaultLibrary!.newFunctionWithName("myVertexShader")
let newFragmentFunction = defaultLibrary!.newFunctionWithName("myFragmentShader")
接下来,我们创建渲染管线描述符。首先,我们必须创建一个描述符,稍后将其分配给状态。所以,让我们创建一个新的管线描述符,如下面的代码所示:
//Render Pipeline
let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
在描述符中,我们提供了顶点和着色器函数以及要使用的像素格式,如下面的代码所示:
pipelineStateDescriptor.vertexFunction = newVertexFunction
pipelineStateDescriptor.fragmentFunction = newFragmentFunction
pipelineStateDescriptor.colorAttachments[0].pixelFormat = .BGRA8Unorm
像素格式指定了颜色组件的顺序、每个组件的位深度和数据类型。有二十多种格式。要了解更多关于不同类型的像素格式,您可以访问苹果的文档,网址为developer.apple.com/library/ios/documentation/Metal/Reference/MetalConstants_Ref/#//apple_ref/c/tdef/MTLPixelFormat。
然后,我们根据描述符创建了一个RenderPipeline状态,如下面的代码所示:
//Render pipeline state from descriptor
var pipelineState: MTLRenderPipelineState!
pipelineState = device.newRenderPipelineStateWithDescriptor(
pipelineStateDescriptor,
error: nil)
创建了一个新的变量pipelineState,其类型为MTLRenderPipeLineState,并将pipeLibeStateDescriptor常量传递给它。
然后,我们创建一个类型为CAMetalLayer的图层并将其添加到当前视图中,这样我们就可以在上面绘制对象。因此,添加以下代码来准备视图并将其图层添加到其中:
//prepare view with layer
let metalLayer = CAMetalLayer()
metalLayer.device = device //set the device
metalLayer.pixelFormat = .BGRA8Unorm
metalLayer.frame = view.layer.frame
view.layer.addSublayer(metalLayer)
我们将metalLayer常量分配给设备、图层的像素格式(与我们在管道描述符中分配的相同),以及框架大小,它等于视图框架的大小(框架大小与屏幕大小相同)。最后,将metalLayer作为子图层添加到当前视图图层。
这就是设置所有所需的内容。我们可以继续到下一个阶段,即实际绘制三角形。
在下一步中,我们创建渲染通道描述符。然而,在我们可以创建它之前,我们需要从图层获取下一个可绘制纹理的引用,因为它将被传递到渲染描述符中:
//get next drawable texture
var drawable = metalLayer.nextDrawable()
接下来,我们创建渲染描述符:
//create a render descriptor
let renderPassDescriptor = MTLRenderPassDescriptor()
renderPassDescriptor.colorAttachments[0].texture = drawable.texture //assign drawable texture
renderPassDescriptor.colorAttachments[0].loadAction = .Clear //clear with color on load
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1.0,
green: 1.0,
blue: 0.0,
alpha: 1.0) // specify color to clear it with
现在,我们创建一个新的常量renderPassDescriptor,其类型为MTLRenderPassDescriptor。首先,我们分配可绘制图层的纹理,以便渲染的内容被绘制在纹理上。因此,可绘制图层的纹理被传递进来。
接下来,调用加载操作。一旦加载,图层首先用颜色清除。然后,我们传递一个颜色,图层将用这个颜色清除。在这里,我们传递了一个紫色。
在此基础上,我们的描述符就准备好了。接下来,我们必须渲染图层和三角形。所以首先,我们从命令队列中获取命令缓冲区。这些是存储在内存中的命令:
//Command Buffer - get next available command buffer
let commandBuffer = commandQueue.commandBuffer()
所有这些命令都需要通过MTLRenderCommandEncoder编码成机器语言。我们在这里传递renderPassDescriptor变量来编码渲染代码:
//create Encoder - converts code to machine language
let renderEncoder:MTLRenderCommandEncoder = commandBuffer.renderCommandEncoderWithDescriptor(renderPassDescriptor)!
接下来,我们必须在编码器中设置管道和顶点缓冲区状态。在传递vertexBuffer时,我们必须传递偏移量和索引缓冲区值。由于我们创建了一个新的缓冲区,偏移量值是0,对于索引,我们传递0。这个索引值是顶点着色器所引用并传递到[[buffer(0)]]的值:
//provide pipelineState and vertexBuffer
renderEncoder.setRenderPipelineState(pipelineState)
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, atIndex: 0)
最后,我们可以通过创建原语类型来绘制三角形。最终,所有形状都是由三角形组成的,正如我们在第一章中关于船的例子中所看到的。顶点的数量和它们的位置定义了物体的形状。在这里,我们创建一个单独的三角形形状,因此我们传递三个顶点来创建一个三角形。这个相同的三角形原语用于制作正方形、立方体、茶壶、球体等等:
//drawing begin
renderEncoder.drawPrimitives(.Triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1) //drawin
我们已经绘制完毕,因此我们可以结束编码:
//End drawing
renderEncoder.endEncoding()
尽管我们已经绘制了三角形,但我们仍然需要在屏幕上呈现它。在下一步中,我们将提供纹理并将其提交到视图中:
//commit to view
commandBuffer.presentDrawable(drawable)
commandBuffer.commit()
那就是全部了。最后,你将能够在屏幕上看到紫色三角形和黄色背景,如下面的截图所示:

恭喜!你已经成功完成了。
所有这些只是为了绘制三角形,但我希望你现在能够欣赏到所做的努力。问题是,如果你在任何一个步骤中出错,三角形可能不会显示出来。
代码也保持得很简单。我们绝对可以通过添加渲染器类和创建独立的类来创建顶点数组来优化代码。
在下一个例子中,我们将创建一个正方形。这次,我们将为每个坐标传递颜色,而不是在片段着色器函数中键入颜色值。
彩色四边形项目
要创建一个彩色正方形,我们需要对 vertexArray 进行一些修改,因为我们需要传递六个顶点而不是三个。我们必须传递六个顶点,因为,正如你之前所看到的,我们只能绘制三角形。因此,我们需要三个点来形成正方形的顶部,还需要另外三个点来形成其底部:
let vertexArray:[Float] = [
-1.0, 1.0, 0, 1, //a
-1.0, -1.0, 0, 1, //b
1.0, -1.0, 0, 1, //c
-1.0, 1.0, 0, 1, //a
1.0, -1.0, 0, 1, //c
1.0, 1.0, 0, 1, //d
]
你会看到 a 和 c 点被重复以形成第二个三角形,因为正方形的对角线与第一个三角形的点是相同的。

注意,在顶点数组中,我们现在为每个顶点传递四个值,而不是像三角形那样传递两个值。这将简化修改着色器函数的过程。
提供的坐标值是 x、y 和 z 值,还有一个额外的第四个参数 w 也被传递进来。就像二维空间中的 z 值一样,它目前并没有太多意义。稍后,当你创建一个三维对象时,w 参数将发挥重要作用。但,就目前而言,让这个值保持为 1。
此外,正如我们在创建三角形时所见,视图是一个以中心为原点的 2 x 2 矩形。由于我们在 x-y 方向上传递的坐标在 1 和 -1 之间,实际上这个矩形将覆盖整个屏幕。如果你仍然想看到黄色背景,将值 1 改为更小的值,就像我们在三角形的情况下所做的那样。不要更改与 0 对应的值。
类似于传递顶点,我们也将通过代码将这些坐标的颜色值作为缓冲区传递,以创建一个新的数组colorArray,如下面的行所示:
let colorArray:[Float] = [
1, 0, 0, 1, //a
0, 1, 0, 1, //b
0, 0, 1, 1, //c
1, 0, 0, 1, //a
0, 0, 1, 1, //c
1, 0, 1, 1, //d
]
这些是相应坐标的简单 RGBA 值。在这里,a表示红色red = 1,绿色green = 0,蓝色blue = 0,透明度alpha = 1。这些值的每个都在0和1之间。因此,a将呈现全红色。我们可以通过保持红色为1并添加更多绿色或蓝色来创建自定义颜色,例如在c和d的情况下。
接下来,类似于我们创建vertexBuffer的方式,我们必须创建一个类型为MTLBuffer的colorBuffer:
let colorBuffer = device.newBufferWithBytes(colorArray,
length: colorArray.count * sizeofValue(colorArray[0]), //sizeof(colorArray)
options: nil)
我们传递了我们创建的colorArray以及整个数组的大小。
在代码中,我们不需要对设备、层或渲染管线进行任何更改。但我们需要更改顶点和着色器函数,因为我们将要传递关于颜色的信息。
在任何着色器语言中,我们也可以创建自己的数据类型。通过在着色器文件中使用结构体创建了一个名为VertexInOut的新数据类型。因此,在Shader.metal文件中输入以下代码:
struct VertexInOut{
float4 position [[position]];
float4 color;
};
我们创建了一个包含两个float4值的结构体,其中一个用于位置,另一个用于颜色。使用双方括号的位置表示我们将通过位置属性传递和检索位置属性。
vertex函数已更改,如下面的代码行所示:
注意
压缩变量意味着你不能单独访问每个组件,这与常规的float4不同。例如,在一个具有位置数据的常规float4中,我们可以访问x、y、z和w值,但在压缩的float中,我们无法这样做。
vertex VertexInOut vertexShader(uint vid [[ vertex_id ]],
constant packed_float4* position [[ buffer(0) ]],
constant packed_float4* color [[ buffer(1) ]]){
VertexInOut outVertex;
outVertex.position = position[vid];
outVertex.color = color[vid];
return outVertex;
};
在这里,我们首先使用vertex关键字来声明vertex函数类型;我们返回一个之前创建的名为VertexInOut的类型,并提供函数名称。我们将顶点 ID、索引为0的位置缓冲区和索引为1的颜色缓冲区传递给函数。
在函数中,我们创建了一个名为outVertex的新变量,其类型为VertexInOut,为每个顶点 ID 分配位置和颜色值,然后返回outVertex变量。
我们还需要更改片段着色器函数,因为通过缓冲区传入的颜色需要应用到立方体上:
fragment half4 fragmentShader(VertexInOut inFrag [[stage_in]]){
return half4(inFrag.color);
};
在片段着色器中,我们使用fragment关键字来指定这是一个片段着色器,返回一个half4(类似于float4但占用更少的内存),并为shader函数提供一个名称。该函数接收VertexInOut变量。[[stage_in]]部分用于表示操作将需要在每个像素的基础上进行。在函数中,我们要求它返回类型转换后的颜色值。
注意
更多关于stage_in的信息可以在developer.apple.com/library/ios/documentation/Metal/Reference/MetalShadingLanguageGuide/func-var-qual/func-var-qual.html#//apple_ref/doc/uid/TP40014364-CH4-SW13找到。
由于我们添加了一个新的颜色缓冲区并增加了顶点数量,我们必须对renderEncoder进行修改,如下面的代码所示:
//draw - prep drawing
renderEncoder.setRenderPipelineState(pipelineState)
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, atIndex: 0)
renderEncoder.setVertexBuffer(colorBuffer, offset: 0, atIndex: 1)
renderEncoder.drawPrimitives(.Triangle, vertexStart: 0, vertexCount: 6, instanceCount: 1)
renderEncoder.endEncoding()
我们设置了一个新的顶点缓冲区,并传递了颜色缓冲区和索引值1。在drawPrimitives中,我们仍然在创建一个三角形类型的原始图形,但这次它有六个顶点。
最后,由于我们更改了着色器函数,我们需要更新管道中的名称,如下面的代码所示:
pipelineStateDescriptor.vertexFunction = defaultLibrary!.newFunctionWithName("vertexShader")
pipelineStateDescriptor.fragmentFunction = defaultLibrary!.newFunctionWithName("fragmentShader")
pipelineStateDescriptor.colorAttachments[0].pixelFormat = .BGRA8Unorm
现在一切都已经构建完成,我们可以看到彩色的正方形,或者通常所说的四边形:

你可能想知道,当你只为四个顶点传递了四个颜色时,为什么整个屏幕都被着色,颜色也在混合。
顶点着色器函数根据传入的顶点数量被调用。因此,顶点着色器函数被调用了六次。另一方面,片段着色器函数实际上被调用,直到填满每个三角形原始图形生成的区域。颜色根据坐标的距离进行插值。在这个例子中,左下角的坐标传递了绿色颜色值。随着它从左下角移动,颜色逐渐与其他颜色混合,从而降低了绿色的值。所以在中心,我们有了所有颜色的混合。这是任何片段着色器的一般特性。
纹理四边形项目
在下一个项目中,我们将创建一个纹理四边形对象。这是精灵类的基本构建块。在这里,我们将取一个图像并将其粘贴到我们在早期项目中创建的四边形或正方形上。
我之所以说它是精灵类的基本构建块,是因为我们无法移动、旋转或缩放精灵;我们只是在屏幕上显示精灵。
回到将精灵添加到任何四边形的过程,类似于将壁纸贴到墙上,我们可以在墙上正着或倒着贴壁纸。同样,在将图像添加到四边形时,我们必须指定哪个方向是向上的,否则精灵将被粘贴到四边形的倒置或侧置。
为了做到这一点,我们必须传递一个额外的坐标数组,这些坐标被称为纹理坐标。纹理坐标与顶点坐标不同,因为顶点坐标是以屏幕坐标系统为参考的,屏幕中心是原点。请参考提供的图像。
注意
我们在 2D 空间中工作,因此我们可以称之为屏幕坐标系,目前为了方便,它与世界坐标系相同,但在 3D 中实际上是世界坐标系。
纹理坐标系相对于每个四边形或矩形。此外,四边形的左上角是纹理坐标系统的原点。因此,为了在屏幕上移动四边形,您将更改顶点数组中的值。纹理坐标将改变,以便在四边形内实际移动图像。
因此,在所有理论都讲完之后,让我们创建一个新的数组,称为textureCoordsArray,如图所示。但在那之前,更改vertexArray并减小我们之前创建的四边形的尺寸,以便我们可以更好地理解纹理坐标所发生的情况:
let vertexArray:[Float] = [
-0.75, 0.75, 0, 1, //a
-0.75, -0.75, 0, 1, //b
0.75, -0.75, 0, 1, //c
-0.75, 0.75, 0, 1, //a
0.75, -0.75, 0, 1, //c
0.75, 0.75, 0, 1, //d
]
接下来,添加纹理坐标数组:
let textureCoordsArray:[Float] = [
0.0, 0.0, //a
0.0, 1.0, //b
1.0, 1.0, //c
0.0, 0.0, //a
1.0, 1.0, //c
1.0, 0.0 //d
]
在创建纹理四边形时,有两件事我们需要特别注意:
-
为每个三角形集提供的
vertex数组需要按逆时针方向排列。因此,对于顶点数组中的前三个位置顶点,两个三角形分别需要按abc和acd的顺序排列,否则纹理将无法正确显示。 -
vertex数组和纹理坐标的顺序必须相同;如果顶点数组顺序是abc和acd,那么纹理坐标顺序也必须按相同顺序指定。

在前面的图像中,红色字母a、b、c和d表示顶点坐标,绿色坐标表示纹理坐标。红色原点是顶点坐标的原点,绿色原点是纹理坐标的原点。
纹理坐标准备好后,我们必须创建一个类型为MTLBuffer的纹理缓冲区,并将纹理坐标数组传递给它,同时传递数组的大小。这与我们之前为相应数组创建缓冲区的方式类似:
//initialize textureCoordBuffer
let textureCoordBuffer: MTLBuffer = device.newBufferWithBytes(textureCoordsArray,
length: textureCoordsArray.count * sizeofValue(textureCoordsArray[0]),
options: nil)
接下来,我们必须加载我们想要粘贴到四边形上的纹理。为此,我们导入在 SpriteKit 项目中使用的Bg2.png文件。在初始化阶段的末尾添加以下代码行。
首先,我们从本地包位置获取文件。通过传递文件名及其扩展名来获取文件的路径。然后,必须使用NSData方法检索数据,方法中传递路径,如图所示:
//get texture
let path = NSBundle.mainBundle().URLForResource("Bg2", withExtension: "png")
let data = NSData(contentsOfURL: path!)
接下来,我们从数据中获取图像并将其存储在类型为UIImage的常量中:
let image = UIImage(data: data!)
然后,我们获取图像的宽度和高度,并指定图像的颜色空间。颜色空间决定了颜色如何被解释。除了 RGBA 值之外,还有其他存储颜色的方式;例如,我们可以提供 CMYK 格式的颜色。由于图像中的颜色值是以 RGB 指定的,因此我们必须在这里指定它。
宽度和高度是通过 CGImage 类获得的,因此在这一步我们将图像从 UIImage 类型转换为 CGImage 类型:
let width = CGImageGetWidth(image?.CGImage)
let height = CGImageGetHeight(image?.CGImage)
let colorSpace = CGColorSpaceCreateDeviceRGB();
为了存储图像的每个像素,我们需要指定整个图像数据的大小,因此为要写入的数据创建内存。每个像素占用 4 字节内存空间。因此,为了获取整个图像占用的内存,我们需要将宽度乘以高度,然后将这些值乘以四,这将给出位图图像的数据值。这个值存储在 bitmapData 常量中,如下所示:
let bitmapData = calloc(height * width * 4, UInt(sizeof(UInt8)))
如前所述,我们将每像素字节数赋值给一个名为 bytesPerPixel 的常量。同时,我们创建一个 bytesPerRow 常量来获取每行的字节数:
let bytesPerPixel: UInt = 4
let bytesPerRow: UInt = bytesPerPixel * width
我们还需要指定像素中每个组件占用多少位。一个像素由 R、G、B 和 A 值组成。为了存储每个值,我们需要每个值 8 位。因此,为了存储每个 RGBA 值,我们需要总共 32 位。由于我们稍后需要使用每像素组件的位数,我们在这里将值存储在一个名为 bitsPerComponent 的常量中:
let bitsPerComponent: UInt = 8
然后,我们创建一个上下文,通过存储图像所需的所有数据来创建环境。我们必须向上下文提供 bitmapData、图像的宽度和高度、每组件的位数、每行的字节数,以及最后的 colorSpace 和 bitmapInfo。
在位图信息中,我们指定图像中是否存在 alpha 通道,alpha 通道的位置,以及值是整数还是浮点值:
let context = CGBitmapContextCreate(bitmapData,
width,
height,
bitsPerComponent,
bytesPerRow,
colorSpace,
CGBitmapInfo(CGImageAlphaInfo.PremultipliedLast.rawValue))
接下来,我们获取图像的矩形尺寸。我们创建一个类型为 CGRect 的 rect 变量,并传入图像的起点、宽度和高度,如下面的代码行所示:
let rect = CGRectMake(0.0,
0.0,
CGFloat(width),
CGFloat(height));
接下来,我们获取图像并将 RGBA 数据通过上下文存储到 bitmapData 中。我们首先清除上下文,然后传入上下文、矩形和图像:
CGContextClearRect(context, rect);
CGContextDrawImage(context, rect, image?.CGImage);
为了将纹理加载到 Metal 中,我们需要一个纹理描述符,它存储所有相关信息。我们创建一个新的常量 textureDescriptor 和一个具有 RGBA8 均匀正常像素格式的纹理描述符,并传入宽度、高度以及如果想要图像进行mipmap处理的数据。
Mipmap,正如我们在 SceneKit 中看到的,将创建图像的较低分辨率版本,并在相机远离纹理时显示它,以减少系统的负载,如下所示:
let textureDescriptor = MTLTextureDescriptor.texture2DDescriptorWithPixelFormat(.RGBA8Unorm,
width: Int(width),
height: Int(height),
mipmapped: false)
我们创建一个类型为 MTLTexture 的纹理,并传入纹理描述符:
let texture: MTLTexture = device.newTextureWithDescriptor(textureDescriptor)
最后,我们使用MTLTexture类型的替换区域函数,用存储在bitmapData中的图像数据替换像素。我们传入的区域基本上是一个矩形,然后我们指定mipmapLevel,它保持在0并切割以确定我们将图像粘贴到四边形的哪个表面,因为我们只指定了一个四边形,其值为0。如果我们有更多的四边形,例如一个立方体,我们不得不为其他面指定除0之外的其他值。接下来,我们传入bitmapData,其中存储了图像的 RGBA 值;然后,我们传入bytesPerRow和bytesPerImage值,如下面的代码行所示:
let region = MTLRegionMake2D(0, 0, Int(width), Int(height))
texture.replaceRegion(region,
mipmapLevel: 0,
slice: 0,
withBytes: bitmapData,
bytesPerRow: Int(bytesPerRow),
bytesPerImage: Int(bytesPerRow * height))
由于我们传递了一个纹理坐标缓冲区并且需要绘制一个纹理,我们必须对着色器文件进行一些修改。所以,转到着色器文件。
首先,修改VertexInOut结构体,如下所示:
struct VertexInOut
{
float4 position [[position]];
float4 color;
float2 m_TexCoord [[user(texturecoord)]];
};
在高亮显示的代码中,当在着色器中指定用户定义的属性时使用了用户关键字:
vertex VertexInOut vertexShader(uint vid [[ vertex_id ]],
constant float4* position [[ buffer(0) ]],
constant packed_float4* color [[ buffer(1) ]],
constant packed_float2* pTexCoords [[ buffer(2) ]])
{
VertexInOut outVertex;
outVertex.position = position[vid];
outVertex.color = color[vid];
outVertex.m_TexCoord = pTexCoords[vid];
return outVertex;
};
由于textureCoordinate缓冲区需要传递,以及位置和颜色信息,我们使用缓冲区索引2将其传递。同时,在返回outVertex时,我们将在函数中为该顶点 ID 分配纹理坐标。
接下来,我们还需要对fragment着色器函数进行修改,因为我们将要传递纹理到片段着色器中。所以,在四边形上绘制,如下面的代码行所示:
fragment half4 texturedQuadFragmentShader(
VertexInOut inFrag [[ stage_in ]],
texture2d<half> tex2D [[ texture(0) ]])
{
constexpr sampler quad_sampler;
half4 color = tex2D.sample(quad_sampler, inFrag.m_TexCoord);
return color;
}
除了我们上次传递的stage_in参数外,我们还将以0的索引值将纹理传递到片段着色器中。
在实际选择颜色的函数中,使用了一个采样器。采样器将决定如何从传入的纹理中选取颜色。采样器被设置为constexpr,即类似于const类型的常量表达式。
采样器根据使用text2D.sample函数提供的纹理坐标从 2D 纹理中选取颜色。结果颜色被存储并由片段着色器返回。
接下来,在renderEncoder中添加以下高亮显示的行:
renderEncoder.setVertexBuffer(colorBuffer, offset: 0, atIndex: 1)
renderEncoder.setVertexBuffer(textureCoordBuffer, offset: 0, atIndex: 2)
renderEncoder.setFragmentTexture(texture, atIndex: 0)
renderEncoder.drawPrimitives(.Triangle, vertexStart: 0, vertexCount: 6, instanceCount: 1)
注意,尽管vertexBuffer和纹理具有相同的索引,但它们被处理方式不同,因为vertexBuffer是一种与纹理分开的独立缓冲区。由于textureCoordinate缓冲区是一种类型缓冲区,我们必须传入索引值2,因为我们已经为顶点和颜色缓冲区分别传入了0和1。
最后,修改管道描述符以使用新创建的片段着色器,如下所示:
pipelineStateDescriptor.vertexFunction = defaultLibrary!.newFunctionWithName("vertexShader")
pipelineStateDescriptor.fragmentFunction = defaultLibrary!.newFunctionWithName("texturedQuadFragmentShader")
pipelineStateDescriptor.colorAttachments[0].pixelFormat = .BGRA8Unorm
最后,还有一件事要做,那就是构建并运行它!

通过这样,我们在本书中完成了整个循环。在 2D SpriteKit 游戏开发中,你首先学习的是如何将图像添加到场景中。我们输入了三行代码,图像就出现在屏幕上了。
实际上,我们必须做所有这些事情才能将一个简单的图像显示在屏幕上。但是,正如我之前所说的,这只是一个开始;我们还没有看到如何移动、旋转或缩放四边形。
我们还没有查看深度缓冲区,它决定了图像的某个部分是否需要根据当前对象前面的任何其他对象绘制到屏幕上。
此外,我们只看了二维的情况。我们还没有创建一个立方体,这需要额外的顶点。需要更深入地了解代数、三角学和矩阵。还需要对投影和模型空间、世界空间、视图空间和屏幕空间有更深入的理解,这些都是图形编程的不可或缺的部分。
不用说,这些主题远远超出了本书的范围。事实上,可以有一整本书专门讲述使用 Metal 的图形编程,而另一本书可以专门讲述使用 Metal 着色器语言创建酷炫效果。
对于学习 Metal,我建议先学习 OpenGLES,因为它已经存在了这么多年。一旦你对它有了很好的理解,你就可以将知识应用到 Metal 的实验中。
对于学习 OpenGLES,我推荐《使用 OpenGLES 构建 Android 游戏》。尽管它教授的是 Android 开发,因为 OpenGLES 是跨平台的,你可以用同样的概念进行 iOS 游戏开发。视频链接可以在www.packtpub.com/game-development/building-android-games-opengl-es-video找到。
摘要
在本章中,你看到了如何创建一个简单的三角形、四边形和一个纹理四边形,以及如何将这些显示到屏幕上。你只是触及了图形编程的表面,并学习了诸如顶点、缓冲区、纹理和着色器等术语的含义。
这仅仅是学习过程的开始;图形编程是一个庞大而深奥的主题,为此专门设计了课程,还有很多东西要学习。
希望这一章激发了你对该主题的兴趣;至少,我认为你至少会对那些坐几个小时开发框架和引擎以供你制作梦想中的游戏的人有所赞赏,而你对此一无所知。
说到梦想,在下一章中,你将看到如何最终将游戏发布到 iOS App Store。
第十章。发布和分发
所以,这一刻终于到来了。我们投入了时间和精力,制作了这款出色的游戏。现在,我们想让全世界都知道我们的创作。
如果正确遵循步骤,发布游戏的道路实际上非常直接。就像我们创建开发者配置文件以便在设备上运行游戏一样,我们还需要创建发布者配置文件,以便在 App Store 上发布游戏或在除我们自己的设备以外的任何设备上运行。
下一步将在 iTunesConnect 门户中创建应用。这是你将给应用命名、描述、图标和截图的地方。最后,你将从系统上传应用文件到 App Store。
你还将有机会决定是想免费提供游戏还是收取费用。
虽然成就和排行榜在本书中没有涉及,但你绝对可以将其添加到游戏中,以及广告,这样你就可以通过游戏盈利。
因此,让我们开始准备发布游戏。以下是本章你将学到的内容:
-
准备应用
-
分发证书
-
iTunesConnect 门户
-
创建应用
-
演示安装
-
参考资料
-
替代方案
-
最后的备注
准备 Ms. tinyBazooka 应用
首先,我们将对应用进行一些更改。点击项目根目录,然后点击项目的信息屏幕。在这里,通过双击它旁边的区域来更改包标识符。例如,我将包名称更改为com.growlgamesstudio.Ms.TinyBazooka。同时,更改包名称以反映游戏名称;否则,将显示项目名称,这我们不想看到。
确保将包版本设置为1.0。这是发布版本。稍后,当你对游戏进行更新时,你需要为每次新的更新更改数字。其余的项目可以保持默认设置。

接下来,我们将更改图标,以便我们可以使用自己的图标来代替默认图标,用于游戏。在硬盘上的项目目录中,导航到images.xcassets文件夹,并用资源文件夹中提供的图标替换当前存在的AppIcon.appiconset文件夹。
现在,如果你构建游戏,名称将正确反映出来,包括你提供的新的图标。

如果你点击项目中的images.xcassets文件夹中的AppIcon,这也会反映在你的项目中。

这里有为所有 iOS 设备指定的图标。所以,当我想制作通用应用时,我无需为不同的 iPhone 和 iPod 设备指定图标。
最后,我们还将更改启动图像,目前它显示一个空白屏幕,上面有游戏名称和一些版权信息。点击项目根目录。在通用选项卡中,位于应用图标和启动图像部分下,点击启动图像源旁边的使用资产目录按钮。

然后,点击迁移。然后,点击指向右方的箭头以打开启动图像下拉菜单:

这将带你去资产目录中的启动图像。由于我们只针对 iPad 横屏模式进行开发,我们可以将背景图像拖放到横屏部分的1x和2x槽位。确保在应用图标源和启动图像源中的启动屏幕文件字段为空。
最后,我们将移除显示添加的节点数量和 FPS 的调试信息。所以,前往GameViewController类并注释或删除这两行:
// Configure the view.
let skView = self.view as SKView
//skView.showsFPS = true // comment this line
//skView.showsNodeCount = true //Comment this line
现在,当你启动游戏时,你会看到背景图像而不是其他白色背景。应用将显示正确的图标,并且调试信息将被移除。
生成分发证书
为了分发应用,我们需要分发证书。这包含在所有在 App Store 上发布的应用中,因为没有它,应用无法安装在任何设备上。
获取分发证书的步骤与设置开发者证书类似。前往 Apple 开发者门户,并使用你在第一章创建的开发者账户登录developer.apple.com/devcenter/ios/index.action。
在屏幕的右侧,点击iOS 开发者计划下的证书、标识符和配置文件链接。然后,点击证书。接下来,点击屏幕右上角的+号。
在下一屏幕,选择App Store 和 AdHoc并点击继续。在下一步,创建一个证书签名请求(CSR)文件,因为门户要求将其上传。前往 Launchpad,搜索KeyChain并打开它。

然后,导航到KeyChainAccess | 证书助手 | 请求证书授权。

输入你的电子邮件地址和姓名,选择保存到磁盘,并在下一步点击继续。将文件保存在桌面。
在开发者网站上,点击继续。在下一步,点击选择文件和生成。接下来,下载创建的证书。下载后双击文件以安装证书。

在网站上,点击完成。现在我们准备好分发游戏。
iTunesConnect 门户
要创建应用,我们需要前往 iTunesConnect 门户。在开发者门户中,点击 iTunesConnect 链接,或访问 itunesconnect.apple.com。
你可能需要登录,请使用开发者 ID 和密码进行登录。

一旦进入,你应该会看到一个像这样的页面:

该页面的各个部分如下:
-
我的应用:这是我们创建新应用和更新现有应用的地方
-
销售和趋势:这将显示你在一段时间内销售的应用的各个应用的单位数量。你可以检查按地区、平台、类别、内容类型、交易类型等销售情况。
-
支付和财务报告:在这里,你将获得销售摘要、收入、苹果欠款和苹果支付给你的报告:
![iTunesConnect 门户]()
-
iAds:苹果有自己的广告网络,你可以从中收集并展示游戏中的广告。你必须使用 iAds 工作台来配置 iAds。
-
用户和角色:在这里,可以更新与账户相关的个人的角色信息。创建 Apple ID 的人需要在此处输入他们的详细信息。稍后,如果你为游戏添加测试人员,你也需要在此处包括他们的详细信息。
-
协议、税务和银行:正如其名所示,这里你需要与律师一起坐下,审查苹果的协议,并数字化地同意并签署所有这些协议。你还将在此处提供你的银行信息,以便当你创建付费应用时,你可以因应用的销售而获得报酬。税务部分将包含需要填写的税务表格。主要来说,有针对美国、加拿大和澳大利亚的表格,具体取决于你的位置。如果你不是这三个国家中的任何一个,那么你需要填写美国的税务表格。
![iTunesConnect 门户]()
-
资源和帮助:在这里,你可以找到论坛和常见问题解答等资源链接,如果你需要回答任何问题,可以参考。你也可以点击联系我们并发送带有查询的电子邮件。苹果的人员将回复你。
在基本介绍完成之后,我们可以点击我的应用来发布和分发 Ms. TinyBazooka 游戏。
创建应用
要创建一个新应用,在我的应用部分,点击页面左上角的+号。然后,点击新建 iOS 应用,如图所示:

以下窗口将打开:

该页面的各个部分如下:
-
名称:这是应用在 App Store 中显示的名称。如果建议的名称已被占用,则需要提供一个备选名称,并在 Xcode 的信息文件中进行更改。
-
版本:这需要与项目信息中提供的版本号相同。由于我们的游戏版本为 1.0,我们输入此值。
-
主要语言:选择游戏中使用的语言。您可以选择英语。
-
SKU:这是您需要提供的库存单位编号。您可以输入
001。 -
捆绑 ID:这是应用在 App Store 中被识别的标识。我们需要在开发者门户中注册
com.growlgamesstudio.Ms.TinyBazook捆绑 ID,以便当我们上传应用到 iTunesConnect 时,它将应用与捆绑 ID 关联起来。
点击捆绑 ID 下拉菜单下的开发者门户链接。

在应用 ID 描述中,为应用输入一个名称;这只是为了参考。接下来,在应用 ID 后缀中,输入与应用信息中输入的完全相同的应用捆绑 ID。在这种情况下,我必须输入com.growlgamesstudio.Ms.TinyBazooka。再次强调,这对每个应用都是唯一的。

点击继续以进行下一步。在下一页,确认您的应用 ID 后,点击提交。注册现在完成。点击窗口底部的完成。

您现在会看到捆绑 ID 已经添加到开发者门户中标识下的应用 ID。
返回到应用创建窗口,在捆绑 ID下拉列表中,您可以选择 tinyBazooka 捆绑 ID。选择它并点击提交。这可能需要几分钟的时间来更新并反映在下拉菜单中。
您现在将看到应用页面。在这里,我们将添加描述和截图,并上传二进制文件。在提交审核按钮旁边,有一个保存按钮,每次您进行更改时都会亮起。保存按钮是您的朋友,所以尽可能经常使用它,以免丢失您所做的任何更改。
首先,我们将添加一些截图。您可以通过同时按设备上的电源和主页按钮从您的设备创建截图。拍摄五张截图,将设备连接到 Mac,并将图像传输到桌面。然后,您可以将文件从桌面拖动到 iTunes 应用页面上的空间。您可以单独拖放每个文件或同时拖放所有文件。点击顶部的iPad标签,因为我们目前只为 iPad 开发游戏。

接下来,我们必须提供应用在 App Store 中显示的名称。因此,在名称部分,输入应用的名称:

我们将需要提供游戏的小描述。我们可以突出游戏类型、故事以及我们想要告诉用户的任何内容。你可以让它尽可能长或尽可能短。

在描述的右侧,我们可以提供关键词、支持、营销和隐私政策 URL。关键词、描述和标题是排名算法的一部分,因此请做好研究以确保您的游戏排名更高。

接下来,我们需要上传应用的图标。图标不应该有圆角和透明度。更高的分辨率会更好;1024 x 1024 将是理想的。在Resources文件夹中提供了一个示例图标供您测试。将图标拖入应用图标槽中。

现在,我们必须选择应用的主要类别。您可以选择最多两个子类别。对于主要类别,选择游戏。子类别主要取决于游戏的类型。在这里,我选择了动作和街机作为子类别。

接下来,我们需要指定游戏的评级。在评级部分,点击编辑链接:

如果描述的内容在游戏中根本不存在,则将每个项目评为无。如果存在,则选择不频繁或非常频繁。根据这一点,游戏将针对合适的受众进行评级。因此,如果您的目标受众是儿童,那么第一部分应该是无,最后两项理想情况下应该是否。这将使游戏评级为 4 岁及以上。否则,您针对的是青少年或成人受众,因此请仔细选择。
在编辑许可协议下,默认选中标准最终用户许可协议。否则,您可以包含自定义许可协议:

现在,我们必须上传游戏的构建版本。因此,在 Xcode 中打开游戏。一旦 Xcode 打开,在顶部菜单中,选择产品然后存档。

一旦应用存档,将打开以下窗口。如果没有打开,你也可以通过转到窗口 | 组织者 | 存档来访问它:

注意,标识符和版本号与我们创建应用时在 iTunesConnect 中输入的一致。这很重要,因为如果不一致,则无法上传捆绑包。要上传构建版本,请点击右侧的提交按钮。
接下来,Xcode 将检查配置文件所属的团队。如果您是个人,您的账户名称将如以下截图所示显示。选择它并点击选择:

接下来,将弹出发送应用至苹果窗口。点击提交。现在将构建存档包并将其上传到苹果。

这可能需要 5-25 分钟,具体取决于年份和时间以及您的互联网连接的上传速度。在圣诞节和其他假期期间,可能需要更长的时间,因为许多开发者会上传他们的应用,所以最好提前考虑并提前上传。
一旦上传了二进制文件,点击完成。如果您遇到一些错误,请不要惊慌。修复提到的错误,创建一个新的存档,并上传新的二进制文件。
现在,返回到 iTunesConnect 上的应用页面。在添加构建部分下,点击左侧的+号,选择您刚刚上传的构建,并在弹出窗口中选择完成:

接下来,在应用审核信息标题下,添加您的联系信息。包括您的姓名、电子邮件地址和联系方式。
在版本发布下,您可以选择自动发布版本或手动发布游戏。
如果您选择自动发布,应用将在通过审核后立即发布。在手动发布中,您可以指定应用发布的日期。因此,即使应用通过了审核,也只有在发布日期之前才会发布。
接下来,我们必须指定游戏的定价。在顶部,选择定价。在这里,您可以选择您希望发布应用的日期以及应用的定价层级。

如果您选择了自动发布,则无需指定日期。对于定价,您可以从提供的任何层级中选择,或者选择免费。要创建付费应用,您必须在银行和税务标题下更新银行信息。
在选择定价后,点击屏幕底部的保存。接下来,点击提交审核:

如果您的应用包含加密、第三方应用或广告,请点击是,否则选择否。然后,点击提交。
恭喜!您已成功提交您的应用进行审核。应用的状态将变为等待审核。通常需要 5-7 天时间,苹果的应用审核团队才会审核您的应用,所以请耐心等待。您将收到一封电子邮件,告知您的应用是否被接受或拒绝。

如果应用被接受,它将很快出现在 App Store 中。如果应用被拒绝,请不要担心!您将能够纠正错误,并再次提交应用进行审核。再次审核也需要大约一周的时间。因此,在制作应用时,请确保在上传过程中避免出现错误。同时,当您想在 App Store 上发布应用时,请提前做好准备。
创建一个临时应用
您可以通过连接到机器将游戏构建到设备上,但如果朋友或客户想在他们的设备上运行应用程序,而他们住在别处呢?为此,您可以创建一个 ad hoc 应用程序。步骤与在 App Store 上创建和发布非常相似;只是这次,您需要提供应用程序可以运行的设备 ID。
因此,首先,我们需要添加我们想要运行应用程序的设备的设备 ID。设备 ID 是一个唯一的数字,特定于某个特定设备。因此,如果您的朋友有一个 iPad 和一个 iPhone,并且他希望在这两个设备上运行它,那么您将需要从他那里获取两个设备 ID。设备 ID 也是一个 用户设备 ID(UDID)。
要获取 UDID,将设备连接到 Mac 并打开 iTunes。在 Settings 下的摘要中,点击 Serial Number 以显示设备的 UDID:

您也可以在 Xcode 的 Window 下找到 Devices 并选择要显示设备 UDID 的设备:

因此,一旦您从朋友那里获取到 UDID,请打开 Apple 开发者门户。转到 Certificates, Identifiers and Profiles 部分。在左侧面板中点击 Devices。
一旦打开,点击页面右上角的 + 号。

在 Name 部分,输入朋友和他们的设备名称,直接在下面输入 UDID,然后点击 Continue。重复此过程以添加其他设备的 UDID。
您还可以上传包含 UDID 的文件,通过选择 Register Multiple Devices 并上传文件一次性注册所有设备。无论如何,请注意苹果允许您注册最多 100 台设备。因此,它更适合与测试人员测试您的游戏。
现在,在 Xcode 中打开项目。在顶部栏中,转到 Window 并从下拉列表中选择 Organizer。

这将打开与您存档文件以发布时打开的相同窗口。这次不是点击提交按钮,而是点击 Export 按钮。在 Select the Method for Export 下,选择 Save for AdHoc deployment 并点击 Next。

接下来,选择 用于配置的 Development Team。从下拉列表中选择您的名字,然后点击 Choose。
现在,将创建一个存档。在下一屏幕中,将提供有关应用程序和将包含在捆绑包中的文件的摘要。点击 Export。
接下来,您将被要求选择一个位置来保存导出的文件。因此,在项目文件夹中创建一个名为 builds 的目录,并在其中命名和保存文件。

这.ipa文件就是现在需要发送给你的朋友,这样他们就可以在自己的设备上运行它。
接下来,让我们看看如何在设备上运行这个应用。将设备连接到你的 Mac 或 PC。你需要安装最新版本的 iTunes 来安装应用。所以,如果你还没有更新 iTunes,那么现在是一个好时机。设备连接后,打开 iTunes;在大多数情况下,iTunes 应该会自动打开。
点击顶部电影部分旁边的三个点,并选择应用。

这将显示所有应用。接下来,将你刚刚创建的.ipa文件拖入这个区域。应用将被添加到当前存在的应用列表中。然后,选择你之前选择的三个点旁边的设备图标。在左侧的设置面板下选择应用。这将显示所有准备安装的应用。
滚动列表,查看为 iPad 和 iPhone 设计的应用。选择 Ms.TinyBazooka 应用,并点击安装。

一旦点击安装,它将变为将要安装。在窗口底部,选择应用。现在,它将开始将应用安装到设备上。安装完成后,将要安装文本将变为移除。这意味着安装已完成。
现在,你可以运行这个应用,就像运行其他任何应用一样。
参考文献
对于 Swift、SpriteKit、SceneKit 和 Metal,苹果提供了非常好的文档。所有这些都可以从苹果开发者门户免费访问。
所有 API 和函数都解释得非常清晰,如果你想要更深入地了解所使用的函数和变量,每个部分都提供了链接。你可以点击它们,按顺序阅读,以了解它们的实现。
你可以访问developer.apple.com/library/mac/navigation/并搜索 SpriteKit 或 SceneKit 以获取相应的文档。

要了解更多关于 Metal 的信息,你可以访问网站developer.apple.com/metal/:

关于 Swift 的更多信息,你可以访问苹果网站developer.apple.com/swift/:

替代框架/引擎
对于 SpriteKit 和 2D 游戏开发,有几个替代方案。一旦你使用 SpriteKit 创建了一个游戏,如果它变得很受欢迎,你显然也希望将其带到其他平台,如 Android 和 Windows Phone。
对于跨平台游戏开发,你有 Cocos2d 和 Cocos2d-x。使用 Cocos2d,你可以同时为 iOS 和 Android 开发游戏。你可以用 Objective-C 或 Swift 开发你的游戏。一旦你了解了使用 SpriteKit 的开发,你将会注意到语法相当相似,所以你会感到非常自在。
行业专业人士长期以来一直使用 Cocos2d 来开发杰出的游戏。它是开源的,并且完全免费。它有一个良好的社区,你可以在这里提问以帮助找到解决问题的答案,并且它定期更新。如果你感兴趣,你可以通过cocos2d.spritebuilder.com/访问它:

与 Cocos2d 一样,Cocos2d-x 也是完全开源和免费的,并且支持跨平台游戏开发。使用 Cocos2d-x,你可以为 iOS、Android、Windows Phone、PC、Mac、Linux 等平台开发游戏。
你可以使用 Cocos2d-x 创建二维和三维游戏。
为了使用 Cocos2d-x 开发游戏,你需要了解 C++。但还有 Cocos2d-js,它使用 JavaScript 作为其开发语言。通过使用它,你甚至可以开发一个网页游戏。它可以从www.cocos2d-x.org/下载:

对于三维游戏开发引擎,你可以看看 Unity 和 Unreal Engine。你也可以使用这些引擎来开发二维游戏。
Unity 使用 JavaScript、C# 和 Boo 作为其开发语言,而 Unreal 使用 C++。在 GDC 上,这两个框架都是完全免费的,所以我强烈建议你查看unity3d.com/5和www.unrealengine.com/。
最后的评论
到现在为止,我希望你已经为使用 Swift、SpriteKit 和 SceneKit 开发 2D 和 3D 游戏打下了良好的基础。我也希望你对使用 Metal 进行图形编程有了基本的了解。
使用这些工具,你应该能够开发并在 iOS 商店发布你的游戏。这是第一步;如果你想要制作看起来更好的游戏,还有更多关于这些框架的知识需要学习。
根据我在 Swift、SpriteKit 和 SceneKit 方面的经验,我可以告诉你,Swift 和 SceneKit 都是在 iOS 8 和 Xcode 6 中首次引入的。这对苹果来说绝对是一个良好的开端,我希望在下一个版本中,这些功能将得到改进。随着越来越多的人开始接受和使用 Swift 和 SceneKit,将会有一个更大的开发者社区供你寻求帮助和参与。这就像 SpriteKit 一样,被许多业余爱好者和爱好者广泛使用。
如果你有任何问题,需要专家意见,或者只是想分享你的作品,你可以通过访问我公司的联系我部分来给我发邮件。你也可以在@sidshekar上关注我,我会尽快回复你的询问。
摘要
因此,在本章的最后,我们看到了如何为我们的应用发布做准备,并创建发布许可证。你了解了 iTunesConnect 门户,在其中创建了一个应用,并最终在 iOS 商店发布了应用。最后,我们看到了如何创建一个可以在特定设备上运行的游戏的临时构建版本。
我在撰写这本书的过程中度过了美好的时光。在这个过程中,我自己对这些技术和游戏开发有了很多了解。我希望你们能像我享受将这本书带给你们一样,享受这本书。
祝你游戏开发愉快!























浙公网安备 33010602011771号