iOS和tvOS 2D游戏开发教程

978-7-115-44296-3
作者: 【美】raywenderlich.com教程开发组
译者: 李军左嘉
编辑: 陈冀康

图书目录:

详情

本书就讲解了如何利用iOS和tvOS的游戏开发,全书分为6个部分,共计29章,以开发入门、物理和节点、游戏优化、GameplayKit、高级主题、附加章节等,其中涉及Swift 2、Sprite Kit 和 GameplayKit的相关知识,帮助读者从入门开始直到完全掌握开发技巧。

图书摘要

版权信息

书名:iOS和tvOS 2D游戏开发教程

ISBN:978-7-115-44296-3

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [美] raywenderlich.com教程开发组

  译    李 军  左 嘉

  责任编辑 陈冀康

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


Simplified Chinese translation copyright ©2017 by Posts and Telecommunications Press ALL RIGHTS RESERVED

2D iOS & tvOS Games by Tutorials

by the raywenderlich.com Tutorial Team, Mike Berg, Michael Briscoe, Ali Hafizji, Neil North, Toby Stephens, Rod Strougo, Marin Todorov and Ray Wenderlich.

Copyright © 2016 by Razeware LLC.

本书中文简体版由Razeware公司授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。

版权所有,侵权必究。


Sprite Kit是Apple内建的框架,专门用于开发iOS的2D游戏。tvOS是Apple TV所使用的操作系统平台,可以将App和游戏等呈现到大屏幕的TV上。本书详细介绍了如何使用Apple内建的2D游戏框架Sprite Kit和Swift语言来开发iOS和tVOS游戏。

全书分为6个部分共29章。每个部分针对一类技术主题,并且通过一款生动的游戏的开发贯穿其中。当学习完每个部分的时候,读者通过一款游戏的关卡或功能的逐步完成和完善,不知不觉掌握了作者所要介绍的技术主题。

第一部分包括第1~7章,涵盖了用Sprite Kit进行2D游戏开发的基础知识,分别介绍了角色、手动移动、动作、场景、相机、标签等主题,并且初步认识了tvOS。这部分将开发一款叫做Zombie Conga的僵尸游戏,并将其迁移到tvOS上。第二部分包括第8~13章,主要介绍场景编辑器、游戏开发的物理知识,裁剪、视频和形状节点以及中级tvOS知识。这部分将开发一款叫做Cat Nap的谜题游戏,并将其迁移到tvOS上。第三部分关注给游戏添加特效,包括第14~17章,将开发一款叫做Drop Charge的游戏,并通过状态机、粒子系统、声影效果、动画等众多技术来点亮这款游戏。第四部分包括第18~20章,主要关注iOS 9引入的GameplayKit技术。这部分会开发一款Dino Defense塔防攻击游戏,并通过实体—组件系统、寻路算法和代理、目标和行为等技术,实现游戏中的恐龙的寻路和移动行为。第五部分包括第21~24章,涉及贴图地图、程序式关卡生成、GameplayKit随机性和游戏控制器等高级话题。这部分将开发一款叫做Delve的地牢探险游戏,并应用各章所介绍的技术。第六部分包括第24~29章,涉及和游戏相关的其他技术,包括向游戏中添加Game Center排行榜和成就、使用ReplayKit录制游戏视频并进行分享、把iAd加入到游戏中,以及程序员如何实现游戏美工。这部分将打造一款叫做Circuit Racer的赛车游戏中,并加入各章所介绍的技术。

本书内容详尽、生动有趣,通过丰富、完整的游戏案例,帮助读者学习和掌握最新的游戏开发技术。本书适合对iOS和tvOS上的游戏开发感兴趣的初学者阅读参考。


在本书中,我们将学习如何使用Apple内建的2D游戏框架Sprite Kit和Swift语言来开发iOS和tVOS游戏。那么,问题来了:

如果你是初学者,开发2D游戏绝对是最佳的入门方式。

如果你是高级开发者,开发2D游戏还是会比开发3D游戏快很多。既然并不一定能够通过3D游戏赚到更多的钱,为什么不选择更容易胜出的方式呢?此外,一些人(例如我)更喜欢2D游戏。

那么,剩下的事情就很自然了,有了iOS、tvOS、2D游戏和Sprite Kit,你就做出了最佳的选择。

两年前,我编写了一本名为《iOS Games by Tutorials》的图书,介绍了如何使用Sprite Kit开发2D游戏。一年后,我发布了这本书完全升级到Swift的第2版,并为已有的电子书读者免费更新。

在今年的WWDC上,Apple发布了一组叫做GameplayKit的、新的API。这一组API使得我们很容易将寻路算法、AI和其他很酷的功能添加到游戏中。

然后,还有“房间里的大象”——tvOS,现在,它使得我们能够开发在起居室里玩的游戏。

这些更新都很重要,以至于无法仅仅对《iOS Games by Tutorials》进行更新来完全涵盖这些内容,因此,我决定完全重新写一本书。这就是你手里的这本书。

如果你曾经阅读过《iOS Games by Tutorials》的话,你可能会问,本书中有什么新的内容呢?这里按照本书中包含的游戏来简单介绍一下。

正如你所看到的,这些就是主要的更新。如果你之前阅读过这本书,并且时间有限,最好的办法是关注那些新的游戏,或者你最感兴趣的章节。

这本书有点与众不同。在raywenderlich.com上,我们的目标是让本书成为你所阅读过的关于游戏编程的最好的图书。

已经有很多关于游戏编程的图书了,并且其中的很多图书都是相当不错的,因此,这是一个崇高的目标。为了实现这一目标,我们做了如下的一些设计。

当你阅读完本书后,如果你认为我们已经成功地满足了这些目标,请让我们知道。你可以随时给我们发E-mail:ray@raywenderlich.com。

我们希望你喜欢这本书,并且我们已经迫不及待地想要看看你会开发出什么样的游戏了。

正如你将会看到的,使用Sprite Kit开发iOS游戏是很容易的,但是,并非总是这样的。在iOS早期的日子里,你开发游戏的唯一的选择就是OpenGL ES,它(和Metal)是该平台上可用的最底层的图形API。OpenGL ES特别难以学习,并且它是很多游戏开发初学者的一个巨大障碍。

此后不久,第三方的开发者基于OpenGL发布了一些游戏开发框架,其中最流行的是Cocos2D,实际上,我们还编写过一本关于Cocos2D的图书。App Store排行榜上的很多游戏都是使用Cocos2D开发的,并且很多开发者表示Cocos2D是他们进入游戏开发领域的起点。

Cocos2D是一个很好的框架,但是,它并不是为Apple而编写的,也没有得到Apple的有力支持。为此,当iOS的新版本发布的时候,或者当系统中加入其他的Apple API的时候,常常会出现问题。

为了解决这个问题,Apple发布iOS 7的时候带有一个新的、用于制作2D游戏的框架,这就是Sprite Kit。其API和Cocos2D非常相似,精灵、动作和场景的类型都和开发者所熟知和喜爱的Cocos2D类似,因此,较旧的框架的粉丝肯定可以快速上手。Sprite Kit还拥有一些额外的功能,例如,支持播放视频、制作形状和应用特殊图像效果。

Sprite Kit API设计精良并且易于使用,特别适合于初学者。更好的是,你可以放心地使用,因为它得到了Apple的完全的支持,并且针对iOS上的2D游戏进行了很大程度的优化。

从现在开始,如果你想要开发在iOS、tvOS或MacOS X上运行的2D游戏的话,我们明确地推荐你使用Sprite Kit而不是其他的游戏开发框架。只有一个例外情况,那就是如果你想要开发跨平台的游戏(例如支持Android和Windows等)。Sprite Kit只是Apple的API,因此,要将你的游戏从Sprite Kit迁移到其他的平台上,你所面临的挑战可比使用Unity这样的其他框架大得多。

如果你想要让事情简单化,并且只支持Apple平台,Sprite Kit就是合适的选择。那么,让我们带你快速学习和掌握Sprite Kit吧。

要学习本书中的教程,你需要具备以下条件:

对于本书中的大多数章节,可以在Xcode自带的iOS 9模拟器上运行代码。然而,本书后面有一些章,需要在设备上进行测试。还要注意,Sprite Kit在设备上比在模拟器上表现得更好,因此,当在模拟器中运行游戏的时候,帧速率似乎比预期得要慢。

如果你还没有安装最新版本的Xcode,确保先安装它,然后再继续阅读本书。

本书针对从初级到高级iOS开发者。如果你是本书的目标读者,那么,你将会从本书中学到很多知识。

本书确实需要一些Swift的基础知识。如果你不了解Swift,也可以继续阅读本书,因为所有的介绍都是按部就班的。然而,很可能由于你的知识缺乏而导致对一些部分无法理解。在开始阅读本书之前,最好是先上我们的Swift初学者系列课程,www.raywenderlich.com/store,这个课程介绍了Swift开发的基础知识。

阅读本书有两种方式,这取决于你是iOS游戏开发的完全初学者,还是具备其他2D游戏框架知识的高级开发者。

如果你是一位完全初学者,阅读本书的最好的方式是从头到尾连续阅读。我们安排章节的时候,是按照最符合逻辑的方式进行的,从介绍性的内容开始,到一次一个层级地构建开发技能。

如果你是一位高级开发者,已经具备了2D游戏框架的知识,你可能会很容易就掌握了Sprite Kit,因为你会很熟悉核心的概念和语法。建议你跳过前面的章节,并专注于后面的、较为高级的章节,或者是你特别感兴趣的章节。

本书分为5个部分,从最基础的主题逐渐进入高级的主题。在每个部分中,我们都将从头创建一个完整的小游戏。本书最后还包括一些额外的章节,我们相信你会喜欢这些内容。让我们来依次看看各个部分的内容:

这一部分介绍了使用Sprite Kit开发2D游戏的基础知识。这些都是最重要的技术,几乎会在你所开发的每一款游戏中用到。学习完这个部分,你就为自己开发简单游戏做好了准备。

在整个这个部分中,我们将创建一款叫做Zombie Conga的、真正的游戏,其中,你将扮演一个无忧无虑的僵尸,它只是想举办舞会,如图1所示。

这个部分一共包括7章,我们将在这部分分步骤地构建这款游戏。

图1

第1章 精灵。首先,我们将第1个精灵添加到游戏中,这就是背景和僵尸。

第2章 手动移动。我们将让僵尸跟随你的触摸在屏幕上移动,并且完成2D向量数学基础知识的课程。

第3章 动作。我们将给游戏添加小猫和疯狂的猫女士,以及一些基本的碰撞检测和游戏设置。

第4章 场景。我们将给游戏添加一个主菜单,以及获胜和失败场景。

第5章 相机。我们将让游戏从左向右滚动,并且最后,还要添加其自己的康茄舞队。

第6章 标签。我们将添加标签,以显示僵尸的生命值以及他的康茄舞队中的小猫的数目。

第7章 初识tvOS。我们将让Zombie Conga在tvOS上运行,只需要一些简单的步骤就可以做到这一点。

在本部分中,我们将学习如何使用Sprite Kit所包含的、内建的物理引擎,来创建类似《愤怒的小鸟》和《割绳子》游戏中的逼真的移动。我们还将学习如何使用特殊类型的节点,以允许在游戏中播放视频或创建形状。

在此过程中,我们将创建一个叫做Cat Nap的物理谜题游戏,其中你扮演一只小猫,它在经过了漫长的一天后只想要上床睡觉,如图2所示。

这个部分一共包括5章,我们将在这部分中分步骤地构建这款游戏。

图2

第8章 场景编辑器。我们首先创建了游戏的第一个关卡。最终,我们更好地了解了Xcode的关卡设计器,也更好地了解了场景编辑器。

第9章 物理基础。在本章中,我们打算开一个小差,学习一些为游戏创建物理模拟效果的基础知识。我们还将学习如何在Xcode playground中制作游戏原型。

第10章 中级物理。我们将学习基于物理的碰撞检测,并且为Sprite Kit节点创建定制类。

第11章 高级物理。我们将给这款游戏再添加两个关卡,并在此过程中学习交互性实体、实体之间的接合以及组合实体等。

第12章 裁剪、视频和形状节点。我们将给Cat Nap添加一种新的木头块,在此过程中,将学习一些其他类型的节点,这些节点允许我们做一些惊人的事情,如播放视频、裁剪图像以及创建动态形状。

第13章 中级tvOS。在这部分的最后这一章中,我们将把Cat Nap迁移到tvOS上。我们打算从这款完全开发好的游戏开始,并且为其添加tvOS的支持,以便玩家能够坐在沙发上、使用遥控器轻松地玩这款游戏。

在这部分中,我们将学习如何给一款较好的游戏添加大量的特殊效果和体验(即所谓的果汁),将其变成一款伟大的游戏。

在此过程中,我们将创建一款叫做Drop Charge的游戏。游戏中,你是一位太空英雄,肩负着炸毁外星人的太空飞船的任务,并且要在飞船爆炸之前逃生。为了做到这一点,你必须从一个平台跳到另一个平台,一路上收集特殊的激励。但是要小心,不要跌入滚烫的岩浆之中,如图3所示。

这个部分一共包括4章,我们将在这部分中分步骤地构建这款游戏。

图3

第14章 开发Drop Charge。我们将使用场景编辑器和代码将基本的游戏逻辑组合起来,发挥出在前面各章中所学的Sprite Kit技术的强大威力。

第15章 状态机。我们将学习什么是状态机以及如何使用它们。

第16章 粒子系统。我们将学习如何使用例子系统来创建惊人的特殊效果。

第17章 点亮游戏。设法给游戏添加音乐、声音和动画,更多的粒子系统和其他特殊效果,让你体验到掌握这些技术细节的好处。

在这个部分中,我们将学习如何使用iOS 9的、新的GameplayKit来改进游戏的架构和可重用性,并且还要添加寻路算法和基本的游戏AI。

在此过程中,我们将创建一款叫做Dino Defense的、有趣的塔防攻击游戏。游戏中,你要构建起完美的防御,以保护你的村庄免遭愤怒的恐龙的攻击,如图4所示。

这个部分一共包括3章,我们将在其中分步骤地构建这款游戏。

图4

第18章 实体—组件系统。我们将学习使用GameplayKit所提供的、新的GKEntity和GKComponent对象来建模游戏对象所需的所有知识,并且,我们还将使用所学的知识来实现第一个恐龙和塔防。

第19章 寻路算法。我们将使用GameplayKit的寻路功能让恐龙在场景中移动,并且避开障碍物和塔防。

第20章 代理、目标和行为。 最后,我们将给游戏添加另一只恐龙,它将使用GKAgent、GKGoal和GKBehavior对象,以一种更有组织的替代寻路方式在场景中移动。

在本部分中,我们将深入到一些较为高级的话题,例如程序式关卡生成、GameplayKit随机性和游戏控制器等。

在此过程中,我们将构建一款叫做Delve的、基于贴图的地牢探险游戏,其中,你将尝试引导矿工通过一个岩石构成的、充满地下生物的地牢,如图5所示。

这个部分一共包括4章,我们将在这部分中分步骤地构建这款游戏。

图5

第21章 贴图地图游戏。我们将学习构建贴图地图关卡的技术,包括如何创建一个功能完整的贴图地图游戏。

第22章 随机性。利用新的GameplayKit类GKRandom,来生成游戏世界。

第23章 程序式关卡。去除掉关卡生成的一些随机性,使得这个过程更具可预测性,但是仍然保持未知的惊险性。

第24章 游戏控制器。这个游戏特别适合使用一个外部的游戏控制器,我们将添加一个tvOS目标,并且探讨如何使用Apple TV遥控器作为游戏控制器。

本书并不是到此为止,在此基础上,我们还提供了一些额外的章节。

在这些章节中,你将学习Sprite Kit之外的一些API,当你开发iOS游戏的时候,了解这些API是很有帮助的。特别是,你将要学习如何向游戏中添加Game Center排行榜和成就,使用新的iOS 9 ReplayKit API,以及给游戏添加iAds。

在此过程中,我们将把这些API加入到一款叫做Circuit Racer的、自上向下滚动的(top-down)赛车游戏中,你在其中扮演一个优秀的赛车手,驾驶汽车去挑战世界纪录,如图6所示。只要不撞到赛道上的围栏,就不会有问题。

图6

第25章 Game Center成就。在游戏中打开Game Center,并且当用户完成某些任务的时候给他颁发成就。

第26章 Game Center排行榜。为游戏设置各种排行榜和数据,并且报告玩家的得分。

第27章 ReplayKit。你将学习如何允许玩家使用ReplayKit录制游戏视频并进行分享。

第28章 iAd。我们将学习如何把iAd加入到游戏中,以便能够有一个不错的收入来源。

第29章 写给程序员的2D美工知识。如果你喜欢这些小游戏中的美工图片,并且想要了解如何雇佣一位美术师,或者想要自己制作一些美工,那么,应该阅读本章。本章指导你使用Illustrator绘制一只可爱的小猫。

本书的每一章都带有完整的源代码。其中的一些章节,还有一个初始工程或者其他所需的资源,在阅读本书的时候,你肯定会需要这些内容。

我们为本书建立了一个论坛raywenderlich.com/forums。这是提出和本书相关的问题或者与Sprite Kit开发游戏相关的问题的好地方,也可以在这里提交你所发现的勘误。

感谢很多人为本书的编写过程提供帮助。


献给我的妻子和家人,他们使得我能够做想做的事情。

—— Mike Berg

献给家庭生活中真正重要的那些人。给我的妈妈Barbara、姐姐Kimberly、朋友Kelli、女儿Meghan和Brynne,还有我的6个孙子孙女儿。感谢你们的爱和支持。还要献给我已经去世的父亲,我想念您!

—— Michael Briscoe

献给我美丽的妻子Batul,还有我的父母。感谢你们对我的支持和信任。

—— Kauserali Hafizji

献给我的家人和爱人,你们总是支持我的雄心壮志。

—— Neil North

献给Caroline、Nina和Lucy,感谢你们给我不断的鼓励和支持。

—— Toby Stephens

献给总是支持和理解我的妻子Agata,还有我的家人。我非常爱你们。

—— Rod Strougo

献给我的父母,你们曾经如此的支持我和爱我。献给Mirjam。

—— Marin Torodov

献给raywenderlich.com的编辑、作者和译者。团队工作使得你们的梦想更大。

—— Ray Wenderlich


Mike Berg是一位全职的游戏美术师,他很高兴和来自全世界的独立游戏开发者一起工作。当他没有处理像素色彩的时候,他喜欢美食、花时间和家人在一起,以及玩游戏并感受快乐。可以通过www.weheartgames.com找到他的作品。

Michael Briscoe是一位有着30多年经验的独立的软件开发者。他所选择的平台包括Apple的所有产品,从Macintosh到iPhone、iPad和Apple TV。他的专长是开发娱乐性软件,例如游戏和模拟器。可以访问他的Web站点skyrocketsoftware. wordpress.com。

Kauserali Hafizji是一位开发者。他是一位热衷编写代码的程序员,甚至在周末的时候,他也在编程。一本好书,泡个澡,再吃上一顿大餐,对他来说就可以过一个很好的周末了。你可以通过@Ali_hafizji在Twitter上找到他。

Neil North是一位资源管理员、软件开发者和业务自动化专家,他很喜欢开发独立的游戏、从事音频工程,以及帮助其他人实现有创意的目标。他还教授iOS游戏和App开发,并且在Udemy和CartoonSmart上都开设了课程。Neil居住在澳大利亚,你可以访问他的网站apptlycreative.com。

Toby Stephens有20多年的软件开发经验,并且目前是伦敦的inplaymaker移动开发总监。Toby热衷于游戏编程。他还会编写乐曲,并且喜欢烘焙面包。可以通过@TJShae在Twitter上联系他,也可以访问他的网站: tjshae.com。

Rod Strougo从Apple II开始了自己的物理和游戏之旅,开始用Basic编写游戏。Rod的职业生涯有过转型,他花了15年的时间为IBM和AT&T编写软件。在那些日子里,他始终保持着在游戏开发和教学方面的热忱,并且在Big Nerd Ranch等处提供iOS培训。他最初来自巴西的Rio de Janeiro,现在和妻子和儿子一起居住在亚特兰大。

Marin Todorov是一位独立的iOS开发者和作者。他20多年前开始在Apple II上进行开发,并且一直持续到今天。除了编写代码,Marin还喜欢写博客、写书、教学和演讲。他有时候会花时间为开源项目编写代码。他居住在Santiago。可以访问他的Web站点www.underplot.com

Ray Wenderlich是一位iPhone应用开发者,也是Razeware LLC的创始人。Ray对于开发App和教授其他人开发App都充满了热情。他和他的教程团队已经编写了很多iOS开发教程,参见www.raywenderlich.com 。


你可能没有注意到本书的所有作者都男的。这很遗憾,但不是故意为之。如果你是一位从事iOS开发的女性,并且有兴趣加入我们的教程团队来编写游戏主题的教程的话,我们求之不得。

Tammy Coron是这本图书的技术编辑。她是一位作家、音乐家、美术家和软件工程师。作为一名独立的创意专业人士,Tammy将自己的时间花在开发软件、写作、绘图和提醒其他人“不可能的事情只不过颇费些时日”。她还是Roundabout: Creative Chaos播客的主持人。

Bradley C. Phillips是本书的编辑。他是第一位加入raywenderlich.com的编辑,并且他担任过期刊编辑,之前负责管理纽约的一家调查公司的智能部门。现在,Bradley是一名自由职业者并且从事自己的项目。如果你的博客、图书或者其他产品需要一位专业的、有经验的编辑的话,请联系他。


Ray Wenderlich是本书的最后把关编辑。他是一位iPhone应用开发者,也是Razeware LLC的创始人。Ray对于开发App和教授其他人开发App都充满了热情。他和他的教程团队已经编写了很多iOS开发教程,参见www.raywenderlich.com。


Mike Berg创作了本书中的大多数游戏所需的美工。Mike Berg是一位全职的游戏美术师,他很高兴和来自全世界的独立游戏开发者一起工作。当他没有处理像素色彩的时候,他喜欢美食、花时间和家人在一起,以及玩游戏并感受快乐。可以通过www.weheartgames.com 找到他的作品。

Vinnie Prabhu创建了本书中涉及的游戏的所有音乐和声音。Vinnie是北维吉尼亚州的一位音乐作曲家,也是软件工程师,他从事音乐会、戏剧和视频游戏的音乐工作。他也是音乐和视频游戏粉丝的社群OverClocked ReMix的一位成员。你可以通过@palpablevt在Twitter上联系他。

Vicki Wenderlich创作了本书中的大多数插图,以及游戏Drop Charge的美工。Vicki数年前爱上了数字艺术图像,并且从那时候起开始从事App美工和数字图像工作。她很喜欢帮助人们实现梦想,并且为开发者制作App美工。可以访问她的网站:gameartguppy.com。


这个部分将介绍使用Sprite Kit制作2D游戏的基础知识。这是一些最重要的技术,几乎在你所制作的每一款游戏中都会用到。当你学习完这个部分,就能够自己制作简单的游戏了。

在整个这一部分中,我们将创建一款名为Zombie Conga的游戏,你负责在游戏中扮演无忧无虑的僵尸,而僵尸只是想参加舞会。

第1章 精灵

第2章 手动移动

第3章 动作

第4章 场景

第5章 相机

第6章 标签

第7章 初识tvOS


Ray Wenderlich撰写

既然你已经知道了什么是Sprite Kit以及为什么要使用它,现在我们来自己尝试一下。我们将要构建的第一款小游戏叫做Zombie Conga,其完成后的样子如图1-1所示。

图1-1 

在Zombie Conga中,你负责扮演无忧无虑的、只是想参加舞会的僵尸。好在,僵尸所占据的海边小镇有足够多的小猫。你只需要咬住这些小猫,它们就会加入到僵尸的舞队中来。

不过要小心疯狂的猫女士!这些身穿红色的衣服的老太太,对于想要偷走她们心爱的小猫的任何人都会毫不客气,并且会拼尽全力让僵尸平静下来——让它们永久地平静下来。

我们将在接下来的7章中构建这款游戏。

第1章 精灵。你已经开始阅读本章了。我们将开始给游戏添加精灵,主要是一个背景以及僵尸。

第2章 手动移动。我们将让僵尸跟随对屏幕的触摸而移动,并且我们会在本章快速学习基本的2D向量数学。

第3章 动作。把小猫和疯狂的猫女士添加到游戏中,并且添加了基本的碰撞检测和游戏设置。

第4章 场景。我们将给游戏添加一个主菜单,还添加了获胜或失败的画面。

第5章 相机。我们将让游戏从左向右滚动,并且最终添加其自己的康茄舞队。

第6章 标签。我们将添加一个标签来显示僵尸的生命值,以及它所吃到的小猫的数目。

第7章 初识tvOS。我们将让Zombie Conga游戏在tvOS上运行,而这只需要几个简单的步骤就可以做到。

让我们开始开发这个游戏吧!

启动Xcode并且从主菜单中选择File\New\Project...。选择iOS\Application\Game模板,并且点击Next按钮,如图1-2所示。

图1-2 

在Product Name字段中输入ZombieConga,在Language栏选择Swift,在Game Technology栏选择SpriteKit,在Devices栏选择Universal,然后点击Next按钮,如图1-3所示。

图1-3 

选择想要将工程保存在硬盘上的什么位置,并且点击Create按钮。此时,Xcode将会生成一个简单的Sprite Kit初始工程。

看一下所生成的Sprite Kit工程。在Xcode的工具栏上,选择iPhone 6并点击Play按钮,如图1-4所示。

在一个简短的开始屏幕之后,可以看到一个标签显示“Hello, World!”。当在该屏幕上点击的时候,会出现一个旋转的飞船,如图1-5所示。

在Sprite Kit中,有一个叫做场景的对象,它可以控制你的App的每一个“界面”。场景是Sprite Kit的SKScene类的一个子类。

图1-4

图1-5 

现在,这个App只有一个单个的场景,就是GameScene。打开GameScene.swift,你将会看到显示这个标签和旋转的飞船的代码。理解这些代码并不是很重要,我们打算完全删除它,并且一步一步地来构建自己的游戏。

现在,删除GameScene.swift中的所有内容,并且用如下的代码替代。

import SpriteKit

class GameScene: SKScene { 
   override func didMoveToView(view: SKView) {
      backgroundColor = SKColor.blackColor()
   }
}

didMoveToView()是Sprite Kit在向你展示一个场景之前所调用的方法,这个方法是对场景的内容进行一些初始设置的好地方。在这里,我们直接将背景颜色设置为黑色。

Zombie Conga设计为以横向模式运行,因此,要为App进行这一设置。从工程导航器中选择ZombieConga工程,然后选择ZombieConga Target。点击General标签页,并且确保只有Landscape Left和Landscape Right选项选中,如图1-6所示。

图1-6 

还需要进行一些修改。打开Info.plist并找到Supported interface orientations (iPad)条目。删除在这里所看到的Portrait (bottom home button)和Portrait (top home button)条目,而只保留横向模式的相关选项,如图1-7所示。

图1-7 

Sprite Kit模板自动创建了一个名为GameScene.sks的文件。可以使用Xcode内建的场景编辑器来编辑这个文件,以可视化地布局游戏场景。请将这个场景编辑器当做是Sprite Kit的一个简单的Interface Builder。

我们将在本书第7章中介绍场景编辑器,但是我们不会在Zombie Conga游戏中用到它,因为对于这款游戏来说,通过编程来创建精灵要更为容易和直接。

因此,按下Control键并点击GameScene.sks,选择Delete,然后选择Move to Trash。由于不再使用这个文件了,还必须相应地修改模板代码。

打开GameViewController.swift,并且用如下的内容替换它。

import UIKit
import SpriteKit

class GameViewController: UIViewController {
   override func viewDidLoad() {
      super.viewDidLoad()
      let scene =
         GameScene(size:CGSize(width: 2048, height: 1536))
      let skView = self.view as! SKView
      skView.showsFPS = true
      skView.showsNodeCount = true
      skView.ignoresSiblingOrder = true
      scene.scaleMode = .AspectFill
      skView.presentScene(scene)
   }
   override func prefersStatusBarHidden() -> Bool {
      return true
   }
}

之前,视图控制器从GameScene.sks加载场景,但是现在,它通过在GameScene上调用一个初始化程序来创建场景。

注意,当创建该场景的时候,通过直接编码为2048×1536的大小,并且将缩放模式设置为AspectFill。现在,很适合介绍一下如何将这款游戏设计为一个通用App。

 注意

本小节是可选的内容,适合那些特别感兴趣的读者。如果你只想要尽可能快地编写代码,可以跳到下一个小节阅读。

我们在本书中的所有游戏都是作为通用的App而设计的,这意味着它们在iPhone和iPad上都能运行。

本书中的这款游戏的场景已经设置为2048×1536,或者说是横屏模式(和竖屏模式相反),其缩放模式设置为AspectFill。AspectFill让Sprite Kit缩放场景的内容以填充整个屏幕,即便Sprite Kit在这么做的时候可能需要裁切一部分内容。

这会导致场景似乎显示于iPad Retina上,而iPad Retina的分辨率是2048×1536,但是,在iPhone上会进行缩放/裁剪以适应手机较小的屏幕以及不同的高宽比。

如下的几个例子,展示了本书中的游戏在不同的设备上的横屏模式显示的样子,包括从最小高宽比到最大的高宽比,如图1-8所示。

图1-8 

由于AspectFill会针对iPhone从顶部和底部裁剪场景,我们把本书中的游戏设置为拥有一个主要的“游戏区域”,从而保证其在所有的设备上都可见。基本上,游戏将会在横屏模式的顶部/底部或者竖屏模式的左边/右边拥有192个像素的边距,你应该避免将基本内容放置到这个边距之中。在本书稍后,我们将向你可视化地展示这一点。

注意,你只需要为此指定一组美工图片就可以工作,图片适合于最大的屏幕尺寸2048×1536就可以了。在iPad Retina之外的其他设备上,这些图片将会缩小。

 注意

这种方法的缺点是,图片会比某些设备实际所需的大小还要大,例如对于iPhone 4s这样的设备,这会造成材质内存和空间的浪费。这种方法的优点是,游戏在所有设备上都显示的更好且更容易显示,并且能很好地工作。

这种方法的一个替代方案是,针对每种设备和缩放比例来添加不同的图像(例如,iPad@1x、iPad @2x、iPhone@2x、iPhone @3x),这要借助Apple的强大的资源目录来实现。然而,在编写本书的时候,Sprite Kit并不能在所有情况下根据各种设备和缩放从资源目录加载正确的图像,因此,我们现在仍然使用简单的做法。

接下来,我们需要给工程添加游戏图像。

在Xcode中,打开Assets.xcassets,选择Spaceship条目并且按下删除键以删除它,遗憾的是,我们这个游戏并不是关于太空僵尸的游戏!现在,只有AppIcon还保留着,如图1-9所示。

图1-9 

选中AppIcon之后,从starter\resources\icons中将合适的图标拖放到每一组之中,如图1-10所示。

然后,将starter\resources\images中的所有文件拖放到左边的边栏中,如图1-11所示。

通过将图像包含到资源目录中,Xcode将会在后台建立材质图册以包含这些图像,并且在游戏中使用它们,这会自动地提高性能。

图1-10 

图1-11 

 注意

本小节是可选内容,因为它对游戏的运行不会有任何的影响,这只是为游戏“锦上添花”。如果你想要直接进行编码,可以跳到下一个小节阅读。

让游戏迈上正轨之前,还有最后一件事情要做,就是配置启动界面。

启动界面是当你的App初次加载的时候,iOS所显示的内容,它通常会显示几秒钟的时间。启动界面使得玩家一开始就能够快速对你的App有印象,当然,至少它不是黑色的屏幕。对于Zombie Conga来说,我们将显示带有游戏名称的一个启动界面。

你的App实际上已经有了一个启动界面。你之前启动App的时候,可能已经注意到了有一个简短的、空白的白色界面,那就是启动界面。

在iOS中,App有一个特殊的启动界面文件,它基本上就是一个故事板,在这个工程中就是LaunchScreen. storyboard文件;可以配置它从而在App加载的时候在屏幕上显示一些内容。和只是显示一幅图像的老办法相比,这种方法的优点是,你可以使用Auto Layout来更精细地控制这个界面在不同的设备上的样子。

让我们来尝试一下。打开LaunchScreen.storyboard。将会看到如图1-12所示的内容。

图1-12 

在右边栏的Object Library中,拖动一个图像视图到视图之中,并且重新调整其大小以填充整个区域,如图1-13所示。

图1-13 

接下来,需要设置这个图像视图,以便它总是和其包含视图具有相同的宽度和高度。要做到这一点,确保选中了图像视图,然后点击右下方的Pin按钮(它看上去就像是一架战斗机)。在Add New Constraints窗口中,点击4条红色的线条,以便图像视图锁定到每一个边上。确保Constrain to margins没有选中,并且所有的值都设置为0,然后点击Add 4 Constraints按钮,如图1-14所示。

图1-14 

保持图像视图仍然是选中的,确保选择了Attributes检视器,这是右边的第4个标签页。将Image设置为MainMenu,并且将View Mode设置为Aspect Fill,如图1-15所示。

图1-15 

再次编译并运行App。这一次,你会看到简短的Zombie Conga启动界面,如图1-16所示。

很快,接下来就是一个空白的、黑色界面,如图1-17所示。

图1-16

图1-17 

这看上去可能还不太像样子,但是现在,有了一个起点,我们可以在此基础上构建第一个Sprite Kit游戏。

让我们继续下一个任务,这也可能是在制作游戏的时候最重要和最常见的任务之一,即在屏幕上显示图像。

在制作2D游戏的时候,通常要将表示游戏的各种要素的图像放置到屏幕上,如英雄、敌人、子弹等,如图1-18所示。这些图像中的每一个,都叫做精灵(sprite)。

图1-18

Sprite Kit有一个叫做SKSpriteNode的特殊的类,它使得创建和使用精灵更为容易。我们就是使用这个类来为游戏添加所有的精灵的。让我们来尝试一下。

打开GameScene.swift,给didMoveToView()添加如下的一行,就放在设置了背景颜色之后:

let background = SKSpriteNode(imageNamed: "background1")

不需要传入该图像的扩展名,Sprite Kit将自动为你确定它。

编译并运行,现在先忽略警告。好了,你认为这很简单吧,但是现在,你还是会看到一个空白的界面,怎么会这样呢?

这确实很简单。因为在你把精灵作为场景的一个子节点或者场景的一个后代节点添加之前,精灵是不会显示在屏幕上的。

要做到这一点,在上面的那一行代码之后,添加如下这行代码:

addChild(background)

我们稍后将学习节点和场景。现在,再次编译并运行,你将会看到背景的一部分出现在屏幕的左下方,如图1-19所示。

图1-19 

显然,这还不是我们想要的样子。要让背景处于正确的位置,必须要设置其位置。

默认情况下,Sprite Kit将精灵放置在(0, 0),这在Sprite Kit中表示屏幕左下方的位置。注意,iOS中的坐标系统和UIKit的坐标系统不同,在iOS中,(0, 0)表示左上方。

尝试设置position属性,从而将背景放置到其他的某个位置。添加如下的一行代码,放在调用addChild(background)之前:

background.position = CGPoint(x: size.width/2, y: size.height/2)

这里,我们将背景设置到了屏幕的中央。即便是这样一行简单的代码,也有重要的4点需要了解:

1.position属性的类型是CGPoint,这是一个简单的结构体,包含了x和y部分。

2.可以很容易地使用如下所示的初始化程序来创建一个CGPoint。

struct CGPoint { 
   var x: CGFloat
   var y: CGFloat
}

3.既然在一个SKScene子类中编写这段代码,任何时候,都可以使用size属性来访问场景的大小。size属性的类型是CGSize,这是和CGPoint一样的一个简单结构体,包含了width和height部分。

struct CGSize {
   var width: CGFloat
   var height: CGFloat
}

4.一个精灵的位置在其父节点的坐标空间之中,在这个例子中,其父节点就是场景本身。我们将在第5章中更详细地介绍这一点。

编译并运行,现在,背景完全可见了,如图1-20所示。

 注意

你可能注意到了,在iPhone设备上,是无法看到整个背景的,其顶部和底部的一部分重叠了。这是因为这款游戏设计为在iPad和iPhone上都可以工作,正如本章前面的1.1.1小节“通用App支持”所介绍的那样。

图1-20 

设置背景精灵的位置,意味着把精灵的中心点设置为该位置。这就说明了为什么在此之前我们只能够看到背景的上半部分。在我们设置精灵的位置之前,其默认位置在(0, 0),这会将精灵的中心放置在屏幕的左下角,因此,我们只能够看到精灵的上半部分。

可以通过设置精灵的锚点来改变这一行为。把锚点当做是“精灵中的一个点,通过这个点将精灵固定在一个特定的位置”。图1-21展示了放置在屏幕中心的精灵,但是它们使用了不同的锚点。

图1-21 

要看看这是如何工作的,找到将背景位置设置为屏幕中心的那一行代码,并且用如下的代码替换它。

background.anchorPoint = CGPoint.zero
background.position = CGPoint.zero

CGPoint.zero是的(0, 0)一种方便的简写。这里,我们将精灵的锚点设置为(0, 0),以便将其精灵的左下角固定到所设置的位置,也就是(0, 0)。

编译并运行,现在图像仍然在正确的位置,如图1-22所示。

图1-22 

这之所以有效,是因为我们将背景图像的左下角固定到了场景的左下角。

这里,为了学习的目的,我们修改了背景的锚点。然而,通常可以将锚点保留为其默认值(0.5, 0.5),除非你有特定的需求,要让精灵围绕一个特定的点旋转,我们将在下一小节给出这样的一个例子。

因此,简而言之,当设置精灵的位置的时候,默认情况下,你将设置精灵的中心点的位置。

要旋转一个精灵,直接设置其zRotation属性。在调用addChild()之前,添加如下的这行代码,从而尝试一下旋转背景精灵。

background.zRotation = CGFloat(M_PI) / 8

旋转值以弧度为单位,这是用来度量角的一个单位。这个示例将精灵旋转π/ 8个弧度,这等于22.5°。还要注意将M_PI(这是一个Double类型)转换为一个CGFloat。之所以要这么做,是因为zRotation需要一个CGFloat,而Swift不会像其他的语言那样,自动地在这些类型之间转换。

 注意

我发现用角度来考虑旋转比用弧度要容易,不知道你怎么看。在本书稍后,我们将创建一个辅助程序,来实现角度和弧度之间的转换。

编译并运行,查看一下旋转后的背景精灵,如图1-23所示。

图1-23 

这展示了重要的一点,精灵是围绕其锚点旋转的。由于我们将背景精灵的锚点设置为(0, 0),背景将围绕着其左下角旋转。

 注意

记住,在iPhone上,图像的左下角实际上在屏幕之外。如果不确定为什么这样,请参考本章前面的1.1.1小节“通用App支持”。

尝试一下围绕着精灵的中心点来旋转精灵。将设置精灵位置和锚点的代码行,替换为如下的代  码行:

background.position = CGPoint(x: size.width/2, y: size.height/2)
background.anchorPoint = CGPoint(x: 0.5, y: 0.5) // default

编译并运行,这一次,背景将会围绕其中心点旋转,如图1-24所示。

图1-24 

了解了这些知识点就很好了。但是对于Zombie Conga,我们并不想要一个旋转后的背景,所以,注释掉如下这行代码:

// background.zRotation = CGFloat(M_PI) / 8

如果对于在游戏中什么时候需要修改锚点还心存疑惑,假想一下我们要创建这样一个角色,其身体是由不同的精灵组成的,而每个精灵分别表示脑袋、身体、左胳膊、右胳膊、左腿到右腿,等等,如图1-25所示。

图1-25 

如果需要绕着这个角色的关节来旋转它的身体的各个部分,那么对于每一个精灵,都必须要修改其锚点。

再一次强调,通常应该保持锚点为默认位置,除非你有特殊的需求,就像图1-25中所给出的例子那样。

有时候,当操作精灵的时候,我们想要知道它有多大。精灵的大小默认为图像的大小。在Sprite Kit中,表示图像的类叫做材质。

在addChild()调用的后面,添加如下的代码行,以获取背景的大小并将其打印到控制台:

let mySize = background.size
print("Size: \(mySize)")

编译并运行,在控制台的输出中,应该会看到如下所示的内容:

Size: (2048.0, 1536.0)

有时候,通过编程(就像上面那样)而不是使用直接编码的数字来获取精灵的大小,这是很有用的。你的代码将会健壮很多,并且更易于修改。

在前面,我们学习了如何让精灵出现在屏幕上,这需要将其作为场景的一个子节点或者一个后代节点添加。本小节将更深入地介绍节点的概念。

在Sprite Kit中,屏幕上所显示的一切内容,都派生自一个叫做SKNode的类。场景类(SKScene)和精灵类(SKSpriteNode)都派生自SKNode,如图1-26所示。

SKSpriteNode的很多功能都是继承自SKNode的。例如,position和zRotation属性都是派生自SKNode的,而并非SKSpriteNode自己所特有的。这意味着,你可以对场景本身或者派生自SKNode的任何对象做相同的事情,就像能够设置一个精灵的位置或旋转精灵一样。

可以将出现在屏幕上的一切内容综合起来,看做是节点组成的一幅图,这通常称之为场景图。例如,假设Zombie Conga游戏中只有一个僵尸、两只小猫和一个猫女士,其场景图如图1-27所示。

图1-26

图1-27 

我们将在第5章中更详细地了解节点以及能对节点做的事情。现在,我们将把精灵作为场景的直接子节点添加。

每个节点都有一个名为zPosition的属性,它的默认值为0。每个节点都会按照其子节点的z位置,从低到高依次绘制其子节点。

在本章前面,我们给GameViewController.swift添加了如下这一行:

skView.ignoresSiblingOrder = true

通常,将这个属性设置为true是较好的做法,因为这允许Sprite Kit在幕后进行性能优化,以使得你的游戏运行得更快。

然而,如果不小心的话,将这个属性设置为true也可能会引发问题。例如,如果把和背景拥有相同的zPosition的一个僵尸添加到了场景中(如果还是把僵尸和背景的zPositon都保留为默认的0的话,就可能会发生这种情况),Sprite Kit可能会把背景绘制于僵尸之上,这会盖住了僵尸而让玩家看不到它。并且如果僵尸很吓人的话,想象一下看不到它的情况。

为了避免这种情况,我们将背景的zPosition设置为-1。通过这种方式,Sprite Kit将会先绘制它,然后再绘制添加到场景中的任何其他内容(这些内容的默认的zPosition为0)。

background.zPosition = -1

本章的内容到此为止了。正如你所看到的,只需要三四行代码就可以把精灵添加到场景中。步骤如下:

1.创建精灵。

2.放置精灵。

3.可选地设置zPosition。

4.把精灵添加到场景图中。

现在,我们来把僵尸添加到场景中,以测试一下新学的知识。

自行练习所学习过的知识,这对你来说是很重要的,因此,本书的每一章末尾都会按照从易到难的顺序,给出1到3个挑战。

我强烈建议你尝试一下所有的挑战,因为尽管按照这个按部就班的教程也可以学到东西,但通过自己解决问题,则可以学到更多的知识。此外,每一章都将从前一章的挑战所完成的地方开始继续,因此,你将会连续地学习。

如果你遇到困难,可以在本章的资源中找到解决方案,但是,要从本书中获得尽可能大的收获,在查看解决方案之前,请尽自己最大的努力去尝试。

现在,我们游戏有了一个漂亮的背景,但是,“明星”还没有出场。作为第一个挑战,为僵尸打开大门吧!

提示:

图1-28 

在iPad Air 2模拟器上运行游戏,以证实它能够在该设备上工作,只不过似乎有一个稍大一点的可视区域,如图1-29所示。

图1-29 

本章介绍了要开发游戏所需要了解的与精灵和节点相关的所有知识。

然而,知道在遇到问题或困难的时候应该到哪里去查找更多的信息,将会是比较好的。我强烈推荐你查看Apple的SKNode Class ReferenceSKSpriteNode Class Reference,这两个文档介绍了我们在Sprite Kit中最常用到的两个类,并且,基本熟悉它们所包含的属性和方法是有好处的。

可以从Xcode的主菜单中选择Help\Documentation and API Reference,并且搜索SKNode和SKSpriteNode以找到相关的参考,如图1-30所示。

图1-30 

现在,来完成第2个挑战,使用这些文档所提供的信息,将僵尸的大小放大为原来的两倍(缩放为2x)。回答这个问题:你是否使用了SKSpriteNode或SKNode的一个方法来做到这一点?


Ray Wenderlich撰写

如果你完成了第1章的挑战,现在屏幕上已经有一个较大一些的僵尸了,如图2-1所示。

图2-1 

 注意

如果没有能够完成挑战或者跳过了第1章,也不要担心,直接打开本章的初始工程,从第1章留下的地方继续进行。

当然,你想要让精灵移动起来,而不只是站在那里,这个僵尸也渴望动起来。

在Sprite Kit中,要移动一个精灵,有两种方法:

1.在第1章中,你可能已经注意到了(如果看一下Apple所提供的模板代码的话),可以使用一个叫做动作(action)的概念,让精灵动起来。我们将在第3章中更详细地学习动作。

2.也可以用更为“经典”的方式来移动精灵,这就是随着时间流逝而手动地设置位置。先学习这种方式是很重要的,因为它提供了大多数的控制,并且将帮助你理解动作能做些什么。

然而,要随着时间流逝而设置精灵的位置,需要一个方法,让游戏在运行的时候周期性地调用它。这就引入了一个新的概念,即Sprite Kit游戏循环。

游戏就像一个翻书动画一样地工作,如图2-2所示。如果你绘制了一系列连续的图像,当尽快地翻动它们的时候,就会产生移动的错觉。

你所绘制的每一个单个的图片,都叫做一帧(frame)。游戏通常会试图在每秒中绘制30到60帧,以使得动画给人流畅的感觉。这个绘制的速率,叫做帧速率(frame rate),或者更具体地称之为每秒帧数(frames per second,FPS)。默认情况下,Sprite Kit会在游戏的右下角显示这个数据,如图2-3所示。

图2-2

图2-3 

 注意

Sprite Kit默认地在屏幕上显示每秒帧数,这一点很方便,因为你在开发自己的游戏的时候,希望能看到FPS,以确保游戏能够很好地运行。理想情况下,我们希望FPS至少达到30。

然而,你只应该关注在一台真实设备上的FPS,因为在模拟器上所得到的FPS会颇为不同。特别是,你的Mac的CPU比iPhone或iPad的处理器要更快,而且Mac拥有更多的内存,但糟糕的是拖慢了模拟的熏染,因此,你不能指望Mac给出任何准确的性能数据。再强调一下,一定在真实设备上测试性能。

除了FPS,Sprite Kit还会显示它在最近一次循环中所渲染的节点数。

可以打开GameViewController.swift,并且把skView.showsFPS和skView.showsNodeCount都设置为false,从而从屏幕去除掉FPS和节点数显示。

在场景背后,Sprite Kit运行了一个无限的循环,我们通常称之为游戏循环(gameloop),如图2-4所示。

图2-4 

图2-4示意了在每一帧中,Sprite Kit都做如下的事情:

1.在场景上调用一个名为update()的方法。可以将想要在每一帧中运行的代码放到这里,因此这是更新精灵的位置或旋转精灵的好地方。

2.做一些其他事情。我们将会在后续的各章再次回顾游戏循环,以进一步理解图2-4中剩下的内容。

3.渲染场景。然后,Sprite Kit会绘制场景图中的所有对象,在场景背后使用OpenGL的绘制命令。

Sprite Kit试图尽可能快地绘制帧,最高可达到60FPS。然而,如果update()方法花得时间太长,或者如果Sprite Kit必须绘制的精灵比硬件一次所能处理的精灵更多的话,帧速率可能会下降。

1.保持update()尽可能的快。例如,在这个方法中,要避免那些较慢的算法,因为该方法在每一种帧中都会调用。

2.保持节点数尽可能的少。例如,当节点离屏并且你不再需要它们的时候,将其从场景图删除,这是一种好的做法。

现在,我们已经知道了update()会在每一帧中调用,并且这是更新精灵位置的好地方,让我们来尝试让僵尸动起来。

我们打算通过5次迭代过程来实现僵尸移动的代码。这样,你可以看到初学者常犯的一些错误及其解决方案,最终,你会理解移动是如何一步一步实现的。

首先,实现一种简单但并不理想的方法,即在每一帧中将僵尸移动固定的数量。在开始之前,打开GameScene.swift并且在didMoveToView()中注释掉把僵尸的大小设置为其两倍的那一行代码,如下所示:

// zombie.setScale(2) // SKNode method

这一行只是一个测试,因此,我们不再需要它了。正常大小的僵尸,已经够让人害怕的了。

在GameScene.swift中,添加如下的方法:

override func update(currentTime: NSTimeInterval) { 
    zombie.position = CGPoint(x: zombie.position.x + 8,
                                  y: zombie.position.y)
}

这里,我们沿着x轴将僵尸的位置更新为比上一次多8个点,而在y轴上保持相同的位置。这会让僵尸从左向右移动,如图2-5所示。

图2-5 

编译并运行,你将会看到僵尸从屏幕上走过。这是不错的内容,但是僵尸的移动给人感觉有点卡顿或不规则。为了探究原因,我们回到Sprite Kit游戏循环来看看。

还记得吧,Sprite Kit试图尽可能快地绘制帧。然而,通常其绘制每一帧所花的时间还是略有不同的,有的时候较慢,有的时候较快。

这意味着,调用update()的循环之间的时间间隔是不同的。为了亲眼看看这一点,可以添加一些代码来打印出自上一次update()调用后经过的时间。在GameScene的属性部分,紧接着zombie属性之后,添加如下这些变量:

var lastUpdateTime: NSTimeInterval = 0
var dt: NSTimeInterval = 0

这里,我们创建了属性来记录Sprite Kit上一次调用update()的时间和自上一次调用到现在经过的时间(即时间增量),后者简写为dt。

然后,将如下这些代码行添加到update()的开始处,在这里,我们计算了从上一次调用update()后经过的时间,并且将其存储到dt中,然后,以毫秒为单位(1秒等于1000毫秒)来显示出时间。

if lastUpdateTime > 0 {
   dt = currentTime - lastUpdateTime
} else {
   dt = 0
}
lastUpdateTime = currentTime
print("\(dt*1000) milliseconds since last update")

编译并运行,你应该会在控制台中看到如下所示的内容:

33.44  51289963908 milliseconds since last update
16.35  37669868674 milliseconds since last update
34.18  78019971773 milliseconds since last update
15.69  98310121708 milliseconds since last update
33.98  83069973439 milliseconds since last update
33.57  79220040422 milliseconds since last update

正如你所看到的,update()调用之间的时间间隔总是略有不同。

 注意

Sprite Kit试图每秒钟调用update()方法60次(每次调用大约花费16毫秒)。然而,如果更新和渲染游戏的一帧所需的时间太长,Sprite Kit可能会减少调用更新方法的频次,并且FPS将会下降。在这里可以看到这一点,有些帧花了30毫秒以上。

之所以看到如此之低的FPS,是因为在模拟器上运行游戏。正如前面所提到的,不要指望模拟器提供准确的性能数据。如果尝试在一台设备上运行代码,应该会看到更高一些的FPS。

注意,即便你的游戏以60FPS平滑地运行,Sprite Kit调用更新方法的频度还是会有些变化。因此,在计算中需要考虑时间增量,接下来我们将学习如何做到这点。

由于你要以每帧固定的量来更新僵尸的位置,而不会考虑这一时间上的变化,你可能最终得到一个看上去有点卡顿或不规则的移动,如图2-6所示。

图2-6 

正确的解决方案是,搞清楚僵尸每秒钟想要移动多远,然后,将其和自上一次更新后经过的时间的因子相乘。让我们来尝试一下。

首先在GameScene的开始处、紧跟在dt之后,添加如下的属性:

let zombieMovePointsPerSec: CGFloat = 480.0

这表示僵尸每秒钟应该移动480个点,大约是场景宽度的1/4。我们将其类型设置为CGFloat,因为将会使用它和CGPoint中的另一个CGFloat进行计算。

在这行代码之后,再添加一个属性:

var velocity = CGPoint.zero

到目前为止,我们已经使用了GPoint来表示位置。然而,使用GPoint来表示2D向量也是很常见且很方便的。

2D向量表示一个方向和一个长度。图2-7给出了可以用来表示僵尸的移动的一个2D向量的例子。可以看到,箭头的方向表示了僵尸应该移动的方向,而箭头的长度表示僵尸应该每秒钟移动多远的距离。方向和长度一起表示僵尸的速率,可以将其看作是僵尸在1秒钟之内应该在哪个方向上移动多远。

图2-7 

然而,注意,这个速率没有设置位置。毕竟,不管僵尸是从哪里开始移动的,我们应该能够让僵尸沿着这个方向、以这样的速度移动。

通过添加如下的方法来尝试这一点:

func moveSprite(sprite: SKSpriteNode, velocity: CGPoint) {
   // 1 
   let amountToMove = CGPoint(x: velocity.x * CGFloat(dt),
                                    y: velocity.y * CGFloat(dt))
   print("Amount to move: \(amountToMove)")
   // 2
   sprite.position = CGPoint(
      x: sprite.position.x + amountToMove.x,
      y: sprite.position.y + amountToMove.y)
}

我们将代码重新组织到一个可以复用的方法中,该方法让精灵开始移动,并且还有决定移动速率的一个向量。让我们一行一行地看看这段代码:

1.速率是每秒移动多少个点,并且你需要计算出僵尸在这一帧中移动多少个点。为了确定这一点,这里将每秒的点数乘以上一次更新之后的秒数的因子。现在,有了一个点来表示僵尸的位置,也可以将其看作是从原点到僵尸的位置的一个向量,另外,还有一个向量来表示僵尸在这一帧中移动的距离和方向,如图2-8所示。

2.要确定僵尸的新的位置,直接给僵尸的位置加上表示移动的向量就可以了,如图2-9所示。

可以用图2-9来表示,但是在代码中,直接将这个点的x坐标和y坐标和向量相加就可以了。

 注意

要学习有关向量的更多知识,请查阅这个不错的指南:http://www.mathsisfun.com/algebra/

最后,在update()中,用如下这一行代码来替代设置僵尸位置的代码行:

moveSprite(zombie,
   velocity: CGPoint(x: zombieMovePointsPerSec, y: 0))

图2-8

图2-9

编译并运行,现在,僵尸从屏幕前移动的时候要平滑得多了。

看一下控制台日志,你将会看到现在僵尸会根据自上一次更新过去了多少时间,在每一帧中移动不同的点数。

0.0  milliseconds since last update
Amount to move: (0.0,0.0)
47.85  30780237634 milliseconds since last update
Amount to move: (11.4847387257032,0.0)
33.34  98929976486 milliseconds since last update
Amount to move: (8.00397431943566,0.0)
34.21  96339915972 milliseconds since last update
Amount to move: (8.21271215798333,0.0)

如果你的僵尸的移动看上去还是有些卡顿,确保在设备上而不是在模拟器上测试它,这可能会有不同的性能表现。

到目前为止,一切都还不错,但是我们想要让僵尸朝着玩家触摸的位置移动。毕竟,每个人都知道僵尸是喜欢热闹的。

我们的目标是让僵尸朝着玩家点击的位置移动,并且持续移动甚至超过了点击的位置,直到玩家点击了另一个位置而吸引了僵尸的注意力。要做到这一点有4个步骤,让我们来逐步介绍。

步骤1:找到偏移向量

首先,我们需要搞清楚玩家点击的位置和僵尸的位置之间的偏移量。可以直接用点击的位置减去僵尸的位置来得到这个偏移量,如图2-10所示。

点和向量之间的减法,和将它们相加是类似的,但是,不是加上x坐标和y坐标值,而是减去x和y坐标值。

图2-10 

图2-10显示,如果用点击的位置减去僵尸的位置,将会得到表示偏移量的一个向量。如果将偏移向量移动到从僵尸的位置开始的话,我们可以更加清晰地看到这一点,如图2-11所示。

图2-11

通过将两个位置相减,我们得到了具有方向和长度的一个内容,称之为偏移向量。

尝试添加如下的方法:

func moveZombieToward(location: CGPoint) {
   let offset = CGPoint(x: location.x - zombie.position.x,
                            y: location.y - zombie.position.y)
}

步骤2:获得偏移向量的长度

现在,需要搞清楚偏移向量的长度,这是在步骤3中所需要的信息。

将偏移向量当做是一个直接三角形的斜边,其中,构成三角形的另外两条边的长度,就是由这个向量的x坐标和y坐标确定的,如图2-12所示。

图2-12 

我们想要得到斜边的长度。为了做到这一点,要使用勾股定理。你可能还记得几何学中的这个简单的公式,即斜边的长度等于另外两条边的平方之和的平方根,如图2-13所示。

图2-13 

根据勾股定理,在moveZombieToward()的末尾添加如下这行代码:

let length = sqrt(
   Double(offset.x * offset.x + offset.y * offset.y))

现在还没有完事呢!

步骤3:设置偏移向量的长度

现在,我们有了一个偏移向量:

而我们想要一个速度向量:

因此,我们已经实现了一半了,向量的方向是对的,但是长度不对。如何让一个向量指向和偏移向量相同的方向,但是又具有某个指定的长度呢?

第一步是将偏移向量转换为一个单位向量,这意味着,该向量的长度为1。根据几何学原理,可以直接将偏移向量的x坐标和y坐标分别除以偏移向量的长度,来做到这一点,如图2-14所示。

图2-14

将一个向量转换为一个单位向量的过程,叫做向量的正规化(normalizing)。

一旦有了这个单位向量,我们知道其长度为1,将它和zombieMovePointsPerSec相乘,可以很容易地得到想要的长度,如图2-15所示。

图2-15 

尝试一下,在moveZombieToward()的末尾添加如下的代码行:

let direction = CGPoint(x: offset.x / CGFloat(length),
                             y: offset.y / CGFloat(length))
velocity = CGPoint(x: direction.x * zombieMovePointsPerSec,
                       y: direction.y * zombieMovePointsPerSec)

现在,我们得到了拥有正确方向和长度的一个速度向量。只有一步之遥了!

步骤4:连接触摸事件

在Sprite Ki中,要在一个节点上获得触摸事件的通知,只需要将该节点的userInteractionEnabled属性设置为true,然后覆盖该节点的touchesBegan(withEvent:)、touchesMoved(withEvent:)方法或touchesEnded(withEvent:)方法。和其他的SKNode类不同,SKScene的userInteractionEnabled属性默认就是设置为true的。

要看看这是如何使用的,为GameScene实现如下这些触摸处理方法:

func sceneTouched(touchLocation:CGPoint) {
   moveZombieToward(touchLocation)
}

override func touchesBegan(touches: Set<UITouch>,
   withEvent event: UIEvent?) {
      guard let touch = touches.first else {
         return
      }
      let touchLocation = touch.locationInNode(self)
      sceneTouched(touchLocation)
}

override func touchesMoved(touches: Set<UITouch>,
   withEvent event: UIEvent?) { 
      guard let touch = touches.first else {
         return
      }
      let touchLocation = touch.locationInNode(self)
      sceneTouched(touchLocation)
}

最后,在update()中,修改对moveSprite()的调用,以传入一个速率(根据触摸),而不是使用预先设定的量:

moveSprite(zombie, velocity: velocity)

好了!编译并运行,现在,僵尸将会朝着你的点击而移动,如图2-16所示。不要离僵尸太近,它可是饥肠辘辘!

图2-16 

 注意

还可以使用Sprite Kit的手势识别器。如果你试图实现复杂的手势的话,例如,捏取或旋转,这会特别方便。

可以在didMoveToView()中,将手势识别器添加到场景的视图中,并且可以使用SKScene的convertPointFromView()方法和SKNode的convertPoint(toNode:)方法,让触摸处于你所需要的坐标空间中。

为了展示这一点,请查看本章的示例代码,我在其中包含了一个注释掉的手势识别器的演示程序。由于它和你所实现的触摸处理程序所做的事情相同,如果在运行这个手势识别器的时候想要确定是手势在起作用,请先注释掉你自己的触摸处理程序。

当我们玩这款游戏的最新版的时候,可能会注意到,如果你让僵尸向前移动的话,它会很欢快地直接跑到屏幕之外。我们很羡慕僵尸的热情,但是在Zombie Conga游戏中,我们希望僵尸随时都待在屏幕之上,如果碰到屏幕边界的话,它可以弹回来。

为了做到这一点,我们需要检测新计算的位置是否超越了屏幕的任何边界,如果是的话,要让僵尸弹回来。添加如下这个新的方法:

func boundsCheckZombie() { 
   let bottomLeft = CGPointZero
   let topRight = CGPoint(x: size.width, y: size.height)

   if zombie.position.x <= bottomLeft.x {
      zombie.position.x = bottomLeft.x
      velocity.x = -velocity.x
   }
   if zombie.position.x >= topRight.x {
      zombie.position.x = topRight.x
      velocity.x = -velocity.x
   }
   if zombie.position.y <= bottomLeft.y {
      zombie.position.y = bottomLeft.y
      velocity.y = -velocity.y
   }
   if zombie.position.y >= topRight.y {
      zombie.position.y = topRight.y
      velocity.y = -velocity.y
   }
}

首先,设定表示场景的左下角和右上角坐标的常量。

然后,检查僵尸的位置,看看它是否超过了任何的屏幕边界,或者在边界之上。如果是的,让僵尸停止在该位置并且将速度部分取反,以使得僵尸朝着相反的方向弹回。

现在,在update()方法的末尾,调用这个新的方法:

boundsCheckZombie()

编译并运行,可以看到僵尸在屏幕上弹回了,如图2-17所示。

图2-17 

在iPhone 6模拟器上运行你的游戏,并且将僵尸朝着屏幕顶端的方向移动。注意,僵尸移动到了屏幕之外而没有弹回,如图2-18所示。

图2-18 

在iPad模拟器上运行游戏,你会看到游戏像预期的那样工作(即僵尸会从顶部的边界弹回)。你是否知道发生了什么情况?

还记得在第1章的1.1.1小节“通用App支持”中,我们提到Zombie Conga设计为4:3的高宽比(屏幕分辨率为2048×1536)。然而,你想要支持16:9的高宽比,而这正是iPhone 5、iPhone 6和iPhone 6 Plus所使用的高宽比(屏幕分辨率分别为1136×640、1334×750和1920×1080)。

我们来看看在16:9的设备上发生了什么。由于我们已经将场景配置为使用AspectFill模式,Sprite Kit首先计算填充了2048×1536的空间的、最大的16:9的矩形,也就是2048×1152。然后,将这个矩形居中,并将其缩放到实际的屏幕大小,例如,iPhone 6的1334×750屏幕需要的缩放比例是0.64,如图2-19所示。

图2-19

这意味着,在16:9的设备上,场景的顶部和底部有192个点的空隙是不可见的(1536 -1152 = 384,384 / 2 = 192)。因此,应该避免在这些区域进行重要的游戏设置,例如,不要让僵尸移动到这些空隙中。

让我们来解决这个问题。首先,给GameScene添加一个新的属性来存储表示游戏区域的矩形:

let playableRect: CGRect

然后,添加如下的初始化函数来相应地设置这个值:

override init(size: CGSize) {
   let maxAspectRatio:CGFloat = 16.0/9.0 // 1
   let playableHeight = size.width / maxAspectRatio // 2
   let playableMargin = (size.height-playableHeight)/2.0 // 3
   playableRect = CGRect(x: 0, y: playableMargin,
                             width: size.width,
                             height: playableHeight) // 4
   super.init(size: size) // 5
}
required init(coder aDecoder: NSCoder) {
   fatalError("init(coder:) has not been implemented") // 6
}

我们来一行行地看看这些代码做些什么:

1.Zombie Conga支持的高宽比从3:2(1.33)到16:9(1.77)。这里,我们将这个常量设置为所支持的最大的高宽比16:9(1.77)。

2.使用AspectFill模式,不管高宽比是多少,游戏区域的宽度总是等于场景的宽度。要计算游戏区域的高度,用场景的宽度除以最大高宽比。

3.我们希望游戏区域矩形在屏幕上居中,因此,用游戏区域的高度减去场景的高度,然后将结果除以2,得到顶部和底部的边距。

4.综合起来,使得矩形在屏幕上居中,并具有最大的高宽比。

5.调用超类的初始化程序。

6.无论何时,当你覆盖Sprite Kit节点的默认初始化程序的时候,必须还要覆盖所需的NSCoder初始化程序,当从场景编辑器加载一个场景的时候,会使用该初始化程序。由于我们不会在这款游戏中使用场景编辑器,直接添加一个占位符实现以记录一个错误。

为了使其可视化,添加一个辅助方法将这个游戏区域绘制到屏幕上:

func debugDrawPlayableArea() {
   let shape = SKShapeNode()
   let path = CGPathCreateMutable()
   CGPathAddRect(path, nil, playableRect)
   shape.path = path
   shape.strokeColor = SKColor.redColor()
   shape.lineWidth = 4.0
   addChild(shape)
}

现在,先不要关心它是如何工作的,我们将在第11章中学习关于SKShapeNodes的所有内容。现在,将其当做是一个黑箱,它只是在屏幕上绘制调试矩形。

接下来,在didMoveToView()的末尾调用这个方法:

debugDrawPlayableArea()

最后,修改boundsCheckZombie()中的头两行代码,以考虑playableRect的y值:

let bottomLeft = CGPoint(x: 0,
                              y: CGRectGetMinY(playableRect))
let topRight = CGPoint(x: size.width,
                            y: CGRectGetMaxY(playableRect))

编译并运行,你将会看到僵尸现在能够根据游戏区域矩形正确地弹回了,如图2-20所示,这个游戏区域矩形用红色线条绘制并且和屏幕的边缘一致。

然后,在iPad模拟器上编译并运行,你将会看到僵尸在这里也会根据游戏区域矩形正确地弹回,如图2-21所示。

这个游戏区域带有红色边框,这就是在iPhone设备上所看到的区域,它拥有所支持的最大的高宽比,即16:9。

既然有了游戏区域矩形,只需要确保剩下的游戏设置都发生在这个矩形之中并且僵尸可以随处跳舞,就可以了。

图2-20 

图2-21 

 注意

一种可选的方法,是根据当前设备的可视区域来限制僵尸的移动。换句话说,总是让僵尸能够在各个方向上移动到iPad的边缘,而不是将其限制在一个最小的游戏区域中。

然而,这可能会让游戏在iPad上更容易玩,因为iPad上有更多的空间可以躲避敌人。对于Zombie Congo来说,我们认为更重要的是让它在所有设备上具有相同的难度,因此,我们让核心游戏设置保持在有保证的游戏区域之中。

僵尸移动的很不错,但是它总是朝着相同的方向。实际上,它是“亡灵”,但是,这个僵尸总是对什么都好奇,它很想在移动的时候转身到处看看。

我们已经有了一个向量指向僵尸所要朝向的方向,这就是速度向量。只需要找出一个旋转角度,让僵尸朝向该方向就可以了。

再一次,把方向向量当做是直角三角形的斜边,就可以找到这个角度,如图2-22所示。

图2-22

你可能还记得,三角学中所谓的正切函数,表示为:

tan(angle) = opposite / adjacent

既然有了对边和邻边的长度,可以将上面的公式重写为如下的形式,来得到需要旋转的角度:

angle = arctan(opposite / adjacent)

如果回忆不起来任何三角学的知识,也不要担心。只要把这当做是计算角度的一个公式就可以了,只需要知道这些就够了。

尝试使用这个公式,添加如下的新的方法:

func rotateSprite(sprite: SKSpriteNode, direction: CGPoint) { 
   sprite.zRotation = CGFloat(
      atan2(Double(direction.y), Double(direction.x)))
}

这里用到了上面的公式。它包括很多的强制转型,因为CGFloat在64位的机器上定义为一个Double,而在32位的机器上则定义为一个Float。

这能够有效,是因为僵尸图像本身是朝向右边的。如果僵尸图像是朝向屏幕上方的,还必须添加一个额外的旋转来进行补偿,因为角度0本来是指向右边的。

现在,在update()方法的末尾调用这个新的方法。

rotateSprite(zombie, direction: velocity)

编译并运行,僵尸已经旋转到面朝它移动的方向了,如图2-23所示。

图2-23 

恭喜你,已经让僵尸具有了生命了!这个精灵现在在iPhone和iPad上都能够平滑地移动,可以从屏幕弹回并且会旋转,是开始玩这个游戏的最佳时机了。但是,我们还没有大功告成。应该自己尝试一些内容,以确定已经学到了知识。

本章有3个挑战,它们都很重要。完成这些挑战,能够让你练习使用向量,并且会引入新的数学工具,而在本书的剩下内容中,你将会用到这些工具。

同样,如果遇到困难,可以从本章的资源文件中找到解决方案,但是你最好是自己能够解决它。

你肯定已经注意到了,在开发这款游戏的时候,经常要进行点和向量的计算,例如,把点相加和相减,求取长度值等等。我们还需要在CGFloat和Double之间做很多强制转型。

在本章中,到目前为止,我们都是以内嵌的方式自行完成这些计算的。这是做事情的一种很好的方式,但是,在实际工作中,这可能变得很繁琐而且具有重复性;还容易出错。

使用iOS\Source\Swift File模板创建一个新的文件,将其命名为MyUtils。然后,使用如下的代码替换MyUtils的内容:

import Foundation
import CoreGraphics

func + (left: CGPoint, right: CGPoint) -> CGPoint {
   return CGPoint(x: left.x + right.x, y: left.y + right.y)
}

func += (inout left: CGPoint, right: CGPoint) {
   left = left + right
}

在Swift中,可以让+、-、*和/这样的运算符作用于任何想要的类型之上。这里,我们让它们作用于CGPoint之上。

现在,可以像下面这样来把点相加了,但是,不要在任何地方添加这些代码;这里只是给出一个示例:

let testPoint1 = CGPoint(x: 100, y: 100)
let testPoint2 = CGPoint(x: 50, y: 50)
let testPoint3 = testPoint1 + testPoint2

让我们也覆盖CGPoints上的减法、乘法和除法运算符。在MyUtils.swift的末尾,添加如下的代码:

func - (left: CGPoint, right: CGPoint) -> CGPoint {
   return CGPoint(x: left.x - right.x, y: left.y - right.y)
}

func -= (inout left: CGPoint, right: CGPoint) {
   left = left - right
}

func * (left: CGPoint, right: CGPoint) -> CGPoint {
   return CGPoint(x: left.x * right.x, y: left.y * right.y)
}

func *= (inout left: CGPoint, right: CGPoint) {
   left = left * right
}

func * (point: CGPoint, scalar: CGFloat) -> CGPoint {
   return CGPoint(x: point.x * scalar, y: point.y * scalar)
}

func *= (inout point: CGPoint, scalar: CGFloat) {
   point = point * scalar
}

func / (left: CGPoint, right: CGPoint) -> CGPoint {
   return CGPoint(x: left.x / right.x, y: left.y / right.y)
}

func /= (inout left: CGPoint, right: CGPoint) {
   left = left / right
}

func / (point: CGPoint, scalar: CGFloat) -> CGPoint {
   return CGPoint(x: point.x / scalar, y: point.y / scalar)
}

func /= (inout point: CGPoint, scalar: CGFloat) {
   point = point / scalar
}

现在,可以把一个CGPoint和另一个CGPoint相减、相乘和相除了。还可以将点和标量的CGFloat值相乘和相除,如下所示。同样的,不要在任何地方添加这些代码,这里只是给出一个示例。

let testPoint5 = testPoint1 * 2
let testPoint6 = testPoint1 / 10

最后,添加扩展了CGPoint的类,它带有一些辅助方法:

#if !(arch(x86_64) || arch(arm64))
func atan2(y: CGFloat, x: CGFloat) -> CGFloat { 
   return CGFloat(atan2f(Float(y), Float(x)))
}

func sqrt(a: CGFloat) -> CGFloat {
   return CGFloat(sqrtf(Float(a)))
}
#endif

extension CGPoint {

   func length() -> CGFloat {
      return sqrt(x*x + y*y)
   }

   func normalized() -> CGPoint {
      return self / length()
   }

   var angle: CGFloat {
      return atan2(y, x)
   }
}

当这个App在32位架构的机器上运行的时候,#if/#endif语句块为true。在这种情况下,CGFloat和Float具有相同的大小,因此,这段代码编写了接受CGFloat/Float值(而不是默认的Double)的atan2和sqrt版本;这就允许你对CGFloat/Float使用atan2和sqrt,而不会受到设备架构的限制。

接下来,这个类扩展添加了一些方便的方法来获取点的长度,返回该点的一个正规化的版本(即长度为1),并且得到该点的一个角度。

使用这些辅助函数,将会使得代码更加简洁和清晰。例如,来看看moveSprite(velocity:)方法:

func moveSprite(sprite: SKSpriteNode, velocity: CGPoint) {
   let amountToMove = CGPoint(x: velocity.x * CGFloat(dt),
                                    y: velocity.y * CGFloat(dt))
   print("Amount to move: \(amountToMove)")
   sprite.position = CGPoint(
      x: sprite.position.x + amountToMove.x,
      y: sprite.position.y + amountToMove.y)
}

使用*将velocity和dt相乘,避免了强制转型,简化了第1行代码。此外,使用+=运算符将精灵的位置和移动的量相加,简化了最后一行代码。

最终的结果应该如下所示:

func moveSprite(sprite: SKSpriteNode, velocity: CGPoint) {
   let amountToMove = velocity * CGFloat(dt)
   print("Amount to move: \(amountToMove)")
   sprite.position += amountToMove
}

你的挑战是,修改剩下的Zombie Conga以使用新的辅助代码,并且验证游戏仍然能够像预期的那样工作。当你完成之后,应该进行如下的调用,这包括对前面已经提及的两个操作符的调用:

你将会注意到,当完成了这些工作的时候,代码变得整洁了很多,而且更加易于理解了。在后续的几章中,你将要使用我们所编写的一个数学库,它和这里所创建的数学库非常相似。

在Zombie Conga,当你点击屏幕的时候,僵尸会朝着点击的位置移动,但是随后,它会继续移动以超过该位置。

这是我们想要在Zombie Conga中得到的效果,但是,在其他的游戏中,你可能想要让僵尸在点击的位置停下来。你的挑战是修改游戏以做到这一点。

如下是针对一种可能的实现的一些提示:

目前,僵尸会立即旋转以面朝点击的位置。这可能有点突兀,如果僵尸随着时间的流逝逐渐平滑地旋转以面朝新的方向的话,看上去会好很多。

为了做到这一点,需要一个新的辅助程序。将如下代码添加到MyUtils.swift(to typeπ, use Option-p)的末尾。

letπ = CGFloat(M_PI)

func shortestAngleBetween(angle1: CGFloat,
                               angle2: CGFloat) -> CGFloat {
   let twoπ = π * 2.0
   var angle = (angle2 - angle1) % twoπ
   if (angle >= π) {
      angle = angle - twoπ
   }
   if (angle <= -π) {
      angle = angle + twoπ
   }
   return angle
}

extension CGFloat {
   func sign() -> CGFloat {
      return (self >= 0.0) ? 1.0 : -1.0
   }
}

如果CGFloat大于或等于0,sign()返回1,否则的话,它返回-1。

shortestAngleBetween()返回两个角之间的最短的角度。这并不是将两个角相减那么简单,理由有两个:

1.角度在超过360度(2 * M_PI)之后会“舍入”。换句话说,30度和390度表示相同的角度,如图2-24所示。

2.有时候,两个角之间旋转最短的方式是向左,而有时候又是向右。例如,如果从0度开始,想要转到270度,最短的方式是转-90度,而不是转270度,如图2-25所示。我们不想让僵尸转一大圈,虽然它是僵尸,但是它并不蠢笨。

图2-24

图2-25 

因此,这个程序求得两个角度之间的差,去掉任何比360度大的部分,然后确定是向右旋转还是向左旋转更快。

你的挑战是修改rotateSprite(direction:),以接受并使用一个新的参数,即僵尸每秒应该旋转的弧度数。

定义如下的常量:

let zombieRotateRadiansPerSec:CGFloat = 4.0 * π

并且将该方法的签名修改为如下所示:

func rotateSprite(sprite: SKSpriteNode, direction: CGPoint,
                      rotateRadiansPerSec: CGFloat) {
   // Your code here!
}

这里针对这个方法的实现给出一些提示:

如果你完成了所有这3个挑战,做的真是不错!你真的已经理解了如何使用“经典的”方法随着时间来更新值,从而移动和旋转精灵。

然而,经典的方法只是为了便于理解,它总是会让步于现代的方法的。

在第3章中,我们将学习Sprite Kit如何通过神奇的动作,让一些常见的任务变得非常容易。


相关图书

iOS 14开发指南【进QQ群414744032索取配套资源】
iOS 14开发指南【进QQ群414744032索取配套资源】
iOS 11 开发指南
iOS 11 开发指南
Swift 3开发指南
Swift 3开发指南
iOS  项目开发全程实录
iOS 项目开发全程实录
iOS 10 开发指南
iOS 10 开发指南
iOS 9应用开发入门经典(第7版)
iOS 9应用开发入门经典(第7版)

相关文章

相关课程