IOS8-游戏开发精要-全-
IOS8 游戏开发精要(全)
原文:
zh.annas-archive.org/md5/5679582944100e0edcae563cf220176d译者:飞龙
前言
自 2014 年 iOS 8 发布以来,iOS 平台的游戏开发经历了一些重大变化。这些变化中的第一个是 Swift 编程语言的引入,这是一种由苹果公司制作的函数式编程语言,旨在编写简单且功能现代,同时足够快以处理现代应用程序和游戏开发。iOS 8 还引入了 3D 游戏开发框架 SceneKit。SceneKit 允许开发者快速设计 3D 游戏,并以类似于 iOS 7 的 SpriteKit 的方式处理 3D 资产。一年后,在 2015 年夏季,推出了 iOS 9,同时为经验丰富的和全新的 iOS 游戏开发者提供了一套新工具。新的框架 GameplayKit 允许开发者分别处理游戏规则、人工智能、游戏实体和游戏状态,而不必从继承逻辑中分离出来。除了 GameplayKit 之外,苹果还展示了 Fox 游戏示例,展示了 Xcode 现在可以执行多平台游戏引擎(如 Unity 和 Unreal Engine)中看到的许多相同的视觉编辑功能。
我们首先将熟悉 Swift 编程语言及其在游戏开发中的应用范围。目标是深入理解 iOS 游戏开发,学习以代码为中心的更困难的方法来制作游戏。除了查看苹果公司为各种 iOS 游戏框架提供的示例项目外,我们还将看到两个游戏中的代码示例,第一个游戏是一个已发布的 Swift 开发的 2D 滚动游戏,名为 PikiPop,另一个是基于拼图的扫雷克隆游戏,名为 SwiftSweeper。随着我们阅读本书的进展,我们仍将代码作为游戏开发方法的核心,但将探讨 iOS 9 中引入的视觉工具,这些工具除了 GameplayKit 和基于组件的结构外,还可以使我们能够创建仅受想象力限制的游戏。然后,我们将深入探讨针对希望直接接触 GPU 的开发者的低级 API 主题,例如 OpenGL ES 和 Metal。
最后,我们希望您了解 iOS 如何继续成为强大的游戏开发平台,无论您是来自传统代码中心的计算机科学学校,还是您是日益增长的基于视觉/拖放设计范例的一部分。我们的目标是,当您完成这本书后,您将拥有许多截然不同且详细的游戏想法,然后您可以立即使用 Swift 和 iOS 9 平台开始构建。
本书涵盖的内容
第一章,Swift 编程语言,提供了对 Swift 编程语言的介绍和指导。
第二章, 使用 Storyboards 和 Segues 结构化和规划游戏,通过利用故事板和转场来规划或预先规划 iOS 游戏,帮助读者了解 iOS 应用的基本流程。
第三章, SpriteKit 和 2D 游戏设计,介绍了 iOS 2D 游戏开发框架 SpriteKit 的使用和解释。
第四章, SceneKit 和 3D 游戏设计,帮助读者深入了解 iOS 3D 开发框架 SceneKit 以及 iOS 中引入的可用于 SceneKit 和 SpriteKit 的视觉工具。
第五章, Gameplaykit,介绍了 GameplayKit 框架,这是一个在 iOS 9 中引入的通用游戏逻辑和 AI 框架。
第六章, 在你的游戏中展示 Metal,讨论了高级主题,如 GPU 图形管道以及 Apple 的低级 API Metal 的介绍,以从你的游戏中获得最佳性能。
第七章, 发布我们的 iOS 游戏,通过利用测试和质量保证服务 TestFlight,解释了进行测试和发布 iOS 游戏所需的步骤。
第八章, iOS 游戏开发的未来,讨论了编程、iOS 以及整体游戏开发在不久的将来可能会如何改变或改进,以及它与最新的 iOS 平台和框架的关系。
你需要这本书什么
这本书你需要什么:
-
Xcode 7 或更高版本
-
Mac OS X Yosemite 或更高版本
本书面向对象
本书旨在为希望为 iPhone 和 iPad 开发 2D 和 3D 游戏的游戏开发者编写。如果你来自其他平台或游戏引擎,如 Android 或 Unity,是一名希望了解更多 Swift 和 iOS 9 最新特性的当前 iOS 开发者,或者即使你是游戏开发的新手,这本书也是为你准备的。建议具备一定的编程知识,但不是必需的。
习惯用法
在这本书中,你将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、文件名、文件扩展名、路径名和用户输入以如下方式显示:“正如我们所见,AppDelegate.swift和ViewController.swift文件为我们自动创建,紧接着我们就能找到Main.Storyboard文件。”
代码块设置如下:
let score = player.score
var scoreCountNum = 0
do {
HUD.scoreText = String(scoreCountNum)
scoreCountNum = scoreCountNum * 2
} while scoreCountNum < score
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
// Collision bit masks
typedef NS_OPTIONS(NSUInteger, AAPLBitmask) {
AAPLBitmaskCollision = 1UL << 2,
AAPLBitmaskCollectable = 1UL << 3,
AAPLBitmaskEnemy = 1UL << 4,
AAPLBitmaskSuperCollectable = 1UL << 5,
AAPLBitmaskWater = 1UL << 6
};
新术语和重要词汇将以粗体显示。例如,您在屏幕上看到的单词,如菜单或对话框中的单词,在文本中显示如下:“或者,想象一下玩家失去了所有生命,并收到了一个游戏结束的消息。”
注意
警告或重要注意事项以如下框的形式出现。
小贴士
小贴士和技巧看起来是这样的。
读者反馈
我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或不喜欢什么。读者反馈对我们来说非常重要,因为它帮助我们开发出您真正能从中获得最大价值的书籍。
要向我们发送一般反馈,只需发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中提及书籍的标题。
如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您已经是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大价值。
下载示例代码
您可以从您在www.packtpub.com的账户下载示例代码文件,适用于您购买的所有 Packt 出版社的书籍。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
下载本书的彩色图像
我们还为您提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。彩色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/iOS9_GameDevelopmentEssentials_ColorImages.pdf下载此文件。
错误清单
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误,请访问 www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。
侵权
在互联网上对版权材料的侵权是一个跨所有媒体的持续问题。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过 <copyright@packtpub.com> 联系我们,并提供疑似侵权材料的链接。
我们感谢您在保护我们的作者和我们为您提供有价值内容的能力方面的帮助。
问题
如果您在这本书的任何方面遇到问题,您可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决问题。
第一章. Swift 编程语言
游戏开发的核心理念是游戏代码。它是项目的灵魂,除了艺术、声音和各种资产开发之外。你将在那里花费大部分时间创建和测试你的游戏。直到 2014 年 6 月的苹果公司全球开发者大会 WWDC14,iOS 游戏和应用程序开发的代码选择一直是Objective-C。在WWDC14上,宣布了一种新的更快编程语言 Swift,现在它是所有当前和未来 iOS 游戏以及通用应用程序创建的推荐语言。
到目前为止,你仍然可以使用 Objective-C 来设计你的游戏,但无论是新手还是经验丰富的程序员,都会看到为什么用 Swift 编写不仅更容易表达游戏逻辑,而且更加规范。保持游戏在关键性的 60 fps 运行取决于快速代码和逻辑。苹果公司的工程师从头开始开发 Swift 编程语言,考虑到性能和可读性,因此这种语言可以比 Objective-C 更快地执行某些代码迭代,同时将代码歧义降到最低。Swift 还使用了更多现代语言中找到的许多方法和语法,例如 Scala、JavaScript、Ruby 和 Python。
那么,让我们深入探讨 Swift 语言。
注意
建议在之前对面向对象编程(OOP)有一些基本了解,但我们将努力使代码构建和解释简单易懂,随着我们转向与游戏开发相关的更高级主题。
Hello World!
在学习编程语言时,从Hello World示例开始是一种相当传统的做法。Hello World 程序只是使用你的代码来显示或记录文本“Hello World”。它一直是通用起点,因为有时只是设置你的代码环境并确保代码正确执行就是战斗的一半。至少,这在较老的编程语言中更为常见。
Swift 使得这一切变得比以往任何时候都更容易,而且不需要深入了解 Swift 文件的结构(我们将在稍后进行,这比 Objective-C 和过去使用的语言都要简单得多),以下是如何创建一个 Hello World 程序的方法:
print("Hello, World!")
就这样!这就是你需要在 Xcode 的调试区域输出中看到文本Hello World的全部内容。
不再需要分号
我们这些编程一段时间的人可能会注意到通常至关重要的分号(;)不见了。这不是一个错误;在 Swift 中,我们不需要使用分号来标记表达式的结束。如果我们想,我们可以这样做,有些人可能仍然会这样做作为一种习惯,但 Swift 已经省略了这种常见的担忧。
注意
使用分号标记表达式结束的做法源于编程的早期,当时代码是在简单的文字处理器中编写的,需要特殊字符来表示代码的表达式何时结束以及何时开始。
变量、常量和原始数据类型
在编程任何应用程序时,无论是编程新手还是试图学习不同的语言,首先我们应该了解一种语言如何处理变量、常量和各种数据类型,例如布尔值、整数、浮点数、字符串和数组。你可以把程序中的数据想象成信息或容器。这些容器可以是不同风味或类型的。在整个游戏的生命周期中,数据可以改变(变量、对象等)或者保持不变。
例如,玩家拥有的生命值将作为变量存储,因为预计在游戏过程中会发生变化。这个变量将是原始数据类型整数,基本上是整数。存储诸如游戏中某种武器或增强效果的名称的数据将存储在所谓的常量中,因为这个项目的名称永远不会改变。在玩家可以更换武器或增强效果的游戏中,表示当前装备物品的最佳方式是使用变量。变量是一段会变化的绑定数据。这种武器或增强效果可能还会包含比名称或数字更多的信息;我们之前提到的原始类型。当前装备的物品将由属性组成,例如其名称、力量、效果、索引号以及代表它的精灵或 3D 模型。因此,当前装备的物品不仅是一个原始数据类型的变量,而是一种称为对象类型的类型。编程中的对象可以包含许多属性和功能,可以将其视为功能和信息的一种黑盒。在我们的例子中,当前装备的物品将是一种占位符,可以持有该类型的物品并在需要时进行交换,从而完成其作为可替换物品的用途。
注意
Swift 是一种被称为类型安全语言的语言,因此我们应该跟踪数据的精确类型及其未来的使用(即,数据是否或将是NULL),因为在与其他语言相比使用 Swift 时,这非常重要。苹果让 Swift 以这种方式运行,以帮助将应用程序中的运行时错误和错误降到最低,这样我们就可以在开发过程中更早地发现它们。
变量
让我们看看 Swift 中如何声明变量。
var lives = 3 //variable of representing the player's lives
lives = 1 //changes that variable to a value of 1
对于那些在 JavaScript 中开发过的人来说,这里会感到非常熟悉。就像 JavaScript 一样,我们使用关键字var来表示一个变量,并将其命名为lives。
编译器隐式地知道这个变量的类型是一个整数,原始数据类型Int。
类型可以显式地声明如下:
var lives: Int = 3 //variable of type Int
我们也可以将生命值表示为浮点数据类型 double 或 float,如下所示:
// lives are represented here as 3.0 instead of 3
var lives: Double = 3 //of type Double
var lives: Float = 3 //of type Float
在变量名称声明后使用冒号允许我们显式地进行类型转换。
常量
在你的游戏中,将会有一些数据在整个游戏生命周期或当前关卡/场景中不会改变。这些可以是各种数据,例如重力、抬头显示(HUD)中的文本标签、角色 2D 动画的中心点、事件声明或游戏检查新触摸/滑动之前的时间。
声明常量几乎与声明变量相同。
在变量名声明后使用冒号允许我们显式地进行类型转换。
let gravityImplicit = -9.8 //implicit declaration
let gravityExplicit: Float = -9.8 //explicit declaration
如我们所见,我们使用关键字 let 来声明常量。
这里还有一个使用字符串的例子,它可以代表在阶段开始或结束时在屏幕上显示的消息:
let stageMessage = "Start!"
stageMessage = "You Lose!" //error
由于字符串 stageMessage 是一个常量,一旦声明后我们无法更改它。像这样使用 var 而不是 let 的变量会更好。
小贴士
“为什么我们不把所有东西都声明为变量?”
这是一些新开发者经常提出的问题,为什么会有这样的疑问是可以理解的,尤其是考虑到游戏应用通常比普通应用有更多的变量和可交换的状态。当编译器构建它内部的游戏对象和数据列表时,在变量方面比在常量方面幕后发生的事情更多。
不深入探讨诸如程序的堆栈和其他细节等主题,简而言之,使用 let 关键字将对象、事件和数据声明为常量比使用 var 更有效率。尽管在今天最新的设备上的小型应用中不推荐这样做,但我们可能在不看到大量性能损失的情况下做到这一点。然而,当涉及到视频游戏时,性能是关键。尽可能多地恢复性能可以允许更好的玩家体验。苹果建议,在不确定的情况下,始终在声明时使用 let,让编译器决定何时改为 var。
关于常量的更多信息...
在 Swift 版本 1.2 中,常量可以有一个条件控制的初始值。
在此更新之前,我们必须使用单个起始值初始化常量,或者被迫将属性变为变量。在 Xcode 6.3 及更高版本中,我们可以执行以下逻辑:
let x : SomeThing
if condition
{
x = foo()
}
else
{
x = bar()
}
use(x)
在游戏中,这样的例子可以是:
let stageBoss : Boss
if (stageDifficulty == gameDifficulty.hard)
{
stageBoss = Boss.toughBoss()
}
else
{
stageBoss = Boss.normalBoss()
}
loadBoss(stageBoss)
使用此功能,常量的初始化可以有一个可变性的层级,同时仍然保持其不可变性,或者通过其使用保持不变。在这里,常量 stageBoss 可以根据游戏难度是两种类型之一:Boss.toughBoss() 或 Boss.normalBoss()。在这个阶段过程中,Boss 不会改变,所以将其保持为常量是有意义的。关于 if 和 else 语句 的更多内容将在本章后面介绍。
数组、矩阵、集合和字典
变量和常量可以表示各种属性和对象的集合。最常见的集合类型有数组、矩阵、集合和字典。数组是有序的不同对象的列表;矩阵简而言之,是数组的数组;集合是无序的不同对象的列表;而字典是无序的列表,它利用了 键 : 值 关联来存储数据。
数组
下面是一个 Swift 中数组的示例:
let stageNames : [String] = ["Downtown Tokyo","Heaven Valley","Nether"]
对象 stageNames 是一个表示游戏阶段名称的字符串集合。数组通过下标从 0 到数组长度 -1 进行排序。因此,stageNames[0] 将会是 Downtown Tokyo;stageNames[2] 将会是 Nether;而 stageNames[4] 会引发错误,因为那超出了数组的范围且不存在。我们使用 [] 方括号包围 stageNames 的类类型 [String],以告诉编译器我们正在处理字符串数组。方括号也用于包围这个数组的各个成员。
第二章:二维数组/矩阵
在物理学计算、图形设计和游戏设计中,尤其是基于网格的益智游戏中,常用的一个集合类型是二维数组/矩阵。二维数组简单来说就是数组的成员也是数组。这些数组可以以行列的形式以矩形方式表达。
例如,15 个拼图游戏中的 4x4(4 行,4 列)拼图板可以表示如下:
var tileBoard = [[1,2,3,4],
[5,6,7,8],
[9,10,11,12],
[13,14,15,""]]
在 15 个拼图游戏中,你的目标是使用一个空位(用空字符串""表示)移动拼图,使它们最终按照我们看到的 1-15 的顺序排列。游戏开始时,数字会以随机且可解的顺序排列,然后玩家必须交换数字和空白空间。
小贴士
为了更好地执行对 15 游戏(和其他游戏)中每个拼图的各种操作和/或存储有关每个拼图的信息,最好创建一个拼图对象,而不是使用这里看到的原始值。为了理解矩阵或二维数组是什么,只需注意数组是如何被双重括号[[]]包围的。我们将在后面的示例游戏中使用SwiftSweeper来更好地理解益智游戏是如何使用对象的二维数组来创建完整游戏的。
这里有一些声明空白二维数组的方法,使用严格类型:
var twoDTileArray : [[Tiles]] = [] //blank 2D array of type,Tiles
var anotherArray = Array<Array<Tile>>() //same array, using Generics
变量twoDTileArray使用双括号[[Tiles]]来声明它为一个空白的二维数组/矩阵,用于虚构的类型,即tiles。变量anotherArray是一个相当奇怪的声明数组,它使用尖括号字符<>作为括号。它使用的是所谓的泛型。泛型是一个相当高级的话题,我们将在后面更多地讨论。它们允许在广泛的数据类型和类之间实现非常灵活的功能。目前,我们可以将其视为处理对象的一种通用的方法。
要填充数组的这两种版本的数据,我们就会使用 for 循环。关于循环和迭代的更多内容将在本章后面解释。
集合
这是我们如何在 Swift 中创建各种游戏物品集合的方法:
var keyItems = Set([Dungeon_Prize, Holy_Armor, Boss_Key,"A"])
这个keyItems集合包含各种对象和一个字符A。与数组不同,集合是无序的,包含唯一的项。因此,与stageNames不同,尝试获取keyItems[1]会返回错误,而items[1]可能不一定是Holy_Armor对象,因为在集合中对象的放置是内部随机的。集合相对于数组的优势在于,集合非常适合检查集合中的重复对象和特定内容搜索。集合使用散列来定位集合中的项目,因此检查集合内容中的项目可能比在数组中更快。在游戏开发中,游戏的关键物品,玩家可能只能获得一次且不应有重复,可以作为集合使用得很好。使用函数keyItems.contains(Boss_Key)在这种情况下返回true的布尔值。
注意
集合是在 Swift 1.2 和 Xcode 6.3 中添加的。它们的类由泛型类型 Set<T> 表示,其中 T 是集合的类类型。换句话说,集合 Set([45, 66, 1233, 234]) 将是 Set<Int> 类型,而我们的示例将是一个 Set<NSObject> 实例,因为它包含各种数据类型的集合。
我们将在本章后面讨论更多关于泛型和类层次的内容。
字典
在 Swift 中,字典可以这样表示:
var playerInventory: [Int : String] = [1 : "Buster Sword", 43 : "Potion", 22: "StrengthBooster"]
字典使用 键 : 值 关联,因此 playerInventory[22] 根据键 22 返回值 StrengthBooster。键和值都可以初始化为几乎任何类类型***。除了给出的库存示例外,我们还可以有以下的代码:
var stageReward: [Int : GameItem] = [:] //blank initialization
//use of the Dictionary at the end of a current stage
stageReward = [currentStage.score : currentStage.rewardItem]
注意
在 Swift 中,尽管字典的值相当灵活,但确实存在限制。键必须符合所谓的可哈希协议。基本数据类型,如 Int 和 String,已经具有这种功能。因此,如果你要创建自己的类/数据结构,这些类/数据结构将用于字典,例如将玩家动作与玩家输入映射,必须首先使用此协议。我们将在本章后面讨论更多关于协议的内容。
字典类似于集合,因为它们是无序的,但它们还有一个额外的层次,即与内容相关联的键和值,而不是仅仅的哈希键。像集合一样,字典非常适合快速插入和检索特定数据。在 iOS 应用和 Web 应用中,字典用于解析和选择 JavaScript 对象表示法 (JSON) 数据。
在游戏开发领域,可以使用 JSON 或通过 Apple 的内部数据类 NSUserDefaults 来保存和加载游戏数据、设置游戏配置或访问游戏 API 的特定成员。
例如,以下是在 iOS 游戏中使用 Swift 保存玩家最高分的其中一种方法:
let newBestScore : Void = NSUserDefaults.standardUserDefaults().setInteger(bestScore, forKey: "bestScore")
这段代码直接来自一个名为 PikiPop 的已发布的 Swift 开发游戏,我们将不时使用它来展示实际游戏应用中使用的代码。
再次提醒,字典是无序的,但 Swift 有方法遍历或搜索整个字典。我们将在下一节以及后续的循环和控制流部分进行更深入的讨论。
可变/不可变集合
我们遗漏的一个重要讨论是如何从数组、集合和字典中减去、编辑或添加内容。然而,在我们这样做之前,你应该理解可变和不可变数据/集合的概念。
可变集合是简单的数据,可以对其进行更改、添加或减去,而不可变集合则不能进行更改、添加或减去。
为了在 Objective-C 中有效地处理可变和不可变集合,我们必须事先明确声明集合的可变性。例如,Objective-C 中类型为NSArray的数组始终是不可变的。我们可以调用NSArray上的方法来编辑集合,但幕后,这将创建全新的NSArray对象,因此在游戏生命周期中经常这样做会相当低效。Objective-C 通过类类型NSMutableArray解决了这个问题。
多亏了 Swift 类型推断的灵活性,我们已经知道如何使集合可变或不可变!在 Swift 中,关于数据可变性的常量和变量概念已经涵盖了。在创建集合时使用关键字let将使该集合不可变,而使用var将初始化它为可变集合。
//mutable Array
var unlockedLevels : [Int] = [1, 2, 5, 8]
//immutable Dictionary
let playersForThisRound : [PlayerNumber:PlayerUserName] = [453:"userName3344xx5", 233:"princeTrunks", 6567: "noScopeMan98", 211: "egoDino"]
整数数组unlockedLevels可以简单地编辑,因为它是一个变量。不可变的字典playersForThisRound不能更改,因为它已经被声明为常量。关于额外的类类型没有额外的模糊性。
编辑/访问集合数据
只要集合类型是变量,使用var关键字,我们就可以对数据进行各种编辑。让我们回到我们的unlockedLevels数组。许多游戏在玩家进步时都有解锁级别的功能。假设玩家达到了解锁之前被锁定的第 3 关所需的高分(因为3不是数组的成员)。我们可以使用append函数将3添加到数组中:
unlockedLevels.append(3)
Swift 的另一个巧妙属性是,我们可以使用+=赋值运算符向数组添加数据:
unlockedLevels += [3]
然而,这样做只会将3添加到数组的末尾。所以,我们之前的数组[1, 2, 5, 8]现在变成了[1, 2, 5, 8, 3]。这可能不是期望的顺序,因此为了在第三个位置插入数字3,即unlockedLevels[2],我们可以使用以下方法:
unlockedLevels.insert(3, atIndex: 2)
现在,我们的解锁级别数组已按[1, 2, 3, 5, 8]排序。
这是在假设我们知道数组中索引为3之前的元素已经排序。Swift 提供了各种排序功能,可以帮助保持数组排序。我们将把排序的细节留到本章后面的循环和控制流讨论中。
从数组中移除项目非常简单。让我们再次使用我们的unlockedLevels数组。想象一下,我们的游戏有一个玩家可以前往和返回的开放世界,而玩家刚刚解锁了一个触发事件并阻止访问第 1 关的秘密。现在必须从解锁的级别中移除第 1 关。我们可以这样做:
unlockedLevels.removeAtIndex(0) // array is now [2, 3, 5, 8]
或者,想象一下,玩家已经失去了所有的生命,并得到了一个 游戏结束 的消息。对此的惩罚可能是锁定最远的关卡。尽管这可能是一个相当令人愤怒的方法,并且我们知道第 8 关是我们数组中最远的关卡,但我们可以使用数组类型的 .removeLast() 函数将其删除。
unlockedLevels.removeLast() // array is now [2,3,5]
注意
这假设我们知道集合的确切顺序。集合或字典可能更适合控制你游戏中的某些方面。
这里有一些编辑集合或字典的快速指南。
集合
inventory.insert("Power Ring") //.insert() adds items to a set
inventory.remove("Magic Potion") //.remove() removes a specific item
inventory.count //counts # of items in the Set
inventory.union(EnemyLoot) //combines two Sets
inventory.removeAll() //removes everything from the Set
inventory.isEmpty //returns true
字典
var inventory = [Float : String]() //creates a mutable dictionary
/*
one way to set an equipped weapon in a game; where 1.0 could represent the first "item slot" that would be placeholder for the player's "current weapon"
*/
inventory.updateValue("Broadsword", forKey: 1.0)
//removes an item from a Dictionary based on the key value
inventory.removeValueForKey("StatusBooster")
inventory.count //counts items in the Dictionary
inventory.removeAll(keepCapacity: false) //deletes the Dictionary
inventory.isEmpty //returns false
//creates an array of the Dictionary's values
let inventoryNames = String
//creates an array of the Dictionary's keys
let inventoryKeys = String
迭代集合类型
我们在讨论集合类型时不能不提如何成批迭代它们。
下面是我们在 Swift 中迭代数组、集合或字典的一些方法:
//(a) outputs every item through the entire collection
//works for Arrays, Sets and Dictionaries but output will vary
for item in inventory {
print(item)
}
//(b) outputs sorted item list using Swift's sorted() function
//works for Sets
for item in sorted(inventory) {
print("\(item)")
}
//(c) outputs every item as well as its current index
//works for Arrays, Sets and Dictionaries
for (index, value) in enumerate(inventory) {
print("Item \(index + 1): \(value)")
}
//(d)
//Iterate through and through the keys of a Dictionary
for itemCode in inventory.keys {
print("Item code: \(itemCode)")
}
//(e)
//Iterate through and through the values of a Dictionary
for itemName in inventory.values {
print("Item name: \(itemName)")
}
如前所述,这是通过所谓的 for-loop 实现的;在这些例子中,我们展示了 Swift 如何使用 in 关键字利用 for-in 变体。在这些所有例子中,代码将重复,直到达到集合的末尾。在示例(c)中,我们还看到了 Swift 函数 enumerate() 的使用。这个函数为每个项目返回一个复合值,(index,value)。这个复合值被称为元组,Swift 对元组的利用为函数、循环以及代码块提供了广泛的功能。
我们将在后面更深入地探讨元组、循环和代码块。
Objective-C 和 Swift 比较
这里快速回顾一下我们的 Swift 代码,并与 Objective-C 的等效代码进行比较。
Objective-C
下面是 Objective-C 的示例代码:
const int MAX_ENEMIES = 10; //constant
float playerPower = 1.3; //variable
//Array of NSStrings
NSArray * stageNames = @[@"Downtown Tokyo", @"Heaven Valley", @" Nether"];
//Set of various NSObjects
NSSet *items = [NSSet setWithObjects: Weapons, Armor,
HealingItems,"A", nil];
//Dictionary with an Int:String key:value
NSDictionary *inventory = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:1], @"Buster Sword",
[NSNumber numberWithInt:43], @"Potion",
[NSNumber numberWithInt:22], @"Strength",
nil];
Swift
下面是 Swift 中等效的代码:
let MAX_ENEMIES = 10 //constant
var playerPower = 1.3 //variable
//Array of Strings
let stageNames : [String] = ["Downtown Tokyo","Heaven Valley","Nether"]
//Set of various NSObjects
var items = Set([Weapons, Armor, HealingItems,"A"])
//Dictionary with an Int:String key:value
var playerInventory: [Int : String] = [1 : "Buster Sword", 43 : "Potion", 22: "StrengthBooster"]
在前面的代码中,我们使用了一些变量、常量、数组、集合和字典的例子。首先,我们看到它们的 Objective-C 语法,然后是使用 Swift 语法等效声明的声明。从这个例子中,我们可以看到 Swift 与 Objective-C 相比是多么紧凑。
字符和字符串
在本章的某些时间里,我们一直在提到字符串。字符串也是一种数据类型集合,但它是专门处理字符的集合,属于字符串类。Swift 是 Unicode 兼容的,因此我们可以有如下字符串:
let gameOverText = "Game Over!"
我们可以有如下带有表情符号字符的字符串:
let cardSuits = "♠ ♥ ♣ ♦"
在前面的代码中,我们创建了一个所谓的字符串字面量。字符串字面量是我们明确地在两个引号 "" 中定义字符串。
我们可以为游戏中的后续使用创建空字符串变量,例如:
var emptyString = "" // empty string literal
var anotherEmptyString = String() // using type initializer
这两种方法都是创建空字符串 "" 的有效方式。
字符串插值
我们还可以从其他数据类型的混合中创建字符串,这被称为字符串插值。字符串插值在游戏开发、调试以及一般字符串使用中相当常见。
最显著的例子是显示玩家的得分和生命值。这就是我们其中一个示例游戏 PikiPop 使用字符串插值来显示当前玩家状态的方式:
//displays the player's current lives
var livesLabel = "x \(currentScene.player!.lives)"
//displays the player's current score
var scoreText = "Score: \(score)"
注意\(variable_name)的格式化。我们实际上在之前的代码片段中已经见过这种格式化。在各种print()输出中,我们使用它来显示我们想要获取信息的变量、集合等。在 Swift 中,要在字符串中输出数据类型的值,可以使用这种格式化。
对于那些来自 Objective-C 的人来说,这与以下内容相同:
NSString *livesLabel = @"Lives: ";
int lives = 3;
NSString *livesText = [NSString stringWithFormat:@" %@ (%d days ago)", livesLabel, lives];
注意 Swift 如何使字符串插值比其 Objective-C 前辈更干净、更容易阅读。
修改字符串
有多种方法可以更改字符串,例如像我们对集合对象所做的那样向字符串中添加字符。以下是一些基本示例:
var gameText = "The player enters the stage"
gameText += " and quickly lost due to not leveling up"
/* gameText now says
"The player enters the stage and lost due to not leveling up" */
由于字符串本质上是由字符组成的数组,就像数组一样,我们可以使用+=赋值运算符向之前的字符串中添加内容。
此外,类似于数组,我们可以使用append()函数向字符串的末尾添加一个字符。
let exclamationMark: Character = "!"
gameText.append(exclamationMark)
//gameText now says "The player enters the stage and lost due to not leveling up!"
下面是如何在 Swift 中遍历字符串中的字符:
for character in "Start!" {
print(character)
}
//outputs:
//S
//t
//a
//r
//t
//!
注意我们再次使用了 for-in 循环,并且甚至有使用字符串字面量作为迭代内容的灵活性。
字符串索引
数组和字符串之间的另一个相似之处是,可以通过索引找到字符串的各个字符。然而,与数组不同,由于字符可以是不同大小的数据,可以分解为 21 位的数字,称为 Unicode 标量,因此它们不能在 Swift 中使用Int类型的索引值来定位。
相反,我们可以使用字符串的.startIndex和.endIndex属性,并通过.successor()和.predecessor()函数分别向前或向后移动一个位置,以检索所需的字符或字符。
下面是一些使用我们之前的gameText字符串的这些属性和函数的示例:
gameText[gameText.startIndex] // = T
gameText[gameText.endIndex] // = !
gameText[gameText.startIndex.successor()] // = h
gameText[gameText.endIndex.predecessor()] // = p
注意
有许多方法可以操作、混合、删除和检索字符串和字符的各种方面。有关更多信息,请务必查看官方 Swift 文档中关于字符和字符串的说明,链接为developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/StringsAndCharacters.html。
Swift 中的注释
在我们迄今为止的代码片段中,可能会注意到带有双斜杠//或带有斜杠和星号的/* */的注释。这是我们如何在 Swift 代码中进行注释或标记的方法。任何在 C++、Java、Objective-C、JavaScript 或其他语言中编码过的人都会看到 Swift 实际上工作方式相同。
单行注释以双斜杠//开始,而多行注释或注释块以/*开始,以*/结束。
以下是一个示例:
//This is a single line comment
/*
This is a comment block
that won't end until it reaches the closing asterisk/forward slash characters
*/
注释用于帮助导航代码、理解代码可能执行的操作,以及注释掉我们可能不想执行但希望保留以备后用的代码行(即,print()日志调用或备选的起始属性值)。
注意
从 Xcode 6 Beta 4 开始,我们还可以利用以下注释:
// MARK:, // TODO:, 和 // FIXME。//MARK与 Objective-C 的#pragma mark 等效,允许程序员为你的代码中的部分添加标签,这些标签可以在 Xcode 顶部的面包屑下拉列表中访问。// TODO:和// FIXME使我们能够将代码的一部分划分出来,我们希望将来可能添加功能或调试。即使结构良好的游戏类也可以让人难以梳理。这些额外标记工具的添加使得规划和使用我们游戏的代码变得更加容易。
Boolean
所有编程、游戏或其他领域的一个基本组成部分是使用布尔值。布尔值通常返回true或false、yes或no、0或1的值。在 Swift 中,这是Bool类对象的工作。在我们过去的集合数据类型示例中使用.isEmpty()函数返回一个布尔值true或false,这取决于该集合是否为空。
在游戏开发中,我们可以使用布尔值的一种方式是有一个全局变量(一个可以在游戏/应用程序范围内访问的变量),用来检查游戏是否结束。
var isGameOver = false
这个变量来自 PikiPop 游戏,游戏开始时使用一个名为isGameOver的布尔类型变量,初始值为false。如果游戏事件导致此值变为true,则这会触发与游戏结束状态相关的事件。
注意
与 Objective-C 中的布尔值不同,Swift 只使用true或false值来表示布尔变量。Swift 严格的类型安全不允许使用YES和NO或0和1,正如我们在 Objective-C 和其他编程语言中看到的那样。
然而,阅读和控制我们游戏中的此类信息,即所谓的游戏状态,最好使用不止一个布尔值。这是因为你的游戏以及游戏中的角色可能有各种状态,例如游戏结束、暂停、生成、空闲、运行、下落等等。一个称为状态机的特殊对象最适合管理此类信息。当讨论GameplayKit框架时,我们将更详细地介绍状态机。
整数、无符号整数、浮点数和双精度浮点数
除了布尔值之外,我们到目前为止简要提到的另一种基本数据类型是各种数值对象,例如整数(Ints)、无符号整数(UInts)、浮点数/小数(floats)和双精度浮点数/小数(doubles)。
整数和无符号整数
整数表示负数和正数,而无符号整数表示正数。与 C 和其他编程语言一样,Swift 允许我们创建从 8 位、16 位、32 位和 64 位的各种整数和无符号整数。例如,Int32 类型是 32 位整数,而 UInt8 类型是 8 位无符号整数。Ints 和 UInts 的位数大小表示分配给存储值的多少空间。以我们的 UInt8 例子来说,由这种无符号 Int 类型构成的数字只能存储 0-255(或二进制系统中的 11111111)的值。这也被称为 1 字节(8 位)。如果我们需要存储大于 255 或负数,那么可能 Int16 类型就足够了,因为它可以存储介于 –32767 和 32767 之间的数字。通常,我们不必太担心我们的整数变量和常量分配的大小。因此,在大多数情况下,只需使用 Int 类名即可。
注意
Int 的大小将取决于我们正在工作的系统类型。如果我们在一个 32 位系统上编译我们的代码,整数将是 Int32,而在 64 位系统上相同的整数将是 Int64。
Swift 允许我们通过 .min 或 .max 类变量(即 Int16.max = 32767 和 UInt.min = 0)查看 Int 变量的最小和最大值。
浮点数和双精度浮点数
浮点数是 32 位浮点数/分数,例如 π(3.14)或黄金比例 φ(1.61803)。
在游戏设计中,我们经常使用浮点数和范围,无论是确定 2D 精灵的 x 和 y 坐标点,使用线性插值平滑 3D 空间中游戏的摄像机移动,还是在对象或 2D/3D 向量上应用各种物理力。每种情况下所需的精度将决定是否需要浮点数或 64 位浮点值,即双精度浮点数。双精度浮点数可以精确到 15 位小数,而浮点数可以精确到 6 位小数。
小贴士
实际上,在可以使用浮点数或双精度浮点数的情况下,使用双精度浮点数是最佳实践。
Swift 中的对象
面向对象编程(OOP)的核心方面当然是对象的概念。C++ 在编程中开启了这种范式,而 Java、C#、苹果的 Objective-C 以及其他语言都基本上是从这个基础上构建的。
Swift 是一种面向对象(OOP)的语言,它具有与 Objective-C 相同的动态对象模型,但以更简洁、类型安全和紧凑的方式呈现。
你可以将对象理解为它的字面意思,一个抽象的事物或容器。一个对象可以像字符串这样简单,也可以像最新视频游戏中的玩家对象那样复杂。从技术角度讲,程序中的对象是对分配的内存块中一组各种数据的引用,但只需理解对象可以是变量或类、结构体或代码块的实例的引用即可。
一个对象可以与各种数据字段/方面相关联,例如属性、函数、父对象、子对象和协议。例如,在 C 语言中,一个整型变量通常只表示为原始数据,但 Swift 中的整型实际上是一个对象。因此,我们可以在代码中对 Int 对象访问额外信息和执行函数。我们之前在 Int.max 变量中看到了这一点,它返回 Int 类可以表示的最高数值。同样,这取决于你正在工作的机器,这可能是 Int32.max 或 Int64.max 的相同值。
var highestIntNumber : Int = Int.max
访问对象的函数和属性使用点符号,正如我们在前面的例子中所看到的。Int.max 和 Int.min 实际上是称为 类变量 的特殊属性,它们代表所有 Int 类型对象的实例。
让我们看看 Swift 如何通过一个虚构的 Player 类型对象来获取对象的属性和函数。
let currentPlayer = Player(name:"Fumi") //(a)
let playerName = currentPlayer.getName() //(b)
var playerHealth = currentPlayer.health //(c)
currentPlayer.attackEnemy() //(d)
我们将回到行 (a) 的后半部分,但只需理解它创建了一个名为 currentPlayer 的 Player 类型对象的实例。行 (c) 创建了一个名为 playerHealth 的变量,它通过 currentPlayer 的 health 属性设置;这里使用的是 点符号。行 (b) 和 (d) 使用点符号调用 getName() 和 attackEnemy() 函数。在这个例子中,getName() 函数是一个返回字符串并将其分配给常量 playerName 的函数。行 (c) 创建了一个名为 playerHealth 的变量,它通过引用 currentPlayer 的健康属性创建,也使用点符号。行 (d) 是直接调用 Player 类的 attackEnemy() 函数,你可以想象它现在只是执行 currentPlayer 的攻击。这个函数不返回任何值,因此被称为 void 类型函数。
至于行 (a),有人可能会注意到它没有使用点符号。这就是 Swift 实现所谓的类初始化器的方式;由类名后面的括号 () 和参数 name(发送一个字符串,Fumi)到对象的类初始化器指定。
当我们继续到函数和类时,我们将更深入地探讨 对象 的使用。
类型安全和类型推断
在 Swift 中,对象及其函数是类型安全的,正如我们将要看到的。这意味着如果我们对一个字符串对象执行一个预期为整数的函数,编译器将在早期阶段警告我们。在游戏设计的范畴内,如果我们让玩家执行只有敌人应该做的动作,Swift 将通过其固有的类型安全特性知道这一点。
Swift 的类型推断是我们之前提到过的。与其他语言不同,你每次初始化对象时都必须声明其类型,Swift 会推断你想要什么类型。例如,我们有以下内容:
var playerHealth = 100
//Swift automatically infers that playerHealth is an Int object
可选参数
如我们之前所述,Swift 是一种类型安全的语言。苹果公司创建 Swift 的初衷也是为了将尽可能多的潜在错误和 bug 保留在开发阶段的编译状态,而不是在运行时。尽管 Xcode 拥有一些出色的调试工具,如断点、日志和 LLDB 调试器,但运行时错误,尤其是在游戏中,可能很难发现,这可能会导致开发过程停滞。为了在编译期间保持类型安全并尽可能无 bug,Swift 处理了可选的概念。
简而言之,可选是可能为空或开始为空的对象。当然,nil 是一个没有任何引用的对象。
在 Objective-C 中,我们可以为游戏声明以下字符串变量:
NSString *playerStatus = @"Poisoned";
playerStatus = nil;
在 Swift 中,我们会以同样的方式编写,但我们会很快发现 Xcode 会因为这样做而给出编译错误:
var playerStatus = "Poisoned"
playerStatus = nil //error!
对于任何刚开始接触 Swift 的人来说,如果我们做了一件像这样简单的事情,我们也会得到一个错误:
var playerStatus : String //error
在我们的游戏中创建空/未声明的对象是有意义的,并且这是我们经常想在类开始时想要做的。我们希望有这种灵活性,以便根据游戏事件稍后分配值。Swift 似乎使这样一个基本概念变得不可能实现!不用担心;在大多数情况下,Xcode 会通知你,在这些nil对象末尾添加一个问号?。这就是如何声明一个可选对象。
因此,如果我们想在 Swift 中规划游戏属性和对象,我们可以这样做:
var playerStatus : String? //optional String
var stageBoss : Boss? //optional Boss object
解包可选
让我们想象一下,我们想在游戏中显示导致玩家失败的原因。
var causedGameOver:String? = whatKilledPlayer(enemy.recentAttack)
let text = "Player Lost Because: "
let gameOverMessage = text + causedGameOver //error
因为字符串causedGameOver是可选的,Xcode 会因为我们没有解包可选而给出编译错误。要在可选中解包值,我们在可选末尾添加一个感叹号!。
这是我们的Game Over消息代码,现在使用解包的可选进行了修复:
var causedGameOver:String? = whatKilledPlayer(enemy.recentAttack)
let text = "Player Lost Because: "
let gameOverMessage = text + causedGameOver! //code now compiles!
我们还可以在声明时强制解包可选,以便在运行时而不是在编译时处理任何潜在的错误。这种情况经常发生在@IBOutlets和@IBActions(与各种基于菜单/视图工具的故事板和其他工具链接的对象和函数)。
@IBOutlet var titleLabel: UILabel! //label from a Storyboard
var someUnwrappedOptional : GameObject! //our own unwrapped optional
注意
尽管如此,如果可能的话,建议尽可能多地使用基本的包装可选?,以便让编译器找到任何潜在的错误。通过使用所谓的可选绑定和链式调用,我们可以在可选对象上进行一些早期的逻辑检查,这在之前的语言中可能需要涉及各种if语句/控制流语句来简单地检查空对象。
保持代码干净、安全且易于阅读是 Swift 的目标,也是为什么 Swift 有时会不遗余力地强制执行许多这些规则,特别是关于可选的。
可选绑定和链式调用
可选绑定是检查可选是否有值。这是使用非常方便的 if-let 或 if-var 语句完成的。让我们回顾一下我们之前的代码:
var causedGameOver:String? = whatKilledPlayer(enemy.recentAttack)
let text = "Player Lost Because: "
if let gotCauseOfDeath = causedGameOver {
let gameOverMessage = text + gotCauseOfDeath
}
代码块 if let gotCauseOfDeath = causedGameOver{…} 做了两件事情。首先,使用关键字 if let,它自动创建一个名为 gotCauseOfDeath 的常量,并将其绑定到可选的 causedGameOver。这同时检查 causedGameOver 是否为 nil 或有值。如果不是 nil,那么 if 语句的代码块将执行;在这种情况下,创建一个名为 gameOverMessage 的常量,该常量将 text 常量与 gotCauseOfDeath 结合起来。
我们可以使用 if-var 进一步简化这一点:
let text = "Player Lost Because: "
if var causedGameOver = whatKilledPlayer(enemy.recentAttack) {
let message = text + causedGameOver
}
if-var 语句使用我们之前使用的可选 causedGameOver 创建一个临时变量,并根据 whatKilledPlayer(enemy.recentAttack) 的结果进行布尔逻辑检查。如果返回一个非 nil 值,则该语句为真。注意,在这种情况下,我们不必使用可选的包装(?)或强制解包(!)。
可选链是在查询对象属性时使用点操作符,同时进行 nil/值检查,就像我们使用可选绑定时做的那样。例如,假设我们有一个游戏,其中某些敌对类型可以通过名为 currentEnemy 的敌对实例立即使玩家失去生命。在这个例子中,currentEnemy.type 将是一个返回击中玩家的敌对类型的字符串。可选链使用自定义点修饰符 ?. 在访问可能为 nil 的属性时进行检查。以下代码可以帮助我们更好地理解它是如何工作的:
if let enemyType = currentEnemy?.type {
if enemyType == "OneHitKill"
{
player.loseLife() //run the player's lost l
}
}
很可能我们不会创建一个没有指定类型的敌对者,但为了理解可选链,观察一下这个检查 currentEnemy.type 可能返回的 nil 对象是如何进行的。就像点操作符的功能一样,你可以深入到属性和属性的属性中,同样的,你也可以使用重复的 ?.per 属性进行深入。在这个代码中,我们使用 == 进行布尔比较,以查看 enemyType 是否是字符串 OneHitKill。
如果 if 语句的语法有点神秘,不要担心;接下来,我们将讨论 Swift 如何使用 if 语句、循环以及其他我们可以控制各种对象数据和它们功能的方法。
Swift 中的控制流
任何程序中的控制流只是代码中指令和逻辑的顺序。Swift,就像任何其他编程语言一样,使用各种语句和代码块来循环、更改和/或迭代你的对象和数据。这包括 if 语句、for 循环、do-while 循环和 Switch 语句等代码块。这些包含在函数中,构成了更大的结构,如类。
如果语句
在我们继续讨论 Swift 如何处理面向对象编程的主要主题之一,即函数和类之前,让我们快速回顾一下 if-else 语句。if 语句检查一个布尔语句是 true 还是 false。以下是一个示例:
if player.health <= 0{
gameOver()
}
这检查玩家的健康是否小于或等于 0,由 <= 运算符表示。注意,Swift 可以接受没有括号,但如果我们愿意或者如果语句变得更复杂,比如这个例子,我们可以使用它:
if (player.health <=0) && (player.lives <=0){ //&& = "and"
gameOver()
}
在这里,我们不仅检查玩家是否失去了所有健康,还检查他们的生命是否全部消失,使用 && 运算符。在 Swift 中,就像在其他语言中一样,我们使用括号将单个布尔检查分开,并且像其他语言一样,我们使用两个竖线键 (||) 进行逻辑或检查。
这里是 Swift 中编写 if 语句的一些更多方法,包括添加了关键字 else-if 和 else,以及 Swift 如何检查非某条语句:
//(a)
if !didPlayerWin { stageLost() }
//(b)
if didPlayerWin
{
stageWon()
}
else
{
stageLost()
}
//(c)
if (enemy == Enemy.angelType){enemy.aura = angelEffects}
else if(enemy == Enemy.demonType){enemy.aura = demonEffects}
else{ enemy.aura = normalEffects }
//(d)
if let onlinePlayerID = onlineConnection()?.packetID?.playerID
{
print("Connected PlayerID: /(onlinePlayerID)"
}
//(e)
if let attack = player.attackType, power = player.power where power != 0 {
hitEnemy(attack, power)
}
//(f)
let playerPower = basePower + (isPoweredUp ? 250 : 50)
让我们看看我们在代码中放入了什么:
-
(a): 这通过!statement使用感叹号!检查语句的非 / 反转。 -
(b): 这检查玩家是否获胜。否则,使用关键字else调用stageLost()函数。 -
(c): 这检查一个敌人是否是天使,并相应地设置其光环效果。如果不是,则使用 else-if 检查它是否是恶魔,如果不是,则使用else语句捕获所有其他实例。我们可以有一系列连续的 else-if 语句,但如果开始堆叠太多,那么使用 for 循环和 Switch 语句将是一个更好的方法。 -
(d): 使用可选链,我们根据if创建一个onlineID常量;我们能够使用 if-let 获取非 nil 的playerID属性。 -
(e): 这使用 if-let,在 Swift 1.2 中可选绑定成为了一个特性。与在后端网络开发中如何执行 SQL 查询类似,我们可以创建非常紧凑、强大的早期逻辑检查。在示例(e)中,敌人根据攻击类型和玩家的力量接收攻击。 -
(f): 这是一个将常量的创建与关键字let结合起来,并执行if语句的简短版本。在 Swift 中,我们使用问号?和冒号:来缩短if语句。简短if语句的格式为:bool ? trueResult : falseResult。如果isPoweredUp为true,则playerPower等于basepower + 250;如果为false,则它是basepower + 50。
对于循环
在处理集合之前,我们提到了 for-in 循环。这里再次是 Swift 中的 for-in 循环,它将遍历一个集合对象:
for itemName in inventory.values {
print("Item name: \(itemName)")
}
对于一些习惯于使用旧方式使用 for 循环的程序员,不用担心,Swift 允许我们以 C 风格编写 for 循环,这是我们大多数人可能习惯的:
for var index = 0; index < 3; ++index {
print("index is \(index)")
}
这里是使用 for 循环而不使用索引变量的另一种方法,用下划线字符 _ 标记,但当然使用 Range<Int> 对象类型来确定 for 循环迭代的次数:
let limit = 10
var someNumber = 1
for _ in 1...limit {
someNumber *= 2
}
注意1和limit之间的…。这意味着这个 for-in 循环将从 1 迭代到 10。如果我们想让它从0迭代到limit-1(类似于在数组索引的范围内迭代),我们可以用0..<limit代替,其中limit等于数组的.count属性。
do-while 循环
在编程中,另一个非常常见的迭代循环是 do-while 循环。很多时候我们可以只利用这个逻辑的 while 部分,所以让我们来看看我们可能如何和为什么使用 while 循环:
let score = player.score
var scoreCountNum = 0
while scoreCountNum < score {
HUD.scoreText = String(scoreCountNum)
scoreCountNum = scoreCountNum * 2
}
在游戏开发中,while 循环的一个用途(尽管在游戏应用中执行方式不同,但这可以适应每帧迭代一次)是显示玩家分数从 0 增加到玩家达到的分数——这是许多游戏在关卡结束时常见的审美。这个 while 循环将迭代,直到达到玩家的分数,并在 HUD 对象上显示直到那个分数的中间值。
do-while 循环实际上与 while 循环相同,只是额外的一个限制是至少迭代一次代码块。结束阶段的分数计数示例也可以说明为什么我们需要这样的循环。例如,让我们想象玩家在关卡结束时表现真的很糟糕,没有得到任何分数。在给出的 while 循环中,零分不会让我们进入 while 循环中的代码块,因为它不满足scoreCountNum < score的逻辑检查。在 while 循环中,我们还有显示分数文本的代码。虽然这可能对玩家来说很尴尬,但我们仍然想要计数到分数,更重要的是,仍然显示分数。以下是使用 do-while 循环完成的相同代码:
let score = player.score
var scoreCountNum = 0
do {
HUD.scoreText = String(scoreCountNum)
scoreCountNum = scoreCountNum * 2
} while scoreCountNum < score
GameCenter achievement (in this case, a 6x combo) based on the number of times the combo was achieved by the player. Don't worry too much about the GameCenter code (used with the GCHelper singleton object); that's something we will go over in future chapters when we make games in SpriteKit and SceneKit.
switch (comboX6_counter) {
case 2:
GCHelper.sharedInstance.reportAchievementIdentifier("Piki_ComboX6", percent: 25)
break
case 5:
GCHelper.sharedInstance.reportAchievementIdentifier("Piki_ComboX6", percent: 50)
break
case 10:
GCHelper.sharedInstance.reportAchievementIdentifier("Piki_ComboX6", percent: 100)
default:
break
}
这里的 switch 语句使用了一个变量来计数玩家击中了 6X 连击的次数,即comboX6_counter,并根据comboX6_counter的值执行不同的任务。例如,当玩家完成了两次 6X 连击时,Piki_ComboX6 成就将完成 25%。当计数器达到 10 时,玩家将获得成就(当达到 100%时)。关键字break的作用是告诉循环在那个点退出;否则,下一个 case 块将继续迭代。有时,这可能符合游戏逻辑的需求,但请注意,Swift,像许多其他语言一样,如果没有break,将继续通过 switch 语句。关键字default是一个通用的块,当 switch 语句检查的项的值不是各种情况时被调用。它可以被认为是else{}块的等价物,而所有的情况都类似于else if(){}。然而,不同之处在于 Swift 要求处理 switch 的所有情况。因此,尽管我们可以用一个没有else的if来满足,但我们必须为 switch 语句提供一个默认情况。再次强调,这是为了在编码过程的早期保持 Swift 代码的安全和整洁。
函数和类
到目前为止,我们还没有讨论 Swift 或任何面向对象编程语言可能最重要的方面——语言如何处理对象上的函数以及如何组织这些对象、对象属性和函数,并执行各种面向对象设计概念,如多态和继承(使用类、Structs、枚举、协议和其他数据结构)。关于 Swift 如何利用这些概念,有更多内容可以讨论,但超出了本章的范围。但在本书的整个过程中,特别是在我们深入研究如何使用苹果的游戏中心 SpriteKit 和 SceneKit 框架时,我们将更详细地探讨这些主题。
Functions
在 Objective-C 中,函数的写法如下:
-(int) getPlayerHealth() {
return player.health;
}
这是一个简单的函数,它返回玩家的生命值作为一个整数——在 Objective-C 中的 Int 等价物。
函数/方法的结构在 Objective-C 中如下所示:
- (return_type) method_name:( argumentType1 )argumentName1
joiningArgument2:( argumentType2 )argumentName2 ...
joiningArgumentN:( argumentTypeN )argumentNameN
{
function body
}
下面是相同的函数在 Swift 中的样子:
func getPlayerHealth() -> Int {
return player.health
}
//How we'd use the function
var currentHealth : Int = 0
currentHealth = getPlayerHealth()
这就是 Swift 中函数的结构:
func function_name(argumentName1 : argumentType1, argumentName2 : argumentType2, argumentNameN : argumentTypeN) -> return_type
{
function body
}
注意我们如何使用关键字 func 来创建一个函数,以及参数/参数名称的顺序是先类型后名称,由冒号(:)分隔,并放在括号内。
下面是一个典型的 Swift 中 void 函数的例子。void 类型的函数是一个不返回值的函数。
//with a Player type as a parameter
func displayPlayerName (player:Player){
print(player.name)
}
//without any parameters; using a class property
func displayPlayerName(){
print(currentPlayer.name)
}
在 void 函数中,不需要写 ->returnType,即使没有参数,我们也要在函数名称的末尾放入 () 括号。
Tuples
Swift 的一个相当强大的特性是函数返回类型(以及常量/变量)可以包含值的组合到一个单一值中。这些组合被称为 元组。下面是一个未命名的元组示例:
let http503Error = (503, "Service Unavailable")
下面是一个从苹果的 Swift 文档中直接使用的作为函数返回类型的元组示例。注意它使用了我们迄今为止学到的大部分内容:
func minMax(array: [Int]) -> (min: Int, max: Int) {
var currentMin = array[0]
var currentMax = array[0]
for value in array[1..<array.count] {
if value < currentMin {
currentMin = value
} else if value > currentMax {
currentMax = value
}
}
return (currentMin, currentMax)
}
Excerpt From: Apple Inc. "IOS Developer Library". https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Functions.html#//apple_ref/doc/uid/TP40014097-CH10-ID164
Classes
在面向对象编程(OOP)中,类构成了对象的基本框架,包括其功能以及与其他类、对象和各种数据结构(如协议、Structs、扩展、泛型和枚举)的交互。在接下来的章节中,当我们开始构建我们的游戏时,我们将更深入地探讨所有这些概念,但就目前而言,让我们先了解类的基本知识以及它们在 Swift 中与 Objective-C 和其他语言的不同之处。
下面是 Swift 中类的基本结构:
//(a)
Global-project wide properties/variables
//(b)
class className : parentClassName, protocolName…protocolnName
{
//(c)
class scope properties
//(d)
initializers (init(), convenience, required, etc)
//(e)
func function_name1(argumentName1 : argumentType1, argumentName2 : argumentType2, argumentNameN : argumentTypeN) -> return_type
{
function-scope variables and body
}
.
.
.
func function_nameN(argumentName1 : argumentType1, argumentName2 : argumentType2, argumentNameN : argumentTypeN) -> return_type
{
function-scope variables and body
}
//(f)
deinit()
} // end of the class
//(g)
global-project wide properties/variables (alternative position)
Swift 的类结构在某种程度上类似于我们在 C# 和 Java 中看到的,而不是 Objective-C 的两个文件(.h/header,.m/.mm/实现)的设置:
-
(a): 我们可以在类声明之外拥有属性(如变量、常量、Structs 和枚举),这将使它们具有全局作用域,即在整个项目/游戏/应用程序中可访问。 -
(b): 这是我们命名.swift文件时所表示的实际类。再次强调,这与 Objective-C 为单个类提供的classname.h - classname.m/.mm双文件设置不同。一个类可以是另一个类的子类。在 Swift 中,我们不必声明父类或基类。我们创建的类可以是它们自己的基类。我们可以通过从 NSObject 继承来创建类似于 Objective-C 类的类。这样做的好处是获得 Objective-C 运行时元数据和功能,但我们会因为额外的“负担”而牺牲性能。我们可以在parentClass的同一位置或parentClass冒号:之后声明这个类将遵守哪些协议。我们将在本书的后面部分更详细地讨论协议,但可以简单地将它们视为确保你的类使用与协议规定相同的函数。 -
(c): 这些是我们将放置变量、常量、结构体、枚举和对象的地方,这些对象对于在类的范围内使用是相关的。 -
(d): 初始化器是我们用来在部分(c)中设置属性的特殊函数,当其他类和数据结构通过className(initializer parameters)使用该类的实例时。在我们构建游戏时,我们将在下一章中更详细地讨论初始化器。它们不必放在类的顶部,但将它们放在那里是一个好的实践。 -
(e): 这些是声明和开发你的类函数的地方。我们可以有被称为类函数的函数。这些函数用关键字class func标识。简而言之,类函数是类整体的一部分,而不是类的实例。将它们放在下一个更常见的函数类型(即公共函数)之上是一个好的实践,这些公共函数可以通过点操作符(即className.function(parameters))被其他类和属性访问。与 C# 和 Java 一样,我们可以使用private func关键字创建私有函数,这些函数只能被类的自身函数和属性访问。 -
(f):deinit()函数是一个特殊的可选函数,用于处理我们如何通过内存管理和消除所谓的内存泄漏来清理由我们的类分配的数据。Apple 的 ARC (自动引用计数)处理了大部分工作,但有时我们不得不在各个属性前使用诸如 weak 和 unowned 这样的关键字,以确保它们在使用后不会继续存在。这是一个相当复杂的话题,但值得深入研究以避免游戏中的内存泄漏。自动引用计数(ARC)确实负责处理大部分工作,但你的游戏中可能存在一些可能长时间存在的对象。强烈建议阅读苹果公司关于此主题的官方文档,因为 iOS 中的内存管理始终处于不断发展之中。你可以在
developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html查看关于 ARC 和 Swift 中内存管理的完整文档。 -
(g): 如果我们愿意,我们也可以在.swift文件的底部,在类声明之后,拥有全局属性。苹果自己的游戏示例,冒险 (developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Properties.html),将全局属性放在了这个位置。
摘要
关于 Swift 编程语言的内容远不止这里所能涵盖的。在本书的整个过程中,我们将根据即将到来的游戏编程需求,穿插一些关于 Swift 的额外信息和细微差别。
如果你希望更精通 Swift 编程语言,苹果实际上提供了一个名为 游乐场 的出色工具。
游乐场是在 2014 年 6 月的 WWDC14 上与 Swift 编程语言一起引入的,它允许我们在不创建项目、构建和运行的情况下测试各种代码输出和语法,在很多情况下我们只需要调整几个变量和函数循环迭代。
在官方 Swift 开发者页面 (developer.apple.com/swift/resources/) 上有许多资源可以查看。
下面是两个非常推荐的游乐场:
-
导游游乐场 (
developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/GuidedTour.playground.zip): 这个游乐场涵盖了本章中提到的许多主题以及更多内容,从 Hello World 到 泛型。 -
气球游乐场 (
developer.apple.com/swift/blog/downloads/Balloons.zip): 气球游乐场是 WWDC14 的关键演示游乐场,展示了游乐场提供的许多功能,尤其是制作和测试游戏。
有时,学习编程语言的最佳方式是测试实时代码,这正是游乐场允许我们做的事情。
除了在我们的游戏中测试代码片段之外,iOS 9 还允许我们规划和构建我们的游戏,这是下一章的主题。
第二章. 使用 iOS 9 Storyboard 和 Segues 构建和规划游戏
电子游戏开发有着有趣的历史。它始于电气工程和计算机科学的分支。游戏是工程师们充分利用有限硬件的巨大挑战,当然,还要制作出有趣的东西。今天,电子游戏和电子游戏开发仍然建立在技术、数学和工程的基础上,但几十年来,也一直是娱乐、叙事和媒体领域的主要参与者。
不论你是大型工作室、小型团队,还是独自制作游戏,规划和构建你的游戏项目可以为你提供在开发过程中节省时间的基础,如果是在团队中,可以将工作分配给他人,当然,还可以将你的游戏制作得尽可能接近你想象的样子。
从 iOS 5 开始,Apple 从娱乐行业借鉴了如何构建和规划项目的方法,无论是大项目还是小项目;通过使用 Storyboard 的概念。Storyboard 是项目各种步骤和结构的图形表示;无论是动画、电影,还是我们这里的 iOS 游戏。Storyboard 将图形化地展示制作或应用程序的流程。例如,在动画中,Storyboard 用于细化制作的主要帧或故事点。一旦就场景中的事件序列达成一致,动画师将围绕这些关键点进行动画制作。根据制作是预录音还是 ADR,配音也可以放入 Storyboard 流程中,这为动画师提供了更多具体的内容来工作。
在实际的游戏应用程序中,Storyboard 可以表示游戏的主要部分,例如开场场景、开始菜单屏幕、暂停屏幕、游戏结束屏幕,或者主游戏级别的通用外观。Apple 在 Xcode 中将这些结构命名为 Storyboard,它们之间的路径被称为 Segues。在本章中,我们将探讨如何在制作游戏应用程序时利用这些功能。

上文是一个简单的 iOS Storyboard 示例。
模型-视图-控制器
在我们深入探讨 iOS 9 中的 Storyboard 之前,最好我们先讨论一下 iOS 应用程序的基本流程以及 模型-视图-控制器(MVC)的概念。模型-视图-控制器是软件工程、编程,甚至在现在的网页设计中使用的一种架构范式。我们可以将 MVC 的模型部分视为应用程序行为的逻辑或 大脑。这种逻辑通常独立于用户界面,并决定如何处理应用程序的数据。
我们实际上已经讨论了 MVC 的模型部分!前一章中讨论的 Swift 编程语言就是那个模型;这与它的 Objective-C 前身以及 iOS 或任何其他游戏开发中使用的任何其他编程语言都是如此。你的游戏代码控制玩家、关卡和敌人/目标数据应该做什么。
MVC 中的视图部分是模型的视觉表示。这当然包括我们游戏中的许多视觉方面,从玩家的动画帧、HUD 上的各种游戏统计信息、粒子效果等等。
MVC 中的控制器部分可以被视为将模型和视图粘合在一起的“胶水”。它也是用户与游戏交互的点。无论是动作,如按钮点击、基本的触摸、滑动或其他由 iOS 设备识别的手势,控制器都会接收用户输入,操作模型,然后模型根据相应地更新视图。

此图来自苹果自己的冒险游戏示例。
当我们与 iOS 应用程序合作时,代码和故事板信息的第一个推荐入口点是根视图控制器。正如我们将要发现的,MVC(模型-视图-控制器)在 iOS 应用程序开发和 Xcode 集成开发环境(IDE)中是固有的。故事板是一系列不同类型的视图控制器,它们具有不同的任务,并通过转场(segues)相互连接。
iOS 应用程序的生命周期
在我们继续处理故事板、转场和游戏应用程序的基础之前,最好回顾一下 iOS 应用程序的整体生命周期,因为了解我们代码的入口点和应用程序的各种对象/结构非常重要。
在我们继续处理故事板、转场和游戏应用程序的基础之前,插入应用程序生命周期图。最好我们回顾一下 iOS 应用程序的整体生命周期,因为了解我们代码的入口点和应用程序的各种对象/结构非常重要。

任何使用过 C/C++、Java 或其他语言的人都会熟悉 main() 函数。main() 函数用于指定程序的入口点。前面的例子是苹果为应用程序指定的典型 main() 函数。本质上,这是在调用 iOS 应用程序典型生命周期中的第一个类,即 AppDelegate 类。
main() 函数
这里是带有 main() 函数的代码:
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[])
{
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
//Objective-C example of the Main() function
注意 main() 函数是如何用 Objective-C 编写的。Swift 再次使声明应用程序的入口点变得简单。
@UIApplicationMain
class firstClassCalled
{
//class code
}
在使用 Swift 构建 iOS 应用程序时,先前 Objective-C 项目中看到的 main.m 文件不再需要。相反,我们在类声明的开头使用一个属性调用,@UIApplicationMain。
注意
Swift 属性
以 at 字符 @ 开头的属性用于向声明或类型添加附加信息。在 Swift 中,它们具有以下语法:
@attribute name
@attribute name(attribute arguments)
在其他编程语言中,属性根据其功能,可以用来描述对象、函数,甚至整个类。
例如,@objc 属性用于声明在 Objective-C 中可读的代码。
正如我们将在使用和将故事板中的各种对象与我们的代码链接时看到的那样,属性 @IBOutlet 和 @IBAction 用于描述我们在 Xcode 的 Interface Builder 中创建的对象和函数。
我们将在第七章发布我们的 iOS 9.0 游戏中进一步讨论属性。
UIApplication 类/对象
UIApplication 是负责控制应用程序的事件循环以及处理其他高级应用程序流程的对象。无论游戏应用程序与否,它都存在于所有 iOS 应用程序中,并在主入口点首先被调用,并与 AppDelegate 类协同工作。尽管可以子类化 UIApplication,但通常不推荐这样做。对您的游戏在应用程序各种状态下的行为进行定制是我们使用 AppDelegate 类和 ViewControllers 的原因,即使没有使用故事板(也就是说,如果您选择主要使用硬编码来编写游戏)。
AppDelegate 类
我们可以将 AppDelegate 类视为您应用程序的主枢纽。它是您游戏的一般自定义的最高级别。在用 Swift(游戏或非游戏)制作应用程序时,它被赋予 @UIApplicationMain 属性,因为它是您游戏模型/代码的一般首次入口。
这里是 Apple 几乎为每个在 Xcode 中预设的 iOS 应用程序提供的代码:
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}
func applicationDidEnterBackground(application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(application: UIApplication) {
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
}
这是 Apple 在 iOS 9 游戏预设中为我们提供的直接代码和注释(截至 Xcode 6.4)。在我们深入使用故事板和两个主要框架(SpriteKit 和 SceneKit)来构建游戏结构之前,最好了解这个类中发生的事情。与设备相关的事件,尤其是那些在玩家控制之外的,如来电、通知以及由于低电量而关闭设备,以及那些由玩家控制的(即暂停游戏)事件,都由这个类处理。正如我们所见,Apple 已经为这个类的每个函数提供了很好的说明,所以请务必查看它们。我们将在创建游戏和处理那些特定情况时回到这些说明。请注意,AppDelegate类有一个可选变量(意味着它可以设置为 nil),名为window,其类型为UIWindow。UIWindow对象是UIView的子类,可以分配各种显示/对象,这些对象可以被放入用户的视图中。技术上,我们可以直接在代码中使用UIWindow和UIView的对象来创建游戏的视觉效果,但 Apple 提供了更健壮的对象,这些对象可以处理用户与屏幕和视图的交互。这些对象构成了 iOS 故事板,被命名为ViewControllers。
视图控制器
视图控制器可能是 iOS 开发中最关键的架构之一,也是 Xcode 的 Interface Builder 在设计时在视觉上所表示的内容。就它们典型的入口顺序而言,是MAIN --> AppDelegate --> RootViewController --> [调用任何额外的ViewControllers实例]。
当我们在 Xcode 中创建一个新的应用项目时,Apple 会为我们创建一个默认的根视图控制器,命名为ViewController。以下是它的代码:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
这是 Xcode 中默认提供的ViewController.swift类作为起始代码。正如我们所见,它是UIViewController的子类,因此继承了其父类的所有功能。其中之一就是这里显示的viewDidLoad()函数。在 Swift 中,当我们希望覆盖父类的一个函数时,我们在函数声明前使用关键字override。我们还可以看到super.viewDidLoad()也被调用了。这样做的作用是在我们添加自己的代码/自定义之前调用父类自己的这个函数版本,当使用UIViewController的任何函数时,这是推荐的。UIViewController函数处理各种视图状态;viewDidLoad()处理视图首次加载时的情况,在应用的生命周期中,UIViewController对象只会被调用一次。如果我们想在每次视图可见时调用一些代码,我们可以使用UIViewController的viewDidAppear()函数代替。
这是对这些视图状态的一种视觉表示。

正如我们将看到的,故事板和转场基本上为我们提供了这些状态及其之间的转换的视觉和可定制表示,而不需要编写太多代码。
要深入了解 UIViewController 方法,请查看苹果关于该主题的文档:
developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/
注意
对于熟悉游戏开发引擎 Unity(其脚本用 C#、JavaScript 或 Python 衍生物编写)的人来说,我们可以想象 UIViewController 函数 viewDidLoad() 和 viewDidAppear() 分别类似于 Unity 函数 Awake() 和 OnEnabled()。一个函数在场景首次加载时被调用,另一个则在对象可见/启用前的第一帧被调用。然而,UIViewController 函数是基于整个应用程序的更高级别,而不是基于每个 gameObject。
想要了解更多关于整个 iOS 应用生命周期和图形信息,请查看完整的文档:
视图控制器类型
视图控制器有多种类型,我们可以通过继承它们来创建自己的视图控制器。主要有两种类型:容器视图控制器,它包含其他视图控制器,以及内容视图控制器,正如我们可以想象的那样,它们是显示内容的地方。内容视图控制器包括 RootViewController,这是在应用程序的入口点之后访问的第一个视图控制器,也是预设 Xcode 项目中默认 Main.Storyboard 文件中看到的第一个视图控制器。还有其他特殊类型的视图控制器,如用于以表格单元格格式显示数据的 UITableViewController 和控制应用程序在视图控制器之间移动时的导航逻辑/图像的 NavigationController。
要更深入地了解 UIKit 中可用的各种视图控制器,请查看以下官方文档:
实际上,正是在这一点上,我们可以开始编写我们的游戏代码,尽管是完全编程 MVC 模型。在 iOS 游戏开发的初期,这基本上是开发原始 iPhone 游戏的方式。我们会通过编程方式与UIWindow和ViewController对象以及我们游戏的自定义类一起工作来构建应用。随着 iOS 设备家族的壮大,一个明显的问题开始出现。尽管我们可以,有时可能不得不根据设备编程地更改代码,但处理不断增长的屏幕尺寸和设备类型使得我们的代码总是需要重构,并且每当宣布一款新的 Apple iOS 设备时,都会产生越来越多的模糊性。此外,别忘了游戏开发与视觉设计师/动画师的工作一样,也是程序员的工作。如果完全通过代码进行编辑、定位、精炼和更新游戏的各个视觉方面,可能会非常耗时。
故事板是为了帮助解决这一问题而设计的,它允许我们在项目中直观地设计我们的游戏,而不是拥有我们自己的可能手写的仅描述基于模型、以代码为中心的设计的故事板。随着 Xcode 5 中自动布局的引入,我们可以不使用任何代码,为所有类型的 iOS 设备制作一个项目和通用视图。当我们现在终于转向与故事板和转场一起工作时,我们将涉及自动布局,但为了更深入地了解自动布局,请查看 Apple 开发者门户上的官方文档:developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG。
故事板和转场
让我们现在终于开始使用这些工具,并学习在更广泛的 Storyboard 级别上结构化游戏应用的基础知识。截至本书编写时,可用的最新 Xcode 版本是 7.0。这将是我们将使用的版本,但 Xcode 总是在更新,甚至有一个单独的测试最新功能的测试版。
访问developer.apple.com/xcode/下载并了解 Xcode 为 iOS 开发者提供的一切。
要开始使用故事板来结构化你的应用,请遵循以下说明:
-
首先,在你的
Applications文件夹中打开 Xcode(或者如果你将其放在 Dock 中以方便访问,就在 Dock 中打开)。 -
接下来,点击创建一个新的 Xcode 项目。
![故事板和转场]()
-
现在,你将需要选择一个模板预设。
-
为了仅仅理解故事板和转场,选择单视图应用模板。(不要担心,我们将在下一章中使用游戏模板)。
![故事板和转场]()
-
现在我们选择我们的项目选项。将你的项目命名为
StoryBoardExample。 -
在语言下拉菜单中,确保它设置为Swift,并确保设备下拉菜单设置为通用。
-
应该还有其他字段由 Xcode 填写,例如你的组织名称和组织标识符。这些与你的应用程序部署时将发布的信息以及你的代码版权注释的内容有关。我们现在可以保持这些默认设置,即 Xcode 已填写的设置。
![故事板和转场]()
-
点击下一步,然后选择一个有效的位置来保存此项目。
现在我们已经通过模板创建了默认应用程序。我们应该在左侧的文件导航器面板中看到为我们创建的各种文件和文件夹。正如我们所看到的,AppDelegate.swift和ViewController.swift文件已经为我们自动创建,在其下方,我们会找到Main.Storyboard文件。这是我们的故事板,当你点击它时,你应该在 Xcode 窗口的中心看到两个面板打开。左侧是视图控制器场景下拉菜单,它显示了由提供的视图控制器控制的场景层次结构。中心右侧的面板允许我们直观地看到视图控制器以及我们可以放置其中的元素。故事板的主要视觉部分可以放大和缩小。当我们向其中添加更多场景时,这将允许我们看到整个故事板或我们正在工作的部分。
你可能需要稍微缩小一些才能看到它(使用鼠标或使用 MacBook 上的捏合手势),但在视图控制器场景的左侧有一个灰色箭头。这是入口点,与这个箭头相连的第一个视图控制器场景是你的RootViewController/初始场景。
小贴士
当你想要为你的故事板添加更多场景,无论是为了调试目的还是设计选择,你可以简单地通过点击并拖动该场景左侧的箭头来更改第一个进入的场景。
让我们先为我们的故事板创建一个单独的场景:
-
在实用工具面板的底部(Xcode 项目的最右侧面板),有四个图标代表我们可以放置在我们项目代码和故事板中的各种代码片段和对象。如果第三个图标尚未选中,请点击它。这将打开对象库。
![故事板和转场]()
-
我们可以看到对象库的顶部有一个视图控制器对象。
![故事板和转场]()
-
将其拖动到故事板的画布上,最好拖动到初始场景的右侧。
注意
如果实用工具面板没有打开,请点击项目工具栏窗口右上角的最图标。
![故事板和转场]()
注意
工具栏中的三个按钮可以切换以关闭导航面板、调试面板和实用工具面板。在适用的情况下关闭这些面板可以帮助扩展一般视图,即你的故事板场景的画布。
现在我们故事板中有两个场景,但没有任何东西告诉我们它们是什么。它们只是两个空场景!
让我们在这些场景中放置一个标签对象来表示它们是什么,并在运行时告诉我们我们处于哪个场景。
为了保持开发游戏的思维模式,让我们在第一个场景中放置一个名为Intro Scene的标签,在那里我们可能会有一个游戏开场动画和一个开始/选项菜单,在下一个场景中,放置标签Game Scene来表示实际游戏玩法将在这里发生。
下面是如何做到这一点:
-
前往实用工具面板底部,使用搜索字段搜索
label。这将隔离label对象,因此你不必在整个列表中滚动。![故事板和转场]()
-
将
label对象拖动到第一个场景的画布上。如果它看起来没有尝试吸附到场景的画布上,你可能需要选择该视图控制器场景层次结构中的视图部分,使用主/故事板主视图的左侧面板。或者,你也可以在检查器中双击视图以获得场景焦点,这样你就可以将标签放置在上面。![故事板和转场]()
-
当我们拖动时,尽量将标签居中。画布会用虚线表示我们处于场景的垂直和/或水平部分。将其放在中心。
选择标签时,实用工具面板应该会显示一些字段,以控制其文本的各个方面,如字体大小、对齐方式和样式。
![故事板和转场]()
-
标签默认情况下会显示为
Label,所以让我们通过在画布上双击标签本身,或者在实用工具面板中文本字段下方第二个字段中更改名称,将第一个场景重命名为Intro Scene。 -
让这个标签更加突出,所以单击标签,点击字体字段中的[T]图标,并将样式设置为粗体,大小为 28。
注意标签是如何被尺寸增加裁剪的,几乎看不见。
![故事板和转场]()
-
简单地单击标签,并展开画布上标签对象角落的任意一个八向缩放图标。
将标签重新定位以将其放回场景中心。
![故事板和转场]()
-
通过简单地按Command + D复制标签(这样就不必重复所有步骤),然后将它拖动到另一个场景的中心。根据需要缩小视图,如果焦点改变阻止你拖动标签,可能还需要单击层次结构视图部分。
虽然这相当基础,并且还需要做更多的工作,但这就是创建单独场景所需的全部。如果你有一个关于如何构建你的游戏的思路,这就是你可以使用故事板开始的地方。当然,在我们使这个故事板具有任何功能之前,这里还有更多的工作要做。
我们可以看到 Xcode 给出了以下警告:
场景由于缺少入口点而无法访问,并且没有用于运行时访问的标识符-instantiateViewControllerWithIdentifier。
这是指由于没有与 Intro 场景或应用的入口点连接而实际上成为孤儿的游戏场景对象。
这就是 segues 发挥作用的地方。然而,在我们使用 segues 并创建到这些场景和其他场景的流程之前,如果我们运行这个应用,我们会注意到另一个问题。我们确信我们已经将文本居中了,但如果我们模拟或运行这个应用,比如在 iPhone 6s 上,文本完全偏离了右上角。这是因为默认画布是通过 Auto Layout 开始的一个通用的所有设备模板。
随着 Xcode 每个新版本的发布,Auto Layout 变得更加容易,但有时仍然可以说它仍然有点麻烦,尤其是在创建约束(设置故事板对象之间的空间/边距)时。让我们快速看看如何处理约束。
解决我们这里问题的快速方法就是通过点击故事板画布底部的中心处的Base Values面板中的w/Any h/Any文本。一旦点击,就会弹出一个单元格表格。用鼠标或触控板在各个单元格上滚动,会显示与w/Any h/Any不同的多种配置。这很好,因为你可以根据设备类型简单地通过这些选项来更改/添加和删除各种对象。
注意
在故事板和 Auto Layout 之前,这会涉及到在视图控制器或 Nib 类中进行大量的测试和代码重构,以获得你想要的视觉布局。然后苹果会创建具有不同屏幕大小的下一个设备,这会变成更大的麻烦,或者开发者会在最新设备上冒游戏损坏的风险。
要使标签在所有纵向模式的 iPhone 上居中,例如:
-
将鼠标悬停在 Auto Layout 面板的中心左侧,那里会显示弹出面板/表格顶部的Compact Width | Any Height。
![故事板和 segues]()
-
这应该会改变画布底部的显示文本为w/Compact h/Any,并缩小场景的宽度,因为这个布局代表所有纵向模式的 iPhone 以及任何高度(所以它可能在较旧的 iPhone 4S 上有点不合适,与 iPhone 5 或更新的设备相比)。
![故事板和 segues]()
-
注意标签是如何偏离中心向右上方的。这就是在模拟器或实际 iPhone 的纵向模式中看到的样子。将它们拖回到中心,它们现在应该看起来像在这个 Storyboard 画布配置中看到的那样。如果为 iPad 设计,那么其他配置也需要相应更改。
注意
使用约束可以实际上简化这个过程。例如,假设你想要在游戏场景的右上角放置一个暂停按钮,并且你知道无论设备方向如何,它始终会保持一定的距离(以百分比或像素为单位)远离屏幕的右侧和顶部。我们可以点击画布底部的
固定按钮来创建这些w/Any h/Any配置的约束,并跳过手动调整每个基本配置上的图标。Xcode 已经为我们提供了一个场景,即
LaunchScreen.xib文件,如果你已经运行过你的代码,那么实际上在 Storyboard 中的第一个视图控制器之前,我们看到的就是这个文件。要在启动时只使用
Main.Storyboard文件,你可以在导航面板的左上角选择主项目文件,并在应用程序图标和启动图像部分的启动屏幕下拉菜单中选择Main.Storyboard。然后,如果你不再需要,可以删除LaunchScreen.xib文件。这个文件可以用来查看工作约束,如果需要的话,它也可以成为你游戏的初始启动画面。更多关于约束的信息可以在官方文档中找到:developer.apple.com/library/prerelease/ios/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithConstraintsinInterfaceBuidler.html。
转场
游戏有场景,所有场景之间都有过渡。转场只是 Storyboard 中场景之间的过渡。转场有多种类型:
-
显示:这种方式将下一个视图控制器推到当前视图控制器之上;如果使用
UINavigationController实例,它还允许回退。 -
显示详情:当使用
UISplitViewController时,通常在 iPad 应用中使用容器视图控制器来浏览新闻/电子邮件应用,其中页面左侧是一个UITableViewController对象,而同一页面的另一侧是该表格/列表的详情。当从UITableViewController侧的选择项触发手势时,它会调用页面DetailView控制器部分的详情。 -
模态呈现:这种方式会在当前视图控制器之上呈现下一个视图控制器,并且可以取消,例如全页弹出窗口。
-
弹出视图: 这类似于模态显示,但提供了更多尺寸选项来创建一个更小的弹出窗口,可以关闭和销毁。
-
自定义: 这是一个可以完全使用面向对象(OOP)代码定制的过渡版本。
当构建例如电子邮件应用时,典型的故事板结构很可能需要使用导航控制器和UITableView控制器来结构化数据和应用的流程。现在,我们同样可以为游戏应用做同样的事情。游戏结束、菜单、排名和暂停屏幕都可以使用这些视图控制器。在我们的例子中,我们将保持简单和不受限制,以便你,开发者,有一个更好的起点来分支。
注意
我们这里的例子相当简单,但除了提供这个项目的代码外,还将提供使用各种视图控制器和对象的更详细的故事板。
让我们来处理那个警告,并将这些场景链接起来,同时开始展示使用故事板的一个典型游戏的总体结构。
-
首先,在简介场景中,在简介场景标签下方放置一个标签为开始的按钮。在故事板上放置按钮的方式与放置标签的方式完全相同。在实用工具面板中搜索
button或向下滚动对象,然后将按钮拖放到场景中。 -
现在在游戏场景视图中创建另外两个按钮;一个按钮标签为暂停位于场景的右上角,另一个名为退出位于暂停按钮的左上角。
-
在场景中创建一个新的
ViewController对象,最好在画布上的游戏场景之上或之下。 -
在新的暂停场景中,创建一个标签
PAUSED,其方式与创建Game Scene和Intro Scene标签的方式相同。 -
然后,添加两个按钮,退出和继续,并将它们放在PAUSED标签的正下方。
现在我们可以使用故事板来创建视觉上的过渡:
-
在简介场景中,按住控制键点击开始按钮对象,然后仍然按住控制键,将对象拖向画布上的游戏场景。当你拖动时,你应该会看到一条蓝色线条跟随你的光标。(如果你需要更多空间,稍微缩小一些,并暂时使用工具栏按钮关闭导航和实用工具面板)。
-
将此点放置在视图上的任何非对象位置;这样做时,你应该会看到整个视图变蓝。
-
会弹出一个询问过渡类型的对话框。选择显示。
-
就这样!你已经创建了一个过渡,并且也已经告诉故事板,当用户点击那个按钮时,它将打开游戏场景—视图控制器。
-
在你继续创建更多过渡之前,点击画布上代表过渡的门形符号。在实用工具面板的资产检查器中,顶部右侧应该看到一个空的标识符字段。如果我们愿意,我们可以让过渡保持为空,但如果我们希望在代码中使用以下行来调用过渡,那么命名它可能是有用的:
performSegueWithIdentifier("segueIDNameeWithIdentifi) -
现在重复步骤1至3以创建以下过渡:
-
将游戏场景的退出按钮重新链接到 Intro 场景。
-
将游戏场景的暂停按钮链接到 PAUSED 场景。
-
将 PAUSED 场景的恢复按钮链接到游戏场景。
-
将 PAUSED 场景的退出按钮链接到 Intro 场景。
-
由于所有场景都已通过过渡连接,警告应该已经消失,并且在可能进行一些自动布局修复后,现在运行的应用程序具有类似游戏的场景结构,并且过渡方式与我们通常在其他游戏中看到的方式相同。我们可以从这里开始并创建其他场景,例如游戏结束场景、阶段胜利场景或其他场景。即使这可能不是你希望最终游戏过渡结束的方式(尤其是默认的 Show 过渡执行快速垂直过渡),这也可以是一个非常快速的方法来直接原型化你的游戏。自定义过渡和通过代码触发的过渡是我们深入了解并微调设置以符合我们对游戏愿景的方式。
如果你真的想深入了解过渡,这里有一些关于创建自定义过渡类的文档:
类似于我们如何通过拖动按钮的链接到下一个视图控制器场景,我们也可以对ViewController.swift文件做同样的事情。
这里是关于如何为第一个视图控制器进行总结:
-
移除之前的过渡。这样做的一种方法是在按钮上右键单击,然后在触发过渡部分点击x。
-
点击层次结构中的 Intro 场景视图以使其获得焦点。
-
从 Intro 场景视图控制器左上角的黄色图标开始,控制拖动一条蓝色线到游戏场景视图控制器,并选择 Show 类型的过渡。
-
点击画布中的过渡图标,现在给这个过渡的标识符命名为
startGame。 -
打开辅助编辑器(Xcode 工具栏右上角的两个重叠的圆圈按钮);关闭一些面板以腾出所需的空间。
-
将开始按钮拖动到
ViewController类中;最好在代码底部,但仍然在类的闭合括号内。 -
这将提示输出/动作弹出窗口。在连接下拉菜单中选择动作选项,并将其命名为
startButton。 -
这将创建
IBAction函数:@IBAction func startButton(sender: AnyObject) {}。 -
在花括号之间输入以下代码:
self.performSegueWithIdentifier("startGame", sender: nil) -
这告诉视图控制器在代码提示此按钮时执行过渡。
故事板与编码
只要遵循 MVC 模型,就没有单一正确的方式来设计你的应用程序的结构。实际上,有些程序员完全满意于只使用初始视图控制器,而从不使用任何 Nib 或 Storyboard 文件;因此,他们纯粹通过代码逻辑和程序调用各种视图对象来构建游戏。在 iOS 开发中,设计上大致分为三个主要分支:硬编码、Nibs 和 Storyboard。最初的方法是编码;Nibs 后来出现,首先允许在 Xcode 中进行直接视觉编辑,然后演变为 Storyboard,并通过添加 Auto Layout 进一步发展。
一些开发者和工作室在 iOS 应用程序的视觉结构方法上存在分歧的原因是,Nibs 和 Storyboard 的一个缺点是它们缺乏可移植性。如果你想要将你的游戏移植到另一个平台,如 Android,并且以合理的速度进行,过度使用 Storyboard 会使将应用程序移植到其他平台变得相当困难,因为这些设计功能是特定于 iOS 平台的。这时,纯代码会更有益。尽管如此,Storyboard 为我们开发者提供了一个可编辑的、可视化的应用程序/游戏表示,以及随着设备家族的变化进行少量或无变化的能力。
即使是其他游戏开发引擎,如 Unity、Unreal Engine 等,也更多地采用沙盒化、视觉表示方法,你的代码在视觉上更像是一个组件,而不是游戏角色在屏幕上渲染之前出现的所有事物的完整结构。
摘要
在本章中,我们讨论了多个应用程序项目结构和介绍主题。首先,我们讨论了所有应用程序(无论是否为游戏)都遵循的模型-视图-控制器(MVC)范式,以及遵循这种结构的 iOS 应用程序的整体生命周期。接下来,我们回顾了典型应用程序中你的代码的入口点(s)和路径,以及沿途使用的上层对象,例如应用程序系统对象、AppDelegate类和视图控制器。最后但同样重要的是,我们讨论了本章的主要主题——Storyboard、转场和创建简单游戏流程结构的说明。从这里,我们可以看到如何相对容易且快速地为游戏构建各种场景并在它们之间通过转场进行切换。再次提醒,尽管 Storyboard 被推荐使用,但它们可以简单地作为一个通向最终产品的通用指南,这给了你,作为开发者,即使最终更倾向于更密集的代码设计选择,也能可视化你的游戏。
在接下来的两章中,我们将最终开始真正编写和设计可玩的游戏。我们将从 2D 游戏开始,自从 iOS 7 以来,苹果为 iOS 开发者提供了一个自己的框架来处理 2D 精灵和游戏物理。这个框架被充分命名为 Spritekit。
第三章:SpriteKit 和 2D 游戏设计
现在我们已经了解了 Swift 编程的基础知识,iOS 应用的通用流程和类结构,以及使用故事板和切换的 app 的可选结构,我们可以继续将我们的应用转换为可玩的游戏。
对于本章,我们将从苹果专为 iOS 游戏开发者创建的 2D 游戏设计和游戏开发框架开始,这个框架被称为 SpriteKit。SpriteKit 首次在 iOS 7 中可用,旨在简化 iOS 设备系列的游戏开发过程。该框架运行一个典型的渲染循环,用于绘制和更新 2D 对象/精灵到游戏场景中。在幕后有很多事情在进行,以运行这个循环并绘制游戏精灵。幸运的是,苹果构建了第一方游戏开发框架,为我们做了很多繁重的工作。这样,我们可以更多地专注于制作游戏本身,而不必过多担心游戏如何与硬件连接和运行,这是过去开发者必须应对的问题。
每次 iOS 和 Xcode 的更新都在继续添加更多工具和框架,以改善游戏设计的便捷性,包括在WWDC15上首次引入的 iOS 9 配套框架,称为GameplayKit。GameplayKit 可以让我们分离、复制和模块化游戏逻辑,甚至可以复制用于未来游戏项目,无论是 SpriteKit 还是我们下一章的 3D 框架 SceneKit。我们将在后面的章节中详细介绍 GameplayKit。在本章结束时,我们将查看一个完整的游戏示例,其游戏玩法简单,但逻辑相对复杂。
iOS 游戏开发引擎简史
SpriteKit 和 3D 游戏框架 SceneKit 并非是 iOS 开发游戏时最初使用的方法。我们将很快看到为什么它们成为了开发者工具集受欢迎的补充。最初,我们,即游戏开发者,必须实际上直接使用 OpenGL API 与 GPU 通信,以便将 2D 和 3D 图形/顶点放置到屏幕上。在更高层次上,始终有 Foundation 和 CocoaTouch 与用户手势交互,以操作 UIKit 对象,但处理游戏开发的基本要素,如 SpriteSheets、mipmap、法线贴图、部分发射器、边界框和剔除,涉及到一定程度的底层结构。当苹果公司在 2011 年创建了他们的 GLKit 框架时,他们使这些对各种图形缓冲区和 VBO 的调用稍微容易了一些。幸运的是,各种第三方框架,如 Cocos2D、Box2D、Sparrow、GameMaker、Unity、Unreal Engine 等,通过努力保持游戏设计中的 设计 方面作为重点,使得这个过程不那么具有工程密集性。GameMaker、Unity 和 Unreal Engine 是更类似沙盒/拖放风格的引擎,类似于故事板和转场背后的思维模式,而像 Cocos2D 和 Sparrow 这样的引擎则更侧重于代码/样板 OOP 结构,从而简化了初始编码构建。Unity 和 Unreal Engine 等引擎很棒,因为它们提供了一个更上手型的沙盒环境,具有简化 MVC 模型的各种功能。这些引擎的一些缺点是它们有时是闭源的,通常需要付费才能充分利用,并且不是针对特定设备的(Unity 尤其属于这一类别)。与这些视觉引擎一起工作有时可能导致在平台特定的 IDE(如 Xcode)中需要进行优化,因为有时采用了一种 一刀切 的方法。苹果的 SpriteKit 和我们稍后将要看到的 3D API SceneKit 提供了一个第一方平台特定的中间地带,它为开发者提供了高级 API 编辑,甚至还有底层图形 API(OpenGL/Metal)的定制化。
注意
随着时间的推移,沙盒/拖放式引擎的缺点已经减少。AAA 工作室使用的引擎,如 Unreal Engine、Unity、Havok 等,已经减少了 API 与目标设备底层代码之间的上层模糊性。一个很好的例子是 Unity 的 IL2CPP,它将上层的 API 调用直接转换为快速的设备特定 C++代码。这包括利用苹果的精简 Metal API 的代码和图形管道优化。这种将上层应用程序与传统样板代码同质化的方法现在允许所有技能水平的开发者制作出惊人的游戏。这就是为什么从 iOS 8、iOS 9 开始,苹果的游戏开发框架采用了更直观的设计方法。Xcode 7 引入了游戏状态机、组件,以及在整个项目中编辑/复制和重用玩家动作和动画的能力。这允许开发者专门在 iOS/Xcode 上工作,同时利用设备无关的游戏引擎的视觉设计优势。
对于本章,我们将学习如何使用 SpriteKit 框架和更传统的样板代码方法制作一个名为SwiftSweeper的拼图游戏。这意味着我们将以代码密集/模型为中心的方式制作我们的第一个演示游戏。这不仅让我们了解了 SpriteKit 代码的内部工作原理,还让我们能够从第一章《Swift 编程语言》中更多地利用 Swift 编程语言。
我们将通过简要提及苹果最新的 SpriteKit 演示游戏 DemoBots 来结束本章,它利用了 Xcode 7 及以后的更多视觉工具/框架。然而,首先看到更密集的代码方法将使我们能够欣赏这些新工具节省的时间。
苹果为了模仿其他引擎中看到的游戏设计视觉设计方法,不遗余力,因为游戏设计在代码/逻辑方面与艺术和设计一样重要。
游戏循环
游戏循环是游戏开发者的路线图。名称根据框架和平台而异,但规则是相同的。游戏循环包括在游戏的单个帧中发生的所有方法、物理更新和绘制调用,以及它们的执行顺序。游戏开发的黄金法则是在不超过 16.6 毫秒,即每秒 60 帧的速度下,始终以完整迭代的方式运行这个循环。
游戏循环的一些方面不需要像过去那样由游戏开发者控制,尽管我们仍然有选择使用 OpenGL 或更好的苹果 Metal API 直接进行 GPU 调用的选项。我们将在稍后讨论这些话题。
下面是 SpriteKit 游戏循环的样子:

上述内容是直接从苹果开发者网站上给出的示例。我们看到在单个帧中调用了许多函数。首先迭代的是update()函数。update()函数是我们添加大部分游戏特定更新和游戏对象(如位置和角色状态)的各种检查的地方。
循环结构让我们有机会在知道帧中某些任务已经发生后进行更新,这就是didEvaluateActions()、didSimulatePhysics()、didApplyConstraints()和didFinishUpdate()函数派上用场的地方。
注意
来自 Unity 的任何人可能对其通用的游戏循环函数很熟悉,例如Awake()、Start()、FixedUpdate()、update()和LateUpdate()。SpriteKit 游戏循环允许一些类似的代码/渲染流程,但正如我们将看到的,有一些细微的差别。
关于游戏循环及其函数的更多信息,请参阅苹果文档中的以下链接:developer.apple.com/library/ios/documentation/GraphicsAnimation/Conceptual/SpriteKit_PG/Actions/Actions.html。
利用其他游戏循环方法可以确保你的游戏中某些调用不会出错,甚至可以帮助以快速、高效的方式完成每个帧的重要任务。
例如,在之前提到的公共游戏 PikiPop 中,以下是游戏如何在主GameScene.swift代码中使用游戏循环的方式:
//Update() Example
//From main GameScene.swift
override func update(currentTime: CFTimeInterval) {
//Update player
if(player?.isPlayable==true){
player!.update(currentTime)
}
}
上述代码首先检查玩家是否可玩,使用的是isPlayable布尔值。这种状态可以意味着许多事情,比如玩家是否存活,是否在生成,等等。游戏循环的update()函数,它覆盖了SKScene对象的父update()函数,接受一个时间实用类型CFTimeInterval的参数。CFTimeInterval是一种特殊的 Core Foundation 双精度类型,以秒为单位测量时间,因此在每个间隔期间更新玩家对象(如果非空)。
作为 PikiPop 的简要总结,它是一个类似于 Flappy Bird 的程序化 2D 横版滚动游戏。与 Flappy Bird 不同,Piki 可以根据玩家的点击和滑动在所有方向上穿越游戏。Piki 可能会被困在舞台对象和舞台边缘之间。

上述图像显示了 Piki 如果被推到屏幕的左侧,会受到伤害。
该游戏阶段中的边缘使用的是 SpriteKit 自己的特殊对象,名为SKConstraints。关于这些内容的更多介绍将在后面提供,但简而言之,它们决定了 SpriteKit 精灵可以采取的范围和方向。SpriteKit 中的精灵(包括开发者定义的对象,如 PikiPop 的玩家对象和默认的SKSpriteNode)都是来自SKNode对象,这些对象与SKConstraints和其他基于物理框架的功能一起工作。
我们可以在游戏循环的 update() 部分检查 Piki 是否被推到角落,但由于约束是框架物理架构的一部分,最好在 SKScene 渲染循环的 didSimulatePhysics() 部分进行此检查,如下所示:
override func didSimulatePhysics() {
//run check on Player
let block: (SKNode!, UnsafeMutablePointer<ObjCBool>) ->
Void = { node, stop in
/*checks if the node is the player and is moved/crushed to the left by a physics object. This is done by comparing the node's position to a position that is, in this case, less than 26% off the left side of the screen; calculated by multiplying the screen's width by 0.26 */
if let playerNode = node as? Player{
if (playerNode.position.x < self.frame.size.width*0.26 && playerNode.isPlayable) {
playerNode.playerHitEdge()
}
}
}
...more code
这段代码的第一部分,let block: (SKNode!, UnsafeMutablePointer<ObjCBool>) -> Void = { node, stop in,使用的是所谓的块或闭包语法,Swift 允许我们以相当动态的方式使用它。目前不必关心这类代码的细节;只需注意,在这一部分的游戏循环中,我们检查玩家的位置在 x 方向上与窗口框架边缘的位置。
注意
这里是关于在 Swift 中编写块/闭包的更多信息:
Tile game – SwiftSweeper
是时候停止谈论 SpriteKit 了,让我们直接进入正题!正如本章开头所述,我们将首先向您展示如何使用稍微复杂一些的样板/代码驱动式设计在 SpriteKit 中制作一个看起来简单的瓦片游戏。不用担心,这不会涉及到像早期控制台游戏开发者那样直接调用 GPU 和处理极小的内存需求。然而,我们将使用大量的与 SpriteKit 对象、函数和类相关的代码调用。诚然,随着 Apple 继续在 Xcode 中提供更多以设计为中心的功能,直接编写代码的开发者责任正在逐渐减少。
了解代码结构可以让你在采用更自上而下的方法进入的开发者中占据优势,并且编写代码始终会落后于自定义游戏逻辑。

SwiftSweeper 是什么?
SwiftSweeper 是一个完全用 Swift 编写的经典瓦片拼图游戏 MineSweeper 的克隆版。SwiftSweeper 利用 Swift 使用 Unicode 表情符号的能力,这样我们就不需要使用许多图像资源,这应该为我们制作自己的具有难度级别的瓦片/拼图游戏提供一个很好的起点。
我们将从零开始构建大部分游戏,但完整的源代码可以在github.com/princetrunks/SwiftSweeper找到。
注意
在本书撰写时,这是在 Xcode 7 测试版(7A120f)中构建的,针对最初的 iOS 9 版本进行了优化,并针对 iPhone 进行了优化。
游戏的目标是在不碰到隐藏在游戏板上的地雷的情况下点击游戏板上的每一个方块。不过,你也会得到一些帮助。不是地雷的每一个方块都会告诉玩家周围有多少个地雷。如果玩家通过排除法确定某个方块肯定有地雷,他们可以在该方块上插上旗帜,以确保不会点击那个空间。点击所有不是地雷的方块以赢得游戏!SwiftSweeper甚至还会保存你赢得每个难度级别所需的时间,为游戏增添一些可玩性。
创建我们的 SpriteKit 游戏
既然我们已经知道了游戏的目标,以下是我们在 SpriteKit 中构建游戏的方法:
-
首先,打开 Xcode 并创建一个新项目。
-
现在选择游戏模板,并点击下一步。
![创建我们的 SpriteKit 游戏]()
-
接下来,填写产品名称。我们将把这个项目命名为
SwiftSweeperExample,并确保语言设置为 Swift,同时将游戏技术选为SpriteKit,并将设备设置为 iPhone。![创建我们的 SpriteKit 游戏]()
-
然后,点击下一步,我们现在有一个全新的 SpriteKit 游戏项目,其中已经为我们准备好了许多文件,以便我们开始。
-
现在点击导航面板中的项目主文件,并在设备方向字段中取消选择除了纵向以外的所有选项。
![创建我们的 SpriteKit 游戏]()
-
由于我们主要会使用代码,我们可以暂时忽略或删除
GameScene.sks文件。这些文件是 Xcode 为你提供的用于视觉设计游戏场景的选项。当我们使用更具有视觉设计的 SpriteKit 游戏示例工作时,我们将对这类文件有更深入的了解。 -
构建并运行应用程序,以查看 Apple 的默认 SpriteKit 项目,其中以 Chalkduster 字体写着
Hello World,并在您点击或触摸屏幕的地方出现一个旋转的宇宙飞船。
SpriteKit 结构和对象的概述
在我们添加代码之前,让我们使用这个模板来了解 SpriteKit 的基本对象、函数和流程。
正如我们在上一章中所述,AppDelegate.swift是主入口点。代码随后移动到GameViewController.swift,它是UIViewController类的一个子类,并导入了 SpriteKit 框架。以下代码是在GameViewController的viewDidLoad()函数中编写的:
override func viewDidLoad() {
super.viewDidLoad()
if let scene = GameScene(fileNamed:"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)
}
}
小贴士
下载示例代码
您可以从您在www.packtpub.com的账户下载示例代码文件,以获取您购买的所有 Packt Publishing 书籍的示例代码。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
使用关键字override,这个版本的viewDidLoad()现在可以添加到或完全覆盖父类的功能。super.viewDidLoad()调用父类的原始功能,然后执行它自己的自定义功能。这就是 Swift 处理面向对象继承概念的方式。
接下来,我们看到如何使用GameViewController首先创建游戏场景。SpriteKit 的一个主要方面是它在SKScene类的场景上工作,这些场景本身是SKNode类的子类。SKNode类是 SpriteKit 中几乎所有对象的主体构建块。无论是精灵、灯光、视频、效果、物理场、音频文件(SKAudioNodes)、相机(SKCameraNodes)还是标签/UI 对象,它们都是SKNode类。这些对象都持有重要信息,最重要的是对象节点家族的坐标信息。对于游戏,这允许开发者创建自定义类,例如Enemies、GameLights、Tiles等,这些类在父节点和子节点上都有屏幕和其他信息。例如,我们可以通过在父Enemy类中调用继承的功能来攻击屏幕上的每个敌人。我们不需要检查每个单独的敌人类型,而是通过在SKScene的各种游戏循环函数中遍历父节点来枚举:
enumerateChildNodesWithName("player", usingBlock: block)
你还记得 PikiPop 中的块/闭包调用吗?为了在SKScene的didSimulatePhysics()函数中实际使用它,我们调用SKNode的enumerateChildNodesWithName函数来仅针对场景中的那些节点,并让该代码块为场景中具有该名称的每个成员运行。
playerNode.name = "player"
名称只是一个可以用SKNode.name属性设置的字符串。让每个自定义节点以一个给定的名称(或在游戏过程中更改),你就有了一组可以在场景中单独识别的对象。
你可以在 Apple 的官方文档中找到更多关于SKNode的信息,链接为developer.apple.com/library/ios/documentation/SpriteKit/Reference/SKNode_Ref/。
场景过渡和代码、故事板以及/或 SKS 文件的选择
我们项目中的GameScene.swift类继承自SKScene,游戏循环/渲染函数就是在这里发生的。SpriteKit 在场景上运行,并且可以从场景中过渡和跳转。
在上一章中,我们展示了如何使用故事板和切换来构建游戏。SKScene 使得你甚至不需要使用故事板,只需直接使用代码进行切换。我们可以使用故事板,也可以使用 .sks 文件或三种方法的组合来视觉设计每个单独的场景。使用代码,SKScene 可以通过 SKTransition 对象和函数进行切换。实际上,正如我们将通过 SwiftSweeper 看到的,我们只需使用代码手动刷新场景中的资源来执行 切换。这种方法相当过时,并且不如 SKTransition 故事板和 SKS 文件优雅,所以让我们快速了解一下如何使用 SKTransition、故事板以及通过代码简要地进入 SKS 文件进行场景切换。稍后,在下一章中,我们将更多地关注视觉 SKS 文件,因为每次 iOS 和 Xcode 的更新都会继续关注这些视觉工具,以缩短编码时间和工作流程。
一个 SKTransition 示例
以下代码更改了游戏的场景:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesBegan(touches, withEvent: event)
if let location = touches.first?.locationInNode(self) {
let touchedNode = self.nodeAtPoint(location)
if touchedNode.name == "SceneChangeButton" {
let transition = SKTransition.revealWithDirection(SKTransitionDirection.Up, duration: 1.0)
let scene = AnotherGameScene(size: self.scene!.size)
scene.scaleMode = SKSceneScaleMode.AspectFill
self.scene!.view!.presentScene(scene, transition: transition)
}
}
}
SKTransition 类实际上只是切换类型。正如前面的代码所示,切换是一个使用 SKTransitionDirection.Up 枚举类型的方向性切换到下一个场景。正如我们在 GameViewController 中看到的,新场景是通过控制场景视图大小和宽高比的类似函数创建的,然后使用 self.scene!.view!.presentScene(scene, transition: transition) 将场景展示给未包装的视图。
还要注意,这个过程发生在与我们在当前项目中的 GameScene.swift 类中看到相同的函数中,即 override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?){}。这是处理玩家触摸手势并检查触摸的节点名称是否与 SceneChangeButton 字符串匹配的函数。
更多关于 SKTransition 以及你可以为你的游戏提供的其他精美切换效果,可以在官方文档中找到:
developer.apple.com/library/prerelease/ios/documentation/SpriteKit/Reference/SKTransition_Ref/
注意
截至 Swift 2.0 / iOS 9,此触摸代理函数通过 touches: Set<UITouch> 接收一个参数,它是一组 UITouches,以及一个可选的 UIEvent。这与之前的 Swift 迭代不同,并且可能会在未来更新中发生变化。
一个 SKScene/Storyboard 示例
这里是一个 SKScene/Storyboard 示例的代码:
@IBAction func buttonPressed(button:UIButton)
{
// Remove button from the view
button.removeFromSuperview()
if let scene = GameScene.unarchiveFromFile("GameScene") as? GameScene {
// Configure the view.
let skView = self.view as SKView
skView.showsFPS = false
skView.showsNodeCount = false
//used for optimization of SKView
skView.ignoresSiblingOrder = true
scene.scaleMode = .AspectFill
skView.presentScene(scene)
}
}
正如我们在上一章中看到的,使用故事板文件的视觉帮助可以为我们提供通往我们应用程序的视觉路线图,无论是游戏还是非游戏。前面的代码使用一个链接到故事板文件中的 @IBAction 链接来设置一个新的场景。
游戏中的故事板在当我们只知道游戏的一般结构时可以用于原型设计阶段,并且对于游戏的菜单导航甚至对于所有单个游戏场景都是完美的***。
在过渡之前,通过调用button.removeFromSuperview()来移除按钮本身,以防止新场景绘制在可能未被看到的菜单按钮上——对玩家来说是看不见的,但对游戏的内存堆栈来说不是。
小贴士
**通常,最好只使用 Storyboard 来设计整体导航菜单,而不是每个单独的水平/场景。SKScene和SKNode的功能可以让我们重用相似的场景结构,并为类似结构的水平节省大量的编码工作。拥有许多级别的游戏可能会将我们的 Storyboard 变成一个令人困惑的结构网,从而取消其最初的目的。实际游戏场景可以仅在其自己的单个视图控制器中存在于 Storyboard 中,而暂停、分享和其他菜单将由 Storyboard 的 segues 控制。
使用 SKS 文件的 SKScene 过渡
.sks文件是一种特殊的 SpriteKit 场景文件,它允许以视觉、拖放的方式创建场景以及放置玩家、粒子、敌人和关卡资源。在 Swift 中将过渡到视觉设计的.sks文件与我们的初始SKTransition示例相同。
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
/* Called when a touch begins */
let introNode = childNodeWithName("introNode")
if (introNode != nil) {
let fadeAway = SKAction.fadeOutWithDuration(1.0)
introNode?.runAction(fadeAway, completion: {
let doors = SKTransition.doorwayWithDuration(1.0)
let gameScene = GameScene(fileNamed: "GameScene")
self.view?.presentScene(gameScene, transition: doors)
})
}
}
使用SKScene初始化器fileNamed创建gameScene常量,然后将该场景展示给视图,无论是.swift文件还是.sks文件,其工作原理都是相同的。这使我们能够既通过代码又/或通过视觉设计我们的游戏场景。在SwiftSweeper的情况下,我们将采用更侧重于代码的方法,但如果您愿意,也可以使用更多的代码、Storyboard 和/或视觉设计的SpriteKitScene(.sks)文件来构建这个游戏。
资产、精灵和图标
截至 Xcode 7,游戏资源放置在Assets.xcassets文件夹中。Xcode 的早期版本可能有一个Images.xcassets文件夹用于游戏的图标和精灵,但这一情况已经改变,并且可能会随着每个新的 iOS 版本而继续变化。

来自 Apple WWDC15 会议的一张图片
从 iOS 9 和 Xcode 7 开始,assets文件夹获得了更多的灵活性,能够处理各种应用图标大小、启动图像、图像集和精灵图集。这也允许我们使用 iOS 9 中引入的各种内存节省功能进行开发,如应用切片/应用瘦身和按需资源。应用切片/瘦身功能确保只有与设备相关的资产被下载,这可以在玩家的 iPhone 或 iPad 上节省空间。按需资源允许我们标记在游戏中的特定部分才存在于设备内存中的资产。这样,我们就可以为玩家创建更大的游戏体验,而不会耗尽苹果设备家族中有时有限的存储空间。
你可以在developer.apple.com/library/prerelease/ios/documentation/IDEs/Conceptual/AppDistributionGuide/AppThinning/AppThinning.html上找到更多关于应用切片/精简的信息。
当为按需服务设置你的游戏时,这是在游戏初期规划中值得了解的,可以在官方文档developer.apple.com/library/prerelease/ios/documentation/FileManagement/Conceptual/On_Demand_Resources_Guide/中找到。
精灵图集和动画精灵
SwiftSweeper 实际上并不使用动画精灵;正如我们将要看到的,它只是使用 Unicode 表情符号字符来动画化屏幕。然而,我们无法在不提及精灵、动画化和使用纹理图集/精灵图来优化它们的情况下讨论 SpriteKit 和 2D 游戏开发,对吧?精灵图集是一组打包成单个图像的图像集合,也称为精灵图或纹理图集。在开发 2D 游戏时,强烈建议使用纹理图集而不是各种图像集,因为对于渲染器来说,纹理图集将等于更少的绘制调用,从而确保你的游戏以所需的 60 fps 运行。《Collectables.atlas》文件夹位于Assets.xcassets中,可以存储你游戏中所有的可收集物品,并且使用SKTextureAtlas类,可以有效地将这些可收集物品绘制到屏幕上。当我们存储玩家静止、行走和跳跃动画的图像时,我们使用纹理图集来存储它们。
创建纹理图集非常简单,如下所示:
-
简单地点击你的
Assests.xcassets文件夹,然后在文件夹层次结构的空白部分右键点击。![精灵图集和动画精灵]()
-
点击新建精灵图集,就像这样,我们就有了可以存储我们游戏各种精灵的文件夹。
![精灵图集和动画精灵]()
-
确保根据你希望如何分类精灵组来命名文件夹。在代码中引用它们时你需要这个名称。
要在代码中创建对这张图集的引用并动画化精灵,我们使用SKTextureAtlas如下所示:
let PlayerAtlas = SKTextureAtlas(named:"Player.atlas")
let textures = map(1...4) { number in
PlayerAtlas.textureNamed("player_sprite_normal_\(number)")
}
let anim = SKAction.animateWithTextures(textures, timePerFrame: self.animationRefreshRate_)
let idleAnimation = SKAction.repeatActionForever(anim)
self.runAction(idleAnimation)
首先,这段代码使用初始化器SKTextureAtlas(named:"Player.atlas")创建了一个指向玩家精灵图集的SKTextureAtlas引用。然后,我们使用 Swift 的一个顺序块map(NSRange){…}创建了一个纹理数组。这是一个闭包块,它根据 map 调用中指定的范围遍历精灵图集中的纹理。number对象是一个简单的索引对象,我们可以用它来表示映射的索引。
这是因为我们的玩家有这些用于正常/静止动画的精灵名称:
"player_sprite_normal_1", "player_sprite_normal_2", "player_sprite_normal_3", "player_sprite_normal_4"
由于我们知道精灵动画是以索引命名结构命名的,因此在这里使用 Swift 的函数式编程工具,如 map(),可以简化代码。具有许多帧帧动画的 2D 精灵(如 Metal Slug)可以以这种方式迭代。
SKTextureAtlas 还有一个名为 preloadTextureAtlases 的类函数,我们可以使用它来预加载一系列纹理图集:
SKTextureAtlas.preloadTextureAtlases([PIKIATLAS,BGATLAS,COLLECTABLESATLAS,HUDATLAS, OBSTACLESATLAS])
{
//perform other tasks while loading TextureAtlases
}
这很好,可以确保在进入舞台之前加载舞台的精灵。
创建我们的游戏逻辑
为了简化起见,扫雷不会有很多不同的资产或任何精灵纹理。它相反使用 Swift 的 Unicode 表情符号字符能力和 UIView 调用来以相当老式、非常类似扫雷的方式设计游戏的图形。
我们这样做不仅是为了给我们一个相对简单的起点,而且还为了展示 Swift 代码和 SpriteKit 类如何让我们在不需要初始精灵资产的情况下创建整个游戏逻辑和流程。这样,无论是作为团队还是个人开发,游戏可以在进行有时令人疲惫的制作精美视觉资产的过程之前制作出来。首先思考代码和结构可以确保你有一个可以稍后用精灵、音乐和氛围进行润色的可工作原型。
到目前为止,我们只是让 SwiftSweeper 作为 SpriteKit 游戏模板的一个外壳等待。是时候我们开始构建游戏模型了:
-
首先,让我们添加我们的图像资产。更多信息,请访问
mega.co.nz/#!XhEgCRgJ!4QqKMl1l1P4opWU7OH2wEN_noVQ86z5mxEyLuyUrcQo。这是一个指向
SwiftSweeper的Assets.xcassets文件夹的链接。我们可以单独添加这些文件,但最简单的方法是直接在计算机上替换你的项目文件夹中的Assets.xcassets文件夹。你可以在此过程中打开 Xcode,它会自动从原始模板文件更新。 -
接下来,让我们添加以下 URL 的声音文件:
mega.co.nz/#!T5dUnJZb!NUT837QQnKeQbTpI8Jd8ISJMx7TnXvucZSY7Frw5gcY -
通过以下步骤添加声音:
-
右键点击包含 Swift 文件的
SwiftSweeperExample文件夹,然后从菜单中选择 New | Group。![创建我们的游戏逻辑]()
-
将此文件夹命名为
Sounds并将其拖到同一SwiftSweeperExample文件夹中的文件底部。![创建我们的游戏逻辑]()
-
右键点击
Sounds文件夹,并选择Add Files To "SwiftSweeperExample"。![创建我们的游戏逻辑]()
-
从
SwiftSweeperSounds文件夹添加声音,现在它们应该已经在你的项目中了。
![创建我们的游戏逻辑]()
-
所有资产现在都应该在项目中,因此现在我们可以构建我们的游戏。让我们首先从实际的瓷砖开始。
现在创建一个新的 Swift 文件,将其命名为 Tile,并将以下代码粘贴到该文件中:
class Tile{
//Properties
//(1)
let row : Int
let column : Int
//(2)
var isTileDown = false
var isFlagged = false
var isAMine = false
//(3)
//Mines counter
var nearbyMines:Int = 0
//(4)
init(row:Int, col: Int){
self.row = row
self.column = col
}
}
在创建地砖时,我们遵循以下逐步逻辑:
-
在构建任何代码逻辑时,我们通常将这个对象的相关属性放在顶部。我们知道在扫雷游戏中的每一块地砖都将是一行和一列的一部分。在游戏过程中,这块地砖所在的行和列的编号不会改变,所以我们将它们设置为常量,使用关键字
let,并使用Int类型来设置,因为我们知道行或列不能有分数,至少在地砖对象方面是这样的。 -
地砖可以有几个不同的状态。它可能是已经被点击的,它上面可能放置了旗帜,或者它可能是一个地雷。由于这些是布尔属性,我们使用布尔变量
isTileDown、isFlagged和isAMine来设置它们。我们最初将它们设置为false。 -
在扫雷游戏中,地砖会计算它们周围有多少地雷,因此我们创建一个整数计数器
nearbyMines来保存这些信息。 -
当创建一个地砖对象的实例时,我们希望游戏在
GameBoard上设置其行和列编号位置,因此我们创建默认初始化器init,它有两个参数输入,分别是行和列。
对于 Tile 对象,我们需要的就这些,所以让我们继续使用 MineTileButton 类来设置这些 Tile 对象的按钮功能。
创建一个新的 Swift 文件,并将其命名为 MineTileButton,然后将以下代码粘贴到其中:
//(1)
import UIKit
class MineTileButton : UIButton {
//(2)
var tile:Tile
let tileSize:CGFloat
//(3)
init(tileButton:Tile, size:CGFloat) {
self.tile = tileButton
self.tileSize = size
let x = CGFloat(self.tile.column) * tileSize
let y = CGFloat(self.tile.row) * tileSize
let tileBoundingFrame = CGRectMake(x, y, tileSize, tileSize)
super.init(frame: tileBoundingFrame)
}
//(4)
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//(5)
//button text;
//replace button with an SKSprite for better GUI interface?
func getTileLabelText() -> String {
if !self.tile.isAMine {
if self.tile.nearbyMines == 0 {
return "0"
}else {
return "\(self.tile.nearbyMines)"
}
}
//(6)
return "💥"
}
}
下面是对代码的解释:
-
由于我们正在创建一个
UIButton对象,因此我们导入 UIKit 框架用于这个对象。 -
这些是这个按钮对象的属性。我们需要一个名为
tile的Tile对象来引用,一个名为tileSize的CGFloat大小来表示这个按钮将占用的矩形。 -
这个类的初始化器接受一个名为
tileButton的Tile对象和一个名为size的CGFloat。我们将类的自己的地砖分配给tileButton,将tileSize分配给size,然后使用CGRectMake()方法创建一个名为tileBoundingFrame的正方形。这是在我们根据tileSize为正方形设置CGFloat的x和y值之后完成的。UIButton的父类初始化器init(frame:)通过super.init(frame: tileBoundingFrame)使用tileBoundingFrame作为参数。 -
由于 Xcode 5 以来,
init函数主要是在处理 UI 对象时让编译器满意所需的。 -
getTileLabelText()函数根据tile对象的状态返回一个字符串。如果方块不是地雷,我们知道我们必须放置一些东西,因为没有方块;传统上,这只是一个空白空间或一个空的""字符串,但到目前为止,我们只是在那里放置0,留下逻辑以供自定义。说实话,我们可以简单地返回嵌套的 if-else 语句的返回值\(self.tile.nearbyMines),它将返回相同的结果。正如我们所见,它返回特定Tile对象的nearbyMines属性。 -
如果方块是地雷,则返回碰撞 Unicode 表情字符。当玩家点击一个未标记的方块时,会调用
getTileLabelText()函数。 -
Swift 使用 Unicode 字符符号的能力可以在您游戏的规划过程中起到很好的视觉辅助作用。在行
(6)中使用的碰撞 Unicode 表情是U+1F4A5 (128165)。如果您只看到一个方框而不是红色的爆炸状字符,可以在本章前面提到的完整项目下载中查看,或者通过以下链接查看。
注意
在 www.charbase.com/1f4a5-unicode-collision-symbol 查找有关此表情符号的更多信息。
GameBoard
现在我们已经有了代表每个名为 MineTileButton 的方块对象的 tile 对象和按钮逻辑,我们需要创建一个代表这些对象集合的对象,即 GameBoard。
完整的 GameBoard.swift 代码在这里全部展示出来会有些过大,所以我们将总结其主要功能和部分。
我们可以在本章前面提到的完整项目链接中查看整个代码,或者直接在下面复制到您当前的游戏项目文件中:
对于我们的 GameBoard,我们希望创建一个 10x10 大小的瓦片板,并且具有三个难度级别:简单、中等和困难。为了创建难度,我们只需使用一个名为 difficulty 的枚举来存储游戏的难度级别。
GameBoard 的最重要的属性包括 boardSize_(在本例中设置为 10),一个将代表放置的地雷数量的变量 mineRandomizer,板上的活动地雷数量 mineCount,以及将填充板上的 tiles 对象。
注意 tiles 属性所使用的语法:
var tiles:[[Tile]] = []
以这种方式,我们可以在 Swift 中创建一个有序的二维数组(或矩阵)。GameBoard 对象基本上将存储一个 Tile 类型对象的数组数组。
注意
*Swift 有更多方式来表示矩阵,例如,我们可以使用 Structs 来定义我们自己的独特矩阵。截至本出版物的时间,Swift 没有自己真正的固定长度数组功能,正如我们在各种 C 语言中看到的那样。然而,使用嵌套的大括号 [[]] 对于我们试图完成的事情来说是完全可以的。
GameBoard的初始化器init(selectedDifficulty:difficulty){}接受玩家选择的难度作为其单一参数,然后根据boardSize属性构建板,然后使用以下嵌套 for-in 循环用Tile对象填充整个板:
for row in 0 ..< boardSize_ {
var tilesRow:[Tile] = []
for col in 0 ..< boardSize_ {
let tile = Tile(row: row, col: col)
tilesRow.append(tile)
}
tiles.append(tilesRow)
}
由于tiles对象是一个二维数组,我们首先需要执行这个嵌套循环,首先为每一行创建一个Tile对象的 1D 数组(命名为tilesRow),然后使用.append函数在该行的每一列中添加一个瓷砖。然后,将tilesRow数组附加到主瓷砖二维数组中。
小贴士
如果你希望创建一个矩形或其他形状的GameBoard实例,你必须考虑到不同的列和行数量。这将使嵌套循环更加复杂,需要单独的columnSize和rowSize属性。许多益智游戏可能会让玩家看起来很复杂,但它们的内部结构可能仍然很简单,要么是正方形,要么是矩形,通过填充那个瓷砖为不可玩区域或背景/透明瓷砖来实现。
这是一种开发者可以节省时间的同时允许复杂功能和设计的做法。这就是我们为什么用代表瓷砖、瓷砖按钮功能和游戏板布局的单独类来构建这个游戏。
通过继承,我们可以继续自定义每个瓷砖的功能,从而基于简单的基础实现众多功能。
这也是为什么电子游戏一直是利用面向对象设计的典范。
如果一开始难以完全理解这一点,不要担心,因为嵌套循环往往很费脑筋。只需观察内部 for 循环不会退出,直到根据boardSize_ 属性填充完列。由于行和列都是 10,这种循环变得更容易。
然后初始化器调用resetBoard()函数,将mineCount重置为0,并执行两个额外的嵌套 for 循环:
for row in 0 ..< boardSize_ {
for column in 0 ..< boardSize_ {
self.createRandomMineTiles(tiles[row][column])
tiles[row][column].isTileDown = false
}
}
这个遍历板的 for 循环使用createRandomMineTiles()函数随机设置哪些瓷砖是地雷,以及使用tiles[row][column].isTileDown = false调用将瓷砖重置为未触摸状态。createRandomMineTiles()函数基于当前难度级别工作,特别是mineRandomizer属性,该属性在implementDifficulty()函数中确定。mineRandomizer值越高,迭代瓷砖成为地雷的机会就越小。
resetBoard()中的下一个嵌套 for 循环如下:
for row in 0 ..< boardSize_ {
for column in 0 ..< boardSize_ {
self.calculateNearbyMines(tiles[row][column])
}
}
这会遍历板上的每个瓷砖,并设置玩家在点击时将看到的数字。这个数字当然是围绕非地雷瓷砖的周围地雷的数量,即Tile类的nearbyMines属性。
这个相当复杂的计算链从calculateNearbyMines()函数开始,然后通过数组/瓷砖索引计算函数getNearbyTiles()和getAdjacentTileLocation()进行计算。我们在这些函数中提供了各种详细的注释,以便更好地理解它们的工作原理。建议你阅读如何实现的详细细节,但为了避免使已经复杂的游戏逻辑解释更加混乱,请在getNearbyTiles()中的以下行做笔记:
let nearbyTileOffsets =
[(-1,-1), //bottom left corner from selected tile
(0,-1), //directly below
(1,-1), //bottom right corner
(-1,0), //directly left
(1,0), //directly right
(-1,1), //top left corner
(0,1), //directly above
(1,1)] //top right corner
如果要理解这三个复杂函数中的任何一行,那就是这一行。nearbyTileOffset对象是一个显式编写的元组数组,包含围绕单个 2D 瓷砖可能存在的每个偏移量。实际上,最好将这个数组中的每个成员视为一个(x,y) 2D 向量。
因此,正如前述代码中注释的那样,偏移量(-1,-1)位于瓷砖的左下角,因为x = -1(左 1)和y = -1(下 1)。同样,(1,0)位于右侧,(1,1)是右上角。
我们还必须考虑到一些瓷砖位于边缘和/或列上,因此一些瓷砖偏移量不会返回另一个瓷砖的引用;相反,它们将返回 nil。
for (rowOffset,columnOffset) in nearbyTileOffsets {
//optional since tiles in the corners/edges could have less than 8 surrounding tiles and thus could have a nil value
let ajacentTile:Tile? = getAjacentTileLocation(selectedTile.row+rowOffset, col: selectedTile.column+columnOffset)
//if validAjacentTile isn't nil, add the Tile object to the nearby Tile array
if let validAjacentTile = ajacentTile {
nearbyTiles.append(validAjacentTile)
}
}
在getNearbyTiles()中的这个 for 循环不仅检查每个瓷砖的偏移量,而且还通过调用getAjacentTileLocation()来考虑边缘或角落瓷砖。
再次强调,这三个函数相当复杂,即使是在对它们的函数功能进行逐行/半通用解释的情况下也是如此。所以,如果你一开始不理解流程/顺序,请不要担心。
最后,对于resetBoard(),如果我们不知道玩家是否得到了每个非地雷瓷砖,我们就无法赢得游戏,所以我们通过以下行获取该信息:
numOfTappedTilesToWin_ = totalTiles_ - mineCount
当玩家的完成移动次数(在GameScene类中计数)等于numOfTappedTilesToWin时,玩家获胜!
所有这些操作都是在玩家进行第一次移动之前完成的!这样做是为了确保值已经预先确定。是的,我们可以在玩家触摸时进行一些计算,但处理样板游戏逻辑通常足够快,可以在加载时准备游戏,这样我们就可以在游戏循环期间使用游戏玩法来关注效果、序列和其他视觉提示。
这个功能由GameScene.swift文件控制,我们将在下一部分对其进行总结。
将所有这些在GameScene.swift中整合在一起
现在我们已经设置了 SwiftSweeper 逻辑的核心,现在是时候将其展示在游戏模板提供的SKScene中,即GameScene。这个场景使用了我们在本章开头提到的游戏/渲染循环函数。
SwiftSweeper 的 GameScene.swift 版本相当大,大约有 800 行代码,所以像 GameBoard 一样,我们不会逐行分析它,而是会总结场景的一些重要方面。正如之前所述,每次 Xcode 和 iOS 的更新都会带来更多设置这些场景的视觉方式,因此了解这个示例中的每一行代码不是必要的,但如果你真的想深入了解如何使用代码来展示 SpriteKit 游戏场景,仍然建议这样做。
完整的代码可以在本章前面提到的完整项目链接中找到,或者(如果你一直在本章从头开始构建)在下面提到的链接中:
我们使用了各种 //MARK: 注释来分隔代码的各个部分,这样你可以更容易地导航。将代码复制到你的项目中后,你可以构建并运行应用程序。只要一切都被正确地放置到项目中,你应该在你的手机或手机模拟器上运行一个可工作的 SwiftSweeper 版本。玩一会儿,以了解游戏场景中正在进行的操作,以展示游戏。有时候,看到游戏的实际运行情况能让我们更好地看到其背后的代码。如果出现任何错误,说明出了问题,如果所有其他方法都失败了,你可以从 github.com/princetrunks/SwiftSweeper 下载完整的项目。
游戏场景(GameScene)的第一个视觉入口点 didMoveToView() 实际上相当小,如下所示:
override func didMoveToView(view: SKView){
self.backgroundColor = UIColor.whiteColor()
stageView_ = view
loadInstructions()
}
我们只是将背景颜色设置为白色并加载说明。再次强调,我们并没有说这是一个看起来很美的游戏。
loadInstructions() 函数手动将说明精灵放置在屏幕上,并将 currentGameState_ 枚举设置为 .Instructions。游戏状态或状态机是常见的游戏开发方法,它指导角色、玩家和游戏本身处于什么状态。这可以用来确保某些游戏玩法不会发生在它们不应该发生的地方。iOS 9 / Xcode 7 引入了框架;我们将在后面的章节中深入探讨,例如 GamePlayKit,它除了其他游戏逻辑函数外,还与可以模块化和独立于特定场景的状态机一起工作。SKComponents 类的组件以及 iOS 9 中引入的 SKAction 的更现代用法以相同的方式工作,独立于 OOP 继承。想想更动态/可用的协议版本。
游戏场景的下一个整体步骤是 chooseDifficultyMenu(),它伴随着 removeInstructions() 函数,在玩家点击屏幕后调用。这个点击在之前提到的几个示例中的函数 touchesBegan() 中进行检查,使用游戏状态作为逻辑检查:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
for touch in touches {
//flag button Pressed
if CGRectContainsPoint(flagButton_.frame, touch.locationInNode(self)) {
flagButtonPressed()
}
//instructions removed when tapped
if CGRectContainsPoint(instructionsSprite_.frame, touch.locationInNode(self)) && currentGameState_ == .Instructions {
removeInstructions()
}
}
}
注意touchesBegan函数实际上相当简单。它只检查我们是否点击了旗帜按钮或是否点击了说明。那么方块呢?记住,我们把这些方块都变成了UIButton的成员,使用MineTileButton类。这是控制这个功能的函数:
func tileButtonTapped(sender: MineTileButton) {
//exit function if not playing the game
if (currentGameState_ != .MineTap && currentGameState_ != .FlagPlanting){
return
}
//reveals the underlying tile, only if the game is in the main state, aka MineTap
if (!sender.tile.isTileDown && currentGameState_ == .MineTap) {
sender.tile.isTileDown = true
sender.setTitle("\(sender.getTileLabelText())", forState: .Normal)
//sender.backgroundColor = UIColor.lightGrayColor()
//mine HIT!
if sender.tile.isAMine {
//sender.backgroundColor = UIColor.orangeColor()
self.mineHit()
}
//counts the moves; also used in calculating a win
moves_++
}
else if (!sender.tile.isTileDown && currentGameState_ == .FlagPlanting){
self.flagPlant(sender)
}
}
UIButton类的成员会向场景发送被点击的引用。在这个游戏中,这是一个MineTileButton类型的对象。使用游戏状态来检查它是否对场景合理,我们要么使用mineHit()函数结束回合,要么增加执行的移动次数(用于通过将其与回合开始时计算的numOfTappedTilesToWin_进行比较来计算胜利)。如果游戏状态是.FlagPlanting,那么我们则处理在方块上放置旗帜的逻辑。带有旗帜的方块不会对.MineTap游戏状态点击做出反应,因此,如果你在错误的方块上放置了旗帜,你将无法在揭示所有非地雷方块之前获得胜利。
在剩余的代码中,我们将找到一个计时器,根据结果向玩家发出警报,甚至可以使用NSUserDefaults类的类函数来保存每个难度级别的计时时间。
再次强调,这并不完全是在视觉上非常优雅,但在代码上很复杂,最重要的是一个完全功能性的游戏。我们建议您查看GameScene.swift中的更多代码,但可能对设计造成的一个主要问题是,这仅在 iPhone 上工作。
使用如 autolayout 这样的视觉工具,如前一章中简要提到的,将允许为 iOS 设备系列更容易地进行设计更改。由于 SwiftSweeper 的 GameScene 中的许多视觉资产都是手动放置在视图中的(尤其是说明),我们不得不在代码中考虑每种设备类型。这是可能的,但随着设备系列的扩展,用于屏幕视觉的手动代码在未来 iOS 更新和设备公告中可能会很容易被破坏。这就是为什么在下一章关于 SceneKit 以及之后的章节中,我们将主要偏离以代码为中心的结构,并拥抱动手工具和更新的框架,如 Xcode 7 及以后的 GamePlaykit。
DemoBots
就在本书最初出版时,WWDC15刚刚结束,为我们提供了一个名为DemoBots的针对 iOS 9 和 Xcode 7 的精彩新的 SpriteKit 演示项目。

DemoBots 是 Apple 提供的 SpriteKit 项目,它使用组件、状态机、按需服务、GameplayKit、ReplayKit 等!
DemoBots 的完整项目文档可以在developer.apple.com/library/prerelease/ios/samplecode/DemoBots/Introduction/Intro.html找到。
要从WWDC15中看到它的实际效果,请查看Deeper into GameplayKit with DemoBots主题演讲的视频和 PDF 文件:
developer.apple.com/videos/wwdc/2015/?id=609
SpriteKit 的演讲可以在以下链接找到:
developer.apple.com/videos/wwdc/2015/?id=604
DemoBots 的游戏玩法甚至有易于编辑的敌人 AI/导航方案,它使用了 iOS 9 中引入的SKCameraNode,它跟随玩家,不会像过去版本的 SpriteKit 那样在视图中移动场景。正如我们在本章开头提到的,模仿我们在多平台游戏引擎中看到的工具。
摘要
在本章中,我们讨论了多个主题。我们首先讨论了为什么在多年的开发者只能使用第三方游戏框架,如 Cocos2D 和 Sparrow 之后,SpriteKit 成为了 iOS 的受欢迎的补充。我们讨论了 SpriteKit 如何在游戏开发生态系统中适应,因为像 Unity 和 Unreal Engine 这样的强大、多平台游戏引擎继续变得更加突出。接下来,我们探讨了SKScene使用的 SpriteKit 游戏循环和渲染周期。然后,我们开始构建我们的演示拼图游戏SwiftSweeper,并更深入地了解了 SpriteKit 最突出的对象类的基本结构。除了纹理图集以及如何使用这些资产工具来动画精灵之外,还回顾了 iOS 9 的assets文件夹。然后,我们进入了模拟类似扫雷这样的拼图游戏的相对复杂的逻辑和代码。
接下来,我们将转向 iOS 的 3D 游戏开发框架 SceneKit,我们将更多地关注自 iOS 8/iOS 9 以来苹果为我们带来的视觉工具。由于我们已经了解了 SceneKit 和 SpriteKit 共享的基本场景/代码结构,我们现在将采用较少以代码为中心的方法。SpriteKit 场景可以覆盖 SceneKit 场景,因此我们将很快看到我们用苹果自己的 DemoBots 演示暗示的一些内容。
第四章:SceneKit 和 3D 游戏设计
对于本章,我们将介绍用于 3D 游戏开发的 iOS 框架,称为 SceneKit。SceneKit 首次在 iOS 7 中可用,但最初仅用于 MacOS 开发。以前,开发者必须使用 OpenGL 或第三方框架和引擎,如 Cocos3D、Unreal Engine、Havok 和 Unity 来编写 3D 游戏。随着 iOS 设备家族图形能力的提升,对沉浸式、互动式的一方 3D 游戏设计引擎的需求也增加了。SceneKit 很快就可供 iOS 使用,为开发者提供了一个 Xcode 内置的解决方案来制作 3D 游戏。
在上一章中,我们以更基于代码的方法论来处理 iOS 游戏开发。我们仍然会编写一些代码,但自从 Xcode 5 和 Xcode 6 的推出以来,Apple 提供了一些出色的示例,展示了 IDE 如何与多平台游戏引擎一样具有视觉动态性。使用 Xcode 和 SpriteKit/SceneKit 框架而不是这些引擎的好处是,您有一个针对特定平台的专业设计环境。在我们的案例中,这个平台是 iOS 和 Apple 设备家族。随着 iOS 的频繁更新并继续提供新功能,Xcode 和这些框架将与其一起更新。多平台引擎的更新通常发生在较晚的时间,有时还需要安装插件以确保您的应用程序在未来的更新中能够顺利运行。
除了非常动态且工具丰富的 DemoBots SpriteKit 示例之外,2015 年 6 月的全球开发者大会(World Wide Developer's Conference)还介绍了一个名为 Fox 的出色 SceneKit 示例。Fox 示例还利用了 iOS 9 中引入的功能,这些功能可用于 SpriteKit 或 SceneKit,例如可重用动作、组件和状态机。
在本章中,我们将介绍 SceneKit 的基础知识,并使用代码和 Xcode 提供的视觉设计工具创建一个简单的 SceneKit 场景(称为 SCNScene)。然后,我们将向我们的 SceneKit 对象和场景添加物理、灯光和粒子。最后,我们将探讨 WWDC15 Fox 示例以及它使用的某些功能/API,这些功能在 iOS 8 和 iOS 9 中可用。
注意
在上一章中,我们在讨论 SpriteKit 时省略了许多这些资产创建功能。使用 SpriteKit 场景文件(.sks),我们也可以以与 SceneKit 场景文件(.scn)相同的方式创建游戏资产,例如灯光、物理场、边界框/物理约束、法线贴图、纹理、整个关卡以及角色。我们有时会展示 SpriteKit 方法来展示类似的功能。
由于 SpriteKit 和 SceneKit 场景资产的工作方式相似,并且可以在同一场景中一起使用(归功于它们的继承节点/树功能),我们认为最好将视觉和资产工具讨论留到本章。前一章关于游戏/渲染循环以及场景代码功能的大部分内容将在 SceneKit 中像之前在 SpriteKit 中那样工作。
因此,换句话说,我们已经准备好直接进入 SceneKit。
SceneKit 基础和节点操作
与 SpriteKit 一样,SceneKit 基于节点的概念。SpriteKit 对象是SKNode类的子类,而 SceneKit 对象是SCNNode类的子类。

上述图像是 Apple SceneKit 介绍中的 SceneGraph 层次结构。正如我们所见,SceneKit 从SCNScene类分支出各种节点。这些包括用于灯光、几何和摄像机的通用SCNNode。
节点是一种可以添加其他节点并具有结构中其他节点信息的数据结构。如前图所示,它通过childNode[]数组和父属性展示。空间信息,如位置、缩放和方向,可以从这些属性中获取。这就是节点在面向对象设计(OOD)中相对于其他父子结构独特的特点。
在 SpriteKit 中,我们通常通过addChild()函数将节点添加到场景或场景中的另一个节点。在 SceneKit 中,相同的功能通过addChildNode()实现。例如,SceneKit 场景中的主要根节点是放置在SCNView节点中的SCNScene节点,即框架的UIView类的独特版本。要将基本球体对象添加到场景中,我们会执行以下操作:
let sphereGeometry = SCNSphere(radius: 1.0)
let sphereNode = SCNNode(geometry: sphereGeometry)
self.rootNode.addChildNode(sphereNode)
正如 SpriteKit 所述,通过 SpriteKit 中的节点,我们可以将游戏场景中的各种成员组合成它们自己的父节点,并在一个调用中对这些节点执行动作,同时通过循环或其他迭代调用进行迭代。
SpriteKit / SceneKit 交互
SceneKit 的一个显著特点是我们可以将 SpriteKit 场景叠加到 3D 游戏中。
self.overlaySKScene = skScene
使用SCNView的overlaySKScene属性,我们可以将已经建立的SKScene节点(可以是角色、动画序列、HUD 等)添加到我们的 3D 场景中。
想要有一个可爱的精灵动画叠加到 3D 角色的舞台胜利场景上,或者想要制作一个 2.5D 游戏,使用 2D 精灵和物理效果叠加在 3D 背景上?那么这就是你可以这样做的方法。

将 SpriteKit 与 SceneKit 混合使用最常见的功能是,SpriteKit 是 SceneKit 场景的用户界面(HUD)。在早期的 Fox 演示中看到的生命、可收集物品和角色图标显示了一个 SpriteKit 节点叠加在 SceneKit 场景上。
一般而言,节点可以帮助为你的游戏和游戏场景添加功能结构。然而,在游戏设计中过度依赖节点和继承并不是没有缺陷的。
基于继承的结构化和游戏设计的问题
在继续前进之前,我们应该提到一个可能困扰过度依赖节点概念甚至面向对象设计(OOD)中基于继承的结构化的一般概念的陷阱。当可能的时候,最好不要过度依赖继承来处理你的游戏逻辑,而更多地使用所谓的基于组合的结构化。我们将在下一章中深入探讨这个问题,当时我们将首先介绍在 iOS 9 中首次引入的辅助游戏开发框架 GamePlayKit,但在这里我们先了解一下,与继承和节点一起工作可能并不是我们游戏中总是最佳的选择。

初看之下,人们可能会认为基于继承的结构化非常适合游戏开发。我们中许多熟悉面向对象设计(OOD)的人都知道,我们可以有通用的父类或游戏对象的节点,例如一个包含所有功能的GameObject类,然后使用继承和多态来处理从这个基类派生出的独特子类。对于小型、简单的游戏来说,这确实是正确的,但游戏往往有可以共享一些相同功能但放在父-子结构中又没有意义的对象。
在塔式策略游戏中,我们可以采用这种典型的结构化方式:

在一个典型的塔式游戏中,我们会有基础、塔和敌人对象,它们都可以从我们定义的通用GameObject类继承。塔可以对敌人开火,敌人也可以对塔和其他基于玩家的对象开火。良好的编程和设计的一部分是拥有可重用的代码和方法。通常,我们会通过继承来实现这一点。前面的图表显示了可以解决这个问题的双向继承。我们接下来会想要一个继承移动和射击功能的ShootingEnemy类。但我们不能这样做,因为这会涉及到从两个完全不同且相当无关的对象类继承。在面向对象设计中,只有一个子-父关系。右边的下一个解决方案是将这种功能添加到通用的GameObject类中。出现的问题是我们曾经简单的GameObject父类变得相当复杂,我们不可避免地想要在我们的游戏对象中添加额外的功能和功能。在过去,这会涉及到重构大量代码以适应本质上只是简单设计附加的功能。协议曾经是某种解决方案,因为它们会强迫我们以某种方式创建一个类,但即使是它们也可能变得令人困惑,并且不涉及这些功能的实现。
解决方案是与实体和组件一起工作。

上述图表给出了基于组合的结构示例。使用这种方法,我们可以拥有具有相似功能的部分,这些部分被多个通常无关的游戏对象使用。这样,本例中的通用GameObject类就不必具备其子类可能的所有功能,我们还可以将Enemy类作为Enemy的成员。共享功能可以一次性编写,然后在整个游戏中以及在其他我们希望制作的游戏中使用。iOS 9 的 SpriteKit 演示、DemoBots 以及前面提到的 SceneKit 演示 Fox 都使用基于组合的结构进行动作和动画。
当在 SpriteKit 和 SceneKit 中考虑节点时,重要的是它们在 MVC 模型的 View 上下文中使用,或者在两个框架中,它们的场景上下文中使用。
关于 SceneKit 中的场景,让我们继续制作一个非常基本的场景。
我们的第一个 SceneKit 场景 – Xcode 模板
3D 艺术和动画是一个非常深入的话题。我们可以无休止地讨论材料、着色器、光照、雕刻、PVR 纹理以及所有使游戏、电影、建筑或任何其他基于 3D 对象的应用程序中的 3D 对象变得出色的主题。
这些主题的一些细节超出了本书的范围,所以现在让我们保持简单。

让我们从 Xcode 提供的默认 SceneKit 场景和对象开始,如图所示:
注意
在撰写本书时,我们使用了 Xcode 7 – Beta 的 SceneKit 模板。根据你使用的版本,可能会有一些差异。
-
首先,打开 Xcode,创建一个新的项目,并选择游戏模板。
![我们的第一个 SceneKit 场景 – Xcode 模板]()
-
接下来,为你的项目命名,确保游戏技术字段显示为SceneKit,然后点击下一步。
![我们的第一个 SceneKit 场景 – Xcode 模板]()
-
项目文件和结构大致与 SpriteKit 相同,但有一些不同,特别是
art.scnassets文件夹。唯一的区别是现在除了Assests.xcassets外,还有一个art.scnassets文件夹。这是我们的 3D 对象所在之处。点击该文件夹以查看 Apple 提供的ship.dae资产。![我们的第一个 SceneKit 场景 – Xcode 模板]()
使用 SceneKit 编辑器,我们可以查看和编辑以下 3D 文件类型:
-
DAE
-
OBJ
-
Alembic
-
STL
-
PLY
给我们的示例是一个 DAE 类型的宇宙飞船,ship.dae文件作为飞船的纹理文件(texture.png)。在我们查看代码和场景如何工作之前,请在自己的设备或 Xcode 设备模拟器上构建并运行程序。
从示例场景中,我们看到我们的宇宙飞船在黑色背景前旋转,并且我们可以通过滑动飞船来改变其方向。点击飞船会使它瞬间发出红色光芒。
现在我们来看看代码中发生了什么,然后我们将探讨编辑器为我们提供的工具,以便在不编写任何代码的情况下编辑我们的对象和场景。
SceneKit 项目流程和结构
与 SpriteKit 类似,SceneKit 场景使用与上一章中看到的相同的游戏渲染循环,以及我们在 第二章 中提到的相同类型的入口点结构,即 使用 iOS 9 Storyboards 和 Segues 设计游戏结构和规划。我们有一个 AppDelegate.swift 文件,它是我们的入口点,可以根据上层设备事件控制特殊的应用功能,例如应用关闭、进入后台和从后台返回。我们还有之前在 SpriteKit 中看到的启动屏幕和 Main.storyboard 文件。

与 Main.storyboard 文件不同的是,它有一个 SceneKit 场景图标,如前一张截图所示,以立方体形式显示。
AppDelegate 转移到的 ViewController 类是 GameViewController.swift 类。这是我们所有演示代码所在的地方:
override func viewDidLoad() {
super.viewDidLoad()
// create a new scene
let scene = SCNScene(named: "art.scnassets/ship.dae")!
...
我们看到我们开始于重写的 viewDidLoad() 函数。SceneKit 允许我们创建一个完整的场景,甚至包括我们的 3D 对象/资产的一个实例,正如我们从解包的 let scene = SCNScene(named: "art.scnassets/ship.dae")! 调用中看到的。这仅仅创建了场景对象。要获取屏幕上显示的对象,我们仍然需要将其附加到 SCNView 节点,正如我们稍后将在函数中看到的。
让我们来看看这里的一些代码:
//(1) create and add a camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
// place the camera
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
//(2) create and add a light to the scene
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = SCNLightTypeOmni
lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
scene.rootNode.addChildNode(lightNode)
// create and add an ambient light to the scene
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = SCNLightTypeAmbient
ambientLightNode.light!.color = UIColor.darkGrayColor()
scene.rootNode.addChildNode(ambientLightNode)
//(3) retrieve the ship node
let ship = scene.rootNode.childNodeWithName("ship", recursively: true)!
//(4) animate the 3d object
ship.runAction(SCNAction.repeatActionForever(SCNAction.rotateByX(0, y: 2, z: 0, duration: 1)))
//(5) retrieve the SCNView
let scnView = self.view as! SCNView
//set the scene to the view
scnView.scene = scene
//(6)allows the user to manipulate the camera
scnView.allowsCameraControl = true
// show statistics such as fps and timing information
scnView.showsStatistics = true
// configure the view
scnView.backgroundColor = UIColor.blackColor()
//(7) add a tap gesture recognizer
let tapGesture = UITapGestureRecognizer(target: self, action: "handleTap:")
scnView.addGestureRecognizer(tapGesture)
之前提到的 viewDidLoad() 函数在模板中提供给我们。实际上,它非常简单易懂,除了 handleTap() 函数外,几乎完成了创建这个场景所需的所有工作。任何在 OpenGL 中为 iOS 或其他平台创建过 3D 图形的开发者都会欣赏 SceneKit 给我们提供的场景和对象的高级简单控制。以下是提供的代码的更多细节:
在行 (1) 上,创建了一个名为 cameraNode 的 SCNNode,并将 SCNNode 的 camera 属性赋值为 SCNCameraNode 类型。然后,使用相机 position 属性上的 SCNVector3() 函数将相机放置在三维空间中。在这种情况下,相机放置在 (x: 0, y: 0, z: 15)。换句话说,x 和 y 坐标被设置为原点,而相机在 z 轴上稍微向后移动。

您可以在 developer.apple.com/library/ios/documentation/SceneKit/Reference/SceneKit_Framework/ 找到 SceneKit 坐标图。
SceneKit 中的坐标系被称为右手坐标系。理解 3D 坐标系的一个技巧是,如果我们用右手,做出类似枪的手势,拇指向上,食指直伸在我们面前,中指与食指成直角,我们就会得到x、y和z坐标。你的中指会在x轴(左右),拇指会在y轴(上下),食指会在z轴(前后)。
在(2)代码块中,我们正在向屏幕添加灯光。SceneKit 以及 SpriteKit 都允许我们创建多种不同的光照效果,从环境遮挡、使用法线贴图等。在这里,创建了一个名为lightNode的SCNNode实例,并将SCNNode属性light赋值为SCNLight类类型。第一个创建并添加到场景中的灯光是所谓的SCNLightTypeOmni类型灯光,正如从隐式解包的调用lightNode.light!.type = SCNLightTypeOmni中看到的那样。这种类型的灯光通常更多地用于调试,因为接下来添加的灯光ambientLightNode将是用于创建游戏氛围的类型之一。正如我们通过ambientLightNode.light!.color = UIColor.darkGrayColor()这一行所看到的那样,我们可以在代码中为该灯光指定颜色。
关于SCNLights的更多信息可以在developer.apple.com/library/prerelease/ios/documentation/SceneKit/Reference/SCNLight_Class/index.html找到。
我们很快就会看到如何将灯光和其他方面可视化地添加到viewDidLoad()函数中,但通常理解幕后的一些样板代码是有益的。
在(3)行中,let ship = scene.rootNode.childNodeWithName("ship", recursively: true)!是我们将ship对象添加到场景根节点的方法。这与场景中的其他对象并没有太大的不同。它从art.scnassets文件夹中ship.dae对象的名称中获取字符串ship。childNodeWithName函数中的recursively: true参数告诉场景它应该将对象的全部子节点添加到场景中。根据原始 3D 模型程序中 3D 对象的建模和绑定方式,对象可能有一个复杂的子节点数组。将recursively设置为true将迭代子节点以及它们的子节点。
以下长行((4)行的一部分)是告诉船只根据其当前方向以x、y和/或z角度持续旋转的紧凑方式:
ship.runAction(SCNAction.repeatActionForever(SCNAction.rotateByX(0, y: 2, z: 0, duration: 1)))
这可以分解为其各个部分,因为它是一个嵌套在SCNAction中的SCNAction,即rotateByX函数被包裹在SCNAction的repeatActionForever函数中。SceneKit(SCNAction)和 SpriteKit(SKAction)中的动作不仅可以通过代码添加到对象中,还可以在 Xcode 的视觉编辑器中添加,正如我们稍后将在 Fox 演示的回顾中看到的那样。
在这里了解更多关于SCNAction和SKAction类的信息:
关于SCNAction,请参阅developer.apple.com/library/ios/documentation/SceneKit/Reference/SCNAction_Class/。
关于SKAction,请参阅developer.apple.com/library/ios/documentation/SpriteKit/Reference/SKAction_Ref/。
在行(5)中,我们创建了SCNView对象,并将其分配给GameViewController的视图,使用行let scnView = self.view as! SCNView。然后,我们使用名为scene的对象在行(1)中创建的场景及其节点被分配给scnView的scene属性,通过scnView.scene = scene。关于哪个场景分配给了哪个节点,有一些轻微的不确定性,但这基本上与rootNode的设置本身有关。
接下来的几行(即(6))展示了我们可以从SCNView类中使用的某些属性;首先是使用allowsCameraControl属性控制摄像头的功能。将此设置为false将阻止玩家移动摄像头。这对于游戏中的剪辑场景或锁定舞台的一部分,使其成为必要的情况可能非常棒。scnView.showsStatistics = true这一行告诉场景显示任何对调试有益的渲染数据。例如,我们可以看到游戏运行的每秒帧数(fps)。
这与 SpriteKit 场景的代码部分skView.showsFPS和skView.showsNodeCount等价,其中skView是SKView对象的名字。
下一行,scnView.backgroundColor = UIColor.blackColor(),允许我们将背景颜色设置为黑色,就像我们使用UIColor类将ambientLightNode.light!.color = UIColor.darkGrayColor()设置为深灰色颜色一样。
SceneKit 调试选项
小贴士
截至 iOS 9,通过使用SCNDebugOptions结构和SCNView的debugOptions属性,更多的调试选项变得可用。
如果我们写下以下内容,我们就能看到我们船的边界框:
scnView.debugOptions = .ShowBoundingBoxes
其他选项还包括ShowLightInfluences、ShowPhysicsShapes和ShowWireframe。

WWDC15 的 Fox 演示,启用了.ShowBoundingBoxes 和 ShowPhysicsShapes 选项
最后,在行(7)中,let tapGesture = UITapGestureRecognizer(target: self, action: "handleTap:")创建了一个名为tapGesture的UITapGestureRecognizer对象,当任何点击发生时,它将调用函数handleTap(gestureRecognize: UIGestureRecognizer),而scnView.addGestureRecognizer(tapGesture)将识别器添加到场景中。
在 SceneKit 中处理用户输入
UITapGestureRecognizer对象在选择性组织我们从玩家那里接收到的输入方面非常出色。这适用于 SceneKit 和 SpriteKit 场景。我们可以为点击、每个方向的滑动、平移、捏合和长按设置识别器;长按非常适合当你可能需要处理角色充电攻击时。
这里是UITapGestureRecognizer类的文档,供您参考:
让我们看看那个handleTaps函数,因为它包含了一个 SceneKit 类的对象SCNTransaction:
func handleTap(gestureRecognize: UIGestureRecognizer) {
//(1) retrieve the SCNView
let scnView = self.view as! SCNView
// check what nodes are tapped
let p = gestureRecognize.locationInView(scnView)
/(2)
let hitResults = scnView.hitTest(p, options: nil)
// check that we clicked on at least one object
if hitResults.count > 0 {
// retrieved the first clicked object
let result: AnyObject! = hitResults[0]
// get its material
let material = result.node!.geometry!.firstMaterial!
//(3)// highlight it
SCNTransaction.begin()
SCNTransaction.setAnimationDuration(0.5)
// on completion - unhighlight
SCNTransaction.setCompletionBlock {
SCNTransaction.begin()
SCNTransaction.setAnimationDuration(0.5)
material.emission.contents = UIColor.blackColor()
SCNTransaction.commit()
}
material.emission.contents = UIColor.redColor()
SCNTransaction.commit()
}
}
在行(1)中,我们只是创建了一个对当前SCNView对象scnView的引用。接下来,使用gestureRecognize.locationInView(scnView)创建了常量p。这所做的是捕获我们希望跟踪的视图中的手势位置。在这种情况下,是整个视图scnView。如果我们有子视图,比如一个游戏菜单屏幕,那么如果我们愿意,我们可以以这种方式仅针对那里的手势进行目标识别。
注意
如果正在构建一个玩家需要即兴点击多次以控制角色移动或躲避的游戏,我们发现 SpriteKit 中提到的touchesBegan()功能比UITapGestureRecognizer要快一些。但随着每个新的、更快的 iOS 设备的推出,这最终可能成为一个无足轻重的问题,但如果你的游戏控制高度依赖于玩家的速度,你可能会注意到通过UITapGestureRecognizer方法对手势的响应会有一些延迟。这可能会影响你的游戏目标,所以尝试使用touchesBegan()函数看看哪个最适合你的游戏。使用touchesBegan()进行滑动和其他非点击手势可能会相当棘手,因此在开发方面也存在权衡。
接下来是 (2),我们使用 SCNView 的 hitTest() 函数来计算在视图中捕获了多少个这样的手势,在我们的例子中是触摸,并且只有当该手势与场景中的任何对象接触时才计数,通过传递位置常量 p 作为参数。hitTest() 函数返回一个事件结果数组,然后 count 属性计算该数组的大小。我们可以通过引用该数组的第一个成员来捕获第一次触摸的引用。在这个演示中,我们只有一个对象提供给我们,即飞船,因此我们可以直接获取 Swift 最上层的父对象 AnyObject 的实例。我们的 hitTest 对象 hitResults 是一个包含在这个上下文中触摸到的每个对象引用的数组。再次强调,这只是一个飞船对象,所以我们可以简单地取 hitTest[0] 处的第一个实例。这就是结果常量所代表的。
这行代码 let material = result.node!.geometry!.firstMaterial! 展示了我们是怎样通过使用点操作符深入到节点的子节点来获取该对象材质的引用,同时通过感叹号(!)隐式解包每个节点。这个材质引用在需要触摸使飞船变红时是必需的。
小贴士
这实际上是一个很好的例子,说明了我们如何只选择 SceneKit 场景中的某些对象作为玩家输入的焦点。在这里,它只是使用宽泛的类型 AnyObject 类选择任何对象,但想象一下一款只有特定类型的角色或角色可选择的游戏;比如一个等距俯视射击游戏或实时策略(RTS)游戏。我们可能在采取任何行动之前检查触摸的对象是否仅是某个特定类类型的成员(isKindOfClass())或符合某个协议(conformsToProtocol())。如果你的 RTS 游戏中的玩家只想对坦克对象采取行动?那么结合一个菜单告诉游戏哪个对象类型是焦点,可能就是你在 SceneKit 中获得这种能力的方法。
在行(3)中,默认的 SceneKit 模板也给了我们这段有用的代码,展示了SCNTransaction的使用。SCNTransaction类首次在 iOS 8 中可用,我们可以将SCNTransaction视为一系列我们希望在场景中特定时间点发生的变化和动画。SCNTransaction类从SCNTransaction.begin()调用开始,以SCNTransaction.commit()调用结束。在该块内的场景图动画调用默认情况下会延迟 0 秒被调用。在许多情况下,我们可能想要控制这些动画的持续时间,因此我们在SCNTransaction块的开始处使用setAnimationDuration()函数来设置。SCNTransaction.setAnimationDuration(0.5)将完成此块的时间设置为半秒。请注意,在这个块内还有一个以SCNTransaction.setCompletionBlock{…}开始的代码块。这样做的作用是在它所在的SCNTransaction块完成后才执行调用。在这个模板演示中,首先在半秒内,船只被高亮显示为红色,就像在material.emission.contents = UIColor.redColor()行中所做的那样。完成之后,在接下来的半秒内,船只的颜色通过将材质发射设置回UIColor.blackColor()恢复到原始颜色。一开始这可能有点令人困惑,但我们可以使用这种方法在单个块中为场景执行一系列动画和事务。查看此链接以获取文档;其他过渡/事务可以是淡入淡出、相机视野、旋转、平移、照明等等:developer.apple.com/library/prerelease/ios/documentation/SceneKit/Reference/SCNTransaction_Class/index.html#//apple_ref/occ/clm/SCNTransaction/valueForKey。
至于默认的 SceneKit 模板,这就是制作场景所使用的全部代码。这是一个基本的场景,远非游戏,但它应该能让我们对 SceneKit 中场景的基本结构和逻辑有一个基本的了解。在我们查看 Fox 演示以及一个实际的全游戏项目之前,让我们看看 iOS 9 / Xcode 7 中添加到 Xcode 的一些其他功能。
iOS 9 / Xcode 7 中引入的 SceneKit 功能
让我们回到过渡和动画。截至 iOS 9,我们可以在 SceneKit 中非常容易地更改角色或其他 3D 对象的混合模式。

WWDC2015 中 SceneKit 的各种混合模式的展示
混合模式可以通过一行代码简单地更改,aSCNMaterial.blendMode = .Add,其中 aSCNMaterial 是代表 SCNNode 材料的一个对象。更改混合模式可以创建多种效果。一些游戏使用玩家的 幽灵 来显示他们试图超越的过去运行,或者当 Boss 角色被击败时,会有淡入淡出的效果。结合 SCNTransaction 可以让角色在这些模式中淡入淡出。
音频节点和 3D 声音
截至 iOS 9,我们可以在 SceneKit 场景中放置 3D 声音。SCNNode 类的 addAudioPlayer() 函数让我们可以将声音附加到该节点,并且无论该节点在 3D 空间中的位置如何,声音都将遵循 3D 音频混音;也就是说,如果音频源的 positional 属性设置为 true。以下是使用音频节点创建 3D 声音的方法:
let source = SCNAudioSource(named: "sound.caf")
let soundEffect = SCNAudioPlayer(source: source)
node.addAudioPlayer(soundEffect)
source.positional = true
source.loops = false
这为游戏对象,名为 node 的 SCNNode 提供了音效。
要实际播放声音,我们需要在它上面调用 SCNAction,如下所示:
let action = SCNAction.playAudioSource(source, waitForCompletion: true)
node.runAction(action)
waitForCompletion 属性确保动作持续的时间与声音一样长。但这可能不适合角色音效,因为你可能希望在中间停止(也就是说,玩家击中敌人,取消他们之前开始吟唱、尖叫或其他类似动作)。
环境音效和音乐
要添加音乐和环境音效,我们可以遵循添加音效到节点的相同方法:创建一个 SCNAudioSource 对象;将其添加到 SCNAudioSource 对象中;然后使用 addAudioPlayer 将其添加到我们的节点中。唯一的区别是,我们会循环播放音乐并将它的 positional 属性设置为 false,如下所示:
source.positional = false
source.loops = true
SceneKit 中的 SpriteKit 场景过渡
SpriteKit 有一些很棒的场景过渡效果。我们可以让它看起来像门在打开或页面在翻动。这可以为你的游戏增添额外的特色和精致。在 iOS 9 之前,我们无法在我们的 3D SceneKit 中进行这些 2D 过渡,但自从 Xcode 7 和 iOS 9 以来,我们可以在 SceneKit 中这样做,下面是如何操作的:
aSCNView.presentScene(aScene, withTransition:aSKTransition,
incomingPointOfView:nil, completionHandler:nil)
再次强调,aSCNView 只是对某个 SCNView 对象的一般引用,当我们向该视图呈现场景时,我们有传递一个 SKTransition 对象作为 withTransition 参数的选项。incomingPointofView 参数可以是在过渡期间相机视点的引用,而 completionHandler 参数是在场景过渡后调用的完成块的名称。例如,我们可以在阶段完成后过渡到得分场景中调用开始计算上一阶段得分的函数。我们可能不想在知道场景已经 100% 过渡到或在这种情况下,在知道前一个场景的总分之后开始新场景的计算和其他函数。
在类参考页面上查看一些 SKTransition 的更多示例,也许有一个过渡可以帮助增强你游戏的设计:
狐狸演示
我们的大部分时间都花在了 SpriteKit 和这里的 SceneKit 上,编写构成游戏逻辑的样板代码。随着 Xcode 的不断更新,iOS 游戏设计中的视觉设计功能也在不断更新,这些功能不需要对代码有深入的了解。总是有一些脚本涉及其中,但游戏设计中的一个关键特性,嗯,就是它的设计方面。在WWDC15活动上,iOS 9 和 Xcode 7 的介绍是一个很好的游戏演示,它不仅能教会我们一些 Xcode 可以做的视觉设计功能,还为我们提供了一个在 SceneKit 中制作平台游戏的美丽起点。这个演示名为Fox,虽然实际上它以红熊猫为主角而不是狐狸,但我们可以原谅这种混淆,因为它功能丰富且对学习如何开发 SceneKit 驱动的 iOS 游戏至关重要。

展示我们的玩家角色和关卡资源的狐狸演示图像
这个演示中还有许多我们在这里无法展示的内容,因此我们鼓励您自己下载并查看它提供的所有 SceneKit 功能。我们将关注一些尚未在 SceneKit 或 SpriteKit 中涵盖的主题,例如粒子、物理和场景图。狐狸演示还使用了 3D 游戏/艺术设计功能,如天空盒、环境遮挡、立方体贴图照明、碰撞网格等。它真的是一个制作美丽 iOS 游戏的优质演示。
这里是苹果提供的下载链接:
developer.apple.com/library/prerelease/ios/samplecode/Fox/Introduction/Intro.html
注意
在撰写本文时,狐狸演示仅使用 Objective-C 编写。我们在这本书的整个过程中都专注于 Swift,但请不要过于担心,如果您对 Objective-C 的一些方面感到陌生。目标是看到 Xcode 提供的视觉工具。在未来某个时间点,狐狸演示肯定将以 Swift 的形式提供,无论是苹果公司自己还是第三方程序员。
粒子系统
任何游戏中的一些基本资源,无论是 SpriteKit 还是 SceneKit 构建的,都是我们添加到角色、对象或整个场景中的各种粒子效果。粒子可以增强收集物品的感觉,给玩家一个信号,表明玩家发生了某些事情,比如他们正在获得或失去健康,或者显示即将到来的 Boss 战的存在的力量。

来自狐狸演示的可收集粒子效果
在过去,制作粒子效果的过程是手动创建有时相当复杂的粒子发射器着色器对象,使用 OpenGL 代码。如果我们选择这样做(使用苹果的快速、低级 API,Metal 或 OpenGL),这仍然可以做到,但随着时间的推移,视觉创建和编辑粒子效果的过程已经变得更加简单。在 iOS 开发历史上不久前,像 Cocos2D/ Cocos3D 这样的框架允许我们使用第三方粒子效果构建器导入到我们的游戏中。随着 SpriteKit 和 SceneKit 的出现,从 iOS 7/iOS 8 开始,Xcode 创建了一个更直观的粒子表示,从而为我们节省了大量创建我们想要和期望在游戏中看到的效果的时间和精力。之前显示的图像显示了 Xcode 粒子系统编辑器,其中包含 Fox 演示的可收集闪光效果。
要在 SceneKit 中创建自己的粒子效果,请按照以下步骤操作:
-
通过导航到文件 | 新建 … 或者简单地使用键盘快捷键command + N,创建一个新文件,就像我们过去做的那样。
-
然后,我们在 iOS 下选择资源部分,并选择SceneKit 粒子系统模板。(如果使用 SpriteKit,请选择SpriteKit 粒子文件。)
![粒子系统]()
-
SpriteKit 和 SceneKit 的粒子选项都为我们提供了一个基本粒子模板列表,我们可以从这里开始,例如反应器、闪光或模糊。选择您想要的任何一个,或者在这里的演示中查看可收集的一个。对于 SpriteKit,这会创建一个 SKS 文件和粒子的图像遮罩。SceneKit 模板通过 SCNP 文件和图像遮罩创建 3D 粒子效果。
让我们回顾一下在演示中为可收集粒子创建的粒子系统。如果没有选择,请点击属性检查器以查看我们可以编辑以自定义粒子效果的各个控件。

在检查器中,您可以随意测试多个变量和字段。这里有出生率,它控制粒子重新开始其起始和结束动画的频率,图像,它可以构成粒子的形状和颜色,以及决定效果整体方向的各个角度。还有一个循环下拉菜单,它使粒子在场景中粒子系统的生命周期内重复。此外,受重力影响切换是我们用来根据场景的重力使粒子下落的。集合粒子在没有重力的情况下不断循环,而纸屑粒子发生一次并下落到重力,正如我们预期的那样。如果场景中的物体有一个物理场,我们还可以让粒子对其做出反应。
将粒子放入我们的 pioscene 中
当我们创建 SpriteKit 或 SceneKit 粒子时,我们可以通过代码在 SpriteKit 或 SceneKit 中调用它们:
SpriteKit
SpriteKit 粒子不在 Fox 演示中,但稍微回顾一下我们对 SpriteKit 的讨论,如果我们想在 2D SpriteKit 游戏中添加粒子,下面是如何实现这一点的示例:
//(1)
var path = NSBundle.mainBundle().pathForResource("Spark", ofType: "sks")
//(2)
var sparkParticle = NSKeyedUnarchiver.unarchiveObjectWithFile(path!) as! SKEmitterNode
//(3)
sparkParticle.position = CGPointMake(self.size.width/2, self.size.height)
sparkParticle.name = "sparkParticle"
sparkParticle.targetNode = self.scene
self.addChild(sparkParticle)
-
我们使用
NSBundle.mainBundle().pathForResource()函数调用创建一个指向我们应用程序包的路径,并传递粒子文件名称的字符串,在本例中为Spark,以及文件类型,SKS。 -
接下来,我们使用
NSKeyedUnarchiver.unarchiveObjectWithFile(path!)调用创建sparkParticle对象,正如我们所看到的,它使用了我们在部分(1)中创建的路径。它被转换为 SpriteKit 的粒子对象SKEmitterNode。NSKeyedUnarchiver是一个用于从键值归档中解码命名对象的类,这是一个归档的编码层次结构。这个类有一些已知类型转换的支持。简而言之,它可以解码文件中的对象,无论是 32 位还是 64 位架构。更多关于这个特殊的文件解码类信息请参阅:developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSKeyedUnarchiver_Class/ -
我们然后为这个粒子效果设置位置和名称,并将其定位到场景中,同时将其添加到场景节点中。
虽然这个例子没有在 Fox 演示中给出,但这是一个很好的例子,说明了我们如何针对项目中导航层次结构中的特定文件。
SceneKit
SceneKit 粒子是SCNParticleSystem类的成员。我们通过SCNNode类的addParticleSystem函数将这些粒子添加到我们的场景中。Fox 演示在collectFlower()函数中通过以下 Objective-C 行来实现这一点:
[self.gameView.scene addParticleSystem:_collectParticles withTransform:particlePosition];
这段代码所做的是在指定的视图中调用场景,并将之前在类中声明的粒子(_collectParticles)添加到我们的场景中。然后它告诉场景这个效果将在空间中的哪个点出现。在这种情况下,是particalPosition变量,当回溯时,它来自传递给collectFlower()函数的SCNNode参数。
备注
下面是如何用 Swift 编写的:
scene.particleEmitNode.addParticleSystem(_collectable!)
Swift 的addParticleSystem API 不幸地没有像 Objective-C 那样的withTransform参数,因此我们必须将粒子系统添加到它将从中发射的节点上,这由particleEmitNode变量表示。这很可能会在 Swift 2.x 及以后的 API 更改中发生变化。
介绍 SceneKit 和 SpriteKit 物理
当我们查看我们的粒子示例中的collectFlower()函数时,我们看到有一个SCNNode参数被传递。这个节点来自physicsWorld函数。在 SpriteKit 和 SceneKit 中,我们可以创建一套整体的物理规则,并处理各种与物理相关的交互,最值得注意的是,两个或更多节点之间的接触。任何游戏最基本的一个方面就是在游戏对象相互碰撞时做些什么。这可能是当玩家触摸可收集物品时,当敌人接触玩家或玩家用攻击击中敌人时。在 iOS 开发和游戏引擎中,我们称这些二维精灵或三维对象之间的边界为边界框。我们在关于 iOS 9 及以后的debugOptions属性的讨论中简要提到了这些物理对象。SceneKit 对象的边界框是基于对象的简化几何形状自动创建的,但我们可以使用SCNBoundingVolume类来编辑这些形状。
更多关于这个类的文档可以在developer.apple.com/library/prerelease/ios/documentation/SceneKit/Reference/SCNBoundingVolume_Protocol/index.html找到。
iOS 中的游戏物理和一般游戏开发是比我们在这个章节中能讨论的更大的主题。所以现在,让我们看看 Fox 演示和一般 iOS 游戏是如何处理两个节点接触其边界框的简单概念的。
func physicsWorld(world: SCNPhysicsWorld, didUpdateContact contact: SCNPhysicsContact) {
if (contact.nodeA.physicsBody.categoryBitMask == AAPLBitmaskSuperCollectable) {
self.collectFlower(contact.nodeA)
}
if (contact.nodeB.physicsBody.categoryBitMask == AAPLBitmaskSuperCollectable) {
self.collectFlower(contact.nodeB)
}
以下是 Fox 演示的physicsWorld函数的 Swift 伪代码示例。该函数接受两个参数,world类型为SCNPhysicsWorld,表示场景的整个物理环境,以及表示物理接触的SCNPhysicsContact类型的对象。该函数检查接触中节点的位掩码。如果接触的第一个或第二个节点(nodeA或nodeB)在花朵的特定类别中,则调用collectFlower()函数,并将该可收集物品的节点作为参数传递。
位掩码是指我们为另一组位指定一组位,这组位可以通过位运算组合在一起。可以这样想,使用 1 和 0 不仅可以将一系列的 1 和 0 进行分类,还可以让我们处理在相同上下文中发生多个分类的情况。
例如,在我们的游戏中,我们有不同类别的事件/对象,并将它们放入一个字节的(8 位)自己的槽中。在 Fox 演示中,游戏碰撞是一个 2 位的位移值,因此它们在二进制中表示为 00000100。Fox 演示中可收集物品的类别指定是一个 3 位的位移值或 00001000,敌人是 4,00010000。
在演示中,我们看到以下AAPLBitmaskSuperCollectable的代码:
// Collision bit masks
typedef NS_OPTIONS(NSUInteger, AAPLBitmask) {
AAPLBitmaskCollision = 1UL << 2,
AAPLBitmaskCollectable = 1UL << 3,
AAPLBitmaskEnemy = 1UL << 4,
AAPLBitmaskSuperCollectable = 1UL << 5,
AAPLBitmaskWater = 1UL << 6
};
当nodeA或nodeB的类别掩码与花朵可收集物匹配时(如果槽位是开启的,或者说等于 1,那么我们知道可收集物参与了碰撞)。
Swift 1 版本并没有真正有类似的方式模仿 Objective-C 中的 NSOptions 所执行的位掩码,但自 Swift 2.0 起,我们可以像以下演示那样执行位掩码操作:
struct AAPLBitmask : AAPLBitmaskType {
let rawValue: Int
init(rawValue: Int) { self.rawValue = rawValue }
static var None: AAPLBitmaskType { return AAPLBitmask(rawValue: 0) }
static var AAPLBitmaskCollision : AAPLBitmask { return AAPLBitmask(rawValue: 1 << 2) }
static var AAPLBitmaskCollectable : AAPLBitmask { return AAPLBitmask(rawValue: 1 << 3) }
static var AAPLBitmaskEnemy : AAPLBitmask { return AAPLBitmask(rawValue: 1 << 4) }
static var AAPLBitmaskSuperCollectable : AAPLBitmask { return AAPLBitmask(rawValue: 1 << 5) }
static var AAPLBitmaskWater : AAPLBitmask { return AAPLBitmask(rawValue: 1 << 6) }
}
实际上,它是一个返回自身位运算静态变量的结构体。它不如 Objective-C 和过去的 C 实现中那样优雅,但如果我们希望在 Swift 的样板代码中使用位掩码,这应该允许你这样做。
最后关于physicsWorld()函数的一点说明,为了在两个物理体碰撞时调用该函数,我们需要设置物理世界代理。在大多数情况下,这个代理会是当前的游戏场景。
scene.physicsWorld.contactDelegate = self
Xcode 很可能会告诉你没有设置物理世界代理,如果你还没有设置,通常这段代码会放在ViewController的viewDidLoad()函数中。
视觉组成的游戏场景图
回到 Fox 演示的视觉方面,让我们看看项目中创建的游戏场景对象以及我们如何查看所谓的场景图中的节点。

我们可以看到在 Fox 演示中,游戏对象和粒子效果可以在 Xcode 中视觉上操作,并在一个视图中一起显示。前面的图像显示了花朵可收集物及其组件,包括 3D 网格、光照、边界框和粒子效果。在 SceneKit 中,我们使用 SceneKit 场景文件(SCN)来完成这项操作。

要查看场景的场景图,请点击 Xcode 窗口中视觉编辑器窗口底部左侧的侧窗图标,如图下截图所示:

这是场景图的截图。那些熟悉游戏引擎,如 Unity 和 Unreal Engine 的人,会对这种组件/游戏场景视图非常熟悉。场景图显示了场景中所有对象的下拉层次结构,包括它们自己的内部组件。花朵升级由一个名为flower的 3D 网格模型组成,它有两个子粒子效果以及一个物理体。这三个都由图右侧的对象上的三个符号表示。

我们可以使用前面图像中看到的X、Y和Z标记在场景中移动模型。我们还可以放大、缩小、旋转场景,以及向场景添加更多对象。

要向场景添加更多对象,请按照以下步骤操作:
-
前往屏幕底部右侧的库窗口中找到的媒体库(如图所示)。
-
现在搜索
grass并将其简单地拖放到场景中。现在预制的草地对象已作为参考出现在这个场景中。![视觉组成游戏场景 scgs]()
-
这实际上就是
level.scn文件是如何组成的。 -
还可以选择将基本对象添加到场景中,并从那里构建它们,这又与以设计为中心的游戏引擎非常相似。只需从位于媒体库图标旁边的对象库标签中选择,并搜索
geometry。有基本对象,如球体、平面和盒子。![视觉组成游戏场景 scgs]()
-
基本对象缺少我们在项目中的草地和其他预制对象中可以看到的照明、材质和其他细节。使用这些对象的更详细检查器窗口来查看和编辑各种细节,例如物理体、材质、烘焙照明以及任何脚本/编码的对象名称识别。
![视觉组成游戏场景 scgs]()
-
还可以添加到这些对象中的动作。点击位于流程场景视图右侧的次要编辑器图标(一个倒三角形在正方形中,位于底部)。如果场景图中选择了花朵资产,这将打开次要编辑器,显示一个
RotateByEuler动作。![视觉组成游戏场景 scgs]()
-
这个动作的作用是每秒旋转一次花朵。要看到这个动作的效果,请点击位于次要编辑器窗口时间轴上方的播放按钮。我们可以看到这个对象将如何旋转。
-
可以通过在次要编辑器时间轴中展开或收缩来缩短或扩展动作。更多关于动作的详细信息可以在检查器窗口中编辑,如果我们愿意,可以使用库向该对象添加更多动作或删除提供的动作以使其以不同的方式行动。
自己测试几个动作、时间和属性。我们可以看到,不使用任何代码,我们如何可以直观地设置场景并动态控制场景中每个对象的动作。许多这些视觉功能和动作都适用于 SceneKit 和 SpriteKit 场景。

查看level.scn文件,可以看到一个包含完整组成级别相机对象的场景(如前一张截图所示)。你想制作一个类似的级别,可能包含更多障碍物和不同的天空盒吗?复制级别并更改这些资产,并将其命名为level2。这可以在游戏和级别的设计中节省大量时间。从 Xcode 7 开始,我们有了 IDE 中直接的工具,这些工具最初仅用于多平台游戏引擎。这真的让设计回到了游戏设计。
我们之前讨论的大多数手动代码可能会让人感到沮丧,尤其是对于那些可能想进入游戏设计但仍然相对新手的人来说。
//Objective-C
SCNScene *scene = [SCNScene sceneNamed:@"game.scnassets/level.scn"];
//Swift
let scene = SCNScene(named: "game.scnassets/level.scn")!
这是我们从视觉设计工具中引用场景所需的所有内容。就像我们在 SceneKit 基本模板中提到的那样,将其添加到视图的根节点,它就准备好了。使用代码添加生成敌人、玩家和 2D SpriteKit 叠加层(它本身可以在其 SKS 文件中拥有动作和视觉设计),这样就构成了一个完整的游戏。
如需更多关于 SceneKit 框架以及其库的最新更新和新增内容的信息,请参阅以下完整文档链接:
developer.apple.com/library/ios/documentation/SceneKit/Reference/SceneKit_Framework/
摘要
在本章的开头,我们首先简要地谈到了 iOS 中 3D 游戏设计的历史以及 SceneKit 是如何从需要有一个针对 3D 游戏开发复杂性的第一方、动态稳健的框架的需求中产生的。然后,我们介绍了 SceneKit 的基本结构以及它和上一章中提到的 SpriteKit 是如何基于节点概念工作的,从视图开始,然后是场景节点以及该场景中的子节点。接下来,我们讨论了 SpriteKit 和 SceneKit 如何在同一场景中一起使用,然后我们分析了 Xcode 中给出的默认 SceneKit 模板及其各种资源。除了回顾模板项目的代码外,我们还回顾了一些功能、代码和资源,例如自 iOS 9 / Xcode 7 以来可用的音频节点、渲染模式以及调试选项。最后,在本章的剩余部分,我们详细讨论了在WWDC15大会上展示的 Fox 演示以及自 Xcode 7 发布以来可用的各种视觉游戏设计功能。
在下一章中,我们将深入探讨 GameplayKit 框架的特点,这是我们之前在介绍使用基于组合的结构构建游戏时的好处时简要介绍过的。使用 GameplayKit,我们可以像在本章中处理游戏的视觉组件那样,复制和重用预先制作的游戏动作和规则。
第五章. GameplayKit
多年来,视频游戏开发一直依赖于面向对象设计(OOD)的原则。在 OOD 的核心特性中,继承和多态的概念在这一分支的软件工程中最为有用。将游戏中的实体视为同质的对象群体是有意义的;然后我们为这些对象编写规则,以确定它们如何相互交互。例如,得益于继承,我们游戏中的所有对象都可以赋予GameObject类名;它们拥有我们在整个游戏中都会使用的功能,然后我们可以将它们分支到子类,例如Player或Enemy。然后,随着我们提出更多具体的实体类型,无论是Player、不同的敌人、非玩家角色(NPCs)还是我们为制作的游戏所需要的内容,我们可以继续这种思考过程。对那些对象调用函数,如Shoot()或Health(),可能对父类的每个子类都是独特的,因此我们在 OOD 中利用了多态。
然而,正如前一章所述,尽管基于继承的结构对于大多数软件应用(包括简单的游戏)来说都很不错,但视频游戏规则和实体的独特需求和配对导致基于继承的结构违反了 OOP 的一个规则。这个规则就是代码的可重用性。解决这个问题的方法是,将游戏对象和游戏规则分开,称为基于组件的结构。以这种心态构建游戏可以让我们构建独特的对象、动作和规则,不仅能够在单个游戏项目中调整它们,还可以在其他项目中使用它们,从而减少通过基于继承的结构构建游戏时可能导致的过度定制化结构。
苹果针对这个问题提出的解决方案是 GameplayKit 框架。GameplayKit 是一个完全独立的框架,可以与 SpriteKit 或 SceneKit 游戏以及使用底层 API(如 OpenGL 和 Metal)编写的游戏一起使用。在WWDC15上首次宣布支持 iOS 9 和 Xcode 7,GameplayKit 将多年来在游戏开发中使用的常见方法和概念提取出来,使我们能够独立于屏幕上的绘制内容来处理这些方面。这个框架不处理屏幕上的绘制内容,因此它是专门为 MVC 的模型部分设计的。
GameplayKit 处理了几个游戏开发概念,我们将在本章中回顾这些概念。这些概念包括实体和组件、状态机、代理、目标、行为、路径查找、MinMaxAI、随机源和规则系统。
实体和组件
我们可以将实体想象成游戏中的对象。它们可以是玩家、敌人角色、NPC、关卡装饰和背景,甚至是用来告知玩家生命值、力量和其他统计数据用的 UI。实体被视为组件的容器。组件是决定实体外观和行为的特性。有人可能会问,“这与对象和函数有什么不同?”简短的回答是,在基于继承的设计中,对象和函数描述了我们的游戏对象是什么,而基于组件的结构化则更多地关注它们做什么。随着我们处理 GameplayKit 框架的类和功能,我们将能够更好地理解这一点。在这个框架中,我们将看到实体和组件分别通过 GKEntity 和 GKComponent 类来处理。
注意
如果你对于基于组件的结构化还有一点点困惑,可以回顾我们之前的一章,在那里我们对此进行了更详细的介绍。你还可以访问关于这种设计方法的开发者页面:developer.apple.com/library/prerelease/ios/documentation/General/Conceptual/GameplayKit_Guide/EntityComponent.html。
在我们的游戏中使用 GKEntity 和 GKComponent 对象
任何熟悉 Java 或 C# 的人都会理解抽象类的概念。GKComponent 类本质上是一个抽象类。引用 WWDC 的演讲者的话:将组件想象成“功能的小黑盒”。GKEntity 类的实例就像我们之前提到的通用 GameObject 类。然而,与之前我们处理的对象不同,我们通常不会给它们添加太多的自定义功能(否则,我们就会倾向于基于继承的结构化)。
我们首先创建一个游戏对象,并将其作为 GKEntity 类型的成员进行子类化。在这个例子中,让我们将我们的对象类命名为 GameEntity。同时,别忘了导入 GameplayKit API:
import GameplayKit
class GameEntity : GKEntity
WWDC15 conference, in both Objective-C and Swift, showing how we'd create entities, components, and component systems in our projects:
//Objective-C
/* Make our archer */
GKEntity *archer = [GKEntity entity];
/* Archers can move, shoot, be targeted */
[archer addComponent: [MoveComponent component]];
[archer addComponent: [ShootComponent component]];
[archer addComponent: [TargetComponent component]];
/* Create MoveComponentSystem */
GKComponentSystem *moveSystem =
[GKComponentSystem systemWithComponentClass:MoveComponent.class];
/* Add archer's MoveComponent to the system */
[moveSystem addComponent: [archer componentForClass:MoveComponent.class]];
//Swift
/* Make our archer */
let archer = GKEntity()
/* Archers can move, shoot, be targeted */
let moveComponent = MoveComponent()
let shootComponent = ShootComponent()
let targetComponent = TargetComponent()
archer.addComponent(moveComponent)
archer.addComponent(shootComponent)
archer.addComponent(targetComponent)
/* Create MoveComponentSystem */
let moveComponentSystem = GKComponentSystem(componentClass: MoveComponent.self)
/* Add archer's MoveComponent to the system */ moveComponentSystem.addComponent(archer.componentForClass(MoveComponent.self)!)
这段代码的作用是创建我们的 GKEntity 对象;在这个例子中,是塔防游戏示例中的弓箭手角色。接下来,它通过 addComponent(_:) 函数添加预定义的 GKComponent 对象。我们还创建了一个名为 moveComponentSystem 的 GKComponentSystem 对象,它将只更新移动类型组件。弓箭手自己的 moveComponent 类通过 moveComponentSystem.addComponent(_:) 添加到这个系统中。注意,除了初始化之外,通过这个对象传递的参数是组件类型的类类型,这些类型由 .class 或 .self 属性表示,具体取决于我们用哪种语言编写代码。
注意
截至 publication,componentForClass() 函数可能对 Swift 编程语言不完全有效。因此,如果 Swift 实现对于这个和其他 GameplayKit 对象初始化没有按预期工作,您将需要使用 Objective-C 版本的此代码,并通过 Objective-C-Swift 桥接文件将其链接到您的项目中。随着苹果继续将 Objective-C 从平台的主要语言转变为次要语言,这很可能在未来 Swift 的更新中得到解决。有关如何创建此桥接文件的更多信息,请参阅此链接:developer.apple.com/library/ios/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html。
苹果为我们提供了一个名为 Maze 的项目,该项目使用了这些类以及其他我们很快就会讨论的概念。
这里有一个链接,可以帮助您更好地了解实体和组件:
developer.apple.com/sample-code/wwdc/2015/downloads/Maze.zip
在我们讨论与 GKEntity 和 GKComponent 对象相关的更具体的代码使用之前,我们将探讨一个与这些对象最佳结合的游戏开发概念,即状态机的概念。
状态机
与其他类型的应用程序相比,视频游戏的大部分逻辑都基于游戏或游戏中的实体当前是否处于多种不同状态之一。
这可能包括检查游戏是否处于 intro 场景,是否在主游戏模式中运行,玩家是否已死亡,玩家是否空闲,一个 Boss 敌人是否出现,游戏是否结束,阶段是否结束,Boss 血量是否很低,等等。

状态机示例,适用于人工智能或角色动画
在过去,游戏开发者编写自己的自定义状态机逻辑一直是常见的做法,然后使用更新/渲染周期来检查这些各种状态。通常这会在自定义类或简单地在一个自定义的枚举对象中完成,该对象会通过各种状态进行切换,例如 .GameOver、.MainGame、.LowHealth 等。这些状态也可以描述我们游戏中单个实体的状态,并规定要运行哪个动画周期。例如,玩家可能正在充能攻击,我们希望使用玩家的这种状态来动画化充能动画。游戏场景中的对象可能会通过 switch 语句检查这些状态,以确保它们不会执行与状态上下文不符的任何动作。不久之后,多平台游戏引擎使这部分成为工作流程的一部分,尤其是在动画处理程序中。这些让我们能够通知游戏及其中的实体的各种状态的对象被称为状态机。GameplayKit 允许我们与其组件/实体功能一起工作,这个框架为我们提供了一个抽象类 GKState,我们可以从它派生我们的游戏状态,以及一个类 GKStateMachine,用于将这些状态对象放入指定的状态机中。GKStateMachine 类型的对象一次只能处于一个状态,因此它为我们提供了更好的使用和重用这些状态的方法,而不是旧的样板/switch 语句方法。
之前的图表来自 WWDC15,展示了类似 PacMan 的幽灵角色或任何其他游戏角色的动画和 AI 状态机的样子。请注意,并非所有路径都能相互连接。例如,幽灵可以在追逐和逃跑之间来回切换,但在追逐时无法被击败,除非它之前处于被击败状态,否则无法重生。这些被称为状态转换或状态机中的边。
默认情况下,所有边都是有效的,我们在自己的 GKState 对象/组件中重写 isValidNextState(_:) 函数,以告诉状态机我们是否允许在某些状态之间移动。
在 DemoBots 示例程序中的 TaskBotAgentControlledState 类中,就是这样实现的。DemoBots 是在 第三章 中提到的 iOS 9 SpriteKit 示例,SpriteKit 和 2D 游戏设计:
override func isValidNextState(stateClass: AnyClass) -> Bool {
switch stateClass {
case is FlyingBotPreAttackState.Type, is GroundBotRotateToAttackState.Type, is TaskBotZappedState.Type:
return true
default:
return false
}
}
GKState objects to them:
/* Make some states - Chase, Flee, Defeated, Respawn */
let chase = ChaseState()
let flee = FleeState()
let defeated = DefeatedState()
let respawn = RespawnState()
/* Create a state machine */
let stateMachine = GKStateMachine(states: [chase,flee,defeated,respawn])
/* Enter our initial state - Chase */
stateMachine.enterState(chase.classForCoder)
在前面的代码中,我们看到从GKState(追逐、逃跑、失败和重生)的预制类中创建的状态。在初始化时,stateMachine对象接收一个GKState对象数组的参数,如下所示:let stateMachine = GKStateMachine(states: [chase,flee,defeated,respawn])。然后,在这个例子中,我们从chase状态开始那个状态机。当然,这会根据你自己的游戏组件的逻辑而有所不同。GKStateMachine对象还可以返回currentState()函数;因此,我们可以根据游戏对象的当前脉动来引导我们游戏中的各种实体和组件。
在以下完整文档中了解更多关于GKState和GKStateMachine的信息:
接下来,我们将介绍代理、目标和行为。
代理、目标和行为
当我们在游戏中创建实体时,尤其是那些不是玩家的实体,我们希望它们执行各种动作。这些动作是由我们赋予它们的人工智能(AI)决定的,并基于游戏的各种状态、玩家、环境或玩家自身。我们可以让一组敌人沿着特定的路径移动,追踪玩家,或者使用游戏物理世界自动平滑地绕过障碍物。该框架允许我们使我们的游戏实体成为所谓的代理。代理是可以附加目标和行为的实体。
GameplayKit 中的代理,利用GKAgent类,可以拥有自动设置各种行为并基于其目标权重GKComponent对象。目标权重通常是一个从0到1的浮点数。与其他目标相比,目标权重值越高,代理执行那些行为的可能性就越大。例如,如果一个敌人角色的健康值很低,我们可能希望他们的Heal目标具有更高的目标权重。敌人将通过更频繁地治疗来表现出当前低健康状态的紧迫性,从而给玩家提供一个更具挑战性和智能的对手。换句话说,代理、目标和行为是一个可堆叠和可塑的 AI 系统。
这里是 GameplayKit 中此功能的一个概述:

通过 GKBehavior 类,一个行为由一系列 GKGoal 对象组成,每个对象都被赋予了一定的权重。例如,我们可以为赛车游戏中的非玩家角色(NPC)创建一个名为 RacingBehavior 的 GKBehavior 类。这种行为将是两个目标(如 FollowPath 和 AvoidAgents)的组合,这些目标将使我们的游戏中的角色在当前阶段保持在当前赛道上,同时自动避开其他 NPC。
这里是这些类的视觉表示:

如前图所示,GKAgent 对象具有许多基于物理的属性,例如 mass(质量)、radius(半径)、maxSpeed(最大速度)等。与其他 GameplayKit 对象一样,它使用 updateWithDeltaTime() 函数与 GKComponentSystem 或 GKEntity 的渲染/游戏循环更新同步。你开始看到这些对象的模式了吗?从某种意义上说,我们也可以将 GKAgent 对象视为类似于 SpriteKit 或 SceneKit 节点,因为它们在我们的游戏物理上工作。然而,无论我们是用 SpriteKit、SceneKit 还是自己的自定义渲染组件(如 OpenGL 或 Metal)制作游戏,我们都需要使用特殊的 GKAgentDelegate 类将这些类链接到屏幕上显示的内容。以下是该类及其功能的图解:

agentWillUpdate() 函数是我们用来在游戏 update() 函数之前告诉智能体要做什么的,而 agentDidUpdate() 函数则是用来在 update() 函数之后告诉智能体在屏幕上要做什么的。在 Follow GKGoal 对象的情况下,这可以是在更新发生之前对玩家位置的引用。以下是从 WWDC15 中找到的例子,但与提供的 Objective-C 示例不同,这里使用的是 Swift 编写:
func agentWillUpdate(agent:GKAgent)
{
/* Position the agent to match our sprite */
agent.position = self.position
agent.rotation = self.zRotation
}
func agentDidUpdate(agent:GKAgent)
{
/* Position the sprite to match our agent */
self.position = agent.position
self.zRotation = agent.zRotation
}
TaskBotBehavior.swift class, which is a child of GKBehavior:
//(1)
let separationGoal = GKGoal(toSeparateFromAgents: agentsToFlockWith, maxDistance:
GameplayConfiguration.Flocking.separationRadius, maxAngle: GameplayConfiguration.Flocking.separationAngle)
//(2)
behavior.setWeight(GameplayConfiguration.Flocking.separationWeight, forGoal: separationGoal)
在行 (1) 中,GKGoal 的 toSeparateFromAgents 参数允许我们传递一个引用,用于我们希望保持一定距离的 GKAgent 对象。
在行 (2) 中,behavior.setWeight() 函数将预定的浮点数 GameplayConfiguration.Flocking.separationWeight 作为这个目标的权重传递。权重越高,该目标的重要性就越高。
从稍后链接的 GKGoal 的完整文档中,你可以注意到 GKGoal 类的大部分内容都处理智能体之间的吸引或排斥。通过组合这种基本功能的不同特性,我们可以创建独特的目标,这些目标由 GKAgent 参数获得,如下所示:developer.apple.com/library/prerelease/ios/documentation/GameplayKit/Reference/GKGoal_Class/index.html。
为了回顾一下,以下是在会议中展示的创建这些对象的基本方法,无论是使用 Objective-C 还是 Swift。
//Objective-C
/* Make some goals, we want to seek the enemy, avoid obstacles, target speed */
GKGoal *seek = [GKGoal goalToSeekAgent:enemyAgent];
GKGoal *avoid = [GKGoal goalToAvoidObstacles:obstacles];
GKGoal *targetSpeed = [GKGoal goalToReachTargetSpeed:50.0f];
/* Combine goals into behavior */
GKBehavior *behavior = [GKBehavior behaviorWithGoals:@[seek,avoid,targetSpeed]
andWeights:@[@1.0,@5.0,@0.5]];
/* Make an agent - add the behavior to it */
GKAgent2D *agent = [[GKAgent2D* alloc] init];
agent.behavior = behavior;
//Swift
/* Make some goals, we want to seek the enemy, avoid obstacles, target speed */
let seek = GKGoal(toSeekAgent: enemyAgent)
let avoid = GKGoal(toAvoidObstacles: obstacles, maxPredictionTime: 0.5)
let targetSpeed = GKGoal(toReachTargetSpeed: 50.0)
/* Combine goals into behavior */
let behavior = GKBehavior(goals: [seek, avoid, targetSpeed], andWeights: [1.0, 5.0, 0.5])
/* Make an agent - add the behavior to it */
let agent = GKAgent2D()
agent.behavior = behavior
我们在前面的代码中看到,当我们创建目标时,我们会将代理分配给它们,这些代理要么是寻求的,要么是避免的。代理上的目标可以有目标速度,如toReachTargetSpeed:参数所示,所有这些都可以捆绑到当前的行为中,并给予它们相应的权重。
这是关于GKGoal、GKAgent、GKAgentDelegate和GKBehavior的更多文档:
另一点需要注意的是,这里传递的障碍物数组引用是GKObstacle类的一部分。这个类引用了场景中的对象,我们告诉代理在移动时通常要避开这些对象,它们是我们下一个主题路径查找的一部分。
路径查找
导航是大多数游戏的一个基本组成部分。在我们的游戏中,可以有一个世界地图场景,显示已经穿越或尚未访问的各种关卡,每个点都有分支路径,或者我们可以有一个 3D 动作平台游戏,其中有一个法术可以指出通往下一个任务或战斗位置的合理路径。我们还可以在俯视视角的等距游戏中看到路径查找。例如,玩家可能正在屏幕上与一群锁定在玩家位置上的敌人战斗。好的路径查找 AI 不仅会告诉敌人向目标移动,还会动态避开任何不可逾越的障碍物,并自动绕行到更好的路径。在我们的关于代理、目标和行为的讨论中,我们多少涉及了这一点。遵循GKAgent对象的行为,与各种游戏物理同步,从而创建与其他场景中的代理/对象一起平滑的 AI 移动。然而,如果能通知这些组件在场景中可以和不可以穿越的地方,那就太好了,这就是路径查找的作用所在。

以下图表显示了路径查找是什么,以及苹果公司在WWDC15会议上提供的游戏内视觉。
-
路径查找涉及具有穿越路径的节点,这些路径被称为导航图中的节点。
-
这些节点可以是单向的或双向的,最重要的是,可以使用这个图计算出一个路径,该路径代表了
GKAgent可以采取的最佳路径。 -
在前面的场景中显示的方块代表放置在场景中的
GKObstacle对象(无论是通过代码还是通过 Xcode 编辑器的工具进行视觉放置)。
这是GKObstacle类的完整文档:
就像其他 GameplayKit 功能一样,我们使用各种抽象类来子类化以设置导航图和整体路径搜索功能;这些类是 GKGraph、GKGridGraph、GKGridGraphNode 和 GKObstacleGraph。

当我们看到类的前置图并逐个处理它们时,并不会感到太过令人担忧。最主要的,也是最常用的类是 GKGraph 类。这就是我们可以附加两种不同的图规范类型之一的地方:GKGridGraph 或 GKObstacleGraph。GKGraph 允许我们添加和删除节点,连接它们,并找到节点之间的最优路径。在这两种规范类型中,GKGridGraph 具有更简单的功能,旨在轻松创建基于 2D 的导航图,而 GKObstacleGraph 允许我们使用 GKObstacle 对象设置导航图。节点会根据它们的形状自动创建在那些障碍物周围,这些类做了许多计算路径的工作,我们的智能体需要从起点到它们设定的路径的终点。如果我们想给节点添加更多功能,比如说如果我们想根据地形类型进行定制化移动,除了形状之外,我们就可以使用 GridGraphNode 的节点。
例如,costToNode() 函数可以用来表示,尽管这条路径在平坦、均匀且相似的平面上是最佳路径,但穿越它将花费更多。例如,如果我们的游戏中存在流沙,玩家可以穿越它,因此没有必要在流沙上创建不可逾越的 GKObstacle 对象。相反,我们会说,两个节点之间穿越这种地形的路径成本更高。这将使我们的游戏导航更智能,并能处理这样的自定义参数。
注意
costToNode() 函数实际上是一个最佳实践的例子。我们可以选择不使用它,但如果我们不小心,我们的游戏路径搜索 AI 可能会变得相当不直观。这不仅会给玩家带来糟糕的体验,而且最终会在调试错误的 AI 行为上花费更多时间。
让我们看看一些代码示例,以更好地理解这些类以及如何使用它们。请注意,截至 WWDC15 的代码是 Objective-C 编写的。
/* Make an obstacle - a simple square */
vector_float2 points[] = {{400,400}, {500,400}, {500,500}, {400,500}};
GKPolygonObstacle *obstacle = [[GKPolygonObstacle alloc] initWithPoints:points count:4];
/* Make an obstacle graph */
GKObstacleGraph *graph = [GKObstacleGraph graphWithObstacles:@[obstacle] bufferRadius:10.0f];
/* Make nodes for hero position and destination */
GKGraphNode2D *startNode = [GKGraphNode2D nodeWithPoint:hero.position];
GKGraphNode2D *endNode = [GKGraphNode2D nodeWithPoint:goalPosition];
/* Connect start and end node to graph */
[graph connectNodeUsingObstacles:startNode];
[graph connectNodeUsingObstacles:endNode];
/* Find path from start to end */
NSArray *path = [graph findPathFromNode:startNode toNode:endNode];
GKObstacleGraph by first manually creating 2D vector points in the points array and initializing the GKObstacleGraph object and graph with those points. Next, the two GKGraphNode2D objects are created to represent the start and end nodes for a hero character in the game. Then, finally, the optimal path for that hero character is created and stored into the array automatically; that is, a path using the graph's findpathFromNode: and toNode: parameters using the startNode and endNode objects, respectively. This path object can then be used in our hero's movement component or may be a map visual component to move to or indicate to the player the correct path needed to traverse the game stage's obstacles.
以下代码示例展示了 DemoBots 项目如何使用 Swift 中的导航,使用所谓的懒存属性。
更多关于 Swift 关键字 lazy 的信息可以在这里找到:
来自 DemoBots 的 Swift 示例:
lazy var graph: GKObstacleGraph = GKObstacleGraph(obstacles: self.polygonObstacles, bufferRadius: GameplayConfiguration.TaskBot.pathfindingGraphBufferRadius)
lazy var obstacleSpriteNodes: [SKSpriteNode] = self["world/obstacles/*"] as! [SKSpriteNode]
/*the above line casts the obstacles in our project's "world/obstacles/" folder path as an implicitly unwrapped array of SKSpriteNodes
*/
lazy var polygonObstacles: [GKPolygonObstacle] = SKNode.obstaclesFromNodePhysicsBodies(self.obstacleSpriteNodes)
简而言之,懒惰变量是快速数组初始化,它们的值最初是未知的,并由外部来源控制。在 DemoBots 的情况下,这些是通过 SpriteKit 节点的边界自动创建的障碍物,这是通过 SpriteKit 节点函数obstaclesFromNodePhysicsBodies()完成的。这个例子仅仅展示了使用提供的框架可以节省多少时间。在第一个例子以及过去的游戏开发中,这部分逻辑大多需要通过极其复杂的样板代码逻辑手动完成。
想了解更多关于使用 GameplayKit 进行路径查找的信息,请查看以下示例和文档:
MinMaxAI
到目前为止,我们创建的 AI 非常适合那些在场景中活跃的组件和对象,它们有运动、行为和导航,但关于能够理解游戏规则的 AI 呢?一个很好的例子是象棋或其他各种类似棋盘/拼图的游戏。控制计算机在游戏中以不同难度级别取得进展的能力将是非常棒的。我们还可以希望让游戏为我们决定下一步的最佳走法。这种类型的东西在类似宝石迷阵®**或**糖果传奇®的三匹配类型游戏中很常见,在这些游戏中,你看着网格,游戏会给你提示。这种逻辑正是MinMaxAI发挥作用的地方。

MinMaxAI 通过列出游戏中所有可能的走法并将它们放入决策树中来工作。根据我们给 AI 提供的参数,我们可以告诉它如何选择这些决策分支,通常是以游戏难度为标准。这是通过输入玩家、他们所有可能的走法以及他们的分数,并将它们插入到一个Game Model协议中完成的,然后该协议使用 MinMaxAI 来确定最佳走法。WWDC15中的井字棋示例在前面图中展示。注意,有些分支可能会导致计算机 AI 的损失多于平局或胜利。更难的游戏难度会使计算机玩家选择更有可能带来胜利的路径,或者,在那些三匹配游戏中,为玩家提供下一步最佳走法的建议。
当然,正如人们可能已经猜到的,这种逻辑最适合回合制或基于拼图的游戏。MinMaxAI 可以在任何游戏中工作,但那个游戏,或者至少 MinMaxAI 的实现,只有在有一个固定的移动基础和未来移动可以纳入其Game Model协议的情况下才能工作。例如,一个动作平台游戏除非提供了某些功能选择,否则无法使用 MinMaxAI。GameplayKit 中这个功能很棒的地方在于,它不需要了解你游戏规则的具体细节;它只需要能够查看未来可能的移动。

类图显示了处理 MinMaxAI 时使用的类和函数。我们看到GKGameModel,它实际上是一个游戏状态的协议。遵循此协议的GKState对象需要提供一个玩家列表、活动玩家、玩家的得分以及玩家的移动列表,后者通过gameModelUpdatesForPlayer()函数实现。然后我们通过applyGameModelUpdate()函数告诉GKGameModel对象在移动到下一个游戏动作时应该做什么。GKGameModelUpdate本质上是对游戏动作的抽象,并由GKMinMaxStrategist类用来构建决策树,因此应用于GKGameModel以在setGameModel()函数中改变状态。
GKGameModelPlayer类是一个协议,用于表示进行移动的游戏玩家,如前所述,使用GKGameModelUpdate。playerId属性是一个可以设置的唯一数字,用于在游戏逻辑中区分玩家并处理他们自己的移动集合。这允许玩家(或多人游戏中的玩家)具有提示结构,同时计算机玩家也有自己的 AI 进行移动。playerID属性必须遵循此协议,因为没有它我们就不知道我们引用的是哪个玩家。
GKMinMaxStrategist类是实际与之前协议中创建的gameModel属性关联的 AI。maxLookAheadDepth属性表示 AI 将查看多少步,越多越好,然后通过bestMoveForPlayer()函数返回最佳移动。我们可以使用randomMoveForPlayer()函数为下一个移动选择添加一些随机性;这可以特别用于计算机自己的 AI,可能通过选择一个不太理想的移动来故意使其出错。
在以下代码中提供了一个快速的对象 C 代码片段,展示了如何通过代码实现这一点。如果你只熟悉我们在这本书中提供的 Swift 语言,不必担心语法;只需了解设置这些对象的基本知识即可。
/* ChessGameModel implements GKGameModel */
ChessGameModel *chessGameModel = [ChessGameModel new];
GKMinmaxStrategist *minmax = [GKMinmaxStrategist new];
minmax.gameModel = chessGameModel;
minmax.maxLookAheadDepth = 6;
/* Find the best move for the active player */
ChessGameUpdate *chessGameUpdate =
[minmax bestMoveForPlayer:chessGameModel.activePlayer];
/* Apply update to the game model */
[chessGameModel applyGameModelUpdate:chessGameUpdate];
这也是本章中许多代码片段的来源,就像WWDC15会议上的内容。它以棋局为例。设置棋局模型的详细过程有些复杂,所以只需注意在这个代码中,首先创建了一个ChessGameModel对象(它是抽象GKGameModel类的子类)。然后,我们创建了一个名为minmax的GKMinMaxStrategist类对象,设置了它的游戏模型,将其maxLookAheadDepth属性设置为6,并将游戏移动和当前活动玩家传递给minMax对象。最后,我们使用applyGameModelUpdate()函数更新了游戏模型。当时,这是在 Objective-C 中完成的,但请查看这里找到的FourInaRow演示:developer.apple.com/library/prerelease/ios/samplecode/FourInARow/Introduction/Intro.html。
这个项目将让我们看到这个 AI 的更完整实现。
想要了解更多关于 MinMaxAI 的信息,请查看以下文档链接:
接下来,我们将讨论如何使用 GameplayKit 的随机源为我们的游戏添加可控的随机性。
随机源
在游戏开发初期,随机性一直是人工智能、玩家移动、关卡设计和游戏可玩性的基石。在多种编程语言中的rand()函数,以及一系列用于缩放随机性的数字,通常被用来让我们的应用程序产生一些不太可预测的结果。然而,有时游戏需要我们所说的可控随机性。在调试游戏时,我们不希望遇到已发布产品存在未测试状态的问题。有时,在采用过去的随机性惯例时,我们可能会遇到一些罕见事件只在游戏发布后,在成千上万的,如果不是数百万的玩家手中发生,这些玩家增加了我们在开发阶段没有的测试池。因此,我们可能想要控制随机性的分布。在典型的随机结果选择中,我们得到一个结果呈钟形曲线,平均或中等范围的结果会比边缘结果更频繁地发生。这在某些游戏中是可以接受的,但在其他游戏中则不太理想。关于rand()函数的另一件事是,它的随机性可能会根据其他因素而变化,例如它所在的系统、当前日期和时间以及其他不可控因素。因此,我们需要的是平台无关的确定性和可定制的分布。通过 GameplayKit 的随机源,我们可以实现这一点。

我们在前面的图像中看到了我们可以使用的多个不同类。基本类是 GKRandomSource,它实际上默认使用 ARC4 类型算法(通过其 GKARC4RandomSource 子类)。ARC4 是一种快速/低开销的算法,具有我们在许多实例中使用的典型随机性。它与 arc4Random() C 调用不同,其中 GKARC4RandomSource 的实例是相互独立的。GKRandomSource 也可以成为线性同余或梅森旋转算法的子类。它们的优缺点在图中显示。
不建议将这些对象用于加密,因此最好使用苹果推荐的加密/哈希框架(developer.apple.com/library/ios/documentation/Security/Conceptual/cryptoservices/GeneralPurposeCrypto/GeneralPurposeCrypto.html)。
剩余的类为我们提供了控制随机数/结果分布方法的能力。GKRandDistribution 对象允许我们使用辅助方法,例如,除了让我们设置其最低和最高范围值外,还能创建 x 面骰子。GKGaussianDistribution 和 GKShuffledDistribution 类也允许我们使用这些辅助函数,但GKGaussianDistribution 在我们需要具有中间值比边缘值更频繁出现的钟形曲线类型随机化时使用。其均值和偏差属性为我们提供了对钟形曲线的控制,以及如果我们可能想要更多边缘值出现的情况。GKShuffledDistribution,正如其名称所示,非常适合创建均匀且完整的范围分布,用于洗牌或确保每个值均匀出现。此类的 uniformDistance 属性是一个介于 0.0 和 1.0 之间的浮点数。在 0.0 时,所有洗牌都是完全随机的;在 1.0 时,所有值的分布都是均匀的。
在我们的游戏中添加随机源非常简单。以下是一些使用这些类的代码示例:
/* Create a six-sided die with its own random source */
let d6 = GKRandomDistribution.d6()
/* Get die value between 1 and 6 */
let choice = d6.nextInt()
/* Create a custom 256-sided die with its own random source */
let d256 = GKRandomDistribution.die(lowest:1, highest:256)
/* Get die value between 1 and 256 */
let choice = d256.nextInt()
/* Create a twenty-sided die with a bell curve bias */
let d20 = GKGaussianDistribution.d20()
/* Get die value between 1 and 20 that is most likely to be around 11 */
let choice = d20.nextInt()
/* Create a twenty-sided die with no clustered values — fair random */
let d20 = GKShuffledDistribution.d20()
/* Get die value between 1 and 20 */
let choice = d20.nextInt()
/* Get another die value that is not the same as 'choice' */
let secondChoice = d20.nextInt()
/* Make a deck of cards */
var deck = [Ace, King, Queen, Jack, Ten]
/* Shuffle them */
deck = GKRandomSource.sharedRandom().shuffle(deck)
/* possible result - [Jack, King, Ten, Queen, Ace] */
/* Get a random card from the deck */
let card = deck[0]
如我们所见,这些是非常快速、简单的代码行,所有这些代码都使用了各种随机源类。大多数都是简单的属性调用,因此当我们使用 Swift 创建对象时,如前述代码所示,只需一行或两行代码即可利用这些类类型及其各种随机化功能。结合例如游荡或追踪 AI 行为的目标权重,我们就可以为游戏中的对象和角色获得一些多样化和适度控制的随机性。
要了解更多关于此框架中的随机源/随机化信息,请参阅此处文档链接:
规则系统
最后但同样重要的是,我们来到了 GameplayKit 的规则系统。框架的这一部分使用所谓的模糊逻辑或近似,主要是在游戏状态之间的转换上下文中。这并不是游戏开发中特别新颖的东西。任何熟悉线性插值的人都会感到非常熟悉,因为这实际上是一个相同的概念。与通常围绕物理动作之间的转换的线性插值不同,GameplayKit 的规则系统在各个游戏状态之间执行这些近似转换。想想看,我们游戏中的对象/实体就像名词,组件和动作就像动词,而规则则是这些动词和名词之间的交互。正如我们在本章中看到的,这非常符合游戏状态。那么为什么要在逻辑中添加一个额外的层次呢?好吧,让我们看看 GameplayKit 公告中的这个例子。这是游戏状态和/或实体-组件动作之间可能使用这种模糊逻辑的地方:
if (car.distance < 5) {
car.slowDown()
}
else if (car.distance >= 5) {
car.speedUp()
}
这段伪代码可能代表我们游戏中一个 NPC 汽车。也许是一个城市建造游戏,其中有许多具有此代码作为其行为一部分的GKAgent汽车对象。这似乎听起来不错,直到我们接近或达到5的值。在我们的游戏中,我们可能会注意到一群 NPC 汽车以颠簸的方式加速和制动。为了解决这个问题,我们让制动和加速之间的转换不是那么有限,而是以近似的方式进行转换。

上一张图更好地说明了这一点,左边的原始逻辑和右边的模糊逻辑。这创造了在规则系统起作用时动作或状态之间的平滑过渡;以下是实现此类逻辑所使用的类:

我们使用GKRuleSystem和GKRule类实例来利用规则系统。GKRule代表基于外部状态要作出的一个特定决策,而GKRuleSystem则评估一组规则与状态数据,以确定一组事实。我们可以断言事实或撤销它们,并且我们可以对这些规则之间的模糊性因素进行评分。
让我们通过代码来看看,以更好地理解它:
/* Make a rule system */
GKRuleSystem* sys = [[GKRuleSystem alloc] init];
/* Getting distance and asserting facts */
float distance = sys.state[@"distance"];
[sys assertFact:@"close" grade:1.0f - distance / kBrakingDistance];
[sys assertFact:@"far" grade:distance / kBrakingDistance];
/* Grade our facts - farness and closeness */
float farness = [sys gradeForFact@"far"];
float closeness = [sys gradeForFact@"close"];
/* Derive Fuzzy acceleration */
float fuzzyAcceleration = farness - closeness;
[car applyAcceleration:fuzzyAcceleration withDeltaTime:seconds];
首先,创建 GKRuleSystem 的 sys 对象,然后我们获取 distance 状态值并将其保存到 distance 变量中。接着,我们断言/添加一个名为 close 的规则,当 1.0f - distance / kBrakingDistance 成立时触发。接下来添加的有限规则是 far,定义为 distance / kBrakingDistance,或者基本上是任何大于 1 - distance / kBrakingDistance 的距离。我们创建了新的模糊值 close 和 far,分别命名为 farness 和 closeness,这些值基于 GKRuleSystem 的 gradeForFact 属性。然后,从这个基础上,我们通过 farness 和 closeness 之间的差异得到 fuzzyAcceleration 值,并将这个加速度应用到我们的汽车上。这个操作在更新渲染周期中自动进行,以保持逻辑转换的平滑性,消除不同状态之间的突然运动。
这个简单的示例代码来自 WWDC15,使用的是 Objective-C,但我们可以在完整的文档页面中看到更多示例(其中一些是 Swift 编写的),如下所示:
我们还可以在之前链接到的演示项目中看到一些这样的实现。
使用这些类,我们可以创建许多复杂的规则系统,这些系统以更流畅的方式转换。
摘要
本章深入探讨了大量的独立游戏中心框架。我们首先回顾了实体和组件的基本概念,以及 GameplayKit 如何利用基于组件的结构。然后,我们转向游戏开发的一个基本概念——状态机,以及 GameplayKit 如何利用它们。接着,我们回顾了通过代理、目标和行为自动控制游戏中的组件和实体的方法,以及路径查找的导航图,这些图增加了这种自动化的功能。我们了解到 MinMaxAI 允许我们向玩家暗示未来的移动,或者给计算机提供一种在各种回合制游戏中挑战我们的智能方式。最后,我们看到了如何通过随机源为游戏的结果添加可控制的变化,而规则系统可以防止各种状态转换过于有限。GameplayKit 的内容远不止于此,所以我们强烈建议您阅读之前提供的部分文档链接,以更好地了解这个框架所能提供的内容。在下一章中,我们将继续探讨 Metal API 以及一些其他技巧和提示,这些技巧和提示有助于您充分利用游戏,并保持游戏在至关重要的 60 fps。
第六章:展示你的 Metal 游戏
到目前为止,我们已经学到了很多。我们研究了苹果的 Swift 编程语言,了解了 iOS 应用的一般流程,以及如何通过代码和/或故事板来控制它。我们了解了如何使用SpriteKit制作 2D 游戏和 2D 叠加层,以及如何在Xcode编辑器中使用 SceneKit 设计 3D 游戏。最后,我们回顾了如何使用GameplayKit的各个方面创建可重用的游戏逻辑、组件和 AI。
实际上,这就是规划、编码和构建你自己的游戏所需的所有内容。如果你此时脑海中闪过一个游戏想法,那就直接开始规划吧。前几章中的框架和 Xcode 功能可以帮助将你的抽象想法转化为可能很快就能玩的应用程序。
然而,在继续前进之前,我们想利用这个时间回顾一些额外的提示、技巧和主题,我们要么简要提到了,要么还没有涉及。这些主题主要涵盖了我们可以优化游戏并从苹果硬件中获得更多的方式。在本章中,我们将简要回顾一下苹果 Metal 低级图形 API 的相当高级的主题。
注意
这是一个关于低级图形 API 可能相当高级的警告。这不会是一个关于该主题的全面教程;更像是高级总结,以及如何欣赏 SpriteKit 和 SceneKit 在后台为我们所做的一切。我们希望至少它能让你想要探索如何构建自己的自定义渲染对象,这可能会使开发出性能极强且细节丰富的游戏成为可能。
苹果 Metal API 和图形管线
如果不是现代视频游戏开发的黄金法则,那么一条规则就是保持我们的游戏以每秒 60 帧或更高的速度持续运行。如果为 VR 设备和应用程序开发,这一点尤为重要,因为帧率下降可能会导致玩家感到恶心,甚至结束游戏体验。
在过去,保持精简是游戏的名字;硬件限制不仅阻止了将内容写入屏幕,还限制了游戏可以存储的内存量。这限制了场景、角色、效果和级别的数量。在过去,游戏开发更多地采用工程思维,因此开发者用他们所拥有的少量资源使事物工作。许多 8 位系统及更早的游戏中的关卡和角色之所以不同,仅仅是因为复杂的精灵切割和重新着色。
随着时间的推移,硬件的进步,尤其是 GPU 的进步,使得图形体验更加丰富。这导致了计算密集型 3D 模型、实时照明、强大的着色器以及其他效果的诞生,我们可以利用这些效果让我们的游戏呈现出更佳的玩家体验;同时,我们还要在这宝贵的 0.016666 秒/60Hz 窗口中尽可能塞入所有这些内容。
为了从硬件中获取一切,并对抗设计师希望创造最佳视觉体验的需求与硬件在当今 CPU/GPU 中存在的限制之间的冲突,苹果开发了 Metal API。
CPU/GPU 框架级别
Metal 是一种低级 GPU API。当我们在 iOS 平台上构建游戏时,我们的 GPU/CPU 硬件中的机器代码与我们用来设计游戏的东西之间存在不同的级别。这适用于我们工作的任何计算机硬件,无论是苹果还是其他公司。例如,在 CPU 方面,在最底层是机器代码。再往上一层是芯片组的汇编语言。汇编语言根据 CPU 芯片组的不同而有所不同,允许程序员尽可能详细,比如确定交换数据进出的处理器中的单个寄存器。在 C/C++中,一个简单的 for 循环可能需要几行代码在汇编语言中实现。在代码的较低级别工作的好处是我们可以让我们的游戏运行得更快。然而,大多数中高级别的语言/API 都是为了足够好地工作而设计的,因此这不再是必需的。
注意
即使在游戏开发的早期阶段,游戏开发者就已经开始使用汇编语言编写代码。在 20 世纪 90 年代末,游戏开发者 Chris Sawyer 几乎完全使用 x86 汇编语言创建了其游戏Rollercoster Tycoon™!对于任何喜欢摆弄计算机硬件内部结构的热情开发者来说,汇编语言可能是一个巨大的挑战。
在链向上移动,我们会遇到 C/C++代码的位置,再上面则是 Swift 和 Objective-C 代码的位置。像 Ruby 和 JavaScript 这样的语言,一些开发者可以在 Xcode 中使用,它们又是一个级别向上。
那是关于 CPU 的,现在转到 GPU。图形处理单元(GPU)是协同 CPU 工作以计算我们在屏幕上看到的视觉效果的协处理器。以下图表显示了 GPU、与 GPU 一起工作的 API 以及基于所选框架/API 可以制作的可能的 iOS 游戏。

就像 CPU 一样,最低级别是处理器的机器码。为了尽可能接近 GPU 的机器码,许多开发者会使用 Silicon Graphics 的OpenGL API。对于移动设备,如 iPhone 和 iPad,将是 OpenGL 子集,OpenGL ES。苹果提供了一个名为GLKit的辅助框架/库来帮助 OpenGL ES。GLKit 有助于简化一些着色器逻辑,并减少在此级别与 GPU 一起工作时的人工操作。对于许多游戏开发者来说,这实际上是最初在 iOS 设备家族上制作 3D 游戏的唯一选择;尽管 iOS 的 Core Graphics、Core Animation 和 UIKit 框架的使用对于简单的游戏来说完全是可以接受的。
在 iOS 设备家族的生命周期中不久,第三方框架开始发挥作用,这些框架旨在游戏开发。以 OpenGL ES 为基础,直接位于其上一级的是Cocos2D 框架。实际上,这个框架被用于 Rovio 的 Angry Birds™游戏系列在 2009 年的原始版本中。最终,苹果公司意识到游戏对于平台成功的重要性,并创建了他们自己的以游戏为中心的框架,即 SpriteKit 和 SceneKit 框架。它们也像 Cocos2D/3D 一样,直接位于 OpenGL ES 之上。当我们在我们 Xcode 项目中创建 SKSprite 节点或 SCNNodes 时,直到 Metal 的引入,OpenGL 操作在幕后用于在更新/渲染周期中绘制这些对象。截至 iOS 9,SpriteKit 和 SceneKit 使用 Metal 的渲染管线将图形处理到屏幕上。如果设备较旧,它们会回退到 OpenGL ES 作为底层图形 API。
图形管线概览
这个主题可以是一本完整的书,但让我们看看图形管线,至少在较高层次上了解 GPU 在单个渲染帧期间做了什么。我们可以想象我们的游戏图形数据被分为两大类:
-
顶点数据:这是这些数据可以在屏幕上渲染的位置信息。向量/顶点数据可以表示为点、线或三角形。记住关于视频游戏图形的古老说法,“一切皆三角形。”游戏中所有的多边形都是通过它们的点/向量位置集合而成的三角形。GPU 的顶点处理单元(VPU)处理这些数据。
-
渲染/像素数据:由 GPU 的光栅化器控制,这是告诉 GPU 如何根据顶点数据定位的对象在屏幕上着色/阴影的数据。例如,这里处理了颜色通道,如 RGB 和 alpha。简而言之,这是像素数据,这是我们实际上在屏幕上看到的内容。
下面是一个显示图形管线概览的图表:

图形管道是将我们的数据渲染到屏幕上的步骤序列。前面的图是这个过程的一个简化示例。以下是可能组成管道的主要部分:
-
缓冲区对象:在 OpenGL 中被称为 顶点缓冲区对象,在 Metal API 中是
MTLBuffer类。这些是我们代码中创建的对象,从 CPU 发送到 GPU 进行 原始处理。这些对象包含数据,例如位置、法向量、alpha 值、颜色等。 -
原始处理:这些是在 GPU 中进行的步骤,它们将我们的缓冲区对象分解成各种顶点和渲染数据,然后将这些信息绘制到帧缓冲区中,这是我们看到的设备上的屏幕输出。
在我们介绍 Metal 中原始处理步骤之前,我们首先应该了解着色器的历史和基础知识。
着色器是什么?
GPU 的首次使用并非因为其他原因,而是因为视频游戏行业。20 世纪 70 年代的街机柜中装有与主 CPU 分离的 GPU 芯片,以处理游戏相对于当时其他计算应用的专业视觉需求。最终,在 1990 年代中期,在游戏中绘制 3D 图形的需求导致了我们现在所拥有的现代 GPU 架构。着色器实际上是在 1988 年由皮克斯公司首次引入的,当时公司由苹果公司的联合创始人史蒂夫·乔布斯领导。着色器是我们可以直接写入 GPU 以处理顶点和像素数据的小程序。最初,OpenGL ES 1.0 等 API 并未使用着色器处理,而是被称为固定功能 API。在固定功能 API 中,程序员只需将简单的渲染命令引用到 GPU 上。随着 GPU 的发展,从 CPU 中接管了更多的工作,着色器的使用量也随之增加。尽管着色器是比固定功能方法更高级的遍历图形管道的方式,但它允许对 GPU 显示到屏幕上的内容进行更深入的定制。游戏开发者和 3D 艺术家继续使用它们推动游戏中的视觉效果。
从 OpenGL 2.0 开始,着色器是用 API 的类似 C 语言 GLSL 构建的。在 Apple Metal API 中,我们使用 Metal 着色语言来构建着色器,这是一种 C++11 的子集,文件类型为 .metal,并且可以使用我们的视图控制器在 Objective-C 或 Swift 中运行管道。
着色器的类型
着色器有多种类型,随着 3D 游戏和艺术动画的不断发展,这些类型也在不断增加。最常用的有顶点着色器和片段着色器。顶点着色器用于将 3D 坐标转换为屏幕显示的 2D 坐标,简而言之,就是我们的图形的位置数据。片段着色器,也称为像素着色器,用于转换屏幕上像素的颜色和其他视觉属性。片段着色器的其他属性还可以包括凹凸贴图、阴影和特定的高光。我们强调“属性”这个词,因为通常这是我们的着色器程序属性或输入的名称。
下面是一个用 Metal 着色语言编写的简单顶点和片段着色器的代码示例。
//Shaders.metal
//(1)
#include <metal_stdlib>
using namespace metal;
//(2)
vertex float4 basic_vertex(
//(3)
const device packed_float3* vertex_array [[ buffer(0) ]],
//(4)
unsigned int vertexID [[ vertex_id ]]) {
//(5)
return float4(vertex_array[vertexID], 1.0);
}
//(6)
fragment half4 basic_fragment() {
return half4(1.0);
这里的代码与我们在书中看到的不同。让我们逐行过一遍。
-
Metal 着色语言是一种类似于 C++11 的语言,所以我们看到 Metal 标准库通过在着色器文件中使用
#include <metal_stdlib>行导入,以及using namespace metal;。 -
下一行是使用
vertex关键字创建我们的顶点着色器。这个着色器有四个浮点数。为什么是四个浮点数,而 3D 空间只处理x、y和z坐标?为了总结,3D 矩阵数学涉及一个第四个组件w,以准确处理 3D 空间的数学计算。简而言之,如果w= 0,则x、y和z坐标是向量;如果w = 1,则这些坐标是点。这个着色器的作用将是将简单的点绘制到屏幕上,所以w将是 1.0。 -
在这里,我们创建了一个指向
float3类型数组(用于我们的x、y和z坐标)的指针,并将其设置为第一个缓冲区,使用[[ buffer(0) ]]声明。[[ ]]语法用于声明着色器输入/属性。 -
无符号整数
vertexID是我们为这个特定顶点数组的vertex_id属性命名的名称。 -
这里的
float4类型是返回的,或者在这种情况下,这个顶点数组的最终位置。我们看到它返回输出中的两个部分:第一部分是引用这个顶点数组,通过vertex_id属性和w值为1.0来识别,表示这些是空间中的点。 -
这一行是我们创建片段着色器的地方,使用
fragment关键字。这个着色器的数据类型是half4,它是一个[4,4]的 16 位浮点数数组。在这种情况下,最终目的是创建 16 位彩色像素。这个[4,4]组件向量类型将 16 位保存到 R、G、B 和 alpha 通道。这个着色器将简单地显示纯白色像素着色,没有透明度,所以我们简单地写return half4(1.0);。这会将所有位设置为 1,相当于rgba(1,1,1,1)。
当我们创建一个缓冲区对象时,它可以是屏幕上浮点数的结构体,我们将这些数据通过这些着色器传递,然后在屏幕上会出现一个白色三角形或一组三角形形状。
回顾一下图形管线图,我们看到在顶点着色器计算之后,GPU 执行了所谓的原语装配。这本质上是将顶点着色器中定义的点向量映射到屏幕空间中的坐标。简而言之,光栅化步骤确定从顶点数据中我们可以和不能使用片段着色器信息将像素数据着色到屏幕上的位置和方式。在接收了片段着色器信息后,GPU 然后使用这些信息进行像素数据的混合。最后,这些输出被发送到或提交到帧缓冲区,玩家在那里看到这些输出。所有这些都在渲染周期中的单个绘制调用中发生。让你的游戏中的所有灯光、像素、效果、物理和其他图形在 0.016666 秒内通过这个循环,这就是游戏的名字。
我们将在稍后介绍更多的 Metal 代码,但你现在需要理解的是,着色器就像是我们 Swift/Object-C 代码中发送给它们的输入数据的小型指令工厂。多年来出现的其他着色器类型包括几何着色器和细分着色器。
注意
在这个单独的.metal文件中,既有顶点着色器也有片段着色器,但通常着色器是单独编写的。Xcode 和 Metal 会将项目中的所有.metal文件合并,所以着色器是否在一个文件中并不重要。OpenGL 的 GLSL 大部分情况下强制着色器类型的分离。
多年来,OpenGL 为许多不同的 GPU 提供了良好的工作效果,但正如我们所见,Apple Metal 允许我们以比 OpenGL ES 快 10 倍的速度执行绘制调用。
为什么 Metal 比 OpenGL ES 快?
在 2013 年底,Apple 宣布了iPhone 5s。5s 内置了A7 处理器,这是 iOS 设备家族的第一个 64 位 GPU。与之前的设备相比,它提供了相当不错的图形提升,并反映了移动设备中的 GPU 如何快速赶上几年前发布的游戏机。尽管 OpenGL 是底层图形 API 的基石,但它并没有充分利用 A7 芯片。
在下一个图中可以看到,CPU 和 GPU 之间的交互并不总是以我们希望的方式为我们的游戏执行。

不论是纹理、着色器还是渲染目标,绘制调用都使用它们自己的状态向量。CPU 通过低级 API 使用大量时间来验证绘制调用的状态。这个过程对 CPU 来说非常昂贵。发生的情况是,在许多周期中,GPU 处于空闲状态,等待 CPU 完成其之前的指令。以下是 API 中占用所有这些时间的操作:
-
状态验证:确认 API 使用是有效的。这会将 API 状态编码到硬件状态。
-
着色器编译:运行时生成着色器机器代码。这涉及到状态和着色器之间的交互。
-
向 GPU 发送工作:管理资源驻留批处理命令。
苹果在他们的 Metal API 中所做的,是以更智能的方式完成这些步骤。着色器编译是在应用程序的加载时间完成的。不需要在每个周期重新加载着色器;这仅仅是旧硬件限制的遗迹。这就是为什么在我们的上一个代码示例中,我们可以在一个 Metal 文件中构建多个着色器,而在 OpenGL ES 中这是被禁止的。尽管状态验证很重要,但不需要在每个周期进行检查。状态验证的检查可以设置为仅在加载新内容时发生。
备注
即使 Metal 有其优势,这也是为什么建议将 2D 动画存储在 SpriteSheets 中的原因。我们之前在讨论 SpritKit 时提到了 SpriteSheets。它们是一组适合在一个纹理上的精灵。图形管线随后只需处理该内容的单个版本。在 SpriteKit 的内部机制下,与每个角色动画都放置在其单独的纹理上相比,GPU 内部不需要进行那么多的状态向量调用。
CPU 的最后一个处理过程是将信息发送到 GPU 进行处理。这将在每次绘制调用期间完成,无论是在 Metal 还是 Open GL ES 中,这个过程都将是最频繁发生的。以下是 Metal API 中进行的这种内部、低级别重构的结果:

正如我们在 WWDC14 的图中看到的,在渲染周期中可以添加多达 10 个额外的绘制调用!我们可以利用节省下来的时间用于其他过程,而不是额外的绘制调用,例如在我们的游戏中增加更多的物理或人工智能。
备注
展示的周期图来自 WWDC2014 上原始 Metal API 的公告,并使用了 30 fps 的帧率。如果开发 VR 游戏需要 60 fps 或更高的帧率,这些数字将减半。无论如何,这对于移动设备 GPU 来说都非常令人印象深刻。在网上搜索使用 Metal 开发的游戏,你会感到惊讶。由于在每个渲染周期中都有这么多的空间来添加更多内容,所以在 60 fps 下拥有令人印象深刻的游戏是没有理由的。此外,截至 iOS 9,SpriteKit 和 SceneKit 框架默认由 Metal 支持。即使 Metal API 难以理解,我们仍然可以利用我们从这些框架中学到的渲染节省优势。
基本的 Metal 对象/代码结构
为了结束我们对 Apple Metal 的讨论,让我们看一下 API 的对象和代码结构概述。我们已经在 Metal 着色语言中简要地看到了一些着色器代码,所以让我们看看我们如何在项目中使用这个 API。
| 对象 | 目的 |
|---|---|
| 设备 | 对 GPU 的引用 |
| 命令队列 | 命令缓冲区的串行序列 |
| 命令缓冲区 | 包含 GPU 硬件命令 |
| 命令编码器 | 将 API 命令转换为 GPU 硬件命令 |
| 状态 | 帧缓冲区配置、深度、采样器、混合等 |
| 代码 | 着色器(顶点、片段、几何和细分) |
| 资源 | 纹理和数据缓冲对象(顶点、常量等) |
前面的表格表示我们在直接在 Metal API 中编写游戏时将与之工作的各种对象类型。它们是设备、状态、命令缓冲区、我们的着色器、纹理以及更多。
我们可以使用以下方式将 Metal API 导入到 ViewController.swift 类中:
import Metal
import QuartzCore
这导入了 Metal API。还需要 QuartzCore API,因为我们将要与之工作的 CAMetalLayer 对象是该库的一个成员。同时,确保将您的目标设备设置为实际 iOS 设备,新或更新的 iPhone 5S,Xcode 模拟器不支持 Metal。否则,Xcode 将给出“无法构建”Objective-C 模型 Metal 错误。截至本书编写时的 Xcode 7 Beta 版本,这是真的。随着时间的推移,很可能在 El Capitan OS 的官方公共版本发布后,这将不再需要。目前,为了测试您自己的自定义 Metal 代码,您必须在实际设备上进行测试。这样做将涉及支付自己的 Apple 开发账户费用。更多内容将在下一章中介绍。
这是我们要按照的顺序与之前显示的表格中的对象一起工作,以及一些用 Swift 实现的这些步骤的代码示例:
-
使用
MTLDevice类创建对设备的引用:let device: MTLDevice = MTLCreateSystemDefaultDevice() -
为这些对象在屏幕上创建一个
CAMetalLayer对象:let metalLayer = CAMetalLayer() -
创建顶点数据/缓冲对象(VBOs),如下发送数据到着色器:
/*Simple Vertex Data object, an array of floats that draws a simple triangle to the screen */ let vertexData:[Float] = [ 0.0, 1.0, 0.0, -1.0, -1.0, 0.0, 1.0, -1.0, 0.0] -
为这些 VBO 创建将与之工作的着色器。
我们在之前的着色器代码示例中做了这件事。顶点数据与之前制作的着色器结合,在屏幕上创建了一个简单的白色三角形。
-
如下设置渲染管线:
//Library objects that reference our shaders we created let library = device.newDefaultLibrary()! //constant where we pass the vertex shader function let vertexFunction = library.newFunctionWithName("basic_vertex") //now the fragment shader let fragmentFunction = library.newFunctionWithName("basic_fragment") /*Describes the Render Pipeline and sets the vertex and fragment shaders of the Render Pipeine*/ let pipelineStateDescriptor = MTLRenderPipelineDescriptor() //initiates the descriptor's vertex and fragment shader function properties with the constants we created prior pipelineStateDescriptor.vertexFunction = vertexFunction pipelineStateDescriptor.fragmentFunction = fragmentFunction //Makes the pixel format an 8bit color format pipelineStateDescriptor.colorAttachments.objectAtIndexedSubscript(0). pixelFormat = .BGRA8Unorm /*Checks if we described the Render Pipeline correctly, otherwise, throws an error. */ var pipelineError : NSError? pipelineState = device.newRenderPipelineStateWithDescriptor(pipelineStateDescriptor, error: &pipelineError) if pipelineState == nil { println("Pipeline state not created, error \(pipelineError)") -
如下创建一个命令队列:
var commandQueue = device.newCommandQueue()
要在我们的游戏中实际渲染这些对象,我们必须在视图控制器中执行以下过程:
-
创建一个显示链接。这是一个每次屏幕刷新时都会刷新的计时器。它是
CADisplayLink类的一个成员,并且每次屏幕刷新时,我们都会调用gameRenderLoop函数。var timer = CADisplayLink(target: self, selector: Selector("gameRenderLoop")) timer.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)gameRenderLoop函数可能看起来像以下这样。它调用即将填充的函数render():func gameRenderloop() { autoreleasepool { self.render() } -
创建一个渲染传递描述符。对于这个例子,要在我们的白色三角形周围创建一个主要为红色的纹理,如下所示:
let passDescriptor = MTLRenderPassDescriptor() passDescriptor.colorAttachments[0].texture = drawable.texture passDescriptor.colorAttachments[0].loadAction = .Clear passDescriptor.colorAttachments[0].storeAction = .Store passDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.8, 0.0, 0.0, 1.0) -
在我们的
render()函数中创建一个命令缓冲区:let commandBuffer = commandQueue.commandBuffer() -
创建一个渲染命令编码器。换句话说,是
commandQueue的一组命令。在稍后的代码示例中,这告诉 GPU 使用我们之前创建的 VBO 绘制三角形。这被放置(在这个例子中)在render()函数中。let renderEncoderOpt = commandBuffer.renderCommandEncoderWithDescriptor(renderPassDescriptor) if let renderEncoder = renderEncoderOpt { renderEncoder.setRenderPipelineState(pipelineState) renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, atIndex: 0) renderEncoder.drawPrimitives(.Triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1) renderEncoder.endEncoding() } -
提交你的命令缓冲区。这本质上告诉 GPU 根据已经打包到
commandBuffer对象中的命令进行绘制调用。在render()函数中,将此代码放在之前的if语句之后。commandBuffer.presentDrawable(drawable) commandBuffer.commit()
这就是它的简短版本。这是在屏幕上绘制一个简单的三角形并手动在 GPU 上创建渲染循环的一般过程。
你是否更愿意选择 SpriteKit 和 SceneKit 来为你完成所有这些手动工作?这是可以理解的。但记住,就像在困难模式下玩游戏一样,选择更难的路线也会带来回报。是的,从 iOS 9 开始,SpriteKit 和 SceneKit 框架默认使用 Metal。游戏引擎,如 Unity 和 Unreal Engine,在将项目转换为平台时也会实现 Metal。然而,了解如何在 Metal 或 OpenGL 这样的低级图形 API 中构建你的游戏,将赋予开发者创建性能最优化游戏的能力。下次你在网上搜索时,一定要查看一些使用 Metal 创建的游戏。它们真的可以为你的玩家提供极佳的体验。同时,这也将挑战你作为游戏开发者的技能,因为成为一名游戏开发者是艺术家、工程师和计算机科学家的结合。直接在 GPU 的基本功能上工作将挑战这一切。
要更深入地探索 Metal 的低级图形开发这个兔子洞,请查看以下链接:
第一个链接是苹果官方开发者页面上的 Metal 页面。下一个链接是苹果在 Metal API 中使用的数据类型列表。最后两个链接是两个使用 Swift 创建简单 Metal 场景的教程。我们使用的部分代码也可以在这些教程以及完整的 Xcode 项目中找到。这两个链接中的第一个链接是 iOS 教程网站www.raywenderlich.com。最后一个链接是一个页面,上面有前苹果工程师沃伦·摩尔关于 Swift 和 Metal 3D 图形的精彩视频演示和完整说明。
摘要
恭喜你走到这一步。如果这本书是一部游戏,我们可能仅凭这一章就能获得成就。正如我们所见,使用像 Metal 这样的低级 API 可能会有些令人畏惧。我们首先回顾了当开发者和工程师提到低级和高级框架以及代码时,这意味着什么。在 CPU 方面,我们看到最低级的是机器代码,Swift 和 Objective-C 位于中间,而 C/C++和汇编代码位于其上。接下来,我们讨论了 GPU 方面,以及我们在前几章中提到的视觉图形 API 在层次结构中的位置。然后,我们了解了低级图形 API(如 OpenGL ES)的历史,图形管道在底层通常是如何工作的,以及如何制作基本的着色器。最后,我们回顾了为什么 Metal 在渲染周期中比 OpenGL 更快,Metal 背后的总体结构,以及一些用于手动设置渲染循环的代码/对象。这一章只是在这个主题上略作探讨,所以如果你愿意接受挑战,强烈建议继续阅读有关 Metal 如何让你的游戏与众不同的文档。
到目前为止,你应该已经拥有了在 iOS 平台上制作游戏所需的一切。iOS 游戏开发的最后一堂必修课是学习如何测试、发布和更新你在 App Store 上发布的应用。在下一章中,我们将学习如何将游戏上传到 Apple App Store。
第七章。发布我们的 iOS 游戏
制作一款优秀的游戏是件辛苦的工作。作为游戏开发者,我们的目标是开发一款能够被成千上万,甚至数百万的人玩的应用。我们希望他们能玩到我们在那些漫长的星期和几个月里制作的东西。在应用发布之前,我们也可能希望让其他人测试游戏,以排除可能遗漏的任何错误。在 iOS 平台上发布可以让我们做到这两点。
我们可以通过TestFlight服务允许他人在我们发布前尝试我们的游戏,当然,我们也可以随后将我们的游戏提交到苹果 App Store 进行发布。发布后,我们可以提交更新。这些更新和未来的版本可能只是为了修复在初始发布中可能遗漏的一些小错误,添加新功能,例如关卡、成就和其他苹果服务,或者我们可能需要在以后更新我们的应用以符合 iOS 平台最终发生的更新。
在本章中,我们将涵盖几个主要主题:
-
在 iTunes Connect 中设置我们的应用以进行测试或发布
-
将我们的应用到应用商店提交以供游玩的步骤
-
测试飞行服务在预发布阶段测试的总结
-
如何使用 iTunes Connect 创建我们应用的更新
我们不会告诉你如何推广你的游戏,因为这完全是一本关于这个主题的书/话题,它取决于你的预算和偏好。然而,我们可以说的是,如果你或你的测试者发现玩游戏很有趣,那么其他人也可能会有同样的感觉。提交到应用商店并不保证立即成功。
即使那些在日益增长的独立游戏奖励展示或游戏马拉松中获得高度赞誉的未发布游戏,在发布后也可能没有在销售和下载量上反映出这种赞誉。我们必须记住,游戏开发场景,部分得益于游戏在流行文化中的地位,是一片由成千上万的开发者,无论是大是小,都在努力制作下一个伟大游戏的海洋。
尽管如此,不要因此气馁。如果你的游戏表现良好,这完全有可能通过苹果 App Store 实现,那么这可能会是一次改变一生的体验。无论结果如何,让制作你自己的游戏和学习这个开发平台的经验让你谦卑,并让你想要在下一个项目中成为一名更好的开发者。最终,这些努力会得到回报。
应用提交的持续变化过程
在我们继续解释发布你的游戏所需的步骤之前,我们想指出关于这个主题的一个小事实。这个事实是,提交我们的 iOS 应用进行测试或发布的具体步骤经常发生变化。每隔几个月,这个过程可能会从我们在这里描述的方式改变。
几年来,自从 iOS 开发开始,苹果一直在使这个过程变得更简单、更流畅。Xcode 为我们做的签名/配置工作比过去要多得多,我们应用在 App Store 中永远无法出现的问题几乎不再是问题。
例如,当 Swift 游戏 PikiPop 在 2014 年 11 月提交时,从提交审查到出现在 App Store 供公众使用,仅用了五个工作日。这个审查时间因人而异,但只要我们的应用中没有严重的错误或违反政策,我们可以期待我们的作品能够供数百万玩家潜在地游玩。为了确保您游戏的发布顺利进行,最好审查这里找到的 App 审查指南:developer.apple.com/app-store/review/guidelines/.
注意
我们是在 2015 年夏末编写这本书的,所以如果您在很久以后阅读这本书,觉得其中一些过程可能已经过时,请确保查看苹果自己 App 提交文档页面上的最新更新:
在提交您的应用之前
在您选择将应用提交到 App Store 之前,我们必须提醒您一个潜在的开发陷阱。截至本书出版时,如果您在 Xcode 的 beta 版本中构建应用,该应用将被拒绝审查。
在本书的编写过程中,我们一直在构建我们的应用,并回顾了目前 Xcode 7 的 beta 版本中已有的功能。这是因为 iOS 9 / Xcode 7 中有一系列新功能,在 iOS 8 / Xcode 6 中并不存在,即 GameplayKit 框架和 SceneKit 的视觉编辑工具,这使得 Xcode 开发像多平台游戏引擎一样直观。
注意
到您阅读这本书的时候,Xcode 7 应该已经不再处于 beta 阶段。因此,您应该能够发布 iOS 9(或更高版本)的游戏,而不用担心这些功能是否仅为 beta 版本。
当您为发布到 App Store 构建游戏时,请确保首先在当前的非 beta 版本中构建它们。使用 Xcode 的 beta 版本来测试最新未发布的特性以及预发布阶段即将到来的 iOS 构建。
为 iTunes Connect 准备我们的应用
所以你已经编码、模拟,并且希望玩玩你投入了大量精力的那个 iOS 游戏。下一步是将你的应用带入测试/预发布阶段。这个阶段的目标是将你的游戏带给更小的游戏者/测试者群体,以模拟成千上万甚至数百万可能玩你的游戏的人的体验。如果你还没有这样做,第一步是注册 Apple 开发者计划:developer.apple.com/programs/。
这将涉及一些费用,并且这个费用将基于你的开发目标。对于个人、个体工商户账户,成为 iOS 开发者每年需要支付 99 美元。如果你是作为开发者团队的一部分工作,那么 299.99 美元的企业计划可能是一个更好的选择。
注意
在你的开发者页面,你还需要确保你的配置文件设置正确。这个步骤过去是 iOS 开发者完成的最困难的事情之一,但 Xcode 随着每个新版本的更新使这个过程更加自动化。如果你已经在 Xcode 中使用实际设备测试你的应用,显然你已经完成了这一步。如果没有,这里有一些关于设置你的配置文件(s)的更多信息:developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ProvisioningDevelopment.html。
在发布应用的过程中,将成为你最好朋友的门户网站是 iTunes Connect,在这里:
iTunes Connect 是你可以查看你提交的应用、跟踪各种应用分析、分配 TestFlight 测试者以及查看你的应用收入的地方。我们无法详细介绍 iTunes Connect 中所有可用的功能;主要我们将查看发布、发布和更新你的游戏的步骤。随着每个新的 iOS 更新,功能不断增加,你可以自由探索 iTunes Connect 为你应用提供的所有功能和设置。
在测试/预发布阶段提交你的应用
让我们直接进入提交你的应用的步骤。首先,我们将讨论你游戏的测试/预发布阶段。
这里是游戏测试/预发布阶段所需步骤的摘要:
-
创建一个 iTunes Connect 应用记录。
-
更新构建字符串。
-
归档并验证你的应用。
-
将你的应用上传到 iTunes Connect。
-
使用 TestFlight 服务测试你的游戏。
-
分析崩溃报告并征求测试者的反馈。
注意
步骤 1-4 在你的游戏的测试和发布阶段都是相同的。
创建 iTunes Connect 应用记录
这些是将你的 App 添加到 iTunes Connect 应用记录所需的步骤:
-
登录您的账户至
itunesconnect.apple.com/并进入我的应用部分。 -
现在点击页面左上角的+图标,从下拉菜单中选择New iOS App。
![创建 iTunes Connect 应用记录]()
-
填写适当的字段,如果您已经知道这些信息,请点击创建。对于不确定在这里填写什么的人,我们将在以下屏幕截图中回顾每个这些字段:
![创建 iTunes Connect 应用记录]()
注意SKU、版本和Bundle ID字段。SKU 必须是唯一的,且尚未使用,因为苹果将使用此信息在商店中识别您的应用。版本和Bundle ID字段必须与您在游戏 Xcode 项目中设置的构建设置相匹配。Bundle ID字段是一个下拉菜单,最初可能只显示通配符 App/Bundle ID。
通配符 ID 是两种类型的 Bundle ID 之一,另一种是显式 App ID。以下是一个链接,指向苹果的文档/常见问题解答,说明哪种 ID 最适合您的游戏:developer.apple.com/library/ios/qa/qa1713/_index.html。简而言之,如果您打算使用苹果服务,例如通知和游戏中心成就,您将需要一个显式 App ID;如果不是,那么通配符 ID 是最佳选择。
注意
如果您希望使用显式 Bundle ID,您必须在苹果开发者门户中注册您的应用 Bundle ID。在开发者门户上注册的 ID 将填充该下拉菜单。这是开发者网站上该页面的链接:developer.apple.com/account/ios/identifiers/bundle/bundleCreate.action。
Bundle ID 后缀字段位于您项目的info.plist文件中。这是一个由您的 Xcode 项目创建的唯一字符串,也称为 Bundle Seed ID。当我们在“更新构建字符串”步骤中讲解时,我们将向您展示如何找到这个以及其他基于 bundle/build 的信息。
名称字段是您的游戏在 App Store 中的名称。这是人们搜索并希望访问您游戏着陆页时将看到的内容。主要语言下拉菜单是如果应用商店无法为该地区本地化您的游戏信息时,您的游戏默认语言将是什么。
更新构建字符串
构建字符串代表您游戏包的一个迭代版本。它是一个由两个点分隔的正整数列表,例如 1.2.3。基本上,构建字符串是为您的游戏添加的另一层版本控制。在制作 iOS 应用程序时,正如我们的案例一样,更改此构建信息将在上传步骤中自动被 iTunes Connect 所见。如果我们不更新此字段,即使我们更改了游戏代码,iTunes Connect 仍然会认为您正在尝试上传相同的构建版本,并将拒绝您的上传。
在您的 Xcode 项目中,您可以在以下位置找到此信息:

当我们在导航面板中点击项目的主要文件时,在检查器窗口的 常规 选项卡中可以找到包标识符、构建字符串、版本号和其他应用程序标识符/全局设置。我们还可以在 info.plist 文件中找到这些信息。请确保这些字段与您的 iTunes Connect 记录匹配。
现在,让我们继续在 Xcode 中上传我们的游戏到 iTunes Connect。
存档并验证您的应用程序
应用程序发布流程的下一步是存档您游戏的项目包。为此,请转到顶部的下拉菜单,然后导航到 项目 | 存档。如果您的测试设备是模拟器而不是 iOS 设备,存档选择可能不可用。
构建完成后,您的存档应用程序将与其他存档一起显示在存档组织器中,这些存档是从该应用程序和其他应用程序创建的。当您构建存档时,此窗口将打开,但您可以通过在顶部菜单中选择 窗口 | 组织器 来随时访问它。
窗口如下截图所示:

接下来,我们验证您的游戏以确保它符合提交的最小要求。为此,请按照以下步骤操作:
-
点击前一个存档组织器窗口右侧的 验证 按钮。
-
将会弹出一个窗口,您可以在其中选择为该应用程序进行配置的 开发团队。(这是假设您的配置文件已正确设置。)点击 选择 以进入下一步。
-
这将打开一个弹出窗口,显示在执行实际验证之前应用程序的摘要。您还可以在此处看到有关您的应用程序的信息,例如应用程序的包标识符以及之前提到的包种子标识符。
![存档并验证应用程序]()
-
点击 验证,如果您的应用程序项目中的信息与我们在 iTunes Connect 上设置的信息正确匹配,那么您的应用程序应该已通过验证,并准备好提交到 iTunes Connect,最终提交到 App Store 本身。

如果您的应用程序未通过验证,请确保您在 iTunes Connect 中的所有信息都匹配,最重要的是包标识符。
小贴士
应用验证步骤可能只需通过点击上传到 App Store…按钮就能跳过,但这是一个在早期测试游戏是否一切正常的好方法。
更多关于应用验证的信息可以在这里找到:
将您的应用上传到 iTunes Connect
此步骤现在应该相当简单。点击蓝色上传到 App Store…按钮,你应该会看到与应用验证阶段相同的提示。你将被要求选择开发团队;展示您的应用详情,然后你可以通过点击提交来选择上传您的应用。如果你的游戏之前已经通过验证,那么它应该可以顺利上传到 iTunes Connect。现在你的游戏应该离在 App Store 上可供测试或购买/下载又近了一步。
使用 TestFlight 服务测试您的游戏
每个应用在发布前都应该至少进行某种形式的测试,视频游戏通常是比其他应用更需要这种测试的类型,因为游戏通常有更多的变量和崩溃的机会,而普通的移动程序则没有。此外,像 GameCenter 和内购这样的 Apple 服务,如果不进入这个阶段是无法正确测试的。
在过去,发布 iOS 应用之前测试的唯一方法是通过 ad hoc 分发方法,验证具有 UDID 的个别设备,并给测试者提供带有清单文件的下载,该文件允许应用在他们的设备上实际运行。这就是苹果与 Android 等其他平台有很大不同的地方。苹果在保持开发者喜欢称之为封闭花园的应用分发方面非常小心。
在过去,这有点头疼,与 Android 相比,导致应用错误直到发布后才被发现。为了保持苹果应用分发的完整性,并为开发者提供一种更好的方式来轻松预测试他们的应用,并且有比原始的 100 台设备限制更多的人,TestFlight 服务被创建。TestFlight 图标如下所示:

TestFlight 是一个任何人都可以从 Apple App Store 下载到他们的 iOS 设备的 App。对于你,作为开发者,它可以是早期分发你游戏的一个很好的工具。TestFlight 测试者被分为两组:内部测试者和外部测试者。内部测试者由你的团队成员组成,你可以有最多 25 名内部测试者。
在 iTunes Connect 中,您可以在用户和角色主部分为您的团队设置角色。这些角色包括管理员、技术、营销和其他。属于管理员和技术类别的成员是您可以指定为内部测试人员的。将这些用户设置为内部测试人员就像打开他们名字旁边的内部测试人员开关一样简单。

要让这些用户在 TestFlight 中测试您的游戏,请在 iTunes Connect 的我的应用部分中找到您的游戏。如果您的游戏构建已成功添加到 iTunes Connect,如前一部分中提供的步骤,那么您应该会在预发布标签中看到它列出。

注意
当您将应用程序上传到 iTunes Connect 时,它们可以分成版本,然后这些版本再细分为它们自己的构建。例如,版本 1.0(1)是您的游戏的版本 1.0,构建 1,而 1.0(1.2)将是您的游戏的版本 1.0 / 构建版本 1.2。在项目中更改构建字符串然后上传新构建是您如何在 iTunes Connect 的此页面上划分应用程序的方法。我们将在创建您游戏的新更新时进一步讨论这一点,但这是对预发布构建进行版本化的过程。
下一步是点击构建或版本,这将打开构建的元数据页面。填写这些信息以更好地帮助测试人员了解应联系谁以及要测试什么。这些信息是测试人员在下载您游戏的测试版时将看到的信息。

要允许此构建/版本进行 TestFlight 测试,只需在预发布标签中版本列表的右上角切换TestFlight Beta Testing开关。

现在要让您的测试人员在他们的 TestFlight 应用程序中测试此构建,只需在游戏的预发布页面上的构建标签旁边的内部测试人员标签上点击,点击他们名字旁边的勾选框,然后点击邀请。
他们应该收到一封电子邮件以接受邀请,一旦他们在 TestFlight 中安装了它,您将看到他们正在测试哪个构建。
外部测试人员邀请
要使用 TestFlight 为您的游戏获取外部测试人员也是一个相当简单的流程,但有一个注意事项;您的应用程序需要提交进行 beta 审查。这样做很简单,您只需点击应用程序构建右侧的提交 beta 应用审查链接;再次在预发布部分的构建标签中。
与实际 App Store 提交一样,可能需要等待才能进行下一步。等待的时间不如完整应用提交长,这是一个非常好的迹象,表明当你进行公开发布时一切都会顺利。与内部测试者不同,所有元数据都必须完成,但你可以有最多 1000 名测试者!一旦点击提交以供 Beta 应用审查链接,你的应用等待 beta 测试审查时,你就可以开始邀请测试者。
现在转到预发布页面中的外部测试者标签,然后点击+按钮,通过他们的电子邮件地址(可选)以及他们的名字和姓氏添加他们。点击下一步将该人添加到邀请列表中。请注意,你只有 30 天的时间让外部测试者审查你游戏的这个版本。

一旦你的应用通过 beta 审查,外部测试者可以像内部测试者一样测试你的应用。
分析崩溃报告和测试者的反馈
现在你已经有人测试你的游戏了,从他们的电子邮件中记录下游戏中可能存在的问题,然后回到你的游戏项目中进行必要的修复。更新 Xcode 项目中构建字符串的版本号,并重新上传构建,以便让测试者能够跟上每个新的预发布更新。
应用崩溃报告可以在 iTunes Connect 的App Analytics主部分中查看,如 PikiPop 的信息图片所示。然而,这些详细的崩溃分析似乎是发布后而不是预发布期间。

更多关于 TestFlight 以及视频解释可以在苹果官方关于该主题的页面找到,如下所示:
developer.apple.com/testflight/
提交游戏以供审查
这是你游戏开发中努力工作的一个阶段,将其提交到苹果 App Store。好消息是,大部分工作已经完成!要提交游戏以供审查,此时,你所要做的就是前往应用的版本标签,然后点击提交以供审查标签。
你可以在以下冒险应用示例图片中看到这一点:

在实际提交之前,你将需要回答一些问题,例如关于出口合规性、内容权利和广告标识符(IDFA)信息。有关 IDFA 的更多信息可以在此处找到:
现在,您的游戏处于等待审核状态,等待游戏审核的时间开始。再次强调,应用审核的等待时间会有所不同,但通常比过去要短得多。希望一切顺利,您将看到表示游戏提交版本号的绿色标记,并附有准备销售的字样;如下所示:

恭喜!您的游戏现在应该位于 App Store 中,供所有 iOS 设备用户下载和游玩!请确保查看 iTunes Connect 提供的各种分析工具,以保持对您游戏的关注。
更新您的游戏
今天的游戏几乎都不是一次性的交易。它们往往通过更新、附加内容和修复来保持活跃,甚至在发布之后也是如此。您的应用更新将适用于任何未来的 iOS 更新,增加新的游戏内容,或者两者兼而有之。为此,只需重复构建字符串阶段的大部分过程,从创建一个新的版本号开始。这样做将在预发布下的构建标签中创建一个新的部分,就像 PikiPop 自己的页面所示如下:

您可以使用 TestFlight 测试版测试来测试新构建的内部和外部测试者,就像您之前做的那样。为了准备好最新版本以供发布,请点击版本主标签上的新版本按钮,并在弹出窗口中提交最新版本的编号。

版本标签现在将通过易于导航的标签分为当前版本和新版本。就像在原始发布中一样,您可以填写各种元数据以及玩家将获得的新更新的信息。提交版本进行审核,一旦批准,最新的游戏更新将出现在应用商店,玩家将根据其设备的 App Store 更新设置自动下载或收到通知。
大概就是这样!如果 iOS 未来的版本中出现了更多的游戏框架和工具,您可以将它们作为您游戏更新的一部分。为游戏制作出更好的游戏始终是每个游戏开发者的正确选择。
摘要
在本章中,我们看到了将您的游戏从 Xcode 项目最终变成 iOS 设备上可玩的游戏所需的一切。我们学习了在提交到 iTunes Connect 之前设置 Xcode 项目所需的步骤。然后,我们了解了 TestFlight,这是一个在我们发布游戏之前进行测试的出色服务。我们看到这些步骤已经为我们提交游戏进行审核所需的大部分内容做好了准备。最后,我们看到了在 iTunes Connect 中创建应用更新是多么容易。
你现在有一个游戏应用,希望成千上万的玩家都能享受它。无论这个游戏是大是小,为玩家创造乐趣的事实本身就值得骄傲。即使是最简单的游戏也能是一项值得骄傲的成就。在这本书的整个过程中,我们都看到制作 iOS 游戏的过程,尽管比几年前容易,但仍需要一些努力和勤奋才能做好。这总结了我们将讨论的所有技术方面。接下来,我们将简要讨论 iOS 和整体游戏开发的未来。
第八章:iOS 游戏开发的未来
在这本书的整个内容中,我们已经介绍了 iOS 8 的功能和最近关于 iOS 9 的公告。那么,iOS 游戏开发的未来会怎样呢?显然,没有人能预测未来会带来什么,但我们可以根据 iOS/Xcode 平台、编程以及整体游戏开发如何变化等方面的近期发展,猜测一些可能性。
更大的关注点在于函数式编程
到本书出版时,Swift 编程语言只有一年历史,但它代表了编程领域最近的一次范式转变。面向对象编程和设计仍然适用,但转向函数式编程是像 Scala 和 Swift 这样的语言所关注的焦点。简而言之,函数式编程关注的是函数作为对象的纯数学计算,避免了我们过去在 C++/Java 以及苹果自己的 Objective-C 中所看到的州变化和可变数据。与处理子例程不同,一个函数只对其给定的参数进行操作。Swift 通过其闭包做得很好,我们在本书中已经看到几次,并且经常用于压缩 iOS 游戏编程中的大量逻辑。
自从 WWDC14 以来,苹果公司已经告诉开发者,Swift 是 C 和 Objective-C 的继任者。这是一个从头开始完全重建的语言,其重点是速度和效率,这是游戏开发者必须始终利用的,也是为什么 Swift 现在成为 iOS 游戏开发前进方向上的首选语言。Objective-C 并不会消失,Swift 在能够完全接管 Objective-C 为 iOS 开发所做的一切之前,还需要一些成长。Swift 2.0 的更新从 iOS 9 开始,增加了更多的错误捕获和调试功能,例如 throw、catch 和 guard 关键字。幸运的是,我们不必放弃任何过去的 Objective-C 项目,因为通过使用 Objective-C/Swift 桥接文件,在同一个项目中使用 Swift 和 Objective-C 非常简单。
随着我们转向 iOS 的未来版本,期待更多的调试控制和 Swift 将详细逻辑压缩成几行的能力得到改进。Swift 的一些数据排序功能比 Objective-C 快,在游戏开发的世界里,任何帧率的提升都是好事。尽管 CPU/GPU 性能的进步和游戏开发框架(如 SpriteKit 和 SceneKit)的日益便捷,但如果忘记了游戏设计的工程方面,设计方面可能会成为一把双刃剑。最近游戏开发场景中提供的某些工具可以让几乎任何人成为游戏开发者。这是好事。我们希望各行各业的人都能成为游戏开发者,但我们不能让一些最近和未来的工具,使得 iOS 或其他平台上的游戏制作变得懒惰,并忘记这个行业中始终存在并始终存在的编程/工程方面。iOS 和其他游戏引擎的框架和可视化工具应该始终被视为工具,而不是拐杖。过去、现在和未来的最佳游戏将是那些充分利用游戏开发各个方面,尤其是游戏开发中最困难方面的游戏,比如图形管线。这就是为什么苹果通过 Metal API 深入 GPU 细节的原因。
Apple Watch
自从 iOS 9 开始,Apple Watch 的平台已经通过 watchOS 2 进行了升级。目前,Apple Watch 通常不是一个游戏平台。不考虑小屏幕尺寸,为 Apple Watch 制作的 app 在制作类似游戏的图形更新方面并没有太多优势。在未来,这种情况可能会改变。目前,在 watchOS 中制作的 app 就像是主 iOS app 的子 app。最终,我们可以独立制作 watchOS app,而无需将其附加到父 iOS 项目中。然而,一些开发者已经为 Apple Watch 制作了简单的基于文本的游戏。未来,我们可能会为 Apple Watch 制作更多以动作为导向的游戏。
目前,通过一点创意,完全有可能制作一个使用手表作为配件数据(如库存、地图等)的游戏。我们可以用Force Touch来设计游戏或游戏控制,这是 Apple Watch 的一个特性。Force Touch 可以感知按下手势的力度。这并不是游戏设计整体上的新事物,但在 iOS 上,随着下一代的 iPhone 和 iPad 很可能也具备这一功能,这还是第一次。获取玩家的触摸和点击的力度可以允许一些直观的游戏玩法机制,适用于下一代的移动游戏。
关于 watchOS 可能激发的一些针对该设备的游戏开发想法,请查看www.apple.com/watchos-2-preview/的 watchOS 2 预览页面。
基于组件的结构
在前几章中,我们看到了 iOS 已经传授了大量的基于组件的架构范式,以应对来自游戏开发独特的软件/编程需求。它不是构建一个从面向对象设计看到的庞大父子结构,而是构建一个通过宽度增长其结构的结构。例如,GKEntity、GKComponent和GameplayKit的其他方面等类,使我们能够利用这些功能。这种结构在游戏开发中并不算特别新颖。基于组件的架构已经被多平台游戏引擎,如 Unity 和 Unreal Engine 所使用,并且继续是我们这样的游戏开发者制作和重用游戏部分的方式。预计 Xcode 和 iOS 的未来更新将更多地利用这些功能。在不久的将来,我们可能会看到 Xcode 的外观和功能甚至更像这些游戏引擎,但具有专门为 iOS 设备制作的好处,允许更深入、更定制的集成。在 Xcode 和 iOS 框架中直接完成所有这些操作,允许即时访问 Apple 的功能,而无需使用付费资产工具或等待插件更新。下一代 iOS 游戏开发者将能够使用在一种游戏中制作的 AI 动作、角色能力、HUD 动画和其他功能,几乎可以立即为完全不同的游戏重用。基于组件的架构使得开发者能够通过将游戏设计置于典型开发障碍之前,构建一个可重用功能的库。
虚拟现实(VR)的兴起
这一直是苹果相对沉默的话题,而其他平台则相对活跃。即使是苹果 A7/A8 芯片的制造商三星,也通过其 GearVR 设备和与 Oculus 在项目上的合作加入了 VR 开发环境。谷歌创建了简单的 Cardboard 设置,类似于 GearVR,它只是让用户将智能手机放入设备或盒子中,以体验 VR 体验和游戏。最著名的是 Oculus Rift,它将在 2016 年第一季度推出消费版,并可能成为这个新游戏环境中的领跑者。
虚拟现实是一个在过去几十年中多次出现的话题。它来过又去,但到目前为止,它似乎正在获得其在技术领域应得的立足点。例如,Unity 游戏引擎最近才允许原生支持 VR。制作这些游戏的思维过程略有不同,并且尚未完全明确。可能很快苹果会加入这个领域。如果你还没有这样做,学习如何在 VR 空间中制作有趣的游戏可能是一种有远见的行动。
摘要
我们希望这本书中看到的讨论和教程能帮助你第一次学习平台,或者丰富你对 iOS 可能已经了解的知识。我们希望你能利用这些知识为 iOS 设备系列制作一些令人惊叹的游戏,并继续深入了解平台和整体游戏开发。
最后,始终记住,游戏设计和编程是计算机科学、工程、艺术和大量辛勤工作的结合。这是一个复杂的创意领域,它结合了从音乐制作到电影制作、2D 动画、3D 雕塑等其他所有创意职业。与其他创意领域一样,开发者永远不应该感到“完成”,并且应该谦逊地对待自己所知,并承认在这个不断发展的领域中总有更多东西要学习。就像电子游戏一样,接受挑战。当你回顾过去时,意识到你实际上已经从你所知的内容中提升了等级,并且总有更多的能力可以获取。















固定按钮来创建这些w/Any h/Any配置的约束,并跳过手动调整每个基本配置上的图标。



















浙公网安备 33010602011771号