IOS-游戏开发实例-全-
IOS 游戏开发实例(全)
原文:
zh.annas-archive.org/md5/453c85f2ce3736815d49efeab12bbf84译者:飞龙
前言
Sprite Kit 是一套用于开发苹果 iOS 平台 2D 游戏的工具。Sprite Kit 提供了强大的图形和纹理图像动画等功能。它是 iOS 设备上最好的游戏引擎之一。它非常简单且功能强大,由苹果公司提供全面支持,因此比任何现有的第三方游戏引擎都更可靠和方便。
苹果公司为应用开发提供的集成开发环境(IDE)Xcode 也可以用于 Sprite Kit 游戏开发。可以使用 Objective-C 或 Swift 这两种编程语言中的任何一种来开发 Sprite Kit 游戏。Sprite Kit 已经被许多开发者用于 iOS 游戏开发。关于 Sprite Kit 设置和开发的信息量很大。然而,目前还没有一个结构化和简洁的资源来讨论完整开发过程和功能集。本书解释了 Sprite Kit 开发的基础知识,并允许初学者通过使用 Swift 编程语言及其完整开发功能,成为 Sprite Kit 游戏开发的专家。本书是 Sprite Kit 的完整指南,对于那些想要在 iOS 游戏行业扬帆起航的人来说是一个完美的起点。
本书涵盖内容
第一章, Sprite Kit 简介,介绍了 Sprite Kit 游戏引擎及其各种元素和功能。它还帮助设置一个新的 Xcode 项目以开发 Sprite Kit 游戏。
第二章, Sprite Kit 中的场景,解释了 Sprite Kit 中的一个重要主题,即场景。此外,还有关于节点树绘制顺序的简要介绍。
第三章, 精灵及其属性,解释了精灵及其属性。它还将在示例游戏中应用一些属性。
第四章, Sprite Kit 中的节点,详细讨论了节点及其子类。它还解释了示例游戏中各种节点子类的实现。
第五章, Sprite Kit 中的物理,讨论了 Sprite Kit 游戏中的物理模拟。它解释了物理体的类型。本章将物理功能应用于示例游戏。
第六章, 精灵、控件和 SceneKit 的动画,涵盖了在 Sprite Kit 游戏中动画节点和添加控件的功能。这些功能已添加到示例游戏中。它还讨论了 SceneKit。
第七章, 粒子效果和着色器,讨论了粒子效果和着色器,以及它们在示例游戏中的实现。
第八章, 处理多个场景和级别,帮助我们理解游戏中不同级别的需求。本章还解释了如何创建多个场景。
第九章, 性能提升和附加功能,详细讨论了如何提高 Sprite Kit 游戏的表现,以及使用工具进行性能测量。它还解释了评分系统、声音和示例游戏中的玩家跑步动画。
第十章, 重新审视我们的游戏以及在 iOS 9 上的更多内容,讨论了游戏开发中涉及的各个步骤,并向读者介绍了游戏中心,并讨论了将在 iOS 9 中引入的新功能。
您需要为本书准备的内容
读者将需要 Xcode,这是苹果提供的用于开发软件的 IDE,以及 Mac OS X 和 iOS 设备的示例游戏。对于运行书中正在开发的示例游戏,iOS 运行设备对读者将有所帮助。
本书面向的对象
本书适合想要在 iOS 平台上开始他们的游戏开发之旅的初学者。如果你是一位来自不同开发平台的中级或熟练的游戏开发者,这本书将是进入 Sprite Kit 引擎的完美入门。读者不需要对 Sprite Kit 有任何先前的知识,也不需要在 iOS 平台上构建游戏。
惯例
在本书中,您会发现许多不同的文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“要制作一个游戏中的精灵,我们必须创建SKSpriteNode类的实例。”
代码块设置如下:
init(name: String){
//it is designated initializer . initialization part
}
convenience init(){
//Calling the Designated Initializer in same class
self.init(name: "Hello")
}
新术语和重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“取消勾选肖像复选框,并在设备方向部分下勾选横屏左。”
注意
警告或重要注意事项以如下框显示。
小贴士
小贴士和技巧看起来像这样。
读者反馈
我们欢迎读者的反馈。请告诉我们您对本书的看法——您喜欢或不喜欢的地方。读者反馈对我们来说很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。
要向我们发送一般反馈,请简单地发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中提及书籍的标题。
如果您在某个主题上具有专业知识,并且您对撰写或为本书做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。
下载本书的颜色图像
我们还为您提供了一个包含本书中使用的截图/图表的颜色图像的 PDF 文件。这些颜色图像将帮助您更好地理解输出的变化。您可以从:www.packtpub.com/sites/default/files/downloads/B04201_4694OS_Graphics.pdf下载此文件。
下载示例代码
您可以从您在www.packtpub.com的账户下载示例代码文件,适用于您购买的所有 Packt Publishing 书籍。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
勘误
尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误表,请访问www.packtpub.com/books/content/support,并在搜索字段中输入本书的名称。所需信息将在勘误部分显示。
盗版
互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的我们作品的非法副本,请立即向我们提供位置地址或网站名称,以便我们可以追究补救措施。
请通过链接mailto:copyright@packtpub.com与我们联系,并提供疑似盗版材料的链接。
我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面提供的帮助。
问题
如果您在这本书的任何方面遇到问题,您可以通过链接mailto:questions@packtpub.com与我们联系,我们将尽力解决问题。
第一章. Sprite Kit 简介
在这本书中,我们将讨论使用 Sprite Kit 进行 iOS 游戏开发。我们将采取一种有趣的方法,并在过程中在 iPhone 上制作一个实际的 2D 平台游戏。我们将开发一个 2D(二维)游戏;一个只依赖于两个坐标的游戏。一些著名的 2D 游戏包括 马里奥,爬山赛车,愤怒的小鸟,割绳子,等等。
2D 游戏只处理沿 x 和 y 轴(左右和上下)的两个维度,但不处理沿 z 轴(前后)。所以基本上,玩家不能在 3D 空间中自由旋转或移动摄像头来从其他角度和视角查看对象。尽管有例外,如 2.5D 游戏;我们将在后面的章节中讨论这一点。所以,让我们不要让事情等待,直接进入这本书。
iOS 8 的新特性是什么?
你可能熟悉苹果的移动操作系统,通常称为 iOS;这个操作系统的最新版本是 iOS 8。这个版本在其前辈 iOS 7 上有很多新增功能。这个版本的一些新增功能包括 Swift 编程语言的引入,大量的新 API,以及最重要的是,Sprite Kit 及其外围框架的改进。
在这本书中,我们将使用 Swift 编程语言而不是 Objective-C。尽管你可以使用 Sprite Kit 与 Objective-C 或 Swift 一起使用,但 Swift 提供了更简单的语法,并且学习曲线更简单。
了解 Swift
Swift 是苹果为在苹果设备上开发应用程序而设计的全新多范式编程语言。Swift 开发已经进行了 4 年,于 2014 年在 全球开发者大会(WWDC)上宣布。Swift 既是脚本语言也是编程语言;它具有返回多个返回值的能力。Swift 采用了许多语言中受喜爱的不同结构,包括 Objective-C、Rust、Haskell、Ruby、Python、C#、CLU 等。它具有类型安全功能,即防止你将字符串作为 int 传递,从而最小化代码中的可能错误。
我们将在后续章节中根据需要进一步讨论 Swift。
了解 Sprite Kit
Sprite Kit 是苹果的一个框架,旨在为 iOS 设备开发 2D 游戏。它是制作 iOS 设备游戏的最佳方式之一。它易于学习,功能强大,并且完全由苹果支持,这使得它比第三方游戏开发引擎更可靠。
Sprite Kit 在 iOS 7 中引入,允许轻松、快速的游戏开发;它与流行的游戏开发库 Cocos2d 有相似之处。如果你对 Cocos2d 有一定了解,Sprite Kit 对你来说将是一件轻而易举的事情。
Sprite Kit 提供了各种对游戏开发有用的功能,例如图形渲染、动画工具、声音播放、粒子系统和物理模拟。在 Sprite Kit 中,每个节点都将有一个属性名称和物理体,物理体可以由任意形状组成,如矩形、多边形、圆形、路径等。Sprite Kit 提供了一个更丰富的粒子系统,在动画过程中可以通过代码更改任何方面。在 Sprite Kit 的粒子系统中,你还可以为创建的粒子添加自定义动作。此外,Xcode 为 Sprite Kit 提供了内置支持,因此你可以在 Xcode 中直接创建复杂特效和纹理图集。这种框架和工具的组合使 Sprite Kit 成为游戏和其他需要类似动画功能的 apps 的好选择。
由于 Sprite Kit 支持丰富的渲染基础设施,并处理所有将绘图命令提交给 OpenGL 的底层工作,你可以将精力集中在解决更高级的设计问题和创建游戏功能上。
由于 Sprite Kit 是 iOS 的原生框架,它提供了内置支持以使用粒子效果、纹理效果和物理模拟。由于它是原生框架,Sprite Kit 的性能优于其他第三方框架/游戏引擎。
Sprite Kit 的优势
Sprite Kit 的主要优势是它内置在 iOS 中。开发 2D 游戏,无需下载任何其他第三方库或依赖外部资源。其他 iOS API,如 iAd、内购等,也可以轻松使用,无需依赖额外的插件。你无需熟悉任何新的编程语言,支持 Sprite Kit 的语言也可以用于 iOS 的 app 开发。最好的是它是免费的,你可以免费获得 Sprite Kit 的所有功能。你可以在 Mac 和 iOS 上轻松运行你的游戏,你所需做的只是更改其控制方式。
Sprite Kit 的元素
现在我们将讨论一些 Sprite Kit 的元素,这些元素对于游戏开发至关重要。在 Sprite Kit 中制作的游戏由许多场景组成,这些场景由节点构成,场景中节点的功能由动作决定。
场景
游戏中的关卡或环境被称为场景。我们根据需求制作场景,例如菜单、关卡等。因此,不同关卡和不同菜单有不同的场景。它就像一个画布,你在上面放置你的元素。
Sprite Kit 中的场景由一个 SKScene 对象表示。场景包含要渲染的精灵和其他内容。要切换场景,我们可以使用 SKTransition 类。
节点
节点是场景中所有内容的根本构建块。SKScene 类是 SKNode 类的子类,因此场景是一个根节点。SKNode 类本身不会在场景上绘制任何内容;我们可以将其视为其他节点类的基类。以下是一些节点子类:
-
SKSpriteNode:这可以用于绘制纹理精灵,播放视频内容等 -
SK3DNode:这可以用于将 Scene Kit 场景渲染为 2D 纹理图像 -
SKVideoNode:这可以用于播放视频内容 -
SKLabelNode:这可以用于渲染文本字符串 -
SKShapeNode:这可以用于根据核心图形路径渲染形状 -
SKEmitterNode:这可以用于创建和渲染粒子 -
SKCropNode:这可以用于使用蒙版裁剪子节点 -
SKEffectNode:这可以用于将其子节点应用核心图像滤镜 -
SKLightNode:这可以用于将光照和阴影应用于场景 -
SKFieldNode:这可以用于将物理效果应用于场景的特定部分
动作
一个动作告诉节点要做什么,并允许你执行不同的事情,例如:
-
在任何方向上移动节点
-
使任何节点遵循路径
-
旋转节点
-
缩放节点
-
显示或隐藏一个节点
-
更改精灵节点的内文
-
播放声音
-
从场景中删除节点
-
在一个孩子的节点上执行操作,等等
要创建一个运行动作,首先,使用特定的动作类创建动作,配置创建的动作属性,并通过传递动作对象作为参数来调用运行动作。当场景处理节点时,该节点的动作将被执行。
Sprite Kit 的功能
Sprite Kit 提供了许多功能来简化游戏开发。这些功能可以用于增强游戏体验和性能。让我们简要讨论它们。
粒子编辑器
此功能是在 iOS 7 中引入的。粒子编辑器用于在游戏中添加特殊效果,例如在游戏场景中添加雾效。在这里,我们可以自定义许多事情,例如:
-
粒子的数量
-
允许的粒子数量限制
-
粒子的颜色
-
粒子的大小
-
粒子的寿命
-
场景中粒子的位置,等等
纹理图集生成器
纹理图集生成器将所有图像文件合并为一个或多个大图像,以提高性能。我们将在后面的章节中详细讨论这一点。建议使用较少的图像以减少绘制调用(场景上渲染的图像数量)。
着色器
着色器是在 iOS 8 中引入的。它们用于产生各种特殊效果;它们以高度灵活的方式在图形硬件上计算渲染效果,例如,我们在许多应用程序/游戏中看到了涟漪效果。无论用户在屏幕上触摸哪里,都会产生涟漪效果。
在 Sprite Kit 中,着色器由 SKShaderNode 类对象表示。
灯光与阴影
灯光与阴影在 iOS 8 中引入。这些效果是通过 SKLightNode 类对象产生的。SKLightNode 对象可以:
-
在场景的任何期望位置扩散灯光效果
-
在任何精灵中添加灯光
-
支持颜色和阴影
它只是一个 SKNode 类型,因此我们可以应用任何应用于任何 SKNode 的属性。
物理
在 Sprite Kit 中模拟物理可以通过向场景中添加物理体来实现。物理引擎的唯一目的是在模拟世界中移动对象。物理体具有对象的属性,如质量、形状、材料、当前轨迹等,并为所有这些对象计算新的位置。
在 Sprite Kit 游戏场景中的每一个对象都将拥有一个物理体。物理体对象连接到特定场景的节点树上的一个节点。每当场景计算新的动画帧时,场景将模拟作用于那些连接到节点树上的特定物理体的力和碰撞效果。我们可以使用它们的特定物理属性(如重力、质量、力、摩擦等)在这些节点上应用特定的物理属性。
游戏循环
以下是一个帧生命周期图:

在开始时,调用更新函数来设置游戏的逻辑。之后,场景评估动作。动作评估后,我们得到一个回调。然后,如果有的话,我们设置物理。当物理模拟完成后,我们通过 didSimulatePhysics 获得另一个调用。然后,我们应用约束并得到另一个回调,didApplyConstraints。最后一个回调方法是 didFinishUpdate;我们在帧完成和视图准备好渲染之前得到它。最后 SKView 渲染场景;帧完成,并且每秒继续 60 次。
设置项目
我们已经讨论了许多关于 Sprite Kit 的内容,现在是时候看看一个实际的项目并获取一些实际知识了。
Hello World 项目
我们需要创建一个新的项目来构建 Hello World。Xcode 项目将您的应用程序所需的一切组织到一个方便的地方。让我们通过执行以下前两点中的任意一点,然后在列表中继续操作,来在 Xcode 中创建一个全新的游戏项目:
-
在欢迎屏幕上点击 创建一个新的 Xcode 项目:
![Hello World 项目]()
-
相反,您也可以从文件菜单中选择 文件 | 新建 | 项目…:
![Hello World 项目]()
-
从新项目选择窗口中选择 游戏:
![Hello World 项目]()
-
下一个窗口会要求您自定义项目的选项。填写如下截图所示的字段:
![Hello World 项目]()
-
产品名称:这是游戏名称
-
组织名称:如果您是个人,则您的姓名或组织的名称
-
组织标识符:您组织的唯一标识符
-
包标识符:这是一个默认 ID,通过组织标识符和产品名称自动生成。
-
语言:您正在使用的编程语言,即 Objective-C 或 Swift
-
游戏技术:正在使用的游戏框架,例如 Scene Kit、Sprite Kit、Metal 等
-
设备:您希望游戏运行的设备;iPad、iPhone 或两者都行
-
这些字段可以是您想要的任何内容
-
-
按下一步,Xcode 将询问您要保存新项目的地方。选择一个目录,然后点击创建。
-
保存后,它应该会打开 Xcode 到您全新的
Hello World项目,具体到项目属性屏幕。在此屏幕上,取消选择纵向选项下的设备方向。此文件将自动保存,因此您不需要做任何事情:![Hello World 项目]()
结果
通过按键盘上的⌘ + R或点击左上角的播放按钮来运行默认的游戏项目。如果没有模拟器,Xcode 将在启动应用之前为您下载一个。结果将如下所示:

摘要
我们还学习了如何创建 Sprite Kit 项目并在其中运行Hello World。
在下一章中,我们将深入探讨场景,以及如何将场景添加到我们的平台游戏中。
第二章. Sprite Kit 中的场景
在上一章中制作的 Hello World 游戏是 Sprite Kit 的第一步。我们还认识了 Swift 编程语言,这是我们使用 Sprite Kit 进行 iOS 游戏开发将要使用的语言。
在本章中,我们将深入探讨 Sprite Kit 项目的各种基础,并深入讨论游戏中的场景。我们还将继续开发游戏 Platformer,并使用它作为学习 Sprite Kit 的工具。我们将学习 Xcode 项目中的不同自动生成文件及其重要性。只有这样,我们才能理解场景是什么,以及它们在游戏开发中的重要性。进一步地,我们还将学习节点如何在 Sprite Kit 中发挥重要作用,帮助我们提高游戏优化和控制。在本章中,我们还将学习如何在游戏中添加多个场景,并成功从一个场景过渡到另一个场景,同时动画各种过渡效果。
我们将通过学习所有这些内容,并通过游戏 Platformer 的开发来测试我们的进度,以便在本书结束时,你能够从头开始制作自己的 2D 游戏。让我们开始吧!
Sprite Kit 中的设备方向
有两种模式,即竖屏和横屏;在设置项目时,你可以选择你游戏的所需方向。在游戏开发过程中,你可以在 Sprite Kit 项目的属性部分随时更改方向。有四种方向可供选择:
-
竖屏
-
上下颠倒
-
横屏左
-
横屏右
你可以根据你的游戏选择任何方向。如果你想制作竖屏模式的游戏场景,你可以选择 竖屏 或 上下颠倒 选项。如果你想制作横屏模式的游戏,你可以选择 横屏左 或 横屏右 选项。如果你想制作同时支持竖屏和横屏的游戏,你也可以选择这两个选项。注意,如果你想制作同时支持竖屏和横屏模式的游戏,确保你在运行时处理游戏中精灵的位置。
我们项目中的方向
由于我们正在制作 Platformer 游戏,选择横屏模式会更好。虽然你可以选择 横屏左 和 横屏右,但选择一个方向进行编程会更简单。以下是将相同方向设置的步骤:
-
启动我们在上一章制作的
Platformer项目,可以通过双击项目目录中的Platformer.xcodeproj,或者从你的 Xcode 中启动。 -
点击 项目导航器,然后点击左侧面板中位于其下的 Platformer。
-
取消勾选 竖屏 复选框,并在 设备方向 部分勾选 横屏左:
![我们项目中的方向]()
回顾项目元素
现在我们将讨论您 Sprite Kit 项目中的一些自动生成文件。它们可以在 Xcode 的左侧面板中找到。
AppDelegate.swift
此文件是进入我们游戏的入口文件。当游戏从活动状态变为非活动状态(或后台状态),简单来说,当出现某些临时中断(如来电或短信消息)或用户强制退出应用程序时,其存在至关重要。此文件在项目中的本质在于,当您需要在活动和非活动状态之间执行任何特定任务时,例如,当游戏因电话而进入后台状态时保存游戏数据。
GameScene.sks
此文件是场景内容的静态存档。此文件在编辑器中显示一个视图,用于保存游戏静态内容,如玩家的生成位置、关卡结束位置等。此文件的主要本质和重要性在于,它有助于您将游戏的动态部分和静态部分分开。现在,开发者不需要编写额外的代码来指定游戏中的琐碎元素,如生成位置等。
GameScene.swift
此文件包含 GameScene 类,它是一种 SKScene 类型。SKScene 类对象用于在游戏中创建场景。当我们在前一章中开发 "Hello World" 示例游戏时,逻辑部分就在此文件中。
GameViewController.swift
当游戏开始时,会向游戏中添加一个默认视图,该视图由游戏视图控制器控制。如果用户想要向游戏中添加场景,则它将添加到视图之上。
Main.storyboard
这负责在屏幕上显示内容。创建了一个具有 SKView 视图的视图控制器的故事板,场景随后显示 Sprite Kit 游戏的内容。您可以在它们之间应用转换的同时创建额外的视图控制器和故事板。
LaunchScreen.xib
新项目使用此启动屏幕文件创建。启动屏幕使用大小类来适应不同的屏幕尺寸和方向。
调整项目
我们将对已创建的项目 Platformer 进行一些调整。请按照以下步骤操作,以根据我们的需求定制项目:
-
删除项目中现有的
GameScene.swift和GameScene.sks文件。我们将根据需要重新创建这些文件。不用担心错误,我们将在下一步中修复它。GameScene.swift是 Xcode 提供的默认场景;我们正在删除默认场景,因为我们将在游戏场景之前创建菜单场景。请看下一张截图:![调整项目]()
-
打开
GameViewController.swift文件并删除代码,如图所示:![调整项目]()
-
从
Images.xcassets中删除Spaceship图像。在这个项目中不需要 Spaceship 图像。
现在,你将不会在 Xcode 中看到错误,如果你运行Platformer,你将看不到任何东西。嗯,这不是我们想要的。现在,在我们开始编写代码之前,我们需要知道我们到目前为止已经做了什么(几乎什么都没做,只是删除了):
-
扩展 SKNode:这个扩展由 Sprite Kit 插入,假设每个游戏都必须有一个初始场景,创建一个 GameScene.sks 文件。在我们的平台游戏开始时,我们不需要这个初始场景,因为我们将在开始时创建自己的菜单屏幕。
-
viewDidLoad 中的 if 语句:由于由扩展
SKNode创建的GameScene.sks文件用于此语句。
现在我们将为此游戏创建自己的自定义场景,但在那之前,让我们先看看场景到底是什么。
什么是场景?
场景基本上是不同元素(如精灵、声音等)的逻辑集合。假设我们想要创建一个菜单,我们必须以根据我们的需求定位的方式放置一些按钮、背景和声音。
场景对象是节点的集合,但场景本身充当一个节点。想象一个以场景对象为根的节点树。由于场景中的所有节点都定位在定义的坐标中,它们的链接可以表示为:
节点(内容)→ 后代节点
节点与其后代之间的这种链接非常有用。比如说,如果你旋转树顶部的节点,所有节点都将随后旋转。
在技术术语中,场景是一个SKScene对象,它在一个视图(SKView对象)内部持有SKNode对象(例如用于精灵的SKSpriteNode对象),这样我们就可以渲染和使用它们。场景本身也是一个SKNode对象,它作为根节点并附加在SKView对象中。该场景所需的其他对象作为子节点添加到这个节点中。场景运行不同的动作并模拟物理(如果需要),然后渲染节点树。一个游戏由许多场景组成,我们可以通过子类化SKScene类来创建所需数量的场景。显示场景需要一个SKView对象。
坐标系统
在 Sprite Kit 中构建的游戏中的所有内容都与节点相关,它遵循节点树结构,其中场景是根节点,其他节点是它的子节点。当我们把节点放入节点树中时,它使用其位置属性将其放置在其父提供的坐标系中。
由于场景也是一个节点,它被放置在由SKView对象提供的视图中。我们在viewDidLoad、GameScene中删除的代码部分被添加为SKView对象的子节点。场景使用其父SKView对象的坐标系来渲染自身及其内容。坐标系与我们学习的基本数学中的坐标系相同。

如前图所示,如果我们从(0,0)向右移动,则x值为正,如果我们从(0,0)向左移动,则x值为负。如果我们从(0,0)向上移动,则y值为正,如果我们从(0,0)向下移动,则y值为负。坐标值以点为单位测量,当场景渲染时,它将被转换为像素。
Sprite Kit 中的所有节点都不会绘制内容。例如,SKSpriteNode用于在游戏中绘制精灵,但SKNode类不绘制任何内容,因为SKNode是大多数 Sprite Kit 内容的基石。
创建场景
当我们创建场景时,我们可以定义许多属性,如大小、原点等,以满足我们在游戏中的需求。场景大小定义了SKView对象中的可见区域。当然,我们可以将节点放置在这个区域之外,但它们将被渲染器完全忽略。
然而,如果我们尝试更改场景的位置属性,Sprite Kit 将会忽略它,因为场景是节点树中的根节点,其默认值是CGPointZero。但我们可以通过anchorPoint属性移动场景原点。anchorPoint的默认值是(0.5,0.5),表示屏幕的中心点。通过重新分配新的anchorPoint属性,我们可以为其子节点改变坐标系。例如,如果我们设置anchorPoint为(0,0),场景的子节点将从场景的左下角开始。
如果我们将anchorPoint (0.5, 0.5)或屏幕中间设置为锚点,场景的子节点将从屏幕中间开始。这完全取决于我们以及我们根据需求选择的anchorPoint。
创建节点树
场景的节点树是以父子关系创建的。作为一个场景类似于根节点,另一个节点作为其子节点。以下是一些常用的创建节点树的方法:
-
addChild:它将节点添加到接收者子节点列表的末尾 -
insertChild:atIndex:它在接收者子节点列表中的特定位置插入一个子节点
如果你想从一个节点树中移除一个节点,你可以使用以下方法:
removeFromParent:它从父节点中移除接收到的节点
节点树的绘制顺序
当节点树渲染时,所有子节点也会渲染。首先渲染父节点,然后按照它们添加到父节点的顺序渲染其子节点。如果你在一个场景中有很多节点需要渲染,保持它们顺序是一个困难的任务。为此,Sprite Kit 提供了一个使用z位置的解决方案。你可以通过使用zPosition属性设置节点的z位置。
当考虑z位置时,节点树将按照以下方式渲染:
-
首先,计算每个节点的全局z位置
-
然后,节点按照从最小的z值到最大的z值的顺序绘制
-
如果两个节点具有相同的z值,则先绘制祖先节点,然后按照子节点顺序绘制兄弟节点
正如你所看到的,Sprite Kit 使用基于高度的节点及其在节点树中的位置的确定性渲染顺序。但是,由于渲染顺序非常确定,Sprite Kit 可能无法应用它可能应用的某些渲染优化。例如,如果 Sprite Kit 能够收集所有共享相同纹理和绘图模式的节点,并使用单个绘图过程绘制它们,可能会更好。为了启用这些类型的优化,你必须将视图的ignoresSiblingOrder属性设置为true。
当你忽略兄弟顺序时,Sprite Kit 使用图形硬件来渲染节点,使它们按 z 轴顺序出现。它将节点排序到一个绘图顺序,以减少渲染场景所需的绘制调用次数。但是,使用这种优化的绘图顺序,你无法预测具有相同 z 轴索引的节点的渲染顺序。渲染顺序可能会在每次渲染新帧时改变。在许多情况下,这些节点的绘图顺序并不重要。例如,如果节点处于相同的高度但屏幕上不重叠,它们可以按任何顺序绘制。
因此,我们可以通过设置ignoresSiblingOrder属性为false或true来使用基于节点树渲染或基于深度渲染。如果我们将其设置为true,我们可以设置 z 位置,但如果设置为false,我们必须在向父节点添加子节点时注意顺序。
以下是基于节点渲染(父子渲染)的描述:

接下来是基于深度渲染(基于 z 位置的渲染)的描述:

在我们的游戏中添加第一个场景
现在是时候将菜单场景添加到我们的游戏中了。为此,选择Platformer文件夹,右键单击此文件夹,选择New File。选择iOS | Source | Swift File然后Next。在Save As中,将其命名为MenuScene,然后点击Create。
点击你的MenuScene.swift文件。现在该做一些代码工作了:
import SpriteKit
class MenuScene: SKScene
{
//#1
let PlayButton: SKSpriteNode
let Background: SKSpriteNode
//#2
init(size:CGSize, playbutton:String, background:String)
{
PlayButton = SKSpriteNode(imageNamed: playbutton)
Background = SKSpriteNode(imageNamed: background)
super.init(size:size)
}
//#3
required init?(coder aDecoder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
//#4
override func didMoveToView(view: SKView)
{
addChildToScene();
}
//#5
func addChildToScene()
{
PlayButton.zPosition = 1
Background.zPosition = 0
Background.size = CGSize(width:self.size.width, height:self.size.height)
addChild(PlayButton)
addChild(Background)
}
//#6
override func update(currentTime: NSTimeInterval) {
}
}
在前面的代码中,我们创建了一个MenuScene类,其类型为SKScene。SKScene是一个用于创建场景的类。让我们看看在这个代码中使用的术语:
-
在
#1代码块(参考前面的代码)中,我们定义了两个SKSpriteNode引用。一个用于播放按钮,另一个用于背景。let关键字表示一旦我们为这个引用分配了一个值,我们就不能改变它。如果你想改变它,你应该使用var关键字而不是let。 -
在
#2代码块(参考前面的代码)中,我们为这个类定义了一个初始化器。初始化器用于创建特定类型的实例。在这个初始化器内部,我们初始化PlayButton和Background。我们通过设置其size属性为全屏大小来给背景。最后,我们通过super.init调用父类的init。 -
在
#3代码块(参考前面的代码)中,我们在编译时移除错误。所需的关键字表示该类的每个子类都必须实现该初始化器。 -
在
#4代码块(参考前面的代码)中,我们重写了其父类方法。didMoveToView方法在视图呈现场景后立即被调用。在这里,我们调用了自定义方法addChildToScene。 -
在
#5代码块(参考前面的代码)中,我们定义了addChildToScene方法。在这个方法内部,我们只是给PlayButton分配了z位置,并为Background定义了size。记住,我们可以使用z深度来控制哪个层将渲染在哪个之上。如果你将z深度设置为最小值,它将首先渲染,然后是最大值。这意味着z深度越低,在场景中的位置就越低。这就是为什么我们将Background的z深度设置得低于PlayButton,这样PlayButton就可以渲染在Background之上。之后,我们将PlayButton和Background添加到场景中。 -
在
#6代码块(参考前面的代码)中,我们只是重写了更新方法。此方法的代码将在以后更新。
哇!我们已经创建了第一个场景。现在是时候看看我们做了什么。但在那之前,我们必须将此场景添加到视图中,以便我们可以使其可见并活跃。打开你的 GameViewController 类,在 super.viewDidLoad() 下的 viewDidLoad 中粘贴代码:
let menuscene = MenuScene(size: view.bounds.size, playbutton: "Play", background: "BG")
let skview = view as SKView
skview.showsFPS = true
skview.showsNodeCount = true
skview.ignoresSiblingOrder = true
menuscene.scaleMode = .ResizeFill
menuscene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
skview.presentScene(menuscene)
在此代码中,我们创建了 menuscene 实例并将其添加到视图中。Play 和 BG 是 PNG 精灵的名称,我们稍后会添加。我们将视图类型转换为 SKView 并设置了一些属性。如果我们想看到每秒帧率,我们将 showFPS 设置为 true。对于计数节点也是如此。如果我们将 ignoresSiblingOrder 属性设置为 false,那么这意味着共享相同 z 深度的节点将按照父到子的顺序渲染。
这意味着父节点将首先渲染,然后是其子节点。如果我们将其设置为 true,那么这意味着具有相同 z 深度的所有节点将同时渲染,而不是按照父到子的偏好顺序。为了最大化优化,我们将此值设置为 true;简单来说,如果你想得到更快的结果,最好将其设置为 true。
.scaleMode 用于填充视图内的场景。ResizeFill 表示它会调整自身大小以填充整个视图。
现在,对于 anchorPoint。它决定了根据父节点位置,子节点的坐标系将是什么。如果我们将其设置为 .5, .5,这意味着将添加到这些场景中的节点,其坐标系将从屏幕中间开始。你可以选择你感到舒适的方式。
在最后一行,我们只是将 menuscene 添加到视图中,以便它可以渲染。
现在,是时候向项目中添加一些图像了。首先想到的问题是,“如何在各种屏幕尺寸上保持图像的质量?”
为了优化大屏幕设备上图像的质量,我们在两个不同的大小中添加了相同的图像,1x——原始图像,和 2x——原始图像的两倍大小,以便在更大的设备上获得更好的显示质量。iOS 将自动选择适当的大小。
小贴士
此外,你也可以选择 3x 的图像大小,用于更大的设备。
两组图片大小足以覆盖大多数常用的屏幕尺寸。
下面是向项目中添加图片的步骤:
-
点击
Images.xcassets| 选择 New Image Set:![在我们的游戏中添加第一个场景]()
-
之后,将其命名为
BG,并根据大小拖放你的背景图片。如图所示:![在我们的游戏中添加第一个场景]()
-
对一组游戏图片重复此过程。
-
运行它并查看。你将看到背景全屏显示,屏幕中央有一个 Play 按钮。我们也可以控制 Play 按钮的大小,就像我们对
Background做的那样。
恭喜,你创建了你的第一个场景。现在是你创建另一个场景的时候了,即 GameScene,以及从 Menuscene 到 Gamescene 的过渡。
向我们的游戏中添加另一个场景
创建 GameScene 文件,就像我们为 MenuScene 做的那样:
import SpriteKit
class GameScene: SKScene
{
let backgroundNode = SKSpriteNode(imageNamed: "BG")
override func didMoveToView(view: SKView) {
addBackGround()
}
func addBackGround()
{
backgroundNode.zPosition = 0
backgroundNode.size = CGSize(width:self.size.width, height:self.size.height)
addChild(backgroundNode)
}
override func update(currentTime: NSTimeInterval) {
}
}
代码是自我解释的,我们只为 GameScene 添加了背景,这与我们对 MenuScene 做的相同。
从一个场景到另一个场景的过渡
过渡动画是通过使用一个名为 SKTransition 的对象来实现的;在从一个场景切换到另一个场景时,会用到这个对象来执行这个动作。正如我们所知,场景是游戏的基本构建块。在游戏中,从一个场景过渡到另一个场景在许多情况下都是必要的,例如:
-
一个加载场景,在游戏中显示其他对象正在加载时
-
一个主菜单场景,其中向用户展示了不同的选项
-
一个关卡选择菜单场景,用于选择可用的不同关卡
-
一个游戏玩法场景,其中包含游戏的主要元素
-
游戏结束场景,用于表示游戏的结束,等等
当你在已经显示场景的视图中呈现一个新的场景时,你有使用过渡来动画化从旧场景到新场景的变化的选项。使用过渡提供了连续性,因此场景变化不会突然发生,也不会打扰游戏的 UI。
当过渡发生时,场景属性会立即更新以指向新的场景。然后,动画开始。最后,对旧场景的强引用被移除。如果你需要在过渡发生后保留场景,你的游戏必须保留对旧场景的自己的强引用。
小贴士
下载示例代码
您可以从您在 www.packtpub.com 的账户下载所有已购买 Packt 出版物的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册以直接将文件通过电子邮件发送给您。
设置过渡期间的动画播放
通常,当两个场景之间发生过渡时,两个场景都会暂停。这意味着如果两个场景中的任何一个场景正在播放动画,它将暂停,直到过渡完成。有时,需要完成一个场景的动画效果。过渡对象上的 pausesIncomingScene 和 pausesOutgoingScene 属性定义了在过渡期间播放哪些动画。
创建过渡对象
通过将 SKTransition 作为对象使用来应用过渡;执行此操作的一些方法如下:
-
class func crossFadeWithDuration(_ sec: NSTimeInterval) -> SKTransition: 这将创建一个交叉淡入淡出过渡;它接受过渡的持续时间作为参数,并返回一个SKTransition对象。 -
class func doorsCloseHorizontalWithDuration(_ sec: NSTimeInterval) -> SKTransition: 这将创建一个过渡,新场景以一对关闭的水平门的形式出现;它也接受过渡的持续时间作为参数,并返回一个SKTransition对象。 -
class func doorsCloseVerticalWithDuration(sec: NSTimeInterval) -> SKTransition: 这将创建一个过渡,新场景以一对关闭的垂直门的形式出现。它也接受过渡的持续时间作为参数,并返回一个SKTransition对象。 -
class func doorsOpenHorizontalWithDuration(_ sec: NSTimeInterval) -> SKTransition: 这将创建一个过渡,新场景以一对开启的水平门的形式出现。它也接受过渡的持续时间作为参数,并返回一个SKTransition对象。 -
class func doorsOpenVerticalWithDuration(_ sec: NSTimeInterval) -> SKTransition: 这将创建一个过渡,新场景以一对开启的垂直门的形式出现。它还接受过渡的持续时间作为参数,并返回一个SKTransition对象。 -
class func doorwayWithDuration(_ sec: NSTimeInterval) -> SKTransition: 这将创建一个过渡,前一个场景以一对开启的门的形式消失。新场景在背景中开始,随着门的开启而逐渐靠近。它也接受过渡的持续时间作为参数,并返回一个SKTransition对象。 -
class func fadeWithColor(_ color: UIColor, duration sec: NSTimeInterval) -> SKTransition: 这将创建一个过渡,首先淡入到恒定颜色,然后淡入到新场景。它接受淡入颜色和过渡的持续时间作为参数,并返回SKTransition对象。 -
class func fadeWithDuration(_ sec: NSTimeInterval) -> SKTransition: 这创建了一个过渡效果,首先淡入黑色,然后淡入新的场景。它接受过渡的持续时间作为参数,并返回一个SKTransition对象。 -
class func flipHorizontalWithDuration(_ sec: NSTimeInterval) -> SKTransition: 这创建了一个过渡效果,其中两个场景在视图中通过中心线的水平方向翻转。它接受过渡的持续时间作为参数,并返回一个SKTransition对象。 -
class func flipVerticalWithDuration(_ sec: NSTimeInterval) -> SKTransition: 这创建了一个过渡效果,其中两个场景在视图中通过中心线的垂直方向翻转。它接受过渡的持续时间作为参数,并返回一个SKTransition对象。 -
class func moveInWithDirection(_ direction: SKTransitionDirection, duration sec: NSTimeInterval) -> SKTransition: 这创建了一个过渡效果,其中新场景在旧场景之上移动。它接受移动的方向和持续时间作为参数,并返回一个 SKTransition 对象。 -
class func pushWithDirection(_ direction: SKTransitionDirection, duration sec: NSTimeInterval) -> SKTransition: 这创建了一个过渡效果,其中新场景进入,将旧场景推出视图。它接受推动的方向和过渡的持续时间作为参数,并返回一个 SKTransition 对象。 -
class func revealWithDirection(_ direction: SKTransitionDirection, duration sec: NSTimeInterval) -> SKTransition: 这创建了一个过渡效果,其中旧场景从视图中移出,从而揭示其下方的新的场景。它接受揭示的方向和过渡的持续时间作为参数,并返回一个SKTransition对象。
在我们的游戏中添加过渡效果
现在,打开 MenuScene。首先,在 MenuScene 类中初始化代码块之前定义 GameScene 引用:
var gameScene : GameScene?
Add the following code below update function
override func touchesBegan(touches: NSSet, withEvent event: UIEvent)
{
for touch: AnyObject in touches
{
let location = touch.locationInNode(self)
let node = self.nodeAtPoint(location)
if node.name == PlayButton.name
{
goToGameScene()
}
}
}
func goToGameScene(){
let transitionEffect = SKTransition.flipHorizontalWithDuration(1.0)
gameScene = GameScene(size: self.size)
gameScene!.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.view?.presentScene(gameScene , transition:transitionEffect)
}
在 didMoveToView 中,将以下行放置在 addChildToScene 之下:
PlayButton.name = "PLAY"
如果你现在运行它,你会看到我们的菜单场景,其中有一个播放按钮和背景;如果你点击播放按钮之外的地方,什么也不会发生。当你点击播放按钮时,你会看到一个平滑的过渡到游戏场景。
在前面的代码中,var 是一个关键字,表示它的值可以改变。但是 GameScene 后面的 ? 符号是什么意思?
? 符号表示引用是可选的。这意味着它可以有值,也可以是 nil。
那就是为什么我们不需要在初始化代码块中初始化它的原因。
在 PlayButton.name = "PLAY" 中,我们只是给 SKSpriteNode 对象赋予一个名称,这样当我们触摸这个精灵时,我们可以通过名称来验证它。
touchesBegan 是一个重写方法,用于识别触摸事件刚开始时的情况。在这个方法中,我们获取触摸位置的节点并检查是否包含所需的节点。如果存在 SKSpriteNode 节点,它将通过其名称被识别,并调用 goToGameScene 方法。
在 goToGameScene 方法中,我们只是添加了带有一些过渡效果的 GameScene 到视图中。过渡是 SKTransition 类的实例。在这里,我们使用了 flipHorizontalWithDuration 过渡效果。
你还可以调整并尝试其他可用的过渡效果。
在 gameScene!.anchorPoint = CGPoint(x: 0.5, y: 0.5) 中,我们在 GameScene 后面放置了一个感叹号。正如我们所知,GameScene 是可选的,我们必须告诉编译器我们知道它有值,并且我们正在强制展开其值。! 符号用于强制展开可选值。
摘要
在本章中,我们学习了设备方向以及 Sprite Kit 项目中自动生成的不同文件。我们还研究了场景,并看到了如何在 Sprite Kit 项目中创建它们。此外,我们还讨论了场景之间的过渡及其类型。
在下一章中,我们将学习关于精灵和纹理图集的内容。随着我们的深入,我们的 Platformer 游戏将变得更加有趣和吸引人。
第三章. 精灵
在前一章中,我们设置了我们的第一个场景,学习了场景中节点的渲染,在一个项目中添加了多个场景,并学习了从一个场景到另一个场景的过渡。
在本章中,我们将学习关于精灵的内容。精灵是一个二维图像,集成到场景中。精灵的集合称为精灵表。在这里,我们将学习如何在游戏中添加精灵、定位精灵、纹理图集以及如何在我们的游戏中变换精灵。
在 Sprite Kit 中,游戏基于节点树层次结构。场景作为根节点,其他添加到其中的节点是子节点。一旦所有节点都渲染到场景中,我们就得到了视图。精灵也作为节点添加到游戏中;Sprite Kit 为此提供了 SKSpriteNode 类。在前一章中,我们通过创建 SKSpriteNode 引用并将其添加到相应场景中,添加了背景精灵和播放按钮精灵。现在我们将发现 Sprite Kit 在 SKSpriteNode 类中为我们提供了什么,以及我们可以用它做什么。
SKSpriteNode
SKSpriteNode 类是一个根节点类,用于以许多自定义方式绘制纹理图像;它继承自 SKNode 类。我们可以简单地绘制一个图像,或者我们可以添加一些效果,例如自定义着色器或阴影。为此,我们首先需要了解 SKSpriteNode 类及其提供的功能。
初始化精灵
要在游戏中创建一个精灵,我们必须创建 SKSpriteNode 类的一个实例。Sprite Kit 提供了许多初始化 SKSpriteNode 类实例的方法。其中一些如下:
init(name: String){
//it is designated initializer . initialization part
}
convenience init(){
//Calling the Designated Initializer in same class
self.init(name: "Hello")
}
在 Swift 中,必须通过创建结构体的对象来初始化一个类。为此提供了两个初始化器,即指定初始化器和便利初始化器。
指定初始化器为类属性执行实际初始化。现在问题来了,“为什么需要便利初始化器?”在编程过程中,有时便利初始化器非常有用,因为它们需要的输入参数较少,并将实际初始化工作交给指定初始化器。
Swift 中初始化器的示例如下:
-
convenience init(color color:UIColor!, size size: CGSize): 这用于初始化一个彩色精灵。如果您想创建一个不使用任何纹理而仅使用颜色的精灵,可以使用此初始化器。它接受颜色和大小作为参数,并返回一个新初始化的精灵对象。 -
convenience init(imageNamed name: String): 这个初始化器将纹理分配给精灵。精灵将从图像名称分配纹理,并将精灵的颜色初始化为白色。 -
convenience init(texture texture: SKTexture!): 这个初始化器接受一个现有的纹理精灵并返回一个新初始化的精灵。精灵的大小设置为纹理的尺寸,精灵的颜色设置为白色(1.0, 1.0, 1.0)。 -
init(texture texture: SKTexture!,color color: UIColor!,size size: CGSize): 如前所述,这个初始化器需要一个便利初始化器作为其参数,因此这是一个指定初始化器。现在,我们的Platformer游戏精灵将被初始化为所需的纹理、颜色和大小。它返回一个新初始化的精灵。 -
convenience init(texture texture: SKTexture!,size size: CGSize): 这个初始化方法接受纹理和大小作为参数,并返回一个新初始化的精灵。 -
convenience init(imageNamed name: String, normalMapped generateNormalMap: Bool): 这个初始化方法接受一个图像名称和一个布尔值作为参数,并返回一个新初始化的对象。 -
convenience init(texture texture: SKTexture!,normalMap normalMap: SKTexture?): 这个初始化方法接受两个纹理作为参数,一个用于精灵绘制,另一个用于向精灵添加光照行为。它返回一个新初始化的精灵。
在了解了SKSpriteNode的初始化之后,现在是时候让我们熟悉一些SKSpriteNode的物理属性了,比如size、anchorPoint等等。
SKSpriteNode的属性
让我们在以下章节中讨论SKSpriteNode的属性。
物理
让我们来看看SKSpriteNode的一些物理属性:
-
size: 这个属性决定了精灵在点上的大小。在我们的GameScene和MenuScene类中,我们使用这个属性在背景精灵上以覆盖屏幕。 -
AnchorPoint: 锚点是与精灵相关的坐标点。例如,一个精灵每个角落的坐标是(0,0)、(1,0)、(0,1)和(1,1),分别代表左下角、右下角、左上角和右上角。这些参考点可以被分配为锚点,以便在屏幕上相应地绘制精灵。分配的锚点将根据情况在屏幕上定位精灵。例如,假设我们的精灵锚点是
(0,0)。如果我们将这个精灵放置在屏幕上,它将从坐标(0,0)开始放置,即左下角。要将精灵从中心位置放置,我们需要将锚点坐标设置为(0.5, 0.5)。
但要向这个精灵添加另一个节点,该节点的(0,0)坐标将位于精灵的锚点上。当我们向视图中添加场景时,场景的(0,0)坐标将成为默认的锚点。
在size和anchorPoint下已经很好地讨论了精灵的物理属性。现在,是时候讨论一些与精灵纹理相关的属性了。
纹理
它是SKSpriteNode类中的一个可选属性;这意味着它可以 nil 或者将具有纹理。如果它是 nil,那么精灵将通过使用其color属性以矩形形状绘制,否则精灵将通过这个纹理绘制。
centerRect
这个属性是创建矩形按钮或场景中任何其他固定大小元素的一个非常有用的工具。当你使用centerRect属性时,你实际上是在控制由坐标指定的矩形部分的纹理缩放因子。
默认情况下,矩形覆盖整个纹理;这就是为什么整个纹理都会被拉伸。但如果这个矩形只覆盖纹理的一部分,那么纹理就可以在一个3 * 3的网格中可视化,将这个矩形放在网格的中间,并在每侧从其每个边缘画一条线。

原始图像
如果我们尝试在两个方向上拉伸纹理,那么它将遵循以下规则:
-
网格的中间部分将在每侧水平垂直拉伸
-
网格的所有四个角落部分都不会被拉伸
-
网格的上下中间部分将水平拉伸
![centerRect]()
从中心水平拉伸的图像
-
网格的左右中间部分将垂直拉伸
![centerRect]()
从中心垂直拉伸的图像
此外,以下是一个图像在垂直和水平方向上都被拉伸的情况:

从中心垂直和水平拉伸的图像
这是一个非常有用的属性,可以实现纹理的一些特定行为,例如在游戏中制作生命条,我们不想拉伸纹理的角落部分,所以如果它们是圆角,它们不应该变形。
颜色
SKSpriteNode也有一些颜色属性。让我们详细了解一下它们:
-
color: 这个属性用于为精灵添加颜色。例如,当生命条减少到 50%、25%等时,你需要更改精灵的颜色。 -
colorBlendFactor: 这个属性用于控制精灵纹理的颜色混合。它可以在0.0到1.0(包含)之间取值;默认值为0.0。如果值为0.0,则表示忽略color属性,使用未修改的纹理值。如果你增加值,精灵将添加更多颜色。例如,我们可以使用这个属性来随着对角色攻击次数的增加,在我们的角色中混合更多颜色:![Color]()
由于 colorBlendFactor 值的变化而产生的颜色效果
-
blendMode: 这个属性用于根据场景混合精灵。精灵的每个像素颜色和其下对应场景像素的颜色,将由 Sprite Kit 渲染器比较,为精灵分配一个结果颜色。当你向场景添加光照效果或闪光效果时,这个属性非常有用。
在 iOS 8 中,为精灵添加了一些光照属性以生成光和影效果。让我们来看看它们:
-
lightingBitMask:此属性用于在精灵上显示光照效果,并通过逻辑与操作与光的categoryBitMask属性进行测试。如果值为非零,精灵将发光,否则它将不受光的影响。其默认值为0x00000000。 -
shadowedBitMask:此属性确定精灵是否会被光照产生的阴影所影响。此属性通过逻辑与操作与光的categoryBitMask属性进行测试。如果值为非零,精灵将以阴影效果绘制,否则它将不受光的影响。其默认值为0x00000000。 -
shadowCastBitMask:此属性确定精灵是否会阻挡光线并投射阴影。此属性通过逻辑与操作与光的categoryBitMask属性进行测试。如果值为非零,精灵将投射超出自身的阴影,否则它将不受光的影响。其默认值为0x00000000。 -
normalTexture:当精灵被照亮时,使用正常贴图纹理,给它带来更逼真的外观,带有阴影和令人惊叹的高光。纹理必须是正常贴图纹理。
除了光照属性外,iOS 8 还引入了着色器属性来定制渲染效果。
着色器
着色器属性在第七章中专门讨论,粒子效果和着色器。
这些是SKSpriteNode属性,通过这些属性,我们可以根据我们的需求自定义精灵来使用。游戏的大部分内容都是由精灵组成的,因此了解这些属性以及我们如何使用它们非常重要。现在,是时候在我们的游戏中使用这些属性并看看它们会产生什么效果了。
添加不带纹理的精灵
在游戏中,我们通常会给我们的精灵添加纹理,但也可以不使用纹理来制作精灵。纹理属性是SKSpriteNode类中的一个可选属性。如果纹理为 nil,这意味着我们没有可拉伸的纹理,因此收缩参数被忽略。让我们打开我们的GameScene.swift文件,在backgroundNode声明下方创建一个SKSpriteNode变量:
var spriteWithoutTexture : SKSpriteNode?
现在,有了前面的声明,我们已经将spriteWithoutTexture声明为可选的。由于我们已将其声明为可选的,纹理不需要值。现在在didMoveToView下添加以下函数:
func addSpriteWithoutTexture(){
spriteWithoutTexture = SKSpriteNode(texture: nil, color: UIColor.redColor(), size: CGSizeMake(100, 100))
addChild(spriteWithoutTexture!)
}
之后,在didMoveToView()函数中调用此函数,在addBackGround()函数下方:
addSpriteWithoutTexture()
现在点击播放,看看会发生什么。在我们的GameScene中没有变化。这不是我们想要的。实际上,我们遗漏了纹理的z位置。这就是为什么它渲染在背景后面,没有显示给我们。在我们的addSpriteWithoutTexture()函数中添加此行,在addChild(spriteWithoutTexture!)之前:
spriteWithoutTexture!.zPosition=1;
运行它。你会在屏幕中间看到一个红色的正方形。
代码是自解释的。我们通过实例化创建了一个SKSpriteNode实例。我们传递 nil 作为纹理参数,这意味着我们不想为这个精灵添加纹理。由于我们已经使这个精灵引用为可选的,在使用任何SKSpriteNode属性之前,我们必须解包它,我们通过在spriteWithoutTexture后使用!标记来实现这一点。
我们也可以用另一种方式初始化。从初始化部分删除texture参数:
spriteWithoutTexture = SKSpriteNode(texture: nil, color: UIColor.redColor(), size: CGSizeMake(100, 100))
将前面的初始化部分更改为如下所示:
spriteWithoutTexture = SKSpriteNode(color: UIColor.redColor(), size: CGSizeMake(100, 100))
运行代码,它会产生与上一个相同的结果。它自动将 nil 分配给纹理,并初始化一个具有颜色和指定边界的精灵。让我们用它做一些有趣的事情。
更改颜色属性
我们将使用color属性在用户点击这个精灵时改变颜色。为此,首先给spriteWithoutTexture起一个名字,这样我们就能识别出对其的点击:
spriteWithoutTexture!.name = "HELLO"
在GameScene.swift文件中添加以下函数以更改颜色,如下所示代码:
var
现在,我们使用touchesBegan函数来检测用户的触摸(就像之前在MenuScene类中使用的那样):
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches{ currentno = 0;
func changeColor(){
switch(currentno%3){
case 0:
spriteWithoutTexture!.color = UIColor.redColor()
case 1:
spriteWithoutTexture!.color = UIColor.greenColor()
case 2:
spriteWithoutTexture!.color = UIColor.blueColor()
default :
spriteWithoutTexture!.color = UIColor.blackColor()
}
}
let location = touch.locationInNode(self)
let node = self.nodeAtPoint(location)
if node.name == spriteWithoutTexture!.name {
currentno++
changeColor()
}
}
}
现在,在运行 Xcode 后,点击GameScene中的彩色区域。你会看到该区域颜色发生变化。
在这段代码中,当用户点击精灵时,它将向当前值添加一个值并调用changeColor()函数。在changeColor()函数中,我们使用switch语句来确定spriteWithoutTexture的颜色属性。在 Swift 中,switch语句的使用方式与许多其他语言类似。我们不需要使用break语句。每个switch语句都必须是详尽的。这意味着我们必须对每个单独的情况进行检查。因此,我们必须为每个switch情况编写一个default值。
如果我们的纹理不是 nil,我们可以使用colorBlendFactor属性来着色纹理。我们可以用它来实现着色效果,例如游戏中受到的伤害;如果纹理是 nil,则忽略colorBlendFactor。它的默认值是0.0,这意味着纹理应该保持未修改状态。当我们增加值时,纹理颜色将被混合颜色所替代。
在MenuScene中更改colorBlendFactor
让我们在播放按钮上添加一些色调。打开MenuScene并在MenuScene类中定义一个名为tintChanger的可选Float类型变量,这样我们就不需要在初始化器中为其分配值了:
var tintChanger : Float?
在MenuScene类中添加以下函数:
func tintPlayButton(){
if PlayButton.colorBlendFactor >= 1{
tintChanger = -0.02
}
else if PlayButton.colorBlendFactor <= 0{
tintChanger = 0.02
}
PlayButton.colorBlendFactor += CGFloat(tintChanger!)
}
从update函数中调用它:
override func update(currentTime: NSTimeInterval) {
tintPlayButton()
}
现在运行 Xcode。你会看到播放按钮分别出现和消失。
在这段代码中,我们只创建了一个Float类型的变量。在我们的tintPlayButton函数中,我们检查其colorBlendFactor属性的值是否在1到0之间。
现在让我们在addChildToScene函数中给它一个颜色:
PlayButton.color = UIColor.redColor()
运行它,你会看到播放按钮的颜色从原始颜色变为红色。现在,是时候看看位置属性的实际效果了。
改变精灵的位置
现在,让我们看看SKSpriteNode的position属性。让我们再次打开GameScene,因为我们将要查看spriteWithoutTexture.position属性以及我们可以如何设置它。在changeColor函数下方添加此函数:
func changePosition(){
switch(currentno%3){
case 0:
spriteWithoutTexture!.position = CGPointZero
case 1:
spriteWithoutTexture!.position = CGPointMake(self.size.width/2-spriteWithoutTexture!.size.width/2, 0)
case 2:
spriteWithoutTexture!.position = CGPointMake
(-self.size.width/2+spriteWithoutTexture!.size.width/2, 0)
default :
spriteWithoutTexture!.position = CGPointMake(0, 0)
}
}
并且在changeColor()调用下方调用它。
changePosition()
现在如果你运行它并在游戏场景中点击,你会看到spriteWithoutTexture改变位置并在它们之间切换。
代码的大部分与changecolor()相同,除了位置。在case 0中,我们将它的位置设置为CGPointZero。位置是以CGPoint单位测量的。CGPointZero等同于CGPointMake(0, 0)。精灵的位置不仅取决于其anchorPoint,还取决于其父anchorPoint。
由于我们将GameScene的anchorPoint定义为(0.5 , 0.5),这意味着任何将被添加到GameScene的其他节点都将有起始位置(0,0),从屏幕的中间开始。这就是为什么背景和spriteWithoutTexture (0,0)坐标将在屏幕中间。
现在,正如我们指定的spriteWithoutTexture的anchorPoint,它将采用其默认值(0.5,0.5)。这意味着它的anchorPoint将位于其中心。因此,在case 0中,它以对称的方式在屏幕中间渲染。在case 1和case 2中,我们只是将其移动到屏幕的右中角和左中角。
让我们尝试改变anchorPoint并看看会发生什么。在addSpriteWithoutTexture内部添加此行:
spriteWithoutTexture!.anchorPoint = CGPointZero
现在运行它。

Before tap

After tap
你会发现所有位置都不像之前那样了。你能猜出这个原因吗?
在前面的代码行中,我们将新值(0,0)赋给了spriteWithoutTexture,这将移除其默认值(0.5,0.5)。这意味着它的anchorPoint将不会从中间开始。它将从这个的左下角开始。为了可视化它,考虑你的精灵的右上角为1,1,左下角为0,0。现在如果你将anchorPoint设置为0,0,它将在精灵的左下角。如果你将其设置为0,1,它将在左上角。对于1,1,它将在右上角,对于1,0,它将在右下角。你可以将其更改为任何你喜欢的值,例如负的(-1,-2)等等。
现在,我们可以看到,一旦我们熟悉了屏幕上的坐标数字,设置anchorPoint就变得容易了。所以,让我们通过将spriteWithoutTexture的位置设置回之前的位置,使用0,0作为anchorPoint来测试一下。将changePosition函数替换为以下内容:
func changePosition(){
switch(currentno%3){
case 0:
spriteWithoutTexture!.position = CGPointMake
(-spriteWithoutTexture!.size.width/2,
-spriteWithoutTexture!.size.height/2)
case 1:
spriteWithoutTexture!.position = CGPointMake(self.size.width/2-spriteWithoutTexture!.size.width,
-spriteWithoutTexture!.size.height/2)
case 2:
spriteWithoutTexture!.position = CGPointMake
(-self.size.width/2, -spriteWithoutTexture!.size.height/2)
default :
spriteWithoutTexture!.position = CGPointMake(0, 0)
}
}
在addSpriteWithoutTexture()函数内部添加以下行:
spriteWithoutTexture!.position = CGPointMake
(-spriteWithoutTexture!.size.width/2,
-spriteWithoutTexture!.size.height/2)
现在运行它。你将在GameScene中看到与之前相同的结果。
在这段代码中,我们做了一些小的调整。我们希望spriteWithoutTexture位于中心。由于它的anchorPoint是(0,0),其左下角将位于屏幕中央。因此,为了在屏幕中央显示它,我们必须通过从每个宽度和高度的中间点(0,0)减去一半的宽度和高度来设置其位置,这些中间点是屏幕的左上角。同样适用于精灵的左右位置。
现在,只需在GameViewController.swift文件中将MenuScene anchorPoint设置为(1,1),并尝试自己调整按钮和背景的位置。如果你无法做到这一点,只需在addChildToScene函数中添加以下代码:
Background.position = CGPointMake(-self.size.width/2,
-self.size.height/2)
PlayButton.position = CGPointMake(-self.size.width/2,
-self.size.height/2)
现在,如果你运行这段代码,你会注意到与之前相同的结果。在定位之后,让我们谈谈调整精灵大小。
调整精灵大小
由于SKSpriteNode类是从SKNode类继承的,它也从SKNode类继承了xScale和yScale属性。在我们的场景中,我们已经将背景的宽度设置为与我们的视图相同。如果我们使用其原始尺寸并缩放其宽度和高度,我们将得到与之前相同的结果。打开GameScene类,并更新addBackGround函数如下:
func addBackGround() {
backgroundNode.zPosition = 0
var scaleX = self.size.width/backgroundNode.size.width
var scaleY = self.size.height/backgroundNode.size.height
backgroundNode.xScale = scaleX
backgroundNode.yScale = scaleY
addChild(backgroundNode)
}
我们已经修改了函数addBackGround(),以便我们的游戏能够检测设备的屏幕尺寸。这使我们的游戏具有可移植性(例如,iPhone 5 和 iPhone 6 的屏幕尺寸不同)。现在这个函数将返回两个浮点值,作为屏幕尺寸和背景尺寸在宽度和高度上的比例。在将它们设置为backgroundNode.xScale和backgroundNode.yScale之后,如果你运行这段代码,你将得到与之前相同的结果。
与纹理对象一起工作
当创建精灵时,Sprite Kit 也会创建一个纹理。但有时我们需要纹理来完成一些复杂的工作,例如:
-
更改精灵
-
动画
-
在多个精灵之间使用相同的纹理
-
将节点树渲染成纹理,就像屏幕截图一样
为了简化,Sprite Kit 为我们提供了SKTexture类。我们可以创建这个类的对象,并按需使用它。
打开你的MenuScene.swift文件,并创建一个SKTexture的引用:
let testingTexture : SKTexture
Now initialize it inside init code block
init(size:CGSize, playbutton:String, background:String) {
PlayButton = SKSpriteNode(imageNamed: playbutton)
Background = SKSpriteNode(imageNamed: background)
MyPlayButton = SKSpriteNode(imageNamed: playbutton)
testingTexture = SKTexture(imageNamed: playbutton)
super.init(size:size)
}
让我们创建一个函数调用generateTestTexture,并在didMoveToView中调用它:
override func didMoveToView(view: SKView) {
addChildToScene();
PlayButton.name = "PLAY"
generateTestTexture()
}
func generateTestTexture(){
for var i = 0 ; i < 10; i++ {
var temp = SKSpriteNode(texture: testingTexture)
temp.xScale = 100/temp.size.width
temp.yScale = 50/temp.size.height
temp.zPosition = 2
temp.position = CGPointMake(-self.size.width + CGFloat (100 * i), -self.size.height/2)
addChild(temp)
}
}
运行它,你将看到一系列的播放纹理。我们只使用一个纹理就制作了这些纹理。之前,我们是从图像名称创建SKSpriteNode对象,允许 Sprite Kit 创建纹理。现在,我们将纹理分配给由我们创建的SKNode对象。现在,由于我们对精灵做了许多自定义,让我们看看纹理图集。
什么是纹理图集?
一个游戏的表现取决于其中使用的精灵数量。精灵数量越少,性能越好。为此,Sprite Kit 提供了纹理图集,它自动将我们的图像文件打包到一个或多个大图像中。
它为我们提供了一种通过单次绘制调用绘制多个图像来提高游戏性能的方法。当游戏处于开发阶段时,编译器会遍历每个文件夹以查找具有 *.atlas 格式的文件夹。当这些文件夹被识别后,它们内部的全部图像将合并成一个或多个大图像文件。因此,如果你想使用这个功能,请将你的图像放入一个文件夹中,然后通过在其名称后添加 .atlas 后缀来重命名它。
现在,我们将向 GameScene 添加一个玩家。让我们将玩家的所有空闲状态图像移动到一个文件夹中,并将其命名为 idle.atlas。
现在在 Xcode 中,在 Project Navigator 中,右键单击你的项目并选择 Add to Project:

选择目录(不是文件),然后点击 Add。默认值应该是 OK,但请确保它设置为复制。

现在我们将向我们的 GameScene 添加一个玩家。打开 GameScene 并创建一个函数,名为 addPlayer:
func addPlayer(){
var player = SKSpriteNode(imageNamed:"bro5_idle0001")
player.position = CGPoint(x:0,y:0)
player.zPosition = 2;
self.addChild(player)
}
在 addSpriteWithoutTexture 函数上注释并调用 addPlayer 函数。你的函数将如下所示:
override func didMoveToView(view: SKView) {
addBackGround()
//addSpriteWithoutTexture()
addPlayer();
}
将代码中的 touchesBegan 部分转换为注释,这样在发生触摸时我们不会卡住;否则,玩家的图像可能会阻挡触摸:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches{
let location = touch.locationInNode(self)
let node = self.nodeAtPoint(location)
// if node.name == spriteWithoutTexture!.name {
// currentno++
// changeColor()
// changePosition()
//}
}
}
现在,运行它并查看。你将看到玩家位于屏幕中央。
在此代码中,我们通过传递图像名称来创建一个 SKSpriteNode 实例。它将搜索该图像的图集。但如果我们的项目中有一个同名图像,则将加载该图像而不是纹理图集。你必须使用你图像的任何名称。
当我们将图像放入一个以 .atlas 扩展名结尾的文件夹中时,Xcode 通过将所有图像合并成一个来生成一个或多个大图像。
-
要查看打包的图像,请转到你的项目中的
Products文件夹。 -
在那里的
.app文件上右键单击,然后点击 Show in Finder,这样我们就可以进入它的目录。 -
现在,在
.app文件上右键单击并选择 Show Package Contents。 -
然后,转到 Contents | Resources |
*.atlasc。在这里,你会看到两个文件,一个图像和一个 plist。如果你查看图像,你会发现图像被合并成一个纹理,其高度和宽度都是 2 的幂。如果你打开 plist,你会看到它包含了打包纹理中图像的位置,这样我们就可以直接使用 Texture Atlas 访问它们。
你也可以访问纹理图集。让我们使用 TextureAtlas 并在点击时做些事情:
-
首先,在
GameScene中创建一个纹理图集引用:let myAtlas = SKTextureAtlas(named:"idle.atlas") -
之后,在函数外部创建一个玩家引用,这样我们就可以在另一个函数中使用它:
var player :SKSpriteNode? -
现在按照以下方式编辑
addPlayer函数:func addPlayer(){ player = SKSpriteNode(texture: myAtlas.textureNamed("bro5_idle0001")) player!.position = CGPoint(x:0,y:0) player!.zPosition = 2; player!.name = "Player" self.addChild(player!) } -
创建函数
changeSpriteFromTextureAtlas(),并在touchesBegan中调用它。现在,它应该看起来像以下几行:override func touchesBegan(touches: NSSet, withEvent event: UIEvent) { for touch: AnyObject in touches{ let location = touch.locationInNode(self) let node = self.nodeAtPoint(location) if node.name == player!.name { currentno++ changeSpriteFromTextureAtlas() } } } func changeSpriteFromTextureAtlas(){ switch(currentno%3){ case 0: player!.texture = myAtlas.textureNamed("bro5_idle0001") case 1: player!.texture = myAtlas.textureNamed("bro5_idle0004") case 2: player!.texture = myAtlas.textureNamed("bro5_idle0007") default : break } } -
现在,运行并点击玩家。你会看到玩家在点击时改变了它的精灵。
我们创建了SKTextureAtlas引用。我们将其命名为atlas文件并将其添加到项目中。现在我们可以从纹理图集文件中获取图像。这是我们可以直接访问纹理图集的方式。纹理图集对于制作动画序列或从瓦片生成关卡非常有用。我们将在后续章节中讨论动画。
现在,由于我们正在使用纹理来制作精灵,有时我们可能需要将纹理预加载到内存中。让我们详细讨论这个问题。
预加载纹理到内存中
Sprite Kit 在内存管理方面表现良好。当一个纹理需要在场景中渲染,但尚未准备时,Sprite Kit 将其加载到内存中,并通过将其转换为可用形式上传到图形硬件。如果需要一次性加载许多未加载的纹理,可能会减慢游戏速度。为了避免这种情况,我们需要在使用之前预加载纹理,尤其是在更大或更复杂的游戏中。
当用户需要从关卡屏幕转到游戏屏幕时,可能会出现这个问题。由于游戏屏幕可能有多个纹理,它需要加载,并且由于纹理的加载可能会变慢。为了避免这种情况,我们可以使用SKTexture class preloadTextures(_:withCompletionHandler:)函数。它接受一个SKTexture数组和一个块,该块在所有纹理加载完成后被调用。因此,我们可以使用这个块来加载一个场景。
对于一个小游戏,我们可以在游戏启动时一次性加载所有纹理。对于一个大游戏,我们需要根据场景和其他标准将纹理分成不同的级别。对于对某个级别没有用的纹理,我们将丢弃它们以节省内存。如果游戏太大,我们还需要在游戏运行时动态加载纹理。
在加载纹理的同时,我们还需要从内存中删除不必要的纹理。当一个纹理被加载到内存中时,它将保留在那里,直到其引用SKTexture对象被删除。要删除SKTexture对象,我们必须从它那里移除引用;这将使纹理从内存中卸载。
概述
在本章中,我们详细了解了精灵。我们学习了如何初始化一个精灵,以及精灵的大小和位置。我们还学习了精灵的各种颜色属性。还讨论了光照和着色器属性。最后,我们讨论了Texture对象、纹理图集的使用以及纹理的预加载到内存中。
在下一章中,我们将学习关于节点和树节点结构的各种其他概念。
第四章。Sprite Kit 中的节点
在上一章中,我们学习了如何在游戏中以各种方式使用精灵。我们讨论了精灵的物理属性、精灵的纹理以及诸如颜色属性、光照、着色器等各种其他属性。我们还学习了与纹理对象一起工作,并对纹理图集进行了介绍。
在上一章中,我们实现了SKSprite类,它是SKNode类的子类;这就是为什么SKSprite本身就是一个节点,继承了SKNode属性。在本章中,我们将研究节点,它们在理解游戏树结构中起着重要作用。此外,我们将详细讨论 Sprite Kit 中的节点类型及其用途。
你需要了解的所有关于节点的内容
到目前为止,我们已经讨论了许多关于节点的内容。几乎你在使用 Sprite Kit 制作的游戏中所做的每一件事都是一个节点。我们展示给查看的场景是SKScene类的实例,它是SKEffectNode类的子类,而SKEffectNode类本身又是SKNode类的子类。间接地,SKScene是SKNode类的子类。
由于游戏遵循节点树结构,场景就像一个根节点,其他节点则用作其子节点。应该记住,尽管SKNode是场景中看到的节点的基类,但它本身并不绘制任何内容。它只为它的子节点提供一些基本功能。我们在 Sprite Kit 制作的游戏中看到的所有视觉内容,都是通过使用适当的SKNode子类来绘制的。
以下是一些SKNode类的子类,它们用于 Sprite Kit 游戏中的不同行为:
-
SKSpriteNode:这个类用于在游戏中实例化纹理精灵;这是在第三章 精灵 中经常提到的熟悉节点类。SKVideoNode,这个类用于在场景中播放视频内容。 -
SKLabelNode:这个类用于在游戏中绘制标签,具有许多自定义选项,例如字体类型、字体大小、字体颜色等。 -
SKShapeNode:这个类用于在运行时基于路径创建形状。例如,绘制线条或制作绘图游戏。 -
SKEmitterNode:这个类用于在场景中发射粒子效果,具有许多选项,例如位置、粒子数量、颜色等。 -
SKCropNode:这个类基本上用于使用遮罩裁剪其子节点。使用它,你可以选择性地屏蔽图层的一部分。 -
SKEffectNode:SKEffectNode是SKScene类的父类,也是SKNode类的子类。它用于对其子节点应用图像滤镜。 -
SKLightNode:SKLightNode类用于在场景中创建光和阴影效果。 -
SKFieldNode:这是 Sprite Kit 的一个有用功能。你可以定义场景的一部分并赋予其一些物理属性,例如在太空游戏中,对黑洞施加引力效果,吸引附近的物体。
因此,这些都是 Sprite Kit 中常用到的 SKNode 的基本子类。SKNode 为其子类提供了一些基本属性,用于在场景中查看节点,例如:
-
position: 这设置了场景中节点的位置 -
xScale: 这在节点的宽度上进行缩放 -
yScale: 这在节点的高度上进行缩放 -
zRotation: 这有助于节点按顺时针或逆时针方向旋转 -
frame:frame是一个包含节点内容的矩形,包括其 x 缩放、y 缩放和 z 旋转属性,忽略节点的子节点
我们知道 SKNode 类本身不绘制任何内容。那么,它的用途是什么呢?嗯,我们可以使用 SKNode 实例来分别管理不同层的其他节点,或者我们可以使用它们来管理同一层中的不同节点。让我们看看我们如何做到这一点。
在游戏中使用 SKNode 对象
现在,我们将发现 SKNode 的各种方面用途。比如说,你需要从精灵的不同部分制作一个身体,比如一辆车。你可以从车轮和车身精灵制作它。车轮和车身的车在同步运行,所以一个可以控制它们的动作,而不是分别管理每个部分。这可以通过将它们添加为 SKNode 类对象的子节点并更新此节点来控制汽车的活动来实现。
SKNode 类对象可以用作游戏中分层的目的。假设我们在游戏中有三个层:前景层,代表前景精灵;中间层,代表中间精灵;背景层,代表背景精灵。
如果我们想在游戏中实现视差效果,我们必须分别更新每个精灵的位置,或者我们可以制作三个 SKNode 对象,分别对应每一层,并将精灵添加到相应的节点中。现在我们只需要更新这三个节点的位置,精灵将自动更新它们的位置。
SKNode 类可以用作游戏中的一种检查点,它是隐藏的,但在玩家穿过它们时执行或触发某些事件,例如关卡结束、奖励或死亡陷阱。
我们可以在节点内部移除或添加整个子树,并执行必要的功能,如旋转、缩放、定位等。
好吧,正如我们描述的那样,我们可以使用 SKNode 对象作为游戏中的检查点,因此在你的场景中识别它们是很重要的。那么,我们该如何做呢?嗯,SKNode 类提供了一个属性来实现这一点。让我们更深入地了解它。
识别一个节点
SKNode 类提供了一个带有名称的属性,用于识别正确的节点。它接受字符串作为参数。你可以通过名称搜索节点,或者可以使用 SKNode 提供的两个方法之一,如下所示:
-
func childNodeWithName(name:String) -> SKNode: 这个函数接受一个字符串作为参数,如果找到一个具有特定名称的节点,则返回该节点,否则返回 nil。如果有多个节点具有相同的名称,则返回搜索中的第一个节点。 -
func enumerateChildNodesWithName(name:String, usingBlock:((SKNode!,UnsafeMutablePointer<ObjCBool>)->Void)!): 当你需要所有具有相同名称的节点时,请使用此函数。此函数接受名称和块作为参数。在usingBlock中,您需要提供两个参数。一个是匹配的节点,另一个是布尔类型的指针。在我们的游戏中,如果您还记得,我们使用PlayButton中的name属性来识别当用户点击时识别的节点。这是一个非常有用的属性,用于搜索所需的节点。
因此,让我们快速查看SKNode类的其他属性或方法。
初始化一个节点
有两个初始化器可以创建SKNode的实例。这两个初始化器在 iOS 8.0 或更高版本中都是可用的。
-
convenience init (fileNamed filename: String): 这个初始化器用于通过从主包中加载存档文件来创建节点。为此,您必须在主包中传递一个具有sks扩展名的文件名。 -
init(): 它用于创建一个不带任何参数的简单节点。这在游戏中的分层用途中非常有用。
如我们之前讨论过的节点的定位,让我们讨论一些用于构建节点树的函数和属性。
构建节点树
SKNode提供了一些函数和属性来处理节点树。以下是一些函数:
-
addChild(node:SKNode): 这是一个非常常见的函数,主要用于创建节点树结构。我们之前已经用它向场景中添加节点。 -
insertChild(node:SKNode,atIndex index: Int): 这用于在数组中插入一个子节点到特定位置。 -
removeFromParent(): 这只是简单地从一个节点中移除。 -
removeAllChildren(): 这用于清除节点中的所有子节点。 -
removeChildrenInArray(nodes:[AnyObject]!): 它接受一个SKNode对象的数组,并将其从接收节点中移除。 -
inParentHierarchy(parent:SKNode) -> Bool: 它接受一个SKNode对象作为接收节点的父节点进行检查,并返回一个布尔值来表示该条件。
在节点树中,有一些有用的属性,如下所示:
-
children: 这是一个只读属性。它包含接收节点在数组中的子节点。 -
parent: 这也是一个只读属性。它包含接收节点的父节点的引用,如果没有父节点,则返回 nil。 -
scene: 这也是一个只读属性。如果节点嵌入在场景中,它将包含场景的引用,否则为 nil。
在游戏中,我们需要对节点执行一些特定任务,例如将其位置从一个点移动到另一个点,按顺序更改精灵等。这些任务是通过节点上的动作完成的。现在让我们来谈谈它们。
节点树上的动作
游戏中的一些特定任务需要动作。为此,SKNode类提供了一些基本函数,如下所示。
-
runAction(action:SKAction!): 此函数接受一个SKAction类对象作为参数,并在接收节点上执行该动作。 -
runAction(action:SKAction!,completion block: (() -> Void)!): 此函数接受一个SKAction类对象和一个编译块作为对象。当动作完成时,它调用该块。 -
runAction(action:SKAction,withKey key:String!): 此函数接受一个SKAction类对象和一个唯一键,以识别此动作并在接收节点上执行。 -
actionForKey(key:String) -> SKAction?: 它接受一个String键作为参数,并返回一个关联的SKAction对象,用于该键标识符。如果存在,则发生这种情况,否则返回 nil。 -
hasActions() -> Bool: 通过此操作,如果节点有任何正在执行的动作,则返回true,否则返回false。 -
removeAllActions(): 此函数从接收节点中移除所有动作。 -
removeActionForKey(key:String): 它接受String名称作为键,并移除与该键关联的动作(如果存在)。
控制这些动作的一些有用属性如下:
-
speed: 这用于加快或减慢动作运动的速度。默认值为1.0以正常速度运行;随着值的增加,速度增加。 -
paused: 这个布尔值确定节点上的动作是否应该暂停或恢复。
有时,我们需要根据场景中的节点更改一个点坐标系统。SKNode类提供了两个函数来交换一个点相对于场景中节点的坐标系统。让我们来谈谈它们。
节点的坐标系
我们可以转换任何节点树的坐标系中的点。执行此操作的功能如下:
-
convertPoint(point:CGPoint, fromNode node : SKNode) -> CGPoint: 它接受另一个节点坐标系统中的一个点以及另一个节点作为其参数,并返回根据接收节点坐标系统转换后的点。 -
convertPoint(point:CGPoint, toNode node:SKNode) ->CGPoint: 它接受接收节点坐标系中的一个点以及节点树中的其他节点作为其参数,并返回根据其他节点坐标系转换后的相同点。
我们还可以确定一个点是否在节点区域内或不在。
-
containsPoint(p:CGPoint) -> Bool: 它根据点的位置返回布尔值,该点位于接收节点的边界框内部或外部。 -
nodeAtPoint(p:CGPoint) -> SKNode:此函数返回与点相交的最深子节点。如果没有这样的节点,则返回接收节点。 -
nodesAtPoint(p:CGPoint) -> [AnyObject]:此函数返回一个数组,包含所有在子树中与点相交的SKNode对象。如果没有节点与点相交,则返回一个空数组。
除了这些之外,SKNode 类还提供了一些其他函数和属性。让我们来谈谈它们。
其他函数和属性
SKNode 类的其他一些函数和属性如下:
-
intersectsNode(node:SKNode) -> Bool:正如其名所示,它根据接收节点与函数参数中的另一个节点的交集返回一个布尔值。 -
physicsBody:它是SKNode类的一个属性。默认值是 nil,这意味着此节点不会参与场景中的任何物理模拟。如果它包含任何物理体,则其位置和旋转将根据场景中的物理模拟进行更改。 -
userData : NSMutableDictionary?:userData属性用于以字典形式存储节点的数据。我们可以在其中存储位置、旋转以及许多关于节点的自定义数据集。 -
constraints: [AnyObject]?:它包含一个约束SKConstraint对象数组到接收节点。约束用于限制节点在场景内的位置或旋转。 -
reachConstraints: SKReachConstraints?:这基本上用于通过创建一个SKReachConstraints对象来为接收节点设置限制值。例如,使关节在人体中移动。 -
节点混合模式:
SKNode类声明了一个enum SKBlendMode的int类型,用于通过源像素颜色和目标像素颜色混合接收节点的颜色。用于此的常量如下:-
Alpha:用于通过乘以源 alpha 值混合源颜色和目标颜色 -
Add:用于将源颜色和目标颜色相加 -
Subtract:用于从目标颜色中减去源颜色 -
Multiply:用于将源颜色乘以目标颜色 -
MultiplyX2:用于将源颜色乘以目标颜色,然后双倍结果颜色 -
Screen:用于分别乘以反转的源颜色和目标颜色,然后反转最终结果颜色 -
Replace:用于用源颜色替换目标颜色
-
-
calculateAccumulatedFrame()->CGRect:我们知道节点本身不会绘制任何内容,但如果节点有绘制内容的子节点,那么我们可能需要知道该节点的整体框架大小。此函数计算包含接收节点及其所有子节点内容的框架。
现在,我们准备看看一些基本的 SKNode 子类在实际中的应用。我们将讨论的类如下:
-
SKLabelNode -
SKCropNode -
SKShapeNode -
SKEmitterNode -
SKLightNode -
SKVideoNode
为了研究这些类,我们将在项目中创建六个不同的 SKScene 子类,这样我们就可以分别学习它们。
现在,我们已经详细学习了节点,我们可以进一步利用节点在游戏中的概念。
为我们的平台游戏创建子类
在理解了节点的理论知识后,人们会想知道这个概念在开发游戏中有何帮助。为了理解使用节点概念开发游戏,我们现在继续编写和执行我们的 平台游戏 的代码。
按照给定的步骤在 Xcode 中创建不同节点的子类:
-
从主菜单中选择 新建文件 | Swift | 另存为 | NodeMenuScene.swift:
确保将 Platformer 标记为目标。现在 创建 并 打开,通过继承
SKScene创建NodeMenuScene类。 -
按照之前的相同步骤,分别创建
CropScene、ShapeScene、ParticleScene、LightScene和VideoNodeScene文件。 -
打开
GameViewController.swift文件,并用以下代码替换viewDidLoad函数:override func viewDidLoad() { super.viewDidLoad() let menuscene = NodeMenuScene() let skview = view as SKView skview.showsFPS = true skview.showsNodeCount = true skview.ignoresSiblingOrder = true menuscene.scaleMode = .ResizeFill menuscene.anchorPoint = CGPoint(x: 0.5, y: 0.5) menuscene.size = view.bounds.size skview.presentScene(menuscene) }
在这段代码中,我们只是从 GameViewController 类中调用了我们的 NodeMenuScene 类。现在,是时候向 NodeMenuScene 类添加一些代码了。
NodeMenuScene
打开 NodeMenuScene.swift 文件,并输入如下所示的代码。不要担心代码的长度;因为这段代码是为了创建节点菜单屏幕,大多数函数都与创建按钮类似:
import Foundation
import SpriteKit
let BackgroundImage = "BG"
let FontFile = "Mackinaw1"
let sKCropNode = "SKCropNode"
let sKEmitterNode = "SKEmitterNode"
let sKLightNode = "SKLightNode"
let sKShapeNode = "SKShapeNode"
let sKVideoNode = "SKVideoNode"
class NodeMenuScene: SKScene {
let transitionEffect = SKTransition.flipHorizontalWithDuration(1.0)
var labelNode : SKNode?
var backgroundNode : SKNode?
override func didMoveToView(view: SKView) {
backgroundNode = getBackgroundNode()
backgroundNode!.zPosition = 0
self.addChild(backgroundNode!)
labelNode = getLabelNode()
labelNode?.zPosition = 1
self.addChild(labelNode!)
}
func getBackgroundNode() -> SKNode {
var bgnode = SKNode()
var bgSprite = SKSpriteNode(imageNamed: "BG")
bgSprite.xScale = self.size.width/bgSprite.size.width
bgSprite.yScale = self.size.height/bgSprite.size.height
bgnode.addChild(bgSprite)
return bgnode
}
func getLabelNode() -> SKNode {
var labelNode = SKNode()
var cropnode = SKLabelNode(fontNamed: FontFile)
cropnode.fontColor = UIColor.whiteColor()
cropnode.name = sKCropNode
cropnode.text = sKCropNode
cropnode.position = CGPointMake(CGRectGetMinX(self.frame)+cropnode.frame.width/2, CGRectGetMaxY(self.frame)-cropnode.frame.height)
labelNode.addChild(cropnode)
var emitternode = SKLabelNode(fontNamed: FontFile)
emitternode.fontColor = UIColor.blueColor()
emitternode.name = sKEmitterNode
emitternode.text = sKEmitterNode
emitternode.position = CGPointMake(CGRectGetMinX(self.frame) + emitternode.frame.width/2 , CGRectGetMidY(self.frame) - emitternode.frame.height/2)
labelNode.addChild(emitternode)
var lightnode = SKLabelNode(fontNamed: FontFile)
lightnode.fontColor = UIColor.whiteColor()
lightnode.name = sKLightNode
lightnode.text = sKLightNode
lightnode.position = CGPointMake(CGRectGetMaxX(self.frame) - lightnode.frame.width/2 , CGRectGetMaxY(self.frame) - lightnode.frame.height)
labelNode.addChild(lightnode)
var shapetnode = SKLabelNode(fontNamed: FontFile)
shapetnode.fontColor = UIColor.greenColor()
shapetnode.name = sKShapeNode
shapetnode.text = sKShapeNode
shapetnode.position = CGPointMake(CGRectGetMaxX(self.frame) - shapetnode.frame.width/2 , CGRectGetMidY(self.frame) - shapetnode.frame.height/2)
labelNode.addChild(shapetnode)
var videonode = SKLabelNode(fontNamed: FontFile)
videonode.fontColor = UIColor.blueColor()
videonode.name = sKVideoNode
videonode.text = sKVideoNode
videonode.position = CGPointMake(CGRectGetMaxX(self.frame) - videonode.frame.width/2 , CGRectGetMinY(self.frame) )
labelNode.addChild(videonode)
return labelNode
}
var once:Bool = true
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
if !once {
return
}
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
let node = self.nodeAtPoint(location)
if node.name == sKCropNode {
once = false
var scene = CropScene()
scene.anchorPoint = CGPointMake(0.5, 0.5)
scene.scaleMode = .ResizeFill
scene.size = self.size
self.view?.presentScene(scene, transition:transitionEffect)
}
else if node.name == sKEmitterNode {
once = false
var scene = ParticleScene()
scene.anchorPoint = CGPointMake(0.5, 0.5)
scene.scaleMode = .ResizeFill
scene.size = self.size
self.view?.presentScene(scene, transition:transitionEffect)
}
else if node.name == sKLightNode {
once = false
var scene = LightScene()
scene.scaleMode = .ResizeFill
scene.size = self.size
scene.anchorPoint = CGPointMake(0.5, 0.5)
self.view?.presentScene(scene , transition:transitionEffect)
}
else if node.name == sKShapeNode {
once = false
var scene = ShapeScene()
scene.scaleMode = .ResizeFill
scene.size = self.size
scene.anchorPoint = CGPointMake(0.5, 0.5)
self.view?.presentScene(scene, transition:transitionEffect)
}
else if node.name == sKVideoNode {
once = false
var scene = VideoNodeScene()
scene.scaleMode = .ResizeFill
scene.size = self.size
scene.anchorPoint = CGPointMake(0.5, 0.5)
self.view?.presentScene(scene , transition:transitionEffect)
}
}
}
}
我们将从之前的代码中获得以下屏幕:

执行 NodeMenuScene.swift 文件时,我们获得了屏幕。
在前面的代码中,在 import 语句之后,我们定义了一些 String 变量。我们将使用这些变量作为场景中的 Label 名称。我们还添加了我们的字体名称作为一个字符串变量。在这个类内部,我们创建了两个节点引用:一个用于背景,另一个用于我们将在这个场景中使用的那些标签。我们使用这两个节点来制作游戏中的层。最好将场景中的节点分类,这样我们可以优化代码。我们创建了一个 SKTransition 对象引用,用于翻转水平效果。您也可以使用其他过渡效果。
在 didMoveToView() 函数内部,我们只是获取节点并将其添加到我们的场景中,并设置它们的 z 位置。
现在,如果我们查看 getBackgroundNode() 函数,我们可以看到我们通过 SKNode 类实例创建了一个节点,通过 SKSpriteNode 类实例创建了一个背景,并将其添加到节点中并返回。如果您看到这个函数的语法,您会看到 -> SKNode。这意味着这个函数返回一个 SKNode 对象。
在函数 getLabelNode() 中也是同样的情况。它也返回一个包含所有 SKLabelNode 类对象的节点。我们为这些标签指定了字体和名称,并在屏幕上设置了它们的位置。SKLabelNode 类用于在 Sprite Kit 中创建具有许多可定制选项的标签。
在 touchBegan() 函数中,我们获取被触摸的标签信息,然后调用带有过渡效果的适当场景。
通过这种方式,我们已经创建了一个带有过渡效果的场景。通过点击每个按钮,你可以看到过渡效果。
CropScene
在这个场景中,我们将使用 SKCropNode 类对象。这个类用于在另一个节点上遮罩一个节点。我们将使用我们的游戏精灵作为遮罩,并将背景图像作为根据遮罩区域渲染的图像。打开 CropScene.swift 文件,并输入以下代码,如下所示:
import Foundation
import SpriteKit
class CropScene : SKScene {
var play : SKSpriteNode?
override func didMoveToView(view: SKView) {
play = SKSpriteNode(imageNamed: "Play")
var crop = SKCropNode()
crop.maskNode = play
crop.addChild(SKSpriteNode(imageNamed: "BG"))
addChild(crop)
addBackLabel()
}
func addBackLabel() {
var backbutton = SKLabelNode(fontNamed: FontFile)
backbutton.fontColor = UIColor.blueColor()
backbutton.name = "BACK"
backbutton.text = "BACK"
backbutton.position = CGPointMake(CGRectGetMinX(self.frame) + backbutton.frame.width/2 , CGRectGetMinY(self.frame))
self.addChild(backbutton)
}
var once:Bool = true
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
let node = self.nodeAtPoint(location)
if node.name == "BACK" {
if once {
once = false
let transitionEffect = SKTransition.flipHorizontalWithDuration(1.0)
var scene = NodeMenuScene()
scene.anchorPoint = CGPointMake(0.5, 0.5)
scene.scaleMode = .ResizeFill
scene.size = self.size
self.view?.presentScene(scene, transition:transitionEffect)
}
}
}
}
}
使用前面的代码,我们将得到以下屏幕:

执行 Cropscene.swift 文件时,我们得到前面的屏幕。
在此代码中,我们仅为 SKLabelNode 类对象的后退按压添加了一个标签。
在这个类中,我们将游戏图像添加到 SKCropNode 对象的遮罩节点中,并为这个裁剪节点添加了一个背景。如果你在 NodeMenuScene 中点击 SKCropNode 标签,你会看到游戏图像作为遮罩覆盖在背景图像上。
ShapeScene
现在,打开 ShapeScene.swift 文件,并添加以下代码以创建 SKShapeNode 类:
import Foundation
import SpriteKit
class ShapeScene : SKScene {
override func didMoveToView(view: SKView) {
var shape = SKShapeNode()
var path = CGPathCreateMutable()
CGPathMoveToPoint(path, nil, 0, 0)
CGPathAddLineToPoint(path, nil, 10 , 100)
CGPathAddLineToPoint(path, nil, 20, 0)
CGPathAddLineToPoint(path, nil, 10, -10)
CGPathAddLineToPoint(path, nil, 0, 0)
shape.path = path
shape.fillColor = UIColor.redColor()
shape.lineWidth = 4
addChild(shape)
addBackLabel()
}
func addBackLabel() {
var backbutton = SKLabelNode(fontNamed: FontFile)
backbutton.fontColor = UIColor.blueColor()
backbutton.name = "BACK"
backbutton.text = "BACK"
backbutton.position = CGPointMake(CGRectGetMinX(self.frame) + backbutton.frame.width/2 , CGRectGetMinY(self.frame))
self.addChild(backbutton)
}
var once:Bool = true
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
let node = self.nodeAtPoint(location)
if node.name == "BACK" {
if once {
once = false
let transitionEffect = SKTransition.flipHorizontalWithDuration(1.0)
var scene = NodeMenuScene()
scene.anchorPoint = CGPointMake(0.5, 0.5)
scene.scaleMode = .ResizeFill
scene.size = self.size
self.view?.presentScene(scene, transition:transitionEffect)
}
}
}
}
}
使用前面的代码,我们将得到以下屏幕:

执行 ShapeScene.swift 文件时,我们得到此屏幕。
SKShapeNode 类主要用于在场景中创建运行时图形。在这个例子中,我们创建了一个由四条线组成的图形,然后使用 fillColor 属性填充了颜色。
ParticleScene
现在,打开 ParticleScene.swift 文件,并添加以下代码以创建 SKEmitterNode 类:
import Foundation.
import SpriteKit
class ParticleScene : SKScene {
var emitternode :SKEmitterNode?
override func didMoveToView(view: SKView) {
var path = NSBundle.mainBundle().pathForResource("MagicParticle", ofType: "sks")
emitternode = NSKeyedUnarchiver.unarchiveObjectWithFile(path!) as? SKEmitterNode
self.addChild(emitternode!)
addBackLabel()
}
func addBackLabel() {
var backbutton = SKLabelNode(fontNamed: FontFile)
backbutton.fontColor = UIColor.blueColor()
backbutton.name = "BACK"
backbutton.text = "BACK"
backbutton.position = CGPointMake(CGRectGetMinX(self.frame) + backbutton.frame.width/2 , CGRectGetMinY(self.frame))
self.addChild(backbutton)
}
var once:Bool = true
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch:AnyObject in touches {
var location = touch.locationInNode(self)
emitternode?.position = location
let node = self.nodeAtPoint(location)
if node.name == "BACK" {
if once {
once = false
let transitionEffect = SKTransition.flipHorizontalWithDuration(1.0)
var scene = NodeMenuScene()
scene.anchorPoint = CGPointMake(0.5, 0.5)
scene.scaleMode = .ResizeFill
scene.size = self.size
self.view?.presentScene(scene, transition:transitionEffect)
}
}
}
}
}
使用前面的代码,我们得到以下屏幕:

执行 ParticleScene.swift 文件时,我们得到此屏幕。
我们使用了 SKEmitterNode 类对象来创建粒子效果。Sprite Kit 提供了许多预定义的粒子效果。你可以根据需求进行自定义。要创建粒子效果,请按照以下步骤操作:
-
右键点击项目资源管理器,新建文件 | 资源 | SpriteKit 粒子文件。
-
从列表中选择一个粒子模板,然后点击 下一步。
-
另存为,给你的粒子系统命名。在我们的项目中,我们将其命名为
MagicParticle。确保在点击 创建 按钮之前,在 目标 选项中选择了 Platformer(项目)。
在项目导航器中,屏幕左侧,你会看到MagicParticle.sks文件。如果你点击这个文件,你可以在编辑器窗口中看到粒子效果。现在,在右侧面板中,有许多选项可供选择,如粒子、颜色、形状等。你可以根据自己的喜好选择任何值。
LightScene
现在,打开LightScene.swift文件,添加以下代码以创建SKLightNode类:
import Foundation
import SpriteKit
class LightScene : SKScene {
var lightNode : SKLightNode?
override func didMoveToView(view: SKView) {
var background = SKSpriteNode(imageNamed: "BG")
background.zPosition = 0.5
var scaleX = self.size.width/background.size.width
var scaleY = self.size.height/background.size.height
background.xScale = scaleX
background.yScale = scaleY
addChild(background)
println(background.size)
var playbutton = SKSpriteNode(imageNamed: "Play")
playbutton.zPosition = 1
playbutton.size = CGSizeMake(100, 100)
playbutton.position = CGPointMake(-200, 0)
addChild(playbutton)
var playbutton2 = SKSpriteNode(imageNamed: "Play")
playbutton2.zPosition = 1
playbutton2.size = CGSizeMake(100, 100)
playbutton2.position = CGPointMake(0, 100)
addChild(playbutton2)
var playbutton3 = SKSpriteNode(imageNamed: "Play")
playbutton3.zPosition = 1
playbutton3.size = CGSizeMake(100, 100)
playbutton3.position = CGPointMake(200, 0)
addChild(playbutton3)
lightNode = SKLightNode()
lightNode!.categoryBitMask = 1
lightNode!.falloff = 1
lightNode!.ambientColor = UIColor.greenColor()
lightNode!.lightColor = UIColor.redColor()
lightNode!.shadowColor = UIColor.blueColor()
lightNode!.zPosition = 1
addChild(lightNode!)
playbutton.shadowCastBitMask = 1
playbutton2.shadowCastBitMask = 1
playbutton3.shadowCastBitMask = 1
background.lightingBitMask = 1;
addBackLabel()
}
func addBackLabel() {
var backbutton = SKLabelNode(fontNamed: FontFile)
backbutton.fontColor = UIColor.blueColor()
backbutton.name = "BACK"
backbutton.text = "BACK"
backbutton.position = CGPointMake(CGRectGetMinX(self.frame) + backbutton.frame.width/2 , CGRectGetMinY(self.frame))
backbutton.zPosition = 3
self.addChild(backbutton)
}
var once:Bool = true
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
for touch : AnyObject in touches {
let location = touch.locationInNode(self)
lightNode!.position = location
let node = self.nodeAtPoint(location)
if node.name == "BACK" {
if once {
once = false
let transitionEffect = SKTransition.flipHorizontalWithDuration(1.0)
var scene = NodeMenuScene()
scene.anchorPoint = CGPointMake(0.5, 0.5)
scene.scaleMode = .ResizeFill
scene.size = self.size
self.view?.presentScene(scene, transition:transitionEffect)
}
}
}
}
}
使用前面的代码,我们将获得以下屏幕:

上一屏是在执行LightScene.swift文件时获得的
在这个类中,我们使用了一个光源并设置了位掩码到图像上。如果你运行项目,你会看到背景颜色受到光源的影响,其他播放图像在相反方向上投射阴影。如果你点击场景,光源将改变其位置,阴影也会根据光源改变自己。
VideoNodeScene
现在,打开VideoNodeScene.swift文件,添加以下代码以创建SKVideoNode类:
import Foundation
import SpriteKit
import AVFoundation
class VideoNodeScene : SKScene {
var playonce :Bool = false
var videoNode : SKVideoNode?
override func didMoveToView(view: SKView) {
var background = SKSpriteNode(imageNamed: "BG")
background.zPosition = 0
var scaleX = self.size.width/background.size.width
var scaleY = self.size.height/background.size.height
background.xScale = scaleX
background.yScale = scaleY
addChild(background)
var fileurl = NSURL.fileURLWithPath(NSBundle.mainBundle().pathForResource
("Movie", ofType: "m4v")!)
var player = AVPlayer(URL: fileurl)
videoNode = SKVideoNode(AVPlayer: player)
videoNode?.size = CGSizeMake(200, 150)
videoNode?.zPosition = 1
videoNode?.name = "Video"
self.addChild(videoNode!)
addBackLabel()
}
func addBackLabel() {
var backbutton = SKLabelNode(fontNamed: FontFile)
backbutton.fontColor = UIColor.blueColor()
backbutton.name = "BACK"
backbutton.text = "BACK"
backbutton.position = CGPointMake(CGRectGetMinX(self.frame) + backbutton.frame.width/2 , CGRectGetMinY(self.frame))
self.addChild(backbutton)
}
var once:Bool = true
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
let node = self.nodeAtPoint(location)
if node.name == videoNode?.name {
if !playonce {
videoNode?.play()
playonce = true
}
}
if node.name == "BACK" {
if once {
once = false
let transitionEffect = SKTransition.flipHorizontalWithDuration(1.0)
var scene = NodeMenuScene()
scene.anchorPoint = CGPointMake(0.5, 0.5)
scene.scaleMode = .ResizeFill
scene.size = self.size
self.view?.presentScene(scene, transition:transitionEffect)
}
}
}
}
}
我们将获得以下屏幕:

执行LightScene.swift文件时,将获得以下屏幕
在我们的场景中使用音频和视频,我们在代码中导入了AVFoundation。我们在项目中添加了一个.m4v格式的视频文件。我们为这个项目使用了名为Movie.a4v的文件。因此,我们完成了这一章的编码部分。我们学习了SKNode类在 Sprite Kit 中常用的六个主要子类。
概述
在本章中,我们详细学习了节点。我们讨论了 Sprite Kit 中SKNode类的许多属性和函数,以及它的用法。我们还讨论了节点树的构建和节点树上的动作。现在我们熟悉了SKNode的主要子类,即SKLabelNode、SKCropNode、SKShapeNode、SKEmitterNode、SKLightNode和SKVideoNode,以及它们在我们游戏中的实现。
在下一章中,我们将学习在 Sprite Kit 游戏中添加物理模拟的基础知识。我们还将学习如何将物理添加到我们游戏中的不同节点。
第五章. Sprite Kit 中的物理
在前面的章节中,我们学习了在 Sprite Kit 中开发游戏的基本知识。此外,我们已经开始开发起始场景,这些场景是与菜单项关联的不同屏幕视图。
为了回顾上一章,我们详细讨论了节点,我们研究了SKNode类及其相关的属性和函数。此外,我们还讨论了构建节点树和节点树上的动作。我们还应用了我们在游戏中创建菜单场景的主要子类,如SKLabelNode、SKCropNode、SKShapeNode、SKEmitterNode、SKLightNode和SKVideoNode。现在,是时候进一步探索 Sprite Kit 了。
在现实世界中,我们受到许多物理定律的影响,例如质量、重力、速度等。为了使游戏更加逼真,Sprite Kit 为我们提供了一些类和函数,这些类和函数用于使节点像在真实环境中一样行动。通过将这些类应用于角色、环境等,我们的游戏变得逼真。
例如,在一个涉及玩家在道路上行走的平台游戏中。给玩家、道路或任何其他障碍物应用重力、力、摩擦等将更好。现在,我们将讨论在我们的游戏Platformer中模拟物理。
在 Sprite Kit 中模拟物理
大多数游戏引擎都有内置的物理引擎,你还可以向游戏引擎添加外部物理引擎。幸运的是,Apple 在 Sprite Kit 中提供了一个物理引擎。在 Sprite Kit 中,物理属性通过SKPhysicsBody类的对象应用。正如我们已经学到的,对象与节点树中的节点相关联,物理模拟使用节点的方向和位置进行模拟。在 Sprite Kit 中,当游戏渲染时,每一帧都会在循环中调用一些函数,如下所示:
update
didEvaluateActions
didSimulatePhysics
didApplyConstraints
didFinishUpdate
在动作(例如节点动画中的图像变化)之后,SKScene会模拟物理以执行所有动作,例如物理体上的重力、速度变化、两个物理体之间的碰撞等。如果我们查看我们的SKNode类,我们会看到一个名为physicsBody的属性。它接受SKPhysicsBody对象作为参数,并定义了这些对象上的物理定律;很明显,它将从其子类继承,例如SKSpriteNode、SKEmitterNode、SKVideoNode等。因此,我们可以通过在它上面设置physicsBody属性,使任何SKNode子类成为一个物理体。
现在是时候深入研究负责场景中节点物理行为的必要文档了。让我们来讨论一下SKPhysicsBody类。
SKPhysicsBody
一个节点的physicsBody属性使用SKPhysicsBody类对象。在帧的生命周期中,didSimulatePhysics函数在动作评估之后被调用。这个函数的工作是计算物理属性,如重力、速度、摩擦、恢复力、碰撞或其他力。在完成这些计算后,节点的位置和方向在update函数中更新。如果我们打算对一个节点施加一些力,那么首先将SKPhysicsBody对象分配给该节点是必要的。
Sprite Kit 为我们提供了两种物理体:
-
基于体积:这些是有质量和体积的物理体类型
-
基于边缘:这些是没有质量和体积的物理体类型
在基于体积的物理体中,我们可以通过将其设置为static或dynamic来控制它是否应该受到重力、摩擦、碰撞等的影响。这个属性非常有用,因为我们只需调整这个属性就可以创建一个静态平台或移动对象。这些物体定义在特定的边界内,如圆形、矩形、多边形等。不允许不规则形状。对于不规则形状的物体,可以通过连接小的基于体积的物体来实现所需的物理对象图案。
另一方面,边缘基于的物理体用于在游戏场景中创建无体积的空间。这意味着它们不是实心的,允许其他物理体在其边界内。边缘基于的物理体始终被视为其dynamic属性为false,并且只能与其他基于体积的物理体发生碰撞。为了理解边缘基于物理体的概念,可以想象一个有云的场景;云永远不会是实心的,基于体积的物理对象可以进入其中。

基于体积和边缘物理体的图形示例
这两种物理体是通过调用SKPhysicsBody的适当初始化方法来创建的。在我们的场景中,我们主要使用基于体积的物理体。
正如我们所定义的,我们需要实例化SKPhysicsBody类来创建基于体积或边缘的物理体。
基于体积物理体的初始化
以下是基于体积物理体的初始化器:
-
init(circleOfRadius r: CGFloat) -> SKPhysicsBody:这个初始化器用于创建圆形物理体。它接受半径作为参数,并返回一个SKPhysicsBody对象。这个物体的重心位于接收节点的中心,即应用此函数的节点。 -
init(circleOfRadius r: CGFloat,center center: CGPoint) -> SKPhysicsBody:这个初始化器与之前的非常相似,只是在它还接受一个额外的参数,即物理体的原点。我们可以使用这个初始化器分配的接收坐标系统相对于我们的重力或圆形物理体的中心进行偏移。 -
init!(rectangleOfSize s: CGSize) -> SKPhysicsBody: 这个初始化器用于创建矩形形状的物理体。它接受一个rectangle作为参数,并返回一个包含其中心在接收节点中心的SKPhysicsBody对象。 -
init!(rectangleOfSize s: CGSize,center center: CGPoint) -> SKPhysicsBody: 这个初始化器与上一个非常相似,除了它多了一个参数,即物理体的原点。我们可以通过这个初始化器分配的接收坐标系,将重力在矩形物理体上移至中心。 -
init(bodies bodies: [AnyObject]) -> SKPhysicsBody: 这个初始化器用于通过使用现有物理体的数组来创建一个新的物理体。为此,我们只需在数组中传递基于体积的物理体对象。从这个初始化器创建的物理体的结果区域是数组中其他子物理体的并集。因为它使用其子体的形状,这意味着它内部可以有空间,甚至空白区域。 -
init!(polygonFromPath path: CGPath!) -> SKPhysicsBody: 这个初始化器用于创建多边形形状的物理体。它接受一个顺时针方向的凸多边形路径作为参数。 -
init!(texture texture: SKTexture!,size size: CGSize) -> SKPhysicsBody: 这个初始化器用于使用纹理创建物理体。当我们需要根据纹理形状创建物理体形状时,会用到它。这被称为逐像素物理,当形状既不是矩形也不是圆形时非常有用。它是在 iOS 8 中引入的。在这个初始化器中,使用纹理和大小作为参数。首先,纹理被缩放到那个大小,然后,新创建的物理体的形状由所有具有非零 alpha 值的像素决定。 -
init!(texture texture: SKTexture!,alphaThreshold alphaThreshold: Float,size size: CGSize) -> SKPhysicsBody: 这个初始化器与上一个非常相似,也是在 iOS 8 中引入的,但它多了一个参数,即alpha。我们可以定义像素的 alpha 值,当低于这个值时,像素将被忽略,以创建新的物理体。其余的过程与上一个相同。
在此之后,让我们看看如何创建基于边缘的物理体。
基于边缘的物理体的初始化
以下是用于创建基于边缘的物理体的初始化器列表:
-
init (edgeLoopFromRect rect: CGRect) -> SKPhysicsBody: 这个初始化器接受一个矩形作为参数,并返回一个新的基于矩形边缘的物理体。 -
init (edgeFromPoint p1: CGPoint, toPoint p2: CGPoint) -> SKPhysicsBody: 这个初始化器接受两个点作为参数,并在这两个点之间返回一个基于边缘的物理体。 -
init (edgeLoopFromPath path: CGPath!) -> SKPhysicsBody: 此初始化器接受一个path作为参数,并基于该路径返回一个基于边的物理体。路径不得相交。如果路径未封闭,它将自动通过连接该路径的第一个和最后一个点来创建一个循环。 -
init (edgeChainFromPath path: CGPath!) -> SKPhysicsBody: 此初始化器接受一个path作为参数,并基于该路径返回一个基于边链的物理体。路径不得相交。
这些是基于体积和基于边的物理体的初始化过程。我们可以通过调整其一些属性来自定义物理体的行为。
物理体的行为控制器属性
以下是我们可以控制物理体行为的属性列表:
-
受重力影响: 这是一个布尔值。它确定物理体是否会被场景中的重力影响。基于边的物理体简单地忽略此属性,因为它们不受重力影响。默认值是true。 -
允许旋转: 这也是一个布尔值。它确定物理体是否会被场景中施加到它上的角力和冲量影响。基于边的物理体简单地忽略此属性。默认值是true。 -
动态: 这也是一个布尔值。它确定物理体是否会被场景中的物理模拟移动。基于边的物理体简单地忽略此属性。默认值是true。
这些是基于体积的物理体的行为控制器属性。此外,物理体还有一些自己的物理属性。
物理体的物理属性
这些是物理体拥有的属性。正如你所知,速度、力、重力、碰撞等取决于物体的质量、密度、面积等。
以下是物理体的物理属性列表。
-
质量: 这是物体的质量,单位为千克。 -
密度: 这是物体每平方米的密度。密度和质量属性是相互关联的。每当其中一个属性改变时,另一个属性就会重新计算。默认值是 1.0。 -
面积: 这是物体覆盖的面积。这是一个只读属性,并用于在密度属性的帮助下定义物理体的质量。 -
摩擦力: 它用于确定应施加到与当前物体接触的另一个物理物体上的摩擦力大小。此属性值介于 0.0 和 1.0 之间。默认值为 0.2。 -
恢复: 它用于确定物理体的弹性。此属性值介于 0.0 和 1.0 之间。默认值是 0.2。 -
线性阻尼: 它用于减少物理体的线性速度。此属性值介于 0.0 和 1.0 之间。默认值为 0.1。 -
angularDamping: 它用于减少物理物体的角速度。此属性值介于 0.0 和 1.0 之间。默认值为 0.1。
这些属性定义了物理物体的物理行为。
SKPhysicsBody类提供了一些用于碰撞控制的属性和函数。
碰撞控制属性和函数
物理物体使用某些类别与其他物理物体进行碰撞检测。碰撞在几乎每个游戏中都非常重要。当物体发生碰撞时,物体的速度和方向会发生变化,这需要对物理参数的变化进行精确计算。我们必须指定我们游戏中物理物体的类别。有一个限制,因为我们只能为我们游戏中的物理物体定义 32 种不同的类别。我们使用这些类别来定义物理物体是否应该与另一个物理物体发生碰撞。这是非常有用的行为,并且在 Sprite Kit 的物理游戏中使用。
以下是碰撞控制属性的列表:
-
categoryBitMask: 这是一个定义物理物体类别的掩码。我们可以有最多 32 个不同的类别。借助类别掩码,您可以定义哪些物理物体应该相互交互。此属性与contactTestBitMark一起使用。 -
collisionBitMask: 此属性用于定义可能发生碰撞的物理物体的类别。它用于通过与其他物理物体的 AND 操作来确定是否发生碰撞。如果结果是非零值,则此物体将受到碰撞的影响,否则不会。这有助于在速度变化很小的场合跳过碰撞计算。 -
usesPreciseCollisionDetection: Bool: 如果为true,则此物体将受到碰撞的影响,否则它将在一个帧内穿过另一个物体。任一物体的true值都会导致碰撞,这意味着 Sprite Kit 将使用更多的计算能力来检测碰撞和执行精确计算。对于非常小且快速移动的物体,此属性可以设置为true,否则默认值为false。 -
contactTestBitMask: 此属性定义了BitMask物理物体应该通过 AND 门操作通知与接收到的物理物体相交的类别。如果值为非零,则创建SKPhysicsContact对象并将其传递给物理世界代理。 -
allContactedBodies() -> [AnyObject]: 这是一个用于确定一个或多个物体是否与接收到的物理物体接触的函数。它简单地返回一个包含所有与接收到的物理物体接触的物理物体对象的数组,即应用此函数的物体。
这些碰撞控制属性和功能决定了在物理模拟中两个或更多物理体碰撞或接触的行为。但有时我们需要为了某些特定的行为给物理体施加速度或力。SKPhysicsBody 类定义了一些用于在此目的上对物理体施加力和冲量的函数。
力和冲量
要移动一艘太空船或一辆汽车,我们需要在运动方向上施加力;要保持其运动,必须持续施加力。冲量是用来改变物体的动量,例如,发射子弹时,一旦它开始运动,我们就不需要再施加任何力。
以下是用于在物理体上施加力和冲量的函数列表:
-
func applyForce(_ force: CGVector): 此函数用于对接收到的物理体施加力。它接受force作为参数,并加速接收到的物理体,而不产生任何角加速度。 -
func applyTorque(_ torque: CGFloat): 此函数用于对接收到的物理体施加角力。它接受torque作为参数,并对接收到的物理体施加角加速度。它不会对接收到的物理体施加任何线加速度。 -
func applyForce(_ force: CGVector, atPoint point: CGPoint): 此函数用于在特定点上对接收到的物理体施加力。由于它作用于物理体上的特定点,因此可能会改变物体的角加速度和线加速度。 -
func applyImpulse(_ impulse: CGVector): 此函数用于对接收到的物理体的重心施加冲量。它接受impulse作为参数,并影响线速度,而不改变角速度。 -
func applyImpulse(_ impulse: CGVector, atPoint point: CGPoint): 此函数用于在特定点上对接收到的物理体施加冲量。由于它作用于物理体上的特定点,因此可能会改变接收物理体的角速度和线速度。 -
func applyAngularImpulse(_ impulse: CGFloat): 此函数用于对接收到的物理体施加角冲量。它接受impulse作为参数,并对接收到的物理体施加角速度。它不会对接收到的物理体施加任何线速度。
除了这些函数之外,我们还需要知道物理体的结果速度和角速度。为此,SKPhysicsBody 类有一些属性。
物理体的速度
以下是用于在物理体上施加速度的函数列表:
-
velocity: 它用于确定物理体的线速度。 -
angularVelocity: 它用于确定物理体的角速度。 -
resting:它确定物理对象是否在物理世界中处于静止状态。这意味着它不参与物理模拟,直到被力或碰撞唤醒。这个属性有助于减少物理模拟中的计算,从而提高性能。 -
SKPhysicsBody:它为我们提供了一些其他重要的属性。其他属性joints,这个属性包含一个SKPhysicsJoint对象的数组,这些对象连接到接收到的物理体。 -
fieldBitMask:这个属性应用于物理体。一旦这个体进入一个SKFieldNode对象内部,fieldBitMask属性将与字段节点的categoryBitMask属性执行逻辑与操作。如果值为非零,字段节点的影响将应用于此体。 -
charge:它用于计算SKFieldNode对象对接收器物理体的电磁场力。 -
pinned:它确定接收器是否相对于其父级固定在位置。其默认值是false。如果是true,则节点可以自由地围绕其相对于父级的位置旋转,将物理应用到我们的 平台游戏。
现在,我们将继续我们的 平台游戏 并在其中实现各种物理引擎功能。在我们开始将物理应用到我们的游戏之前,我们需要首先确保最初加载的菜单场景是 MenuScene(如第三章 所述,精灵) 而不是 NodeMenuScene。当我们讨论着色器和粒子发射器时,我们将实现 NodeMenuScene 类。
请转到 GameViewController.swift 文件,并在 GameViewController 类中注释掉以下内容:
let menuscene = NodeMenuScene()
取而代之,写下以下内容:
let menuscene = MenuScene(size: view.bounds.size, playbutton: "Play", background:"BG")
上述代码将使你的游戏加载 MenuScene 类。现在,前往 GameScene.swift 文件以在我们的游戏中添加物理体。
使用 GameScene.swift 添加物理体。
首先打开你的 GameScene.swift 文件。
-
编辑
GameScene类声明以添加SKPhysicsContactSelegate如下:class GameScene: SKScene, SKPhysicsContactDelegate -
然后在其中添加以下代码:
let backgroundNode = SKSpriteNode(imageNamed: "BG") var spriteWithoutTexture : SKSpriteNode? let myAtlas = SKTextureAtlas(named: "idle.atlas") var player:SKSpriteNode = SKSpriteNode(imageNamed:"bro5_idle0001@2x") var currentno = 0 // SETTING UP "RUNNING BAR", "BLOCK 1", "Block 2 let runningBar = SKSpriteNode(imageNamed:"bar") let block1 = SKSpriteNode(imageNamed:"block1") let block2 = SKSpriteNode(imageNamed:"block2") var origRunningBarPositionX = CGFloat(0) var maxBarX = CGFloat(0) var groundSpeed = 5 var playerBaseline = CGFloat(0) var onGround = true // INITIALIZING PHYSICAL PROPERTIES VALUES var velocityY = CGFloat(0) let gravity = CGFloat(0.6) var blockMaxX = CGFloat(0) var origBlockPositionX = CGFloat(0) var blockStatuses:Dictionary<String,BlockStatus> = [:] //COLLISION TYPE BETWEEN "BLOCKS" AND "PLAYER" enum ColliderType:UInt32 { case player = 1 case Block = 2 }如果你查看前面的代码,你会看到我们添加了三张新图片:一张是顶部运行的条形,我们的玩家将跑步或看起来在跑步,其他两张是
block1和block2。这两张图片是障碍物,我们的玩家将与它们碰撞。除此之外,我们还初始化了一些物理属性值,如速度、重力等。我们还定义了一个枚举来控制Blocks和Player之间的碰撞类型。 -
现在,添加以下函数以启动执行流程,并定义接触代理以检测屏幕上的触摸/接触(触摸将帮助我们确定跳跃强度):
override func didMoveToView(view: SKView) { self.physicsWorld.contactDelegate = self //#1 addBackGround() addRunningBar() addPlayer() addBlocks() }在前面的代码中,
#1代码块用于使用方法将背景、跑步条、玩家和方块添加到场景中。并且还用于设置物理属性,如categoryBitMask、ContactTestBitMask、CollisionBitMask等。 -
现在,添加以下函数以随机生成方块,取一个介于
50和200之间的数字;这是用于在屏幕上随机显示方块:func random() -> UInt32 { var range = UInt32(50)..<UInt32(200) return range.startIndex + arc4random_uniform(range.endIndex - range.startIndex + 1) //CREATING BLOCKS FROM LIBRARY METHOD OF iOS } -
现在,添加下一个函数以使用无纹理的精灵:
func addSpriteWithoutTexture() { spriteWithoutTexture = SKSpriteNode(texture: nil, color:UIColor.redColor(), size: CGSizeMake(100, 100)) addChild(spriteWithoutTexture!) } -
添加下一个函数以在场景中插入背景:
func addBackGround() { backgroundNode.zPosition = 0 var scaleX = self.size.width/backgroundNode.size.width var scaleY = self.size.height/backgroundNode.size.height backgroundNode.xScale = scaleX backgroundNode.yScale = scaleY addChild(backgroundNode) } -
将以下函数添加到定义我们游戏中玩家/角色的物理属性:
func addPlayer() { player.zPosition = 2; player.name = "Player" // PHYSICS PROPERTIES FOR player self.playerBaseline = self.runningBar.position.y + (self.runningBar.size.height / 2) + (self.player.size.height / 2) self.player.position = CGPointMake(CGRectGetMinX(self.frame) + self.player.size.width + (self.player.size.width / 4), self.playerBaseline) self.player.physicsBody = SKPhysicsBody(circleOfRadius: CGFloat(self.player.size.width / 2)) self.player.physicsBody?.affectedByGravity = false self.player.physicsBody?.categoryBitMask = ColliderType.player.rawValue // Will become '1' because its defined in "ColliderType" enum self.player.physicsBody?.contactTestBitMask = ColliderType.Block.rawValue self.player.physicsBody?.collisionBitMask = ColliderType.Block.rawValue self.addChild(player) } -
现在,设置跑步条;玩家将出现在上面的条上:
func addRunningBar() { self.runningBar.anchorPoint = CGPointMake(0, 0.5) self.runningBar.position = CGPointMake(CGRectGetMinX(self.frame),CGRectGetMinY (self.frame) + (self.runningBar.size.height / 2)) self.origRunningBarPositionX = self.runningBar.position.x self.maxBarX = self.runningBar.size.width - self.frame.size.width self.maxBarX *= -1 self.addChild(self.runningBar) } -
现在插入以下函数以在游戏中添加方块:
func addBlocks() { // PHYSICS PROPERTIES FOR BLOCK 1 self.block1.position = CGPointMake(CGRectGetMaxX(self.frame) + self.block1.size.width, self.playerBaseline) self.block2.position = CGPointMake(CGRectGetMaxX(self.frame) + self.block2.size.width, self.playerBaseline + (self.block1.size.height / 2)) self.block1.physicsBody = SKPhysicsBody(rectangleOfSize: self.block1.size) self.block1.physicsBody?.dynamic = false self.block1.physicsBody?.categoryBitMask = ColliderType.Block.rawValue self.block1.physicsBody?.contactTestBitMask = ColliderType.player.rawValue self.block1.physicsBody?.collisionBitMask = ColliderType.player.rawValue // PHYSICS PROPERTIES FOR BLOCK 2 self.block2.physicsBody = SKPhysicsBody(rectangleOfSize: self.block1.size) self.block2.physicsBody?.dynamic = false self.block2.physicsBody?.categoryBitMask = ColliderType.Block.rawValue self.block2.physicsBody?.contactTestBitMask = ColliderType.player.rawValue self.block2.physicsBody?.collisionBitMask = ColliderType.player.rawValue self.origBlockPositionX = self.block1.position.x //ORIGINAL BLOCK POSITION (0,0) self.block1.name = "block1" // SETTING BLOCK NAMES self.block2.name = "block2" // ADDING BLOCK 1 and BLOCK 2 to DICTIONARY BLOCKSTATUS blockStatuses["block1"] = BlockStatus(isRunning: false, timeGapForNextRun: random(), currentInterval: UInt32(0)) blockStatuses["block2"] = BlockStatus(isRunning: false, timeGapForNextRun: random(), currentInterval: UInt32(0)) self.blockMaxX = 0 - self.block1.size.width / 2 self.addChild(self.block1) self.addChild(self.block2) } -
添加以下函数,当用户触摸屏幕时调用。它使角色跳跃:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) { for touch: AnyObject in touches { let location = touch.locationInNode(self) let node = self.nodeAtPoint(location) if node.name == player.name { currentno++ //changeSpriteFromTextureAtlas() if self.onGround { self.velocityY = -18.0 self.onGround = false } } } } -
还添加下一个函数,当屏幕触摸释放时调用。它将在跳跃后将角色降下:
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) { if self.velocityY < -9.0 //SETTING VELOCITY FOR JUMP ACTION IS FINISHED { self.velocityY = -9.0 } } -
添加下一个方法以定义在场景动作评估之前需要执行的特定场景更新操作:
override func update(currentTime: NSTimeInterval) { if self.runningBar.position.x <= maxBarX { self.runningBar.position.x = self.origRunningBarPositionX } // JUMP ACTION self.velocityY += self.gravity self.player.position.y -= velocityY if self.player.position.y < self.playerBaseline // STOPPING PLAYER TO FALLDOWN FROM BASELINE { self.player.position.y = self.playerBaseline velocityY = 0.0 self.onGround = true } //move the ground runningBar.position.x -= CGFloat(self.groundSpeed) blockRunner() } -
最后,添加以下函数以使方块移动:
func blockRunner() { // LOOP FOR THE DICTIONARY TO GET BLOCKS for(block, blockStatus) in self.blockStatuses { var thisBlock = self.childNodeWithName(block)! if blockStatus.shouldRunBlock() { blockStatus.timeGapForNextRun = random() blockStatus.currentInterval = 0 blockStatus.isRunning = true } if blockStatus.isRunning { if thisBlock.position.x > blockMaxX // IF IT IS POSITIVE (KEEP MOVING BLOCKS FROM RIGHT TO LEFT) { thisBlock.position.x -= CGFloat(self.groundSpeed) } else // IF ITS TIME TO OFF THE SCREEN ie when BLOCKS should DISAPPEAR { thisBlock.position.x = self.origBlockPositionX //blockStatus.isRunning = false } } else { blockStatus.currentInterval++ } } } -
还需要在 Xcode 中创建一个名为
BlockStatus.swift的swift文件。此文件包含初始化和运行方块代码:class BlockStatus // { var isRunning = false //CURRENTLY RUNNING ON SCREEN OR NOT var timeGapForNextRun = UInt32(0) // HOW LONG WE IT SHOULD WAIT FOR NEXT RUN var currentInterval = UInt32(0) //TOTAL INTERVAL WAITED // INITIALIZING BLOCK STATUS init(isRunning:Bool, timeGapForNextRun:UInt32, currentInterval:UInt32) { self.isRunning = isRunning self.timeGapForNextRun = timeGapForNextRun self.currentInterval = currentInterval } // RUNNING BLOCKS func shouldRunBlock() -> Bool { return self.currentInterval > self.timeGapForNextRun } } -
现在开始运行游戏;确保尝试不同的值来发现你游戏的行为。这将增加你在 Sprite Kit 中对物理学的理解。
![使用 GameScene.swift 添加物理体]()
以下是第二张图片:

前面的两个截图显示了角色为静态,方块为移动。我们还可以通过在屏幕上触摸角色来执行跳跃:

你注意到玩家没有在跑,而是玩家下面的条和与之碰撞的方块在移动。角色只是看起来像在跑;在这种情况下,我们只是实现了跳跃的速率方法,而不是在 y 方向上应用力。
摘要
在本章中,我们讨论了物理引擎。我们学习了SKPhysicsBody。现在我们非常了解 Sprite Kit 中的基于边缘和基于体积的物理体。在我们的游戏中,朝向角色的方块是体积型体。我们还学习了这些类型物理体的各种初始化方法。我们学习了如何在游戏中应用 Sprite Kit 的物理引擎,以便探索其功能。
在下一章中,我们将学习在 Sprite Kit 中动画精灵以及在我们的游戏中添加各种类型的控制。我们还将讨论通过 Sprite Kit 进行碰撞和 SceneKit 集成。
第六章. 动画精灵、控件和 SceneKit
在上一章中,我们详细学习了物理引擎。我们讨论了SKPhysicsBody,即基于边缘和体积的物理体。我们还了解了各种初始化方法和物理属性,这些帮助我们将物理集成到我们的平台游戏中。现在我们对如何在 Sprite Kit 游戏中模拟物理有了更清晰的认识。
在我们的游戏中拥有良好的动画特性总是很好的,这可以增强用户体验;这个 Sprite Kit 拥有SKAction类,它帮助我们应用动作到节点上,例如节点的移动、旋转、缩放等。例如,在玩家移动时使用动画或使用动画来描绘碰撞等。现在是时候讨论SKAction类以及在我们平台游戏中实现动画了。
除了动画,我们还将讨论如何在我们的游戏中提供控制,例如当用户触摸屏幕时产生反应或使用加速度计来响应游戏中的方向。我们还将在我们平台游戏中实现控制,并添加一个跳跃按钮,使玩家能够跳过方块。
Scene Kit 是苹果提供的一个 3D 图形框架,我们可以在 Sprite Kit 游戏中使用 SceneKit 的 3D 元素来进一步增强游戏体验,并在需要时拥有更好的游戏玩法。我们将讨论如何将 SceneKit 集成到 Sprite Kit 游戏中。
动画节点
动画图片在游戏过程中提供了非常动态和精致的感觉;在我们的游戏中总是偏好拥有动画。要在 Sprite 节点中添加动画,我们可以使用SKAction类的属性和方法,这些方法将动画添加到 Sprite Kit 节点中。让我们详细讨论SKAction类。
SKAction
SKAction类的属性和方法有助于向场景中的节点提供动作。动作用于改变它们所附加的节点的排列和外观。当场景运行其节点时,节点中的动作会被执行。
要分配一个动作,我们可以根据需要调用特定的SKAction类方法。然后,我们可以配置动作的属性。最后,为了执行动作,我们调用节点对象的runAction()方法并传递动作对象。
将单个动作添加到节点
将单个动作添加到节点的步骤有两个:
-
创建动作:首先,我们创建一个动作,它可以在 Sprite Kit 节点上执行特定的活动,如旋转、缩放、移动等。
-
执行动作:最后,我们通过在该节点上调用
runAction()方法来运行动作。
将多个动作添加到节点
在将多个动作添加到节点过程中的涉及三个步骤:
-
创建动作:在这里,我们不仅可以创建单个动作,还可以创建多个动作,以在 Sprite Kit 节点上执行不同的行为。
-
创建动作序列:在这里,我们将创建在 Sprite Kit 节点中动作应该表现出的执行顺序。
-
执行动作:最后,我们将通过在节点的
runAction()方法中指定动作序列来运行动作。
创建动作
可以对节点应用各种类型的动作,以使其表现出不同的行为,现在我们将详细研究其中大部分。
使用动作移动节点
SKAction类为在场景中移动节点提供了各种动作方法。它们如下:
-
func moveByX( x: CGFloat, y : CGFloat, duration sec: NSTimeInterval): 这将使节点移动到其新位置。这里,x和y的增量以及持续时间的秒数作为参数传递。 -
func moveBy( delta: CGVector, duration sec: NSTimeInterval): 这将使节点相对于其当前位置移动。这里,指向新位置的增量向量和持续时间的秒数作为参数传递。 -
moveTo(location: CGPoint, duration sec: NSTimeInterval): 这将使节点移动到新位置。新位置的坐标和持续时间的秒数作为参数传递。这里的位置是一个CGPoint值,其默认值为(0,0)。 -
func moveToX( x: CGFloat, duration sec: NSTimeInterval): 这将使节点水平移动。在这里,x值和动作的持续时间作为参数传递。 -
func moveToY(y: CGFloat, duration sec: NSTimeInterval): 这将使节点沿着相对路径垂直移动。在这里,y值和动作的持续时间(以秒为单位)作为参数传递。 -
func followPath( path: CGPath, duration sec: NSTimeInterval): 这将使节点沿着相对路径移动。path和sec作为参数,其中path是一个相对于节点当前位置的CGPath值。 -
func followPath( path: CGPath, speed: CGFloat): 这将使节点以指定的速度沿着相对路径移动。速度的单位是每秒点数。 -
func followPath( path: CGPath, asOffset : Bool, orientToPath : Bool, duration : NSTimeInterval ): 此函数将使节点沿着路径移动。在这个函数中,我们传递四个参数:一个是节点将要移动的path;第二个是offset参数,它可以是true或false。true表示路径中的点相对于节点初始位置的相对偏移,而另一方面false表示点具有绝对性质。orientToPath将是一个布尔属性,如果节点可以沿着z轴跟随路径。 -
func followPath( path: CGPath, asOffset : Bool, orientToPath : Bool, speed : CGFloat): 此函数将以指定的速度沿着路径移动节点。
使用动作旋转节点
SKAction 类提供了各种用于在场景中旋转节点的动作方法。它们是:
-
func rotateByAngle( radians: CGFloat, duration sec: NSTimeInterval): 此函数有助于在指定角度旋转节点。它接受两个参数:一个是节点旋转的radians数量,另一个是旋转的持续时间(以秒为单位)。此旋转相对于节点。 -
func rotateToAngle( radians: CGFloat, duration sec: NSTimeInterval): 此函数有助于将节点旋转到绝对角度,逆时针方向。它也接受两个参数:一个是旋转节点的角度,以radians为单位测量,另一个是以秒为单位的动画持续时间。 -
func rotateToAngle(radians: CGFloat, duration sec: NSTimeInterval, shortestUnitArc shortestUnitArc: Bool): 此函数有助于将节点旋转到绝对角度。它接受三个参数:一个是节点要旋转到的角度,第二个是持续时间(以秒为单位),第三个是布尔值,用于指定我们是否想要最小的旋转路径。如果true,则旋转将沿最短方向进行,否则旋转将在离散点之间插值。
更改节点的动画速度
SKAction 类提供了各种用于更改节点动画速度的动作方法。它们如下:
-
func speedBy(speed: CGFloat, duration sec: NSTimeInterval): 使用此函数,我们可以控制节点动作的速度。它接受两个参数:一个是节点中要添加的speed量,另一个是以秒为单位的动画持续时间。 -
func speedTo(speed: CGFloat, duration sec: NSTimeInterval): 使用此函数同样可以控制节点动作的速度。但与传递参数以将其值添加到先前速度不同,此函数将speed更改为设置的值。另一个传递的参数是动画的持续时间(以秒为单位)。
更改节点的缩放位置
SKAction 类提供了各种用于缩放节点的动作方法。它们如下:
-
func scaleBy( scale: CGFloat, duration sec: NSTimeInterval): 使用此函数,您可以更改节点的xScale和yScale值。此函数接受两个参数:一个是节点x和y值中要添加的量,另一个是动画的持续时间。此缩放应用于当前大小。 -
func scaleTo( scale: CGFloat, duration sec: NSTimeInterval): 使用此函数同样可以更改节点的x和y值。它接受两个参数:一个是节点x和y值的新值,另一个是动画的持续时间。 -
func scaleXBy(xScale: CGFloat, y yScale: CGFloat, duration sec: NSTimeInterval): 使用此函数,您可以更改节点的x和y值。在此函数中传递了三个参数:第一个是要添加到节点x值中的量,第二个是要添加到节点y值中的量,第三个是动画的持续时间。当您必须使用不同的值缩放节点的x和y时,使用此函数。 -
func scaleXTo(xScale: CGFloat, y yScale: CGFloat, duration sec: NSTimeInterval): 此函数同样可以更改节点的x和y值,但不是传递要添加到x和y中的值,而是通过传递相应的参数将x和y缩放设置为新的值。 -
func scaleXTo(scale: CGFloat, duration sec: NSTimeInterval): 使用此函数,您只能将节点的x值更改为新值。它接受两个参数:一个是节点的x值,另一个是动画的duration。 -
func scaleYTo(scale: CGFloat, duration sec: NSTimeInterval): 使用此函数,您只能将节点的y值更改为新值。它接受两个参数:一个是节点的y值,另一个是动画的duration。
显示或隐藏节点
SKAction类为在场景上隐藏或显示节点提供了各种动作方法。让我们看看这两个函数:
-
func unhide(): 使用此函数,您可以创建一个使节点可见的动作。此函数是在 iOS 8.0 中引入的。 -
func hide(): 使用此函数,您可以创建一个使节点隐藏的动作。此函数也是在 iOS 8.0 中引入的。
更改节点的透明度
在SKAction的帮助下,您还可以更改节点的透明度。以下函数可以帮助您实现这一点:
-
func fadeInWithDuration(sec: NSTimeInterval): 您可以使用此函数将节点的 alpha 值更改为1.0。此函数只传递一个参数,即动画的持续时间。 -
func fadeOutWithDuration(sec: NSTimeInterval): 您可以使用此函数将节点的 alpha 值更改为0.0。此函数只传递一个参数,即动画的持续时间。 -
func fadeAlphaBy(factor: CGFloat, duration sec: NSTimeInterval): 使用此函数,您可以控制要添加到节点中的 alpha 值的量。在此函数中,您传递两个参数:一个是添加到节点 alpha 值中的量,另一个是节点的duration。 -
func fadeAlphaTo(alpha: CGFloat, duration sec: NSTimeInterval): 使用此函数,您可以设置节点的新 alpha 值。在此函数中传递了两个参数:一个是节点的新 alpha 值,另一个是节点的duration。
更改精灵节点的内容
通过一些SKAction函数,您可以创建动作来更改精灵节点的内容。让我们看看它们:
-
func resizeByWidth(width: CGFloat, height: CGFloat, duration: NSTimeInterval): 这个函数创建一个动作,调整精灵节点的尺寸。这个函数接受三个参数:第一个是添加到精灵width的量,第二个是添加到精灵height的量,第三个是动画的duration。 -
func resizeToHeight(height: CGFloat, duration: NSTimeInterval): 这个函数创建一个动作,将精灵的height更改为新值。传递的一个参数是精灵的新height,另一个参数是动画的duration。 -
func resizeToWidth(width: CGFloat, duration: NSTimeInterval): 这个函数创建一个动作,将精灵的width更改为新值。传递的一个参数是精灵的新width,另一个参数是动画的duration。 -
func resizeToWidth(width: CGFloat, height: CGFloat, duration: NSTimeInterval): 这个函数创建一个动作,将精灵节点的width和height更改为新值。在这个函数中,您可以分别指定新的height和width。它接受三个参数:一个是精灵的新width,二是精灵的新height,三是动画的duration。 -
func setTexture(texture: SKTexture): 这个函数有助于创建一个改变精灵纹理的动作。这个函数只传递了一个参数,即精灵的新texture。 -
func setTexture(texture: SKTexture, resize: Bool): 这个函数有助于创建一个改变精灵纹理的动作。除了这个之外,您还可以控制精灵是否应该调整大小以匹配新纹理。传递的两个参数是用于精灵的新texture和控制调整大小的布尔值。 -
func animateWithTextures(textures: [AnyObject], timePerFrame sec: NSTimeInterval): 这个函数创建一个动作,用于动画化精灵纹理的变化。当动作执行时,texture属性会动画化传递作为参数的纹理数组。动作会持续进行,直到数组中的所有纹理都完成动画。这个函数传递了两个参数:一个是纹理数组,另一个是数组中每个纹理显示的时间。 -
func animateWithTextures(textures: [AnyObject], timePerFrame sec: NSTimeInterval, resize: Bool, restore: Bool): 这个函数创建一个动作,可以动画化精灵纹理的变化,并在需要时调整精灵大小以匹配新纹理。它接受四个参数:一个是用于动画精灵的纹理数组,第二个是每个纹理显示的时间,第三个是布尔值,用于控制精灵是否调整大小以匹配新纹理,第四个是恢复精灵大小到原始纹理大小。 -
func colorizeWithColor(color: UIColor, colorBlendFactor: CGFloat, duration sec: NSTimeInterval): 此函数创建一个动画,该动画会动画化精灵的颜色和混合因子。此函数中传递了三个参数:一个是用于新精灵的color,第二个是新混合因子,第三个是动画的duration。 -
func colorizeWithColorBlendFactor( colorBlendFactor: CGFloat, duration sec: NSTimeInterval): 此函数将创建一个动画,该动画会动画化精灵的混合因子。它接受两个参数:一个是新的混合因子,另一个是动画的duration。
一些其他重要的动作
到目前为止,我们已经讨论了大多数用于在节点上创建动作的重要函数。现在,我们将看看在 Sprite Kit 中创建节点动作的一些其他重要函数:
-
func runAction( action: SKAction, onChildWithName name: String): 此函数将创建一个action,然后在该节点的子节点上运行action。您需要传递要执行的action和子对象的name作为参数。 -
func group( actions: [AnyObject]): 您可以使用此函数的动作并行运行一系列actions。它接受一个SKAction对象的数组作为参数。 -
func sequence( actions: [AnyObject]): 您可以使用此函数的动作按顺序运行一系列动作。它接受一个SKAction对象的数组作为参数。动作的顺序与数组中传递的动作顺序相同。 -
func repeatAction( action: SKAction, count count: Int): 您可以创建一个动作来重复指定次数的动作。将重复的动作和重复次数作为参数传递。 -
func repeatActionForever( action: SKAction): 它创建一个动作,该动作会无限期地重复另一个动作。它接受要重复的动作作为参数。 -
func reversedAction(): 使用此动作,您可以反转另一个动作的行为。
在 Sprite Kit 中添加控制
在 Sprite Kit 中添加控制不需要任何外部预定义框架;我们可以使用以下方法在 Sprite Kit 中实现控制:
-
点击
-
手势识别(任意方向的滑动、捏合、旋转)
-
使用加速度计移动精灵
让我们详细讨论前面提到的每个控制,以及我们如何在游戏中实现它们。
节点点击和双击
我们有四个重载方法用于处理带有 UIResponder 类的触摸事件,这是 Apple 提供的 UIKit 的一部分。让我们来了解一下:
-
func touchesBegan(touches:Set<NSObject>, withEvent event:UIEvent): 当用户触摸视图/窗口时,会调用此方法 -
func touchesMoved(touches:Set<NSObject>, withEvent event:UIEvent): 当用户在视图/窗口上移动手指时,会调用此方法 -
func touchesEnded(touches:Set<NSObject>, withEvent event:UIEvent): 当用户从视窗/窗口移除手指时,此方法会被调用 -
func touchesCancelled(touches:Set<NSObject>!, withEvent event:UIEvent!): 当系统事件发生时,如内存警告等,此方法会被调用
要在场景中的节点上点击时实现一个动作,我们首先获取场景上的点击位置,如果点击位置在节点的坐标轴点上,那么我们可以为该点击定义动作。这将在touchesBegan()方法中实现。
Sprite Kit 在UITouch中包含一个类别;这是其最佳特性之一。UITouch包含两个方法,即locationInNode()和previousLocationInNode()。这些方法在SKNode对象坐标系内找到触摸的坐标。
在我们的游戏中,我们将使用它来确定触摸在场景坐标系中的位置。
手势识别(在任何方向上滑动、捏合或旋转)
如果需要在游戏中检测手势,如点击、捏合、拖动或旋转,使用 Swift 和内置的UIGestureRecognizer类将非常容易。
以下是一个 Swift 中手势识别的代码片段;它将实现左右、上下滑动。
首先,我们为每个方向设置四个函数,以便处理用户在屏幕上向这些方向滑动时想要执行的操作。然后,在didMoveToView语句中,我们为每个方向创建UISwipeGestureRecognizer变量并将它们添加到视图中。注意每个action: selector部分,在下面的代码中调用它们各自的功能:
func swipedRight(sender:UISwipeGestureRecognizer){
println("swiped right")
}
func swipedLeft(sender:UISwipeGestureRecognizer){
println("swiped left")
}
func swipedUp(sender:UISwipeGestureRecognizer){
println("swiped up")
}
func swipedDown(sender:UISwipeGestureRecognizer){
println("swiped down")
}
override func didMoveToView(view: SKView) {
/* Setup your scene here */
let swipeRight:UISwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: Selector("swipedRight:"))
swipeRight.direction = .Right
swipeRight.numberOfTouchesRequired = 1
view.addGestureRecognizer(swipeRight)
let swipeLeft:UISwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: Selector("swipedLeft:"))
swipeLeft.direction = .Left
swipeRight.numberOfTouchesRequired = 1
view.addGestureRecognizer(swipeLeft)
let swipeUp:UISwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: Selector("swipedUp:"))
swipeUp.direction = .Up
swipeRight.numberOfTouchesRequired = 1
view.addGestureRecognizer(swipeUp)
let swipeDown:UISwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: Selector("swipedDown:"))
swipeDown.direction = .Down
swipeRight.numberOfTouchesRequired = 1
view.addGestureRecognizer(swipeDown)
使用前面的代码块,你可以在 Sprite Kit 游戏中实现滑动控制。当用户向特定方向滑动时,UISwipeGestureRecognizer将识别用户滑动方向,并通过addGestureRecognizer()方法将滑动手势对象添加到手势中。因此,特定的对象将被发送到函数,并调用适当的方法,然后执行相应的动作。
使用加速度计移动精灵
加速度计是一种测量实际加速度(“g-force”)的传感器。实际加速度与坐标加速度(速度变化率)不同。许多游戏使用加速度计作为控制器。我们也可以在我们的 Sprite Kit 游戏中使用它。
让我们看看如何在 Sprite Kit 游戏中实现加速度计。我们不会在我们的平台游戏中使用加速度计,但了解相关知识会很好。
作为主要点,我们需要从加速度计读取值,因此我们需要导入CoreMotion框架。在import SpriteKit行之后添加以下行:
import CoreMotion
add the following properties.
var airplane = SKSpriteNode()
var motionManager = CMMotionManager()
var destX:CGFloat = 0.0
CMMotionManager 对象是访问 iOS 提供的运动服务的入口。在 didMoveToView 方法中执行自定义代码。让我们看看它:
override func didMoveToView(view: SKView) {
/* Setup your scene here */
// 1
airplane = SKSpriteNode(imageNamed: "Airplane")
airplane.position = CGPointMake(frame.size.width/2, frame.size.height/2)
self.addChild(airplane)
if motionManager.accelerometerAvailable == true
{
// 2motionManager.startAccelerometerUpdatesToQueue
(NSOperationQueue.currentQueue(), withHandler:{
data, error in
var currentX = self.airplane.position.x
// 3
if data.acceleration.x < 0 {
self.destX = currentX + CGFloat(data.acceleration.x * 100)
}
else if data.acceleration.x > 0 {
self.destX = currentX + CGFloat(data.acceleration.x * 100)
}
})
}
}
请参考前面代码中的注释,以下是一些要点:
-
图片将被加载并居中显示在主视图中。
-
startAccelerometerUpdatesQueue方法读取加速度计的输入并持续获取新的更新。 -
如果加速度值是负数,则从
x位置减去该值,因此飞机将向左移动。如果加速度值是正数,则将该值添加到x位置。实际移动将在更新方法中完成,该方法将在每一帧被调用。
override func update(currentTime: CFTimeInterval) { /* Called before each frame is rendered */ var action = SKAction.moveToX(destX, duration: 1) self.airplane.runAction(action) }
将 moveToX 动作分配给飞机。如果你想要实现用于控制游戏的加速度计,这段代码将是一个有用的参考。现在,让我们来了解一下 SceneKit。
SceneKit 简介
SceneKit 是一个框架,可以用来将 3D 图形组件的功能实现到我们的 iOS 游戏中。SceneKit 提供了一种在更高层次上集成高性能渲染引擎的设施。它还提供了导入、操作和渲染 3D 图形资源的设施。
在 iOS 8 中,将 SceneKit 元素集成到 Sprite Kit 游戏中相当简单。首先,你只需要在所需的 Sprite Kit 类中导入 SceneKit 框架。然后,你就可以访问 SceneKit 的所有方法和属性了。
在我们的 Platformer 游戏中添加动画和控制
在讨论了 SKAction 类和向我们的游戏添加控制的各种方法之后,是时候回顾我们的 Platformer 游戏,并实现其中的一些方法了。
添加动作
现在,是时候在我们的游戏中添加动作了。让我们从给玩家和方块碰撞添加动画开始。直到最后一章,玩家和方块之间没有碰撞效果。
在这里,我们将添加方块和玩家的碰撞。同时,我们可以让玩家以一种动画的方式死亡。我们可以将玩家和方块碰撞后的动画表示为玩家死亡动画。
首先,我们将更新方块 X 轴的最大尺寸,因为目前方块在跑步条结束之前就被销毁了。因此,我们将用更新的代码替换相应的代码。
在 GameScene.swift 文件中的 addBlocks() 方法中,将 self.blockMaxX = 0 - self.block1.size.width / 2 替换为 self.blockMaxX = 0 - self.runningBar.size.width。
现在,我们将处理方块和玩家碰撞的部分。为此,我们将使用库方法函数 didBeginContact(),当发生碰撞时会被调用,因为我们已经在 GameScene.swift 文件中的 addBlocks() 方法中设置了所有必要的物理属性,例如,contactTestBitMask、categoryBitMask 和 collisionBitMask 对于方块和玩家。
包含didBeginContact()方法,并添加以下代码,其中我们定义了玩家和方块碰撞时的动作:
func didBeginContact(contact: SKPhysicsContact)
{
var inOutActionWhenPlayerDied = SKAction.scaleBy(0.5, duration: 0.5)
var upActionWhenPlayerDied = SKAction.moveToY(self.player.size.height * 4, duration: 2)
var removeFromParent = SKAction.self.removeFromParent()
self.player.runAction(SKAction.sequence(
[inOutActionWhenPlayerDied,
inOutActionWhenPlayerDied.reversedAction(),
upActionWhenPlayerDied,removeFromParent]),
gotoMenuScreen)
}
在前面的函数中,我们使用inOutWhenPlayerDied通过乘以浮点值0.5来缩放玩家,并指定duration为0.5秒。在upActionWhenPlayerDied中,我们通过将玩家的height乘以浮点值4,动画持续时间为2秒,沿着 y 轴移动玩家。
在这些动画之后,我们还应该从场景和节点树中移除玩家。这是由removeFromParent处理的。
接下来,我们按照所需的顺序调用动作。
如果你注意到的函数,我们刚刚添加到我们的Platformer游戏中,在调用序列时,我们还通过使用reversedAction()反转了一个动作。我们还调用了gotoMenuScreen函数。让我们讨论一下相同的内容:
添加此动作序列后,我们的游戏将看起来像这样:

玩家与方块碰撞时的动画。
从 GameScene 过渡到 MenuScene
玩家死亡后,是时候调用gotoMenuScreen()方法以过渡到MenuScreen了。在GameScene类中添加以下函数来完成此操作:
func gotoMenuScreen()
{
self.player.removeFromParent()
let transitionEffect = SKTransition.doorsCloseHorizontalWithDuration(1.5)
menuSceneInstance = MenuScene(size: self.size , playbutton: "Play", background: "BG")
menuSceneInstance!.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.view?.presentScene(menuSceneInstance , transition:transitionEffect)
}
略微一瞥过渡效果:

玩家死亡后的门关闭过渡。
在我们的游戏中添加控制
为了控制玩家,我们可以让他跳过方块并救他免于一死。目前,这是通过点击玩家来完成的,但有一个按钮执行此动作会更好。
要在我们的游戏中实现JUMP按钮,首先我们需要在GameScene.swift文件中为JUMP按钮创建一个精灵节点。创建一个名为btnjump的精灵节点,然后为节点分配一个按钮图像;我们可以将其命名为jump。为此功能添加以下代码:
var btnJump:SKNode = SKSpriteNode(imageNamed: "jump")
现在,我们需要将我们的按钮放置在GameScene上。为此,我们可以在didMoveToView()方法中调用addBackground()函数之前添加以下代码。
self.btnJump.position = CGPointMake(-(self.size.width/2.2),
-(self.size.height/4))
self.addChild(btnJump)
现在您的didMoveToView()函数应该看起来像以下这样:
override func didMoveToView(view: SKView)
{
self.physicsWorld.contactDelegate = self
// JUMP BUTTON POSITION SETTING AND ADDING ONTO THE SCREEN
self.btnJump.position = CGPointMake
(-(self.size.width/2.2), -(self.size.height/4))
self.addChild(btnJump)
addBackGround()
addRunningBar()
addPlayer()
addBlocks()
//addSpriteWithoutTexture()
}
到目前为止,我们只是在场景中添加了JUMP按钮,但尚未定义按钮被点击时的动作。因此,让我们为执行此动作编写一段代码块:
if self.btnJump.containsPoint(location)
{
println("tapped!")
if self.onGround
{
self.velocityY = -18.0
self.onGround = false
}
}
在GameScene.swift的touchesBegan方法中添加前面的代码块。现在你的touchesBegan()方法函数应该看起来像以下这样:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches
{
let location = touch.locationInNode(self)
let node = self.nodeAtPoint(location)
if node.name == player.name
{
currentno++
//changeSpriteFromTextureAtlas()
if self.onGround
{
self.velocityY = -18.0
self.onGround = false
}
}
// JUMP BUTTON ACTION
if self.btnJump.containsPoint(location)
{
println("tapped!")
if self.onGround
{
self.velocityY = -18.0
self.onGround = false
}
}
}
}
以下是添加JUMP按钮后GameScene的外观:

JUMP 按钮现在出现在屏幕上
现在,如果你运行游戏,将有两个主要变化:一个是玩家与方块碰撞时玩家死亡动画,另一个是JUMP按钮,使玩家能够跳过方块。
概述
在本章中,我们详细学习了SKAction类;这个类负责为节点创建动作。我们还讨论了各种控制方式,通过这些方式可以玩 Sprite Kit 游戏(例如,点击、手势识别和加速度计)。我们还阅读了关于 SceneKit 的内容以及如何在 Sprite Kit 游戏中集成 SceneKit。现在,我们的平台游戏有了两个新功能。一个是玩家的死亡动画,另一个是控制玩家跳跃的跳跃按钮。
在下一章中,我们将学习粒子系统和着色器。与此同时,我们还将在我们的平台游戏中添加粒子效果,以增强游戏体验。
第七章。粒子效果和着色器
在上一章中,我们详细讨论了如何动画节点、控制、SceneKit 方法等。我们还讨论了处理场景动画。我们学习了 SKAction 类的属性和方法,同时了解了各种控制方式,例如手势识别或加速度计,通过这些方式可以玩游戏。
我们在我们的平台游戏中添加了玩家动画、控制和动作,这使得游戏玩起来非常有趣,学习起来也很有趣。
在本章中,我们将研究在 Sprit Kit 游戏中的粒子效果和着色器。粒子效果是 Sprite Kit 提供的非常令人兴奋的能力。我们可以使用 SKEmitterNode 对象生成粒子;这些粒子可以创建如雨、火、景深、火花等美丽的视觉效果。着色器是在 iOS 8 中引入到 Sprite Kit 中的。着色器用于为场景提供定制的特殊效果。SKShader 类用于在我们的 Sprite Kit 游戏中包含着色器。
粒子效果
游戏中的粒子效果是一种技术,使用小精灵或其他图形对象来模拟扩散效果,例如,通过粒子系统渲染以下效果是非常常见的:
-
火
-
爆炸
-
烟雾
-
流动的水
-
落叶
-
云朵
-
雾
-
雪
-
灰尘
-
流星
-
星星
-
星系
-
轨迹
粒子效果中的整个行为都是由发射器节点定义的。Sprite Kit 中的粒子类似于 SKSpriteNode 对象,其中它渲染一个纹理或非纹理图像,该图像可以在场景中调整大小、着色或混合。

不同效果的示例
SKEmitter 节点
SKEmitterNode 对象是一个节点,它可以自动创建和渲染小精灵。我们可以从我们的 Xcode 中配置发射器节点的属性。我们使用粒子发射器编辑器来完成这个目的。
我们可以使用目标节点来改变粒子的目的地。以下是一个示例代码片段,演示我们如何实现相同的功能。
// CREATING THE EMITTER NODE
var emitter:SKEmitterNode = (fileNamed: "PlayerCollide.sks")
// SETTING THE EMITTER POSITION AND NAME
emitter.position = CGPointMake(0,-40)
emitter.name = "playerCollide"
// SEND THE PARTICLES TO THE SCENE
emitter.targetNode = self.scene
// ADDING EMITTER NODE
self.addChild(emitter)
让我们讨论在实现 Sprite Kit 的发射器节点时使用的属性和方法。
创建粒子效果
Sprite Kit 提供了属性和变量来根据游戏需求自定义粒子效果。让我们讨论这些属性和变量:
-
var particleBirthRate: 在这个属性中,你定义了发射器每秒创建的粒子数量。默认值是0.0。 -
func advanceSimulationTime(sec:NSTimeInterval): 这个方法帮助你推进发射器粒子的模拟。它以秒为单位接受参数,这是模拟所需的时间。最好在将发射器节点添加到场景后,使用此方法让发射器节点忙于粒子。 -
var numParticlesToEmit: 在这个属性中,你定义了发射器必须发射的粒子数量。默认值是0,这意味着发射器创建无限数量的粒子。 -
func resetSimulation (): 这个方法会移除所有粒子并重新启动模拟。重置模拟会清除其内部状态。 -
var targetNode: 如前所述,我们可以使用targetNode来改变粒子的目标位置。如果该属性为nil,则粒子被视为发射节点下的子节点。当此属性指向目标节点时,新粒子被视为目标节点的子节点,但先前生成的粒子将基于发射节点的属性进行计算。其默认值是nil。
用于确定粒子寿命的属性
这是用户创建的粒子保持存活和功能的时间。当寿命耗尽并降至零以下时,粒子将被销毁。
-
var particleLifetime: 这个属性决定了粒子在秒内的平均寿命。其默认值是0.0。 -
var particleLifetimeRange: 我们在这个属性中指定一个范围,粒子的寿命将在该范围内随机确定。
现在是时候在我们的 Platformer 游戏中添加粒子效果了。
在我们的平台游戏 Platformer 中添加粒子效果
让我们在 Platformer 游戏中集成粒子效果,在玩家碰撞时进行。我们将在方块和玩家碰撞时进行粒子模拟。
作为实现的一个初始步骤,让我们创建一个粒子效果。转到 项目导航器 并添加新的 文件 | SpriteKit 粒子文件 | Spark | 创建。

您可以从一系列粒子模板中选择,例如 Snow(雪),Bokeh(散景),Fire(火焰),Rain(雨),Spark(火花)等等。这里我们使用的是 Spark 效果模板:

打开我们刚刚创建的 ParticleEffectPlayerCollide.sks 文件。粒子效果文件以 .sks 扩展名保存。您可以使用粒子发射器编辑器更改所选粒子效果的不同属性,您可以在右侧访问该编辑器。

Sprite Kit 中的粒子发射器编辑器
让我们讨论一些在 SpriteKit 粒子发射器 面板上显示的属性:
-
粒子纹理(Particle Texture): 您可以选择用于创建粒子的图像。对于粒子纹理,也可以使用与项目相关的图像。要分配图像,必须记住,复杂且较大的图像将需要过度使用资源。建议使用简单且小的图像。
-
出生率(Birthrate): 这个属性用于设置发射器生成粒子的速率。如果出生率高于粒子效果,看起来会更加密集。因此,始终建议遵循较低的出生率以获得最佳帧率。
-
生命周期:此属性将定义粒子在屏幕上的总生命周期。这里的范围指的是从第一个值+或-范围内的随机值。
-
位置范围:此属性将告诉您效果应距离原发射节点多远,使用X和Y坐标值。此属性的变化会影响发射器的大小。
-
角度:此属性将告诉粒子效果应发生的角度。这也会使用起始和范围值。
-
速度:此属性将定义效果应发生的初始速度。这也会使用起始和范围值。
-
加速度:此属性将负责粒子从源发射器使用X和Y坐标出现的加速度。
-
透明度:此属性将负责效果的不透明度。这也会使用起始和范围值以及速度。
-
缩放:此属性将定义用于效果的纹理/图像的缩放位置。这也会使用起始、范围和速度值。
-
旋转:此属性用于定义粒子效果的旋转速度。这也会使用起始、范围和速度值。
-
颜色混合因子:此属性用于定义粒子效果生命周期中使用的颜色。粒子在其生命周期中可能遵循不同的颜色。这将使用因子、范围和速度值来定义属性。
添加代码以方便粒子效果
在设置完所需的属性后,在GameScene.swift文件中创建一个粒子节点对象(SKEmitterNode对象):
var particlePlayerNode = SKEmitterNode(fileNamed: "ParticleEffectPlayerCollide.sks")
现在,在didMoveToView()方法中设置位置,并隐藏创建的粒子节点。最后,将粒子节点添加到玩家中。现在didMoveToView()方法应如下代码所示:
override func didMoveToView(view: SKView)
{
self.physicsWorld.contactDelegate = self
// JUMP BUTTON POSITION SETTING AND ADDING ONTO THE SCREEN
self.btnJump.position = CGPointMake
(-(self.size.width/2.2), -(self.size.height/4))
self.addChild(btnJump)
//PROPERTIES FOR PARTICLE NODE CHAPTER 7
self.particlePlayerNode.zPosition = 1
self.particlePlayerNode.hidden = true
addBackGround()
addRunningBar()
addPlayer()
//ADDING PARTICLE NODE ON SCREEN (AS CHILD TO PLAYER)
self.player.addChild(self.particlePlayerNode)
addBlocks()
//addSpriteWithoutTexture()
}
现在,让我们定义这个粒子效果应在何时发生,在didBeginContact()方法中取消隐藏我们创建的particlePlayerNode,因为这个方法将在发生碰撞时被调用。
didBeginContact方法应如下所示:
func didBeginContact(contact: SKPhysicsContact)
{
// SHOWING PARTICLE EFFECT WHEN COLLISION HAPPENS
self.particlePlayerNode.hidden = false
var inOutActionWhenPlayerDied = SKAction.scaleBy(0.5, duration: 0.5)
var upActionWhenPlayerDied = SKAction.moveToY(self.player.size.height * 4, duration: 2)
var removeFromParent = SKAction.self.removeFromParent()
self.player.runAction(SKAction.sequence
([inOutActionWhenPlayerDied,
inOutActionWhenPlayerDied.reversedAction(),
upActionWhenPlayerDied,removeFromParent]),
gotoMenuScreen)
}
这就是碰撞与粒子效果的外观:

现在,我们已经成功地将粒子效果添加到我们的平台游戏中,是时候讨论着色器和如何在我们的游戏中添加它们了。
着色器
Sprite Kit 中的着色器使SKScenenode能够以特殊、定制的绘制行为出现。这可以通过创建SKShader对象并分配自定义 OpenGL ES 片段着色器来实现。
如果自定义着色器(SKShader 对象)需要提供一个统一着色器,那么您需要创建一个或多个 SKUniform 对象,并将它们与您的着色器对象关联起来。着色器程序主要分为:
-
顶点着色器
-
片段着色器
让我们详细讨论这两个方面:
-
顶点着色器:这些着色器作用于每个顶点,大部分计算都在顶点部分完成。它们由 Sprite Kit 自动设置。由于这些着色器的计算主要在顶点部分完成,因此在形成过程中不会消耗太多 CPU 资源。
-
片段着色器:这些着色器是用 OpenGL 着色语言编写的。正如其名所示,它们作用于每个像素。它们使用非常重的计算,因此在需要太多着色器时会被避免。
![着色器]()
顶点和片段着色器的图形表示
使用自定义着色器的注意事项
如果您之前没有做过 GLSL 代码,编写自己的着色器是一项复杂的工作,但将着色器脚本添加到现有的 Sprite Kit 中是有意义的。
您可以从各种网站轻松获取着色器文件,并开始工作。例如,从 www.shadertoy.com/ 或 www.glslsandbox.com 等网站,您将获得一个具有 .fsh 扩展名的简单文本文件。然后您只需将着色器代码添加到您需要的地方即可。
现在,让我们讨论一下在我们的游戏中初始化和创建新着色器对象。
新着色器对象的创建和初始化
方法如下所述:
-
Init! (name: string):此方法通过利用app包中具有.fsh文件扩展名的片段着色器文件来初始化一个新的着色器对象。您传递文件名作为参数,并返回一个新初始化的着色器对象。 -
Init (source: String!, uniforms: [AnyObject]!):此方法还使用指定的源初始化一个新的着色器对象。但除此之外,我们还可以设置一个要添加到着色器对象的uniforms列表。Uniforms 是访问片段着色器中数据的方式。Uniforms 对每个像素具有相同的值,例如,结果的图像大小。我们通过这个初始化器获得一个初始化的着色器对象。 -
Init (source: string!):此方法使用包含着色器对象初始source的string初始化一个新的着色器对象。
让我们讨论一下可以用于着色器对象中统一数据的属性和方法。
着色器中的统一数据
方法如下所述:
-
addUniform(uniform: SKUniform):此方法将一个uniform对象添加到着色器对象中。它接受要添加的uniform对象作为其参数。 -
removeUniformNamed(name: String):此方法从着色器对象中删除一个uniform对象。 -
uniformNamed(name: String): 此方法返回类似于特定uniform变量的uniform对象。如果未找到uniform对象,则返回nil。 -
var uniforms: [AnyObject]: 此属性包含与着色器相关联的所有uniforms列表。
为了保存自定义 OpenGL SL 着色器的统一数据,我们使用SKUniform对象。统一数据对所有包含uniform的着色器都是可用的。
在平台游戏中实现着色器
让我们在平台游戏中实现着色器,并更深入地了解着色器的集成。
-
让我们在游戏中创建一个新的
SKScene并在此处加载着色器。我们可以在菜单场景中放置一个按钮,它可以将我们带到这个场景。 -
现在将一个名为
ShaderDemo.swift的新swift文件添加到我们的项目中。 -
创建一个名为
box的SKSpriteKit节点,并导入一个 300 x 300 px 大小的图像box.png。盒子可以是任何颜色,但应该只有一个颜色,没有任何设计。我们使用这个盒子图像在盒子的边界内添加着色器效果。同时,在ShaderDemo.swift的didMoveToView()方法中设置盒子图像的位置:let box = SKSpriteNode(imageNamed: "box") let location = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame)) box.position = location self.addChild(box) -
接下来,我们必须创建实际的着色器程序,创建一个名为
blurShader.fsh的新空文件。我们可以从任何在线资源获取此着色器的代码。以下代码是从www.shadertoy.com获取的。感谢 Shadertoy 团队及其贡献者为我们提供了如此简洁的资源。《blurShade.fsh》文件应如下所示:void main() { #define iterations 256 vec2 position = v_tex_coord; // gets the location of the current pixel in the intervals [0..1] [0..1] vec3 color = vec3(0.0,0.0,0.0); // initialize color to black vec2 z = position; // z.x is the real component z.y is the imaginary component // Rescale the position to the intervals [-2,1] [-1,1] z *= vec2(3.0,2.0); z -= vec2(2.0,1.0); //vec2 c = z; vec2 c = vec2(-0.7 + cos(u_time) / 3.0,0.4 + sin(u_time) / 3.0); float it = 0.0; // Keep track of what iteration we reached for (int i = 0;i < iterations; ++i) { z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y); z += c; if (dot(z,z) > 4.0) { // dot(z,z) == length(z) ^ 2 only faster to compute break; } it += 1.0; } if (it < float(iterations)) { color.x = sin(it / 3.0); color.y = cos(it / 6.0); color.z = cos(it / 12.0 + 3.14 / 4.0); } gl_FragColor = vec4(color,1.0); } -
现在,我们只需要使用模式创建
SKShader对象。将blurshade.fsh作为文件名,并在didMoveToView()方法中将它添加到精灵节点中:let pattern = SKShader(fileNamed: "blurShade.fsh") box.shader = pattern -
由于
ShaderDemo.fsh已经准备好运行,让我们也在着色器场景中添加一个BACK按钮,以便用户可以返回到上一个屏幕。ShaderDemo.swift应如下所示:class ShaderDemo : SKScene { var menuSceneInstance : MenuScene? override func didMoveToView(view: SKView) { let box = SKSpriteNode(imageNamed: "box") let pattern = SKShader(fileNamed: "blurShade.fsh") let location = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame)) box.position = location box.shader = pattern self.addChild(box) addBackLabel() } override func touchesBegan(touches: NSSet, withEvent event: UIEvent) { for touch: AnyObject in touches { let location = touch.locationInNode(self) let node = self.nodeAtPoint(location) gotoMenuScreen() } } func gotoMenuScreen() { let transitionEffect = SKTransition.flipVerticalWithDuration(2) menuSceneInstance = MenuScene(size: self.size , playbutton: "Play", background: "BG") menuSceneInstance!.anchorPoint = CGPoint(x: 0.5, y: 0.5) self.view?.presentScene(menuSceneInstance , transition:transitionEffect) } func addBackLabel() { var backbutton = SKLabelNode(fontNamed: FontFile) backbutton.fontColor = UIColor.blueColor() backbutton.name = "BACK" backbutton.text = "BACK" backbutton.position = CGPointMake(CGRectGetMinX(self.frame) + backbutton.frame.width/2 , CGRectGetMinY(self.frame)) backbutton.zPosition = 3 self.addChild(backbutton) } }以下图像显示了代码着色器效果的外观:
![在平台游戏实现着色器]()
-
让我们也在
MenuScene.swift中设置一个按钮,以便用户可以转到ShaderDemo场景。以下是为添加此按钮的代码:var shaderSceneInstance : ShaderDemo? func addShaderSceneBtn() { var backbutton = SKLabelNode(fontNamed: FontFile) backbutton.fontColor = UIColor.blueColor() backbutton.name = "SHADOWS" backbutton.text = "SHADOW EFFECT" backbutton.position = CGPointMake(CGRectGetMinX(self.frame) + backbutton.frame.width/2 , CGRectGetMinY(self.frame)) backbutton.zPosition = 3 self.addChild(backbutton) }
现在,代码已经准备好了。为了展示,让我们也为按钮添加一个过渡效果,以便在点击时呈现着色器场景:
func goToShaderScene(){
let transitionEffect = SKTransition.flipHorizontalWithDuration(1.0)
shaderSceneInstance = ShaderDemo(size: self.size)
shaderSceneInstance!.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.view?.presentScene(shaderSceneInstance , transition:transitionEffect)
}
通过检查节点名称是否等于"SHADOWS"来从touchesBegan()方法中调用此方法,因为我们想在菜单屏幕上同时有播放按钮和阴影效果按钮:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
let node = self.nodeAtPoint(location)
if node.name == PlayButton.name {
goToGameScene()
//goToShaderScene()
}
else if node.name == "SHADOWS"
{
goToShaderScene()
}
}
}
func goToShaderScene(){
let transitionEffect = SKTransition.flipHorizontalWithDuration(1.0)
shaderSceneInstance = ShaderDemo(size: self.size)
shaderSceneInstance!.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.view?.presentScene(shaderSceneInstance , transition:transitionEffect)
}
现在文件已经准备好运行了。以下截图显示了主菜单屏幕的外观:

摘要
在本章中,我们详细学习了粒子效果和着色器。我们讨论了SKEmitterNode对象和SKShader对象,以及如何在我们的平台游戏中实现它们。我们还讨论了在 Sprite Kit 项目中添加 OpenGL ES 代码,以及我们如何在游戏中利用着色器。现在,当玩家与方块碰撞时,我们的平台游戏会有粒子效果,着色器场景是一个独立的屏幕,用于显示着色器效果。
在下一章中,我们将为游戏添加关卡,这将进一步提高用户的游戏体验,并帮助他们更详细地理解游戏概念。我们还将添加一个暂停按钮,以便在需要时暂停游戏。
第八章。处理多个场景和层次
在上一章中,我们讨论了 Sprite Kit 的一个非常重大的主题,那就是粒子效果和着色器。我们还讨论了SKEmitterNode对象和SKShader对象。我们还在我们的平台游戏中实现了它们。在我们的游戏中实现着色器是最有趣的部分。
在本章中,我们将讨论游戏的一个重要方面,那就是增加多个层次。在游戏中拥有各种层次会使游戏更加有趣,因为随着层次的增加,游戏的复杂性也会增加,使其更具挑战性。随着游戏中增加各种层次,添加暂停按钮也变得很重要,这样游戏就可以在需要时暂停。
优化游戏层次
如果一个游戏以相同的难度持续运行,将会变得单调乏味,用户很快就会失去兴趣。那么,你如何让你的游戏保持有趣、刺激和具有挑战性?如果你的游戏不断增加难度并为用户添加新的挑战,它将一直保持到游戏结束。
游戏中的不同层次不过是游戏的部分或章节。通常,在大多数游戏中,游戏场景被划分为多个层次。层次将游戏划分为小节,并且一次只加载一个层次。在游戏中,层次可以用不同的名称表示,如回合、阶段、章节、幕、地图、世界等。不同的层次可以通过名称或数字来表示。如果用数字系统来表示层次,那么一个明显的类比是数字越高,层次越高。
层次的名称是游戏层次的第一印象;建议对此进行简要思考。让我们来讨论一下层次的命名:
-
功利主义:这主要包含一个数字系统或任何其他类似类比。这个系统给玩家提供了一个关于他们进度感的想法。
-
位置:这需要使用层次的地理位置作为层次名称,如城市、村庄、城镇等。这给玩家一个关于他们将会看到什么的想法。
-
描述性:这些更像是书的章节。这包括诸如飞艇堡垒、绿色山丘区、秋之柱等名称。
这是你可以为你的游戏层次命名的主要三种方式。除此之外,还有一种方法可以为层次起一些俏皮的名字。这完全取决于你如何命名你的层次。
要完成一个游戏层次,用户必须通过一些限制或困难,例如达到一定的分数点或执行特定任务以到达下一个层次。这通常被称为游戏进度。
程序员通常以以下两种方式之一创建不同的层次:
-
新境界,新层次:在这个方法中,为每个新层次创建一个新的场景。
-
单个场景中的多级:如果新级别在精灵或其他游戏元素上没有太多变化,我们也可以在单个场景中实现多级功能。对于每个新级别只改变一个或两个元素的游戏,单个场景可能是一个不错的选择。
多级策略
游戏中的不同级别定义了游戏的难度,或者某些级别表示用户在游戏中当前应该达到的点。
在大多数游戏中,级别 1 将是第一个级别的第一个SKScene类的名称。当从一个级别过渡到另一个级别或描述任何其他效果、动画或信息时,随着级别的增加,也有许多好的过渡选项可供选择。
我们可以使用数组或字典来存储玩家数据,例如物品、健康、达到的级别等。除非您有大量数据需要保存,否则NSUserDefaults可能是最佳选择。
Core Data
对于游戏较大的数据存储需求,我们可以使用单独的数据文件。对于此类要求,Apple 提供了一款强大的工具,即 Core Data。这个工具对于存储级别信息、用户信息等非常有用。
什么是 Core Data?它是由 Apple 提供的一个框架,充当游戏与 SQLite 和其他存储环境之间的桥梁。就像 SQL 一样,您可以有表、关系和查询。与 SQLite 相比,Core Data 的优势在于它不需要语法,并且与关系数据库不同,它表示对象和类。

Core Data 中的重要术语如下:
-
管理对象模型:这是一个允许您建模类(实体)、关系和查询的工具。(这是由 Core Data 框架使用的)。
-
管理对象[每行一个对象]:这指的是在您的游戏中创建的对象。这些是您的数据类,例如玩家信息、级别信息等。每个管理对象代表您表(实体)中的一行。
-
管理对象上下文:这是一个重要的对象,因为它管理在模型中定义的所有上下文对象之间的关系。它还跟踪上下文对象的状态。所有与底层数据库的交互都通过上下文进行。管理对象上下文请求持久协调器以获取数据,并在必要时告诉它保存数据。
-
持久存储协调器:通过持久存储协调器,我们在设备上提供了一个数据存储位置。
-
持久对象存储:这是设备上的一个数据存储环境。
在我们的平台游戏平台中添加级别
让我们在平台游戏中添加级别。我们将在一个场景中添加级别。对于级别难度的变化,我们可以增加玩家跑步的速度,并可以指定一个距离,在该距离后级别将增加。
现在,在我们深入添加游戏中的层级之前,首先,我们应该了解正在玩的游戏层级。因此,我们将在游戏场景中添加层级标签,以便用户了解正在玩的游戏层级。
添加层级标签
层级标签是一个简单的文本,它将在游戏场景中显示,并作为识别正在玩的游戏层级的部分信息。如前所述,层级的名称可以是数字、位置或描述。作为层级标识符的数字,在游戏中是最常见的词源。在我们的平台游戏中,我们使用数字作为层级标签。
在GameScene.swift文件中添加以下代码。此代码将在我们的游戏中添加level标签功能:
func addLevelLabel()
{
self.levelLabel.text = "Level: 1"
self.levelLabel.fontSize = 30
self.levelLabel.zPosition = 3
self.levelLabel.position = CGPointMake(CGRectGetMidX(self.frame) + scoreText.frame.width , CGRectGetMidY(self.frame) + levelLabel.frame.height * 4.2)
self.addChild(self.levelLabel)
}
// ADDING LEVELS
let levelLabel = SKLabelNode(fontNamed: "Chalkduster")
var level = 1
addLevelLabel()
在前面的代码中,我们使用SKLabelNode添加了level标签,并应用了字体chalkduster。初始层级设置为1,然后从那里开始递增。
这是在添加了用于标识层级:l的label后游戏的外观:

添加层级
随着游戏的进行,难度级别会增加。难度级别的增加可以根据任何数量的因素进行;我们可以当玩家跨越特定数量的方块时增加层级,或者当分数达到一定限制时,或者当时间增加时增加层级。
在我们的平台游戏中,我们将根据跨越的方块数量增加难度级别。随着游戏的进行,我们将识别出需要下一层的情况,如下所示:
-
层级 1:这个层级在游戏开始时加载。
-
层级 2:当玩家从第五个方块跳起时,我们将开始第二层。
-
层级 3:当玩家从第十个方块跳起时,我们将开始第三层。
-
最后一层:当玩家从 20 个方块跳起时,我们将引入平台游戏的最后一层。
随着层级的每次提升,我们也将增加游戏的难度。在我们的游戏中,我们将增加地面速度,这将使游戏更难玩。
要添加层级功能,需要在GameScene.swift文件中的blockRunner方法中添加。以下是添加了功能的代码:
func blockRunner()
{
// LOOP FOR THE DICTIONARY TO GET BLOCKS
for(block, blockStatus) in self.blockStatuses
{
var thisBlock = self.childNodeWithName(block)!
if blockStatus.shouldRunBlock()
{
blockStatus.timeGapForNextRun = random()
blockStatus.currentInterval = 0
blockStatus.isRunning = true
}
if blockStatus.isRunning
{
if thisBlock.position.x > blockMaxX // IF IT IS POSITIVE (KEEP MOVING BLOCKS FROM RIGHT TO LEFT)
{
thisBlock.position.x -= CGFloat(self.groundSpeed)
}
else // #1
{
thisBlock.position.x = self.origBlockPositionX
blockStatus.isRunning = false
self.numberOfBlocksCrosssed += 1
self.levelLabel.text = "Level: \(String(self.level))"
if self.numberOfBlocksCrosssed == 5
{
self.level = level + 1
self.groundSpeed = self.groundSpeed + 7
}
else if self.numberOfBlocksCrosssed == 10
{
self.level = level + 1
self.groundSpeed = self.groundSpeed + 9
}
else if self.numberOfBlocksCrosssed == 20
{
self.level = level + 1
self.groundSpeed = self.groundSpeed + 12
}
else if self.numberOfBlocksCrosssed > 20
{
println("Final Level")
}
}
}
else
{
blockStatus.currentInterval++
}
}
}
在前面的代码中,在标记为#1的else语句内部,添加了增加层级的代码。该代码有一个嵌套的if,else if条件,其中我们检查了跨越的方块数量,并根据这个数量增加了层级和游戏的地面速度。
前面的代码中有四个描述层级和地面速度增加的语句。第二层在玩家跨越 5 个方块后开始,地面速度也会增加。同样,在跨越 10 和 20 个方块后,层级和地面速度也会增加。
现在,我们已经成功添加了在跨越一定数量的方块后增加层级的函数。
以下是如何在玩家穿越五个方块时,Level: 2 标签将看起来:

注意
对于拥有更多关卡的游戏,建议为关卡逻辑代码创建一个单独的文件。例如,如果我们游戏中拥有 10 个不同的关卡,那么我们也会创建一个单独的文件。
添加暂停功能
在游戏过程中暂停游戏是一个重要的功能。我们的游戏将受益于 pause 功能;它将允许玩家从上次离开的地方继续游戏。
让我们添加 pause 功能:
-
主要的,我们将为
GameScene创建一个 Play/Pause** 按钮,并配置按钮的位置和图像。我们将在GameScene.swift类内部添加以下代码行:var pauseBtn:SKSpriteNode = SKSpriteNode(imageNamed: "PLAY-PAUSE") -
设置
pauseBtn标签的属性,例如size、position等,就像我们在addPlayPauseButton()方法中为其他标签所做的那样。这将是这样看起来:func addPlayPauseButton() { //self.runAction(sound) self.pauseBtn.name = "PAUSE" self.pauseBtn.zPosition = 3 self.pauseBtn.position = CGPointMake(CGRectGetMaxX(self.frame) - pauseBtn.frame.width/2 , CGRectGetMaxY(self.frame) - pauseBtn.frame.height/2) self.addChild(pauseBtn) }请确保您也从
didMoveToView()方法中调用它。 -
现在,我们必须添加实际暂停游戏的功能。我们通过在
touchesBegan()方法中添加以下代码来实现这一点:if self.pauseBtn.containsPoint(location) { if(self.view?.paused == false) { println("Game Scene is Paused") self.view?.paused = true } else { println("Game Scene is Resumed") self.view?.paused = false } }上述代码将在按钮被按下时暂停游戏,如果按钮再次被按下,游戏将继续。
以下截图显示了添加 pause 功能后游戏的外观;截图的右上角出现了一个暂停按钮:

注意右上角的暂停按钮;点击此按钮将暂停游戏。
添加 NODE MENU 按钮
我们创建了一个显示游戏节点示例的节点菜单场景。我们现在将在主菜单上添加一个按钮,允许用户访问节点菜单场景:
-
首先,我们必须在开始时使用以下代码行创建
NodeMenuScene的实例:var nodeMenuSceneInstance : NodeMenuScene? -
现在,我们必须设置 NODE MENU 按钮标签的属性,就像我们之前为 Level: 标签所做的那样。为此,在
addNodeMenuSceneBtn()方法中添加以下代码,并从didMoveToView()方法中调用它:func addNodeMenuSceneBtn() { var backbutton = SKLabelNode(fontNamed: "Chalkduster") backbutton.fontColor = UIColor.cyanColor() backbutton.name = "NODEMENU" backbutton.text = "NODE MENU" backbutton.position = CGPointMake(CGRectGetMaxX(self.frame) - backbutton.frame.width/2 , CGRectGetMaxY(self.frame) - backbutton.frame.width/8) backbutton.zPosition = 3 self.addChild(backbutton) } -
现在,在
touchesBegan()方法中添加以下代码,通过点击我们刚刚创建的NODEMENU按钮来移动到节点菜单场景:else if node.name == "NODEMENU" { goToNodeMenuScene() } -
使用以下代码创建从我们当前场景的过渡:
func goToNodeMenuScene() { let transitionEffect = SKTransition.flipHorizontalWithDuration(1.0) nodeMenuSceneInstance = NodeMenuScene(size: self.size) nodeMenuSceneInstance!.anchorPoint = CGPoint(x: 0.5, y: 0.5) self.view?.presentScene(nodeMenuSceneInstance , transition:transitionEffect) }
在前面的代码中,我们创建了 goToNodeMenuScene() 方法,并为场景从一个切换到另一个添加了水平翻转的过渡效果。
以下截图显示了创建访问节点菜单场景按钮后,主菜单将看起来如何:

当有人点击按钮,NODE MENU,节点菜单场景将在屏幕上打开。
摘要
在本章中,我们在平台游戏中增加了难度级别。我们通过创建关卡标签和关卡递增功能来更新了我们的游戏。现在提供了一个重要功能,暂停。此外,我们还学会了如何通过节点菜单按钮集成节点菜单场景来在我们的游戏中添加场景。
在下一章中,我们将讨论性能提升技术,以及将要添加到我们的平台游戏中的重要额外功能。
第九章. 性能提升与附加功能
在上一章中,我们讨论了在我们的游戏中添加多个级别;在大多数游戏中,添加多个级别是一个正常的功能。我们还添加了使用级别标签显示游戏当前级别的功能。除此之外,我们还添加了暂停按钮和从主菜单访问节点菜单场景的按钮。
本章是本书中最重要的章节之一;在这里,我们将讨论性能提升的技巧和窍门。除此之外,我们还将为我们的游戏添加一些非常重要的功能。这些功能包括:
-
评分系统
-
声音
-
玩家的运行动画
声音是游戏的一个基本组成部分;它极大地增强了玩家的整体游戏体验。评分系统帮助玩家衡量其随时间的变化表现。运行纹理在游戏中产生良好的动画效果,大大增加了游戏体验。我们将在这章中添加所有这些功能,并讨论一些 Sprite Kit 游戏的重要性能提升技术。
性能提升
运行游戏需要大量使用设备的内存和其他资源。这导致电池加速耗尽。我们需要优化游戏对设备资源的利用。游戏需要更高的帧率,因此由于对设备资源的过度使用,电池耗尽会更多。一个优化的游戏将导致设备资源的有效使用,从而减少电池耗尽。以下是优化游戏效率的一些最佳实践:
-
在场景中系统化游戏内容
-
提高绘图性能
-
使用
SKAction和约束提高性能 -
提高物理性能
-
提高形状的性能
-
提高效果性能
-
提高光照
现在我们将详细讨论之前列出的每种方法。
在场景中系统化游戏内容
如我们所知,场景是 Sprite Kit 游戏中基本的构建块。根据需求,一个游戏可以包含多个场景。一个场景可以包含多个节点,节点可以执行特定的动作。我们清楚地了解如何创建场景、节点以及节点的动作。具有挑战性的任务是设计游戏场景和转换,以便不会降低游戏性能。
应当记住的一件事是,场景在传统 iOS 应用中与故事板不同,它们没有默认的行为。相反,我们为各自的场景定义和实现行为,这可能包括以下内容:
-
何时创建新的场景
-
定义场景的内容
-
定义场景间转换应何时发生
-
定义转换的视觉效果
-
定义数据如何从一个场景传输到另一个场景
通过预加载纹理来提高性能
这是提高游戏性能最强大的方法之一。Sprite Kit 提供了两种相同的方法:
-
func preloadWithCompletionHandler(completionHandler: () -> Void): 此方法使用一个负责将图集纹理加载到内存中的函数,该函数需要参数completionHandler,在任务完成后调用。 -
func preloadTextureAtlases(textureAtlases: [AnyObject]!, -
withCompletionHandler completionHandler: (() -> Void)!): 此方法将多个图集的纹理加载到内存中,并在任务完成后调用完成处理程序。完成处理程序期望两个参数:一个是textureAtlases,它是一个SKTextureAtlas对象的数组,另一个参数是completionHandler,在图集纹理加载后调用。
使用纹理图集可以减少绘制调用,从而减少对设备资源的使用。到目前为止,我们已经讨论了一些游戏性能提升的重要技术。现在,是时候讨论一些游戏的基本元素了,例如得分系统、声音等。
提高绘制性能
构建节点树的最大部分是组织需要绘制的图形内容。我们应该注意先绘制什么,最后绘制什么。有两个因素会影响绘制性能:
-
绘制顺序,即图形提交给引擎的顺序
-
通过资源共享来完成绘制
关于绘制顺序,你可以设置节点树的兄弟顺序以减少通过忽略兄弟顺序提交的绘制次数:
View.ignoreSiblingOrder = true
你可以使用深度顺序作为规则将它们一起批处理,并使用纹理图集进一步优化批处理。
确保开启性能指标,如每秒帧数(FPS)、节点计数、绘制计数和四边形计数。这些指标将帮助你确定游戏的性能。以下是我们可以使用来查看性能指标的代码:
View.showsFPS = true // #1
View.showsNodeCount = true //#2
View.showsDrawCount = true //#3
View.showsQuadCount = true //#4
关于前面的代码块,让我们讨论每个指标:
-
在代码
#1中,我们显示游戏场景中每秒的帧数。游戏的最佳帧率是60。在游戏中显示帧率,可以轻松测量帧率。 -
在代码
#2中,我们显示场景中SKNodes的数量。场景中的节点越少,性能越好。一个游戏需要节点来拥有游戏中的元素,但我们可以一起测量帧率和节点,以确保有多少节点产生了最佳的帧率。 -
在代码
#3中,我们显示场景计数的批次数,即场景将要绘制的批次数。你的游戏绘制的次数越少,性能越好。 -
在代码
#4中,我们显示了四边形数量。Sprite Kit 将节点树转换为渲染传递。每个渲染传递都使用四边形进行渲染。我们拥有的四边形数量越少,游戏性能越好。
使用 SKActions 和约束提高性能
提高性能的主要解决方案是构建一次动作并尽可能多地使用它。尽量避免在update()方法中使用自定义动画代码。通过使用SKAction和SKConstraint类,你可以优化游戏中的动画效果。
提高物理性能
每当SKScene计算动画的新帧时,它会模拟节点树连接的物理体上的力和碰撞效果。它为每个物理体计算最终的位置、方向和速度。
关于提高游戏性能,动态对象比静态对象成本更高,因此如果可能的话,我们可以设置以下属性,以便性能逐渐提高。
一些指导原则如下:
-
你应该使用碰撞掩码来分组对象以提高性能
-
你可以使用力场来代替游戏逻辑
-
如果需要,你应该打开场调试绘制
在将指定的边界分配给物理体之前,你必须考虑你对象的最有效形状。边界的形状定义了设备需要执行的计算/操作的数量,这影响了效率。圆形是最便宜的,其次是矩形、多边形、复合体和Alpha 掩码,计算成本按顺序增加。

不同形状边界的计算成本比例
提高形状的性能
对象节点的形状在游戏性能方面起着重要作用。如果节点需要较少的计算,性能将会提高。
与物理性能主题中描述的相同,你可以提高形状节点的效率成本。在性能成本方面,多边形是最便宜的,其次是曲线、线性描边、描边曲线和填充曲线,计算成本按顺序增加。

形状节点的性能成本比例
提高效果的性能
关于 Sprite Kit 中的效果,SKEffectNodes成本较高,因此请尽量少用。它在其屏幕外进行渲染并将结果传输到帧缓冲区,这降低了效率。当不需要屏幕外传递时,最好使用SKShaders。
如果效果变化不大,最好通过使用shouldRasterize属性来光栅化这些效果。如果shouldRasterize属性为true,效果节点将缓存图像以供未来帧使用。
提高光照性能
照明是按像素计算的,因此计算成本与被照亮的像素数量成正比。环境光不会消耗计算能力。阴影的计算成本与灯光的数量成正比,因此建议保持阴影数量较低。
使用仪器测量性能
Instruments 是苹果在 Xcode 中提供的性能测量和测试工具,用于代码的跟踪和剖析。Instruments 有助于分析代码的性能。有许多仪器可用于检查性能问题、内存泄漏或其他问题。
一旦识别出任何问题,就很容易纠正。您还可以查看我们的游戏缓存了多少,并根据这一点对游戏中的资产做出决定。
您可以通过导航到Xcode | 打开开发者工具 | Instruments来访问仪器。然后,您可以选择合适的仪器进行操作。在开发过程的初期阶段查看分析会更好,这样您可以轻松地了解代码中的哪些包含导致了错误。
Instruments 为您提供一个跟踪模板列表。跟踪模板是一组预配置的仪器。让我们详细讨论每个跟踪模板:

-
活动监视器:活动监视器用于监控 CPU、内存、磁盘和网络使用统计的进程。
-
分配:分配工具用于跟踪进程的匿名虚拟内存和堆。
此工具还提供了对象的类名以及可选的保留/释放历史记录。
-
自动化:自动化模板执行一个脚本,该脚本模拟 iOS 应用程序的 UI 交互,该应用程序从仪器启动。
-
Cocoa 布局:Cocoa 布局观察
NSLayoutConstraint对象的变化,以帮助确定何时以及在哪里布局约束消失了。 -
核心动画:核心动画仪器通过时间剖析测量应用程序的图形性能以及进程的 CPU 使用情况。
-
核心数据:此仪器模板跟踪核心数据文件系统活动,包括检索、缓存未命中和保存的缓存。这在第八章 处理多个场景和级别中已有讨论。
-
计数器:计数器将收集性能监视器计数器(PMC)事件,使用基于时间或事件采样的方法。
-
调度:此模板将监控调度队列的活动,并记录块调用及其持续时间。
-
能源诊断:此模板将提供有关能源使用以及主要设备组件的基本开/关状态的诊断信息。
-
文件活动:这将监控文件和目录活动,包括文件打开/关闭调用、文件权限修改、目录创建、文件移动等。
-
GPU 驱动程序:此模板用于测量 GPU 驱动程序统计信息,并且它还采样活动 CPU 使用情况。
-
泄漏:泄漏将测量一般内存使用情况;它定期扫描是否有对象被创建但没有被访问,并检测由此产生的内存损失。
-
网络:网络分析使用连接工具分析应用程序如何使用 TCP/IP 和 UDP/IP 连接。
-
OpenGL ES 分析:此模板测量和分析 OpenGL ES 活动,以检测 OpenGL ES 精度和性能问题。它还提供了解决这些问题的建议。
-
突然终止:突然终止用于分析目标进程的突然终止支持,报告文件系统访问和突然终止启用/禁用调用的跟踪。
-
系统跟踪:此工具提供系统信息,例如进程名称、生成的线程数、每个线程的 CPU 使用情况等。
-
系统使用:此模板用于记录通过工具启动的单个进程与文件、套接字和共享内存相关的 I/O 系统活动。
-
时间分析器:时间分析器用于执行低开销的时间采样,我们可以检查系统 CPU 上运行的过程的状态。分析是一种测量方法,通过分析会话的输出,你可以了解代码中哪些部分被使用得最多,并告诉你哪些代码部分可以改进。
-
僵尸:如果一个游戏删除了一个对象,但在稍后阶段试图访问该对象,它将使游戏崩溃。僵尸工具将删除的对象保持为已死亡状态,并在游戏调用时释放它,从而避免崩溃。这样,僵尸工具就指出了游戏可能崩溃的地方。调试器无法定位这种异常。
游戏中的评分系统
在游戏中添加评分或得分系统可以使游戏更有趣,更有趣。在游戏中拥有评分系统使玩家更容易衡量他们的表现,使用户的目标更加明确。
总是在主屏幕的某个位置显示分数是有意义的,这样玩家在玩游戏时可以查看分数。
在我们的平台游戏添加评分系统
在我们游戏添加评分系统的第一步,我们创建一个标签节点来向玩家显示分数。初始变量将为零。
创建分数标签
让我们在GameScene类的开头添加以下代码片段:
let scoreText = SKLabelNode(fontNamed: "Chalkduster")
var score = 0
在前面的代码中,你正在创建一个SKLabelNode并将其分配给字体Chalkduster。与此相关,你还在初始化一个变量score,其值为零。
现在,让我们将上面创建的ScoreText标签设置为 0。我们还可以在addScoreLabel()方法中设置字体的大小和位置,并从GameScene的didMoveToView()中调用此方法:
func addScoreLabel()
{
self.scoreText.text = "Score: 0"
self.scoreText.fontSize = 30
self.scoreText.position = CGPointMake(CGRectGetMinX(self.frame) + scoreText.frame.width / 1.8 , CGRectGetMidY(self.frame) + scoreText.frame.height * 4.2)
self.addChild(self.scoreText)
}
前面的代码将定义分数文本为分数:0和字体大小为30。此外,我们还定义了scoreText的位置。
以下是实现分数标签后的游戏屏幕将看起来如何:

在需要时增加分数
在我们的游戏中定义何时增加分数非常重要。同样,这也应该在创建的scoreText标签中显示。
由于我们的平台游戏处理作为障碍物的方块,当玩家跳过一个方块时,最好奖励玩家分数。
在blockrunner()方法中添加以下代码行,条件是方块应该成功跨越玩家的X位置而不与他碰撞(第一个else条件):
self.score = score + 10
self.scoreText.text = "Score: \(String(self.score))"
现在,为了保存最高分和用户的名字,我们将使用 iOS 提供的特殊功能,通过NSUserDefaults保存频繁需要的数据,如下所示:
self.highestScore = self.score
NSUserDefaults.standardUserDefaults().setObject(highestScore, forKey:"HighestScore")
NSUserDefaults.standardUserDefaults().setInteger(highestScore, forKey:"SCORE")
之前的代码需要在if语句blockStatus.isRunning的末尾之前添加。这段代码将成功增加分数。现在,是时候保存高分了。
保存高分
当用户得分高时,我们将添加一个弹出屏幕来保存高分。为了实现这一点,首先,我们必须创建一个新的场景,ScoreList.swift,并在玩家出局时调用此场景,即游戏结束时。
在我们的didBeginContact()方法中,我们有以下代码行:
self.player.runAction(SKAction.sequence(
[inOutActionWhenPlayerDied,
inOutActionWhenPlayerDied.reversedAction(),
upActionWhenPlayerDied,removeFromParent]),gotoMenuScreen)
将前面的行替换为以下行:
self.player.runAction(SKAction.sequence(
[inOutActionWhenPlayerDied,
inOutActionWhenPlayerDied.reversedAction(),
upActionWhenPlayerDied,removeFromParent]),
completion: gotoSavePlayerScreen)
新增的行在玩家死亡时添加了ScoreList场景。
现在,我们将创建一个名为gotoSavePlayerScreen()的新方法,以检查当前分数是否大于保存的分数。然后,应该调用ScoreList场景,否则调用主屏幕,即MainMenu场景。相应的代码如下:
func gotoSavePlayerScreen()
{
self.player.removeFromParent()
println("The Saved Score Is: \(savedScore)")
println("The Highest Score Is: \(highestScore)")
if self.highestScore > savedScore
{
let transitionEffect = SKTransition.doorsCloseHorizontalWithDuration(1.5)
highScorerListInstance = ScoreList
(size: self.size) // , playbutton: "Play", background: "BG")
highScorerListInstance!.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.view?.presentScene(highScorerListInstance , transition:transitionEffect)
}
else if self.highestScore <= savedScore
{
gotoMenuScreen()
}
}
我们已经实现了在游戏完成后打开场景的方法。现在,让我们构建ScoreList场景。
创建保存高分的场景
让我们创建一个ScoreList场景来显示保存最高分的弹出窗口。
此外,还需要添加一个标签来恭喜用户。以下是为同一目的的代码:
func congratsUserAndSaveScorerName()
{
var congratsUserLabel = SKLabelNode(fontNamed: "Chalkduster")
congratsUserLabel.fontColor = UIColor.redColor()
congratsUserLabel.name = "CONGRATS"
congratsUserLabel.color = UIColor.lightGrayColor()
congratsUserLabel.text = "Congratulations!! "
congratsUserLabel.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame) + congratsUserLabel.frame.height * 2)
congratsUserLabel.zPosition = 3
self.addChild(congratsUserLabel)
}
如果用户不想用名字保存分数,我们需要添加一个取消按钮。请从ScoreList.swift的didMoveToView()方法中添加以下代码,其中取消按钮将带您进入MenuScene:
func addCancelBtn()
{
var Cancelbutton = SKLabelNode(fontNamed: FontFile)
Cancelbutton.fontColor = UIColor.blueColor()
Cancelbutton.name = "CANCEL"
Cancelbutton.text = "CANCEL"
Cancelbutton.position = CGPointMake(CGRectGetMinX(self.frame) + Cancelbutton.frame.width/2 , CGRectGetMinY(self.frame))
Cancelbutton.zPosition = 3
self.addChild(Cancelbutton)
}
func gotoMenuScreen()
{
self.playerNameTextField.removeFromSuperview()
let transitionEffect = SKTransition.flipHorizontalWithDuration(1.0)
menuSceneInstance = MenuScene(size: self.size , playbutton: "Play", background: "BG")
menuSceneInstance!.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.view?.presentScene(menuSceneInstance , transition:transitionEffect)
}
前面的代码在蓝色中添加了一个取消按钮,点击此按钮将玩家带到MenuScene。现在,为了处理取消按钮的点击,请在ScoreList.swift的touchesBegan()方法中的触摸循环内添加以下代码:
if node.name == "CANCEL"
{
gotoMenuScreen()
}
成绩列表已经成功创建。我们也为用户添加了一个取消按钮。现在,是时候添加一个文本框,玩家将在其中添加他的/她的名称。
添加文本框以保存玩家名称
我们需要显示一个文本框供用户输入要保存的玩家名称。添加以下代码行以在框架内插入一个文本字段:
let playerNameTextField = UITextField(frame: CGRectMake(50, 150, 250, 50))
以下方法将创建要使用的文本框:
func addPlayerNameTextBox()
{
playerNameTextField.center = CGPointMake(self.size.width / 2, self.size.height / 2)
playerNameTextField.backgroundColor = UIColor.whiteColor()
playerNameTextField.placeholder = "Enter Your Name"
playerNameTextField.borderStyle = UITextBorderStyle.RoundedRect
self.view?.addSubview(playerNameTextField)
}
现在,让我们添加一个UITextFieldDelegate的textFieldShouldReturn方法,以便在文本框中输入玩家名称并点击回车键时使键盘消失:
func textFieldShouldReturn(playerNameTextField: UITextField) -> Bool
{
println("Text Field Return Key")
playerNameTextField.resignFirstResponder()
return true
}
现在,在Scorelist类开始时添加UITextFieldDelegate代理。这个代理使键盘出现。
前面的代码片段将在按下回车键后成功使键盘消失。现在,下一个任务是将添加的名称保存。

这是键盘打开时的屏幕外观
保存高分玩家的玩家名称
我们将命名按钮为添加玩家。这个按钮将使用户输入的名称与高分一起保存。首先,创建以下节点,命名为add-player,并带有图像:
let addPlayerButton = SKSpriteNode(imageNamed:"add-player")
添加以下代码方法来设置添加玩家按钮的属性。同时,确保从didmoveToView()方法中调用相同的代码:
func addScoresSceneBtn()
{
addPlayerButton.name = "SCORES"
self.addPlayerButton.position = CGPointMake(CGRectGetMidX(self.frame),CGRectGetMinY(self.frame)/3)
self.addChild(self.addPlayerButton)
}
declare the following variable before adding didMoveToView() method
var highestScorerName:String = String()
在ScoreList.swift的touchesBegan()方法中添加以下代码行,如前所述,在触摸循环中处理添加玩家按钮的点击:
if node.name == "SCORES"
{
if playerNameTextField.text.isEmpty
{
playerNameTextField.placeholder = "Please Enter the Player Name"
}
else
{
self.highestScorerName = self.playerNameTextField.text NSUserDefaults.standardUserDefaults().setObject(highestScorerName, forKey:"HighestScorerName")
NSUserDefaults.standardUserDefaults().synchronize()
gotoMenuScreen()
}
}
我们还添加了gotoMenuScene()来返回主菜单,正如我们所知。以下是它的代码:
func gotoMenuScreen()
{
self.playerNameTextField.removeFromSuperview()
let transitionEffect = SKTransition.flipHorizontalWithDuration(1.0)
menuSceneInstance = MenuScene(size: self.size , playbutton: "Play", background: "BG")
menuSceneInstance!.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.view?.presentScene(menuSceneInstance , transition:transitionEffect)
}
现在,SceneList.swift的工作已经完成。是时候开始处理高分榜了。以下截图显示了屏幕将如何显示:

创建高分榜
到目前为止,我们已经保存了获得高分玩家的名称,但我们还没有创建一个分数榜来向玩家显示高分。最好直接从主菜单访问分数榜,因为它使操作更方便。
在我们的游戏中,我们将创建一个高分菜单场景,在主菜单中有一个按钮可以访问此屏幕。
首先,创建一个名为AddScoreScene.swift的场景,以显示高分。
现在,创建一个名为showHeightestScorerName()的方法来显示得分最高玩家的名称,并从AddScoreScene.swift文件中的didMoveToView()方法中调用相同的方法:
var savedScorerName: String = String()
func showHeighestScorerName()
{
if(NSUserDefaults.standardUserDefaults().objectForKey
("HighestScorerName")) == (nil)
{ savedScorerName = " "
}
else
{ savedScorerName = NSUserDefaults.standardUserDefaults().objectForKey
("HighestScorerName") as String
println(savedScorerName)
}
var highScorerNameLabel = SKLabelNode(fontNamed: "Chalkduster")
highScorerNameLabel.fontColor = UIColor.blueColor()
highScorerNameLabel.name = "HIGHESTSCORERNAME"
highScorerNameLabel.color = UIColor.lightGrayColor()
highScorerNameLabel.text = "High Scorer : \(savedScorerName)"
highScorerNameLabel.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame) + (highScorerNameLabel.frame.height * 2))
highScorerNameLabel.zPosition = 3
self.addChild(highScorerNameLabel)
}
我们已经显示了得分最高的玩家的名称,现在该显示玩家创下的最高分了。为此,创建showHeighestScores()方法,并从AddScoreScene.swift文件中的didMoveToView()方法中调用相同的方法。以下是在showHeighestScores()方法中要添加的代码:
func showHeighestScores()
{
if(NSUserDefaults.standardUserDefaults().objectForKey
("HighestScore")) == (nil)
{
savedScore = 0
}
else
{
savedScore = NSUserDefaults.standardUserDefaults().objectForKey("HighestScore") as! Int
println(savedScore)
}
var highScoreLabel = SKLabelNode(fontNamed: "Chalkduster")
highScoreLabel.fontColor = UIColor.blueColor()
highScoreLabel.name = "HIGHESTSCORE"
highScoreLabel.color = UIColor.lightGrayColor()
highScoreLabel.text = "The Score is: \(savedScore)"
highScoreLabel.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
highScoreLabel.zPosition = 3
self.addChild(highScoreLabel)
}
现在,是时候添加一个返回按钮了,它将玩家返回主菜单。将以下代码添加以实现返回按钮的功能:
func addBackBtn()
{
var mainMenubutton = SKLabelNode(fontNamed: FontFile)
mainMenubutton.fontColor = UIColor.blueColor()
mainMenubutton.name = "MAIN MENU"
mainMenubutton.text = "MAIN MENU"
mainMenubutton.position = CGPointMake(CGRectGetMinX(self.frame) + mainMenubutton.frame.width/2 , CGRectGetMinY(self.frame))
mainMenubutton.zPosition = 3
self.addChild(mainMenubutton)
}
var menuSceneInstance : MenuScene?
func goToMenuScene()
{
let transitionEffect = SKTransition.flipHorizontalWithDuration(1.0)
menuSceneInstance = MenuScene(size: self.size , playbutton: "Play", background: "BG")
menuSceneInstance!.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.view?.presentScene(menuSceneInstance , transition:transitionEffect)
}
前面的代码已添加返回按钮功能;现在我们必须在AddScore.swift中的touchesBegan()方法中处理按钮的触摸/点击,就像我们之前做的那样。在touchesBegan()方法中添加以下代码:
for touch: AnyObject in touches
{
let location = touch.locationInNode(self)
let node = self.nodeAtPoint(location)
if node.name == "MAIN MENU"
{
goToMenuScene()
}
}
下面是高分榜将呈现的样子:

最后,高分屏幕也完成了。这标志着我们在平台游戏(Platformer)中集成计分系统的完成。
将声音添加到游戏中
一款游戏只有配上不同的音乐和声音效果才能完整。游戏中可以有背景音乐,以及在每个动作中的声音效果,例如,当用户点击时,我们可以播放一个声音,稍后,当玩家击中障碍物或主游戏中的其他元素时,我们可以播放另一个声音。我们还可以在不同的关卡中使用不同的音乐。声音效果在增强整体游戏体验中起着至关重要的作用,因为它们让用户沉浸在全面的游戏体验中。
将声音添加到 Sprite Kit 游戏中
在 Sprite Kit 游戏中添加声音效果有两种方式:
-
使用
SKActions -
使用
AVFoundation框架
与使用AVFoundation框架相比,使用SKActions添加声音效果效率不高。SKActions有很多限制,例如,在游戏过程中无法暂停或播放声音等。因此,建议使用AVFoundation。
将声音添加到我们的平台游戏(Platformer)中
让我们在我们的平台游戏(Platformer)中添加声音效果。我们将使用AVFoundation框架来添加声音。
-
首先,通过点击你的项目添加框架,然后,在通用类别下,转到链接的框架和库部分,并添加
AVFoundation框架。 -
现在,将以下代码添加到将
AVFoundation框架导入到我们的GameScene.swift文件中:Import AVFoundation -
将
AVAudioPlayerDelegate代理添加到GameScene类中,以使用AVAudioPlayer的特定属性和方法。 -
现在,为我们的
GameScene文件创建一个AVAudioPlayer实例:var avPlayer:AVAudioPlayer! -
我们将两个声音文件
game_music.mp3和Strong_Punch-Mike_Koenig-574430706.wav添加到我们的项目中(WAV 文件格式适合短声音,而 MP3 格式适合较长时间的声音),并使用两个字符串变量指定它们的名称,如下所示:let backgroundSound = "game_music" let gameOutSound = "Strong_Punch-Mike_Koenig-574430706" -
将以下代码方法添加到使
AVAudioPlayer获取指定的音频文件并播放相同的代码中:func readFileIntoAVPlayer(soundName:String, ext:String) { var error: NSError? let fileURL:NSURL = NSBundle.mainBundle().URLForResource(soundName, withExtension: ext)! // the player must be a field. Otherwise it will be released before playing starts. self.avPlayer = AVAudioPlayer(contentsOfURL: fileURL, error: &error) if avPlayer == nil { if let e = error { println(e.localizedDescription) } } if avPlayer.playing { avPlayer.stop() } println("playing \(fileURL)") avPlayer.delegate = self avPlayer.prepareToPlay() avPlayer.volume = 1.0 avPlayer.play() }在前面的代码中,我们传递了声音名称和文件格式作为两个参数。然后该方法将播放声音文件。如果用户在设备上播放声音时开始游戏,此代码将停止之前播放的声音,然后开始游戏声音。
-
由于我们希望背景音乐始终播放,我们将调用
readFileIntoAVPlayer()方法并传递backgroundSound和mp3作为参数。在GameScene.swift的didMoveToView()方法开头添加此方法。以下是要添加的行:readFileIntoAVPlayer(backgroundSound, ext: "mp3")上述行将在游戏开始时播放背景音乐。
-
我们还为玩家死亡时添加了另一个声音文件。现在,是时候添加当玩家死亡时播放声音效果的代码了。在
GameScene.swift的didBeginContact()方法开头添加以下代码行:avPlayer.stop() readFileIntoAVPlayer(gameOutSound, ext: "wav")
在前面的代码中,我们通过调用与之前相同的方法但使用不同的参数来停止玩家中的背景音乐,并通过调用相同的方法为玩家的死亡播放新的声音效果。
使用 SKTexture 的动画帧
到目前为止,我们在游戏中使用了静态图像,但如果你看的话,大多数游戏都有动画效果,如玩家跑步效果、汽车跑步效果或任何其他增强游戏玩法并创造更好体验的效果。
在平台游戏玩家中添加运行动作纹理
我们之前添加了一个名为 idle.atlas 的图像图集,其中包含玩家站立位置的相似图像。
现在,我们将为玩家添加跑步纹理图像,这将使玩家看起来像在 GameScene 中跑步。
首先,添加一个名为 bro5_run.atlas 的纹理图像集,这是我们提供的。图像图集包含七组图像,有时也被称为精灵表。在我们的案例中,它将被称为玩家跑步精灵表。这些图像组将按照快速的时间速率在纹理图集中依次运行。
现在,让我们为玩家分配纹理图像。在 didMoveToView() 方法的开头添加以下代码行:
player = SKSpriteNode(texture:atlasForPlayerRun.textureNamed("bro5_run0001.png"))
在下一步中,我们将添加一个方法来为 atlasForPlayerRun.atlas 的不同纹理创建一个 SKAction。通过创建一个 runForwardTexture() 方法并在 didMoveToView() 中调用它来实现这一点。确保在添加玩家纹理图像后执行此操作:
func runForwardTexture()
{
let hero_run_anim = SKAction.animateWithTextures([
atlasForPlayerRun.textureNamed("bro5_run0002.png"),
atlasForPlayerRun.textureNamed("bro5_run0002.png"),
atlasForPlayerRun.textureNamed("bro5_run0003.png"),
atlasForPlayerRun.textureNamed("bro5_run0004.png"),
atlasForPlayerRun.textureNamed("bro5_run0005.png"),
atlasForPlayerRun.textureNamed("bro5_run0006.png"),
atlasForPlayerRun.textureNamed("bro5_run0007.png")
], timePerFrame: 0.06)
let run = SKAction.repeatActionForever(hero_run_anim)
player.runAction(run, withKey: "running")
}
上述代码已成功实现了玩家的跑步动画。以下截图显示了精灵表的外观:

玩家跑步动画的纹理图集
摘要
在本章中,我们介绍了一些游戏的重要方面,包括阅读关于性能改进的内容。此外,您可以使用 Xcode 提供的性能测量工具来提高您游戏的表现。我们还集成了得分系统、音效和玩家跑步动画到我们的平台游戏中。
在本书的下一章和最后一章中,我们将讨论我们平台游戏的每个元素,深入探讨苹果提供的游戏中心,并讨论苹果带来的 iOS 9 的最新增补。
第十章。回顾我们的游戏以及更多关于 iOS 9 的内容
首先,恭喜您克服了与游戏开发相关的所有障碍,并成功进入最后一章。现在您已经具备了独立开发 2D 游戏的能力,使用 Sprite Kit 游戏引擎。在前一章中,我们了解了性能提升,并在我们的游戏中添加了一些额外的功能,例如得分系统、声音和玩家跑步动画。
在本章中,我们将通过一些最后的润色来完善游戏,并讨论那些使您的游戏变得超级酷的强大奖励物品。我们还将了解如何集成游戏中心,以充分利用我们的游戏开发经验!
我们平台游戏开发过程回顾
让我们通过讨论构建的每个场景来回顾我们平台游戏的整个开发过程,从主菜单开始:
-
主菜单屏幕:这是我们开始游戏后看到的第一个屏幕。这个屏幕上有四个按钮。在左上角,您会看到一个得分菜单按钮,点击它将带您到高分排行榜。在右上角,有一个名为节点菜单的按钮,点击它将带您到节点菜单屏幕。在左下角,有一个名为阴影效果的按钮,点击它将显示阴影效果。最后,在屏幕中央是播放按钮。点击这个按钮,您将进入游戏屏幕。
![我们平台游戏开发过程回顾]()
-
得分菜单屏幕:这个屏幕是高分排行榜。您可以在屏幕上看到高分者的姓名和分数。屏幕左下角是主菜单按钮,点击它将带您回到主菜单。
![我们平台游戏开发过程回顾]()
-
节点菜单屏幕:这个屏幕展示了各种节点的示例。您可以看到五个不同的按钮,分别称为SKCropNode、SKLightNode、SKEmitterNode、SKShapeNode和SKVideoNode。除了这五个按钮外,还有一个返回按钮,可以带您回到主菜单屏幕。每个节点按钮都显示与按钮名称相对应的示例。
![我们平台游戏开发过程回顾]()
-
SKCropNode 屏幕:这个屏幕展示了
SKCropNode的示例。您可以看到在这个屏幕上对节点进行了裁剪。此外,还有一个返回按钮,可以带您回到节点菜单屏幕。![我们平台游戏开发过程回顾]()
-
SKLightNode 屏幕:这个屏幕展示了
SKLightNode的示例。您可以看到屏幕中央有一个灯光,您可以拖动它来查看在图像背后产生的阴影效果的变化。![我们平台游戏开发过程回顾]()
-
SKEmitterNode 屏幕:这个屏幕展示了
SKEmitterNode的一个示例。你可以在屏幕上看到发射的粒子,并且随着这些粒子的定期创建或销毁,你还可以注意到屏幕上节点数量和fps的变化。![我们的平台游戏开发过程回顾]()
-
SKShapeNode 屏幕:这个屏幕展示了
SKShapeNode的一个示例。你可以在屏幕上看到一个形状,并且如之前设置的那样,通过按屏幕上的返回按钮,你可以回到上一个节点菜单屏幕。![我们的平台游戏开发过程回顾]()
-
SKVideoNode 屏幕:这个屏幕展示了SKVideoNode的一个示例。你可以在屏幕中央看到一个视频,当你点击屏幕时,它将开始播放。你可以按返回按钮回到上一个屏幕。
![我们的平台游戏开发过程回顾]()
-
阴影效果屏幕:这个屏幕展示了阴影效果。你可以在屏幕中间看到阴影效果运行,并且像发射节点屏幕一样,你可以注意到这个屏幕的fps因为阴影效果而发生变化。
![我们的平台游戏开发过程回顾]()
-
游戏屏幕:这是带有跳跃和暂停按钮的游戏屏幕。如果玩家碰到任何障碍物,游戏将终止,并弹出得分列表屏幕。
![我们的平台游戏开发过程回顾]()
-
得分列表屏幕:游戏结束后,将显示这个屏幕。它有一个恭喜的提示和一个用于输入玩家名字的文本框。一旦你添加了玩家的名字,你可以点击添加玩家按钮来保存玩家的名字。
![我们的平台游戏开发过程回顾]()
前面的几点简要描述了每个屏幕的功能以及每个屏幕上存在的元素。
进一步开发平台游戏
到目前为止,我们已经研究了 Sprite Kit 为游戏开发提供的每个基本方面。我们甚至集成了得分系统、音效和跑步动画来增强游戏体验。然而,仍有改进的空间;你可以尝试各种效果和功能,或者只是增强当前的功能,使你的游戏更加有趣。以下是一些你可以尝试的想法:
-
障碍物:目前,我们只有两种类型的障碍物;你可以努力在游戏中添加更多障碍物,使游戏玩法更加刺激。
-
等级:目前,游戏中我们只有三个等级,但随着等级的提升,你可以努力添加更多等级,使游戏更具挑战性。
-
额外生命:如果玩家碰到障碍物,玩家就会死亡;你可以努力给玩家增加额外生命,从而增加游戏时间。
-
加分:另一个想法是在玩家击中特殊道具时添加一些加分。当玩家击中这个加分物品时,你可以额外增加 100 或 200 分。
-
声音:我们的游戏目前有两个音效:一个是背景音,另一个是玩家死亡音。你可以在游戏中添加更多声音,例如菜单和游戏玩法中的不同音乐。除此之外,你还可以在节点菜单屏幕等地方使用单独的音乐。
这些想法只是开始;你可以在游戏中应用大量的创意,使其成为下一个超级热门的游戏标题。
游戏中心简介
iOS 和 OS X 平台上的游戏可以利用苹果的社交游戏网络,即游戏中心。游戏玩家可以在排行榜上比较分数,跟踪成就,邀请朋友,或通过自动匹配开始多人游戏。游戏中心是游戏套件的一部分,除了游戏中心外,还有两个其他功能。
游戏中心允许设备连接到游戏中心服务并交换信息。游戏中心还确保将信息添加到排行榜和成就中。一个人还可以使用游戏中心服务玩多人游戏。
游戏中心在游戏中的优势
游戏中心处理用户认证、朋友、排行榜、成就、挑战、多人游戏、回合制游戏和邀请。从某种意义上说,可以说游戏中心为我们提供了与社交互动相关的服务器服务;类似于其网络系统。使用游戏中心的一些优点包括:
-
无需服务器端烦恼:使用游戏中心,你不必担心设置自己的服务器。你可以使用游戏中心的服务器来完成社交游戏中所需的大部分任务。
-
用户认证:游戏中心还帮助进行用户认证,因此你不必担心重复的 ID 或其他类似问题。
-
朋友:你可以和朋友玩游戏;玩家可以通过别名与其他玩家互动。玩家还可以设置状态,以及将其他玩家标记为朋友。
-
多人游戏:你可以通过游戏中心玩多人游戏。玩家可以邀请朋友或连接到游戏中心网络中的匿名玩家。
-
回合制游戏:使用此功能,你可以拥有一个回合制网络基础设施。比赛在没有所有玩家同时连接到游戏中心的情况下进行;玩家通过回合制方法相互对战。
-
排行榜:这允许玩家将游戏分数存储在游戏中心的排行榜上。每个游戏都将有一个本地和网络排行榜,你可以与本地和全球玩家比较分数。
-
成就:玩家可以在游戏中实现各种目标或成就,并通过解锁成就获得特殊奖励。
-
挑战:这允许玩家挑战其他玩家,并与其他玩家竞争分数或成就。
在游戏中集成游戏中心
在游戏中集成游戏中心并不难,在宏观层面上,包括两个步骤:
-
第一,是在 Xcode 中集成我们游戏所需的所有游戏中心库的实现和集成。
-
另一步是在 iTunes Connect 上注册应用程序,启用游戏中心支持,并设置游戏中所需的任何排行榜和成就。
要在 Sprite Kit 游戏中集成游戏中心,首先必须有一个 Apple ID,这样您就可以将游戏注册到 Apple。除了拥有 Apple ID 之外,还必须在代码和设计中做一些调整,以便成功地将游戏中心集成到您的游戏中。例如,您需要在游戏启动时进行游戏中心的登录(身份验证);如果您希望在游戏中显示排行榜,最好在游戏中显示它们,等等。
现在,让我们更详细地讨论这两个步骤。
使用 Xcode
您必须执行一些活动,例如创建或集成 Apple ID,以便开发游戏并启用 Xcode 中的游戏中心。让我们看看这个过程:
-
要添加 Apple ID,首先点击 Xcode 菜单中的偏好设置。偏好设置窗口将出现。在偏好设置窗口的顶部,将有各种标签页。点击账户标签页。
![使用 Xcode]()
-
现在,点击窗口的左下角,并点击带有+符号的按钮,以获取一个包含三个选项的小菜单。
![使用 Xcode]()
-
在小菜单中的三个选项中,点击添加Apple ID选项。在弹出的窗口中,输入您的Apple ID和密码,然后点击添加按钮。
![使用 Xcode]()
-
现在,你将在“账户”标签页中看到你的Apple ID的摘要。
![使用 Xcode]()
-
现在,在项目导航器中,点击项目目标。
![使用 Xcode]()
-
在“通用”标签页下的“标识”部分中,将有一个名为团队的下拉菜单。如果您点击它,您将看到其中的开发者姓名。这将确认 Apple ID 已成功集成。然后,您必须点击它,以便 Xcode 使用您的开发者账户。Xcode 将自动创建 App ID。
![使用 Xcode]()
-
现在,点击紧挨着通用标签页的能力标签页。在所有提供的功能列表中,展开游戏中心。现在,在列表右侧打开开关。
![使用 Xcode]()
游戏中心集成在游戏中的第一步已完成。我们已经启用了游戏中心,并在下一步中创建了一个用于 iTunes Connect 的 App ID。
与 iTunes Connect 一起工作
在这一步,我们将在 iTunes Connect 中为新的应用程序创建一个记录,然后我们将通过创建排行榜和成就来管理游戏中心部分。
-
前往 iTunes Connect 并使用你的开发者凭据登录。然后点击所有选项中的我的应用选项。
![与 iTunes Connect 一起工作]()
-
然后,在右上角,有一个+按钮用于添加新的 iOS 应用。
![与 iTunes Connect 一起工作]()
-
当你点击新建 iOS 应用按钮时,你将得到一个弹出窗口,询问有关新 iOS 应用的信息。所需的信息包括公司名称、名称(应用名称)、版本、主要语言、包标识符和SKU。
-
公司名称是公司或开发者的名称(之后不能更改)
-
接下来是应用名称(名称),其长度不能超过 255 个字符
-
下一个是应用中的主要语言,你可以从下拉菜单中选择
-
下一个选项是包标识符,它是一个下拉菜单,并将包含 Xcode 中应用的包标识符
-
之后,你可以在应用商店中显示的版本号中添加,并且它应该与 Xcode 中使用的版本号相匹配
-
最后,SKU是应用的一个唯一标识符,在应用商店中不可见
![与 iTunes Connect 一起工作]()
-
-
在点击新建 iOS 应用弹出窗口中的创建后,你将进入一个页面,你需要填写如描述、定价、评分和其他发布相关选项的详细信息。在选项卡中,将有一个名为游戏中心的选项。点击该按钮即可进入启用游戏中心页面。
![与 iTunes Connect 一起工作]()
-
由于你只有一个游戏,请点击为单个游戏启用,然后,游戏中心将为我们游戏启用。
![与 iTunes Connect 一起工作]()
-
在下面,将有一个选项用于添加排行榜和成就;你可以根据自己的方便添加它们。
![与 iTunes Connect 一起工作]()
现在,我们已经完成了游戏中心集成的第二步,这涉及到与 iTunes Connect 一起工作。
你已经对在 Sprite Kit 游戏中集成游戏中心所需的具体操作有了宏观层面的了解。
iOS 9 的新功能
苹果公司在 2015 年 6 月宣布了 iOS 9,它将在今年晚些时候正式发布。它建议为 Sprite Kit 框架添加一些新功能。以下列出了其中的一些:
-
Metal 渲染支持:Metal 提供了最低开销的图形处理单元(GPU)访问。这使得我们能够最大化我们应用和游戏的图形和计算能力。Metal 具有简化的 API、多线程支持以及预编译着色器,这些都有助于使我们的游戏或应用在性能和效率上更优越。
-
改进的场景编辑器和新的动作编辑器:Xcode 的最新版本现在拥有一个大幅改进的场景编辑器和一个新的动作编辑器。这将有助于在更短的时间内,通过更少的代码工作来设计 Xcode 中的场景。
-
相机节点:相机节点是一个
SKCameraNode对象,有助于指定场景中的一个位置,从该位置可以渲染场景。如果我们将场景的相机属性设置为相机节点,那么场景将使用相机节点的属性进行渲染。这使得创建 2D 滚动游戏、带状滚动游戏等变得更加容易。场景中的相机节点确定场景坐标空间中应可见的部分。 -
位置音频:我们可以使用此功能添加空间音频效果。通过这个功能,音频效果可以跟踪场景中听者的位置。位置音频效果使用
SKAudioNode对象。
这些功能是苹果为 Sprite Kit 框架推出的几个重要功能之一。
摘要
在本章中,我们讨论了我们的平台游戏及其各个方面,从本书的第一章到最新的一章。我们还讨论了如何在平台游戏中应用新的思想和尝试,并将其进一步扩展,使其成为苹果应用商店的下一个大热门。最后,我们研究了游戏中心,并简要讨论了其集成到 Sprite Kit 游戏中的情况。
话虽如此,我们正在为我们的 iOS 游戏开发书籍画上句号,带着这样的想法:一系列新游戏将席卷应用商店,这本书将对那些新平台游戏开发者产生巨大影响。我期待看到你们带来的令人兴奋的标题。祝你们好运!










































浙公网安备 33010602011771号