Swift 游戏开发(一)

原文:zh.annas-archive.org/md5/d0174909c3206c7de05881da7a3ac0c9

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

现在从未有过更好的时机成为游戏开发者。App Store 为你提供了一个独特的机会,将你的想法传播给庞大的受众。现在,Swift 已经到来,增强了我们的工具集,并提供了更流畅的开发体验。Swift 虽然是新的,但已经被誉为一种优秀、设计精良的语言。无论你是游戏开发的新手还是想增加你的专业知识,我相信你会喜欢用 Swift 制作游戏。

我写这本书的目标是分享 Swift 和 SpriteKit 的基础知识。我们将通过一个完整的示例游戏来学习 Swift 开发过程的每一步。一旦你完成这本书,你将能够轻松地设计并发布自己的游戏想法到 App Store,从开始到结束。

请提出任何问题并与我们分享你的游戏创作:

电子邮件:<stephen@thinkingswiftly.com>

Twitter: @sdothaney

第一章探讨了 Swift 的最佳特性。让我们开始吧!

本书涵盖的内容

第一章,使用 Swift 设计游戏,介绍了 Swift 的最佳特性,帮助你设置开发环境,并启动你的第一个 SpriteKit 项目。

第二章,精灵、摄像机、动作!,教你使用 Swift 绘制和动画的基础知识。你将绘制精灵,将纹理导入到你的项目中,并将摄像机对准主要角色。

第三章,混合物理,涵盖了物理模拟的基本原理:物理体、冲量、力、重力、碰撞等。

第四章,添加控制,探讨了移动游戏控制的多种方法:设备倾斜和触摸输入。我们还将改进示例游戏中的摄像机和核心玩法。

第五章,生成敌人、金币和道具,介绍了我们将在示例游戏中使用的角色阵容,并展示了如何为每种 NPC 类型创建自定义类。

第六章,生成一个永不结束的世界,探讨了 SpriteKit 场景编辑器,为示例游戏构建遭遇,并创建了一个无限循环遭遇的系统。

第七章,实现碰撞事件,深入探讨了高级物理模拟主题,并在精灵碰撞时添加自定义事件。

第八章,精益求精 – HUD、视差背景、粒子等,添加了使每个优秀游戏发光的额外功能。创建视差背景,了解 SpriteKit 的粒子发射器,并将抬头显示叠加到你的游戏中。

第九章, 添加菜单和声音,构建了一个基本的菜单系统,并说明了在您的游戏中播放声音的两种方法。

第十章, 与游戏中心集成,将我们的示例游戏链接到苹果游戏中心,用于排行榜、成就和友好挑战。

第十一章, 发布!准备 App Store 和发布,涵盖了打包您的游戏并将其提交到 App Store 的基本要素。

您需要为本书准备什么

本书使用 Xcode IDE 版本 6.3.2(Swift 1.2)。如果您使用的是 Xcode 的不同版本,您可能会遇到语法差异;苹果公司不断升级 Swift 的语法。

访问developer.apple.com/xcode下载 Xcode。

您需要一个苹果开发者账户来将您的应用程序集成到游戏中心,并将您的游戏提交到 App Store。

本书面向对象

如果您想使用 Swift 创建和发布有趣的 iOS 游戏,那么这本书就是为您准备的。您应该熟悉基本编程概念,如类、类型和函数。然而,不需要先前的游戏开发或苹果生态系统经验。此外,有经验的游戏程序员会发现这本书在过渡到使用 Swift 进行游戏开发时很有用。

惯例

在这本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名如下所示:“游戏在切换到这个场景时调用didMoveToView函数。”

代码块如下设置:

let mySprite = SKSpriteNode(color: UIColor.blueColor(), size: 
    CGSize(width: 50, height: 50))
mySprite.position = CGPoint(x: 300, y: 300)
self.addChild(mySprite)

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

    // Find the width of one-third of the children nodes
 jumpWidth = tileSize.width * floor(tileCount / 3)
}

新术语重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:“在左侧窗格中选择iOS | 应用程序,在右侧窗格中选择游戏。”

注意

警告或重要注意事项如下所示。

小贴士

小贴士和技巧看起来像这样。

读者反馈

我们欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或不喜欢的地方。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。

要向我们发送一般反馈,请简单地发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及本书的标题。

如果您在某个领域有专业知识,并且您对撰写或参与一本书籍感兴趣,请参阅我们的作者指南,网址为www.packtpub.com/authors

客户支持

现在您是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从www.packtpub.com下载示例代码文件,适用于您购买的所有 Packt Publishing 书籍。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

此外,每个章节都提供了检查点链接,您可以使用这些链接下载到该点的示例项目。

下载本书的颜色图像

我们还为您提供了一个包含本书中使用的截图/图表的颜色图像的 PDF 文件。这些颜色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/0531OT_ColorImages.pdf下载此文件。

勘误

尽管我们已经尽最大努力确保内容的准确性,错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者感到沮丧,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。

要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。

盗版

互联网上版权材料的盗版是一个持续存在的问题,所有媒体都存在。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何形式的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过<copyright@packtpub.com>与我们联系,并提供疑似盗版材料的链接。

我们感谢您在保护我们的作者和我们为您提供有价值内容的能力方面的帮助。

询问

如果您对本书的任何方面有问题,您可以通过<questions@packtpub.com>联系我们,我们将尽力解决问题。

第一章:使用 Swift 设计游戏

苹果的新语言对于游戏开发者来说来得正是时候。Swift有独特的机遇成为特别的东西;一个革命性的工具,用于应用开发者。Swift 是开发者进入苹果生态系统中创建下一个大型游戏的门户。我们刚刚开始探索移动游戏的奇妙潜力,Swift 是我们工具集现代化的需要。Swift 快速、安全、现代,对来自其他语言的开发者有吸引力。无论您是苹果世界的初学者,还是Objective-C的老手,我相信您会喜欢用 Swift 制作游戏。

注意

苹果的网站表示:“Swift 是 C 和 Objective-C 语言的继承者。”

我在这本书中的目标是逐步引导您创建 iPhone 和 iPad 的 2D 游戏。我们将从安装必要的软件开始,逐步完成游戏开发的每一层,最终将我们的新游戏发布到 App Store。

我们在旅途中也会有一些乐趣!我们的目标是创建一款以一只壮丽的飞企鹅皮埃尔为主角的无限飞行游戏。什么是无限飞行游戏?想象一下像 iCopter、Flappy Bird、鲸鱼之旅、喷射背包冒险等游戏——这个列表相当长。

无限飞行游戏在 App Store 上很受欢迎,这个类型需要我们涵盖 2D 游戏设计的许多可重用组件;我将向您展示如何修改我们的机制以创建多种不同的游戏风格。我的希望是,我们的演示项目将成为您自己创意作品的模板。不久,您将能够使用我们共同探索的技术发布您自己的游戏想法。

本章包含以下主题:

  • 为什么你会喜欢 Swift

  • 您将在本书中学到什么

  • 设置您的开发环境

  • 创建您的第一个 Swift 游戏

为什么你会喜欢 Swift

作为一种现代编程语言,Swift 受益于编程社区的集体经验;它结合了其他语言的最佳部分,避免了不良的设计决策。以下是我最喜欢的几个 Swift 特性。

美观的语法

Swift 的语法现代且易于接近,无论您现有的编程经验如何。苹果在语法和结构之间取得了平衡,使 Swift 简洁易读。

互操作性

Swift 可以直接集成到您现有的项目中,并与您的 Objective-C 代码并行运行。

强类型

Swift 是一种强类型语言。这意味着编译器将在编译时捕获更多错误——而不是当您的用户在玩游戏时!编译器会期望您的变量属于某种类型(intstring等),如果您尝试分配不同类型的值,则会抛出编译时错误。虽然如果您来自弱类型语言,这可能看起来很严格,但增加的结构会导致更安全、更可靠的代码。

智能类型推断

为了使事情更简单,类型推断将自动检测变量和常量的类型,基于它们的初始值。您不需要显式声明变量的类型。Swift 足够智能,可以在大多数表达式中推断变量类型。

自动内存管理

如苹果 Swift 开发者指南所述,“在 Swift 中,内存管理就是如此简单。”Swift 使用一种称为自动引用计数(您将看到它被称为ARC)的方法来管理游戏内存的使用。除了少数边缘情况外,您可以依赖 Swift 安全地清理并关闭灯光。

一个公平的竞争环境

我最喜欢的 Swift 特性之一是它如何迅速地被主流接受。我们都在共同学习和成长,有巨大的机会开辟新的领域。

Swift 有什么缺点吗?

Swift 是一种非常有趣的语言,但在开始新项目时,我们应该考虑这两个问题。

资源较少

由于 Swift 的历史较短,通过互联网搜索找到常见问题的答案确实更加困难。Objective-C 在 Stack Overflow 等有用的论坛上有多年讨论和答案。随着 Swift 社区的持续发展,这个问题每天都在改善。

操作系统兼容性

Swift 项目将在 iOS7 及以上版本和 OSX 10.9 及以上版本上运行。如果罕见地需要针对运行较旧操作系统的设备,Swift 不是正确的选择。

前置条件

我将努力使这篇文本对所有技能水平的人都容易理解:

  • 我假设您作为语言对 Swift 是全新的

  • 本书不需要先前的游戏开发经验,尽管它会有所帮助

  • 我假设您对常见的编程概念有基本的理解

您将在本书中学到什么

到这本书的结尾,您将能够创建和发布自己的 iOS 游戏。您将知道如何结合我们学到的技术来创建自己的游戏风格,并且您将准备好在 2D 游戏设计的基础上深入更高级的主题。

拥抱 SpriteKit

SpriteKit是 Apple 的 2D 游戏开发框架,也是 iOS 游戏设计的主要工具。SpriteKit 将处理我们的图形渲染、物理和声音播放的机制。就游戏开发框架而言,SpriteKit 是一个极好的选择。它是 Apple 构建和支持的,因此与 Xcode 和 iOS 完美集成。您将学会熟练使用 SpriteKit——在我们的演示游戏中,我们将独家使用它。

我们将学习如何使用 SpriteKit 来驱动我们游戏的核心机制:

  • 为我们的玩家、敌人和道具添加动画

  • 绘制并移动侧边滚动环境

  • 播放声音和音乐

  • 应用类似物理的重力和冲量进行移动

  • 处理游戏对象之间的碰撞

对玩家输入做出反应

移动游戏中的控制方案必须富有创意。移动硬件迫使我们模拟传统的控制器输入,例如方向垫和屏幕上的多个按钮。这占用了宝贵的可见区域,并且与物理设备相比,提供的精度和反馈更少。许多游戏只使用单一输入方式;在屏幕上的任何地方轻触一次。我们将学习如何充分利用移动输入,并通过感应设备运动和倾斜来探索新的控制形式。

结构化你的游戏代码

编写易于重用和修改的代码,以便随着你的游戏设计不可避免地变化,这是非常重要的。你将在开发和测试你的游戏时经常发现机械改进,并且你会感谢自己有一个干净的工作环境。尽管有许多方法可以接近这个主题,但我们将探索一些最佳实践来构建一个有组织的系统。

构建 UI/菜单/关卡

我们将学习如何在我们的游戏中通过菜单屏幕切换场景。在我们构建演示游戏的过程中,我们将涵盖用户体验设计和菜单布局的基础知识。

与 Game Center 集成

Game Center是苹果内置的社交游戏网络。你的游戏可以与 Game Center 集成,以存储和分享高分和成就。我们将学习如何注册 Game Center,将其集成到我们的代码中,并创建一个有趣的成就系统。

最大化乐趣

如果你像我一样,你脑海中会有数十个游戏想法。想法来得容易,但设计有趣的游戏玩法却很难!在你看到你的设计付诸实践后,发现你的想法需要游戏玩法增强是很常见的。我们将探讨如何避免死胡同,并确保你的项目能够顺利到达终点线。此外,我将分享我的技巧和窍门,以确保你的游戏能给你的玩家带来快乐。

冲刺终点线

创作一款游戏是你将珍藏的记忆。分享你的辛勤工作只会让满足感更甜。一旦我们的游戏经过打磨并准备好公开消费,我们将一起导航 App Store 提交流程。你将结束游戏,对自己使用 Swift 创建游戏并将其带到 App Store 的能力充满信心。

进一步研究

我将专注于 iOS 优秀游戏设计中的机制和编程。一些次要主题超出了本书的范围。

游戏营销和盈利

成功推广和营销你的游戏是一项重要的工作,但本文重点在于游戏开发机制和 Swift 代码。如果你对从你的游戏中赚钱感兴趣,我强烈建议你研究在独立游戏社区中推广自己的最佳方式,并在游戏发布前就开始营销你的游戏。

为 OSX 桌面制作游戏

我们将专注于 iOS。你也可以使用这本书中的技术来在 OSX 上进行游戏开发,但你可能需要研究发布和环境差异。

设置你的开发环境

学习新的开发环境可能会成为障碍。幸运的是,苹果为 iOS 开发者提供了一些出色的工具。我们将从安装 Xcode 开始我们的旅程。

介绍 Xcode

Xcode 是苹果公司的 集成开发环境IDE)。您需要 Xcode 来创建您的游戏项目、编写和调试您的代码,以及为 App Store 构建您的项目。Xcode 还附带了一个 iOS 模拟器,可以在您的计算机上虚拟化 iPhone 和 iPad 来测试您的游戏。

注意

苹果公司将 Xcode 赞誉为“一个构建 Mac、iPhone 和 iPad 上令人惊叹应用的极具生产力的环境。”

要安装 Xcode,请在 App Store 中搜索 xcode 或访问 developer.apple.com 并点击 Xcode 图标。请注意您正在安装的 Xcode 版本。在撰写本文时,Xcode 的当前版本是 6.3.2。Swift 正在不断发展,每个新的 Xcode 版本都会为 Swift 带来语法变化。为了获得本书中代码的最佳体验,请使用 Xcode 6.3.x(Swift 版本 1.2)。

注意

苹果公司在 2015 年的 WWDC 上宣布了 Xcode 7 和 Swift 2,但在撰写本文时它仍然处于测试版。看起来会有一些小的语法变化。本书中的知识和技巧仍然适用。

Xcode 执行常见的 IDE 功能,以帮助您编写更好、更快的代码。如果您以前使用过 IDE,那么您可能熟悉自动完成、实时错误突出显示、运行和调试项目,以及使用项目管理面板创建和组织您的文件。然而,任何新的程序在开始时都可能显得令人不知所措。在接下来的几页中,我们将介绍一些常见的界面功能。我还发现 YouTube 上的教程视频特别有帮助,如果您遇到困难的话。

创建我们的第一个 Swift 游戏

您已经安装了 Xcode 吗?让我们直奔主题,看看模拟器中的一些游戏代码的实际效果!

  1. 我们需要创建一个新的项目。启动 Xcode 并导航到 文件 | 新建 | 项目。您将看到一个屏幕,要求您选择您新项目的模板。在左侧面板中选择 iOS | 应用程序,在右侧面板中选择 游戏。它应该看起来像这样:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_01_01.jpg

  2. 一旦你选择了游戏,点击下一步。接下来的屏幕会要求我们输入一些关于我们项目的基本信息。不要担心;我们很快就会进入有趣的部分。对于我们的演示游戏,我们将创建一个侧滚无限飞行的游戏,特色是一只惊人的飞企鹅,名叫皮埃尔。我打算把这个游戏命名为皮埃尔企鹅逃离南极洲,但你可以自由地给你的项目起任何名字。现在,名字并不重要。当你创建自己的游戏并发布时,你将想要选择一个有意义的产品名称组织标识符。按照惯例,你的组织标识符应该遵循反向域名风格。我将使用com.ThinkingSwiftly,如下面的截图所示。

  3. 在填写完名称字段后,请确保选择Swift作为语言SpriteKit作为游戏技术,以及通用作为设备。以下是我的设置:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_01_02.jpg

  4. 点击下一步,你将看到最后的对话框。保存你的新项目。在电脑上选择一个位置并点击下一步。我们进来了!Xcode 已经用基本的 SpriteKit 模板预先填充了我们的项目。

导航我们的项目

现在我们已经创建了项目,你会在 Xcode 的左侧看到项目导航器。你将使用项目导航器来添加、删除、重命名文件,以及通常组织你的项目。你可能注意到 Xcode 在我们的新项目中创建了很多文件。我们会慢慢来;不要因为还不知道每个文件的作用而感到压力,但如果你好奇,可以自由地探索它们:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_01_03.jpg

探索 SpriteKit 演示

使用项目导航器打开名为GameScene.swift的文件。Xcode 创建了GameScene.swift来存储我们新游戏的基本场景。

什么是场景?SpriteKit 使用场景的概念来封装游戏中的每个独特区域。想象一下电影中的场景;我们将为主菜单创建一个场景,为游戏结束屏幕创建一个场景,为我们的游戏中的每个关卡创建一个场景,等等。如果你在游戏的主菜单上点击“播放”,你会从菜单场景移动到 1 级场景。

小贴士

SpriteKit 在其类名前缀字母“SK”;因此,场景类是SKScene

你会看到在这个场景中已经有一些代码了。SpriteKit 项目模板自带一个非常小的演示。让我们快速看一下这个演示代码,并使用它来测试 iOS 模拟器。

注意

请在此阶段不要担心理解演示代码。你的重点应该是学习开发环境。

在 Xcode 窗口的顶部寻找运行工具栏。它看起来可能像这样:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_01_04.jpg

选择你偏好的 iOS 设备进行模拟,使用最右侧的下拉菜单。你应该模拟哪个 iOS 设备?你可以自由选择你喜欢的设备。在这本书的截图里,我会使用 iPhone 6,所以如果你想你的结果与我的图片完全匹配,请选择iPhone 6

注意

很遗憾,你可能会在模拟器中看到你的游戏表现不佳。SpriteKit 在 iOS 模拟器中表现出的 FPS 很低。一旦我们的游戏变得相对复杂,我们甚至在高性能的电脑上也会看到我们的 FPS 下降。模拟器会帮你度过难关,但如果你能插入一个物理设备进行测试,那就更好了。

是时候看看 SpriteKit 的实际效果了!按下灰色播放箭头(方便的运行键盘快捷键:command + r)。Xcode 将构建项目并启动模拟器。模拟器在一个新窗口中启动,所以请确保将其带到前台。你应该看到一个灰色背景和粉笔白色的文本:Hello, World。在灰色背景上点击。

你会在你点击的任何地方看到旋转的战斗机生成:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_01_05.jpg

我可能对战斗机有点过度了……

如果你已经走到这一步,恭喜你!你已经成功安装并配置了你制作第一个 Swift 游戏所需的一切。

一旦你生成了足够的战斗机,你可以关闭模拟器并返回 Xcode。注意:你可以使用键盘命令command + q退出模拟器,或者在 Xcode 中按停止按钮。如果你使用停止按钮,模拟器将保持打开状态,并更快地启动你的下一个构建。

检查演示代码

让我们快速探索一下演示代码。现在不必担心理解一切;我们将在稍后深入探讨每个元素。目前,我希望你能够适应开发环境,并在过程中学到一些东西。如果你遇到了困难,继续前进!实际上,在清除 SpriteKit 演示并开始我们自己的游戏后,下一章的内容将会变得简单。

确保你在 Xcode 中打开了GameScene.swift文件。

GameScene类实现了三个函数。让我们来检查这些函数。你可以随意阅读每个函数内部的代码,但我并不期望你立刻就能理解具体的代码。

  1. 游戏在切换到GameScene时,会调用didMoveToView函数。你可以把它想象成场景的初始化或主函数。SpriteKit 演示使用它来在屏幕上绘制Hello World文本。

  2. touchesBegan函数处理 iOS 设备屏幕上的用户触摸输入。SpriteKit 演示使用这个函数来生成战斗机图形,并将其设置在我们触摸屏幕的任何地方旋转。

  3. update函数会在屏幕上绘制每一帧时运行一次。SpriteKit 演示没有使用这个函数,但我们可能以后会有理由实现它。

清理

我希望你已经吸收了一些 Swift 语法,并对 Swift 和 SpriteKit 有了一个大致的了解。现在是时候为我们的游戏腾出空间了;让我们把所有的示例代码都清理掉!我们想要保留一点模板代码,但我们可以删除函数内部的大部分内容。为了清楚起见,我不期望你现在就能理解这段代码。这只是一个开始我们旅程的必要步骤!请从你的 GameScene.swift 文件中删除行,直到它看起来像以下代码:

import SpriteKit

class GameScene: SKScene {
  override func didMoveToView(view: SKView) {
  }
}

一旦你的 GameScene.swift 看起来像前面的代码,你就可以继续前进到第二章,*精灵、相机、动作!*了。现在真正的乐趣开始了!

摘要

你已经取得了很大的进步。你获得了 Swift 的第一次实践经验,安装并配置了你的开发环境,成功地将代码启动到 iOS 模拟器中,并为你的游戏项目做好了第一步准备。做得好!

我们已经看到了足够的“Hello World”演示——你准备好在自己的游戏屏幕上绘制自己的图形了吗?在第二章,*精灵、相机、动作!*中,我们将使用精灵、纹理、颜色和动画。

第二章:精灵、相机、动作!

使用 SpriteKit 绘制非常简单。我们可以自由地专注于构建出色的游戏体验,同时 SpriteKit 执行游戏循环的机械工作。要在屏幕上绘制一个项目,我们创建一个 SpriteKit 节点的新的实例。这些节点很简单;我们为每个要绘制的项目将子节点附加到场景或现有节点上。精灵、粒子发射器和文本标签在 SpriteKit 中都被视为节点。

注意

游戏循环是一个常用的游戏设计模式,用于每秒多次更新游戏,并在硬件快或慢的情况下保持相同的游戏速度。

SpriteKit 会自动将新节点连接到游戏循环中。随着你对 SpriteKit 的熟练,你可能希望进一步探索游戏循环以了解“幕后”发生了什么。

本章包括以下主题:

  • 准备你的项目

  • 绘制你的第一个精灵

  • 动画:移动、缩放和旋转

  • 与纹理一起工作

  • 将艺术作品组织到纹理图集中

  • 在精灵上居中相机

磨尖我们的铅笔

在我们开始绘制之前,有四个快速事项需要注意:

  1. 由于我们将设计我们的游戏以使用横幅屏幕方向,我们将完全禁用纵向视图:

    1. 在 Xcode 中打开你的游戏项目后,在项目导航器中选择整体项目文件夹(最顶部的项目)。

    2. 你将在 Xcode 的主框架中看到你的项目设置。在部署信息下,找到设备方向部分。

    3. 取消选择纵向选项,如图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_02_01.jpg

  2. SpriteKit 模板为在场景中排列精灵生成一个视觉布局文件。我们不需要它;在探索关卡设计时,我们将使用 SpriteKit 视觉编辑器。要删除这个额外的文件:

    1. 在项目导航器中右键单击 GameScene.sks 并选择删除

    2. 在对话框窗口中选择移动到废纸篓

  3. 我们需要调整场景大小以适应新的横幅视图。按照以下步骤调整场景大小:

    1. 从项目导航器打开 GameViewController.swift 并定位到 GameViewController 类中的 viewDidLoad 函数。viewDidLoad 函数将在游戏意识到它处于横幅视图之前触发,因此我们需要使用在启动过程中较晚触发的函数。完全删除 viewDidLoad,移除其所有代码。

    2. viewDidLoad 替换为名为 viewWillLayoutSubviews 的新函数。现在不必担心理解每一行;我们只是在配置项目。为 viewWillLayoutSubviews 使用以下代码:

      override func viewWillLayoutSubviews() {
          super.viewWillLayoutSubviews()
          // Create our scene:
          let scene = GameScene()
          // Configure the view:
          let skView = self.view as! SKView
          skView.showsFPS = true
          skView.showsNodeCount = true
          skView.ignoresSiblingOrder = true
          scene.scaleMode = .AspectFill
          // size our scene to fit the view exactly:
          scene.size = view.bounds.size
          // Show the new scene:
          skView.presentScene(scene)
      }
      
    3. 最后,在 GameViewController.swift 中找到 supportedInterfaceOrientations 函数并将其缩减到以下代码:

      override func supportedInterfaceOrientations() -> Int {
          return Int(
          UIInterfaceOrientationMask.Landscape.rawValue);
      }
      

      小贴士

      下载示例代码

      你可以从你购买的所有 Packt 出版物书籍的账户中下载示例代码文件。www.packtpub.com。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。

      此外,每个章节还提供了检查点链接,你可以使用这些链接下载到该点的示例项目。

  4. 我们应该再次确认我们已经准备好继续前进。尝试使用工具栏上的播放按钮或command + r键盘快捷键在模拟器中运行我们的清洁项目。加载后,模拟器应该切换到横幅视图,背景为空白灰色(并在右下角显示节点和 FPS 计数器)。如果项目无法运行,或者你仍然看到“Hello World”,你需要从第一章的结尾,使用 Swift 设计游戏,重新追踪你的步骤,以完成你的项目准备。

检查点 2- A

如果你想要下载到这一点的我的项目,你可以从以下网址下载:www.thinkingswiftly.com/game-development-with-swift/chapter-2

绘制你的第一个精灵

是时候编写一些游戏代码了——太棒了!打开你的 GameScene.swift 文件,找到 didMoveToView 函数。回想一下,这个函数每次游戏切换到这个场景时都会触发。我们将使用这个函数来熟悉 SKSpriteNode 类。你将在游戏中广泛使用 SKSpriteNode,无论何时你想添加一个新的 2D 图形实体。

注意

“精灵”一词指的是在屏幕上独立于背景移动的 2D 图形或动画。随着时间的推移,这个术语已经发展到指代 2D 游戏中屏幕上的任何游戏对象。我们将在本章中创建并绘制你的第一个精灵:一只快乐的小蜜蜂。

构建 SKSpriteNode 类

让我们先在屏幕上画一个蓝色方块。SKSpriteNode 类可以绘制纹理图形和实色块。在花费时间在艺术品上之前,用色块原型化你的新游戏想法通常很有帮助。要绘制蓝色方块,向游戏中添加一个 SKSpriteNode 实例:

override func didMoveToView(view: SKView) {
    // Instantiate a constant, mySprite, instance of SKSpriteNode
    // The SKSpriteNode constructor can set color and size
    // Note: UIColor is a UIKit class with built-in color presets
    // Note: CGSize is a type we use to set node sizes
    let mySprite = SKSpriteNode(color: UIColor.blueColor(), size: 
        CGSize(width: 50, height: 50))

    // Assign our sprite a position in points, relative to its 
    // parent node (in this case, the scene)
    mySprite.position = CGPoint(x: 300, y: 300)

    // Finally, we need to add our sprite node into the node tree.
    // Call the SKScene's addChild function to add the node
    // Note: In Swift, 'self' is an automatic property
    // on any type instance, exactly equal to the instance itself
    // So in this instance, it refers to the GameScene instance
    self.addChild(mySprite)
}

好吧,运行项目。你应该在模拟器中看到一个类似的小蓝色方块出现:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_02_02.jpg

小贴士

Swift 允许你将变量定义为常量,它只能被赋予一次值。为了最佳性能,尽可能使用 let 来声明常量。当你需要在代码中稍后更改值时,使用 var 声明变量。

将动画添加到你的工具包中

在我们深入精灵理论之前,我们应该用我们的蓝色正方形玩得开心一些。SpriteKit 使用动作对象在屏幕上移动精灵。考虑以下示例:如果我们的目标是移动正方形穿过屏幕,我们必须首先创建一个新的动作对象来描述动画。然后,我们指示我们的精灵节点执行该动作。我将在本章中用许多示例来说明这个概念。现在,在didMoveToView函数中,在self.addChild(mySprite)行下方添加以下代码:

// Create a new constant for our action instance
// Use the moveTo action to provide a goal position for a node
// SpriteKit will tween to the new position over the course of the
// duration, in this case 5 seconds
let demoAction = SKAction.moveTo(CGPoint(x: 100, y: 100), 
    duration: 5)
// Tell our square node to execute the action!
mySprite.runAction(demoAction)

运行项目。你会看到我们的蓝色正方形滑过屏幕,向(100,100)位置移动。这个动作是可重用的;场景中的任何节点都可以执行这个动作来移动到(100,100)位置。正如你所见,当我们需要对节点属性进行动画处理时,SpriteKit 为我们做了很多繁重的工作。

小贴士

中间画,或称补间,使用引擎在起始帧和结束帧之间进行平滑动画。我们的moveTo动画是一个补间;我们提供起始帧(精灵的原始位置)和结束帧(新的目标位置)。SpriteKit 生成我们值之间的平滑过渡。

让我们尝试一些其他动作。SKAction.moveTo函数只是众多选项之一。尝试将demoAction行替换为以下代码:

let demoAction = SKAction.scaleTo(4, duration: 5)

运行项目。你会看到我们的蓝色正方形增长到原来的四倍大小。

多个动画的序列化

我们可以使用动作组和序列同时执行动作或依次执行。例如,我们可以轻松地将我们的精灵放大并旋转。删除到目前为止的所有动作代码,并用以下代码替换:

// Scale up to 4x initial scale
let demoAction1 = SKAction.scaleTo(4, duration: 5)
// Rotate 5 radians
let demoAction2 = SKAction.rotateByAngle(5, duration: 5)
// Group the actions
let actionGroup = SKAction.group([demoAction1, demoAction2])
// Execute the group!
mySprite.runAction(actionGroup)

当你运行项目时,你会看到一个旋转并变大的正方形。太棒了!如果你想按顺序运行这些动作(而不是同时运行),将SKAction.group更改为SKAction.sequence

// Group the actions into a sequence
let actionSequence = SKAction.sequence([demoAction1, demoAction2])

// Execute the sequence!
mySprite.runAction(actionSequence)

运行代码,观察你的正方形首先变大然后旋转。很好。你不仅限于两个动作;我们可以将所需数量的动作组合或序列化。

我们到目前为止只使用了几个动作;在继续之前,你可以自由探索SKAction类并尝试不同的动作组合。

回顾你的第一个精灵

恭喜你,你已经学会了如何使用 SpriteKit 动作绘制非纹理精灵并对其进行动画处理。接下来,我们将探索一些重要的定位概念,然后为我们的精灵添加游戏艺术。在你继续之前,请确保你的didMoveToView函数与我的匹配,并且你的序列化动画正在正确触发。以下是到目前为止的我的代码:

override func didMoveToView(view: SKView) {
    // Instantiate a constant, mySprite, instance of SKSpriteNode
    let mySprite = SKSpriteNode(color: UIColor.blueColor(), size: 
        CGSize(width: 50, height: 50))

    // Assign our sprite a position
    mySprite.position = CGPoint(x: 300, y: 300)

    // Add our sprite node into the node tree
    self.addChild(mySprite)

    // Scale up to 4x initial scale
    let demoAction1 = SKAction.scaleTo(CGFloat(4), duration: 2)
    // Rotate 5 radians
    let demoAction2 = SKAction.rotateByAngle(5, duration: 2)

    // Group the actions into a sequence
    let actionSequence = SKAction.sequence([demoAction1, 
        demoAction2])

    // Execute the sequence!
    mySprite.runAction(actionSequence)
}

定位的故事

SpriteKit 使用点阵来定位节点。在这个网格中,场景的左下角是(0,0),X 轴向右为正方向,Y 轴向上为正方向。

类似地,在单个精灵级别上,(0,0)指的是精灵的左下角,而(1,1)指的是右上角。

与锚点对齐

每个精灵都有一个anchorPoint属性,或称为原点。anchorPoint属性允许您选择精灵的哪个部分与精灵的整体位置对齐。

注意

默认锚点为(0.5,0.5),因此新的SKSpriteNode在其位置上完美居中。

为了说明这一点,让我们检查一下我们在屏幕上刚刚绘制的蓝色方块精灵。我们的精灵宽度为 50 像素,高度为 50 像素,其位置是(300,300)。由于我们没有修改anchorPoint属性,其锚点为(0.5,0.5)。这意味着精灵将在场景网格的(300,300)位置上完美居中。我们的精灵的左侧边缘始于 275,右侧边缘终止于 325。同样,底部始于 275,顶部终止于 325。以下图表说明了我们的方块在网格上的位置:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_02_03.jpg

为什么我们默认喜欢居中的精灵?您可能会认为通过将anchorPoint属性设置为(0,0)来根据元素的左下角定位元素会更简单。然而,当我们在缩放或旋转精灵时,居中行为对我们更有益:

  • 当我们使用anchorPoint属性为(0,0)缩放精灵时,它只会沿着 y 轴向上扩展并沿 x 轴向外出扩展。旋转动作会使精灵围绕其左下角进行大圆旋转。

  • 默认的anchorPoint属性为(0.5,0.5)的居中精灵在缩放时会在所有方向上等比例扩展,并且在旋转时会在原地旋转,这通常是期望的效果。

有时候您可能想要更改锚点。例如,如果您在绘制火箭船,您可能希望船围绕其圆锥形的前端旋转,而不是围绕其中心。

添加纹理和游戏艺术

您可能想为您的蓝色方块拍一张截图,以备将来欣赏。我非常喜欢回忆我完成的游戏的老截图,当时它们只是简单的彩色方块在屏幕上滑动。现在是我们超越这个阶段,并将一些有趣的艺术作品附加到我们的精灵上的时候了。

下载免费资源

我为这本书中使用的所有艺术资源提供了一个可下载的包。我建议您使用这些资源,这样您将为我们的演示游戏准备齐全。或者,如果您愿意,当然可以自由地为您的游戏创建自己的艺术作品。

这些资源来自 Kenney Game Studio 的一个杰出的公共领域资源包。我提供的是我们将用于游戏的资源包的小子集。请从以下 URL 下载游戏艺术资源:

www.thinkingswiftly.com/game-development-with-swift/assets

更出色的艺术作品

如果你喜欢这些艺术作品,你可以在kenney.itch.io/kenney-donation通过小额捐赠下载超过 16,000 个同风格的游戏资源。我与 Kenney 没有关联;我只是觉得他向独立游戏开发者发布了如此多的公共领域艺术作品令人钦佩。

作为 CC0 资源,你可以复制、修改和分发这些艺术作品,甚至用于商业目的,而无需请求许可。你可以在这里阅读完整的许可证:

creativecommons.org/publicdomain/zero/1.0/

绘制你的第一个纹理精灵

让我们使用你刚刚下载的一些图形。我们将从创建一个蜜蜂精灵开始。我们将把蜜蜂纹理添加到我们的项目中,将图像加载到SKSpriteNode类中,然后调整节点大小以在视网膜屏幕上获得最佳清晰度。

将蜜蜂图像添加到你的项目中

在我们能够在游戏中使用它们之前,我们需要将图像文件添加到我们的 Xcode 项目中。一旦添加了图像,我们就可以在代码中通过名称引用它们;SpriteKit 足够智能,能够找到并实现图形。按照以下步骤将蜜蜂图像添加到项目中:

  1. 在项目导航器中右键单击你的项目,然后点击将文件添加到“Pierre Penguin Escapes the Antarctic”(或你的游戏名称)。参考此截图以找到正确的菜单项:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_02_04.jpg

  2. 浏览你下载的资产包,并在Enemies文件夹中找到bee.png图像。

  3. 选择如果需要则复制项目,然后点击添加

你现在应该在项目导航器中看到bee.png

使用 SKSpriteNode 加载图像

使用SKSpriteNode将图像绘制到屏幕上相当简单。首先,清除我们在GameScene.swift中的didMoveToView函数内编写的所有用于蓝色方块的代码。将didMoveToView替换为以下代码:

override func didMoveToView(view: SKView) {
    // set the scene's background to a nice sky blue
    // Note: UIColor uses a scale from 0 to 1 for its colors
    self.backgroundColor = UIColor(red: 0.4, green: 0.6, blue: 
        0.95, alpha: 1.0);

    // create our bee sprite node
    let bee = SKSpriteNode(imageNamed: "bee.png")
    // size our bee node
    bee.size = CGSize(width: 100, height: 100)
    // position our bee node
    bee.position = CGPoint(x: 250, y: 250)
    // attach our bee to the scene's node tree
    self.addChild(bee)
}

运行项目并见证我们辉煌的蜜蜂——干得好!

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_02_05.jpg

为视网膜设计

你可能会注意到我们的蜜蜂图像相当模糊。为了利用视网膜屏幕,资源需要是其节点大小属性的两倍像素维度(对于大多数视网膜屏幕),或者 iPhone 6 Plus 的节点大小的三倍。暂时忽略高度;我们的蜜蜂节点宽度为 100 点,但 PNG 文件只有 56 像素宽。PNG 文件需要宽度为 300 像素才能在 iPhone 6 Plus 上看起来清晰,或者在 2x 视网膜设备上看起来清晰需要宽度为 200 像素。

SpriteKit 会自动调整纹理大小以适应其节点,因此一种方法是在最高的视网膜分辨率(节点大小的三倍)创建一个巨大的纹理,然后让 SpriteKit 将其调整到较低密度屏幕。然而,这会带来相当大的性能损失,并且旧设备甚至可能因为巨大的纹理而耗尽内存并崩溃。

理想资产方法

这些双倍和三倍大小的视网膜资源可能会让新的 iOS 开发者感到困惑。为了解决这个问题,Xcode 通常允许你为每个纹理提供三个图像文件。例如,我们的蜜蜂节点目前宽度为 100 点,高度为 100 点。在一个完美的世界里,你会向 Xcode 提供以下图像:

  • Bee.png (100 像素 x 100 像素)

  • Bee@2x.png (200 像素 x 200 像素)

  • Bee@3x.png (300 像素 x 300 像素)

然而,目前有一个问题阻止 3x 纹理与纹理图集正确工作。纹理图集将纹理组合在一起并显著提高渲染性能(我们将在下一节中实现我们的第一个纹理图集)。我希望 Apple 能在 Swift 2 中升级纹理图集以支持 3x 纹理。目前,我们需要在 iPhone 6 Plus 的纹理图集和 3x 资源之间做出选择。

我目前的解决方案

在我看来,纹理图集及其性能优势是 SpriteKit 的关键特性。我将继续使用纹理图集,为 iPhone 6 Plus 提供 2x 图像(它仍然看起来相当清晰)。这意味着在这本书中我们不会使用任何 3x 资源。

进一步简化问题,Swift 只运行在 iOS7 及以上版本。唯一运行 iOS7 的非视网膜设备是老化的 iPad 2 和第一代 iPad mini。如果你的最终游戏需要这些旧设备,你应该为你的游戏创建标准图像和 2x 图像。否则,你可以安全地忽略 Swift 的非视网膜资源。

注意

这意味着在这本书中我们只会使用双倍大小的图像。在可下载的资源包中的图像放弃了 2x 后缀,因为我们只使用这个大小。一旦 Apple 更新纹理图集以使用 3x 资源,我建议你切换到理想资源方法部分中概述的方法来为你的游戏使用。

在 SpriteKit 中使用视网膜显示

我们的蜜蜂图像说明了这一切是如何工作的:

  • 由于我们设置了显式的节点大小,SpriteKit 会自动调整蜜蜂纹理的大小以适应我们 100 点宽、100 点高的节点。这种自动调整大小以适应的功能非常方便,但请注意,我们实际上略微扭曲了图像的宽高比。

  • 如果我们不设置显式的大小,SpriteKit 会将节点(以点为单位)的大小调整为与纹理的维度(以像素为单位)相匹配。删除设置我们蜜蜂节点大小的行,并重新运行项目。SpriteKit 会自动保持宽高比,但较小的蜜蜂仍然模糊。这是因为我们的新节点是 56 点 x 48 点,与我们的 PNG 文件的 56 像素 x 48 像素像素维度相匹配……然而,我们的 PNG 文件需要是 112 像素 x 96 像素,才能在 2x 视网膜屏幕上以这个节点大小显示清晰图像。

  • 我们无论如何都需要一个更小的蜜蜂,所以我们将调整节点的大小而不是生成更大的艺术品。将你的蜜蜂节点的size属性设置为纹理像素分辨率的二分之一:

    // size our bee in points:
    bee.size = CGSize(width: 28, height: 24)
    

运行项目,你会看到一个更小、更清晰的蜜蜂,就像这个截图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_02_08.jpg

太棒了!这里的重要概念是要将你的艺术文件设计成节点点大小的两倍像素分辨率,以便利用 2x 视网膜屏幕,或者将点大小增加到三倍以充分利用 iPhone 6 Plus。现在我们将看看如何组织和动画多个精灵帧。

组织你的资源

如果我们像处理蜜蜂一样添加所有纹理,我们的项目导航器很快就会被图像文件淹没。幸运的是,Xcode 提供了几个解决方案。

探索 Images.xcassets

我们可以将图像存储在.xcassets文件中,并轻松地从我们的代码中引用它们。这是一个存储背景图像的好地方:

  1. 从项目导航器中打开Images.xcassets

  2. 目前我们不需要在这里添加任何图像,但将来,你可以直接将图像文件拖到图像列表中,或者右键单击,然后导入

  3. 注意,SpriteKit 演示中的飞船图像存储在这里。我们不再需要它,所以我们可以右键单击它,然后选择删除所选项目来删除它。

将艺术作品收集到纹理图集中

我们将使用纹理图集来组织大部分游戏中的艺术资源。纹理图集通过收集相关的艺术作品来组织资源。它们还通过将每个图集中的所有图像优化为单个纹理来提高性能。SpriteKit 只需要一个绘制调用就能从同一纹理图集中渲染多个图像。此外,它们非常容易使用!按照以下步骤构建你的蜜蜂纹理图集:

  1. 我们需要移除旧的蜜蜂纹理。在项目导航器中右键单击bee.png,然后选择删除,然后移动到废纸篓

  2. 使用 Finder,浏览到你下载的资源包,并定位到Enemies文件夹。

  3. Enemies内部创建一个新的文件夹,并将其命名为bee.atlas

  4. Enemies中找到bee.pngbee_fly.png图像,并将它们复制到你的新bee.atlas文件夹中。现在你应该有一个名为bee.atlas的文件夹,其中包含两个蜜蜂 PNG 文件。创建新的纹理图集你所需要做的就是将相关的图像放置到一个带有.atlas后缀的新文件夹中。

  5. 将图集添加到你的项目中。在 Xcode 中,在项目导航器中右键单击项目文件夹,然后点击添加文件…,就像我们之前为单个蜜蜂纹理所做的那样。

  6. 找到bee.atlas文件夹,并选择文件夹本身。

  7. 选择如果需要则复制项目,然后点击添加

纹理图集将出现在项目导航器中。做得好;我们将蜜蜂资源组织到一个集合中,Xcode 将自动创建之前提到的性能优化。

更新我们的蜜蜂节点以使用纹理图集

我们实际上现在可以运行我们的项目,看到之前相同的蜜蜂。我们旧的蜜蜂纹理是bee.png,而一个新的bee.png存在于纹理图集中。尽管我们删除了独立的bee.png,但 SpriteKit 足够智能,能够在纹理图集中找到新的bee.png

我们应该确保我们的纹理图集正在正常工作,并且我们已经成功删除了旧的单独的bee.png。在GameScene.swift中,将我们的SKSpriteNode实例化行更改为使用纹理图集中的新bee_fly.png图形:

// create our bee sprite
// notice the new image name: bee_fly.png
let bee = SKSpriteNode(imageNamed: "bee_fly.png")

再次运行项目。你应该看到不同的蜜蜂图像,它的翅膀比之前更低。这是蜜蜂动画的第二帧。接下来,我们将学习如何在两个帧之间进行动画,以创建一个动画精灵。

遍历纹理图集帧

我们需要学习一种额外的纹理图集技术:我们可以快速翻阅多个精灵帧,让我们的蜜蜂通过动作变得生动起来。我们现在有两个蜜蜂在飞行中的帧;如果我们在这两个帧之间切换,它应该看起来像是悬停在原地。

我们的小结点将运行一个新的SKAction在两个帧之间进行动画。更新你的didMoveToView函数以匹配我的(我移除了一些旧的注释以节省空间):

override func didMoveToView(view: SKView) {
    self.backgroundColor = UIColor(red: 0.4, green: 0.6, blue: 
        0.95, alpha: 1.0)

    // create our bee sprite
    // Note: Remove all prior arguments from this line:
    let bee = SKSpriteNode()
    bee.position = CGPoint(x: 250, y: 250)
    bee.size = CGSize(width: 28, height: 24)
    self.addChild(bee)

    // Find our new bee texture atlas
    let beeAtlas = SKTextureAtlas(named:"bee.atlas")
    // Grab the two bee frames from the texture atlas in an array
    // Note: Check out the syntax explicitly declaring beeFrames
    // as an array of SKTextures. This is not strictly necessary,
    // but it makes the intent of the code more readable, so I 
    // chose to include the explicit type declaration here:
    let beeFrames:[SKTexture] = [
        beeAtlas.textureNamed("bee.png"), 
        beeAtlas.textureNamed("bee_fly.png")]
    // Create a new SKAction to animate between the frames once
    let flyAction = SKAction.animateWithTextures(beeFrames, 
        timePerFrame: 0.14)
    // Create an SKAction to run the flyAction repeatedly
    let beeAction = SKAction.repeatActionForever(flyAction)
    // Instruct our bee to run the final repeat action:
    bee.runAction(beeAction)
}

运行项目。你会看到我们的蜜蜂翅膀一上一下地拍打——酷!你已经学会了使用纹理图集进行精灵动画的基础。我们将在本书的后面使用相同的技巧创建越来越复杂的动画。现在,给自己鼓掌。结果可能看起来很简单,但你已经解锁了通往你的第一个 SpriteKit 游戏的主要构建块!

将所有这些整合在一起

首先,我们学习了如何使用动作来移动、缩放和旋转我们的精灵。然后,我们探索了通过多个帧进行动画,让我们的精灵栩栩如生。现在,让我们将这些技术结合起来,让我们的蜜蜂在屏幕上飞来飞去,每次转弯时翻转纹理。

didMoveToView函数的底部添加此代码,在bee.runAction(beeAction)行之下:

// Set up new actions to move our bee back and forth:
let pathLeft = SKAction.moveByX(-200, y: -10, duration: 2)
let pathRight = SKAction.moveByX(200, y: 10, duration: 2)
// These two scaleXTo actions flip the texture back and forth
// We will use these to turn the bee to face left and right
let flipTextureNegative = SKAction.scaleXTo(-1, duration: 0)
let flipTexturePositive = SKAction.scaleXTo(1, duration: 0)
// Combine actions into a cohesive flight sequence for our bee
let flightOfTheBee = SKAction.sequence([pathLeft, 
    flipTextureNegative, pathRight, flipTexturePositive])
// Last, create a looping action that will repeat forever
let neverEndingFlight = 
    SKAction.repeatActionForever(flightOfTheBee)

// Tell our bee to run the flight path, and away it goes!
bee.runAction(neverEndingFlight)

运行项目。你会看到蜜蜂在飞来飞去,拍打翅膀。你正式学会了 SpriteKit 中的动画基础!我们将在此基础上构建,为我们的玩家创建一个丰富的动画游戏世界。

将相机中心对准精灵

游戏通常需要相机跟随玩家精灵在空间中的移动。我们确实希望我们的企鹅角色皮埃尔有这种行为,我们很快就会将其添加到游戏中。由于 SpriteKit 没有内置相机功能,我们将创建自己的结构来模拟我们想要的效果。

我们实现这一目标的一种方法是将皮埃尔保持在同一位置,并将其他每个对象移动过他。这是有效的,但在语义上可能有些混乱,并且在定位游戏对象时可能会引起错误。

创建一个新世界

我更喜欢创建一个世界节点,并将所有我们的游戏节点附加到它上(而不是直接附加到场景)。我们可以通过世界将皮埃尔向前移动,并简单地重新定位世界节点,以便皮埃尔始终位于我们设备视口的中心。所有我们的敌人、道具和结构都将作为世界节点的子节点,并且在我们滚动世界时看起来像是在屏幕上移动。

小贴士

每个精灵节点的位置始终相对于其直接父节点。当您更改节点的位置时,所有子节点都会随之移动。这对于模拟我们的相机来说是一个非常方便的行为。

此图展示了该技术的简化版本,并使用了一些虚构的数字:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_02_06.jpg

您可以在以下代码块中找到我们相机功能的代码。阅读注释以获取详细说明。这只是一个快速回顾更改:

  • 我们的didMoveToView函数变得越来越拥挤。我将我们的飞行蜜蜂代码拆分到一个名为addTheFlyingBee的新函数中。稍后,我们将游戏对象,如蜜蜂,封装到它们自己的类中。

  • 我在GameScene类中创建了两个新的常量:世界节点和蜜蜂节点。

  • 我更新了didMoveToView函数。它将世界节点添加到场景的节点树中,并调用新的addTheFlyingBee函数。

  • 在新的蜜蜂函数内部,我移除了蜜蜂常量,因为GameScene现在将其声明为其自己的属性。

  • 在新的蜜蜂函数内部,我们不是通过self.addChild(bee)将蜜蜂节点添加到场景中,而是想通过world.addChild(bee)将其添加到世界中。

  • 我们正在实现一个新的函数:didSimulatePhysics。SpriteKit 在执行物理计算和调整位置后,每帧都会调用此函数。这是一个更新我们世界位置的好地方。更改世界位置的数学计算位于这个新函数中。

请更新您的整个GameScene.swift文件以匹配我的:

import SpriteKit

class GameScene: SKScene {
    // Create the world as a generic SKNode
    let world = SKNode()
    // Create our bee node as a property of GameScene so we can 
    // access it throughout the class
    // (Make sure to remove the old bee declaration inside the 
    // didMoveToView function.)
    let bee = SKSpriteNode()

    override func didMoveToView(view: SKView) {
        self.backgroundColor = UIColor(red: 0.4, green: 0.6, blue: 
            0.95, alpha: 1.0)

        // Add the world node as a child of the scene
        self.addChild(world)
        // Call the new bee function
        self.addTheFlyingBee()
    }

    // I moved all of our bee animation code into a new function:
    func addTheFlyingBee() {
        // Position our bee
        bee.position = CGPoint(x: 250, y: 250)
        bee.size = CGSize(width: 28, height: 24)
        // Notice we now attach our bee node to the world node:
        world.addChild(bee)

        /*
            all of the same bee animation code remains here,
            I am excluding it in this text for brevity
        */
    }

    // A new function
    override func didSimulatePhysics() {
        // To find the correct position, subtract half of the   
        // scene size from the bee's position, adjusted for any  
        // world scaling.
        // Multiply by -1 and you have the adjustment to keep our 
        // sprite centered:
        let worldXPos = -(bee.position.x * world.xScale - 
            (self.size.width / 2))
        let worldYPos = -(bee.position.y * world.yScale - 
            (self.size.height / 2))
        // Move the world so that the bee is centered in the scene
        world.position = CGPoint(x: worldXPos, y: worldYPos)
    }

}

运行游戏。您应该看到我们的蜜蜂直接固定在屏幕中心,每两秒翻转一次。

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_02_07.jpg

蜜蜂实际上正在改变位置,就像之前一样,但世界正在补偿以保持蜜蜂在屏幕中心。当我们第三章中添加更多游戏对象时,加入物理,蜜蜂看起来就像整个世界在屏幕上滚动时在飞行。

检查点 2-B

在本章中,我们对项目进行了许多更改。如果您想下载到这一点的项目,请在此处操作:

Swift 游戏开发

摘要

您已经获得了 SpriteKit 中精灵、节点和动作的基础知识,并且已经朝着用 Swift 制作您的第一个游戏迈出了巨大的步伐。

您已为项目配置了横幅方向,绘制了您的第一个精灵,然后让它移动、旋转和缩放。您为精灵添加了蜜蜂纹理,创建了一个图像图集,并通过飞行帧进行动画。最后,您构建了一个世界节点,以使游戏玩法始终围绕玩家进行。做得好!

在下一章中,我们将使用 SpriteKit 的物理引擎为我们的世界分配重量和重力,生成更多飞行角色,并创建地面和天空。

第三章:混合物理

SpriteKit 包含一个功能齐全的物理引擎。它易于实现且非常有用;大多数移动游戏设计都需要游戏对象之间一定程度的物理交互。在我们的游戏中,我们想知道玩家何时撞到地面、敌人或道具。物理系统可以跟踪这些碰撞,并在这些事件发生时执行我们的特定游戏代码。SpriteKit 的物理引擎还可以将重力应用于世界,使碰撞的精灵相互弹跳和旋转,并通过冲量创建逼真的运动——而且它会在屏幕上绘制每一帧之前完成所有这些。

本章包括以下主题:

  • 为了保持一致性,采用协议

  • 将游戏对象组织到类中

  • 添加玩家的角色

  • 重建 GameScene

  • 物理体和重力

  • 探索物理模拟机制

  • 使用冲量和力进行移动

  • 将蜜蜂撞进蜜蜂中

打好基础

到目前为止,我们通过向 GameScene 类逐个添加小块代码来学习。我们应用程序的复杂性即将增加。为了构建一个复杂的游戏世界,我们需要构建可重用的类并积极组织我们的新代码。

遵循协议

首先,我们想要为每个游戏对象创建单独的类(蜜蜂类、玩家企鹅类、道具类等)。此外,我们希望所有游戏对象类都共享一组一致的属性和方法。我们可以通过创建一个 协议,即我们游戏类的蓝图来强制这种一致性。协议本身不提供任何功能,但采用该协议的每个类都必须完全遵循其规范,Xcode 才能编译项目。如果您来自 Java 或 C# 背景,协议与接口非常相似。

将新文件添加到您的项目中(在项目导航器中右键单击并选择新建文件,然后选择Swift 文件),并将其命名为 GameSprite.swift。然后,将以下代码添加到您的新文件中:

import SpriteKit

protocol GameSprite {
    var textureAtlas: SKTextureAtlas { get set }
    func spawn(parentNode: SKNode, position: CGPoint, size: 
        CGSize)
    func onTap()
}

现在,任何采用 GameSprite 协议的类都必须实现一个 textureAtlas 属性、一个 spawn 函数和一个 onTap 函数。当我们用代码处理游戏对象时,我们可以安全地假设游戏对象提供了这些实现。

重新发明蜜蜂

我们的老蜜蜂工作得非常好,但我们想在世界的各个地方生成许多蜜蜂。我们将创建一个继承自 SKSpriteNodeBee 类,这样我们就可以干净利落地将任意数量的蜜蜂印在世界上了。

将每个类单独分离到其自己的文件中是一种常见的约定。向您的项目中添加一个新的 Swift 文件,并将其命名为 Bee.swift。然后,添加以下代码:

import SpriteKit

// Create the new class Bee, inheriting from SKSpriteNode
// and adopting the GameSprite protocol:
class Bee: SKSpriteNode, GameSprite {
    // We will store our texture atlas and bee animations as
    // class wide properties.
    var textureAtlas:SKTextureAtlas = 
        SKTextureAtlas(named:"bee.atlas")
    var flyAnimation = SKAction()

    // The spawn function will be used to place the bee into
    // the world. Note how we set a default value for the size
    // parameter, since we already know the size of a bee
    func spawn(parentNode:SKNode, position: CGPoint, size: CGSize 
        = CGSize(width: 28, height: 24)) {
        parentNode.addChild(self)
        createAnimations()
        self.size = size
        self.position = position
        self.runAction(flyAnimation)
    }

    // Our bee only implements one texture based animation.
    // But some classes may be more complicated,
    // So we break out the animation building into this function:
    func createAnimations() {
        let flyFrames:[SKTexture] = 
            [textureAtlas.textureNamed("bee.png"), 
            textureAtlas.textureNamed("bee_fly.png")]
        let flyAction = SKAction.animateWithTextures(flyFrames, 
            timePerFrame: 0.14)
        flyAnimation = SKAction.repeatActionForever(flyAction)
    }

    // onTap is not wired up yet, but we have to implement this
    // function to adhere to our protocol.
    // We will explore touch events in the next chapter.
    func onTap() {}
}

现在可以轻松地生成我们想要的任意数量的蜜蜂。切换回 GameScene.swift,并在 didMoveToView 中添加以下代码:

// Create three new instances of the Bee class:
let bee2 = Bee()
let bee3 = Bee()
let bee4 = Bee()
// Use our spawn function to place the bees into the world:
bee2.spawn(world, position: CGPoint(x: 325, y: 325))
bee3.spawn(world, position: CGPoint(x: 200, y: 325))
bee4.spawn(world, position: CGPoint(x: 50, y: 200))

运行项目。蜜蜂,到处都是!我们的原始蜜蜂正在一群蜜蜂中来回飞行。您的模拟器应该看起来像这样:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_03_01.jpg

根据你的看法,你可能觉得新蜜蜂在移动,而原始蜜蜂是静止的。我们需要添加一个参考点。接下来,我们将添加地面。

冰原

我们将在屏幕底部添加一些地面,作为玩家定位的约束和移动的参考点。我们将创建一个名为Ground的新类。首先,让我们将地面艺术纹理图集添加到我们的项目中。

另一种添加资源的方式

我们将使用不同的方法将文件添加到 Xcode 中。按照以下步骤添加新的艺术作品:

  1. 在 Finder 中,导航到你在第二章下载的资产包,精灵、相机、动作!,然后到Environment文件夹。

  2. 你之前已经学会了如何为我们的蜜蜂创建纹理图集。我已经为我们在游戏中使用的其余艺术作品创建了纹理图集。定位ground.atlas文件夹。

  3. 将此文件夹拖放到 Xcode 的项目管理器中,在项目文件夹下,如图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_03_02.jpg

  4. 在对话框中,确保你的设置与以下截图匹配,然后点击完成https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_03_03.jpg

完美——你应该在项目导航器中看到地面纹理图集。

添加地面类

接下来,我们将添加地面代码。在你的项目中添加一个新的 Swift 文件,并将其命名为Ground.swift。使用以下代码:

import SpriteKit

// A new class, inheriting from SKSpriteNode and
// adhering to the GameSprite protocol.
class Ground: SKSpriteNode, GameSprite {
    var textureAtlas:SKTextureAtlas = 
        SKTextureAtlas(named:"ground.atlas")
    // Create an optional property named groundTexture to store 
    // the current ground texture:
    var groundTexture:SKTexture?

    func spawn(parentNode:SKNode, position:CGPoint, size:CGSize) {
        parentNode.addChild(self)
        self.size = size
        self.position = position
        // This is one of those unique situations where we use
        // non-default anchor point. By positioning the ground by
        // its top left corner, we can place it just slightly
        // above the bottom of the screen, on any of screen size.
        self.anchorPoint = CGPointMake(0, 1)

        // Default to the ice texture:
        if groundTexture == nil {
            groundTexture = textureAtlas.textureNamed("ice-
                tile.png");
        }

        // We will create child nodes to repeat the texture.
        createChildren()
    }

    // Build child nodes to repeat the ground texture
    func createChildren() {
        // First, make sure we have a groundTexture value:
        if let texture = groundTexture {
            var tileCount:CGFloat = 0
            let textureSize = texture.size()
            // We will size the tiles at half the size
            // of their texture for retina sharpness:
            let tileSize = CGSize(width: textureSize.width / 2, 
                height: textureSize.height / 2)

            // Build nodes until we cover the entire Ground width
            while tileCount * tileSize.width < self.size.width {
                let tileNode = SKSpriteNode(texture: texture)
                tileNode.size = tileSize
                tileNode.position.x = tileCount * tileSize.width
                // Position child nodes by their upper left corner
                tileNode.anchorPoint = CGPoint(x: 0, y: 1)
                // Add the child texture to the ground node:
                self.addChild(tileNode)

                tileCount++
            }
        }
    }

    // Implement onTap to adhere to the protocol:
    func onTap() {}
}

纹理平铺

为什么我们需要createChildren函数?SpriteKit 不支持内置方法来重复节点大小的纹理。相反,我们为每个纹理瓦片创建子节点,并将它们附加到父节点的宽度上。性能不是问题;只要我们将子节点附加到一个父节点上,并且所有纹理都来自同一个纹理图集,SpriteKit 就会通过一个绘制调用来处理它们。

将电线接到地面上

我们已经将地面艺术添加到项目中并创建了Ground类。最后一步是在场景中创建Ground的实例。按照以下步骤连接地面:

  1. 打开GameScene.swift,并在GameScene类中添加一个新的属性以创建Ground类的实例。你可以将此放在实例化世界节点(新代码用粗体表示)的下面:

    let world = SKNode()
    let ground = Ground()
    
    
  2. 定位didMoveToView函数。在蜜蜂孵化线下面添加以下代码:

    // size and position the ground based on the screen size.
    // Position X: Negative one screen width.
    // Position Y: 100 above the bottom (remember the ground's top
    // left anchor point).
    let groundPosition = CGPoint(x: -self.size.width, y: 100)
    // Width: 3x the width of the screen.
    // Height: 0\. Our child nodes will provide the height.
    let groundSize = CGSize(width: self.size.width * 3, height: 0)
    // Spawn the ground!
    ground.spawn(world, position: groundPosition, size: groundSize)
    

运行项目。你将看到冰原出现在我们的蜜蜂下方。这个小小的改动在很大程度上有助于营造我们的中心蜜蜂正在穿越空间的感受。你的模拟器应该看起来像这样:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_03_04.jpg

一只野企鹅出现了!

在我们开始物理课程之前,还需要构建一个类:Player类!是时候用指定的玩家节点替换移动的蜜蜂了。

首先,我们将添加我们的企鹅艺术纹理图集。到现在为止,你应该熟悉通过项目导航器添加文件。像之前添加地面资产一样添加皮埃尔的美术。我将皮埃尔的纹理图集命名为 pierre.atlas。你可以在资产包中找到它,在 Pierre 文件夹内。

一旦你将皮埃尔的纹理图集添加到项目中,你就可以创建 Player 类。在你的项目中添加一个新的 Swift 文件,并将其命名为 Player.swift。然后添加以下代码:

import SpriteKit

class Player : SKSpriteNode, GameSprite {
    var textureAtlas:SKTextureAtlas = 
        SKTextureAtlas(named:"pierre.atlas")
    // Pierre has multiple animations. Right now we will
    // create an animation for flying up, and one for going down:
    var flyAnimation = SKAction()
    var soarAnimation = SKAction()

    func spawn(parentNode:SKNode, position: CGPoint,
        size:CGSize = CGSize(width: 64, height: 64)) {
        parentNode.addChild(self)
        createAnimations()
        self.size = size
        self.position = position
        // If we run an action with a key, "flapAnimation",
        // we can later reference that key to remove the action.
        self.runAction(flyAnimation, withKey: "flapAnimation")
    }

    func createAnimations() {
        let rotateUpAction = SKAction.rotateToAngle(0, duration: 
            0.475)
        rotateUpAction.timingMode = .EaseOut
        let rotateDownAction = SKAction.rotateToAngle(-1, 
            duration: 0.8)
        rotateDownAction.timingMode = .EaseIn

        // Create the flying animation:
        let flyFrames:[SKTexture] = [
            textureAtlas.textureNamed("pierre-flying-1.png"),
            textureAtlas.textureNamed("pierre-flying-2.png"),
            textureAtlas.textureNamed("pierre-flying-3.png"),
            textureAtlas.textureNamed("pierre-flying-4.png"),
            textureAtlas.textureNamed("pierre-flying-3.png"),
            textureAtlas.textureNamed("pierre-flying-2.png")
        ]
        let flyAction = SKAction.animateWithTextures(flyFrames, 
            timePerFrame: 0.03)
        // Group together the flying animation frames with a 
        // rotation up:
        flyAnimation = SKAction.group([
            SKAction.repeatActionForever(flyAction),
            rotateUpAction
        ])

        // Create the soaring animation, just one frame for now:
        let soarFrames:[SKTexture] = 
            [textureAtlas.textureNamed("pierre-flying-1.png")]
        let soarAction = SKAction.animateWithTextures(soarFrames, 
            timePerFrame: 1)
        // Group the soaring animation with the rotation down:
        soarAnimation = SKAction.group([
            SKAction.repeatActionForever(soarAction),
            rotateDownAction
        ])
    }

    func onTap() {}
}

太好了!在我们继续之前,我们需要用我们刚刚创建的新 Player 类的实例替换原始蜜蜂。按照以下步骤替换蜜蜂:

  1. GameScene.swift 文件中,靠近顶部,删除创建 bee 常量的行。相反,我们想要实例化一个 Player 实例。添加新行:let player = Player().

  2. 完全删除 addTheFlyingBee 函数。

  3. didMoveToView 方法中,删除调用 addTheFlyingBee 的行。

  4. didMoveToView 方法中,在底部添加一行以生成玩家:

    player.spawn(world, position: CGPoint(x: 150, y: 250))
    
  5. 在下方,在 didSimulatePhysics 方法中,将蜜蜂的引用替换为 player 的引用。回想一下,我们在 第二章 中创建了 didSimulatePhysics 函数,当时我们在一个节点上居中相机。

我们已经成功地将原始蜜蜂转换成了企鹅。在我们继续之前,请确保你的 GameScene 类包含了本章中我们迄今为止所做的所有更改。之后,我们将开始探索物理系统。

修复 GameScene 类

我们对我们的项目做了一些更改。幸运的是,这是之前动画代码的最后一次重大修改。向前看,我们将使用本章中构建的出色结构。到现在为止,你的 GameScene.swift 文件应该看起来像这样:

class GameScene: SKScene {
    let world = SKNode()
    let player = Player()
    let ground = Ground()

    override func didMoveToView(view: SKView) {
        // Set a sky-blue background color:
        self.backgroundColor = UIColor(red: 0.4, green: 0.6, blue: 
            0.95, alpha: 1.0)

        // Add the world node as a child of the scene:
        self.addChild(world)

        // Spawn our physics bees:
        let bee2 = Bee()
        let bee3 = Bee()
        let bee4 = Bee()
        bee2.spawn(world, position: CGPoint(x: 325, y: 325))
        bee3.spawn(world, position: CGPoint(x: 200, y: 325))
        bee4.spawn(world, position: CGPoint(x: 50, y: 200))

        // Spawn the ground:
        let groundPosition = CGPoint(x: -self.size.width, y: 30)
        let groundSize = CGSize(width: self.size.width * 3, 
            height: 0)
        ground.spawn(world, position: groundPosition, size: 
            groundSize)

        // Spawn the player:
        player.spawn(world, position: CGPoint(x: 150, y: 250))
    }

    override func didSimulatePhysics() {
        let worldXPos = -(player.position.x * world.xScale – 
            (self.size.width / 2))
        let worldYPos = -(player.position.y * world.yScale – 
            (self.size.height / 2))
        world.position = CGPoint(x: worldXPos, y: worldYPos)
    }
}

运行项目。你会看到我们的新企鹅在蜜蜂附近悬浮。干得好;我们现在准备好使用所有新节点来探索物理系统。你的模拟器应该看起来像这样的截图:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_03_05.jpg

探索物理系统

SpriteKit 使用 物理体 来模拟物理。我们将物理体附加到所有需要物理计算的节点上。在探索所有细节之前,我们将设置一个快速示例。

如飞般坠落

我们的蜜蜂需要成为物理模拟的一部分,因此我们将为它们的节点添加物理体。打开你的 Bee.swift 文件并定位到 spawn 函数。在函数底部添加以下代码:

// Attach a physics body, shaped like a circle
// and sized roughly to our bee.
self.physicsBody = SKPhysicsBody(circleOfRadius: size.width / 2)

向物理模拟中添加节点就这么简单。运行项目。你会看到我们的三个 Bee 实例从屏幕上掉落。它们现在受到重力的作用,默认情况下重力是开启的。

巩固地面

我们希望地面能够捕捉下落的游戏对象。我们可以给地面自己的物理体,这样物理模拟就可以阻止蜜蜂穿过它。打开您的Ground.swift文件,找到spawn函数,然后在函数底部添加以下代码:

// Draw an edge physics body along the top of the ground node.
// Note: physics body positions are relative to their nodes.
// The top left of the node is X: 0, Y: 0, given our anchor point.
// The top right of the node is X: size.width, Y: 0
let pointTopRight = CGPoint(x: size.width, y: 0)
self.physicsBody = SKPhysicsBody(edgeFromPoint: CGPointZero, 
    toPoint: pointTopRight)

运行项目。现在蜜蜂会迅速下落,然后一旦与地面碰撞就会停止。注意下落得更远的蜜蜂弹跳得更有力。蜜蜂着陆后,您的模拟器将看起来像这样:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_03_06.jpg

检查点 3-A

到目前为止,工作做得很好。我们已经为我们的游戏添加了很多结构,并开始探索物理系统。如果您想下载到这一点的我的项目,请在此处操作:

www.thinkingswiftly.com/game-development-with-swift/chapter-3

探索物理模拟机制

让我们更详细地看看 SpriteKit 物理系统的具体细节。例如,为什么蜜蜂受到重力的作用,而地面却保持在原地?尽管我们为两个节点都附加了物理体,但实际上我们使用了两种不同的物理体样式。有三种类型的物理体,它们的行为略有不同:

  • 动态物理体有体积,并且完全受系统中的力和碰撞的影响。我们将为游戏世界的绝大部分使用动态物理体:玩家、敌人、道具等。

  • 静态物理体有体积但没有速度。物理模拟不会移动具有静态体的节点,但它们仍然可以与其他游戏对象发生碰撞。我们可以使用静态体来制作墙壁或障碍物。

  • 边缘物理体没有体积,物理模拟永远不会移动它们。它们标记了运动边界;其他物理体永远不会越过它们。边缘可以交叉以创建小的封闭区域。

体积(动态和静态)体具有各种属性,这些属性可以修改它们对碰撞和空间移动的反应。这使我们能够创建各种逼真的物理效果。每个属性控制一个物体的物理特性的一个方面:

  • 恢复系数决定了当一个物体弹入另一个物体时损失多少能量。这改变了物体的弹性。SpriteKit 在 0.0 到 1.0 的范围内测量恢复系数。默认值是 0.2。

  • 摩擦描述了滑动一个物体相对于另一个物体所需的力。这个属性也使用 0.0 到 1.0 的刻度,默认值为 0.2。

  • 阻尼决定了物体在空间中移动时减速的速度。你可以把阻尼想象成空气摩擦。线性阻尼决定了物体失去速度的速度,而角阻尼影响旋转。两者都从 0.0 到 1.0 测量,默认值为 0.1。

  • 质量是以千克为单位的。它描述了碰撞物体推动物体的距离,并在运动中考虑动量。质量更大的物体在受到另一个物体的撞击时移动较少,并且在它们相互碰撞时会将其他物体推得更远。物理引擎会自动使用物体的质量和面积来确定 密度。或者,你可以设置密度,让物理引擎计算质量。通常设置质量更直观。

好的——教科书就到这里吧!让我们通过一些例子来巩固我们的学习。

首先,我们希望重力跳过我们的蜂。我们将手动设置它们的飞行路径。我们需要蜂成为动态物理体,以便与其他节点正确交互,但我们需要这些体忽略重力。对于这种情况,SpriteKit 提供了一个名为 affectedByGravity 的属性。打开 Bee.swift,在 spawn 函数的底部添加以下代码:

self.physicsBody?.affectedByGravity = false

小贴士

physicsBody 后面的问号是可选链。我们需要解包 physicsBody,因为它是一个可选值。如果 physicsBody 为 nil,整个语句将返回 nil(而不是触发错误)。你可以把它想象成用内联语句优雅地解包一个可选属性。

运行项目。蜂群现在应该像我们添加它们身体之前一样停留在原地。然而,SpriteKit 的物理模拟现在会影响它们;它们会对冲量和碰撞做出反应。太好了,让我们故意让蜂群相撞。

蜂遇蜂

你可能已经注意到我们在游戏世界中将 bee2bee3 放在了相同的高度。我们只需要推动其中一个水平方向,以创建碰撞——完美的碰撞测试假人!我们可以使用 冲量 为外部蜂创建速度。

GameScene.swift 中找到 didMoveToView 函数。在所有生成代码的下方,添加这一行:

bee2.physicsBody?.applyImpulse(CGVector(dx: -3, dy: 0))

运行项目。你会看到最外层的蜂飞向中间并撞到内蜂。这会把内蜂推向左边,并减缓第一只蜂的接触速度。

用一个变量:增加质量,尝试相同的实验。在冲量行之前,添加以下代码来调整 bee2 的质量:

bee2.physicsBody?.mass = 0.2

运行项目。嗯,我们的重蜂在相同的冲量下移动不远(毕竟它是一只 200 克的蜂。)它最终撞到了内蜂,但碰撞并不令人兴奋。我们需要增加冲量来推动我们更重的蜂。将冲量行更改为使用 -15dx 值:

bee2.physicsBody?.applyImpulse(CGVector(dx: -15, dy: 0))

再次运行项目。这次,我们的冲量提供了足够的能量,使重蜂以有趣的方式移动。注意重蜂在碰撞时传递给普通蜂的能量;轻蜂在接触后飞走。两只蜂都有足够的动量,最终完全滑出屏幕。你的模拟器应该看起来像这张截图,就在蜂群滑出屏幕之前:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_03_07.jpg

在您继续之前,您可能希望尝试我在本章前面概述的各种物理属性。您可以创建许多碰撞变体;物理模拟只需付出很少的努力就能提供很多深度。

冲量还是力?

您有几种选项可以使用物理体移动节点:

  • 冲量是对物理体速度的即时、一次性改变。在我们的测试中,冲量给了蜜蜂速度,并且它慢慢因为阻尼和碰撞而减速。冲量非常适合投射物:导弹、子弹、不高兴的鸟等等。

  • 力只在一个物理计算周期内作用于速度。当我们使用力时,我们通常在每一帧之前应用它。力对于火箭船、汽车或其他持续自我推进的任何东西都很有用。

  • 您还可以直接编辑物体的velocityangularVelocity属性。这对于设置手动速度限制很有用。

检查点 3-B

在本章中,我们对我们的项目进行了多项结构性的更改。您可以随意下载我到目前为止的项目:

Swift 游戏开发

摘要

在本章中,我们取得了巨大的进步。我们新的类组织将在整本书的进程中为我们提供良好的服务。我们学习了如何使用协议在类之间强制一致性,将游戏对象封装到不同的类中,并探讨了在地面节点宽度上的平铺纹理。最后,我们从GameScene中清理了一些之前的学习代码,并使用新的类系统生成所有游戏对象。

我们还将物理模拟应用于我们的游戏。我们在 SpriteKit 中强大的物理系统中只是触及了表面——我们将在第七章实现碰撞事件中深入探讨自定义碰撞事件——但我们已经获得了相当多的功能。我们探索了三种类型的物理体,并研究了您可以使用来微调游戏对象物理行为的各种物理属性。然后,我们通过让蜜蜂相互碰撞并观察结果来将所有辛勤工作付诸实践。

接下来,我们将尝试几种控制方案,并将玩家移动到游戏世界中。这是一个令人兴奋的补充;我们的项目将开始感觉像一款真正的游戏。添加控制。

第四章。添加控件

玩家通过非常有限的操作来控制移动游戏。通常,游戏只包含一个机制:在屏幕上任何地方轻触以跳跃或飞行。与此相对的是拥有数十种按钮组合的家用游戏机控制器。在如此少的动作下,保持用户通过光滑、有趣的控件保持兴趣对于游戏的成功至关重要。

在本章中,你将学习实现从应用商店中出现的几种流行控制方案。首先,我们将尝试倾斜控制;设备的物理方向将决定玩家的飞行方向。然后,我们将连接我们的精灵节点上的 onTap 事件。最后,我们将实现并完善我们游戏中飞行的简单控制方案:在屏幕上任何地方轻触以飞得更高。你可以结合这些技术,在你的未来游戏中创建独特且有趣的控件。

本章包括以下主题:

  • Player 类进行飞行改造

  • 使用 Core Motion 轮询设备移动

  • 连接精灵 onTap 事件

  • 教导我们的企鹅学会飞行

  • 改进相机

  • 当玩家向前移动时循环地面

为玩家类进行飞行改造

在我们可以对玩家输入做出反应之前,我们需要执行一些快速设置任务。我们将移除一些旧的测试代码,并为 Player 类添加一个物理体。

蜜蜂饲养者

首先,清理上一章中旧的蜜蜂物理测试。打开 GameScene.swift,找到 didMoveToView,找到底部两行;一行设置了 bee2 的质量,另一行对 bee2 应用了冲量。删除这些行。

更新玩家类

我们需要为 Player 类提供一个自己的 update 函数。我们希望在 Player 中存储与玩家相关的逻辑,并且我们需要它在每一帧之前运行。

  1. 打开 Player.swift 并在 Player 中添加以下函数:

    func update() { }
    
  2. GameScene.swift 中,在 GameScene 类的底部添加以下代码:

    override func update(currentTime: NSTimeInterval) {
        player.update()
    }
    

完美。GameScene 类将在每次更新时调用 player class update 函数。

移动地面

在上一章中,我们最初将地面设置得比必要的位置更高,以确保它在所有屏幕尺寸上都能显示。现在,由于玩家将很快开始移动,将相机带到他们想去的地方,我们可以将地面移动到其最终位置。

GameScene.swift 中,找到定义 groundPosition 常量的行,并将 y 值从 100 更改为 30

let groundPosition = CGPoint(x: -self.size.width, y: 30)

为玩家分配物理体

我们将使用物理力来移动屏幕上的玩家。要应用这些力,我们必须首先为玩家精灵添加一个物理体。

从纹理创建物理体形状

当游戏玩法允许时,您应该使用圆形来定义您的物理体 - 它是物理模拟中最有效的形状,并产生最高的帧率。然而,皮埃尔形状的准确性对我们游戏玩法非常重要,而圆形并不是他的形状的理想选择。相反,我们将根据他的纹理分配一种特殊的物理体类型。

苹果在 Xcode 6 中引入了使用不透明纹理像素定义物理体形状的能力。这是一个方便的补充,因为它允许我们轻松创建极其精确的精灵形状。使用这些由纹理驱动的物理体会有性能损失;使用这些纹理驱动的物理体计算成本很高。您应该谨慎使用,仅在对您最重要的精灵上使用。

要创建皮埃尔的物理体,在Player.swiftspawn函数底部添加以下代码:

// Create a physics body based on one frame of Pierre's animation.
// We will use the third frame, when his wings are tucked in,
// and use the size from the spawn function's parameters:
let bodyTexture = textureAtlas.textureNamed("pierre-flying-3.png")
self.physicsBody = SKPhysicsBody(
    texture: bodyTexture,
    size: size)
// Pierre will lose momentum quickly with a high linearDamping:
self.physicsBody?.linearDamping = 0.9
// Adult penguins weigh around 30kg:
self.physicsBody?.mass = 30
// Prevent Pierre from rotating:
        self.physicsBody?.allowsRotation = false

运行项目后,地面看起来会上升至皮埃尔处。因为我们已经给他一个物理体,所以他现在受到重力的作用。皮埃尔实际上正在下降网格,而摄像头正在调整以保持他居中。现在这很好;稍后我们将给他飞向天空的工具。接下来,让我们学习如何根据物理设备的倾斜移动一个角色。

使用核心运动轮询设备移动

苹果提供了核心运动框架,以暴露 iOS 设备在物理空间中的精确方向信息。我们可以使用这些数据,当用户将设备倾斜到他们想要移动的方向时,在屏幕上移动我们的玩家。这种独特的输入风格为移动游戏提供了新的游戏玩法机制。

注意

您需要一台物理 iOS 设备来完成这个核心运动部分。Xcode 中的 iOS 模拟器无法模拟设备移动。然而,这部分只是一个学习练习,并不是完成我们正在构建的游戏所必需的。我们的最终游戏将不会使用核心运动。如果您无法使用物理设备进行测试,请随意跳过核心运动部分。

实现核心运动代码

检测设备方向非常简单。我们将在每次更新时检查设备位置,并给我们的玩家应用适当的力。按照以下步骤实现核心运动控制:

  1. GameScene.swift中,靠近顶部,在import SpriteKit行下方添加一个新的import语句:

    import CoreMotion
    
  2. GameScene类中,添加一个名为motionManager的新常量,并实例化一个CMMotionManager对象:

    let motionManager = CMMotionManager()
    
  3. GameScene函数didMoveToView中,在底部添加以下代码。这会让核心运动知道我们想要轮询方向数据,因此它需要开始报告数据:

    self.motionManager.startAccelerometerUpdates()
    
  4. 最后,在update函数的底部添加以下代码以轮询方向,构建适当的向量,并给玩家的角色应用物理力:

    // Unwrap the accelerometer data optional:
    if let accelData = self.motionManager.accelerometerData {
        var forceAmount:CGFloat
        var movement = CGVector()
    
        // Based on the device orientation, the tilt number
        // can indicate opposite user desires. The  
        // UIApplication class exposes an enum that allows
        // us to pull the current orientation.
        // We will use this opportunity to explore Swift's    
        // switch syntax and assign the correct force for the 
        // current orientation:
        Switch
       UIApplication.sharedApplication().statusBarOrientation {
        case .LandscapeLeft:
            // The 20,000 number is an amount that felt right
            // for our example, given Pierre's 30kg mass:
            forceAmount = 20000
        case .LandscapeRight:
            forceAmount = -20000
        default:
            forceAmount = 0
        }
    
        // If the device is tilted more than 15% towards complete
        // vertical, then we want to move the Penguin:
        if accelData.acceleration.y > 0.15 {
            movement.dx = forceAmount
        }
        // Core Motion values are relative to portrait view. // Since we are in landscape, use y-values for x-axis.
        else if accelData.acceleration.y < -0.15 {
            movement.dx = -forceAmount
        }
    
        // Apply the force we created to the player:
        player.physicsBody?.applyForce(movement)
    }
    

运行项目。你可以通过倾斜你的设备到你想要移动的方向来滑动 Pierre 横越冰面。干得好——我们成功实现了我们的第一个控制系统。

小贴士

注意,当你将 Pierre 向任何方向移动得太远时,他会穿过地面。在本章的后面部分,我们将改进地面,使其不断重新定位以覆盖玩家下方的区域。

这是一个使用 Core Motion 数据进行玩家移动的简单示例;我们不会在我们的最终游戏中使用这种方法。尽管如此,你仍然可以将这个示例扩展到你自己游戏中的高级控制方案。

检查点 4-A

要下载我的项目,包括 Core Motion 代码,请访问此地址:

www.thinkingswiftly.com/game-development-with-swift/chapter-4

连接精灵的 onTap 事件

你的游戏通常会需要当玩家点击特定精灵时运行代码的能力。我喜欢实现一个包含你游戏中所有精灵的系统,这样你就可以为每个精灵添加点击事件,而无需构建额外的结构。我们已经在所有采用 GameSprite 协议的类中实现了 onTap 方法;我们还需要将场景连接起来,以便在玩家点击精灵时调用这些方法。

注意

在我们继续之前,我们需要移除 Core Motion 代码,因为我们不会在最终游戏中使用它。一旦你完成对 Core Motion 示例的探索,请按照上一节中的项目符号反向操作将其从游戏中移除。

在 GameScene 中实现 touchesBegan

SpriteKit 每次屏幕被触摸时都会调用我们场景的 touchesBegan 函数。我们将读取触摸的位置并确定该位置的精灵节点。我们可以检查被触摸的节点是否采用我们的 GameSprite 协议。如果是,这意味着它必须有一个 onTap 函数,然后我们可以调用它。将以下 touchesBegan 函数添加到 GameScene 类中——我喜欢将其放置在 didSimulatePhysics 函数下方:

override func touchesBegan(touches: Set<NSObject>, withEvent 
    event: UIEvent) {
    for touch in (touches as! Set<UITouch>) {
        // Find the location of the touch:
        let location = touch.locationInNode(self)
        // Locate the node at this location:
        let nodeTouched = nodeAtPoint(location)
        // Attempt to downcast the node to the GameSprite protocol
        if let gameSprite = nodeTouched as? GameSprite {
            // If this node adheres to GameSprite, call onTap:
            gameSprite.onTap()
        }
    }
}

这就是我们连接我们在制作的游戏对象类上实现的全部 onTap 函数所需做的。当然,所有这些 onTap 函数目前都是空的;我们现在将添加一些功能来展示效果。

超乎生活

打开你的 Bee.swift 文件并定位到 onTap 函数。暂时地,当点击时我们将蜜蜂扩展到巨大的尺寸,以演示我们已经正确地连接了 onTap 函数。在蜜蜂的 onTap 函数内添加以下代码:

self.xScale = 4
self.yScale = 4

运行项目并点击蜜蜂。它们将扩展到原来的四倍大小,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_04_01.jpg

哦不——巨大的蜜蜂!这个例子表明我们的 onTap 函数是工作的。你可以从 Bee 类中移除你添加的缩放代码。我们将保留 GameScene 中的 onTap 连接代码,这样我们就可以稍后使用点击事件。

教我们的企鹅飞翔

让我们实现企鹅的控制方案。玩家可以点击屏幕上的任何位置使皮埃尔飞得更高,松开手指让他落下。我们将进行相当多的更改——如果你需要帮助,请参考本章末尾的检查点。首先修改Player类;按照以下步骤为我们的Player准备飞行:

  1. Player.swift中,直接向Player类添加一些新属性:

    // Store whether we are flapping our wings or in free-fall:
    var flapping = false
    // Set a maximum upward force.
    // 57,000 feels good to me, adjust to taste:
    let maxFlappingForce:CGFloat = 57000
    // Pierre should slow down when he flies too high:
    let maxHeight:CGFloat = 1000
    
  2. 到目前为止,皮埃尔默认一直在拍打翅膀。相反,我们想默认显示翱翔动画,只有在用户按下屏幕时才运行拍打动画。在spawn函数中,删除运行flyAnimation的行,而是运行soarAnimation

    self.runAction(soarAnimation, withKey: "soarAnimation")
    
  3. 当玩家触摸屏幕时,我们在Player类的update函数中应用向上的力。记住GameScene每帧调用一次这个update函数。在update中添加以下代码:

    // If flapping, apply a new force to push Pierre higher.
    if self.flapping {
        var forceToApply = maxFlappingForce
    
        // Apply less force if Pierre is above position 600
        if position.y > 600 {
            // The higher Pierre goes, the more force we 
            // remove. These next three lines determine the   
            // force to subtract:
            let percentageOfMaxHeight = position.y / maxHeight
            let flappingForceSubtraction = 
                percentageOfMaxHeight * maxFlappingForce
            forceToApply -= flappingForceSubtraction
        }
        // Apply the final force:
        self.physicsBody?.applyForce(CGVector(dx: 0, dy: 
            forceToApply))
    }
    
    // Limit Pierre's top speed as he climbs the y-axis.
    // This prevents him from gaining enough momentum to shoot
    // over our max height. We bend the physics for gameplay:
    if self.physicsBody?.velocity.dy > 300 {
        self.physicsBody?.velocity.dy = 300
    }
    
  4. 最后,我们将在Player类中提供两个函数,以便其他类可以开始和停止拍打行为。当GameScene类检测到触摸输入时,将调用这些函数。将以下函数添加到Player类中:

    // Begin the flap animation, set flapping to true:
    func startFlapping() {
        self.removeActionForKey("soarAnimation")
        self.runAction(flyAnimation, withKey: "flapAnimation")
        self.flapping = true
    }
    
    // Stop the flap animation, set flapping to false:
    func stopFlapping() {
        self.removeActionForKey("flapAnimation")
        self.runAction(soarAnimation, withKey: "soarAnimation")
        self.flapping = false
    }
    

完美,我们的Player已经准备好飞翔了。现在我们将简单地从GameScene类中调用开始和停止函数。

GameScene中监听触摸

SKScene类(GameScene从中继承)包含了一些方便的函数,我们可以使用这些函数来监控触摸输入。按照以下步骤连接GameScene类:

  1. GameScene.swifttouchesBegan函数中,在底部添加以下代码以在用户触摸屏幕时开始Player拍打:

    player.startFlapping()
    
  2. touchesBegan下方,在GameScene类中创建两个新函数。这些函数在用户从屏幕上抬起手指或 iOS 通知中断触摸时停止拍打:

    override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
        player.stopFlapping()
    }
    
    override func touchesCancelled(touches: Set<NSObject>!, withEvent event: UIEvent) {
        player.stopFlapping()
    }
    

微调重力

在测试我们新的飞行代码之前,我们需要进行一项调整。默认的重力设置为-9.8 感觉太真实了。皮埃尔生活在卡通世界里;现实世界的重力有点拖沓。我们可以在GameScene类中调整重力;在didMoveToView函数的底部添加以下行:

// Set gravity
self.physicsWorld.gravity = CGVector(dx: 0, dy: -5)

展开翅膀

运行项目。点击屏幕使皮埃尔飞得更高,松开手指让他落下。玩这个动作;皮埃尔旋转向他的矢量,并在你点击和松开时积累或失去动力。太棒了!你已经成功实现了我们游戏的核心机制。花一分钟时间享受上下飞翔的感觉,就像这个截图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_04_02.jpg

改进相机

我们相机的代码运行良好;它跟随玩家飞行的任何地方。然而,我们可以改进相机以增强飞行体验。在本节中,我们将添加两个新功能:

  • 当皮埃尔企鹅飞得更高时,放大相机,增强增加高度的感觉。

  • 当玩家下降到屏幕下半部分时,暂停垂直居中。这意味着地面永远不会占据屏幕太多,当皮埃尔飞得更高,相机开始再次跟踪他时,增加了向上切割空气的感觉。

按照以下步骤实现这两个改进:

  1. GameScene.swift 中,在 GameScene 类中创建一个新变量以存储屏幕的中心点:

    var screenCenterY = CGFloat()
    
  2. didMoveToView 函数中,使用计算出的屏幕高度的中间值设置这个新变量:

    // Store the vertical center of the screen:
    screenCenterY = self.size.height / 2
    
  3. 我们需要显著重构 didSimulatePhysics 函数。删除现有的 didSimulatePhysics 函数,并用以下代码替换它:

    override func didSimulatePhysics() {
        var worldYPos:CGFloat = 0
    
        // Zoom the world as the penguin flies higher
        if (player.position.y > screenCenterY) {
            let percentOfMaxHeight = (player.position.y - 
                screenCenterY) / (player.maxHeight - 
                screenCenterY)
            let scaleSubtraction = (percentOfMaxHeight > 1 ? 1 : percentOfMaxHeight) * 0.6
            let newScale = 1 - scaleSubtraction
            world.yScale = newScale
            world.xScale = newScale
            // The player is above half the screen size
            // so adjust the world on the y-axis to follow:
            worldYPos = -(player.position.y * world.yScale - 
                (self.size.height / 2))
        }
    
        let worldXPos = -(player.position.x * world.xScale - 
            (self.size.width / 3))
    
        // Move the world for our adjustment:
        world.position = CGPoint(x: worldXPos, y: worldYPos)
    }
    

运行项目,然后飞起。随着高度的增加,世界会缩小。当飞近地面时,相机现在也允许皮埃尔潜入屏幕中心以下。以下截图说明了两种极端情况。注意顶部屏幕中的小精灵,皮埃尔飞得更高,相机也拉远。在底部画面中,当皮埃尔接近地面时,相机停止垂直跟随:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_04_03.jpg

这种综合效果为游戏增添了大量光泽,并增加了飞行的乐趣。我们的飞行机制感觉很好。下一步是将皮埃尔推进世界。

推进皮埃尔前进

这种游戏风格通常以恒定速度推进世界。而不是应用力或冲量,我们可以在每次 update 中手动为皮埃尔设置一个恒定速度。打开 Player.swift 文件,并在 update 函数中添加以下代码:

// Set a constant velocity to the right:
self.physicsBody?.velocity.dx = 200

运行项目。我们的主角企鹅会穿过蜜蜂群和世界向前移动。这很好,但你很快会发现,随着皮埃尔向前移动,地面会消失,如这个截图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_04_04.jpg

记得我们的地面宽度只有屏幕宽度的三倍。而不是扩展地面,我们将在适时的间隔移动地面的位置。由于地面由重复的瓷砖组成,有许多机会可以无缝地跳过其位置向前移动。我们只需要找出玩家移动了正确的距离。

跟踪玩家的进度

首先,我们需要跟踪玩家飞行的距离。我们稍后会用到这个数据,用于记录高分。这很容易实现。按照以下步骤跟踪玩家飞行的距离:

  1. GameScene.swift 文件中,向 GameScene 类添加两个新属性:

    let initialPlayerPosition = CGPoint(x: 150, y: 250)
    var playerProgress = CGFloat()
    
  2. didMoveToView 函数中,更新生成玩家的行,使用新的 initialPlayerPosition 常量而不是旧的硬编码值:

    // Spawn the player:
    player.spawn(world, position: initialPlayerPosition)
    
  3. didSimulatePhysics 函数中,更新新的 playerProgress 属性以包含玩家的新距离:

    // Keep track of how far the player has flown
    playerProgress = player.position.x - initialPlayerPosition.x
    

完美 – 现在我们可以在 GameScene 类中随时访问玩家的进度。我们可以使用行进距离在正确的时间重新定位地面。

地面循环

有许多可能的方法来创建无限地面循环。我们将实现一个简单的解决方案,在玩家行进到大约三分之一的宽度后,将地面向前跳跃。这种方法保证了如果我们的玩家从中间三分之一开始,地面总是覆盖屏幕。

我们将在 Ground 类上创建跳跃逻辑。按照以下步骤实现无限地面:

  1. 打开 Ground.swift 文件,并为 Ground 类添加两个新属性:

    var jumpWidth = CGFloat()
    // Note the instantiation value of 1 here:
    var jumpCount = CGFloat(1)
    
  2. createChildren 函数中,我们找到从三分之一的孩子瓦片的总宽度,并将其作为我们的 jumpWidth。每次玩家行进这段距离时,我们都需要将地面向前跳跃。你只需要在函数底部附近添加一行:在展开纹理的条件下。以下示例中,我将展示整个函数,以提供上下文,新行将以粗体显示:

    func createChildren() {
        if let texture = groundTexture {
            var tileCount:CGFloat = 0
            let textureSize = texture.size()
            let tileSize = CGSize(width: textureSize.width / 2, 
                height: textureSize.height / 2)
    
            while tileCount * tileSize.width < self.size.width {
                let tileNode = SKSpriteNode(texture: texture)
                tileNode.size = tileSize
                tileNode.position.x = tileCount * tileSize.width
                tileNode.anchorPoint = CGPoint(x: 0, y: 1)
                self.addChild(tileNode)
    
                tileCount++
            }
    
            // Find the width of one-third of the children nodes
     jumpWidth = tileSize.width * floor(tileCount / 3)
        }
    }
    
  3. Ground 类中添加一个名为 checkForReposition 的新函数,位于 createChildren 函数下方。场景将在每一帧调用此函数以检查我们是否应该将地面向前跳跃:

    func checkForReposition(playerProgress:CGFloat) {
        // The ground needs to jump forward
        // every time the player has moved this distance:
        let groundJumpPosition = jumpWidth * jumpCount
    
        if playerProgress >= groundJumpPosition {
            // The player has moved past the jump position!
            // Move the ground forward:
            self.position.x += jumpWidth
            // Add one to the jump count:
            jumpCount++
        }
    } 
    
  4. 打开 GameScene.swift 文件,并在 didSimulatePhysics 函数的底部添加以下行以调用 Ground 类的新逻辑:

    // Check to see if the ground should jump forward:
    ground.checkForReposition(playerProgress)
    

运行项目。当皮埃尔向前飞行时,地面看起来会无限延伸。这种循环地面是最终游戏世界的一大步。这可能看起来是为一个简单的效果而做的大量工作,但循环地面很重要,我们的方法将在任何屏幕尺寸上表现良好。干得好!

检查点 4-B

要下载到这一点的项目,请访问此地址:

www.thinkingswiftly.com/game-development-with-swift/chapter-4

摘要

在本章中,我们将技术演示转变为真实游戏的开始。我们添加了大量新的代码。你学习了如何实现三种不同的移动游戏控制方法:物理设备运动、精灵点击事件以及当屏幕被触摸时飞得更高。我们为飞行机制进行了优化,让皮埃尔飞向世界的前方。

你还学习了如何实现两个常见的移动游戏需求:地面循环和更智能的摄像头系统。这两个功能对我们的游戏产生了重大影响。

接下来,我们将为我们关卡添加更多内容。飞行已经很有趣了,但穿过前几个蜜蜂时感觉有点孤单。我们将在 第五章 生成敌人、金币和道具 中给皮埃尔企鹅一些同伴。

第五章:生成敌人、硬币和增强

游戏开发中最有趣和最具创造性的方面之一是为玩家构建可探索的游戏世界。我们的年轻项目在添加控制后开始像可玩游戏一样,下一步是构建更多内容。我们将为新的敌人、可收集的硬币和给予皮埃尔企鹅额外能量的特殊增强创建额外的类。然后我们可以开发一个系统,随着玩家的进步,逐渐生成越来越困难的这些游戏对象的模式。

本章包括以下主题:

  • 添加增强星

  • 新敌人——疯狂飞虫

  • 另一个恐怖——蝙蝠!

  • 可怕的幽灵

  • 用刀守护地面

  • 添加硬币

  • 测试新游戏对象

介绍角色阵容

穿上你的安全帽,我们将在本章中编写大量的代码。坚持下去!结果绝对值得努力。来看看本章我们将介绍的新角色阵容:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/4532_05_01.jpg

添加增强星

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

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/4532_05_03.jpg

定位艺术资源

您可以在资产包的CoinsPowerups文件夹中的goods.atlas纹理图集中找到增强星和硬币的艺术资源。现在将goods.atlas纹理图集添加到您的项目中。

添加 Star 类

一旦艺术资源到位,您可以在项目中创建一个名为Star.swift的新 Swift 文件;我们将继续将类组织到不同的文件中。Star类将与之前创建的Bee类相似;它将继承自SKSpriteNode并遵循我们的GameSprite协议。星星将为玩家带来很多力量,因此我们还将给它一个基于SKAction的特殊疯狂动画,使其脱颖而出。

要创建Star类,请在您的Star.swift文件中添加以下代码:

import SpriteKit

class Star: SKSpriteNode, GameSprite {
    var textureAtlas:SKTextureAtlas = 
        SKTextureAtlas(named:"goods.atlas")
    var pulseAnimation = SKAction()

    func spawn(parentNode:SKNode, position: CGPoint,
        size: CGSize = CGSize(width: 40, height: 38)) {
        parentNode.addChild(self)
        createAnimations()
        self.size = size
        self.position = position
        self.physicsBody = SKPhysicsBody(circleOfRadius: 
            size.width / 2)
        self.physicsBody?.affectedByGravity = false
        // Since the star texture is only one frame, set it here:
        self.texture = 
            textureAtlas.textureNamed("power-up-star.png")
        self.runAction(pulseAnimation)
    }

    func createAnimations() {
        // Scale the star smaller and fade it slightly:
        let pulseOutGroup = SKAction.group([
            SKAction.fadeAlphaTo(0.85, duration: 0.8),
            SKAction.scaleTo(0.6, duration: 0.8),
            SKAction.rotateByAngle(-0.3, duration: 0.8)
            ]);
        // Push the star big again, and fade it back in:
        let pulseInGroup = SKAction.group([
            SKAction.fadeAlphaTo(1, duration: 1.5),
            SKAction.scaleTo(1, duration: 1.5),
            SKAction.rotateByAngle(3.5, duration: 1.5)
            ]);
        // Combine the two into a sequence:
        let pulseSequence = SKAction.sequence([pulseOutGroup, 
            pulseInGroup])
        pulseAnimation = 
            SKAction.repeatActionForever(pulseSequence)
    }

    func onTap() {}
}

太好了!您应该已经熟悉了大部分代码,因为它与我们之前创建的一些类非常相似。让我们继续添加另一个新角色:一只烦躁的飞虫。

添加一个新敌人——疯狂飞虫

皮埃尔企鹅要实现他的目标,不仅需要躲避蜜蜂。在本章中,我们将添加一些新的敌人,首先是MadFly类。疯狂飞虫相当烦躁,正如你所见:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/4532_05_04.jpg

定位敌人资源

您可以在资产包的Enemies文件夹中的enemies.atlas纹理图集中找到我们新敌人的所有艺术资源。现在将这个纹理图集添加到您的项目中。

添加 MadFly 类

MadFly是一个简单的类;它看起来很像Bee代码。创建一个名为MadFly.swift的新 Swift 文件,并输入以下代码:

import SpriteKit

class MadFly: SKSpriteNode, GameSprite {
    var textureAtlas:SKTextureAtlas = 
        SKTextureAtlas(named:"enemies.atlas")
    var flyAnimation = SKAction()

    func spawn(parentNode:SKNode, position: CGPoint,
        size: CGSize = CGSize(width: 61, height: 29)) {
        parentNode.addChild(self)
        createAnimations()
        self.size = size
        self.position = position
        self.runAction(flyAnimation)
        self.physicsBody = SKPhysicsBody(circleOfRadius: 
            size.width / 2)
        self.physicsBody?.affectedByGravity = false
    }

    func createAnimations() {
        let flyFrames:[SKTexture] = [
            textureAtlas.textureNamed("mad-fly-1.png"),
            textureAtlas.textureNamed("mad-fly-2.png")
        ]
        let flyAction = SKAction.animateWithTextures(flyFrames, 
            timePerFrame: 0.14)
        flyAnimation = SKAction.repeatActionForever(flyAction)
    }

    func onTap() {}
}

恭喜,你已经成功实现了疯狂飞行的敌人。没有时间庆祝——继续前进,迎接蝙蝠!

另一个恐怖——蝙蝠!

我们在创建新类方面已经进入了一种相当有节奏的状态。现在,我们将添加一只蝙蝠来与蜜蜂一起群飞。蝙蝠体型小,但有一对非常锋利的獠牙:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/4532_05_05.jpg

添加Bat

要添加Bat类,创建一个名为Bat.swift的文件,并添加以下代码:

import SpriteKit

class Bat: SKSpriteNode, GameSprite {
    var textureAtlas:SKTextureAtlas = 
        SKTextureAtlas(named:"enemies.atlas")
    var flyAnimation = SKAction()

    func spawn(parentNode:SKNode, position: CGPoint,
        size: CGSize = CGSize(width: 44, height: 24)) {
        parentNode.addChild(self)
        createAnimations()
        self.size = size
        self.position = position
        self.runAction(flyAnimation)
        self.physicsBody = SKPhysicsBody(circleOfRadius: 
            size.width / 2)
        self.physicsBody?.affectedByGravity = false
    }

    func createAnimations() {
        // The Bat has 4 animation textures:
        let flyFrames:[SKTexture] = [
            textureAtlas.textureNamed("bat-fly-1.png"),
            textureAtlas.textureNamed("bat-fly-2.png"),
            textureAtlas.textureNamed("bat-fly-3.png"),
            textureAtlas.textureNamed("bat-fly-4.png"),
            textureAtlas.textureNamed("bat-fly-3.png"),
            textureAtlas.textureNamed("bat-fly-2.png")
        ]
        let flyAction = SKAction.animateWithTextures(flyFrames, 
            timePerFrame: 0.06)
        flyAnimation = SKAction.repeatActionForever(flyAction)
    }

    func onTap() {}
}

现在你已经创建了Bat类,还有两个敌人需要添加。我们将在下一个步骤中添加Ghost类。

可怕的幽灵

我们将用另一个可怕的敌人来补充蝙蝠:如这里所示的幽灵:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/4532_05_06.jpg

我们不会通过多个帧进行动画,而是使用动作来动画幽灵的单帧。

添加Ghost

与其他类一样,在你的项目中创建一个名为Ghost.swift的新文件,然后添加以下代码:

import SpriteKit

class Ghost: SKSpriteNode, GameSprite {
    var textureAtlas:SKTextureAtlas = 
        SKTextureAtlas(named:"enemies.atlas")
    var fadeAnimation = SKAction()

    func spawn(parentNode:SKNode, position: CGPoint,
        size: CGSize = CGSize(width: 30, height: 44)) {
        parentNode.addChild(self)
        createAnimations()
        self.size = size
        self.position = position
        self.physicsBody = SKPhysicsBody(circleOfRadius: 
            size.width / 2)
        self.physicsBody?.affectedByGravity = false
        self.texture = 
            textureAtlas.textureNamed("ghost-frown.png")
        self.runAction(fadeAnimation)
        // Start the ghost semi-transparent:
        self.alpha = 0.8;
    }

    func createAnimations() {
        // Create a fade out action group:
        // The ghost becomes smaller and more transparent.
        let fadeOutGroup = SKAction.group([
            SKAction.fadeAlphaTo(0.3, duration: 2),
            SKAction.scaleTo(0.8, duration: 2)
            ]);
        // Create a fade in action group:
        // The ghost returns to full size and transparency.
        let fadeInGroup = SKAction.group([
            SKAction.fadeAlphaTo(0.8, duration: 2),
            SKAction.scaleTo(1, duration: 2)
            ]);
        // Package the groups into a sequence, then a 
        // repeatActionForever action:
        let fadeSequence = SKAction.sequence([fadeOutGroup, 
            fadeInGroup])
        fadeAnimation = SKAction.repeatActionForever(fadeSequence)
    }

    func onTap() {}
}

完美。我们的幽灵准备就绪,可以行动了。我们已经添加了许多飞行敌人来追逐皮埃尔企鹅穿越天空。我们需要一个地面敌人,它可以阻止玩家在地面以上轻易地前进。接下来,我们将添加Blade类。

守护地面——添加刀片

Blade类将阻止皮埃尔飞得太低。这个敌人类将与其他我们创建的类相似,只有一个例外:我们将基于纹理生成一个物理体。我们一直在使用的物理体圆形在计算上更快,通常足以描述我们敌人的形状;Blade类需要一个更复杂的物理体,考虑到它的半圆形形状和凹凸边缘:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/4532_05_07.jpg

添加Blade

要添加Blade类,创建一个名为Blade.swift的新文件,并添加以下代码:

import SpriteKit

class Blade: SKSpriteNode, GameSprite {
    var textureAtlas:SKTextureAtlas = 
        SKTextureAtlas(named:"enemies.atlas")
    var spinAnimation = SKAction()

    func spawn(parentNode:SKNode, position: CGPoint,
        size: CGSize = CGSize(width: 185, height: 92)) {
        parentNode.addChild(self)
        self.size = size
        self.position = position
        // Create a physics body shaped by the blade texture:
        self.physicsBody = SKPhysicsBody(
            texture: textureAtlas.textureNamed("blade-1.png"),
            size: size)
        self.physicsBody?.affectedByGravity = false
        // No dynamic body for the blade, which never moves:
        self.physicsBody?.dynamic = false
        createAnimations()
        self.runAction(spinAnimation)
    }

    func createAnimations() {
        let spinFrames:[SKTexture] = [
            textureAtlas.textureNamed("blade-1.png"),
            textureAtlas.textureNamed("blade-2.png")
        ]
        let spinAction = SKAction.animateWithTextures(spinFrames, 
            timePerFrame: 0.07)
        spinAnimation = SKAction.repeatActionForever(spinAction)
    }

    func onTap() {}
}

恭喜,Blade类是我们游戏中需要添加的最后一个敌人。这个过程可能看起来很重复——你已经写了很多样板代码——但将我们的敌人分成各自的类可以让每个敌人实现独特的逻辑和行为。随着你的游戏变得越来越复杂,这种结构的优势将变得明显。

接下来,我们添加金币的类。

添加金币

如果有两种价值变化,金币会更有趣。我们将创建:

  • 一枚铜币,价值一枚。

  • 一枚金币,价值五枚。

两个金币将通过屏幕上的颜色和硬币上的面额文字来区分,如下所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/4532_05_08.jpg

创建金币类

我们只需要一个Coin类来创建两种面额。到目前为止,Coin类中的所有内容都应该非常熟悉。要创建Coin类,添加一个名为Coin.swift的新文件,然后输入以下代码:

import SpriteKit

class Coin: SKSpriteNode, GameSprite {
    var textureAtlas:SKTextureAtlas = 
        SKTextureAtlas(named:"goods.atlas")
    // Store a default value for the bronze coin:
    var value = 1

    func spawn(parentNode:SKNode, position: CGPoint,
        size: CGSize = CGSize(width: 26, height: 26)) {
        parentNode.addChild(self)
        self.size = size
        self.position = position
        self.physicsBody = SKPhysicsBody(circleOfRadius: 
            size.width / 2)
        self.physicsBody?.affectedByGravity = false
        self.texture =
            textureAtlas.textureNamed("coin-bronze.png")
    }

    // A function to transform this coin into gold!
    func turnToGold() {
        self.texture = 
            textureAtlas.textureNamed("coin-gold.png")
        self.value = 5
    }

    func onTap() {}
}

干得好——我们已经成功添加了所有需要的游戏对象,为我们的最终游戏做好了准备!

组织项目导航器

你可能会注意到这些新类使项目导航器变得杂乱。这是清理导航器的好时机。在项目导航器中右键单击项目,并选择按类型排序,如图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/4532_05_09.jpg

你的项目导航器将根据文件类型进行分段,并按字母顺序排序。这使得在需要时查找文件变得容易得多。

测试新游戏对象

是时候看到我们的辛勤工作付诸实践了。我们现在将向游戏中添加我们每个新类的一个实例。注意,我们在完成后将移除这个测试代码;你可能想给自己留下注释或额外的空间以便于移除。打开GameScene.swift并定位到生成现有蜜蜂的六行代码。在蜜蜂行之后添加此代码:

// Spawn a bat:
let bat = Bat()
bat.spawn(world, position: CGPoint(x: 400, y: 200))

// A blade:
let blade = Blade()
blade.spawn(world, position: CGPoint(x: 300, y: 76))

// A mad fly:
let madFly = MadFly()
madFly.spawn(world, position: CGPoint(x: 50, y: 50))

// A bronze coin:
let bronzeCoin = Coin()
bronzeCoin.spawn(world, position: CGPoint(x: 490, y: 250))

// A gold coin:
let goldCoin = Coin()
goldCoin.spawn(world, position: CGPoint(x: 460, y: 250))
goldCoin.turnToGold()

// A ghost!
let ghost = Ghost()
ghost.spawn(world, position: CGPoint(x: 50, y: 300))

// The powerup star:
let star = Star()
star.spawn(world, position: CGPoint(x: 250, y: 250))

你可能还希望注释掉移动皮埃尔前进的Player类行,这样摄像机就不会快速移动过你的新游戏对象。只是确保你在完成后取消注释。

一旦你准备好了,运行项目。你应该看到整个家族,如图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/4532_05_10.jpg

了不起的工作!我们所有的代码都得到了回报,我们有一大批角色准备行动。

检查点 5-A

下载到这一点的项目,请访问此 URL:

www.thinkingswiftly.com/game-development-with-swift/chapter-5

准备无限飞行

在第六章《生成无限世界》中,我们将通过生成充满这些新游戏对象的战术障碍课程来构建一个无限关卡。我们需要清除所有测试对象,为这个新的关卡生成系统做好准备。一旦你准备好了,从GameScene类中移除我们刚刚添加的生成测试代码。同时,移除我们之前章节中用来生成三只蜜蜂的六行代码。

当你完成时,你的GameScene类的didMoveToView函数应该看起来像这样:

override func didMoveToView(view: SKView) {
    // Set a sky-blue background color:
    self.backgroundColor = UIColor(red: 0.4, green: 0.6, blue: 
        0.95, alpha: 1.0)

    // Add the world node as a child of the scene:
    self.addChild(world)

    // Store the vertical center of the screen:
    screenCenterY = self.size.height / 2

    // Spawn the ground:
    let groundPosition = CGPoint(x: -self.size.width, y: 30)
    let groundSize = CGSize(width: self.size.width * 3, height: 0)
    ground.spawn(world, position: groundPosition, size: 
        groundSize)

    // Spawn the player:
    player.spawn(world, position: initialPlayerPosition)

    // Set gravity
    self.physicsWorld.gravity = CGVector(dx: 0, dy: -5)
}

当你运行项目时,你应该只看到皮埃尔和地面,如图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/4532_05_02.jpg

我们现在准备好构建我们的关卡。

概述

你在本章中为我们游戏添加了完整的角色阵容。回顾一下你所取得的成就;你添加了能量星、铜币和金币、一个幽灵、疯狂的小飞虫、蝙蝠和一把刀。你测试了所有的新类,然后移除了测试代码,以便项目为我们在下一章中将要放置的关卡生成系统做好准备。

我们在构建每个新类上投入了大量的努力。在第六章《生成无限世界》中,世界将变得生动起来,并回报我们的辛勤工作。

第六章. 生成无尽世界

无尽飞行游戏独特的挑战在于以程序方式生成丰富、有趣的游戏世界,其范围延伸到玩家可以飞行的距离。我们将首先探索 Xcode 中的关卡设计概念和工具;Apple 在 Xcode 6 中添加了一个内置关卡设计师,允许开发者在一个场景中直观地排列节点。一旦我们熟悉了 SpriteKit 关卡设计方法,我们将创建一个自定义解决方案来生成我们的世界。在本章中,您将为我们的企鹅游戏构建一个有趣的世界,并学习如何在 SpriteKit 中为任何类型的游戏设计和实现关卡。

本章包括以下主题:

  • 使用 SpriteKit 场景编辑器设计关卡

  • 为皮埃尔企鹅构建遭遇战

  • 将场景集成到游戏中

  • 为永无止境的世界循环遭遇战

  • 随机添加星级提升

使用 SpriteKit 场景编辑器设计关卡

场景编辑器是 SpriteKit 的一个宝贵补充。以前,开发者被迫硬编码位置值或依赖第三方工具或自定义解决方案。现在,我们可以在 Xcode 中直接布局我们的关卡。我们可以创建节点,附加物理体和约束,创建物理场,并直接从界面中编辑属性。

随意尝试场景编辑器,熟悉其界面。要使用场景编辑器,请向您的游戏添加一个新的场景文件,然后在项目导航器中选择场景。以下是一个您可能为平台游戏构建的简单示例场景:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_06_01.jpg

在这个例子中,我只是在场景中拖动并定位了彩色精灵。如果您正在制作一个简单的游戏,您可以直接在场景编辑器中绘制不需要基于纹理动画的节点。通过在编辑器中编辑物理体,您甚至可以在编辑器中创建整个基于物理的游戏,只需添加几行控制代码。

复杂的游戏需要为每个对象定制逻辑和纹理动画,因此我们将在我们的企鹅游戏中实现一个系统,该系统仅使用场景编辑器作为布局生成工具。我们将编写代码来解析编辑器中的布局数据,并将其转换为本书中创建的游戏类的完整功能版本。这样,我们将以最小的努力将游戏逻辑与数据分离。

将关卡数据与游戏逻辑分离

关卡布局是数据,最好将数据与代码分离。通过将关卡数据分离到场景文件中,您可以提高灵活性。其好处包括:

  • 非技术贡献者,如艺术家和设计师,可以在不更改任何代码的情况下添加和编辑关卡。

  • 迭代时间得到改善,因为您每次需要查看更改时不需要在模拟器中运行游戏。场景编辑器布局提供即时视觉反馈。

  • 每个级别都在一个独特的文件中,这在使用像 Git 这样的源代码控制解决方案时避免合并冲突是理想的。

使用空节点作为占位符

场景编辑器缺乏创建可重用类的能力,也没有将您的代码类链接到场景编辑器节点的方法。相反,我们将使用空节点作为场景编辑器中的占位符,并在代码中使用我们自己的类的实例来替换它们。您经常会看到这种技术的变体。例如,苹果的 SpriteKit 冒险游戏演示使用这种技术进行其部分关卡设计。

您可以在场景编辑器中为节点分配名称,然后在代码中查询这些名称。例如,您可以在场景编辑器中创建名为Bat的空节点,然后在GameScene类中编写代码,将每个名为Bat的节点替换为我们的Bat类的实例。

为了说明这个概念,我们将为企鹅游戏创建我们的第一次相遇。

无尽飞行中的相遇

无尽飞行动作游戏会一直进行,直到玩家失败。它们没有特定的级别;相反,我们将为我们的主角企鹅设计“相遇”。我们可以通过将一次又一次的相遇连接起来,并在需要更多内容时从开始处随机回收,来创建一个无尽的世界。

以下图像说明了基本概念:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_06_10.jpg

一款完成的游戏可能包含 20 个或更多的相遇,以感觉多样化且随机。在本章中,我们将创建三个相遇来充实相遇回收系统。

我们将像对待标准平台游戏或物理游戏中的单独关卡一样,在各自的场景文件中构建每个相遇。

创建我们的第一次相遇

首先,创建一个相遇文件夹组以保持我们的项目有序。在项目导航器中右键单击您的项目,创建一个名为Encounters的新组。然后,在Encounters上右键单击,并添加一个名为EncounterBats.sks的新 SpriteKit 场景文件(从iOS | 资源类别)。

Xcode 会将新的场景文件添加到您的项目中,并打开场景编辑器。您应该看到一个灰色背景和黄色边框,指示新场景的边界。场景默认宽度为 1024 点,高度为 768 点。我们应该更改这些值。如果每个相遇宽度为 1000 像素,高度为 650 点,那么将它们连接起来会更容易。

您可以轻松地在 SKNode 检查器中更改场景的大小值。在场景编辑器的右上角,确保您已通过选择最右边的图标打开 SKNode 检查器,然后更改宽度和高度,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_06_02.jpg

接下来,我们将为Bat类创建第一个占位符节点。按照以下步骤在场景编辑器中创建一个空节点

  1. 你可以从对象库中拖动节点。要打开对象库,请看向场景编辑器的右下角并选择圆形图标,如图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_06_03.jpg

  2. 将一个空节点拖动到你的场景中。

  3. 使用右上角的 SKNode 检查器,将你的节点命名为Bat,如图所示:https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_06_04.jpg

你将看到蝙蝠出现在空节点上方。太好了,我们已经创建了一个占位符。我们将重复此过程,直到为皮埃尔企鹅构建一个完整的遭遇。我们不仅可以使用蝙蝠,但我们需要首先定义我们将用于标记每个节点的名称。如果你是在团队中制作游戏,你将想要事先达成一致。以下是我将用于每个游戏对象的标签:

游戏对象类场景编辑器节点名称
Bat蝙蝠
Bee蜜蜂
Blade刀片
Coin (bronze)青铜币
Coin (gold)金币
Ghost幽灵
MadFly疯狂飞虫

随意构建你的蝙蝠遭遇。添加更多空节点,并使用标签,直到你对设计满意为止。试着想象企鹅角色在遭遇中飞翔。

在我的遭遇中,我创建了一条通过蝙蝠的简单路径,路径上布满了青铜币,以及一条在蝙蝠下方和刀片上方的更难路径,路径上布满了金币。你可以使用以下图像中的我的蝙蝠遭遇作为灵感:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_06_05.jpg

将场景集成到游戏中

接下来,我们将创建一个新的类来管理我们游戏中的遭遇。将一个新的 Swift 文件添加到你的项目中,并将其命名为EncounterManager.swiftEncounterManager类将遍历我们的遭遇场景,并使用位置数据在游戏世界中创建适当的游戏对象类。在新的文件中添加以下代码:

import SpriteKit

class EncounterManager {
    // Store your encounter file names:
    let encounterNames:[String] = [
        "EncounterBats"
    ]
    // Each encounter is an SKNode, store an array:
    var encounters:[SKNode] = []

    init() {
        // Loop through each encounter scene:
        for encounterFileName in encounterNames {
            // Create a new node for the encounter:
            let encounter = SKNode()

            // Load this scene file into a SKScene instance:
            if let encounterScene = SKScene(fileNamed: 
                encounterFileName) {
                // Loop through each placeholder, spawn the 
                // appropriate game object:
                for placeholder in encounterScene.children {
                    if let node = placeholder as? SKNode {
                        switch node.name! {
                        case "Bat":
                            let bat = Bat()
                            bat.spawn(encounter, position: 
                                node.position)
                        case "Bee":
                            let bee = Bee()
                            bee.spawn(encounter, position: 
                                node.position)
                        case "Blade":
                            let blade = Blade()
                            blade.spawn(encounter, position: 
                                node.position)
                        case "Ghost":
                            let ghost = Ghost()
                            ghost.spawn(encounter, position: 
                                node.position)
                        case "MadFly":
                            let madFly = MadFly()
                            madFly.spawn(encounter, position: 
                                node.position)
                        case "GoldCoin":
                            let coin = Coin()
                            coin.spawn(encounter, position: 
                                node.position)
                            coin.turnToGold()
                        case "BronzeCoin":
                            let coin = Coin()
                            coin.spawn(encounter, position: 
                                node.position)
                        default:
                            println("Name error: \(node.name)") 
                        }
                    }
                }
            }

            // Add the populated encounter node to the array:
            encounters.append(encounter)
        }
    }

    // We will call this addEncountersToWorld function from
    // the GameScene to append all of the encounter nodes to the
    // world node from our GameScene:
    func addEncountersToWorld(world:SKNode) {
        for index in 0 ... encounters.count - 1 {
            // Spawn the encounters behind the action, with
            // increasing height so they do not collide:
            encounters[index].position = CGPoint(x: -2000, y: 
                index * 1000)
            world.addChild(encounters[index])
        }
    }
}

太好了,你刚刚添加了在游戏世界中使用我们的场景文件数据的功能。接下来,按照以下步骤在GameScene类中连接EncounterManager类:

  1. GameScene类上添加EncounterManager类的新实例作为常量:

    let encounterManager = EncounterManager()
    
  2. didMoveToView函数的底部,调用addEncountersToWorld以将每个遭遇节点作为GameScene类世界节点的子节点添加:

    encounterManager.addEncountersToWorld(self.world)
    
  3. 由于EncounterManager类在屏幕外生成遭遇,我们将暂时将我们的第一个遭遇直接移动到起始玩家位置以测试我们的代码。在didMoveToView函数中添加此行:

    encounterManager.encounters[0].position = CGPoint(x: 300, y: 0)
    

运行项目。你将看到皮埃尔在新的蝙蝠遭遇中飞翔。你的游戏应该看起来像以下截图:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_06_06.jpg

恭喜你,你已经实现了在场景编辑器中使用占位符节点的核心功能。你可以移除在步骤 3 中添加的定位这个遭遇的行,即添加到遭遇数组中的行(加粗的新行)。接下来,我们将创建一个系统,在皮埃尔企鹅之前重新定位每个遭遇。

检查点 6-A

你可以从这个 URL 下载到这个点的我的项目:

www.thinkingswiftly.com/game-development-with-swift/chapter-6

生成无限遭遇

我们至少需要三个遭遇来无限循环并创建一个永无止境的世界;任何时候可以有任意两个在屏幕上,第三个在玩家前方。我们可以跟踪皮埃尔的进度并重新定位他前方的遭遇节点。

构建更多遭遇

在我们可以实现重新定位系统之前,我们需要构建至少两个更多的遭遇。如果你愿意,可以创建更多;系统将支持任意数量的遭遇。现在,向你的游戏中添加两个额外的场景文件:EncounterBees.sksEncounterCoins.sks。你可以完全用蜜蜂、幽灵、刀片、金币和蝙蝠填充这些遭遇——享受乐趣吧!

为了获得灵感,这里是我的蜜蜂遭遇经历:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_06_07.jpg

这里是我的金币遭遇:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_06_08.jpg

更新 EncounterManager 类

我们必须让 EncounterManager 类了解这些新的遭遇。打开 EncounterManager.swift 文件并将新的遭遇名称添加到 encounterNames 常量中:

// Store your encounter file names:
let encounterNames:[String] = [
    "EncounterBats",
    "EncounterBees",
    "EncounterCoins"
]

我们还需要跟踪在任意给定时间可能出现在屏幕上的遭遇。向 EncounterManager 类添加两个新属性:

var currentEncounterIndex:Int?
var previousEncounterIndex:Int?

在 SKSpriteNode 的 userData 属性中存储元数据

当皮埃尔在世界中移动时,我们将回收遭遇节点,因此我们需要在将其放置在玩家前方之前重置遭遇中的所有游戏对象的功能。否则,皮埃尔之前的遭遇之旅可能会将节点移位。

SKSpriteNode 类提供了一个名为 userData 的属性,我们可以用它来存储有关精灵的任何杂项数据。我们将使用 userData 属性来存储遭遇中每个精灵的初始位置,这样我们就可以在重新定位遭遇时重置精灵。向 EncounterManager 类添加这两个新函数:

// Store the initial positions of the children of a node:
func saveSpritePositions(node:SKNode) {
    for sprite in node.children {
        if let spriteNode = sprite as? SKSpriteNode {
            let initialPositionValue = NSValue(CGPoint: 
                sprite.position)
            spriteNode.userData = ["initialPosition": 
                initialPositionValue]
            // Save the positions for children of this node:
            saveSpritePositions(spriteNode)
        }
    }
}

// Reset all children nodes to their original position:
func resetSpritePositions(node:SKNode) {
    for sprite in node.children {
        if let spriteNode = sprite as? SKSpriteNode {
            // Remove any linear or angular velocity:
            spriteNode.physicsBody?.velocity = CGVector(dx: 0,
                dy: 0)
            spriteNode.physicsBody?.angularVelocity = 0
            // Reset the rotation of the sprite:
            spriteNode.zRotation = 0
            if let initialPositionVal = spriteNode.userData?.valueForKey("initialPosition") as? NSValue {
                // Reset the position of the sprite:
                spriteNode.position = 
                    initialPositionVal.CGPointValue()
            }

            // Reset positions on this node's children
            resetSpritePositions(spriteNode)
        }
    }
}

我们想在 init 时调用我们的新 saveSpritePositions 函数,当我们首次生成遭遇时。更新 EncounterManagerinit 函数,在将遭遇节点添加到遭遇数组中的行下面(加粗的新行):

// Add the populated encounter node to the encounter array:
encounters.append(encounter)
// Save initial sprite positions for this encounter:
saveSpritePositions(encounter)

最后,我们需要一个函数来重置遭遇并在玩家前方重新定位它们。向 EncounterManager 类添加这个新函数:

func placeNextEncounter(currentXPos:CGFloat) {
    // Count the encounters in a random ready type (Uint32):
    let encounterCount = UInt32(encounters.count)
    // The game requires at least 3 encounters to function
    // so exit this function if there are less than 3
    if encounterCount < 3 { return }

    // We need to pick an encounter that is not
    // currently displayed on the screen.
    var nextEncounterIndex:Int?
    var trulyNew:Bool?
    // The current encounter and the directly previous encounter
    // can potentially be on the screen at this time.
    // Pick until we get a new encounter
    while trulyNew == false || trulyNew == nil {
        // Pick a random encounter to set next:
        nextEncounterIndex = 
            Int(arc4random_uniform(encounterCount))
        // First, assert that this is a new encounter:
        trulyNew = true
        // Test if it is instead the current encounter:
        if let currentIndex = currentEncounterIndex {
            if (nextEncounterIndex == currentIndex) {
                trulyNew = false
            }
        }
        // Test if it is the directly previous encounter:
        if let previousIndex = previousEncounterIndex {
            if (nextEncounterIndex == previousIndex) {
                trulyNew = false
            }
        }
    }

    // Keep track of the current encounter:
    previousEncounterIndex = currentEncounterIndex
    currentEncounterIndex = nextEncounterIndex

    // Reset the new encounter and position it ahead of the player
    let encounter = encounters[currentEncounterIndex!]
    encounter.position = CGPoint(x: currentXPos + 1000, y: 0)
    resetSpritePositions(encounter)
}

在 GameScene 类中连接 EncounterManager

我们将在 GameScene 类中跟踪皮埃尔的进度,并在适当的时候调用 EncounterManager 类代码。按照以下步骤连接 EncounterManager 类:

  1. GameScene 类添加一个新属性,以跟踪何时在玩家前方定位下一次遭遇。我们将从 150 开始,以便立即生成第一个遭遇:

    var nextEncounterSpawnPosition = CGFloat(150)
    
  2. 接下来,我们只需在 didSimulatePhysics 函数中检查玩家是否移动到这个位置。在 didSimulatePhysics 的底部添加此代码:

    // Check to see if we should set a new encounter:
    if player.position.x > nextEncounterSpawnPosition {
        encounterManager.placeNextEncounter( nextEncounterSpawnPosition)
        nextEncounterSpawnPosition += 1400
    }
    

太棒了 - 我们已经添加了所有需要的功能,以在玩家前方无限循环遭遇。运行项目。你应该会看到你的遭遇无限循环在你面前。享受飞越你的辛勤工作!

在随机位置生成星力升级

我们还需要将星力升级添加到世界中。我们可以随机在每 10 次遭遇中生成一个星力,以增加一些额外的兴奋感。按照以下步骤添加星力逻辑:

  1. GameScene 类中添加 Star 类的新实例作为常量:

    let powerUpStar = Star()
    
  2. GameScene didMoveToView 函数的任何地方调用星力的 spawn 函数:

    // Spawn the star, out of the way for now
    powerUpStar.spawn(world, position: CGPoint(x: -2000, y: - 2000))
    
  3. GameScene didSimulatePhysics 函数中,按照以下方式更新你的新遭遇代码:

    // Check to see if we should set a new encounter:
    if player.position.x > nextEncounterSpawnPosition {
    encounterManager.placeNextEncounter(
        nextEncounterSpawnPosition)
        nextEncounterSpawnPosition += 1400
    
        // Each encounter has a 10% chance to spawn a star:
        let starRoll = Int(arc4random_uniform(10))
        if starRoll == 0 {
            if abs(player.position.x - powerUpStar.position.x) > 1200 {
                // Only move the star if it is off the screen.
                let randomYPos = CGFloat(arc4random_uniform(400))
                powerUpStar.position = CGPoint(x: 
                    nextEncounterSpawnPosition, y: randomYPos)
                powerUpStar.physicsBody?.angularVelocity = 0
                powerUpStar.physicsBody?.velocity = CGVector(dx: 0, dy: 0)
            }
        }
    }
    

再次运行游戏,你应该会看到星力偶尔在遭遇中生成,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-mobi-zh/raw/master/docs/gmdev-swift/img/Image_B04532_06_09.jpg

检查点 6-B

要下载到这一点的我的项目,请访问此 URL:

www.thinkingswiftly.com/game-development-with-swift/chapter-6

摘要

干得好 - 我们在本章中覆盖了大量的内容。你了解了 Xcode 的新场景编辑器,学会了如何使用场景编辑器来布局占位符节点,并解释了节点数据以在游戏世界中生成游戏对象。然后,你创建了一个系统来循环我们的无尽飞行游戏中的遭遇。

恭喜你;在本章中构建的遭遇系统是我们游戏中最复杂的系统。你现在正式处于一个很好的位置来完成你的第一个 SpriteKit 游戏!

接下来,我们将探讨在游戏对象碰撞时创建自定义事件。我们将在第七章 实现碰撞事件 中添加健康、伤害、金币拾取、无敌状态等功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值