diff --git "a/issue-20/Sprite Kit \346\270\270\346\210\217\344\270\255\347\232\204\344\270\211\350\247\222\345\255\246(1).md" "b/issue-20/Sprite Kit \346\270\270\346\210\217\344\270\255\347\232\204\344\270\211\350\247\222\345\255\246(1).md" new file mode 100644 index 0000000..0ae78af --- /dev/null +++ "b/issue-20/Sprite Kit \346\270\270\346\210\217\344\270\255\347\232\204\344\270\211\350\247\222\345\255\246(1).md" @@ -0,0 +1,1624 @@ +> * 原文链接 : [Trigonometry for Games – Sprite Kit and Swift Tutorial: Part 1/2](http://www.raywenderlich.com/90520/trigonometry-games-sprite-kit-swift-tutorial-part-1) +* 原文作者 : [Nick Lockwood](http://www.raywenderlich.com/u/nicklockwood) +* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn) +* 译者 : [kmyhy](https://github.com/kmyhy) + +Update 04/20/2015: Updated for Xcode 6.3 and Swift 1.2. + +更新 2015/04/20:升级至 Xcode 6.3 和 Swift 1.2 + +Update Note: This is the third incarnation of one of our very popular tutorials – the first version was written by Tutorial Team member Matthijs Hollemans for Cocos2D, and the second version was update to Sprite Kit by Tony Dahbura. This latest version still uses Sprite Kit, but is updated for iOS 8 and Swift. + +更新说明:这是我们广受欢迎的教程之一的第三个版本——第一个版本是 Cocos2D 的,由 Matthijs Hollemans 缩写,第二个版本由 Tony Dahbura 升级为 Sprite Kit。最终的版本仍然是 Sprite Kit 的,但升级至 iOS 8 和 Swift。 + +Does the thought of doing mathematics give you cold sweats? Are you ready to give up on your career as a budding game developer because the math just doesn’t make any sense to you? + +是否一提到数学就让你恐惧?你是否曾经因为数学成绩不好而想放弃游戏开发这个职业? + +Don’t fret – math can be fun, and this cool 2-part game tutorial will prove it! + +不要烦恼——数学其实很有趣,而且也很酷——这两篇教程会证明这一点! + +Here’s a little secret: as an app developer, you don’t really need to know a lot of math. Most of the computations that we do in our professional lives don’t go much beyond basic arithmetic. + +有一个诀窍:作为一个开发者,你其实不需要学习多少数学技能。在我们的职业生涯中的绝大部分计算,其实都用最基本的数学技能就足以应付。 + +That said, for making games it is useful to have a few more math skills in your toolbox. You don’t need to become as smart as Archimedes or Isaac Newton, but a basic understanding of trigonometry, combined with some common sense, will take you a long way. + +对于编写游戏来说,在你的技能中拥有一些数学技能是有用的。不需要你是阿基米德或者艾萨克.牛顿,但需要知道一些三角学以及一些数学常识,你需要做好心理准备。 + +In this tutorial, you will learn about some important trigonometric functions and how you can use them in your games. Then you’ll get some practice applying the theories by developing a simple space shooter iPhone game using the Sprite Kit game framework. + +在本教程中,你需要学习一些重要的三角函数,以及如何在游戏中使用它们。然后,你需要用学到的知识做一些练习,通过 Sprite Kit 开发一个简单的太空射击游戏。 + +Don’t worry if you’ve never used Sprite Kit before or are going to use a different framework for your game – the mathematics covered in this tutorial are applicable to any engine you might choose to use. And you don’t need any prior experience, as I’ll walk through the process step-by-step. + +如果你之前从未使用过 Sprite Kit 或其它游戏开发框架也不要担心——本教程中涉及的数学技能对任何游戏引擎都是有效的。你不需要做任何预习,我会一步一步地开始整个教程。 + +If you supply the common sense, this tutorial will get you up to speed on the trigonometry, so let’s get started! + +如果你已经具备一些基本的背景知识,本教程将让加深对理解三角数学的理解,让我们开始吧! + +> Note: The game you’ll build in this tutorial uses the accelerometer so you’ll need a real iOS device and a paid developer account. + +> 注意:本教程中的游戏使用了加速计,因此你应该使用真实的 iOS 设备以及一个开发者账号。 + +## Getting Started: It’s All About Triangles +## 开始:关于三角学 + +It sounds like a mouthful, but trigonometry (or trig, for short) simply means calculations with triangles (that’s where the tri comes from). + +听起来有点拗口,但三角数学(或简称三角学)的简单定义就是与三角形有关的计算(三角学因此而来)。 + +You may not have realized this, but games are full of triangles. For example, imagine you have a spaceship game, and you want to calculate the distance between these ships: + +你也许不知道,游戏基本上是由三角形构成。例如,一个太空飞船游戏,我们需要计算出飞船之间的距离: + +![](http://cdn4.raywenderlich.com/wp-content/uploads/2013/04/ships.png) + +You have X and Y position of each ship, but how can you find the length of that line? + +假设你知道每张飞船的 x ,y 坐标,如何计算出二者之间的距离? + +Well, you can simply draw a line from the center point of each ship to form a triangle like this: + +从两张飞船的中心画一条连线,构造出一个三角形: + +![](http://cdn3.raywenderlich.com/wp-content/uploads/2013/03/Triangles-in-games-455x320.png) + +Then, since you know the X and Y coordinates of each ship, you can compute the length of each of the new lines. Now that you know the lengths of two sides of the triangle, you can use trig to compute the length of the diagonal line – the distance between the ships. + +因为我们知道每张飞船的 x,y 坐标,因此,我们可以算出新加的两条线的长度。现在,你已经获得三角形两条边的长,通过三角函数,你可以算出对角线的长度——也就是飞船之间的距离。 + +Note that one of the corners of this triangle has an angle of 90 degrees. This is known as a right triangle (or right-angle triangle, for you Brits out there!), and that’s the sort of triangle you’ll be dealing with in this tutorial. + +注意,这个三角形有一个 90 度的角。因此它是直角三角形(或者正三角形,随便你怎么称呼它),这个教程中会针对这种三角形进行特别的处理。 + +Any time you can express something in your game as a triangle with a 90-degree right angle – such as the spatial relationship between the two sprites in the picture – you can use trigonometric functions to do calculations on them. + +只要在游戏中能够以直角三角形描述的问题——比如两个对象之间的空间关系——我们都可以用三角学函数进行计算。 + +So in summary, trigonometry is the mathematics that you use to calculate the lengths and angles of right triangles. And that comes in handy more often that you might think. + +总之,三角学是用来计算直角三角形边长和角度的数学。它们比你想象的还要有用。 + +For example, in this spaceship game you might want to: + +例如,在这个太空飞行游戏中,可能会发生这些事情: + +* Have one ship shoot a laser in the direction of the other ship + + 一只飞船向另一只飞船发射激光束 + +* Have one ship start moving in the direction of another ship to chase + + 一只飞船想另一只飞船追去 + +* Play a warning sound effect if an enemy ship is getting too close + + 如果敌人的飞船靠得太紧,播放报警声 + +All of this and more you can do with the power of trigonometry! + +诸如此类的,你都会用到三角学! + +### Your Arsenal of Functions +### 三角函数 + +First, let’s get the theory out of the way. Don’t worry, I’ll keep it short so you can get to the fun coding bits as quickly as possible. +These are the parts that make up a right triangle: + +首先介绍一些理论。别担心,我会尽量简短,已让你尽快接触到代码。一个直角三角形有以下几部分组成: + +![](http://cdn5.raywenderlich.com/wp-content/uploads/2013/03/Triangle-anatomy.png) + + +In the picture above, the slanted side is called the hypotenuse. It always sits across from the corner with the 90-degree angle (also called a right angle), and it is always the longest of the three sides. + +在上图中,三角形中倾斜的那条边被叫做斜边。它总是对着 90 度角(即直角)的那条边,它是三条边中最长的一条边。 + +The two remaining sides are called the adjacent and the opposite, as seen from one particular corner of the triangle, the bottom-left corner in this case. + +另外两条边叫做邻边和对边,对边是对着三角形某个角的那条边,在这个例子里,也就是位于左下角的角。 + +If you look at the triangle from the point of view of the other corner (top-right), then the adjacent and opposite sides switch places: + +如果你从另一个角的角度(例如右上角)来看,则邻边和对边恰恰相反。 + +![](http://cdn5.raywenderlich.com/wp-content/uploads/2013/03/Triangle-other-corner.png) + +Alpha (α) and beta (β) are the names of the two other angles. You can call these angles anything you want (as long as it sounds Greek!), but usually alpha is the angle in the corner of interest and beta is the angle in the opposing corner. In other words, you label your opposite and adjacent sides with respect to alpha. + +α 和 β 是直角之外的两个角。你可以随便命名这些角(任何希腊字母),一般我们将第一个角叫做 α 角,另一个角叫做 β 角。同时,邻边和对边是相对于 α 角而言的。 + +The cool thing is that if you only know two of these things, trigonometry allows you to find out all the others using the trigonometric functions sine, cosine and tangent. For example, if you know any angle and the length of one of the sides, then you can easily derive the lengths and angles of the other sides and corners: + +最酷的一件事情是,你只需要知道其中两个变量,你就可以用三角函数 sin、cos 和 tan 算出其它所有的变量。例如,你知道任何一个角的大小和一条边的长度,你就可以算出其它所有角的大小好边长: + +![](http://cdn5.raywenderlich.com/wp-content/uploads/2013/03/Trig-functions.png) + +You can see the sine, cosine, and tangent functions (often shortened to sin, cos and tan) are just ratios – again, if you know alpha and the length of one of the sides, then sin, cos and tan are ratios that relate two sides and the angle together. + +你可以把 sin、cos、tan 看成是系数——如果你知道 α 角和一条边的长度,sin、cos 和 tan 则代表了两条边和角度之间的关系的系数。 + +Think of the sin, cos and tan functions as “black boxes” – you plug in numbers and get back results. They are standard library functions, available in almost every programming language, including Swift. + +以 sin 为例,cos 和 tan 函数就像一个”黑盒子“——将几个数字放到盒子中,它就会返回结果。它们是标准库函数,无论哪种编程语言都会有, Swift 也不例外。 + +> Note: The behavior of the trigonometric functions can be explained in terms of projecting circles onto straight lines, but you don’t need to know how to derive those functions in order to use them. If you’re curious, there are plenty of sites and videos to explain the details; check out the Math is Fun site for one example. +> +> 注意:三角函数的作用就像是把一个圆投影到直线上,要使用它们并不需要我们去理解函数是怎么实现的。如果你想知道其中细节,可以在许多站点或视频中找到解释,例如这个站点:[Math is Fun](http://www.mathsisfun.com/sine-cosine-tangent.html) + +###Know Angle and Length, Need Sides +###已知一个夹角和一边之长,求三角形另两边之长 + +Let’s consider an example. Suppose that you know the alpha angle between the ships is 45 degrees, and the length between the ships (the hypotenuse) is 10 points long. + +我们来举一个例子。假设已知两只飞船之间的 α 角为 45 度,以及斜边长度为 10。 + +![](http://cdn2.raywenderlich.com/wp-content/uploads/2015/01/Triangles-in-games-measured.png) + +You can then plug these values into the formula: + +将上述值代入公式: + +sin(45) = opposite / 10 + +To solve this for the hypotenuse, you simply shift this formula around a bit: + +进行等式变形,结果为: + +opposite = sin(45) * 10 + +The sine of 45 degrees is 0.707 (rounded to three decimal places), and filling that in the forumla gives you the result: + +45 度角的 sin 值为 0.707(截取至 3 位小数),于是上式可变为: + +opposite = 0.707 * 10 = 7.07 + +There is a handy mnemonic for remembering what these functions do that you may remember from high school: SOH-CAH-TOA (where SOH stands for Sine is Opposite over Hypotenuse, and so on), or if you need something more catchy: Some Old Hippy / Caught Another Hippy / Tripping On Acid. (That hippy was probably a mathematician who did a little too much trig! :]) + +还记得你在高中的时候学过的一个记住这些函数的小窍门吗:SOH-CAH-TOA(SOH表示:sin 是对边比斜边,依次类推),还有一首顺口溜:Some Old Hippy / Caught Another Hippy / Tripping On Acid,有个老嬉皮士,抓住另一个嬉皮士,陷入了迷幻之中(有可能那个嬉皮士就是一个三角学搞多了的数学家:])。 + +### Know 2 Sides, Need Angle +### 已知两条边之长,求夹角 + +The formulae above are useful when you already know an angle, but that is not always the case – sometimes you know the length of two sides and are looking for the angle between them. To derive the angle, you can use the inverse trig functions, aka arc functions (which has nothing to do with Automatic Reference Counting, before you ask!): + +当你知道角度的时候,上面的公式很有用,但这种情况就不行了——你只知道两条边求它们之间的夹角。这就需要用到反三角函数了,即 arc 函数(这跟自动引用计数毫无关系!)。 + +![](http://cdn1.raywenderlich.com/wp-content/uploads/2013/04/Inverse-trig-functions.png) + +* angle = arcsin(opposite/hypotenuse) + + 角度 = arcsin(对边/斜边) + +* angle = arccos(adjacent/hypotenuse) + + 角度 = arccos(邻边/斜边) + +* angle = arctan(opposite/adjacent) + + 角度 = arctan(对边/邻边) + +If sin(a) = b, then it is also true that arcsin(b) = a. Of these inverse functions, you will probably use the arc tangent (arctan) the most in practice because it will help you find the hypotenuse (remember TOA – Opposite over Adjacant!). Sometimes these functions are written as sin-1, cos-1, and tan-1, so don’t let that confuse you. + +如果 sin(a) = b,则 arcsin(b) = a。在这些反三角函数中,反切函数 arctan 是最实用的,因为它能够帮你找出斜边(即TOA——对边比邻边)。有时候这些函数也被写成 sin-1,cos-1,tan-1,千万别搞错了。 + +Is any of this sinking in or sounding familiar? Good, because you’re not done yet with the theory lesson – there is still more that you can calculate with triangles. + +是不是有点感觉活着听起来很顺耳了?很好,因为理论课还没有上完——在你能够进行三角计算之前还有一些东西需要学习。 + +### Know 2 Sides, Need Remaining Side +### 已知两边之长,求第三边之长 + +Sometimes you may know the length of two of the sides and you need to know the length of the third (as with the example at the beginning of this tutorial, where you wanted to find the distance between the two spaceships). + +有时候,你知道了两条边的长,想求取第三边的长(例如本教程一开始的例子,想计算两个飞船之间的距离)。 + +This is where Trigonometry’s Pythagorean Theorem comes to the rescue. Even if you forgot everything else about math, this is probably the one formula you do remember: + +这就需要用到三角学的勾股定理了。如果你已经彻底忘光了以前学过的数学课,那么这个公式也许会勾起你的记忆: + +a2 + b2 = c2 + +Or, put in terms of the triangle that you saw earlier: + +或者用三角形的专用名词来说: + +opposite2 + adjacent2 = hypotenuse2 + +对边2 + 邻边2 = 斜边2 + +If you know any two sides, calculating the third is simply a matter of filling in the formula and taking the square root. This is a very common thing to do in games and you’ll be seeing it several times in this tutorial. + +如果你知道两边之长,用上面的公式通过开方能够很容易计算出第三边。在游戏中经常需要这样做,在本教程中你会反复看到。 + +> Note: Want to drill this formula into your head while having a great laugh at the same time? Search YouTube for “Pythagoras song” – it’s an inspiration for many! +> +> 注意:要想牢牢记住这个公式,有一个很有趣的方式。在 YouTube 中搜索一首“Pythagoras song”的视频吧,很有意思。 + +### Have Angle, Need Other Angle +### 知道一个角,求取另一个角 + +Lastly, the angles. If you know one of the non-right angles from the triangle, then figuring out the other ones is a piece of cake. In a triangle, all angles always add up to a total of 180 degrees. Because this is a right triangle, you already know that one of the angles is 90 degrees. That leaves: + +最后,来求夹角。如果我们知道一个非直角的角的大小,则很容易得到另一个夹角的大小。在一个三角形中,所有角的大小之和总是 180 度。对于直角三角形,我们知道其中一个角肯定是 90 °,因此剩下两个角就简单了: + +alpha + beta + 90 = 180 + +Or simply: + +简化之后变成: + +alpha + beta = 90 + +The remaining two angles must add up to 90 degrees. So if you know alpha, you can calculate beta, and vice-versa. + +剩余两个角之和总是 90 °。如果你知道 α 是多少,肯定能算出 β,反之亦然。 + +And those are all the formulae you need to know! Which one to use in practice depends on the pieces that you already have. Usually you either have the angle and the length of at least one of the sides, or you don’t have the angle but you do have two of the sides. + +所有这些公式你都需要记住!要用哪一个公式,取决于已知条件。通常,要么已知夹角和一条边的边长,要么已知两条边之长。 + +Enough theory. Let’s put this stuff into practice. + +好了,理论就学到这里。让我们来做些练习。 + +![](http://cdn1.raywenderlich.com/wp-content/uploads/2013/03/Aristotle.png) + +### To Skip, or Not to Skip? +### 跳过,还是不跳过? + +In the next few sections, you will be setting up a basic Sprite Kit project with a spaceship that can move around the screen using the accelerometer. This won’t involve any trigonometry (yet), so if you already know Sprite Kit and feel like this guy: + +接下来几节,你会创建一个基本的 Sprite Kit 项目,这个 App 中有一艘太空飞船会在屏幕上根据加速计来移动。这不会涉及任何三角计算,你如果对 Sprite Kit 非常熟悉了,就像下面这个家伙一样: + +![](http://cdn5.raywenderlich.com/wp-content/uploads/2011/03/fthat.jpg) + +Then feel free to skip ahead to the Begin the Trigonometry! section below – I have a starter project waiting for you there. + +那么你可以跳过开头的内容,直接进入“开始三角计算”一节!——在那里,我会为你提供一个开始项目。 + +But if you’re the type who likes to code everything from scratch, keep reading! :] + +但如果你喜欢从头开始编写代码,请继续阅读 :] + +###Creating the Project +### 创建项目 + +First make sure you have Xcode 6.1.1 or later, as Swift is a brand new language and the syntax is prone to change subtly between versions. + +首先,确保你安装了 Xcode 6.1.1 或以上版本。因为 Swift 是一个崭新的语言,它的每个版本的语法都会何之前的版本有细微的区别。 + +Next, start up Xcode, select File\New\Project…, choose iOS\Application\Game template. Name the project TrigBlaster. Make sure Language is set to “Swift”, Game Technology is set to “SpriteKit”, and Devices is set to “iPhone”. Then click Next: + +打开 Xcode,选择 File\New\Project...,选择 iOS\Application\Game 模板。项目命名为 TrigBlaster,语言选择 Swift,游戏技术设置为 SpriteKit,设备类型设置为 iPhone。然后点击 Next: + +![](http://cdn1.raywenderlich.com/wp-content/uploads/2014/12/SpriteKit-Project-480x283.png) + +Build and run the template project. If all works OK, you should see the following: + +编译运行程序。如果一切顺利,你将看到: + +![](http://cdn4.raywenderlich.com/wp-content/uploads/2014/12/SpriteKitHelloWorld-179x320.png) + +Download the resources for this tutorial. This file contains the images for the sprites and the sound effects. Unzip it, and drag the images individually into your Images.xcassets to set up the sprites. You can delete/replace the Spaceship sprite that comes with the default project template, as you won’t be using that. + +从[这里](http://cdn5.raywenderlich.com/wp-content/uploads/2013/12/TrigResourcesArt.zip)下载本教程所需资源。这个压缩文件包含了图片和声音。解压缩,将每张图片拖到 Images.xcassets 文件夹,以备创建精灵时用到。你可以删除/替换默认项目中的 Spaceship 精灵,如果你不想用它的话。 + +Now add the sounds. You can simply drag the whole Sounds folder into Xcode, but make sure you select the “Create groups” option. + +现在来添加声音。将 Sounds 文件夹拖进 Xcode 中,确保选中 Create groups 选项。 + +![](http://cdn5.raywenderlich.com/wp-content/uploads/2014/12/AddingSounds-281x320.png) +![](http://cdn2.raywenderlich.com/wp-content/uploads/2014/12/FileImport-480x282.png) + +Great; the usual preliminaries are over with – now let’s get coding! + +好,准备工作已经完成——现在让我们来编写代码! + +### Steering with Accelerometers +### 用加速计做方向盘 + +Because this is a simple game, you will be doing most of your work inside a single file: GameScene.swift. Right now, this file contains a bunch of stuff that you don’t need. It also does not run with the correct orientation for the game, so let’s fix that first. + +这是一个简单游戏,你只需要在一个文件中完成绝大部分工作:GameScene.swift。现在,这个文件中包含了一大堆你用不到的代码。游戏运行的方向也不正确,我们先来搞定这个。 + +#### Switching to Landscape Orientation +#### 切换到横屏模式 +Open the target settings by clicking your TrigBlaster project in the Project Navigator and selecting the TrigBlaster target. Then, in the Deployment Info section make sure General is checked at the top and under Device Orientation uncheck all but Landscape Left (as shown below): + +在项目导航窗口中点击 TrigBlaster ,打开 Target 设置,选中 Target 列表中的 TrigBlaster。打开 General 标签窗口,在 Deployment Info 一栏的 Device Orientation 下,反选所有方向,只勾选 Landscape Right(译者注:原文是 Left,但图中又是 Right,根据后面的内容看应该是 Right): + +![](http://cdn3.raywenderlich.com/wp-content/uploads/2014/12/LandscapeRight-480x303.png) + +If you build and run, the app will now launch in landscape orientation. The app is currently loading an empty scene from the GameScene.sks file in GameViewController.swift, and then in GameScene.swift, a “Hello World” label is being added programmatically. + +运行程序, App 将以横屏方向启动。当前 App 打开了一个空的画面,在 GameViewController.swift 的代码中,这个画面是来自于 GameScene.sks 文件。在 GameScene.swift 代码中,添加了一个 Hello World 标签。 + +Replace the contents of GameScene.swift with: + +将 GameScene.swift 中的代码替换为: + +```swift +import SpriteKit + +class GameScene: SKScene { + + override func didMoveToView(view: SKView) { + + // set scene size to match view + size = view.bounds.size + + backgroundColor = SKColor(red: 94.0/255, green: 63.0/255, blue: 107.0/255, alpha: 1) + } + + override func update(currentTime: CFTimeInterval) { + + } +} +``` + +Build and run, and you should see… nothing but purple: + +运行程序,你将看到一个空的、紫颜色的画面: + +![](http://cdn4.raywenderlich.com/wp-content/uploads/2014/12/Purple-480x269.png) + +Let’s make things a bit more exciting by adding a spaceship to the scene. Modify the GameScene class as follows: + +让我们来干点稍微有趣的事情,将一艘太空飞船添加到画面中。将 GameScene 类修改为: + +```swift +class GameScene: SKScene { + + let playerSprite = SKSpriteNode(imageNamed: "Player") + + override func didMoveToView(view: SKView) { + + // set scene size to match view + size = view.bounds.size + + backgroundColor = SKColor(red: 94.0/255, green: 63.0/255, blue: 107.0/255, alpha: 1) + + playerSprite.position = CGPoint(x: size.width - 50, y: 60) + addChild(playerSprite) + } + + ... +} +``` + +This is all pretty basic if you have worked with Sprite Kit before. The playerSprite property holds the spaceship sprite, which is positioned in the bottom-right corner of the screen. Remember that with Sprite Kit it is the bottom of the screen that has a Y-coordinate of 0, unlike in UIKit where y = 0 points to the top of the screen. You’ve set the Y-coordinate to 60 to position it just above the FPS (Frames Per Second) counter in the bottom-left corner. + +这些代码太常见了,如果你以前用过 Sprite Kit 的话。playerSprite 属性用于保存飞船精灵,并将它放到屏幕的右下角。注意,Sprite Kit 的 y 坐标零点位于屏幕最下边,而不是 UIKit 中的屏幕最上边。我们将 y 坐标设置为 60,这样会将它放到屏幕左下角的FPS(帧率)的上方。 + +> Note: The FPS counter is useful for debugging purposes, but you can disable it in GameViewController.swift if it annoys you. You’ll probably want to do that before you submit your game to the App Store! +> +> 注意:FPS 信息是用于调试的,但我们可以隐藏它,如果你不想看到它的话。在你将游戏提交给 App 商店之前,你可以这样做。 + +Build and run to try it out, and you should see the following: + +运行程序,你将看到: + +![](http://cdn5.raywenderlich.com/wp-content/uploads/2014/12/PurpleShip-480x269.png) + +To move the spaceship, you’ll be using the iPhone’s built-in accelerometer. Unfortunately, the iOS Simulator can’t simulate the accelerometer, so from now on you will need to run the app on a real device to test it. + +要让飞船移动,你需要使用 iPhone 的内置加速计。不幸的是,iOS 模拟器无法模拟加速计,因此从现在起,你就需要在真实物理设备上进行开发了。 + +>Note: If you are unsure how to install the app on a device, check out this extensive tutorial that explains how to obtain and install the certificates and provisioning profiles that allow Xcode to install on a physical iPhone or iPad. It’s not as intimidating as it looks, but you will need to sign up for the paid Apple developer program. +> +>注意:如果你不知道如何在设备上安装 App,请看另外一个教程,该教程描述了如何获取和安装证书和设备授权文档,已允许 Xcode 将 App 安装到真实的 iPhone 或 iPad 上。虽然不是强制的,但你必须购买一个苹果开发者证书。 + +To move the spaceship with the accelerometer, you’ll need to tilt your device from side to side. This was the reason you de-selected all Device Orientation options except for Landscape Right in the Project Settings screen earlier, because it would be really annoying for the screen to flip when you’re in the middle of a heated battle! + +要让加速计能够驱动飞船,我们需要将设备向一边倾斜。这就是为什么我们要在项目设置中将设备方向固定为一个横屏方向的原因,因为当你处于激烈战斗中的时候,屏幕突然发生旋转是一件非常悲剧的事情! + +Using the accelerometer is pretty straightforward thanks to the Core Motion framework. There are two ways to get accelerometer data: You can either register to have it delivered to your application at a specific frequency via a callback, or you can poll the values when you need them. Apple recommends not having data pushed to your application unless timing is very critical (like a measurement or navigation service) because it can drain the batteries more quickly. + +加速计的使用非常简单,因为我们可以使用 Core Motion 框架。要获取加速计数据有两种方式:注册一个通知让加速计以某个周期不断地向 App 发送消息并调用回调方法,或者在我们需要数据时主动拉取数据。苹果建议我们不要使用“推”数据的方式除非有必要(比如进行精确测量或导航服务)。因为这种方式会比较耗电。 + +Your game already has a logical place from which to poll the accelerometer data: the update() method that gets called by Sprite Kit once per frame. You will read the accelerometer values whenever this method is fired, and use them to move the spaceship. + +你的游戏有一个地方非常适合“拉取”加速计数据:update()方法每一帧都会被 Sprite Kit 调用。你可以在这个方法中获取加速计数据,并以此来移动飞船。 + +First, add the following import to the top of GameScene.swift: + +首先,在 GameScene.swift 顶部加入一个导入语句: + +```swift +import CoreMotion +``` + +Now you’ll have Core Motion available to you and it’ll be linked into your app. + +现在,Core Motion 框架会链接到 App,你可以使用它了。 + +Next, add the following properties inside the class implementation: + +接着,在类的实现中增加如下属性: + +```swift +var accelerometerX: UIAccelerationValue = 0 +var accelerometerY: UIAccelerationValue = 0 + +let motionManager = CMMotionManager() +``` +You’ll need these properties to keep track of the Core Motion manager and the accelerometer values. You only need to store the values for the x- and y-axes; the z-axis isn’t used by this game. + +我们用这些属性来存储 Core Motion 管理器和加速计的值。你只需要保存 x 的值和 y 值,z 坐标的值在这个游戏中暂时不需要。 + +Next, add the following utility methods to the class: + +然后,新增两个工具方法: + +```swift +func startMonitoringAcceleration() { + + if motionManager.accelerometerAvailable { + motionManager.startAccelerometerUpdates() + NSLog("accelerometer updates on...") + } +} + +func stopMonitoringAcceleration() { + + if motionManager.accelerometerAvailable && motionManager.accelerometerActive { + motionManager.stopAccelerometerUpdates() + NSLog("accelerometer updates off...") + } +} +``` + +The start and stop methods check to make sure the accelerometer hardware is available on the device and, if so, tell it to start gathering data. The stop method will be called when you wish to turn off acceleration monitoring. + +start 方法会检测设备上是否具有加速计硬件,如果是,则开始收集数据。stop 方法则用于关闭加速计监听。 + +A good place to activate the accelerometers is inside didMoveToView(). Add the following line to it underneath the addChild(playerSprite) line: + +激活加速计的较合适的地方是在 didMoveToView() 方法里面。在这个方法的 addChild(playerSprite) 一行后加入: + +```swift +startMonitoringAcceleration() +``` + +For stopping the accelerometers, a good place is in the class de-initializer. Add the following to the class: + +而停止加速计的时机是在类的解析函数里面。在类中增加一个方法: + +```swift +deinit { + stopMonitoringAcceleration() +} +``` + +Next, add the method that will be called to read the values and let your player change positions: + +然后,新增这个方法,每当玩家角色位置发生改变时就调用这个方法读取加速计的值: + +```swift +func updatePlayerAccelerationFromMotionManager() { + + if let acceleration = motionManager.accelerometerData?.acceleration { + + let FilterFactor = 0.75 + + accelerometerX = acceleration.x * FilterFactor + accelerometerX * (1 - FilterFactor) + accelerometerY = acceleration.y * FilterFactor + accelerometerY * (1 - FilterFactor) + } +} +``` + +This bit of logic is necessary to filter, or smooth the data that you get back from the accelerometers so that it appears less jittery. The motionManager.accelerometerData property may be nil if no data is yet available, so you use the ?. operator to access the acceleration property, and wrap the logic in if let ... to ensure it will be skipped if there is no acceleration data to work with yet. + +这里进行了过滤处理,目的是为了使加速计返回的数据更平滑,卡顿感更少。如果没有数据,motionManager.accelerometerData 属性有可能为 nil,因此要用 ?. 操作符和 if let ... 语法访问 acceleration 属性,以确保当加速计数据为空时 if 语句不会被执行。 + +> Note: An accelerometer records the acceleration currently being applied to it. The iPhone is always under acceleration due to the pull of gravity (which is how iOS knows which way to orient the screen), but because the user is holding the iPhone in their hands (and hands are never completely steady) there are a lot of tiny fluctuations in this gravity value. You’re not so interested in these fluctuations as in the larger, deliberate changes that the user makes to the device orientation. By applying this simple low-pass filter, you retain the orientation information but filter out the less important fluctuations. +> +>注意:加速计负责记录当前施加到它身上的加速度。由于重力的作用iPhone 总是处于加速度的作用下(也因此 iPhone 总是知道哪个方向是屏幕的方向),但由于用户是用手拿着 iPhone(手并永远不会完全稳定在一个地方),因此重力会有细微波动。对于这些细微的波动我们不在乎,但比较大的改变就有可能是用户改变了设备的方向。通过一个简单的低通量过滤,我们可以只获取方向改变信息而过滤掉无关的波动。 + + +Now that you have a stable measurement of the device’s orientation, how can you use this to make the player’s spaceship move? + +现在我们已经让设备方向固定为一个,又如何让玩家的飞船移动呢? + +Motion in physics-based games is typically implemented like this: + +基于物理引擎的移动通常是这样实现的: + +* First, you set the acceleration, based on some form of user input (in this case the accelerometer values). + + 首先,基于用户输入(在这里就是加速计数据)改变加速度。 + +* Second, you add the new acceleration to the spaceship’s current velocity. This makes the object speed up or slow down, depending on the direction of the acceleration. + + 然后,将当前加速度加到飞船的当前速度中去。这会让飞船基于加速度的方向进行加速或减速。 + +* Finally, you add the new velocity to the spaceship’s position to make it move. + + 最终,用新的速度改变飞船的位置,使其移动。 + +You have a great mathematician to thank for the equations that control this motion: Sir Isaac Newton! + +在此,我们需要感谢一个伟大数学家艾萨克.牛顿,是他发明了这个位移公式! + +![](http://cdn1.raywenderlich.com/wp-content/uploads/2013/03/Newton1.png) + +You’ll need to add some more properties to track the velocity and acceleration. There is no need to keep track of the player’s position because the SKSpriteNode already does that for you. + +我们需要将速度和加速度保存到属性中。玩家位置是不需要跟踪的,因为 SKSpriteNode 已经保存了这个值。 + +> Note: Technically, Sprite Kit can keep track of velocity and acceleration as well, thanks to the SKPhysicsBody property. Sprite Kit’s physics can track forces on the sprite and update the acceleration, velocity and position automatically. But if you use Sprite Kit’s physics to do all the math, you won’t learn much about trigonometry! So, for this tutorial, you’re going to do all the math yourself. +> +> 注意:实际上,Sprite Kit 也会记录当前速度和加速度,这要用到 SKPhysicsBody 属性。Sprite Kit 的物理引擎会记录精灵所受的力,并自动更新加速度、速度和位置。但如果你要让 Sprite Kit 的物理引擎来进行这些计算,那你就无法学习三角学了。因此在本教程中,你将自己完成这些数学计算。 + +Add these properties to the class next: + +在这个类中增加如下属性: + +```swift +var playerAcceleration = CGVector(dx: 0, dy: 0) +var playerVelocity = CGVector(dx: 0, dy: 0) +``` + +It’s good to set some bounds on how fast the spaceship can travel or it would be pretty hard to maneuver. Unlimited acceleration would make the ship tricky to control (not to mention turning the poor pilot into jello!), so let’s set an upper limit. + +最好将飞船移动的速度做一个限制,否则飞船很难操控。不对加速度进行限制的话,将使飞船失控(让可怜的飞行员变成果冻!),因此,让我们来加一点限制。 + +Add the following lines directly below the import statements: + +直接在 import 语句后加入: + +```swift +let MaxPlayerAcceleration: CGFloat = 400 +let MaxPlayerSpeed: CGFloat = 200 +``` + +This defines two constants: The maximum acceleration (400 points per second squared), and the maximum speed (200 points per second). You’ve used the common Swift convention of capitalising the first letter of your configuration constants to distinguish them from regular “let” variables. + +这里我们新加了两个常量:最大加速度(400 像素/秒2),以及最大速度(200 像素/秒)。依照 Swift 一般约定,将两个常量的首字母大写,以区别于普通的 let 变量。 + +Now add the following code to the bottom of the if let ... statement in updatePlayerAccelerationFromMotionManager: + +在 updatePlayerAccelerationFromMotionManager 方法的 if let ... 一句的最后加入: + +```swift +playerAcceleration.dx = CGFloat(accelerometerY) * -MaxPlayerAcceleration +playerAcceleration.dy = CGFloat(accelerometerX) * MaxPlayerAcceleration +``` + +Accelerometer values are provided in the range -1 to +1, so to get the final acceleration, you simply multiply the accelerometer value by MaxPlayerAcceleration. + +加速计的取值范围一般是 -1 到 +1 之间,因此要获得最终的加速度,需要乘以最大加速度 MaxPlayerAcceleration。 + +> Note: You’re using the accelerometerY value for the x-direction and accelerometerX for the Y-direction. That’s as it should be. Remember that this game is in landscape, so the X-accelerometer runs from top to bottom in this orientation, and the Y-accelerometer from right to left. +> +> 注意:我们在 x 方向上用 accelerometerY 而在 y 方向上用 accelerometerX。这是正确的。注意这个游戏是横屏的,因此 x 方向的加速度是从上到下,y 方向上的加速度是从右到左。 + +You’re almost there. The last step is applying the playerAcceleration.dx and playerAcceleration.dy values to the velocity and position of the spaceship. You will do this from within the game’s update() method. This method is called once per frame (60 times per second), so it’s the natural place to perform all of the game logic. + +继续。接下来是将 playerAcceleration.x 和 playerAcceleration.dy 用到飞船的速度和位置上,这将放在 update() 方法中进行。这个方法每帧调用一次(即 60 次/秒)。因此这个地方是进行所有游戏逻辑的好地方。 + +Add the updatePlayer() method: + +新增一个 updatePlayer() 方法: + +```swift +func updatePlayer(dt: CFTimeInterval) { + + // 1 + playerVelocity.dx = playerVelocity.dx + playerAcceleration.dx * CGFloat(dt) + playerVelocity.dy = playerVelocity.dy + playerAcceleration.dy * CGFloat(dt) + + // 2 + playerVelocity.dx = max(-MaxPlayerSpeed, min(MaxPlayerSpeed, playerVelocity.dx)) + playerVelocity.dy = max(-MaxPlayerSpeed, min(MaxPlayerSpeed, playerVelocity.dy)) + + // 3 + var newX = playerSprite.position.x + playerVelocity.dx * CGFloat(dt) + var newY = playerSprite.position.y + playerVelocity.dy * CGFloat(dt) + + // 4 + newX = min(size.width, max(0, newX)); + newY = min(size.height, max(0, newY)); + + playerSprite.position = CGPoint(x: newX, y: newY) +} +``` + +If you’ve programmed games before (or studied physics), then this should look familiar. Here’s how it works: + +如果你以前编写过游戏(或者学过物理),这些代码看起来会很熟悉。这是它们的大致作用: + +1. This code adds the current acceleration to the velocity. + + 将当前加速度加到当前速度上。 + + The acceleration is expressed in points per second (actually, per second squared, but don’t worry about that). However, the update() method is executed a lot more often than once per second. To compensate for this difference, you multiply the acceleration by the elapsed or “delta” time, dt. Without this, the spaceship would move about sixty times faster than it should! + + 加速度以“像素/秒”为单位(实际上是秒2,但这无关紧要)。而 update() 方法执行的频率要远远大于“1次/秒”。因此,我们需要用加速度乘以 δ 时间(每帧所用的时间),即 dt。否则,飞船会比它理论上的速度要快 60 倍! + +2. This clamps the velocity so that it doesn’t go faster than MaxPlayerSpeed if it is positive or -MaxPlayerSpeed if it is negative. + + 将飞船的速度限制在 ± MaxPlayerSpeed 之内,如果飞船速度为负值,不得小于 ﹣ MaxPlayerSpeed,如果飞船速度为正,不得大于 + MaxPlayerSpeed。 + +3. This adds the current velocity to the sprite’s position. Again, velocity is measured in points per second, so you need to multiply it by the delta time to make it work correctly. + + 将当前速度加到位置计算中去。速度的单位是“像素点/秒”,因此需要将它乘以 δ 时间(dt),然后才能加到当前位置中去。 + +4. Clamp the new position to the sides of the screen. You don’t want the player’s spaceship to fly off-screen, never to be found again! + + 限制飞船的位置不要超出屏幕边沿。我们不想让飞船飞出屏幕以外,然后再也回不来了! + +One more thing: you need to measure time as differences (deltas) in time. The Sprite Kit update() method gets called repeatedly with the current time, but you’ll need to track the delta time between calls to the update() method ourselves, so that the velocity calculations are smooth. + +还有一件事情:你需要计算时间差(δ 时间 dt)。Sprite Kit 会重复调用 update() 方法并传入一个当前时间,因此速度计算是 OK 的。 + +To track the delta time, add another property: + +要记录 δ 时间,需要增加一个属性: + +```swift +var lastUpdateTime: CFTimeInterval = 0 +``` + +Then replace the update() method stub with the actual implementation: + +然后将 update 方法修改为: + +```swift +override func update(currentTime: CFTimeInterval) { + + // to compute velocities we need delta time to multiply by points per second + // SpriteKit returns the currentTime, delta is computed as last called time - currentTime + let deltaTime = max(1.0/30, currentTime - lastUpdateTime) + lastUpdateTime = currentTime + + updatePlayerAccelerationFromMotionManager() + updatePlayer(deltaTime) +} +``` + +That should do it. + +让我们看一下是怎么实现的。 + +You calculate deltaTime by subtracting the last recorded update time from the the current time. Just to be safe, clamp deltaTime to a maximum of 1/30th of a second. That way, if the app’s frame rate should fluctuate or stall for some reason, the ship won’t get catapulted across the screen when the next update occurs. + +用这一次 update() 方法调用的时间,减去上一次 update() 方法调用的时间,得到 δ 时间 dt。为了保险起见,将 dt 限制为最小不得小于 30 分之 1 秒。如果 App 的帧率因为某种原因变得波动较大的时候,飞船不至于在一帧之内突然就飞出屏幕。 + +The updatePlayerAccelerationFromMotionManager() method is called to calculate the players acceleration from the accelerometer values. + +调用 updatePlayerAccelerationFromMotionManager() 方法根据加速计的值计算玩家的加速度。 + +Finally, updatePlayer() is called to move the ship, making use of the delta time to compute the velocity. + +最后,调用 updaePlayer() 方法去移动飞船,将 dt 引入到移动速度的计算中去。 + +Build and run the game on an actual device (not the simulator). You can now control the spaceship by tilting the device: + +在真实设备上(不要在模拟器上)运行程序。现在你可以通过倾斜设备来控制飞船了: + +![](http://cdn2.raywenderlich.com/wp-content/uploads/2014/12/MovingShip-480x269.png) + +One last thing before you proceed: In GameViewController.swift, find the line: + +还剩最后一件事情:在 GameViewController.swift 中,找到这行: + +```swift +skView.ignoresSiblingOrder = true +``` + +And change it to: + +修改为: + +```swift +skView.ignoresSiblingOrder = false +``` + +This disables a small optimization in the way that sprites are rendered, but it means that sprites will be drawn in the order they are added. This will be useful later. + +这一句将 Sprite Kit 绘制精灵时的一个优化特性关闭。也就是说绘制精灵时,将按照精灵被加入的先后顺序进行绘制。这一点将在后面用到。 + +### Begin the Trigonometry! +### 开始三角计算 +If you skipped ahead to this section, here is the starter project at this point. Build and run it on your device – you’ll see there’s a spaceship that you can move around with the accelerometer. +You haven’t used any actual trigonometry yet, so let’s put some into action! + +如果你跳过了前面的内容,直接从这一节开始,请在[这里](http://cdn1.raywenderlich.com/wp-content/uploads/2015/04/TrigBlasterStarter.zip)下载开始项目。在你的设备上运行程序——你会看到一艘飞船,并可以用加速计来控制它移动。当然,这其中没有使用任何三角学的内容,因此接下来让我们开始这部分的内容! + +It would be cool – and much less confusing to the player – to rotate the spaceship in the direction it is currently moving rather than having it always pointing upward. + +我们有一个不错的想法——为了减少玩家的困惑——让飞船根据它当前运动的方向旋转,而不是一直将头朝向一个方向:正前方。 + +To rotate the spaceship, you need to know the angle to rotate it to. But you don’t know what that is; you only have the velocity vector. So how can you get an angle from a vector? + +要旋转飞船,要先计算出它应该旋转多少度。但你并不知道它是多少,你只有一个速度向量。通过这个向量能够得到一个角度吗? + +Let’s think about what you do know. The player’s velocity consists of two components: a length in the X-direction and a length in the Y-direction: + +让我们想一下,我们已知的条件。玩家的速度由两部分组成:一个 x 轴方向上的长度,和一个 y 方向上的长度: + +![](http://cdn1.raywenderlich.com/wp-content/uploads/2014/12/VelocityComponents-349x320.png) + +If you rearrange these a little, you can see that they form a triangle: + +如果你将它们重新排列一下,你就会发现这构成了一个三角形: + +![](http://cdn4.raywenderlich.com/wp-content/uploads/2014/12/VelocityTriangle-434x320.png) + +Here you know the lengths of the adjacent (playerVelocity.dx) and the opposite (playerVelocity.dy) sides. + +这里,邻边(playerVelocity.dx)的长和对边(playerVelocity.dy)的长是已知的。 + +So basically, you know 2 sides of a right triangle, and you want to find an angle (the Know 2 Sides, Need Angle case), so you need to use one of the inverse functions: arcsin, arccos or arctan. + +你已知直角三角形的两边,想知道一个夹角(这符合“已知两条边之长,求角的大小”),因此我们需要用到下列反三角函数之一:arcsin、arccos、arctan。 + +The sides you know are the opposite and adjacent sides to the angle you need, so you’ll want to use the arctan function to find the angle to rotate the ship. Remember, that looks like the following: + +因为我们求的是已知的两边边长之间的夹角,因此用 arctan 函数即可找出飞船旋转的角度。也就是: + +```swift +angle = arctan(opposite / adjacent) +``` + +The Swift standard library includes an atan() function that computes the arc tangent, but it has a couple of limitations: x / y yields exactly the same value as -x / -y, which means that you’ll get the same angle output for opposite velocities. Worse than that, the angle inside the triangle isn’t exactly the one you want anyway – you want the angle relative to one particular axis, which may be 90, 180 or 270 degrees offset from the angle returned by atan(). + +Swift 标注库中有一个计算反切的 atan() 函数,但它有几个限制:x 或 y 得到的结果和 -x 或 -y 是一样的,因此对于两个完全相反的速度向量来说,atan() 计算出来的角度是相同的。此外,这个角度也不是你最终想像的那样——你想计算的是实际上是相对于某个轴的相对角度,在 atan() 返回的结果上加上 90 、180 或者 270 度偏移角度后的角度。 + +You could write a four-way if-statement to work out the correct angle by taking into account the signs of the velocity components to determine which quadrant the angle is in, and then apply the correct offset. But it turns out there’s a much simpler way: + +你可以写一个四个分支的 if 语句,去计算正确的角度,将速度向量中的变量的符号也就是说向量所处的象限也考虑进去,然后再进行正确的偏移。但我们有一个更简单的解决方法: + +For this specific problem, instead of using atan(), it is simplier to use the function atan2(), which takes the x and y components as separate parameters, and correctly determines the overall rotation angle. + +对于这个问题,用 atan2() 函数要比用 atan() 函数要简单得多。atan2() 函数使用单独的 x 参数和 y 参数,并能够正确地判断整个旋转角度。 + +angle = atan2(opposite, adjacent) + +Add the following two lines to the bottom of updatePlayer: + +在 updatePlayer 方法最后加入这两句: + +```swift +let angle = atan2(playerVelocity.dy, playerVelocity.dx) +playerSprite.zRotation = angle +``` + +Notice that the Y-coordinate goes first. A common mistake is to write atan(x, y), but that’s the wrong way around. Remember the first parameter is the opposite side, and in this case the Y coordinate lies opposite the angle you’re trying to measure. + +注意首先传入 y 坐标。通常我们会写成 atan(x,y),但这错的。记住,第一个参数是对边,也就是这里的 y 坐标,位于我们想计算的角的正对面。 + +Build and run the app to try it out: + +运行程序,进行测试: + +![](http://cdn4.raywenderlich.com/wp-content/uploads/2014/12/ShipPointingWrongWay-480x269.png) + +Hmm, this doesn’t seem to be working quite right. The spaceship certainly rotates but it’s pointing in a different direction from where it’s flying! + +呃,有点不对劲。飞船是会转,但它指向的方向不是它正在飞行的方向! + +Here’s what’s happening: the sprite image for the spaceship points straight up, which corresponds to the default rotation value of 0 degrees. But by mathematical convention, an angle of 0 degrees doesn’t point upward, but to the right, along the X-axis: + +这是因为:飞船精灵的图片是指向上方的,默认的旋转角度是 0 度。但在数学中,0 度并不是指向上的,而是指向右的,即 X 轴的方向: + +![](http://cdn5.raywenderlich.com/wp-content/uploads/2014/12/RotationDifferences-480x220.png) + +To fix this, subtract 90 degrees from the rotation angle so that it matches up with the sprite image: + +为了解决这个问题,可以将旋转角度减去 90 度,以便和精灵图片的朝向相一致: + +playerSprite.zRotation = angle - 90 + +Try it out… + +你可以测试一下。 + +Nope! If anything, it’s even worse now! What’s missing? + +不!比刚才还要糟糕了!到底怎么回事? + +###Radians, Degrees and Points of Reference +###弧度、度和参考点 + +Normal humans tend to think of angles as values between 0 and 360 (degrees). Mathematicians, however, usually measure angles in radians, which are expressed in terms of π (the Greek letter Pi, which sounds like “pie” but doesn’t taste as good). + +正常情况下,人们总是习惯于将角度看成是 0-360 度的值。但在数学中,通常用弧度来作为角度的地位,也就是说用 π (希腊字母 pi,读“pie”,但却不能吃)来表达角度。 + +One radian is defines the angle you get when you travel the distance of the radius along the arc of the circle. You can do that 2π times (roughly 6.28 times) before you end up at the beginning of the circle again. + +一个弧度被定义为在圆上的一段长度和圆半径相等的弧所对应的角度。因此,如果要用这个线段(一个弧度长)测量整个圆的长度,就需要用反复测量 2π 次。 + +![](http://cdn2.raywenderlich.com/wp-content/uploads/2013/03/Radian.png) + +Notice the yellow line (the radius) is the same length as the red curved line (the arc). That magic angle where the two are equal is one radian! + +注意黄色的线段(半径)和红色的线段(圆弧)是等长。这个圆弧所夹的角度就是一个弧度! + +So while you may think of angles as values from 0 to 360, a mathematician sees values from 0 to 2π. Most computer math functions work in radians, because it’s a more useful unit for doing calculations. Sprite Kit uses radians for all its angular measurements as well. The atan2() function returns a value in radians, but you’ve tried to offset that angle by 90 degrees. + +当你用 3-360 °来衡量一个角度时,数学家却将它看成是 0-2π 。绝大部分数学函数都使用弧度,因为计算的时候弧度更方便一些。Sprite Kit 在测量角度时一律使用弧度。atan2() 函数的返回值也是弧度,但你却用它和 90 进行加减。 + +Since you will be working with both radians and degrees, it will be useful if you have a way to easily convert between them. The conversion is pretty simple: Since there are 2π radians or 360 degrees in a circle, π equates to 180 degrees, so to convert from radians to degrees you divide by π and multiply by 180. To convert from degrees to radians you divide by 180 and multiply by π. + +由于我们将同时使用弧度和度,因此将二者进行相互转换是很有必要的。转换非常简单:因为不管 2π 还是 360° 都是一个圆,π 就等于 180°,从弧度转换为度只需要除以 π 再乘以 180 即可。至于从度转换到弧度,则除以 180 再乘以 π 即可。 + +The C math library (which is automatically made available to Swift) has a constant, M_PI, that represents the value of π as a double. Swift’s strict casting rules make it inconvenient to use this constant when most of the values you’re dealing with are CGFloat, so you can just define your own constant. In GameScene.swift add the following to the top-level of the file, above the class definition: + +在 C 的数学库中(它在 Swift 中是自动包含的)有一个常量 M_PI,就代表了一个 π,类型为 Double。Swift 严格的类型转换规则使得这个常量并不是很好用,很多时候这个值需要被转换成 CGFloat,因此最好重新定义一个常量。在 GameScene.swift 的类的定义之外,在文件顶部添加下列声明: + +```swift +let Pi = CGFloat(M_PI) +``` + +Now define another two constants that will make it easy to convert between degrees and radians: + +然后定义两个常量,用于在度和弧度之间进行转换: + +```swift +let DegreesToRadians = Pi / 180 +let RadiansToDegrees = 180 / Pi +``` + +Finally, edit the rotation code in updatePlayer again, to include the DegreesToRadians multiplier: + +接下来在 updatePlayer 方法中修改旋转的代码,引入 DegreesToRadians 常量: + +```swift +playerSprite.zRotation = angle - 90 * DegreesToRadians +``` + +Build and run again and you’ll see that the spaceship is finally rotating correctly. + +运行程序,你将看到飞船终于正确地转向了。 + +### Bouncing Off the Walls +### 从墙壁上弹回 + +You have a spaceship that you can move using the accelerometers and you’re using trig to make sure it points in the direction it’s flying. That’s a good start. + +我们的飞船现在可以用加速计来控制移动了,同时我们通过三角计算让它在飞行的同时保持正确的方向。这开了一个很好头。 + +Having the spaceship get stuck on the edges of the screen isn’t very satisfying though. You’re going to fix that by making it bounce off the screen borders instead! + +让飞船在屏幕边沿卡住不动并不是一个很好的做法。我们它替换成:当它飞到屏幕边缘时,让它反弹回来! + +First, delete these lines from updatePlayer(): + +首先将 upatePlayer() 方法中的这几行删除: + +```swift +// 4 +newX = min(size.width, max(0, newX)) +newY = min(size.height, max(0, newY)) +``` + +替换为: + +```swift +And replace them with the following: +var collidedWithVerticalBorder = false +var collidedWithHorizontalBorder = false + +if newX < 0 { + newX = 0 + collidedWithVerticalBorder = true +} else if newX > size.width { + newX = size.width + collidedWithVerticalBorder = true +} + +if newY < 0 { + newY = 0 + collidedWithHorizontalBorder = true +} else if newY > size.height { + newY = size.height + collidedWithHorizontalBorder = true +} +``` + +This checks whether the spaceship hit any of the screen borders, and if so, sets a Bool variable to true. But what to do after such a collision takes place? To make the spaceship bounce off the border, you can simply reverse its velocity and acceleration. +Add the following lines to updatePlayer(), directly below the code you just added: + +这段代码片段飞船是否飞到了屏幕的边沿,如果是,将一个布尔变量设置为 true。当这样的碰撞发生后会怎样?让飞船从边缘弹回,你可以直接将速度向量和加速度向量取反。在 updatePlayer() 方法中继续添加: + +```swift +if collidedWithVerticalBorder { + playerAcceleration.dx = -playerAcceleration.dx + playerVelocity.dx = -playerVelocity.dx + playerAcceleration.dy = playerAcceleration.dy + playerVelocity.dy = playerVelocity.dy +} + +if collidedWithHorizontalBorder { + playerAcceleration.dx = playerAcceleration.dx + playerVelocity.dx = playerVelocity.dx + playerAcceleration.dy = -playerAcceleration.dy + playerVelocity.dy = -playerVelocity.dy +} +``` + +If a collision is registered, you invert the acceleration and velocity values, causing the ship to bounce away again. + +如果碰撞发生,将加速度和速度反向,让飞船从墙上弹开。 + +Build and run to try it out. + +运行程序,进行测试。 + +Hmm, the bouncing works, but it seems a bit energetic. The problem is that you wouldn’t expect a spaceship to bounce like a rubber ball – it should lose most of its energy in the collision, and bounce off with less velocity than it had beforehand. + +呃,弹是会弹了,只不过看起来有点过于灵敏了。问题是你并不想让飞船像一只橡皮球一样弹来弹去——每次碰撞后它都会消耗掉一些能量,因此经过碰撞之后速度会比之前的要小。 + +Define another constant at the top of the file, right below the let MaxPlayerSpeed: + +另外定义一个常量,就放在 let MaxPlayerSpeed: CGFloat = 200 之后: + +```swift +let BorderCollisionDamping: CGFloat = 0.4 +``` + +Now, replace the code you just added in updatePlayer with this: + +现在,将 updatePlayer 方法中刚才新加的代码修改为: + +```swift +if collidedWithVerticalBorder { + playerAcceleration.dx = -playerAcceleration.dx * BorderCollisionDamping + playerVelocity.dx = -playerVelocity.dx * BorderCollisionDamping + playerAcceleration.dy = playerAcceleration.dy * BorderCollisionDamping + playerVelocity.dy = playerVelocity.dy * BorderCollisionDamping +} + +if collidedWithHorizontalBorder { + playerAcceleration.dx = playerAcceleration.dx * BorderCollisionDamping + playerVelocity.dx = playerVelocity.dx * BorderCollisionDamping + playerAcceleration.dy = -playerAcceleration.dy * BorderCollisionDamping + playerVelocity.dy = -playerVelocity.dy * BorderCollisionDamping +} +``` + +You’re now mutliplying the acceleration and velocity by a damping value, BorderCollisionDamping. This allows you to control how much energy is lost in the collision. In this case, you make the spaceship retain only 40% of its speed after bumping into the screen edges. + +现在,我们将加速度和速度乘以了一个衰减系数 BorderCollisionDamping。这样就可以让能量在碰撞后有所损失。当飞船撞上屏幕边沿之后只保留原来速度的 40%。 + +For fun, play with the value of BorderCollisionDamping to see the effect of different values for this constant. If you make it larger than 1.0, the spaceship actually gains energy from the collision! + +如果你有兴趣,可以修改 BorderCollisionDamping 的值,看看效果会有什么不同。如果你将值改成大于 1 的数,则飞船甚至可以从碰撞中获得能量! + +You may have noticed a slight problem: Keep the spaceship aimed at the bottom of the screen so that it continues smashing into the border over and over, and you’ll see that it starts to stutter between pointing up and pointing down. + +你会注意到还有一个小问题:如果你将飞船瞄准屏幕底部,让它反复不停地撞向屏幕边沿,则它会在向上和向下的方向之间打转。 + +Using the arc tangent to find the angle between a pair of X and Y components works quite well, but only if those X and Y values are fairly large. In this case, the damping factor has reduced the speed to almost zero. When you apply atan2() to very small values, even a tiny change in these values can result in a big change in the resulting angle. + +用 arctan 函数计算 x 和 y 组件之间的夹角是 OK 的,但这个 X 和 Y 值必须足够大。在这里,由于衰减系数的存在,速度被降低到接近于 0。当我们用 atan2() 计算飞船小的 x 和 y 值时,一个很小的波动就会导致算出的角度出现非常大的改变。 + +One way to fix this is to not change the angle when the speed is very slow. That sounds like an excellent reason to give a call to your old friend, Pythagoras. + +一个办法是当速度变得很低时,就不要改变角度了。嗯,是该打个电话问候下我们的老朋友毕达哥拉斯(勾股定理的发明者)了。 + +![](http://cdn3.raywenderlich.com/wp-content/uploads/2013/04/pythagoras-480x177.png) + +Right now you don’t actually store the ship’s speed. Instead, you store the velocity, which is the vector equivalent (see here for an explanation of the difference between speed and velocity), with one component in the X-direction and one in the Y-direction. But in order to draw any conclusions about the ship’s speed (such as whether it’s too slow to be worth rotating the ship) you need to combine these X and Y speed components into a single scalar value. + +事实上我们保存的并不是飞船的 speed(快慢)。我们保存的是飞船的 velocity (速度),它是一个向量(关于 speed 和 velocity 的区别,请看[这里](https://en.wikipedia.org/wiki/Velocity#Distinction_between_speed_and_velocity)),速度有两个组件构成,一个 x 方向上的速度,一个 y 方向上的速度。但为了表达最终这个飞船的速度有多快(比如它是否慢到不需要飞船转向),我们需要将速度的 x 组件和 y 组件合并成一个单个的标量值。 + +![](http://cdn3.raywenderlich.com/wp-content/uploads/2014/12/Pythagoras-405x320.png) + +Here you are in the Know 2 Sides, Need Remaining Side case, discussed earlier. + +这就是前面我们讲过的“已知三角形两边之长,求第三边之长。” + +As you can see, the true speed of the spaceship – how many points it moves across the screen per second – is the hypotenuse of the triangle that is formed by the speed in the X-direction and the speed in the Y-direction. + +如图所示,飞船真正的速度是——它每秒钟在屏幕上移动的像素——即屏幕上三角形的斜边,它又是由 x 方向上的速度和 y 方向上的速度构成。 + +Put in terms of the Pythagorean formula: + +使用毕达哥拉斯公式(勾股定理)就是: + +true speed = √(playerVelocity.dx2 + playerVelocity.dy2) + +真实速度 = √(playerVelocity.dx2 + playerVelocity.dy2) + +Remove this block of code from updatePlayer(): + +从 updatePlayer() 中删除以下代码: + +```swift +let angle = atan2(playerVelocity.dy, playerVelocity.dx) +playerSprite.zRotation = angle - 90 * DegreesToRadians +``` + +And replace it with this: + +替换成以下代码: + +```swift +let RotationThreshold: CGFloat = 40 + +let speed = sqrt(playerVelocity.dx * playerVelocity.dx + playerVelocity.dy * playerVelocity.dy) +if speed > RotationThreshold { + let angle = atan2(playerVelocity.dy, playerVelocity.dx) + playerSprite.zRotation = angle - 90 * DegreesToRadians +} +``` + +Build and run. You’ll see the spaceship rotation seems a lot more stable at the edges of the screen. If you’re wondering where the value 40 came from, the answer is: experimentation. Putting NSLog() statements into the code to look at the speeds at which the craft typically hit the borders helped in tweaking this value until it felt right :] + +运行程序。现在飞船在碰到边缘后的转向变得稳定了。如果你奇怪 40 这个值是怎么来的,我的回答是“经验值”。在代码中通过 "NSLog()" 语句打印飞船撞到墙上的速度值,然后不停地调整这个值,一直到你觉得可以就行了。 + +### Blending Angles for Smooth Rotation +### 平滑转向 + +Of course, fixing one thing always breaks something else. Try slowing down the spaceship until it has stopped, then flip the device so the spaceship has to turn around and fly the other way. + +但是,解决一个问题的同时又会带来别的问题。让飞船慢慢减速,直至停止。然后翻转设备,让飞船转向并向另一个方向飞行。 + +Previously, that would happen with a nice animation where you actually saw the ship turning. But because you just added some code that prevents the ship from changing its angle at low speeds, the turn is now very abrupt. It’s only a small detail, but it’s the details that make great apps and games. + +如果是在之前,你会看到一个漂亮的转向动画。但因为我们添加了防止飞船在低速下改变方向的代码,现在的转向会变得非常突然。这只是一个小问题,但这个问题关系到我们能否制作出一个好的 App 和游戏。 + +The fix is to not switch to the new angle immediately, but to gradually blend it with the previous angle over a series of successive frames. This re-introduces the turning animation and still prevents the ship from rotating when it is not moving fast enough. +This “blending” sounds fancy, but it’s actually quite easy to implement. It will require you to keep track of the spaceship’s angle between updates, however, so add a new property for it in the implementation of the GameScene class: + +解决办法是不要立马将方向切换到新的角度,而是在每一帧逐步“混合渗入”新角度和旧角度。这种方式不但重新生成了转向动画而且仍然能够防止飞船在低速下转向。“混合渗入”听起来很神奇,但实际上却不难实现。但是它需要你记录下飞船每帧的角度,因此我们要在 GameScene 类中新增一个属性: + +```swift +var playerAngle: CGFloat = 0 +``` + +Update the rotation code in updatePlayer() to this: + +将 updatePlayer() 中的转向代码修改为: + +```swift +let RotationThreshold: CGFloat = 40 +let RotationBlendFactor: CGFloat = 0.2 + +let speed = sqrt(playerVelocity.dx * playerVelocity.dx + playerVelocity.dy * playerVelocity.dy) +if speed > RotationThreshold { + let angle = atan2(playerVelocity.dy, playerVelocity.dx) + playerAngle = angle * RotationBlendFactor + playerAngle * (1 - RotationBlendFactor) + playerSprite.zRotation = playerAngle - 90 * DegreesToRadians +} +``` + +The playerAngle variable combines the new angle and its own previous value by multiplying them with a blend factor. In human-speak, this means the new angle only contributes 20% towards the actual rotation that you set on the spaceship. Over time, more and more of the new angle gets added so that eventually the spaceship points in the correct direction. + +playerAngle 变量包含了用混合系数乘以新角度和上一帧的角度。也就是说新的角度只占飞船实际转向的 20% 的份额。随着时间的增长,越来越多的新角度被累加进去,直到飞船最终指向了正确的方向。 + +Build and run to verify that there is no longer an abrupt change from one rotation angle to another. + +运行程序,测试飞船从一个方向转到另一个方向时不会再显得突兀。 + +![](http://cdn2.raywenderlich.com/wp-content/uploads/2013/03/Rage-end-of-it.png) + +Now try flying in a circle a couple of times, both clockwise and counterclockwise. You’ll notice that at some point in the turn, the spaceship suddenly spins 360 degrees in the opposite direction. It always happens at the same point in the circle. What’s going on? + +现在,飞出几个圆环,反时针和顺时针都试一试。你会看到在圆环的某些点上,飞船会突然反方向旋转 360°。这种现在总是出现在圆环上的某几个位置。这是怎么回事? + +The atan2() returns and angle between +π and –π (between +180 and -180 degrees). That means that if the current angle is very close +π, and then it turns a little further, it’s going to wrap around to -π (or vice-versa). + +atan2() 返回一个 +π 到 -π (+180°到-180°)之间的角度。也就是说如果当前角度接近 +π 时,并在转动过程中转过了一小点,那么他会反过来转到 -π(反之亦然)。 + +That’s actually equivalent to the same position on the circle (just like -180 and +180 degrees are the same point), but your blending algorithm isn’t smart enough to realise that – it thinks the angle has jumped a whole 360 degrees (aka 2π radians) in one step, and it needs to spin the ship 360 degrees in the opposite direction to catch back up. + +这两个位置实际上是同一个位置( -180 和 +180 在圆上是同一个位置),但混合算法还不够智能,没有意识到这点——它认为角度整个改变了 360 度(2π 弧度),因此飞船做了反方向旋转 360°。 + +![](http://cdn4.raywenderlich.com/wp-content/uploads/2013/03/atan2-range.png) + +To fix it, you need to recognize when the angle crosses that threshold, and adjust playerAngle accordingly. Add a new property to the GameScene class: + +要解决这个问题,需要知道什么时候角度超过了阀值,并适当地调整 playerAngle。在 GameScene 类中添加一个新属性: + +```swift +var previousAngle: CGFloat = 0 +``` + +And change the rotation code one more time to this: + +然后再一次修改旋转代码为: + +```swift +let speed = sqrt(playerVelocity.dx * playerVelocity.dx + playerVelocity.dy * playerVelocity.dy) +if speed > RotationThreshold { + let angle = atan2(playerVelocity.dy, playerVelocity.dx) + + // did angle flip from +π to -π, or -π to +π? + if angle - previousAngle > Pi { + playerAngle += 2 * Pi + } else if previousAngle - angle > Pi { + playerAngle -= 2 * Pi + } + + previousAngle = angle + playerAngle = angle * RotationBlendFactor + playerAngle * (1 - RotationBlendFactor) + playerSprite.zRotation = playerAngle - 90 * DegreesToRadians +} +``` + +Now you’re checking the difference between the current angle and the previous angle to watch for changes over the thresholds of 0 and π (180 degrees). + +这里,我们判断当前和之前的角度之差,看是否超过了这个阀值:0 到 π(180°)。 + +Build and run. That’ll fix things right up and you should have no more problems turning your spacecraft! + +运行程序。这样飞船的转向就不再有任何问题了。 + +### Using Trig to Find Your Target +### 用三角学发现目标 + +This is a great start – you have a spaceship moving along pretty smoothly! But so far the little spaceship’s life is too easy and carefree. Let’s spice things up by adding an enemy: a big cannon! + +我们有了一个很好的开始——我们拥有了一艘能够灵活飞行的飞船。但这艘飞船的日子未免也太舒服、太一帆风顺了。给它添点刺激怎么样?我们将为它增加一个敌人:一挺大炮! + +Add two new properties to the GameScene class: + +在 GameScene 类中加入两个属性: + +```swift +let cannonSprite = SKSpriteNode(imageNamed: "Cannon") +let turretSprite = SKSpriteNode(imageNamed: "Turret") +``` + +You’ll set these sprites up in didMoveToView(). Place this code before the setup for playerSprite, so that the spaceship always gets drawn after (and therefore in front of) the cannon: + +我们将在 didMoveToView() 方法中加入这两个角色。将代码放到创建 playSprite 之前,以便在飞船出现之前大炮就已经存在了: + +```swift +cannonSprite.position = CGPoint(x: size.width/2, y: size.height/2) +addChild(cannonSprite) + +turretSprite.position = CGPoint(x: size.width/2, y: size.height/2) +addChild(turretSprite) +``` + +> Note: Remember that change you made to set skView.ignoresSiblingOrder = false earlier? That ensures that sprites are drawn in the order they are added to their parent. There are other ways to control sprite drawing order – such as using the zPosition – but this is the simplest. +> +> 注意:还记得我们之前写的 skView.ignoresSiblingOrder=false 一句吗?这句代码让精灵按照它们添加到场景的先后顺序绘制。虽然还可以用别的方式来决定精灵绘制的顺序——比如使用 zPosition 属性——但我们采用的是最简单的方法。 + +The cannon consists of two sprites: the fixed base, and the turret that can rotate to take aim at the player. Build and run, and you should see a brand-new cannon sitting smack in the middle of the screen. + +大炮由两部分构成:一个固定不动的底座,以及一个会旋转瞄向玩家的炮塔。运行程序,你会看到一座全新的大炮坐落在屏幕的中央。 + +![](http://cdn3.raywenderlich.com/wp-content/uploads/2014/12/Cannon-480x269.png) + +Now to give the cannon a target to snipe at! + +给大炮一个攻击的目标吧! + +You want the cannon’s turret to point at the player at all times. To get this to work, you’ll need to figure out the angle between the turret and the player. + +我们想让大炮的炮塔随时都能指向玩家。要达到这个目的,我们需要计算出炮塔和玩家之间的角度。 + +Figuring this out will be very similar to how you calculated how to rotate the spaceship to face the direction it’s moving in. The difference is that this time, the triangle won’t be derived from the velocity of the spaceship; instead, it will be drawn between the centers of the two sprites: + +这个计算和让飞船转向前进方向的计算差不多。不同的是这个三角形不是用飞船的速度来构成,而是用飞船和大炮之间的连线来构成: + +![](http://cdn5.raywenderlich.com/wp-content/uploads/2013/03/Triangle-between-cannon-and-player.png) + +Again, you can use atan2() to calculate this angle. Add the following method: + +我们仍然可以用 atan2() 来计算这个角度。添加一个新方法: + +```swift +func updateTurret(dt: CFTimeInterval) { + + let deltaX = playerSprite.position.x - turretSprite.position.x + let deltaY = playerSprite.position.y - turretSprite.position.y + let angle = atan2(deltaY, deltaX) + + turretSprite.zRotation = angle - 90 * DegreesToRadians +} +``` + +The deltaX and deltaY variables measure the distance between the player sprite and the turret sprite. You plug these values into atan2() to get the relative angle between them. + +deltaX 和 deltaY 变量表示了玩家和炮塔之间的距离。将这两个值代入到 atan2() 中,就可以得到它们之间的夹角。 + +As before, you need to convert this angle to include the offset from the X-axis (90 degrees) so the sprite is oriented correctly. Remember that atan2() always gives you the angle between the hypotenuse and the 0-degree line; it’s not the angle inside the triangle. + +同前次一样,我们需要将这个角度偏转到 X 轴方向(90°),以使炮塔的方向正确。注意,atan2() 只会返回一个由斜线和 0 度线构成的夹角,而不是三角形的内角。 + +Finally, add a call this new method. Find update() and add the following code to the end of that method: + +然后来调用这个方法。在 update() 方法中的最后一句加上: + +```swift +updateTurret(deltaTime) +``` + +Build and run. The turret will now always point toward the spaceship. See how easy that was? That’s the power of trig for you! + +运行程序,炮塔会自动对着飞船。很简单是吧?这就是三角学的威力! + +![](http://cdn3.raywenderlich.com/wp-content/uploads/2014/12/TurretTrackingPlayer-480x269.png) + +> Challenge: It is unlikely that a real cannon would be able to move instantaneously – it would have to be able to predict exactly where the target was going. Instead, it would always be playing catch up, trailing the position of the ship slightly. +> +> 挑战:实际上真正的大炮是不会瞬移的——它实际是预判目标下一个位置在哪里。它总是追赶着目标,略略地尾随着飞船的位置。 +> +> You can accomplish this by “blending” the old angle with the new one, just like you did earlier with the spaceship’s rotation angle. The smaller the blend factor, the more time the turret needs to catch up with the spaceship. See if you can implement this on your own. +> +> 要实现这个,我们可以用新角度和老角度进行“混合”,正如我们先前在飞船转向的过程中所做的一样。混合系数越小,炮塔瞄准飞船所需要的时间就越长。你可以试一下,看能否独立实现这个功能。 + + +### Adding Health Bars +### 加入血槽 + +In part 2, you’ll add code to let player fire missiles at the cannon, and the cannon will be able to inflict damage on the player. To show the amount of hit points each object has remaining, you will need to add some health bar sprites to the scene. Let’s do that now. + +在第二部分,你将实现玩家向大炮开火的功能,而大炮也可以给飞船造成损坏。要显示二者剩余的生命值,我们需要为角色添加血槽。让我们开始吧。 + +Add the following new constants to the top of the GameScene.swift file: + +在 GameScene.swift 中添加如下常量: + +```swift +let MaxHealth = 100 +let HealthBarWidth: CGFloat = 40 +let HealthBarHeight: CGFloat = 4 +``` + +Also, add these new properties to the GameScene class: + +在 GameScene 类中加入如下新属性: + +```swift +let playerHealthBar = SKSpriteNode() +let cannonHealthBar = SKSpriteNode() +var playerHP = MaxHealth +var cannonHP = MaxHealth +``` + +Now, insert the following code into didMoveToView(), just before startMonitoringAcceleration(): + +在 didMoveToView() 方法中,在 startMonitoringAcceleration() 一句前插入: + +```swift +addChild(playerHealthBar) +addChild(cannonHealthBar) + +cannonHealthBar.position = CGPoint( + x: cannonSprite.position.x, + y: cannonSprite.position.y - cannonSprite.size.height/2 - 10 +) +``` + +The playerHealthBar and cannonHealthBar objects are SKSpriteNodes, but you haven’t specified an image to display for them. Instead, you will be drawing the health bar images dynamically using Core Graphics. + +playerHealthBar 和 cannonHealthBar 都是 SKSpriteNode 对象,但我们没有为它们指定任何图片。相反,我们将用 Core Graphics 动态地为它们绘制血槽。 + +Note that you placed the cannonHealthBar sprite slightly below the cannon, but didn’t assign a position to the playerHealthBar yet. That’s because the cannon never moves, so you can simply set the position of its health bar once and forget about it. + +注意,我们将 cannonHealthBar 放到大炮稍下一点的位置,但却没有指定 playerHealthBar 所在的位置。因为大炮不会动,只需要设置一次它的位置就可以了。 + +Whenever the spaceship moves though, you’ll have to adjust the position of the playerHealthBar as well. That happens in updatePlayer. Add these lines to the bottom of that method: + +而飞船是在不停运动着的,我们必须随时修改 playerHealthBar 的位置。这个动作应当在 updatePlayer 中完成。在这个方法的最后加入: + +```swift +playerHealthBar.position = CGPoint( + x: playerSprite.position.x, + y: playerSprite.position.y - playerSprite.size.height/2 - 15 +) +``` + +Now all that’s left is to draw the bars themselves. Add this new method to the class: + +剩下是就是绘制血槽自身了。在这个类中新加一个方法: + +```swift +func updateHealthBar(node: SKSpriteNode, withHealthPoints hp: Int) { + + let barSize = CGSize(width: HealthBarWidth, height: HealthBarHeight); + + let fillColor = UIColor(red: 113.0/255, green: 202.0/255, blue: 53.0/255, alpha:1) + let borderColor = UIColor(red: 35.0/255, green: 28.0/255, blue: 40.0/255, alpha:1) + + // create drawing context + UIGraphicsBeginImageContextWithOptions(barSize, false, 0) + let context = UIGraphicsGetCurrentContext() + + // draw the outline for the health bar + borderColor.setStroke() + let borderRect = CGRect(origin: CGPointZero, size: barSize) + CGContextStrokeRectWithWidth(context, borderRect, 1) + + // draw the health bar with a colored rectangle + fillColor.setFill() + let barWidth = (barSize.width - 1) * CGFloat(hp) / CGFloat(MaxHealth) + let barRect = CGRect(x: 0.5, y: 0.5, width: barWidth, height: barSize.height - 1) + CGContextFillRect(context, barRect) + + // extract image + let spriteImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + // set sprite texture and size + node.texture = SKTexture(image: spriteImage) + node.size = barSize +} +``` + +This code draws a single health bar. First it sets up the fill and border colors, then it creates a drawing context, and draws two rectangles: the border, which always has the same size, and the bar itself, which varies in width depending on the number of hit points. The method then generates a UIImage from the drawing context, and assigns it as the texture for the sprite. + +这段代码绘制了一个血槽。首先设定填充色和边框色,然后创建图形上下文,绘制两个方框:一个用作血槽的边框,它总是固定大小,另一个是血条,它是会变的,要看生命的点数。这个方法从上下文中返回一个 UIImage 并赋给 Sprite 的 texture 属性。 + +You need to call this method twice, once for the player and once for the cannon. Because redrawing the health bar is relatively expensive (Core Graphics drawing isn’t hardware accelerated), you don’t want to do it every frame. Instead, you’ll call this code only when the player’s or cannon’s health changes. For now, you’ll call it just once to set the initial appearance for the bars. + +我们需要调用这个方法两次,一次是针对玩家对象,一次是针对大炮。因为绘制血槽的代价相对昂贵(Core Graphics 绘图不使用硬件加速),因此我们不想在帧刷新时绘制。相反,我们只应该在玩家或者大炮的生命值被改变的时候绘制。暂时,我们只调用它一次,用于显示血槽满血的状态。 + +Add the following code to the end of didMoveToView: + +在 didMoveToView 方法最后加入: + +```swift +updateHealthBar(playerHealthBar, withHealthPoints: playerHP) +updateHealthBar(cannonHealthBar, withHealthPoints: cannonHP) +``` + +Build and run. Now, both the player and the cannon have health bars: + +运行程序,现在玩家和大炮都有了血槽: + +![](http://cdn4.raywenderlich.com/wp-content/uploads/2014/12/HealthBars-480x269.png) + +### Using Trig for Collision Detection +### 用三角学进行碰撞检测 + +Right now, the spaceship can fly directly through the cannon without consequence. It would be more challenging (and realistic) if it suffered damage when colliding with the cannon. This is where you enter the sphere of collision detection (sorry about the pun! :]) + +暂时,飞船直接从大炮身上飞过不会导致任何后果。假设让飞船在和大炮发生碰撞后造成一定的伤害,则效果要更刺激(和更真实)一些。现在可以把你扔到碰撞检测范围内试一试了(不好意思,开个玩笑了 :])。 + +At this point, a lot of game devs would think, “I need a physics engine!” and while it’s certainly true that you can use Sprite Kit’s physics for this, it’s not that hard to do collision detection yourself, especially if you model the sprites using simple circles. + +这里,有许多游戏开发者会说:“我需要使用物理引擎!”。当然,你可以用 Sprite Kit 的物理引擎来做这个,但要自己实现碰撞检测其实一点都不难,尤其是如果你的精灵使用了简单的圆形建模时。 + +Detecting whether two circles intersect is a piece of cake: all you have to do is calculate the distance between them (*cough* Pythagoras) and see if it is smaller than the sum of the radii (or “radiuses” if you prefer) of both circles. + +检测两个圆形是否相交其实很简单:你只需要计算二者之间的距离(*咳咳* 勾股定理),然后判断是否小于二者半径之和(或者两个半径)。 + +![](http://cdn5.raywenderlich.com/wp-content/uploads/2013/03/Collision-detection.png) + +Add two new constants to the top of GameScene.swift: + +在 GameScene.swift 顶部加入两个新常量: + +```swift +let CannonCollisionRadius: CGFloat = 20 +let PlayerCollisionRadius: CGFloat = 10 +``` + +These are the sizes of the collision circles around the cannon and the player. Looking at the sprite, you’ll see that the actual radius of the cannon image in pixels is slightly larger than the constant you’ve specified (around 25 points), but it’s nice to have a bit of wiggle room; you don’t want your games to be too unforgiving, or players won’t have fun. + +这是大炮和玩家的碰撞环的大小。查看一下精灵位图,大炮图片的大小实际上要比这里指定的值要略大(25 像素),不过保留一点缓冲空间是好的,我们不准备让这个游戏过于苛求,否则玩家就毫无乐趣可言了。 + +The fact that the spaceship isn’t circular at all shouldn’t deter you. A circle is often a good enough approximation for the shape of an arbitrary sprite, and it has the big advantage that it makes it much simpler to do the trig calculations. In this case, the body of the ship is roughly 20 points in diameter (remember, the diameter is twice the radius). + +事实上,飞船也根本不是圆形也没有关系。对于各种形状的精灵来说,使用圆形模拟都是不错的,而且这样做还有一个好处,即使三角计算更加简单。这里,飞船的直径约 20 像素(直径是半径的两倍)。 + +Add a new method to the class to do the collision detection: + +新增一个方法用于碰撞检测: + +```swift +func checkShipCannonCollision() { + + let deltaX = playerSprite.position.x - turretSprite.position.x + let deltaY = playerSprite.position.y - turretSprite.position.y + + let distance = sqrt(deltaX * deltaX + deltaY * deltaY) + if distance <= CannonCollisionRadius + PlayerCollisionRadius { + runAction(collisionSound) + } +} +``` + +You’ve seen how this works before: first you calculate the distance between the X-positions of the two sprites, then the Y-positions. Treating these two values as the sides of a right triangle, you can then calculate the hypotenuse, which is the true distance between these sprites. + +首先算出两个精灵间的 x 和 y 距离,将 x 和 y 当成是直角三角形的两条边就可以算出斜边,这就是二者间的直线距离。 + +If that distance is smaller than the sum of the collision radii, play a sound effect. You’ll see an error on that line will error for now, because you haven’t added the sound effect code yet – it’s coming soon, so just be patient! + +如果这个距离小于两个碰撞半径之和,播放生效。这个地方会报一个错误,因为我们还没有实现声效代码——耐心一点,待会实现。 + +Add a call to this new method at the end of update(): + +在 update() 最后添加: + +```swift +checkShipCannonCollision() +``` + +Then, add this property to the top of the GameScene class: + +在 GameScene 类顶部新加一个属性: + +```swift +let collisionSound = SKAction.playSoundFileNamed("Collision.wav", waitForCompletion: false) +``` + +Time to build and run again. Give the collision logic a whirl by flying the spaceship into the cannon. + +运行程序,将飞船飞到炮塔上方测试碰撞逻辑是否正确。 + +![](http://cdn5.raywenderlich.com/wp-content/uploads/2014/12/Overlap-480x269.png) + +Notice that the sound effect plays endlessly as soon as a collision begins. That’s because, while the spaceship flies over the cannon, the game registers repeated collisions, one after another. There isn’t just one collision, there are 60 per second, and it plays the sound effect for every one of them! + +注意当碰撞发生时,声效播放起来就没完没了。因为当飞船飞过大炮时,会检测到多次碰撞,一个接一个。不仅仅是一个碰撞,而是每秒 60 次碰撞发生了,而每次碰撞都会播放一次声效。 + +Collision detection is only the first half of the problem. The second half is collision response. Not only do you want audio feedback from the collision, but you also want a physical response – the spaceship should bounce off the cannon. + +碰撞检测只是一方面的问题,另外一方面的问题是碰撞反应。我们不但要在碰撞时播放声效,也想有一个物理反应——飞船会从大炮上弹开。 + +Add this constant to the top of the GameScene.swift file: + +在 GameScene.swift 文件顶部添加一个常量: + +```swift +let CollisionDamping: CGFloat = 0.8 +``` + +Then add these lines inside the if statement in checkShipCannonCollision(): + +然后在 checkShipCannonCollision() 的 if 语句内加入以下语句: + +```swift +playerAcceleration.dx = -playerAcceleration.dx * CollisionDamping +playerAcceleration.dy = -playerAcceleration.dy * CollisionDamping +playerVelocity.dx = -playerVelocity.dx * CollisionDamping +playerVelocity.dy = -playerVelocity.dy * CollisionDamping +``` + +This is very similar to what you did to make the spaceship bounce off the screen borders. Build and run to see how it works. + +就像我们让飞船从屏幕边沿弹开一样。运行程序进行测试。 + +It looks pretty good if the spaceship is going fast when it hits the cannon. But if it’s moving too slowly, then even after reversing the speed, the ship sometimes stays within the collision radius and never makes its way out of it. Clearly, this solution has some problems. + +如果飞船在撞上大炮时飞得很快,这个方法没有什么问题。如果速度很慢,哪怕它从反方向弹开,飞船仍然会有一段时间处于碰撞半径之内,甚至再也无法离开。显然,这个办法也有问题。 + +Instead of just bouncing the ship off the cannon by reversing its velocity, you need to physically push the ship away from the cannon by adjusting its position so that the radii no longer overlap. + +如果不将速度取反来弹开飞船,则我们可以通过改变飞船的位置让它离开碰撞半径,真正地将飞船从大炮身上推开, + +To do this, you’ll need to calculate the vector between the cannon and the spaceship, which, fortunately, you already calculated earlier in order to measure the distance between them. So how do you use that distance vector to move the ship? + +这需要计算大炮和飞船之间的向量,幸运的是,为了计算二者之间的距离,我们已经在前面计算过这个了。那么,如何利用距离向量去移动飞船? + +The vector formed by deltaX and deltaY is already pointing in the right direction, but it’s the wrong length. The length you need it to be is the difference between the radii of the ships and its current length – that way, when you add it to the ship’s current position, the ship will no longer be overlapping the cannon. + +这个向量由一个 deltaX 和一个 deltaY 构成,并且指向了正确的方向,但它的长度是不对的。我们需要的长度是碰撞半径和当前长度之差——这样,我们将可以将这个长度加到飞船当前位置,飞船就不再和大炮发生交叠了。 + +The current length of the vector is distance, but the length that you need it to be is: + +当前向量的长度是 distance,而我们需要将它的长度变成: + +CannonCollisionRadius + PlayerCollisionRadius – distance + +So how can you change the length of a vector? + +如何改变一个向量的长度? + +The solution is to use a technique called “normalization”. You normalize a vector by dividing the X and Y components by the current scalar length (calculated using Pythagoras). The resultant “normal” vector, has an overall length of one. + +办法是使用“向量规范化”。通过将向量的 x 和 y 分别除以向量长度(用勾股定理),就可以对这个向量进行规范化。规范化向量之后,向量的长度就变成了 1。 + +Then, you just multiply the X and Y by the desired length to get the offset for the spaceship. Add the following code immediately underneath the previous lines you added: + +然后,将 x 和 y 乘以上面计算出来的长度,就得到飞船需要移动的距离。在上几行代码后面加入: + +```swift +let offsetDistance = CannonCollisionRadius + PlayerCollisionRadius - distance + +let offsetX = deltaX / distance * offsetDistance +let offsetY = deltaY / distance * offsetDistance +playerSprite.position = CGPoint( + x: playerSprite.position.x + offsetX, + y: playerSprite.position.y + offsetY +) +``` + +Build and run, and you’ll see the spaceship now bounces properly off the cannon. + +运行程序,你将发现飞船能够从大炮上正确地弹开了。 + +To round off the collision logic, you’ll subtract some hit points from the spaceship and the cannon, and update the health bars. Add the following code inside the if statement: + +除了碰撞逻辑,我们还需要让飞船和大炮“掉一些血”,并刷新血槽。在 if 语句中加入: + +```swift +playerHP = max(0, playerHP - 20) +cannonHP = max(0, cannonHP - 5) +updateHealthBar(playerHealthBar, withHealthPoints: playerHP) +updateHealthBar(cannonHealthBar, withHealthPoints: cannonHP) +``` + +Build and run again. The ship and cannon now lose a few hit points each time they collide. + +运行程序,飞船和大炮发生碰撞后都会损失一些生命点。 + +![](http://cdn3.raywenderlich.com/wp-content/uploads/2014/12/Damage-480x269.png) + +### Adding Some Spin +### 碰撞偏移 + +For a nice effect, you can add some spin to the spaceship after a collision. This is additional rotation that doesn’t influence the flight direction; it just makes the effect of the collision more profound (and the pilot more dizzy). Add a new constant to the top of GameScene.swift: + +为使效果更好看,我们可以让飞船在碰撞后发生一些旋转。这些旋转是额外的,不影响飞行的飞行;仅仅是使碰撞效果更显眼一点(飞行员头会更晕)。在 GameScene.swift 顶部加入一个新常量: + +```swift +let PlayerCollisionSpin: CGFloat = 180 +``` + +This sets the amount of spin to half a circle per second, which I think looks pretty good. Now add a new property to the GameScene class: + +设置旋转的速度为每秒半圈就足够了。在 GameScene 类中加入一个新属性: + +```swift +var playerSpin: CGFloat = 0 +``` + +In checkShipCannonCollision(), add the following line inside the if statement: + +在 checkShipCannonCollision() 中,在 if 语句中加入: + +```swift +playerSpin = PlayerCollisionSpin +``` + +Finally, add the following code to updatePlayer(), immediately before the line playerSprite.zRotation = playerAngle - 90 * DegreesToRadians: + +然后,在 updatePlayer() 中,就在 playerSprite.zRotation = playerAngle - 90 * DegreesToRadians 一句之前加入: + +```swift +if playerSpin > 0 { + + playerAngle += playerSpin * DegreesToRadians + previousAngle = playerAngle + playerSpin -= PlayerCollisionSpin * CGFloat(dt) + if playerSpin < 0 { + playerSpin = 0 + } +} +``` + +The playerSpin effectively just overrides the display angle of the ship for the duration of the spin, without affecting the velocity. The amount of spin quickly decreases over time, so that the ship comes out of the spin after one second. While spinning, you update previousAngle to match the spin angle, so that the ship doesn’t suddenly snap to a new angle after coming out of the spin. + +playerSpin 用于表示碰撞偏移过程中飞船偏移的角度,不计算速度的影响。偏移角度会随时间递减,因此飞船在一秒后停止偏移。在碰撞偏移过程中,我们修改 previousAngle 的值,使其和偏移角度匹配,这样飞船才不会在偏移结束时突然转到一个新的角度。 + +Build and run and set that ship spinning! + +运行程序,查看飞船碰撞偏移的效果。 + +### Where to Go from Here? +### 接下来做什么 + +Here is the full example project from the tutorial up to this point. + +[这里](http://cdn5.raywenderlich.com/wp-content/uploads/2015/04/TrigBlasterPart1.zip)是教程中使用到的完整示例项目。 + +Triangles are everywhere! You’ve seen how you can use this fact to breathe life into your sprites with the various trigonometric functions to handle movement, rotation and even collision detection. + +一切都是三角形!通过三角函数我们处理移动、旋转和碰撞侦测的问题,从而使我们的精灵具备了生命! + +You have to admit, it wasn’t that hard to follow along, was it? Math doesn’t have to be boring if you can apply it to fun projects, such as making games! + +我们不得不承认,其实这些并不难学习。数学,如果将它用到有趣的事情上比如制作游戏是,就不会那么索然无味了! + +But there’s more to come: in Part 2 of this Trigonometry for Game Programming series, you’ll add missiles to the game, learn more about sine and cosine, and see some other useful ways to put the power of trig to work in your games. + +但我们还有更多的内容需要学习:在本教程的第二部分,你将在游戏中加入导弹,学习更多关于 sin 和 cos 的知识,学些更多在游戏中三角学的不同用途。 + +Credits: The graphics for this game are based on a free sprite set by Kenney Vleugels. The sound effects are based on samples from freesound.org. + +声明:游戏中使用的图片来自于 Kenney Vleugels,声音来自于 freesound.org。 diff --git "a/issue-20/Sprite Kit \346\270\270\346\210\217\344\270\255\347\232\204\344\270\211\350\247\222\345\255\246(2) .md" "b/issue-20/Sprite Kit \346\270\270\346\210\217\344\270\255\347\232\204\344\270\211\350\247\222\345\255\246(2) .md" new file mode 100644 index 0000000..41292f0 --- /dev/null +++ "b/issue-20/Sprite Kit \346\270\270\346\210\217\344\270\255\347\232\204\344\270\211\350\247\222\345\255\246(2) .md" @@ -0,0 +1,960 @@ +> * 原文链接 : [Trigonometry for Games – Sprite Kit and Swift Tutorial: Part 2/2](http://www.raywenderlich.com/90521/trigonometry-games-sprite-kit-swift-tutorial-part-2) +* 原文作者 : [Nick Lockwood](http://www.raywenderlich.com/u/nicklockwood) +* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn) +* 译者 : [kmyhy](https://github.com/kmyhy) + +Learn Trigonometry for Game Programming! + +游戏编程中的三角学! + +Update 04/20/2015: Updated for Xcode 6.3 and Swift 1.2. + +2015年4月20日 更新:更新到 Xcode 6.3 和 Swift 1.2。 + +Update Note: This is the third incarnation of one of our very popular tutorials – the first version was written by Tutorial Team member Matthijs Hollemans for Cocos2D, and the second version was update to Sprite Kit by Tony Dahbura. This latest version still uses Sprite Kit, but is updated for iOS 8 and Swift. + +更新说明:这是我们广受欢迎的教程之一的第三个版本——第一个版本是 Cocos2D 的,由 Matthijs Hollemans 缩写,第二个版本由 Tony Dahbura 升级为 Sprite Kit。最终的版本仍然是 Sprite Kit 的,但升级至 iOS 8 和 Swift。 + +Welcome back to the Trigonometry for Game Programming series! + +欢迎回到游戏编程中的三角学系列! + +In the first part of the series, you learned the basics of trigonometry and saw for yourself how useful it can be for making games. Math doesn’t have to be boring – as long as you have a fun project to apply it to! + +在这个系列的第一部分,我们学习了基本的三角学知识,它在游戏制作中是非常有用的。数学并不令人讨厌——尤其当你将它放到一个充满趣味的项目中! + +In this second and final part of the series, you will extend your simple space game by adding missiles, an orbiting asteroid shield and an animated “game over” screen. Along the way, you’ll learn more about the sine and cosine functions, and see some other useful ways to put the power of trig to work in your games. + +在第二部分,我们会在这个简单的飞行游戏中加入导弹,一个轨道式卫星护盾以及一个写着的“游戏结束”的动画界面。在这个过程中,你将学习 sin 和 cos 函数,以及其它在游戏中用得到的三角学工具。 + +Get ready to ride the sine wave back into space! + +准备乘上正弦波的快车,回到太空吧! + +##Getting Started +##开始 + +This tutorial picks up where you left off in the last part. If you don’t have it already, here is the project with all of the source code up to this point. + +这个教程和上一部分的内容一脉相承。如果你还没有看过第一部分,你可以从[这里](http://cdn5.raywenderlich.com/wp-content/uploads/2015/04/TrigBlasterPart1.zip)下载第一部分的源代码。 + +As of right now, your game has a spaceship and a rotating cannon, each with health bars. While they may be sworn enemies, neither has the ability to damage the other, unless the spaceship flies right into the cannon (which works out better for the cannon, damage-wise). + +目前,在游戏中有一艘飞船、一座旋转炮台,它们都会有血槽显示。它们是不共戴天的仇敌,但谁也不能把谁怎么样,除非飞船硬要撞到炮台上(炮台的损失要相对好一点)。 + +It’s time for some fireworks. You will now give the player the ability to fire missiles by swiping the screen. The spaceship will launch a missile in the direction of the swipe. For that, you’ll need to add some new properties for tracking touches. + +是时候放点烟火了。我们将增加一个功能,让玩家通过划动屏幕来发射导弹。划动的发现就是导弹发射的方向。要记录用户触摸,我们需要增加新的属性。 + +Open GameScene.swift and add the following properties to the class: + +打开 GameScene.swift ,然后添加如下属性: + +```swift +let playerMissileSprite = SKSpriteNode(imageNamed:"PlayerMissile") + +var touchLocation = CGPointZero +var touchTime: CFTimeInterval = 0 +``` + +You’ll move the missile sprite from the player’s ship in the direction it’s facing. You’ll use the touch location and time to track where and when the user taps on the screen to trigger a missile. + +我们将从飞船前进的方向开始移动导弹。我们用 touchLocation 和 touchTime 记录用户触摸的位置和时间,以便发射导弹。 + +Then, add these lines at the bottom of the code inside didMoveToView(): + +然后,在 didMoveToView() 方法底部加入几行: + +```swift +playerMissileSprite.hidden = true +addChild(playerMissileSprite) +``` + +Note that the missile sprite is hidden initially; you’ll only make it visible when the player fires. To increase the challenge, the player will only be able to have one missile in flight at a time. + +注意,导弹一开始是隐藏的,只有玩家发射它之后才可见。为了降低难度,玩家一次只能发射一颗导弹。 + +The first touch handling method to add is touchesBegan(), which will be called whenever the user first puts their finger on the touchscreen: + +首先添加第一个触摸事件处理方法 touchesBegan()。这个方法会在用户将手指放到屏幕上之后调用: + +```swift +override func touchesBegan(touches: Set, withEvent event: UIEvent) { + + if let touch = touches.first as? UITouch { + + let location = touch.locationInNode(self) + touchLocation = location + touchTime = CACurrentMediaTime() + } +} +``` + +This is pretty simple – whenever a touch is detected, you store the touch location and the time. The actual work happens in touchesEnded(), which you’ll add next: + +非常简单——当触摸被检测到,保存当前触摸的位置和时间。真正的工作放到 touchesEnded() 方法中进行: + +```swift +override func touchesEnded(touches: Set, withEvent event: UIEvent) { + + let TouchTimeThreshold: CFTimeInterval = 0.3 + let TouchDistanceThreshold: CGFloat = 4 + + if CACurrentMediaTime() - touchTime < TouchTimeThreshold && playerMissileSprite.hidden { + + if let touch = touches.first as? UITouch { + + let location = touch.locationInNode(self) + let swipe = CGVector(dx: location.x - touchLocation.x, dy: location.y - touchLocation.y) + let swipeLength = sqrt(swipe.dx * swipe.dx + swipe.dy * swipe.dy) + if swipeLength > TouchDistanceThreshold { + // TODO: more code here + } + } + } +} +``` + +The outer if statement calculates the time between starting and ending the swipe. If the time is greater than the TouchTimeThreshold value of 0.3 seconds, the missile doesn’t fire. Also, since the player can only shoot one missile at a time, the touch is ignored if a missile is already flying. + +外层的 If 语句计算从扫动开始到结束的时间差。如果这个时间差大于 TouchTimeThreshold 值(0.3秒),导弹将被发射。同时,因为玩家一次只能发射一个导弹,如果导弹还在飞行中,则触摸被忽略。 + +The next part works out what sort of gesture the user made; was it really a swipe, or just a tap? You should only launch missiles on swipes, not taps. You have done this sort of calculation a couple of times already – subtract two coordinates, then use the Pythagorean Theorem to find the distance between them. If the distance is greater than the TouchDistanceThreshold value of 4 points, treat it as an intentional swipe. + +接下来计算用户在做什么手势,是轻扫,还是轻击?只有在轻扫的时候才应该发射导弹,而不是轻击。我们花了一点时间来计算这个——将两个位置相减,然后用勾股定理计算它们之间的距离。如果距离大于 TouchDistanceThreshold(4 point),则认为用户想轻扫。 + +> Note:You could have used UIKit’s built in gesture recognizers for this, but the aim here is to understand how trigonometry is used behind the scenes to implement this kind of logic. +> +> 注意:在 UIKit 中有内置的轻扫手势识别器,但我们的教程是学习三角学知识,因此这里用这种方式来识别。 + +There are two ways you could make the missile fly: + +要让导弹飞起来有两种方式: + +The first option would be to create a playerMissileVelocity vector, based on the angle that you’re aiming the missile. Inside update(), you would then add this velocity (multiplied by the time delta) to the missile sprite’s position each frame, and check if the missile has flown outside of the visible screen area, so that it can be reset. This is very similar to how you made the spaceship move in Part 1 of this tutorial. + +第一种方式是创建一个 playerMissileVelocity 向量,根据你瞄准的方向来创建。在update() 方法中,我们可以在每一帧将这个速度(乘以时间差)施加到导弹精灵上,然后判断导弹是否飞出了屏幕边沿以外,以便让导弹重置。这就好比第一部分中我们让飞船移动的方式。 + +However, unlike the spaceship, the missile never changes course; it always flies in a straight line. So you can take a simpler approach: +The second option is to calculate the final destination of the missile in advance, when you launch it. Then you can use a moveTo() action on the missile sprite and let Sprite Kit animate it to it’s final position. + +但和飞船不同,导弹是不需要改变飞行方向的,它成直线飞行。因此我们可以用一个更简单的方法:第二种方式是导弹发射后,计算导弹的终点。然后用 moveTo() 动作,让 Sprite Kit 将它以动画方式移动到终点。 + +This saves you from having to check whether the missile has left the visible screen, and is also an opportunity to do some more interesting math! + +这种方式不需要我们判断导弹是否在屏幕可视区域内,同时也可以做一些更有趣的数学题! + +To begin, replace the // TODO comment in touchesEnded() with the following code: + +首先,将 touchesEnded() 方法中 // TODO 注释替换为: + +```swift +var angle = atan2(swipe.dy, swipe.dx) +playerMissileSprite.zRotation = angle - 90 * DegreesToRadians + +playerMissileSprite.position = playerSprite.position +playerMissileSprite.hidden = false +``` + +Here, you use atan2() to convert the swipe vector to an angle, set the sprite’s rotation and position, and make the missile sprite visible. + +这里我们使用 atan2() 将 swipe 向量转换为角度,然后设置精灵的角度和位置,让导弹不再隐藏。 + +Now, however, comes the interesting part. You know the starting position of the missile (which is the current position of the player’s ship) and you know the angle (derived from the player’s swipe motion). What you need to calculate now is the destination point of the missile based on these facts. + +然后是好玩的地方了。我们知道导弹的开始位置(玩家飞船的当前位置),也知道角度(来自于玩家扫动动作)。我们需要算出终点。 + +You already have the direction vector, and you learned in Part 1 how to use normalization to set the length of a vector to whatever you need. But what length do you want? Well that’s the challenging bit: Because you want the missile to stop when it moves outside the screen border, the length it travels depends on the starting position and direction. + +我们有了方向向量,从第一部分中学到如何用规范化将向量的长度设置为指定长度。但我们怎么知道这个长度是多少?这个问题问得好:因为我们想让导弹一移到屏幕以外就停下来,这个长度应该取决于起点和方向。 + +![](http://cdn4.raywenderlich.com/wp-content/uploads/2013/03/Missile-destination.png) + +The destination point always lies just outside the screen instead of exactly on the edge of the screen, because it looks better if the missile completely flies out of sight before it vanishes. For this purpose, add another constant at the top of GameScene.swift: + +终点是在屏幕以外,而不是整好屏幕的边缘,因为要让导弹完全飞出屏幕才能让它消失。因此,在GameScene.swift 中增加一个常量: + +```swift +let PlayerMissileRadius: CGFloat = 20 +``` + +Finding the destination point is a bit complicated. If you know that (for example) the player is shooting downward, you can work out the vertical distance the missile needs to fly – that is simply the starting Y-position of the missile, plus the PlayerMissileRadius, but you then need to calculate the X component by determining where the missile will intersect that invisible line. + +终点的计算有点复杂。如果你知道(假设)玩家正在朝下射击,你可以算出导弹需要飞行的垂直距离——导弹起点的 Y 坐标,加上 PlayerMissileRaius,但是我们同时还要根据导弹在哪里和可视边界相交来算出 X 坐标。 + +For missiles that fly off the bottom or top edges of the screen the X component of the destination can be calculated with the following formula: + +对于飞出屏幕顶部或底部的导弹,终点的 x 坐标可以用下面的公式来计算: + +destination.x = playerPos.x + ((destination.y – playerPos.y) / swipe.dy * swipe.dx) + +This is similar to the normalization technique from Part 1, where you scaled up a vector by first dividing both components by the current length, and then multiplying by the desired length. Here, you work out the ratio of the swipe vector’s Y component to the final distance, then multiply the X component by the same value and add it to the ship’s current X position in order to get the destination X coordinate. + +这和第一部分教程中的规范化类似,先将两个直角边除以长度,在乘以需要的长度。这里,先用 swipe.y 组件与最终距离的 y 相除算出一个系数,然后用这个系数乘以 scale.x (即算出三角形的对边),然后加上飞船的 x 坐标,得到终点的 x 坐标。 + +For missiles that go off the left or right edges, you’d use essentially the same function, but just swap all the X and Y values. +This technique of extending a vector until it hits an edge is known as projection, and it’s very helpful for all sorts of game applications, such as detecting if an enemy can see the player by projecting a vector along their line of sight and seeing if it hits a wall first, or the player. + +如果导弹从左边或右边飞出屏幕,我们可以使用同样的公式,但 x 和 y 值要调换一下。这种将一个向量拉伸到某个边界的技术,被称作“投影”,它对于所有类型的游戏都非常有用,比如要判断敌人是否能够看到玩家时,就可以将一个向量沿敌人的视线进行投影,一直投射到某堵墙或者玩家。 + +There’s a snag, however. If the intersection point is near a corner, it’s not obvious which edge the missile will intersect first: + +但是这种技术有一点问题。如果交点是一个角(或角的附近),则不好判定是哪一边先发生相交: + +![](http://cdn1.raywenderlich.com/wp-content/uploads/2014/12/DestinationPoints.png) + +That’s OK; you’ll just calculate both intersection points, then see which is the shorter distance from the player! + +很简单,你可以同时计算两个交点,看哪一个更短! + +Add the following code immediately beneath the playerMissileSprite.hidden = false line you added to touchesEnded: + +在 touchesEnded 方法的 playerMissileSprite.hidden = false 一句下面加入: + +```swift +//calculate vertical intersection point +var destination1 = CGPoint.zeroPoint +if swipe.dy > 0 { + destination1.y = size.height + PlayerMissileRadius // top of screen +} else { + destination1.y = -PlayerMissileRadius // bottom of screen +} +destination1.x = playerSprite.position.x + + ((destination1.y - playerSprite.position.y) / swipe.dy * swipe.dx) + +//calculate horizontal intersection point +var destination2 = CGPoint.zeroPoint +if swipe.dx > 0 { + destination2.x = size.width + PlayerMissileRadius // right of screen +} else { + destination2.x = -PlayerMissileRadius // left of screen +} +destination2.y = playerSprite.position.y + + ((destination2.x - playerSprite.position.x) / swipe.dx * swipe.dy) +``` + +Here, you’re calculating the two candidate destination points for the missile; now you need to work out which is nearer to the player. Add the following code next, below the code you just wrote: + +这里,我们计算了俩个交点,我们要找出哪个交点距离玩家更近。因此,在上一段代码之后,紧接着用下面的代码进行判断: + +```swift +// find out which is nearer +var destination = destination2 +if abs(destination1.x) < abs(destination2.x) || abs(destination1.y) < abs(destination2.y) { + destination = destination1 +} +``` + +You could have used the Pythagorean theorem here to work out the diagonal distance from the player to each intersection point, and chosen the shortest, but there’s a quicker way: you know that since the two possible intersection points lie along the same vector, if either the X or Y component is shorter then the distance as a whole must be shorter – so there’s no need to calculate the diagonal lengths. + +你可以用勾股定理算出从玩家到每个交点的对角线距离,然后选择最短的。但还有一个更快的方法:我们知道两个交点都位于同一个向量,则只要点的 x 或者 y 两者中有一个比对方短,则它的长度也应该是较短的——其实并不需要计算对角线长度。 + +Now to fire the missile! +Add this last piece of code to touchesEnded(), immediately after the code you just wrote: + +现在开始发射导弹! +在 touchesEnded() 方法中,紧接着上一段代码之后,加入: + +```swift +// run the sequence of actions for the firing +let missileMoveAction = SKAction.moveTo(destination, duration: 2) +playerMissileSprite.runAction(missileMoveAction) { + self.playerMissileSprite.hidden = true +} +``` + +Build and run the app. You can now swipe to shoot bolts of plasma at the turret. Pretty neat, huh? Note that you can only fire one missile at a time – you have to wait until the previous missile has disappeared from the screen before firing again. + +运行程序,轻扫向炮台开火。很好。注意你一次只能发射一枚导弹——要等上一枚导弹消失,才能再次开火。 + +![](http://cdn4.raywenderlich.com/wp-content/uploads/2013/03/Shooting-missile.png) + +There’s still one problem (isn’t there always?) — The missile appears to travel faster or slower depending on the distance it travels. +That’s because, currently, the duration of the animation is hard-coded to last 2 seconds. If the missile needs to travel further, then it goes faster in order to cover more distance in the same amount of time. It would be more realistic if the missiles always travelled at a consistent speed. + +还有一个问题(就知道是这样!)——导弹的速度会根据它飞行的距离变化。那是因为当前动画时长是硬编码的(2秒)。导弹飞得越远,当然就速度越快,这样才能在同样的时间下飞过更远的距离嘛。如果导弹以匀速飞行好像更理想一点。 + +Your good friend Sir Isaac Newton can help out here! As Newton discovered, time = distance / speed. You can use Pythagoras to calculate the distance, so there’s just the matter of specifying the speed. + +你的好朋友艾萨克.牛顿又来帮忙了!牛顿发明了“时间 = 距离/速度”公式。你可以通过勾股定律算出距离,因此问题就只剩下指定一个速度了。 + +Add another constant to the top of GameScene.swift: + +在 GameScene.swift 头部加入常量: + +```swift +let PlayerMissileSpeed: CGFloat = 300 +``` + +This is the distance that you want the missile to travel each second. Now, replace the last block of code you added with this new version: + +这是我们指定的让导弹在 1 秒内必须移动的距离。现在,将最后添加的那段代码修改为: + +```swift +// calculate distance +let distance = sqrt(pow(destination.x - playerSprite.position.x, 2) + + pow(destination.y - playerSprite.position.y, 2)) + +// run the sequence of actions for the firing +let duration = NSTimeInterval(distance / PlayerMissileSpeed) +let missileMoveAction = SKAction.moveTo(destination, duration: duration) +playerMissileSprite.runAction(missileMoveAction) { + self.playerMissileSprite.hidden = true +} +``` + +This time, instead of hard-coding the duration, you’ve derived it from the distance and speed by using Newton’s formula. Run the app again and you’ll see that the missile now always flies at the same speed, no matter how far or close the destination point is. + +这回,我们将硬编码的动画时长替换成牛顿位移公式计算出来的距离除以速度。运行程序,你将发行导弹的飞行速度是固定的,无论终点位于何方。 + +And that’s how you use trig to set up a moveTo() action. It’s a bit involved, but then it’s largely fire & forget because Sprite Kit does all the work of animating for you. + +接下来是创建一个 moveTo() 动作。虽然有点麻烦,但你一旦创建了动作之后就不用操心了,因为 Sprite Kit 会自动为你完成剩下的动画工作。 + +![](http://cdn5.raywenderlich.com/wp-content/uploads/2014/01/meme_not_bad.png) + +### Hitting Your Targets +### 命中目标 + +Right now, the missile completely ignores the cannon – your spaceship might as well be firing a beam of green light! + +现在,导弹完全就无视于炮台的存在——飞船除了发出一束绿光之外什么也不做! + +That’s about to change. As before, you will use a simple radius-based method of collision detection. You already added a PlayerMissileRadius constant, so you’re all set to detect cannon/missile collisions using the same technique as you used for the cannon/ship collision. + +让我们来解决它。之前,我们使用了一种简单的基于碰撞半径的检测方法。我们也增加了一个 PlayerMissileRadius 常量,因此我们可以用同样的方法来检测导弹和炮台之间的碰撞,正如飞船和炮台之间的碰撞一样。 + +新增一个方法: + +```swift +func checkMissileCannonCollision() { + + if !playerMissileSprite.hidden { + + let deltaX = playerMissileSprite.position.x - turretSprite.position.x + let deltaY = playerMissileSprite.position.y - turretSprite.position.y + + let distance = sqrt(deltaX * deltaX + deltaY * deltaY) + if distance <= CannonCollisionRadius + PlayerMissileRadius { + + playerMissileSprite.hidden = true + playerMissileSprite.removeAllActions() + + cannonHP = max(0, cannonHP - 10) + updateHealthBar(cannonHealthBar, withHealthPoints: cannonHP) + } + } +} +``` + +This works pretty much the same as checkShipCannonCollision(): you calculate the distance between the sprites, and consider it a collision if that distance is less than the sum of the radii. + +这个方法就像 checkShipCannoCollision() 方法一样,首先计算二个精灵之间的距离,当距离小于二者碰撞半径之和时,则可认为碰撞有效。 + +If the collision is detected, first hide the missile sprite and cancel its animations; then reduce the cannon’s hit points, and redraw its health bar. Simple! + +如果碰撞有效,首先隐藏导弹,取消它的动画;然后扣减炮台的生命点,重新绘制它的血槽。简单! + +Add a call to checkMissileCannonCollision() inside the update() method, immediately after the other updates: + +在 update() 方法中,调用 checkMissileCannonCollisoin() 方法: + +```swift +checkMissileCannonCollision() +``` + +Build and run, then try it out. Finally you can inflict some damage on the enemy! + +运行程序,你会发现你可以对敌人造成伤害了! + +![](http://cdn1.raywenderlich.com/wp-content/uploads/2014/01/inflicting_damage-700x338.png) + +Before moving on, it would be nice if the missile had some sound effects. As with the ship-turret collision before, you can play sounds with a Sprite Kit action. Add the following two properties to GameScene: + +这里,最好给导弹一些音效。就像在之前的飞船-炮台碰撞中所做的一样,我们可以使用 Sprite Kit 动作来播放声音。在 GameScene 中增加两个属性: + +```swift +var missileShootSound: SKAction! +var missileHitSound: SKAction! +`` +Set them up in didMoveToView():` + +在 didMoveToView() 方法中初始化它们: + +```swift +missileShootSound = SKAction.playSoundFileNamed("Shoot.wav", waitForCompletion: false) +missileHitSound = SKAction.playSoundFileNamed("Hit.wav", waitForCompletion: false) +``` + +Now, change the line playerMissileSprite.runAction(missileMoveAction) in touchesEnded() to: + +在 touchesEnded() 方法中,将 playerMissileSprite.runAction(missileMoveAction) 一行替换为: + +```swift +playerMissileSprite.runAction(SKAction.sequence([missileShootSound, missileMoveAction])) { +``` + +Rather than a single action to move the missile, you’re setting up a sequence to play the sound and then move the missile. + +与之前执行单个的 move 动作不同,我们创建了一个序列用于在移动导弹的同时播放声音。 + +And, finally, add the following line to checkMissileCannonCollision(), at the bottom of the inner if statement: + +最终,在 checkMissileCannonCollision() 方法中,在内层 if 语句内的最后加入: + +```swift +runAction(missileHitSound) +``` + +That should do it: The missile now shoots out with a ZZAPP sound, and – if your aim is true – hits the turret with a satisfying BOINK! + +这就是我们所做的:导弹发射时播放“嗖嗖”的音效,如果你瞄得准——并且打到了炮塔,则播放“嘣”的爆炸声。 + +### Challenges for the 1337 +### 挑战 + +Here’s a challenge for you: can you make the cannon shoot back at the spaceship? + +这里,我们给你留了一个作业:你能够让炮台向飞船还击吗? + +You already know all the required pieces, and this will be some really good practice to make sure you understand what we’ve covered so far. Try it out for yourself before you look at the spoiler! + +你已经学习了每一个有用的知识点,为了加深理解,最好来一场真枪实弹的练习。在你尽最大努力之前,请不要翻看答案! + +> Solution Inside: Make the cannon shoot back +> 参考答案:让炮台进行还击 +> +> To implement this on your own, all you have to do is create a new missile sprite (using CannonMissile.png), calculate the destination, and send the missile on its way. You know the angle because the turret already points at the player. The destination point for the moveTo() action is obviously the position of the player at that moment. +> 你所需要做的就是创建一个新的炮弹精灵(使用 CannonMissile.png 图片),计算距离,然后发送炮弹。你已经知道发射角度了,因为炮塔指向的方向就是玩家。moveTo() 动作的终点就是当前的玩家位置。 +> +> Collision detection with the player works the same way as before: the missile has hit as soon as the distance between the missile and the player becomes less than the sum of their radii. To make the game extra challenging for the player, allow the cannon to shoot more than one missile at a time (hint: you’ll probably want to set up an array of missile sprites). +> +> 碰撞检测和之前的方法一样:当炮弹和玩家之间的距离小于二者的碰撞半径之和,则认为炮弹打中了玩家。为了让游戏有一定的难度,炮台能够一次发射多枚炮弹(提示:应该用数组来保存炮弹精灵!)。 + +Got that done already? Think you’re pretty hot stuff, huh? Here’s another challenge for you! + +完成了吗?觉得自己干的还不错吧?接下来还有一个挑战! + +Currently, your missiles fly to their destination point in a straight line. But what if the missiles were heat-seeking? A heat-seeking missile adjusts its course when it detects that the player has moved. + +当前,当前炮弹是以直线飞行。但如果炮弹是热辐射自导引的呢?热辐射自导引的炮弹会根据玩家的位置自动调整飞行路线。 + +You’ve got the power of trig on your side, so how would you do it? Hint: instead of calculating the speed and direction of the missile just once, you would do it again and again on each frame, as you do with the ship. + +你已经学会了使用三角学来解决问题,那么到底该怎么做呢?提示:与之前只计算一次炮弹的速度和方向不同,我们应该在每一帧都计算它们,就像飞船的飞行一样。 + +> Solution Inside: Make the missile heat seeking +> 参考答案:能够热辐射自导引的炮弹 +> +> Since you’re calculating the speed and direction of the missile on each frame, you can no longer use a moveTo() action. Instead, you have to do the animation by yourself. Continuously adjust the speed of the missile based on the new angle that it makes with the player. To read more about this sort of “seeking” behavior, check out the Game AI tutorial. +> 因为需要在每一帧都计算炮弹的速度和飞行方向,我们不能再使用 moveTo() 动作。我们必须自定义动画。不断地根据玩家位置来调整炮弹的速度。要了解更多关于“追寻”的行为,请参考[游戏 AI 教程](http://www.raywenderlich.com/?p=24824)。 +> +> Make sure to give the guided missile a limited lifetime, so the player can avoid it if they keep dodging it long enough, or the game might become a bit too hard to play! +> 为自导引炮弹加上点时间限制,如果玩家躲避的时间足够长就能躲开这个炮弹,否则游戏就太难玩了。 + +How’d you do? Is your spaceship dodging guided missiles like Tom Cruise, or still flying around scot-free? +完成得怎么样?你的飞船是像汤姆.克鲁斯一样在躲避炮弹,还是已经安然逃脱了? + +### Adding an Orbiting Shield +### 加上轨道防御盾 + +To make the game more challenging, you will give the enemy a shield. The shield will be a magical asteroid sprite that orbits the cannon and destroys any missiles that come near it. + +为了让我们的游戏更有挑战性,我们将给敌人一个防御盾。这个防御盾是一个小行星,围绕着炮台旋转并抵御住一切试图靠近它的导弹。 + +Add a couple more constants to the top of GameScene.swift: + +在 GameScene.swift 头部新增如下常量: + +```swift +let OrbiterSpeed: CGFloat = 120 +let OrbiterRadius: CGFloat = 60 +let OrbiterCollisionRadius: CGFloat = 20 +``` + +And some new properties to GameScene: + +在 GameScene 中增加几个属性: + +```swift +let orbiterSprite = SKSpriteNode(imageNamed:"Asteroid") +var orbiterAngle: CGFloat = 0 +``` + +Initialize the new sprite inside didMoveToView(), after the other sprites: + +在 didMoveToView() 方法中,初始化新角色: + +```swift +addChild(orbiterSprite) +``` + +Then, add the following new method to GameScene: + +然后,在 GameScene 中加入以下方法: + +```swift +func updateOrbiter(dt: CFTimeInterval) { + + // 1 + orbiterAngle = (orbiterAngle + OrbiterSpeed * CGFloat(dt)) % 360 + + // 2 + let x = cos(orbiterAngle * DegreesToRadians) * OrbiterRadius + let y = sin(orbiterAngle * DegreesToRadians) * OrbiterRadius + + // 3 + orbiterSprite.position = CGPointMake(cannonSprite.position.x + x, cannonSprite.position.y + y) +} +``` + +The asteroid will orbit around the cannon. In other words, it describes a circular path, round and round and round and round. To accomplish this, you need two pieces: the radius that determines how far the asteroid is from the center of the cannon, and the angle that describes how far it has rotated around that center point. + +防御盾会围绕炮台旋转。也就是说,它作圆形轨道运动,一圈接一圈,周而复始。要做到这个,你需要两点:半径——它是防御盾距离炮台中心的距离,角度——它是防御盾沿圆心所旋转的距离。 + +This is what updateOrbiter() does: + +在 updateOrbiter() 方法中,我们做了这些事情: + +1. It increments the angle by a certain speed (from the OrbiterSpeed constant), adjusted for the delta time. The angle is then wrapped to the 0 – 360 range using % 360. That isn’t strictly necessary, as sin() and cos() work correctly with angles outside of that range, however if the angles get too large then floating point precision may become a problem (also, it’s easier to visualise angles if they are in this range, for debugging purposes). + + 根据某个速度(OrbiterSpeed 常量)和时间差(delta time)增加旋转角度,这个角度经过 % 360 取余后得到一个 0-360° 之间的角度——这其实不是很必要,因为哪怕是角度在这个范围外 cos 和 sin 函数也不会有什么问题,只不过如果角度太大,浮点数的精度可能会成为一个问题(而且在测试时,如果角度限制在这个范围内,对我们来说更直观一些)。 + +2. It calculates the new X- and Y-positions for the orbiter using sin() and cos(). These take the radius (which forms the hypotenuse of the triangle) and the current angle, and return the adjacent and opposite sides, respectively. More about this in a second. + + 用 sin() 和 cos() 计算防御盾的 x 和 y。这需要用到半径(三角形的斜边)和当前角度,而返回的是另外两边(邻边和对边)。后面会细述。 + +3. It sets the new position of the orbiter sprite by adding the X- and Y-positions to the center position of the cannon. + + 设置防御盾的新位置,用炮台的中心位置加上 x 和 y。 + +> Note: You’re using the % operator to constrain the orbiterAngle to the range 0 – 360, even though orbiterAngle is a CGFloat. In C/Objective-C you would have had to use the fmod() function for this, because the % operator only works with integers, but Swift’s % operator supports floating point values too! +> +> 注意:% 运算符用于将 orbiterAngle 限制在 0-360°,虽然 orbiterAngle 实际上是一个浮点数。在 C/O-C 中,你必须用 fmod() 函数,因为 % 运算符只能用于整数,但 Swift 中 % 运算符支持浮点数! + +You have briefly seen sin() and cos() in action, but it may not have been entirely clear how they worked. You know that both of these functions can be used to calculate the lengths of the other sides of a right triangle, once you have an angle and the hypotenuse. + +你曾经看到过 sin() 和 cos() 的用法,但可能还不清楚它们是怎样实现的。我们知道这两个函数可以用来在已知一个角和斜边的情况下,计算出直角三角形的另外两边。 + +But aren’t you curious why you can actually do that? + +但你难道不奇怪这是为什么吗? + +Let’s draw a circle: + +让我们来画一个圆: + +![](http://cdn1.raywenderlich.com/wp-content/uploads/2013/03/Circle.png) + +The illustration above exactly depicts the situation of the asteroid orbiting around the cannon. The circle describes the path of the asteroid and the origin of the circle is the center of the cannon. + +上图描述了防御盾围绕炮台旋转的情况。圆表示防御盾的路径,圆心位于炮台的中央。 + +The angle starts at zero degrees but increases all the time until it ends up right back at the beginning. As you can see it, is the radius of the circle that determines how far away from the center the asteroid is placed. + +开始时防御盾的角度为 0,这个角度随着时间增加直到它又回到起点。如上图所示,圆的半径表示防御盾到圆心的距离。 + +So, given the angle and the radius, you can derive the X- and Y-positions using the cosine and sine, respectively: + +因此,在给定角度和半径的情况下,你可以用 cos 和 sin 获得位置的 x 和 y: + +![](http://cdn3.raywenderlich.com/wp-content/uploads/2013/03/Circle-with-sin-and-cos.png) + +Now let’s take a look at a plot of a sine wave and a cosine wave: + +现在来看一下正弦波和与余弦波: + +![](http://cdn2.raywenderlich.com/wp-content/uploads/2013/03/Sin-and-cos-waves.png) + +On the horizontal axis are the degrees of a circle, from 0 to 360 (or 0 to 2π radians, if you’re a mathematician). The vertical axis usually goes from -1 to +1, but if your circle has a radius that is greater than one (and they tend to) then the vertical axis really goes from –radius to +radius. + +x 轴是圆的角度,从 0° 到 360°(或者 0 到 2π 个弧度)。y 轴是 -1 到 +1,如果你的半径不是 1,则 y 轴的取值范围是 -radius 到 +radius。 + +As the angle increases from 0 to 360, find the angle on the horizontal axis in the plots for the cosine and sine waves. The vertical axis then tells you what the values for x and y are: + +因为角度是从 0-360 增加,在正弦波和余弦波的 x 轴上找到这个角度,则可以从 y 轴上查到这个角度的 x 值和 y 值: + +1. If the angle is 0 degrees, then cos(0) is 1 * radius but sin(0) is 0 * radius. That corresponds exactly to the (x, y) coordinate in the circle: x is equal to the radius, but y is 0. + + 如果角度为 0, cos(0) 是 1 * radius,sin(0)是 0 * radius。转换到圆的 x,y 坐标上就是:x 等于半径,但 y 等于 0。 + +2. If the angle is 45 degrees, then cos(45) is 0.707 * radius and so is sin(45). This means x and y are both the same at this point on the circle. (Note: if you’re trying this out on a calculator, then switch it to DEG mode first. You’ll get radically different answers if it’s in RAD mode (no pun intended :]) + + 如果角度为 45°,cos(45) 和 sin(45) 都是 0.707 * radius,也就是说是位于圆上 x=y 的位置。(注意:如果你使用计算器进行计算,请先把 DEG 模式打开。如果你得到的数字不是这个,则说明是 RAD 模式)。 + +3. If the angle is 90 degrees, then cos(90) is 0 * radius and sin(90) is 1 * radius. You’re now at the top of the circle where the (x, y) coordinate is (0, radius). + + 如果角度是 90°,cos(90) 是 0 * radius,sin(90) 是 1 * radius。则当前位置应该位于圆的正上方,x,y 坐标应当为 (0,radius)。 + +4. And so on, and so on. To get a more intuitive feel for how the coordinates in the circle relate to the values of the sine, cosine and even tangent functions, try out this cool interactive circle. + + 依次类推。要更彻底地了解圆上的坐标与 sin、cos 甚至是 tan 函数的关系,请看[这里](http://www.mathsisfun.com/algebra/trig-interactive-unit-circle.html)。 + +Make sense? Awesome. Did you also notice that the curves of the sine and cosine are very similar? In fact, the cosine wave is simply the sine wave shifted by 90 degrees. Go ahead and impress your friends and family with your knowledge of the mathematical origins of sine and cosine. :] + +有点意思了吗?好。发现 sine 曲线和 cos 曲线其实非常像了吗?事实上,余弦波只是在正弦波的基础上偏移 90°。去告诉你的朋友和家人,你所知道的关于正弦和余弦的数学吧! + +Back to coding. Add a call to updateOrbiter() at the bottom of update(): + +回到代码中来。在 update() 方法中的最后调用 updateOrbiter() 方法: + +```swift +updateOrbiter(deltaTime) +``` + +Build and run the app. You should now have an asteroid that perpetually circles the enemy cannon. + +运行程序。你将看到炮台外面有一个旋转着的轨道防御盾! + +![](http://cdn5.raywenderlich.com/wp-content/uploads/2013/03/Orbiter.png) + +You can also make the asteroid spin around its own axis: Add the following line to the bottom of updateOrbiter(): + +我们还以让防御盾成自转运动:将下列代码加在 updateOrbiter() 最后: + +```swift +orbiterSprite.zRotation = orbiterAngle * DegreesToRadians +``` + +By setting the rotation to orbiterAngle, the asteroid always stays oriented in the same position relative to the cannon, much like the Moon always shows the same side to the Earth. Even though it looks like it isn’t spinning, it certainly is! + +通过设置 orbiterAngle,防御盾自转的方向和它相对于炮台旋转的角度一致了,就像月亮总是用同一面对着地球。尽管看起来它根本就没有在自转,但实际上却是自转的! + +If you insert a minus sign, so that orbiterSprite.zRotation = -orbiterAngle * DegreesToRadians, the Asteroid will appear to spin on its axis relative to the cannon, instead of always facing it. Pick whichever effect you like best. Build and run to play around with it for a bit. + +如果你加入一个负号,比如 orbiterSprite.zRotation = -orbiterAngle*DegreesToRadians,则防御盾将相对于炮台做相反方向的自转,而不是总是面对着它。随便你使用哪种效果。运行程序,测试一小会。 + +Let’s give the orbiter a purpose. If the missile comes too close, the asteroid will destroy it before it gets a chance to do any damage to the cannon. Add the following method: + +让我们给繁育吨增加一个任务。如果导弹靠得太近,防御盾将摧毁导弹使其无法对炮台造成伤害。增加下列方法: + +```swift +func checkMissileOrbiterCollision() { + + if !playerMissileSprite.hidden { + + let deltaX = playerMissileSprite.position.x - orbiterSprite.position.x + let deltaY = playerMissileSprite.position.y - orbiterSprite.position.y + + let distance = sqrt(deltaX * deltaX + deltaY * deltaY) + if distance < OrbiterCollisionRadius + PlayerMissileRadius { + + playerMissileSprite.hidden = true + playerMissileSprite.removeAllActions() + + orbiterSprite.setScale(2) + orbiterSprite.runAction(SKAction.scaleTo(1, duration: 0.5)) + } + } +} +``` + +And don’t forget to include a call to it at the end of update(): + +同时,别忘了在 update() 方法最后调用它: + +``` +checkMissileOrbiterCollision() +``` + +This should look pretty familiar; it’s basically the same thing you wrote for checkMissileCannonCollision(). When the collision is detected, the missile sprite is removed. This time, you don’t play a sound, but as an added visual flourish, you increase the size of the asteroid sprite by 2X, and then immediately animate it scaling back down again. This makes it look like the orbiter “ate” the missile! + +这段代码看起来太熟悉了,基本上跟 checkMissileCannonCollision() 没什么两样。当碰撞有效,导弹精灵会被移除。只不过这一次,我们不播放声效,而是添加一个放大效果,将防御盾大小增加到两倍,然后又让它恢复原样。看起来就像防御盾“吞噬掉”了导弹一样! + +![](http://cdn4.raywenderlich.com/wp-content/uploads/2013/03/Meme-eat-missiles.png) + +Build and run to see your new orbiting shield in action. + +运行程序,查看防御盾的效果。 + +![](http://cdn4.raywenderlich.com/wp-content/uploads/2014/01/orbiting_shield-700x394.png) + +### Game Over, With Trig! +### 在游戏结束画面中使用三角学 + +There is still more that you can do with sines and cosines. They’re not just useful for calculating things with triangles and creating circular paths – they also come in handy for animations. + +我们还可以用 sin 和 cos 做更多的事情。它们不仅仅只能用于三角形计算和创建圆型路径——它们还能用于动画的处理。 + +A good place to demo such an animation is the game over screen. Add the following constant to the top of GameScene.swift: + +用于展示动画效果的好时机是游戏结束画面。在 GameScene.swift 顶部增加如下常量: + +```swift +let DarkenOpacity: CGFloat = 0.8 +``` + +And add a few new properties to the top of the GameScene class: + +在 GameScene 类中加入以下属性: + +```swift +var darkenLayer: SKSpriteNode? +var gameOverLabel: SKLabelNode? +var gameOver = false +var gameOverElapsed: CFTimeInterval = 0 +``` + +You’ll use these properties to keep track of the game state and the nodes to show the “Game Over” information. + +我们会用这些属性来记录游戏状态和显示“Game Over”信息。 + +Next, add this method to the class: + +然后,添加如下方法: + +```swift +func checkGameOver(dt: CFTimeInterval) { + + // 1 + if playerHP > 0 && cannonHP > 0 { + return + } + + if !gameOver { + + // 2 + gameOver = true + gameOverElapsed = 0 + stopMonitoringAcceleration() + + // 3 + let fillColor = UIColor(red: 0, green: 0, blue: 0, alpha: 1) + darkenLayer = SKSpriteNode(color: fillColor, size: size) + darkenLayer?.alpha = 0 + darkenLayer/.position = CGPoint(x: size.width/2, y: size.height/2) + addChild(darkenLayer!) + + // 4 + let text = (playerHP == 0) ? "GAME OVER" : "Victory!" + gameOverLabel = SKLabelNode(fontNamed: "Helvetica") + gameOverLabel?.text = text + gameOverLabel?.fontSize = 24 + gameOverLabel?.position = CGPoint(x: size.width/2 + 0.5, y: size.height/2 + 50) + addChild(gameOverLabel!) + + } else { + + // 5 + darkenLayer?.alpha = min(DarkenOpacity, darkenLayer!.alpha + CGFloat(dt)) + } +} +``` + +This method checks whether the game is done, and if so, handles the game over animation: + +这个方法用于判断游戏是否结束,如果判断为 true,进行游戏结束动画: + +1. The game keeps on going until either the player or cannon run out of health points. + + 如果玩家或者炮台的血值为 0,游戏结束。 + +2. When the game is over, you set gameOver to true, and disable the accelerometer. + + 如果游戏结束,设置 gameOver 为 true,关闭加速计。 + +3. Create a new, all-black color layer and add it on top of everything else. Its alpha opacity is set to 0 initially, so that it is completely transparent. Later in this method, you’ll animate the opacity value of this layer so that it appears to fade in. + + 创建一个新的,纯黑色的颜色层,将它添加到所有对象的前面。它的 alpha 一开始设置为 0,这样它就完全透明。在后面的代码中,我们会对这个透明度进行动画,使其呈现渐入的效果。 + +4. Add a new text label and place it on the screen. The text is either “Victory!” if the player won or “Game Over” if the player lost, determined based on the player’s health points. + + 加一个新的文字标签到屏幕中央。如果玩家获胜,文字内容为“Victory!”,否则为“Game Over”,胜负根据玩家的血值来判定。 + +5. The above steps only happen once to set up the game over screen – every time after that, the code enters the else clause. Here, you animate the alpha of the new color layer from 0 to 0.8 (the DarkenOpacity constant) – almost completely opaque, but not quite. + + 上面的代码只会创建一次游戏结束画面——在此之后,代码将进入 else 分支。在这里,我们对颜色图层的 alpha 值进行动画,由 0 渐变到 0.8(即 DarkenOpacity 常量)——接近于完全不透明,但又不是。 + +Add a call to checkGameOver() at the bottom of update(): + +在 update() 底部调用 checkGameOver() : + +```swift +checkGameOver(deltaTime) +``` + +And add a small snippet of logic to the top of touchesEnded(): + +在 touchesEnded() 顶部加入以下代码: + +```swift +if gameOver { + let scene = GameScene(size: size) + let reveal = SKTransition.flipHorizontalWithDuration(1) + view?.presentScene(scene, transition: reveal) + return +} +``` + +This restarts the game when the user taps on the game over screen. + +这段代码在用户点击游戏结束画面后重新启动游戏。 + +Build and run, then try it out. Shoot at the cannon or collide your ship with it until one of you runs out of health. The screen will fade to black and the game over text will appear. The game no longer responds to the accelerometer, but the animations still keep going: + +运行程序。向炮台开火或者撞击,一直到你们中的某一个血掉光。屏幕会渐渐暗下来,游戏结束字样显示。游戏不在对加速计产生响应,只不过动画仍然还会继续: + +![](http://cdn3.raywenderlich.com/wp-content/uploads/2014/01/game_over-700x466.png) + +This is all fine and dandy, but where are the sine and cosines? As you may have noticed, the fade in animation of the black layer was very linear. It just goes from transparent to opaque at a consistent rate. + +一切顺利,但说好的 sin 和 cos 呢?如你所见,黑色图层的渐变动画是以线性方式进行的。也就是说透明度由高到低是以固定的速度变化的。 + +We can do better than this – we can use sin() to alter the timing of the fade. This is known as “easing” and the effect you will apply here is known as an “ease out”. + +我们可以改进这一点——通过 sin() 函数来调整渐变时间。这叫做“淡入淡出”,我们将使用的是“淡出”。 + +> Note: You could just use runAction() to do the alpha fade, as it supports various easing modes. Again, the purpose of this tutorial is not to learn Sprite Kit; it’s to learn the math behind it, including easing! +> +> 注意:你也可以用 runAction() 方法让 alpha 渐变,因为它支持各种淡入淡出模式。但是,出于教学的目的,我们要在这里教大家学习淡入淡出背后的数学只是,而不是教大家怎样使用 Sprite Kit。 + +Add a new constant at the top of GameScene.swift: + +在 GameScene.swift 顶部加入一个新常量: + +```swift +let DarkenDuration: CFTimeInterval = 2 +``` + +Next, change the single line in the else statement in checkGameOver() to: + +然后,将 checkGameOver() 方法中 else 分支中的代码修改为: + +```swift +gameOverElapsed += dt +if gameOverElapsed < DarkenDuration { + var t = CGFloat(gameOverElapsed / DarkenDuration) + t = sin(t * Pi / 2) // ease out + darkenLayer.alpha = DarkenOpacity * t +} +``` + +gameOverElapsed keeps track of how much time has passed since the game ended. It takes two seconds to fade in the black layer (DarkenDuration). The variable t determines how much of that duration has passed by. It always has a value between 0.0 and 1.0, regardless of how long DarkenDuration really is. + +gameOverElapsed 负责记录游戏结束后所经过的时间。黑色图层(DarkenDuration)的渐入需要耗时 2 秒。变量 t 用于计算整个过程过去了多少。无论 DarkenDuration 值是多少,这个值都会在 0-1.0 之间。 + +Then you perform the magic trick: + +然后是最关键的地方: + +```swift +t = sin(t * Pi / 2) // ease out +``` + +This converts t from a linear interpolation into one that breathes a bit more life into things: + +这会将线性插值进行某种转换,如下图所示: + +![](http://cdn3.raywenderlich.com/wp-content/uploads/2013/03/Easing.png) + +Build and run to see the new “ease out” effect. If you find it hard to see the difference, then try it with the “ease out” line commented out, or change the duration of the animation. The effect is subtle, but it’s there. + +运行程序,查看“淡出”效果。如果你无法看到有任何不同,则可以将“ease out”一行注释,或者改变动画时长。效果很微妙,但确实存在。 + +> Note: If you want to play with the values and test the effect quickly, try setting cannonHP to 10 so you can end the game with a single shot. +> +> 注意:如果你想快速看到效果,将 cannonHP 调整为 10,这样你只需要击中一次就能让游戏结束。 + +Easing is a subtle effect, so let’s wrap up with a much more obvious bounce effect – because things that bounce are always more fun! + +淡入淡出是一种细微的效果,我们可以用弹性效果来使其更加明显——因为能够反弹的东西更加有趣! + +Inside that else clause in checkGameOver(), add the following after the previous code: + +在 checkGameOver() 方法的 else 分支内,刚刚加入的代码之后,加入: + +```swift +// label position +let y = abs(cos(CGFloat(gameOverElapsed) * 3)) * 50 +gameOverLabel.position = CGPoint(x: gameOverLabel.position.x, y: size.height/2 + y) +``` + +OK, what’s happening here? Recall what a cosine looks like: + +这是什么意思?还记的余弦波的样子吗: + +![](http://cdn4.raywenderlich.com/wp-content/uploads/2013/03/Absolute-cosine.png) + +If you take the absolute value of cos() – using abs() – then the section that would previously go below zero is flipped. The curve already looks like something that bounces, don’t you think? + +如果你对 cos() 进行取绝对值——用 abs() 函数——则小于 0 的部分将被翻转。这个曲线看起来就像是反弹,不是吗? + +Because the output of these functions lies between 0.0 and 1.0, you multiply it by 50 to stretch it out to 0-50. The argument to cos() is normally an angle, but you’re giving it the gameOverElapsed time to make the cosine move forward through its curve. + +因为这些函数的值分布在 0.0 到 1.0 之间,乘以 50 之后就会让值放大到 0-50 之间。cos() 的参数通常是一个角度,但我们传递了一个 gameOverElapsed 时间进去,导致 cos 根据时间曲线来移动。 + +The factor of 3 is just to make it go a bit faster. You can tinker with these values until you have something that you think looks cool. + +系数 3 的使用使它变化得更快。你可以调整这些值直到你认为 OK。 + +Build and run to check out the bouncing text: + +运行程序,查看文字的弹簧效果: + +![](http://cdn2.raywenderlich.com/wp-content/uploads/2014/01/victory-700x466.png) + +You’ve used the shape of the cosine to describe the bouncing motion of the text label. These cosines are useful for all sorts of things! + +你使用了余弦波的形状来描述文字标签的弹簧运动。余弦非常适合于描述各种事情! + +One last thing you can do is let the bouncing motion lose amplitude over time. You do this by adding a damping factor. Create a new property in GameScene: + +最后一件事情是让弹簧运行随时间曲线逐渐减少振幅。这需要增加一个衰减系数。在 GameScene 中添加一个属性: + +```swift +var gameOverDampen: CGFloat = 0 +``` + +The idea here is when the game ends, you’ll need to reset this value to 1.0 so the damping takes effect. Over time as the text bounces, the damping will slowly fade off to 0 again. + +当游戏结束,我们需要将这个值重置为 1.0 以便进行有效衰减。当文字做弹簧运动时,衰减系数又会逐渐递减到 0. + +In checkGameOver(), find the if block where you set gameOver to true and add the following line right after you set that property: + +在 checkGameOver() 方法中,找到我们设置 gameOver 为 true 的 if 语句块,在设置完 gameOver 之后添加这句: + +```swift +gameOverDampen = 1 +``` + +In the else block, change the code underneath the “// label position” comment to be the following: + +在 else 块中,将 "// label positon" 一句注释后面的代码修改为: + +```swift +let y = abs(cos(CGFloat(gameOverElapsed) * 3)) * 50 * gameOverDampen +gameOverDampen = max(0, gameOverDampen - 0.3 * CGFloat(dt)) +gameOverLabel.position = CGPoint(x: gameOverLabel.position.x, y: size.height/2 + y) +``` + +It’s mostly the same as before, but you multiply the y-value by the damping factor and, simultaneously, you reduce this damping factor slowly from 1.0 back to 0.0 (but never less than 0; that’s what the max() prevents). Build and run, then try it out! + +这和前面几乎没什么区别,除了将 y 乘以衰减系数并逐步将衰减系数从 1.0 递减到 0 以外(永远不要小于 0;也就是要用 max() 来进行限制)。运行程序,进行测试! + +### Where to Go from Here? +### 接下来做什么? + +Here is the full TrigBlasterPart2 from this Trigonometry for Game Programming tutorial series. + +[这是](http://cdn4.raywenderlich.com/wp-content/uploads/2015/04/TrigBlasterPart2.zip)“游戏编程中的三角学”系列教程的完整示例代码。 + +Congratulations, you have delved into the depths of sine, cosine and tangent, and you have witnessed some examples of how they can be applied in a real game. I hope you’ve seen how handy Trigonometry really is for games! + +恭喜你,你已经深入学习了 sin、cos 和 tan 函数,也见到了如何在真实游戏中使用它们的例子。我希望你能够明白,三角形在游戏中是何等的有用! + +Note that we didn’t talk much about arcsine and arccosine. They are much less useful in practice than arctangent, although a common use for arccosine is to find the angle between two arbitrary vectors – for example, to model the reflection of a light beam in a mirror, or to calculate how bright an object should be depending on its angle to a light source. + +注意,我们并没有太多涉及 arcsin 和 arccos。比起 arctan 来说它们很少见,最常见的例子是用 arccos 计算两个任意向量之间的夹角——例如,模拟光束照射到镜面上的反射,或者根据对象与光源之间的角度计算这个对象的亮度。 + +You can find another great example of the usefulness of trigonometric functions in the Tiny Wings tutorial. It uses cosine to give the hills from the game nicely curved shapes. + +你可以在[Tiny Wings tutorial](http://www.raywenderlich.com/?p=3888)中找到更多关于三角函数的非常有用的例子。它使用 cos 函数在游戏中绘制极其优美的山脉曲线。 + +If you fancy using your new-found skills for more game development, but don’t know where to start, then why not try out our book iOS Games by Tutorials. It will certainly kick start your development! +Drop by the forums to share your successes and agonies with trig. And use your new powers wisely! + +如果想在更多游戏开发中使用你刚学到的技巧,但不知道从何开始,请阅读我们的[iOS 游戏开发教程](http://www.raywenderlich.com/?page_id=48022)。它将启动你的游戏开发之旅!请访问我们的论坛,分享你在使用三教数学中的苦与乐。合理地使用你的新技能! + +Credits: The graphics for this game are based on a free sprite set by Kenney Vleugels. The sound effects are based on samples from freesound.org. + +声明:游戏中使用的图片来自于 Kenney Vleugels,声音来自于 freesound.org。 \ No newline at end of file diff --git "a/issue-20/UIStackView\346\225\231\347\250\213\357\274\232\344\272\206\350\247\243Stack View.md" "b/issue-20/UIStackView\346\225\231\347\250\213\357\274\232\344\272\206\350\247\243Stack View.md" index 05f9144..7d6efd2 100644 --- "a/issue-20/UIStackView\346\225\231\347\250\213\357\274\232\344\272\206\350\247\243Stack View.md" +++ "b/issue-20/UIStackView\346\225\231\347\250\213\357\274\232\344\272\206\350\247\243Stack View.md" @@ -428,7 +428,9 @@ Top: 20, Leading: 0, Trailing: 0, Bottom: 20 > stack view. The little blue circle should be positioned at the left > edge between the two stack views and not at the right edge: -> +> 注意:让箭头稍微偏向你正在拖的Stack View左边一点,以便它能够作为外层 Stack View 的 subview 添加。蓝色的小圆圈应当位于两个 Stack View 之间的左端而不是右端: + + 现在,weather版块是从上到下的第三个版块,由于 Hide 按钮它并不是 Stack View的subview,所以它不会参与移动,它的frame当前是不正确的。 diff --git "a/issue-20/iOS 9 App Search\346\225\231\347\250\213.md" "b/issue-20/iOS 9 App Search\346\225\231\347\250\213.md" new file mode 100644 index 0000000..e9d0f64 --- /dev/null +++ "b/issue-20/iOS 9 App Search\346\225\231\347\250\213.md" @@ -0,0 +1,391 @@ +> * 原文链接 : [iOS 9 App Search Tutorial: Introduction to App Search](http://www.raywenderlich.com/116608/ios-9-app-search-tutorial-introduction-to-app-search) +* 原文作者 : [Chris Wagner](http://www.raywenderlich.com/u/cwagdev) +* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn) +* 译者 : [kmyhy](https://github.com/kmyhy) +> +> Ray 注:本文作为《iOS 9 Feast》中的一部分,节略自 《iOS 9 Tutorials》其中一章——通过本文,您可对全书内容窥见一斑。祝您阅读愉快! + +在相当长的一段时间内,iOS 的 Spotlight 都是一个大坑。尽管用户可以用它来搜索你的 App,但他们却无法看到其中的内容——他们真正关心的部分。现在,当用户想读取一个 App 中的内容时,他们只能回到 Home 屏一屏一屏翻,找到 App,打开 App,搜索他们想要的内容——假设你的App实现了搜索功能的话。 + +对于比较老练的用户,则可能会通过Siri 或者 Spotlight 来打开你的 App,但无论哪个工具都不能让用户查找“非苹果官方App”内的内容。也就是说,苹果在 Spotlight 中可以查找通讯录、备忘录、信息、邮件以及其它支持查找功能的App中的内容。用户只需要点击搜索结果就可以直接访问相应的内容。这真是太不公平了! + +有时苹果会将一些有趣的功能保留给自己专用,比如 Spotlight。好消息是,每当苹果的开发者调教好一个功能,觉得已经可以把它放出去的时候,他们就会让大伙也尝尝鲜,比如 iOS 8 中的 App 扩展。 + +在iOS 9 中,苹果又放出来一个很酷的功能给我们,第三方开发者现在可以在Spotlight 中搜索他们的App 内容了! + +在本教程中,你将领略 App Search 的威力,并学会如何将它集成到你自己的App 中。 + +## App Search API +在iOS 9 中,App Search由三个部分组成。每一部分根据不同的目的分成独立的 API,但它们也能和其它部分一起使用: + + - NSUserActivity + - Core Spotlight + - Web markup + +###NSUserActivity + +在App Search 中,使用了NSUserActivity,这是一个灵活小巧功能,在iOS 8 Handoff 中就使用到了NSUserActivity 。 + +在iOS 9 中,NSUserActiviy增加一些新的属性以支持 App Search。从理论上讲,如果一个任务能够转变成一个 NSUserActivity 并转交给其它设备,它也能转换为一个搜索项并在同一个设备上继续处理。这就有可能对App 上的活动、状态和导航点进行索引,这样用户才能在 Spotlight 中对其进行搜索。 + +例如,一个旅游类App 可能会将用户查看过的酒店进行索引,而一个新闻类App 会将用户浏览过的文章进行索引。 +> +> 注意:本教程不涉及 Handoff,我们只会讨论当一个内容被浏览后如何创建可搜索的内容。如果你想熟悉了解 Handoff 的内容,请阅读[ Getting Started with Handoff 教程](http://www.raywenderlich.com/84174/ios-8-handoff-tutorial)。 + +### Core Spotlight + +第二个同时也是App Search 中最“常用到的”概念就是 Core Spotlight,它是存储类 App 诸如邮件、备忘录用于索引内容的东西。它既可以允许用户搜索之前访问过的内容,也可以用它来一次性构建一个巨大的可搜索内容的集合。 + +你可以将Core Spotlight 看成是一个专门用于搜索的数据库。它提供了对添加搜索索引中的内容的细粒度的控制,例如这些内容是什么、什么时候添加的以及如何添加到搜索索引中的。你可以检索任何类型的内容,包括文件、视频、消息等等,还可以更新和移除搜索索引中的条目。 + +Core Spotlight 为全面搜索 App 内部内容提供了一种最好的方式。 + +本教程关注于使用前面提及的 NSUserActivity 对象获取 Spotlight 搜索结果。本教程的完整版位于《iOS 9 Tutorials》中,其中介绍了如何通过Core Spotlight 全面检索你的内容。 + +### Web markup + +App Search的第三个方面是 Web Markup,这个功能允许App 将它们的内容镜像到一个Web站点上。比较好的例子如 Amazon,你可以搜索它上面成千上万的在售产品,甚至是raywenderlich.com上的产品。在web 内容上使用了标准的标签进行标记,你可以将App 内容显示在 Spotlight 和 Safari的搜索结果中,甚至可以直接链接到你的App。 + +本教程不涉及 Web Markup,你可以在《iOS 9 by Tutorials》第三章“Your App On The Web"中学习这部分内容。 + +##开始 + +你将学习的示例程序叫做 Colleagues,它模拟一个公司通讯录。它可以将你的同时添加到你的联系人中,而不是直接给你一个同事的目录。为了简单起见,它使用的是本地数据库,由一个文件夹(存放头像图片)和一个 JSON文件(包含了所有公司职员信息)组成。在生产环境中,你应该使用一个网络组件从 Web 上抓取这些数据。作为教程,JSON 文件就足够了。[下载](http://cdn5.raywenderlich.com/wp-content/uploads/2015/09/intro-app-search-starter.zip)并打开初始项目,不需要做任何事情,直接编译运行。 + + + +你会看到一张职员列表。这是一个小型创业公司,只有25个职员的规模。选择 Brent Reid,可以查看这个职员的信息。你同时还可以看到 Brent Reid 所在的部门的其他人的列表。那是 App 的一个扩展功能——其实非常简单! + +搜索功能将让 App 增色不少。但是现在,你甚至无法在 App 搜索。你不用在 App 中增加搜索功能,相反,你可以用 Spotlight 从 App 外部增加一个搜索功能。 + +### 示例项目 + +花点时间来熟悉一下示例项目的代码。项目中存在两个 Target,一个是 Colleagues,即 App 自身;一个是 EmployeeKit,负责和职员数据库进行交互。 + +在 Xcode 中,展开 EmployeeKit 文件夹,打开 Employee.swift。这是职员信息的模型类,定义了一系列相关属性。Employee 对象使用一个 JSON 对象进行实例化,后者来自于 Database 文件夹下的 employees.json 文件。 + +然后打开 EmployeeService.swift。在文件头部声明了一个扩展,扩展中有一个 destroyEmployeeIndexing()方法,这个方法用TODO标记进行注明。你将在稍后实现这个方法。这个方法负责销毁所有显示过的索引。 + +在 EmployeeKit 这个 Target 中有许多内容,但都和 App Search 毫无关联,因此我们就不多说了。当然,你可以花时间看一看。 + +打开Colleagues 文件夹下的 AppDelegate.swift。注意只有一个方法在里边: +application(_:didFinishLaunchingWithOptions:)。这个方法判断Setting.searchIndexingPreference 是否设置为 .Disabled,如果是,则将所有存在的搜索索引删除。 + +除了知道有这么一个设置项存在外,你并不需要做任何事情。你可以通过 iOS 的设置程序中的 Colleagues 来修改这个设置。 + +参观到此结束。接下来你需要修改 View Controller 中的代码。 + +###搜索曾经浏览过的记录 + +实现App Search时,NSUserActivity 总是第一个要实现的,因为: + +1. 它最简单。创建一个 NSUserActivity 实例就如同设置几个属性那么简单。 + +2. 当你用 NSUserActivity 表示用户活动时,iOS 会对内容进行排序,以便搜索结果对经常被访问的内容进行优先处理。 + +3. 它和实现 Handoff 很像。 + +现在,让我们来看看实现 NSUserActivity 到底有多简单! + +###实现 NSUserActivity + +选中 EmployeeKit 文件夹,依次选择 File \ New \ File...,然后选择 iOS\ Source \ Swift File 模板,再点击 Next。将文件命名为 EmployeeSearch.swift,并确保其 Target 为 EmployeeKit。 + +在这个文件中,首先导入 CoreSpotlight: + +``` +import CoreSpotlight +``` + +然后定义一个 Employee 的扩展: + +``` +extension Employee { + public static let domainIdentifier = "com.raywenderlich.colleagues.employee" +} +``` + +反域名字符串将用于唯一标识 NSUserActivity 所属的一类活动类型。接着,在domainIdentifier 之后增加一个计算属性: + +``` +public var userActivityUserInfo: [NSObject: AnyObject] { + return ["id": objectId] +} +``` + +这个字典用于 NSUserAcitivity 唯一标识某个活动(Activity)。然后再添加一个计算属性,名为 userActivity: + +``` +public var userActivity: NSUserActivity { + let activity = NSUserActivity(activityType: Employee.domainIdentifier) + activity.title = name + activity.userInfo = userActivityUserInfo + activity.keywords = [email, department] + return activity +} +``` + +这个属性用于很方便地根据一个 Employee 创建一个 NSUserActivity 实例。它创建了一个 NSUserActivity 对象,并用对以下属性进行了赋值: + +- activityType:活动所属的类型。你会在后面用它来识别 iOS 传递给你的NSUserActivity实例。苹果建议该值采用反域名命名规则。 + +- title:活动的名字——这将用于在搜索结果中作为主要名显示。 + +- userInfo:一个字典,用于存放你想传递的任意数据。当你的App 收到一个活动时——比如用户从 Spotlight 点击了一个搜索结果,你就可以获取这个字典。你将在这个字典中存放同事的唯一 ID,这样 App 打开后就能显示正确的同事资料。 + +- keywords:一个本地化的关键字列表,用于作为搜索关键字。 + +然后,我们将使用刚才定义的 userActivity 属性去搜索同事记录。因为这些代码位于 EmployeeKit 框架中,我们需要编译框架才能在 Colleagues App 中使用它们。 + +按 Command+B,编译项目。 + +打开 EmployeeViewController.swift,在viewDidLoad()方法最后加入代码: + +``` +let activity = employee.userActivity + +switch Setting.searchIndexingPreference { +case .Disabled: + activity.eligibleForSearch = false +case .ViewedRecords: + activity.eligibleForSearch = true +} + +userActivity = activity +``` + +上述代码读取 userActivity 属性——这个属性是我们刚才通过定义 Employee 扩展时添加的。然后检查 App 的搜索设置。 + +如果搜索被禁用,将 activty 标记为不可用于搜索。如果该设置为 ViewedRecords,则将 activity 标记为能够用于搜索。 + +最后,将 View Controller 的 userActivity 属性设置为 employee 的 userActivity。 + +> 注意:View Controller 的 userActivity 属性继承自 UIResponder 。这个属性是苹果为了支持 Handoff 而增加到 iOS 8 中的。 + +最后还应该覆盖 updateUserActivityState() 方法。这样,当某个搜索结果被选择时,你才可以获得所需要的数据。 + +在 viewDidLoad() 方法后增加这个方法: + +``` +override func updateUserActivityState(activity: NSUserActivity) { + activity.addUserInfoEntriesFromDictionary( + employee.userActivityUserInfo) +} +``` + +在 UIResponder 的生命周期中,系统会多次调用这个方法,你应该在这个方法中保持更新 activity。在我们的例子里,你只需要将包含有 employee的 objectId 的 userActivityUserInfo 字典传递给 activity。 + +好了!现在,在搜索设置被开启的情况下,每当你浏览了一个同事,浏览历史将被记下并可用于搜索。 + +在模拟器或设备上,打开设置程序,找到 Colleagues。将 Indexing 设置改成 Viewed Records。 + + + +现在,编译运行程序,然后选择 Brent Reid。 + +OK,看起来没有什么新奇的事情发生,但在你不知不觉中,Brent 的活动已经被加到搜索索引中了。回到 Home 屏幕(shift+command+H),通过下拉屏幕或者向右划动屏幕,打开 Spotlight。在搜索栏输入 brent reid 。 + + + +"Brent Reid"显示出来了!如果你没看见,可能需要向下滚动列表。如果你点击这个Brend Reid,它将移动到列表上部,以便下次你可以搜索同一个关键字。 + + + +虽然到现在为止结果还蛮不错,但这个搜索结果却是有点索然无味了。 + +除了显示一个名字外,我们还能干什么?现在就让我们彻底进入 Core Spotlight 的殿堂探索一番。 + +###在搜索结果中显示更多信息 + +NSUserActivity 有一个 contentAttributeSet 属性。这个属性的类型是 CSSearchableItemAttributeSet,它允许你用一系列属性来描述你的内容。查看 CSSearchableItemAttributeSet 类参考,你可以发现很多利用这些属性来描述内容的方法。 + +下图是我们需要的搜索结果,每个部分都分别标出了所用的属性名: + + + +前面已经设置过 NSUserActivity 的 title 属性,这个属性正如你所看到的。其它3个属性,thumbnailData、supportsPhoneCall 和 contentDescription 全部都是通过 CSSearchableItemAttributeSet 来设置的。 + +打开 EmployeeSearch.swift,在文件头部,导入 MobileCoreServices: + +``` +import MobileCoreServices +``` + +MobileCoreServices 是必须的,因为在我们创建 CSSearchableItemAttributeSet 对象时需要用到其中定义的一个常量。你已经导入过 CoreSpotlight了,这个框架也是必须的,它的所有 API 都使用了 CS 作为前缀。 + +仍然在 EmployeeSearch.swift中,在 Employee 扩展中添加新的计算属性: + +``` +public var attributeSet: CSSearchableItemAttributeSet { + let attributeSet = CSSearchableItemAttributeSet( + itemContentType: kUTTypeContact as String) + attributeSet.title = name + attributeSet.contentDescription = "\(department), \(title)\n\(phone)" + attributeSet.thumbnailData = UIImageJPEGRepresentation( + loadPicture(), 0.9) + attributeSet.supportsPhoneCall = true + + attributeSet.phoneNumbers = [phone] + attributeSet.emailAddresses = [email] + attributeSet.keywords = skills + + return attributeSet +} +``` +初始化 CSSearchableItemAttributeSet 时,需要提供一个 itemContentType 参数,我们传递了一个 kUTTypeContact 进去(该常量在 MobileCoreServices 框架中定义,关于该常量,请阅读苹果的 [UTType 参考](http://apple.co/1NilqiZ))。 + +attributeSet 中包含了一些与当前 employee 搜索时用到的相关数据:title 来自于 NSUserActivity 的 title,contentDescription 包括了这个同事的部门、称谓和电话号码等信息,而 thumbnailData 则调用 loadPicture() 方法结果并转换为 NSData。 + +要显示”打电话“按钮,我们必须将 supportsPhoneCall 设置为true,并给 phoneNumbers 属性赋一个数组。最后,我们设置了 email 地址,并将同事的 skills (技能)作为 keyword 关键字。 + +现在所有的数据都准备好了,Core Spotlight 在搜索时会检索这些数据并添加到搜索结果中。这样,用户就可以搜索同事的姓名、部门、称谓、电话号码、email甚至是技能。 + +仍然是 EmployeeSearch.swift,在返回 userActivity 前面添加以下语句: +``` +activity.contentAttributeSet = attributeSet +``` + +这句代码告诉 NSUserActivity 使用这些信息作为 contentAttributeSet属性的值。 + +编译运行。查看 Brent Reid 的个人信息以便索引生效。回到 Home 屏幕,拉出 Spotlight,搜索 brent reid。如果你先前的搜索结果仍然存在,你只需要清除并重新搜索。 + + + +噢,你是不是很奇怪实现的代码太少了? + +好了!现在 Spotlight 能够如我们所想的一样搜索同事了。不过,似乎我们还是遗漏了点什么...当你尝试通过搜索结果打开 App 时,什么也不会发生。 + +###打开搜索结果 + +理想的用户体验是直接打开 App 并显示相关的内容。事实上——这个是一个要求——苹果会将能够启动并显示有用的信息的App的排在搜索结果的前列。 + +通过将一个 activityType 和一个 userInfo 对象赋给 NSUserActivity 对象,你已经在上一节中为后续的工作做了铺垫。 + +打开 AppDelegate.swift,在application(_:didFinishLaunchingWithOptions:) 方法下面,添加 +一个application(_:continueUserActivity:restorationHandler:) 方法: + +``` +func application(application: UIApplication, + continueUserActivity userActivity: NSUserActivity, + restorationHandler: ([AnyObject]?) -> Void) -> Bool { + + return true +} +``` + +当用户选择了一个搜索结果时,这个方法会被调用——这个方法也会被Handoff 用来接收其他设备传来的活动。 + +在这个方法返回 true 之前,加入以下语句: + +``` +guard userActivity.activityType == Employee.domainIdentifier, + let objectId = userActivity.userInfo?["id"] as? String else { + return false +} +``` + +guard 语句检查 activityType 是否是我们希望的类型(用于处理 Employee 的活动),然后从 userInfo 中获取 objectId。如果这两个条件中有一个不满足则返回 false,通知系统该活动不会被处理。 + +接着,在 guard 语句后,将 return true 语句替换为: + +``` +if let nav = window?.rootViewController as? UINavigationController, + listVC = nav.viewControllers.first as? EmployeeListViewController, + employee = EmployeeService().employeeWithObjectId(objectId) { + nav.popToRootViewControllerAnimated(false) + + let employeeViewController = listVC + .storyboard? + .instantiateViewControllerWithIdentifier("EmployeeView") as! + EmployeeViewController + + employeeViewController.employee = employee + nav.pushViewController(employeeViewController, animated: false) + return true +} + +return false +``` + +获得 id 之后,你的目标就是用EmployeeViewController 显示匹配的同事信息。 + +上述代码稍微有点乱,但你可以想象一下 App 的设计。App 中有两个 View Controller,一个是同事的列表,另一个是则显示同事的详细信息。上述代码先将导航控制器的视图控制器堆栈弹回到列表界面,然后push 一个该同事细节窗口。 + +如果因为某种原因视图无法呈现,方法会返回一个false。 + +OK,编译和运行!从同时列表中选择 Cary Iowa,然后回到 Home 屏。调出 Spotlight 搜索 Brent Reid。找到结果后,点击它。App 会打开,并且可以看到 Cary 的详情界面迅速地过渡到了 Bent 的详情界面。干得不错! + +###从搜索索引中删除条目 + +回到 App 的话题上来。想象一下,在某个狂风暴雨的一天,一个同事因为将老板用胶带绑在墙上而被解雇。显然,你是无论如何都不想和这个人有任何关系了,因此你必须将他和其他离开公司的人一起从 Colleagues 的搜索索引中删除。 + +由于只是一个示例App,你可以在 App 的索引设置关闭的前提下将整个索引删除。 + +打开EmployeeService.swift 在文件头部添加导入语句: + +``` +import CoreSpotlight +``` + +找到 destoryEmployeeIndexing(),将 TODO 注释替换为: + +``` +CSSearchableIndex + .defaultSearchableIndex() + .deleteAllSearchableItemsWithCompletionHandler { error in + if let error = error { + print("Error deleting searching employee items: \(error)") + } else { + print("Employees indexing deleted.") + } +} +``` + +这个无参的方法将删除整个App的索引数据库。Good! + +现在可以来测试一下。通过下列步骤来测试是否索引一如我们希望的那样已被删除: + +1. 编译运行程序。 + +2. 用 Xcode 终止程序。 + +3. 在模拟器或者设备中,打开 设置 \ Colleagues,将 Indexing 设置为 Viewed Records。 + +4. 再次打开 App,选择一个新的同事,让索引生效。 + +5. 回到 Home 屏,调出 Spotlight。 + +6. 搜索浏览过的同事,等待索引项出现。 + +7. 回到 设置 \ Colleagues,将 Indexing 设置为关。 + +8. 退出 App。 + +9. 重新打开 App。这将清除搜索索引。 + +10. 回到 Home 屏,调出 Spotlight。 + +11. 搜索浏览过的同事,你会发现没有和 Colleagues App 有关的搜索结果。 + +呵呵,删除整个搜索索引实在太容易了。但如果你想只删除某个单独的记录呢?幸运的是——有两个 API 能够让你更精确地删除想删的记录: + +- deleteSearchableItemsWithDomainIdentifiers(_:completionHandler:) 方法允许你删除整个 domain ID 相同的一组索引。 + +- deleteSearchableItemsWithIdentifiers(_:completionHandler:) 方法允许你通过唯一ID 指定要删除哪条记录。 + + +也就是说,如果你所索引的记录具有多种类型的话,全局 ID (在同一个 App 组中)必须唯一。 + +> 注意:如果你不能保证跨类型ID 是唯一的,比如你的 ID 是通过数据库中的自增长类型获得的,则你可以采取一种简单办法,即在记录 ID 前面加上一个类型前缀。例如,如果你有一个联系人记录的 ID 为 123,一个订单记录的 ID 也是 123,则可以将它们的唯一 ID 设置为 contact.123 和 order.123。 + +如果你在运行过程中遇到任何问题,你可以从[这里](http://cdn5.raywenderlich.com/wp-content/uploads/2015/09/intro-app-search-final.zip)下载到最终完成的项目。 + +##接下来做什么? + +这篇 iOS 9 App Search 教程介绍了在 iOS 9 中使用 User Activity 搜索 App 内部内容的简单但强大的方法。搜索的内容从来不会受到限制——你可以用这种方法搜索 App 中的导航点。 + +想象一下,一个 CRM App,它拥有许多窗口,比如联系人、订单和任务。通过 User Activity,用户随时可以到达这些窗口,用户可以搜索订单,然后直接跳到 App 的某个订单界面。这个功能太有用了,尤其是你的 App 有很多层级的导航时。 + +有许多独特的方法将内容推给你的用户。想突破沙盒的限制,就要教会用户使用这个强大的功能。 + +这个教程是《iOS 9 by Tutorials》第2章的精简版。如果你想学习用 Core Spotlight 检索大数据集,或者学习 iOS 9 的 Web Content,请阅读这本书! diff --git "a/issue-20/\350\207\252\345\256\232\344\271\211View Controller\350\275\254\346\215\242\345\212\250\347\224\273.md" "b/issue-20/\350\207\252\345\256\232\344\271\211View Controller\350\275\254\346\215\242\345\212\250\347\224\273.md" new file mode 100644 index 0000000..e50bb9b --- /dev/null +++ "b/issue-20/\350\207\252\345\256\232\344\271\211View Controller\350\275\254\346\215\242\345\212\250\347\224\273.md" @@ -0,0 +1,558 @@ +> * 原文链接 : [Introduction to Custom View Controller Transitions and Animations](http://www.appcoda.com/custom-view-controller-transitions-tutorial/) +* 原文作者 : [joyce echessa](http://www.appcoda.com/author/joyceechessa/) +* 译文出自 : [开发技术前线 www.devtf.cn](http://www.devtf.cn) +* 译者 : [kmyhy](https://github.com/kmyhy) + +Looking at the built in apps from Apple on your iOS device, you will notice the various animated transitions as you move from one view to another for example the way view controllers are presented in master-detail views with a swipe that slides the detail view controller over the master view controller as seen in the Messages app or Settings app and the various transitions that represent a segue to another view controller. + +观察 iOS 自带的 App,你会看到当你从一个视图导航到另一个视图时总是会显示各种各样的转换动画,以“主-从”视图为例(类似的程序有Messages App或者系统设置程序),一个轻扫动作能够让详情视图呈现在主视图之上,在呈现一个新的 View Controller 时还会带有丰富的转换动画。 + +iOS 7 introduced custom view controller transitions which make it possible for developers to create their own animated transitions from one view controller to the next in their apps. In this tutorial, we’ll take a look at how to do this. We’ll also look at how to create gesture driven transitions called interactive transitions. To follow along, download the starter project which we’ll be using throughout the tutorial. + +iOS 7 新增的自定义 View Controller 转换动画,使让开发者在 App 中发生从一个 View Controller 到另一个 View Controller 的导航时,创建自定义的动画效果。在本教程中,我们将学习这个内容。此外,我们还会学习如何通过手势来发起一个导航,也就是所谓的“交互式导航”。在开始之前,请下载本教程中用到的[开始项目](https://www.dropbox.com/s/4p6kk0g3qua1kvt/CustomTransitionsStarter.zip?dl=0)。 + +##Getting Started +##开始 + +To create custom transitions you have to follow three steps: + +创建自定义转换动画的,可以分成 3 个步骤进行: + +* Create a class that implements the UIViewControllerAnimatedTransitioning protocol. Here you will write code that performs the animation. This class is referred to as the animation controller. + + 指定一个类,实现 UIViewControllerAnimatedTransitioning 协议。在这个类中,我们编写执行动画的代码。这个类充当动画控制器。 + +* Before presenting a view controller, set a class as its transitioning delegate. The delegate will get a callback for the animation controller to be used when presenting the view controller. + + 在呈现一个 View Controller 之前,设置这个 View Controller 的 transitioningDelegate 属性为某个对象。这个对象在呈现这个 View Controller 的过程中将被调用,用于获取转场时应该使用什么对象作为动画控制器。 + +* Implement the callback method to return an instance of the animation controller from the first step. + + 实现回调方法,用于返回一个你在第一歩中创建的动画控制器对象。 + +Run the starter project and you will be presented with a table view of a list of items. There is an Action button on the navigation bar and when you tap it you’ll be presented with another view that appears in the usual modal style of sliding up from the bottom. We will write a custom transition for this view. + +运行开始项目,你将看到一个列表。在导航栏上有一个 Action 按钮,点击它,它将呈现一个新的 View Controller(以 modal 方式从底部向上弹起)。我们将为这个转换动作自定义转换动画。 + +![](http://www.appcoda.com/wp-content/uploads/2015/03/vid01.gif) + +##Custom Present Transition +##实现 View Controller 呈现动画 + +The first thing to do as stated previously, is to create the animation controller. Create a new class called CustomPresentAnimationController and make it a subclass of NSObject. Change its declaration as shown. + +如前面所述,首先需要创建动画控制器。新建一个 NSObject 子类 CustomPresentAnimationController,声明将遵守 UIViewControllerAnimatedTransitioning 协议: + +```swift +class CustomPresentAnimationController: NSObject, UIViewControllerAnimatedTransitioning { +``` + +UIViewControllerAnimatedTransitioning protocol has two required methods which we’ll add next. Add the following methods to the class. + +UIViewControllerAnimatedTransitioning 协议有两个必须实现的方法,我们来实现它们: + +```swift +func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval { + return 2.5 +} + +func animateTransition(transitionContext: UIViewControllerContextTransitioning) { + + let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)! + let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)! + let finalFrameForVC = transitionContext.finalFrameForViewController(toViewController) + let containerView = transitionContext.containerView() + let bounds = UIScreen.mainScreen().bounds + toViewController.view.frame = CGRectOffset(finalFrameForVC, 0, bounds.size.height) + containerView.addSubview(toViewController.view) + + UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: .CurveLinear, animations: { + fromViewController.view.alpha = 0.5 + toViewController.view.frame = finalFrameForVC + }, completion: { + finished in + transitionContext.completeTransition(true) + fromViewController.view.alpha = 1.0 + }) +} +``` +The first method specifies the length of the transition animation. For the demo app we’ve set it at 2.5 seconds, but you probably should set it to a smaller number in a real app. + +第一个方法用于指定动画播放的时长。对于本例,我们指定为 2.5 秒。当然在真正的 App 你可以把这个数字调小一些。 + +In the second method we use the transitionContext to get the view controller we are navigating from, to, the final frame the transition context should have after the animation completes and the container view which houses the views corresponding to the to and from view controllers. + +第二个方法带有一个 transitionContext 参数,通过这个对象,我们可以获取转换过程中涉及的 from 控制器(开始控制器)、to 控制器(到达控制器)、当动画完成后的 final frame (to 视图的最终位置及大小),以及 containerView——这个容器用于包含 from 控制器和 to 控制器的 UIView。 + +We then position the to view just below the bottom of the screen. Then we add the to view to the container view and in the animate closure, we animate the to view by setting its final frame to the location given by the transition context. We also animate the from view‘s alpha value so that as the to view is sliding up the screen over the from view, the from view will be faded out. The duration of the animation used is the one set in transitionDuration(transitionContext:). In the completion closure, we notify the transition context when the animation completes and then change the from view‘s alpha back to normal. The framework will then remove the from view from the container. + +然后,我们将 to 视图放在屏幕的下方,并将 to 视图添加到 containerView。在动画块中,我们将 to 视图移动到 final frame 的位置。同时将 from 视图的 alpha 值设置为 0.5,这样,当 to 视图向上滑入的同时 from 视图淡出。执行动画块时,第一个参数调用了transitionDuration(transitionContext:)方法,用这个方法的返回值作为动画块的执行时间。当动画完成,调用完成块,我们在完成块中通知 transitionContext 动画已经完成,同时将 from 视图的 alpha 值设回 1.0。 + +With the animation controller completed, we need to link it to a storyboard segue. + +写好动画控制器类之后,我们需要在故事板中将动画控制器分配给某个 segue。 + +Open the ItemsTableViewController.swift file and change the class declaration as shown. + +打开 ItemsTableViewController.swift ,修改类声明如下: + +```swift +class ItemsTableViewController: UITableViewController, UIViewControllerTransitioningDelegate { +``` +UIViewController has a property named transitionDelegate that supports custom transitions. When transitioning to a view controller, the framework checks this property to see if a custom transition should be used. UIViewControllerTransitioningDelegate supplies custom transitions. + +UIViewController 有一个 transitionDelegate 属性,用于指定自定义转换动画。当转场到一个 View Controller 时,框架会使用这个属性进行转换。而 UIViewControllerTransitioningDelegate 属性则负责提供自定义转换对象。 + +Open Main.storyboard and select the Present modally segue to Action View Controller and in the Attributes Inspector, set its Identifier to showAction. + +打开 Main.storyboard 选择导航到 Action View Controller 的那条 segue,在属性面板将它的 Identifier 设置为 showAction。 +![](http://www.appcoda.com/wp-content/uploads/2015/03/pic02.png) + +Back in ItemsTableViewController add the following to the class. + +回到 ItemsTableViewController ,添加如下代码: + +```swift +let customPresentAnimationController = CustomPresentAnimationController() + +override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + + if segue.identifier == "showAction" { + let toViewController = segue.destinationViewController as UIViewController + toViewController.transitioningDelegate = self + } +} +``` +Here we create an instance of our animation controller and then in the prepareForSegue() function, we detect the segue for the Action screen and set the transitionDelegate property of the destination view controller. + +这里我们创建了一个我们的动画控制器对象,然后在 prepareForSegue() 方法中,找到 ID 为 showAction 的 segue,设置它的 to 视图控制器的 transitioningDelegate 属性。 + +Add the following UIViewControllerTransitioningDelegate method to the class. This returns the instance of our custom animation controller. + +然后在这个类中实现 UIViewControllerTransitioningDelegate 方法。并在这些方法中返回我们的自定义动画控制器。 + +```swift +func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return customPresentAnimationController +} +``` +Run the application and you should see the Action view slide up slowly from the screen and bounce a little before settling. + +运行程序,你可以看到 Action 视图从屏幕底部缓缓弹起,并在停止前呈阻尼运动。 +![](http://www.appcoda.com/wp-content/uploads/2015/03/vid02.gif) + +If you want a slightly different effect, then change this statement in CustomPresentAnimationController.swift + +如果想看看其他效果,在 CustomPresentAnimationController.swift 中找到如下语句: + +```swift +toViewController.view.frame = CGRectOffset(finalFrameForVC, 0, bounds.size.height) +``` +修改为: + +```swift +toViewController.view.frame = CGRectOffset(finalFrameForVC, 0, -bounds.size.height) +``` +to the statement below, which changes the original position of the to view controller to be above the screen. + +这将让 to 视图从屏幕上方向下滑入。 + +Run the app and the Action view should fall from above. + +运行 App 效果如下: +![](http://www.appcoda.com/wp-content/uploads/2015/03/vid03.gif) + +##Custom Dismiss Transition +##自定义解散动画 +We’ve set a custom transition for presenting our view, but when it is dismissed, it uses the default transition set by Apple. + +前面我们自定义了呈现动画,但当视图解散时,仍然使用的是默认的动画效果。 +![](http://www.appcoda.com/wp-content/uploads/2015/03/vid04.gif) + +The UIViewControllerTransitioningDelegate also allows you to specify an animation controller to use when dismissing a view controller as well as when presenting one. We’ll create this next. + +UIViewControllerTransitioningDelegate 协议还允许我们指定一个动画控制器座位解散动画,就如同呈现时我们所做的一样。我们接下来就来实现它。 + +Create a class named CustomDismissAnimationController that is a subclass of NSObject. Modify its declaration as shown. + +创建一个 NSObject 子类 CustomDismissAnimationController,修改其类声明如下: + +```swift +class CustomDismissAnimationController: NSObject, UIViewControllerAnimatedTransitioning { +``` +Add the following to the class. + +添加如下方法: + +```swift +func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval { + return 2 +} + +func animateTransition(transitionContext: UIViewControllerContextTransitioning) { + let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)! + let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)! + let finalFrameForVC = transitionContext.finalFrameForViewController(toViewController) + let containerView = transitionContext.containerView() + toViewController.view.frame = finalFrameForVC + toViewController.view.alpha = 0.5 + containerView.addSubview(toViewController.view) + containerView.sendSubviewToBack(toViewController.view) + + UIView.animateWithDuration(transitionDuration(transitionContext), animations: { + fromViewController.view.frame = CGRectInset(fromViewController.view.frame, fromViewController.view.frame.size.width / 2, fromViewController.view.frame.size.height / 2) + toViewController.view.alpha = 1.0 + }, completion: { + finished in + transitionContext.completeTransition(true) + }) +} +``` +This is similar to the implementation of the presentation transition. In the animateTransition() function, we get the to and from view controllers. The to view controller here is the table view controller. We change its view’s alpha value so that it will start off as being faded when we start animating. We then add the view to the container and place it behind the from view controller’s view so that it won’t be visible just yet. + +其实这和呈现动画的实现真的很像。在 animateTransition() 方法中,我们获取 to/from 视图控制器。在这里,to 控制器变成了表格控制器。我们修改了 to 视图在动画正式开始之前的 alpha 值。然后将 to 视图添加到 containerView,并将它放到 from 视图的后面,以便它在一开始的时候不可见。 + +In the animation block, we animate the from view‘s size to have a width and height of 0, maintaining its center. This will have an effect of shrinking the from view to nothingness. We also animate the to view‘s alpha to being completely visible. + +在动画块中,我们让 from 视图的大小变成 0,0,但中心位置不变。这将导致 from 视图由大变小直至消失。同时将 to 视图的 alpha 值设置为 1.0 使其可见。 + +In ItemsTableViewController add the following property. + +在 ItemsTableViewController 中添加几个属性声明: + +```swift +let customDismissAnimationController = CustomDismissAnimationController() + +Add the following function to the class. + +然后加入如下方法: + +func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return customDismissAnimationController +} + +The UIViewControllerTransitioningDelegate protocol provides the above function which retrieves the animation controller of a dismissed view controller. + +UIViewControllerTransitioningDelegate 协议的这个方法返回一个动画控制器作为解散控制器。 + +Run the app. You should see the following animation. + +运行效果如下: +![](http://www.appcoda.com/wp-content/uploads/2015/03/vid05.gif) + +The animation isn’t what we expected. You can see the white frame of the from view shrinks as expected, but the image on the view doesn’t change in size. This is because changing the view’s frame doesn’t affect its children. We’ll fix this by using UIView snapshotting. + +动画效果不是我们所预料的。你可以看到 from 视图的白色框架确实是如预期的缩小了,但图片的尺寸根本不会改变。这是因为仅仅改变视图的 frame 并不会影响它的 subviews。我们可以用 UIView 的截屏功能解决这个问题。 + +UIView snapshotting works by taking a snapshot of an existing UIView and rendering it into a lightweight UIView. We will then use this snapshot in out animation and not the actual view. + +UIView 截屏功能会对一个 UIView 进行截图,将它绘制在一个“轻量级”的 UIView 中。我们将在淡入淡出动画中使用截屏进行动画,而不是直接用真正的视图进行动画。 + +Replace the animateTransition() function with the following. + +将 animateTransition() 方法代码修改为: + +```swift +func animateTransition(transitionContext: UIViewControllerContextTransitioning) { + let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)! + let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)! + let finalFrameForVC = transitionContext.finalFrameForViewController(toViewController) + let containerView = transitionContext.containerView() + toViewController.view.frame = finalFrameForVC + toViewController.view.alpha = 0.5 + containerView.addSubview(toViewController.view) + containerView.sendSubviewToBack(toViewController.view) + + let snapshotView = fromViewController.view.snapshotViewAfterScreenUpdates(false) + snapshotView.frame = fromViewController.view.frame + containerView.addSubview(snapshotView) + + fromViewController.view.removeFromSuperview() + + UIView.animateWithDuration(transitionDuration(transitionContext), animations: { + snapshotView.frame = CGRectInset(fromViewController.view.frame, fromViewController.view.frame.size.width / 2, fromViewController.view.frame.size.height / 2) + toViewController.view.alpha = 1.0 + }, completion: { + finished in + snapshotView.removeFromSuperview() + transitionContext.completeTransition(true) + }) +} +``` +Here, we create a snapshot of the from view controller‘s view, add it to the container and remove the from view from the container. We then shrink this snapshot in our animation and when the animation completes, we remove the snapshot view from the container. + +这里,我们创建了 from 视图的截屏,将截屏加到 containerView,然后将 from 视图从 containerView 中移除。 + +Run it and the animation should now run smoothly. + +现在,动画效果终于正常了。 +![](http://www.appcoda.com/wp-content/uploads/2015/03/vid06.gif) + +##Navigation controller transitions +##定制导航控制器转换动画 + +We’ve looked at adding a custom transition for modal view controller presentation where we added a transitioning delegate to the presenting view controller. However, setting a delegate on every view controller can get tiresome when working with a UITabBarController or UINavigationController. + +前面看到,要在呈现一个模式窗口的过程中呈现自定义动画,我们需要让负责呈现的 View Controller 实现一个 Transitioning 协议。但是,为每个 View Controller 都指定一个委托对象还是太麻烦了,尤其是当我们在使用 UITabBarController 或 UINavigationController 的时候。 + +These controllers give a simpler approach whereby the animation controller for a transition is supplied via the UITabBarControllerDelegate or UINavigationControllerDelegate. + +对于这两种控制器,有一种更简单的办法,可以使用 UITabBarControllerDelegate 或者 UINavigationControllerDelegate 协议来实现自定义转换动画。 + +We’ll see this in action by adding a custom transition to a navigation controller. + +我们来看一下自定义导航控制器转换动画的例子。 + +To start off, we create an animation controller. Create a class called CustomNavigationAnimationController, make it a subclass of NSObject and change its declaration as follows. + +首先,创建一个动画控制器。新建一个 NSObject 子类 CustomNavigationAnimationController。修改其类声明如下: + +```swift +class CustomNavigationAnimationController: NSObject, UIViewControllerAnimatedTransitioning { +``` + +Add the following to the class. I use a simplified version of this cube animation for this animation controller. The animation controller is set up as usual just like we’ve seen with the previous two animation controllers. Notice the reverse class variable. We use this to determine the direction of the animation, depending on whether we are moving from master to detail view or vice versa. + +然后编写如下代码。这里我准备实现一个简单的魔方动画。这个动画控制器和前面实现的两个动画控制器类似。注意 reverse 变量,我们用这个变量来指定动画的方向,也就是说要进行的动画是主视图->从视图,抑或相反方向。 + +```swift +var reverse: Bool = false + +func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval { + return 1.5 +} + +func animateTransition(transitionContext: UIViewControllerContextTransitioning) { + let containerView = transitionContext.containerView() + let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)! + let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)! + let toView = toViewController.view + let fromView = fromViewController.view + let direction: CGFloat = reverse ? -1 : 1 + let const: CGFloat = -0.005 + + toView.layer.anchorPoint = CGPointMake(direction == 1 ? 0 : 1, 0.5) + fromView.layer.anchorPoint = CGPointMake(direction == 1 ? 1 : 0, 0.5) + + var viewFromTransform: CATransform3D = CATransform3DMakeRotation(direction * CGFloat(M_PI_2), 0.0, 1.0, 0.0) + var viewToTransform: CATransform3D = CATransform3DMakeRotation(-direction * CGFloat(M_PI_2), 0.0, 1.0, 0.0) + viewFromTransform.m34 = const + viewToTransform.m34 = const + + containerView.transform = CGAffineTransformMakeTranslation(direction * containerView.frame.size.width / 2.0, 0) + toView.layer.transform = viewToTransform + containerView.addSubview(toView) + + UIView.animateWithDuration(transitionDuration(transitionContext), animations: { + containerView.transform = CGAffineTransformMakeTranslation(-direction * containerView.frame.size.width / 2.0, 0) + fromView.layer.transform = viewFromTransform + toView.layer.transform = CATransform3DIdentity + }, completion: { + finished in + containerView.transform = CGAffineTransformIdentity + fromView.layer.transform = CATransform3DIdentity + toView.layer.transform = CATransform3DIdentity + fromView.layer.anchorPoint = CGPointMake(0.5, 0.5) + toView.layer.anchorPoint = CGPointMake(0.5, 0.5) + + if (transitionContext.transitionWasCancelled()) { + toView.removeFromSuperview() + } else { + fromView.removeFromSuperview() + } + transitionContext.completeTransition(!transitionContext.transitionWasCancelled()) + }) +} +``` +Open ItemsTableViewController.swift and modify the class declaration as follows. + +打开 ItemsTableViewController.swift,将类声明修改为: + +```swift +class ItemsTableViewController: UITableViewController, UIViewControllerTransitioningDelegate, UINavigationControllerDelegate { +``` +UINavigationControllerDelegate supplies the animation controller. + +UINavigationControllerDelegate 协议负责提供动画控制器对象。 + +Add the following property to the class. + +添加如下属性: + +```swift +let customNavigationAnimationController = CustomNavigationAnimationController() +``` +Add the following at the end of viewDidLoad(). + +在 viewDidLoad 方法最后添加: + +```swift +navigationController?.delegate = self +``` +The above sets the host navigation controller’s delegate so that the new transition delegate methods can be received. + +这句代码将导航控制器的 delegate 设置为 self,这样 ItemsTableViewController 就必须实现新的转换委托方法。 + +Then add the following to the class. + +新增如下方法: + +```swift +func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { + customNavigationAnimationController.reverse = operation == .Pop + return customNavigationAnimationController +} +``` + +The above function is called to request for an animation controller to navigate between the from and to view controllers and it returns an instance of our animation controller. The direction of the transition is based on whether this is a push or pop navigation operation. + +这个方法会在导航控制器发生导航时调用,导航控制器会要求这个方法返回一个动画控制器实例。转换的方向根据动画的类型(Push或Pop)而定。 + +Run the app. Select a table view cell and you should see the animation below. + +运行程序。点击表格单元格,效果如下: +![](http://www.appcoda.com/wp-content/uploads/2015/03/vid07.gif) + +## 交互式转换 +##Making it Interactive +We’ll make the above transition interactive, i.e. the user will be able to control the transition with gestures. + +我们将让上述转换变成“交互式转换”,这样用户可以用手势来进行视图控制器的转换。 + +iOS built-in apps come with this feature. As an alternative to the back button, you can initiate a transition by swiping from the left side of the screen. You can use a short swipe to briefly view the master view and then cancel the transition. A long swipe initiates the pop navigation operation. + +iOS 有许多 App 支持这个特性。通过交互式转换,你可以从左向右滑动来替代返回按钮的功能。你还可以用小幅度的轻扫手势查看 master 视图的内容,然后中途取消转换。大幅度的轻扫手势则是进行 Pop 导航操作。 +![](http://www.appcoda.com/wp-content/uploads/2015/03/vid08.gif) + +To get started, we need an interaction controller. Interactive controllers use the UIViewControllerInteractiveTransitioning protocol. The navigation controller delegate or the transitioning delegate requests for an optional interaction controller after requesting an animation controller. + +首先,需要创建一个交互式控制器。交互式控制器需要实现 UIViewControllerInteractiveTransitioning 协议。UINavigationControllerDelegate 或 Transitioning 委托在请求完一个动画控制器之后还会请求一个交互式控制器。 + +Let’s create the interaction controller. Create a new class and name it CustomInteractionController and make it a subclass of UIPercentDrivenInteractiveTransition. + +接下来创建交互式控制器。新建一个 UIPercentDrivenInteractiveTransition 子类,名为 CustomInteractionController。 + +UIPercentDrivenInteractiveTransition implements the UIViewControllerInteractiveTransitioning protocol so we wont have to add that to our class. + +UIPercentDrivenInteractiveTransition 类已经实现了 UIViewControllerInteractiveTransitioning 协议,因此我们的类就没有必要再声明对这个协议的实现了。 + +To use UIPercentDrivenInteractiveTransition, your animation controller must use a single UIView animation, so that the animation will be able to be stopped, reversed and played. + +对于 UIPercentDrivenInteractiveTransition 子类,我们必须使用单独的 UIView 动画,以便动画能够被停止、返回和播放。 + +Add the following to the class. + +继续添加如下代码: + +```swift +var navigationController: UINavigationController! +var shouldCompleteTransition = false +var transitionInProgress = false +var completionSeed: CGFloat { + return 1 - percentComplete +} + +func attachToViewController(viewController: UIViewController) { + navigationController = viewController.navigationController + setupGestureRecognizer(viewController.view) +} + +private func setupGestureRecognizer(view: UIView) { + view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: "handlePanGesture:")) +} + +func handlePanGesture(gestureRecognizer: UIPanGestureRecognizer) { + let viewTranslation = gestureRecognizer.translationInView(gestureRecognizer.view!.superview!) + switch gestureRecognizer.state { + case .Began: + transitionInProgress = true + navigationController.popViewControllerAnimated(true) + case .Changed: + var const = CGFloat(fminf(fmaxf(Float(viewTranslation.x / 200.0), 0.0), 1.0)) + shouldCompleteTransition = const > 0.5 + updateInteractiveTransition(const) + case .Cancelled, .Ended: + transitionInProgress = false + if !shouldCompleteTransition || gestureRecognizer.state == .Cancelled { + cancelInteractiveTransition() + } else { + finishInteractiveTransition() + } + default: + println("Swift switch must be exhaustive, thus the default") + } +} +``` + +The attachToViewController() method is passed a reference of the navigation controller which it uses to initiate a pop transition when a gesture occurs. We then set up a gesture recognizer, which will call the handlePanGesture() method when a swipe is made. This checks the gesture recognizer state and does the following at each stage: + +attachToViewController方法中,引用了一个导航控制器并保存到实例变量,以便当手势发生时用于初始化 Pop 转换。然后在视图中添加了一个手势识别器,并将手势处理方法指定为 handlePanGesture()方法。手势处理方法将处理手势的每一个状态,包括: + +* Began: It sets transitionInProgress to true and initiates a pop navigation. + + 开始:将 transitionInProgress 设置为 true,然后开始进行 Pop 导航。 + +* Changed: Here the gesture is in progress, so it determines the percentage of the transition. A 200 point swipe will cause the transition to be 100% complete. It then determines if the transition should complete depending on where the gesture finishes. Here we check if the user swiped to at least half the screen before releasing. + + 改变:这个状态表示手势尚处于进行过程中,因此需要计算出转换的进度(百分比)。这里假设扫动距离>= 200像素视同手势100%的完成。如果小于此距离则计算手势划过的距离占200像素的百分之几,并以此作为动画完成的进度。同时我们检查用户在释放手指前,划过的距离是否超过了一半,并将判断结果保存到 shouldCompleteTransition 变量。 + +* Cancelled/Ended: Sets the transitionInProgress to false and cancels the transition if shouldCompleteTransition was set to false or if the gesture was cancelled. Otherwise, the transition is completed. + + 取消/结束:将 transitionInProgress 设置为 false,同时判断 shouldCompleteTransition 变量是否为 false 或者手势已取消,如果是则取消动画,否则让动画完成。 + +We used a computed property to determine the completion speed. completionSeed is a UIPercentDrivenInteractiveTransition property that informs the framework how much of the animation remains when a gesture completes. A larger number will make the view controller snap back quicker if the interaction is cancelled. + +我们使用一个计算属性来衡量完成速度。 UIPercentDrivenInteractiveTransitio 有一个 completionSeed 属性,这个属性用于告诉框架当手势已经完成时,还剩下多少动画需要播放。当手势取消时,如果这个数值越大,则 View Controller 弹回去的速度就越快。 + +To use our interaction controller, open ItemsTableViewController.swift and add the following to the class. + +要使用这个交互式控制器,打开 ItemsTableViewController.swift,添加如下属性: + +```swift +let customInteractionController = CustomInteractionController() +``` + +Add the following at the beginning of the navigationController(_:animationControllerForOperation: +fromViewController:toViewController:) function. + +在 navigationController(_:animationControllerForOperation:fromViewController:toViewController:)方法开始部分加入: + +```swift +if operation == .Push { + customInteractionController.attachToViewController(toVC) +} +``` + +This calls the CustomInteractionController’s attachToViewController() method and passes it a reference of the to view controller when it detects a push navigation operation. + +如果是 Push 动画,则调用 CustomInteractionController 的 attachToViewController() 方法,将 toViewController 传递给动画控制器。 + +Then add the following to the class. + +然后添加如下方法: + +```swift +func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { + return customInteractionController.transitionInProgress ? customInteractionController : nil +} +``` + +After the framework asks for and gets an animation controller, it asks for an interaction controller using the method above. The above returns an instance of our interaction controller if a transition is in progress. + +当框架请求一个动画控制器之时,它也会用上面的这个方法请求一个交互式控制器。在这个方法中,我们判断是否转换开始,如果是,则返回一个交互式控制器给它。 + +Run the app and you should see the transition shown. + +运行 App ,查看转换动画效果: +![](http://www.appcoda.com/wp-content/uploads/2015/03/vid09.gif) + +##Conclusion +##结论 +We’ve looked at how to create custom view controller transitions and how to make a transition interactive. With these features, developers now have full control over the animations used in their application as it transitions from one view controller to the next and thus can build unique experiences to delight their users. You can download the completed project here. + +我们学习了如何创建一个自定义的 View Controller 转换,以及如何创建交互式转换。通过这些特性,开发者可以在 App 中彻底控制从一个视图控制器切换到另一个视图控制器时的转换动画,并通过创建一系列独特的体验来取悦他们的用户。你可以[从此](https://github.com/appcoda/CustomViewTransitionDemo)下载完整的示例项目。 + +Note: This tutorial is also available in Chinese. We’re going to support other languages soon. If you want to join our translation team, please contact us. + + +注意: 本教程有一个中文版本(台湾繁体)。我们还将支持更多语言。如果你想参加我们的翻译小组,请和[我们](http://www.appcoda.com/contact/)联系。 \ No newline at end of file diff --git "a/issue-20/\350\213\271\346\236\234\351\223\205\347\254\224\345\205\245\351\227\250\346\225\231\347\250\213.md" "b/issue-20/\350\213\271\346\236\234\351\223\205\347\254\224\345\205\245\351\227\250\346\225\231\347\250\213.md" new file mode 100644 index 0000000..b046850 --- /dev/null +++ "b/issue-20/\350\213\271\346\236\234\351\223\205\347\254\224\345\205\245\351\227\250\346\225\231\347\250\213.md" @@ -0,0 +1,619 @@ +> * 原文链接 : [Apple Pencil Tutorial: Getting Started](http://www.raywenderlich.com/121834/apple-pencil-tutorial) +* 原文作者 : [Caroline Begbie](http://www.raywenderlich.com/u/caroline) +* 译者 : [kmyhy](https://github.com/kmyhy) + + +有许多人都在为拥有了新的 iPad Pro 以及上面附带的苹果铅笔而兴奋不已。 + +如果你像我一样,体会到了用笔涂鸦的好处,就会恨不得在你所有的 App 中支持苹果铅笔特性。 + +自从我购买了第一代的 iPad 开始,我就在等待这样的设备出现了。就如我的涂鸦作品中所看到的,我不是伦布兰特(译者注:荷兰画家),但我仍然可以用苹果铅笔记录我所想。我不禁在心中幻想,如果艺术家们使用苹果铅笔作画,他们又能创造出多么令人惊异的作品? + +在本教程中,你将学习如何在 App 中支持苹果铅笔。有几个关键的知识点: + +- 如何控制力量 + +- 如何更加精确 + +- 如何绘制阴影 + +- 如何使用橡皮擦 + +- 如何通过预判和实际绘制来提升体验 + +最终,你将了解如何将苹果铅笔集成到你的 App 中! + +##前提 + +要学习本教程,你需要具备: + +- 一个 iPad Pro 和一只苹果铅笔。无法在模拟器中测试苹果铅笔。同时,苹果铅笔无法在老的 iPad 设备是运行,只支持新的 iPad Pro。 + +- Xcode 7.1 及以上,iOS 9.1 及以上。 + +- 对 Core Graphics 有一定的了解。你需要知道什么是上下文,如何创建上下文以及如何绘制线条。你可以看一下我们的 Core Graphics 教程第一部分 —— 这将大大加快你的学习速度。这个 App 会提醒你去喝水 :] + +##开始 + +通过本教程,你会编写一个 App,叫做 Scribble。这是一个简单的 App,功能是允许你使用响应式 UI 比如压力感应和阴影绘制进行涂鸦。 + +[下载并浏览 Scribble 项目](http://cdn4.raywenderlich.com/wp-content/uploads/2016/12/Scribble-StarterV1.zip)。在你的 iPad Pro 上运行这个程序,使用苹果铅笔或者手指,在屏幕上画画,在画的时候让你的手保持放松。 + +你会发现,和老式的 iPad 不同,将手掌放到屏幕上是没有反应的,你必须小心地让手的大部分面积保持悬空,因为只有小面积的接触被视作触摸。 + +![](http://cdn4.raywenderlich.com/wp-content/uploads/2016/01/GettingStarted.png) + +晃动 iPad 可以清空屏幕 —— 就像一块神奇画板! + +实际上,Scribble 只是一个简单的 App,它由一块 cavas (画布)视图构成,cavas 负责俘获来自于苹果铅笔或手指的触摸。它通过不停地刷新画面来显示你的触摸。 + +请看一眼 CanvasView.swift 中的代码。 + +最核心的代码位于 touchesMoved(_:withEvent:) 方法,这个方法在用户与 canvas 视图产生交互时触发。在这个方法中,创建了一个 Core Graphics 上下文并将图形绘制到图形上下文中。 + +touchesMoved(\_:withEvent:) 方法会调用 drawStroke(_:touch:) 方法,后者会在图形上下文中绘制凑够上一次触摸到当前触摸之间的线段。 + +touchesMoved(_:withEvent:) 方法将图形上下文中画好的图形用于更新 canvas 视图。 + +看到了吧?是不是很简单?:] + +##第一次用苹果铅笔进行绘图 + +哪怕是在数位环境中,用手指来作画也并非易事。而苹果铅笔则更接近于以往传统的作画方式,就像用纸和笔在作画一样。 + +现在,准备使用苹果铅笔的第一个特性 —— 力量。当笔尖在屏幕上用力压的时候,笔画变得更粗了。这个特性无法在使用手指时获得,这个技巧我们随后将会学到。 + +力量的大小保存在 touch.force 中。一个 1.0 的力是一个力量中等的触摸,为了获得适当的笔画粗细,我们需要乘以一定的系数。请接着往下看…… + +打开 CanvasView.swift,在类的顶部加入一个常量声明: + +```swift +private let forceSensitivity: CGFloat = 4.0 +``` + +你可以修改 forceSensitivity 的值,可以让笔画粗细对压力的敏感度增强或者减弱。 + +找到 lineWidthForDrawing(_:touch:)。这个方法用于计算笔画的粗细。在返回语句之前,加入: + +```swift +if touch.force > 0 { + lineWidth = touch.force * forceSensitivity +} +``` + +在这段代码中,我们用 touch.force 乘以 forceSensitivity 来计算笔画宽度,但请注意,这只对苹果铅笔有效而对手指无效。如果你使用手指作画,则 touch.force 应当为 0,这样你就不会改变笔画的宽度。 + +编译运行程序。用苹果铅笔胡乱画一些内容,你会注意到笔画的粗细会根据笔尖在屏幕上所施加的力道的大小而变: + +![](http://cdn4.raywenderlich.com/wp-content/uploads/2016/12/Force.png) + +##让线条更光滑 + +在画的时候你可能注意到了,线条上有一些尖锐的突出点,而不是光滑曲线。在苹果铅笔出现之前,为了更像传统作画我们必须做一些很复杂的事情,比如将笔画转换成样条曲线。但有了苹果铅笔之后,这种方法完全没有必要了。 + +苹果告诉我们,iPad Pro 对一个触摸每秒扫描 120 次,但当苹果铅笔放到屏幕上时,这个扫描频率立即放大了 2 倍,即每秒 240 次。 + +iPad Pro 的屏幕刷新率是 60 Hz,即每秒 60 次。也就是说,120 Hz 的扫描速度理论上在每一次刷新都能够识别出 2 个触摸。如果在需要处理大量事务的情况下,某一帧中的触摸有可能会丢失一些,因为主线程阻塞导致来不及处理。 + +试着快速画一个圆。它应该是圆的,但结果更像是一个多边形: + +![](http://cdn1.raywenderlich.com/wp-content/uploads/2016/12/Circle.png) + +苹果用“合并的触摸”来解决这个问题。基本上,它们会将丢失的触摸放到一个新的 UIEvent 数组中,你可以用 coalescedTouchesForTouch(_:) 方法来访问。 + +找到 CanvasView.swift 中的 touchesMoved(_:withEvent:) 方法,将这一句: + +```swift +drawStroke(context, touch: touch) +``` + +替换为: + +```swift + +// 1 +var touches = [UITouch]() + +// 2 +if let coalescedTouches = event?.coalescedTouchesForTouch(touch) { + touches = coalescedTouches +} else { + touches.append(touch) +} + +// 3 +print(touches.count) + +// 4 +for touch in touches { + drawStroke(context, touch: touch) +} +``` + +让我们来过一遍这些代码。 + +1. 首先,创建一个数组,用于保存我们要处理的的所有触摸。 + +2. 检查“合并的触摸”,如果不为空,我们将“合并的触摸”添加到新数组中。如果为空,我们只需要添加一个触摸到数组。 + +3. 用一个 Log 语句打印 touches 数组的长度。 + +4. 最终,将原来只调用一次 drawStroke(\_:touch:) 方法替换为针对 touches 数组中的每一个触摸都调用一次 drawStroke(_:touch:) 方法。 + +运行程序,用苹果铅笔随意绘制一些圆,你会发现笔画光滑顺溜,粗细控制由心: + +![](http://cdn4.raywenderlich.com/wp-content/uploads/2016/12/Curlicue.png) + +注意控制台中的输出。你会看到当你使用笔作画时,所接收到的触摸要比用手指作画时要多得多。 + +此外,通过使用“合并的触摸”,用笔绘制的圆要更圆滑,因为 iPad Pro 一旦侦测到你使用的是笔,扫描的次数会增加一倍。 + +##倾斜 + +现在你已经爱上了这种在 App 中流畅作画的方式。但是,如果你看过关于苹果铅笔的评论,应该还记得它有一个像铅笔一样的绘制阴影的功能。而这只需要用户倾斜笔尖。但随即你就会发现阴影并不会自动出现 —— 这还需要我们的开发者编写代码它才会出现 :] + +###高度、方位角和单位向量 + +本节,我将介绍如何识别倾斜。而在下一节,你将支持简单的阴影绘制功能。 + +在使用苹果铅笔时,你可以在 3 个维度上进行旋转。上、下方向的旋转,我们称之为 Altitude(高度),而水平方向上的旋转称之为 Azimuth(方位角): + +![](http://cdn3.raywenderlich.com/wp-content/uploads/2016/12/AzimuthDiagram.png) + +在 iOS 9.1 中,UITouch 有一个新的属性 altitudeAngle,这个属性专门用于苹果铅笔。这是一个以弧度为单位的角度。当平放于 iPad 表面时,这个 altitude 为 0。当笔直立在屏幕表面时,altitude 为 π/2。要知道 2π 为一个整圆,因此 π/2 就是 90 度。 + +在 UITouch 上有两个新方法,用于获取 azimuth:azimuthAngleInView(\_:) 以及 azimuthUnitVectorInView(\_:)。性能代价最小的是 azimuthUnitVectorInView(\_:) 方法,但这两个方法都很有用。要用哪一个方法,取决于你需要计算什么。 + +你会看到如何使用 azimuth 的单位向量。注,一个单位向量代表一个从原点(0,0) 指向某个方向的长度为 1 的向量: + +![](http://cdn1.raywenderlich.com/wp-content/uploads/2016/12/AzimuthVector.png) + +你可以来试一试,在 touchesMoved(\_:withEvent:) 方法中,在 guard 语句后面添加如下代码: + +```swift +print(touch.azimuthUnitVectorInView(self)) +``` + +运行程序。将 iPad 置为横屏模式 —— Scribble 只支持横屏,以便我们将注意力集中在苹果铅笔 —— 手握苹果铅笔,将笔尖放在屏幕左边,然后向右边倾斜。 + +在控制台中,你可能无法看到非常精确的值,但这个向量应该近似于: x 方向为 1,y 方向为 0,即(1,0)。然后沿反时针方向旋转 90 度,直到笔尖指向 iPad 的下方。这个方向的值应该约等于(0,-1)。 + +注意,x 方向是 cosine(余弦),y 方向是 sine(正弦)。例如,如果你向上图一样握笔 —— 从原来的水平方向逆时针旋转 45 度 —— 则单位向量为(cos(45),sine(-45)) 或者 (0.7071,-0.7071)。 + +> 注意:如果你对向量数学了解不多,则最好先预习一下。这个教程 [Sprite Kit 游戏中的三角学](http://www.raywenderlich.com/90520/trigonometry-games-sprite-kit-swift-tutorial-part-1)也许对你有所帮助。 + +了解完苹果铅笔的方向的改变和它笔尖的指向的关系之后,将上述打印语句从代码中删除。 + +###绘制阴影 + +现在,你已经知道如何计算出笔的倾斜,是时候在 Scribble 中添加一些阴影了。 + +如果笔处于一种自然作图的角度,你通过力道大小来决定笔画的粗细,当用户将笔倾斜,你就可以用力道大小来决定阴影的透明度。 + +你可以通过笔画的方向以及执笔的方向来计算线条的厚薄。 + +如果你还不太明白,你可以找一张纸和一只铅笔,试着将笔倾斜到一边进行描绘,这样笔尖和纸张的接触面积将被最大化。当你向笔倾斜的方向进行绘制时,阴影是细的。但当你以放笔的方向成 90 度方向绘制时,笔划将最粗: + +![](http://cdn3.raywenderlich.com/wp-content/uploads/2016/12/RealPencilShading.png) + +#### 使用纹理 + +事情的第一步是修改线条所使用的纹理,以使它更接近于真正的铅笔。初始项目的资源集中包含了一张图片 PencilTexture ,我们将用它做纹理。 + +在 CanvasView 头部加入属性声明: + +```swift +private var pencilTexture = UIColor(patternImage: UIImage(named: "PencilTexture")!) +``` + +这将允许我们在作图中把 pencilTexture 作为颜色一样使用,而不是现在所使用的默认的红色。 + +在 drawStoke(\_:touch:) 方法中找到如下语句: + +```swift +drawColor.setStroke() +``` + +修改为: + +```swift +pencilTexture.setStroke() +``` + +运行程序。哈!你的线条现在已经和真正的铅笔很像了: + +![](http://cdn1.raywenderlich.com/wp-content/uploads/2016/12/Tree.png) + +> 注意:在本教程,我们使用纹理的方式十分简单。在真正的绘画类 App 中,笔触的实现是非常复杂的,但作为入门,上面的这个方法就足够了。 + +为了明确笔要旋转到什么程度才足以构成阴影,请在 CanvasView 的开始加入常量声明: + +```swift +private let tiltThreshold = π/6 // 30º +``` + +因为每个人握笔姿势的差异,这个值不一定适合于你,你可以改变这个值。 + +> 注意:同时按下 Option 键和 p 键可以输入 π 字符。π 是 CanvasView.swift 中定义的常量,等于 CGFloat(M_PI)。 + +在编写图形程序时,在弧度和度之间来回转换是非常频繁的。关于度和弧度之间的关系,请看来自于维基百科的[这张图片](https://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Unit_circle_angles_color.svg/2000px-Unit_circle_angles_color.svg.png)。 + +接着,在 drawStroke(_:touch:) 方法中找到这句: + +```swift +let lineWidth = lineWidthForDrawing(context, touch: touch) +``` + +修改为: + +```swift +var lineWidth:CGFloat +if touch.altitudeAngle < tiltThreshold { + lineWidth = lineWidthForShading(context, touch: touch) +} else { + lineWidth = lineWidthForDrawing(context, touch: touch) +} +``` + +这里,我们增加了一个片段,检查笔的倾角(altitud)是否大于 π/6 即 30 度。如果是,调用 shading 方法,否则调用 drawing 方法。 + +然后,在 CanvasView 中增加这个方法: + +```swift +private func lineWidthForShading(context: CGContext?, touch: UITouch) -> CGFloat { + + // 1 + let previousLocation = touch.previousLocationInView(self) + let location = touch.locationInView(self) + + // 2 - vector1 is the pencil direction + let vector1 = touch.azimuthUnitVectorInView(self) + + // 3 - vector2 is the stroke direction + let vector2 = CGPoint(x: location.x - previousLocation.x, y: location.y - previousLocation.y) + + // 4 - Angle difference between the two vectors + var angle = abs(atan2(vector2.y, vector2.x) - atan2(vector1.dy, vector1.dx)) + + // 5 + if angle > π { + angle = 2 * π - angle + } + if angle > π / 2 { + angle = π - angle + } + + // 6 + let minAngle: CGFloat = 0 + let maxAngle = π / 2 + let normalizedAngle = (angle - minAngle) / (maxAngle - minAngle) + + // 7 + let maxLineWidth: CGFloat = 60 + var lineWidth = maxLineWidth * normalizedAngle + + return lineWidth +} +``` + +因为数学计算比较复杂,我们来捋一捋这些代码: + +1. 分别将上一个触摸和当前侦测到的触摸保存到局部变量。 + +2. 保存笔的方位角向量。 + +3. 将当前笔画的方向向量保存到局部变量。 + +4. 计算两个方向向量之差。 + +5. 将上一步的计算结果限制到 0-90 度之间。如果角度为 90 度,笔画为最粗。注意所有的角度都是弧度,π/2 为 90 度。 + +6. 将上一步的结果进行归一化,即转换为 0-1 之间的值,1 表示 90 度。 + +7. 将最大线宽(60)乘以前面得到单位向量,即得到相应的阴影宽度。 + +> 注意:无论你是否使用笔,下面的公式都是适用的: +> 求向量的角度:角度 = atan2(对边,邻边) +> 归一化:归一化 = (值 - 最小值)/(最大值 - 最小值) + +运行程序。以下图所示角度握笔,你就可以进行描影法作图了。不要改变角度,画出一张描影图。 + +![](http://cdn4.raywenderlich.com/wp-content/uploads/2016/12/Calligraphic.png) + +观察随着笔画方向的变化,线条发生了粗细不同的变化。以这种方式实现的描影图,虽然看起来斑斑点点,但我们却看到了无限的可能。 + +####用方位角改变宽度 + +还有一件事情需要做:如果你用真实的铅笔以90度角作画,线条会变得比你用其他角度画要窄。但是,如果你试图用苹果铅笔这样做,线条的宽度不会改变。 + +除了方位角,你还应该在计算线条宽度时考虑进铅笔的高度。 + +在 CanvasView 类中增加常量声明: + +```swift +private let minLineWidth: CGFloat = 5 +``` + +这是描影图线条的最小宽度 —— 你可以根据自己的喜好修改它 :] + +在 lineWidthForShading(\_:touch:) 方法最后,返回语句之前,加入代码: + +```swift +// 1 +let minAltitudeAngle: CGFloat = 0.25 +let maxAltitudeAngle = tiltThreshold + +// 2 +let altitudeAngle = touch.altitudeAngle < minAltitudeAngle ? minAltitudeAngle : touch.altitudeAngle + +// 3 +let normalizedAltitude = 1 - ((altitudeAngle - minAltitudeAngle) / (maxAltitudeAngle - minAltitudeAngle)) + +// 4 +lineWidth = lineWidth * normalizedAltitude + minLineWidth +``` + +> 注意:请确认在 lineWidthForShading(\_:touch:) 方法中添加代码,而不是在 lineWidthForDrawing(\_:touch:) 方法中。 + +这段代码分别做了以下事情: + +1. 理论上,笔的最小高度是 0,表明笔被平放在 iPad 上,同时笔尖没有接触到屏幕,但这样的话,altitude 根本是无法获得的。实际的最小值应当是 0.2 左右,但这里将它定为 0.25。 + +2. 如果 altitude 小于最小值,用最小值代替。 + +3. 就像我们在前面做的,将 altitude 归一化为 0-1 之间的值。 + +4. 最终,将用 azimuth(方位角)算出的线宽乘以上面归一化向量的值,然后加上最小线宽。 + +运行程序,当你使用描影作图时,改变比的高度,线条会变窄或宽。加入高度之后,会让你画出的线条变得更加连续和平滑: + +![](http://cdn3.raywenderlich.com/wp-content/uploads/2016/12/ShadeDraw.png) + +####改变透明度 + +本节最后一个任务是随着力道的大小改变纹理的透明度,让描影图看起来更加真实。 + +在 lineWidthForShading(_:touch:) 方法返回语句之前,加入: + +```swift +let minForce: CGFloat = 0.0 +let maxForce: CGFloat = 5 + +let normalizedAlpha = (touch.force - minForce) / (maxForce - minForce) + +CGContextSetAlpha(context, normalizedAlpha) +``` + +看过之前介绍的代码,这里的代码其作用应当是显而易见的了。我们简单地读取了 force 属性,并对它进行归一化,然后用它来设置图形上下文的 alpha 值。 + +运行程序,用不同的压力进行作画: + +![](http://cdn5.raywenderlich.com/wp-content/uploads/2016/12/Shaded.png) + +###手指 vs 苹果铅笔 + +如果你和我一样,这时已经画错了好几个地方了,这时候你可能会想将这些地方擦掉。 + +在本节,你将看到手指和苹果铅笔用起来有什么不同。具体地说,你将修改 App,让手指来扮演一个忠实的橡皮擦。 + +要区分当前触摸的是手指还是苹果铅笔非常简单 —— 直接使用 UITouch 的 type 属性。 + +在 CanvasView 头部加入一个属性,用于保存橡皮擦的颜色。你可以在 canvas 上画出背景色,以这种方式来充当橡皮擦的“擦除”功能。这法子真不错,嗯?:] + +```swift +private var eraserColor: UIColor { + return backgroundColor ?? UIColor.whiteColor() +} +``` + +这里我们将 eraseColor 设置为视图的背景色,如果背景色为空,则将它设置为白色。接着在 drawStoke(_:touch:) 方法中找到下列代码: + +```swift +if touch.altitudeAngle < tiltThreshold { + lineWidth = lineWidthForShading(context, touch: touch) +} else { + lineWidth = lineWidthForDrawing(context, touch: touch) +} + +pencilTexture.setStroke() +``` + +替换为: + +```swift +if touch.type == .Stylus { + if touch.altitudeAngle < tiltThreshold { + lineWidth = lineWidthForShading(context, touch: touch) + } else { + lineWidth = lineWidthForDrawing(context, touch: touch) + } + pencilTexture.setStroke() +} else { + lineWidth = 20 + eraserColor.setStroke() +} +``` + +这里我们增加了一个判断,检查触摸的类型是手指还是笔,如果是手指,我们修改线条的宽度并应用橡皮擦颜色进行绘制。 + +运行程序,现在你可以用手指来修理不整齐的毛边或者擦去任何东西! + +![](http://cdn5.raywenderlich.com/wp-content/uploads/2016/12/Eraser.png) + +###在模拟手指上的压力 + +你知道在 iOS 8 中可以模拟出指尖触摸在屏幕上的压力吗?UITouch 有一个属性 majorRaiuds,顾名思义,可以表示触摸的大小。在前面的代码块中找到这行代码: + +```swift +lineWidth = 20 +``` + +替换为: + +```swift +lineWidth = touch.majorRadius / 2 +``` + +运行程序。用描影法画出一块颜色,然后分别用指尖和指腹擦出粗细不同的痕迹: + +![](http://cdn3.raywenderlich.com/wp-content/uploads/2016/12/EraserForce.png) + +在你已经习惯了实用苹果铅笔涂鸦之后,是不是感觉到用手指作画更加别扭和痛苦?:] + +##减少延迟 + +你可能以为在笔尖从 iPad 上面划过的一瞬间就会立即画出线条。实际不是这样 —— 这只是想象而已,以为在触摸和绘制线条之间会存在延迟。苹果有一个专门对付这个问题的概念,叫做:触摸预判。 + +这简直让人难以置信,全知的苹果可以知道笔或者手指即将画到哪里。这个预判就保存在 UIEvent 的一个数组里。你可以提前绘制这些预判的触摸。是不是很酷?:] + +在开始使用预判触摸之前,需要解决一点小小的技术障碍。现在,我们在图形上下文中的绘图操作,会立即显示在 canvas 视图中。 + +你需要将预判触摸绘制到 canvas,但当真实的触摸到来时,应该撤销预判的触摸。 + +例如,如果你根据预判绘制了一个 S 形曲线,但后来发现实际绘制时的方向跟预判不符,则预判是错误的,应当被撤销。下图演示了这个问题。S 形曲线用红色表示,预判的触摸显示为蓝色。 + +![](http://cdn2.raywenderlich.com/wp-content/uploads/2016/12/Predicted.png) + +要解决这个问题,你需要: + +- 增加一个 UIImage 属性,名为 drawingImage,用于保存图形上下文中的真实的 —— 而非预判的 —— 触摸。 + +- 在触摸移动事件处理方法中,将 drawingImage 绘制到图形上下文。 + +- 将真实的触摸绘制到图形上下文,同时将其保存到 drawingImage 属性,而不是 canvasView.image。 + +- 将预判的触摸绘制到图形上下文。 + +- 图形上下文绘制完预判的触摸后,将被赋给 canvasView.image,并显示给用户。 + +通过这种方式,预判的触摸并不会绘制到 drawingImage 中,每当一个触摸事件到达,所绘制的预判的触摸都会被删掉。 + +### 家庭作业:删除预判的笔画 + +为了确保在每个笔画的最后或者用户取消绘制后,预判触摸会被正确地撤销,我们布置了一个家庭作业。 + +在 CanvasView 顶部增加一个 UIImage 属性,用于保存真实的绘图 —— 不包含任何预判的触摸: + +```swift +private var drawingImage: UIImage? +``` + +接着,在 touchesMove(\_:withEvent:) 方法中找到这句: + +```swift +image?.drawInRect(bounds) +``` + +替换为: + +```swift +drawingImage?.drawInRect(bounds) +``` + +这里,我们将 drawingImage 绘制到了图形上下文,这样 image 就不会立即显示到 canvas 视图。这将导致上一次触摸事件绘制的预判被覆盖。 + +然后,在 touchesMoved(\_:withEvent:) 方法最后,找到这些语句: + +```swift +image = UIGraphicsGetImageFromCurrentImageContext() +UIGraphicsEndImageContext() +``` + +在上述语句之前加上: + +```swift +// 1 +drawingImage = UIGraphicsGetImageFromCurrentImageContext() +// 2 +if let predictedTouches = event?.predictedTouchesForTouch(touch) { + for touch in predictedTouches { + drawStroke(context, touch: touch) + } +} +``` + +上面的代码完成了以下工作: + +1. 将图形上下文保存到 drawingImage,其中包含了刚刚绘制的新的笔画,但不包含预判的笔画。 + +2. 就像我们在“合并的触摸”中所做的,我们读取预判触摸数组,然后对每个预判触摸进行笔画的绘制。 + +然后增加两个方法: + +```swift +override func touchesEnded(touches: Set, withEvent event: UIEvent?) { + image = drawingImage +} + +override func touchesCancelled(touches: Set?, withEvent event: UIEvent?) { + image = drawingImage +} +``` + +这两个方法在笔画结束时会被调用。当一个触摸结束或者取消,我们将 drawingImage 赋给 image 属性,这样就取消了所有绘制到 canvas 上的预判的触摸。 + +最后还有一件事情:你要在“摇晃” iPad 时清除 canvas 以及 drawingImage 属性。 + +在 CanvasView.swift 的 clearCanvas(animated:) 方法,找到位于动画闭包中的这一句: + +```swift +self.image = nil +``` + +在这一句之后加入: + +```swift +self.drawingImage = nil +``` + +就在同一个方法的稍后一点,找到这句: + +```swift +image = nil +``` + +同样在后面加上: + +```swift +drawingImage = nil +``` + +这样,你就清除了曾经画过的所有图形。 + +运行程序,胡乱画一些字和线条。你会注意到,在绘制时会将苹果预判的触摸也绘制出来,这样就大大降低了延迟感。你可能要仔细观察才能发现,因为这种现象并不明显。:] + +![](http://cdn3.raywenderlich.com/wp-content/uploads/2016/12/Final.png) + +> 注意:当你运行本教程最后的第二个代码示例时,你可以直观地看到预判触摸的效果。在代码中,你可以将预判触摸的纹理替换成蓝色。 + +苹果的预判触摸算法给了我们一个惊喜。由于这样的小玩意存在,在苹果平台上进行开发变成了一件让人心情愉快的事情。 + +##接下来做什么 + +恭喜你!你完成了一个简单的绘画类 App,它能让你用苹果铅笔进行涂鸦或者艺术创作。:] 你可以[下载完整的项目](http://cdn5.raywenderlich.com/wp-content/uploads/2016/12/Scribble-Final.zip)来预览最终效果。 + +你完成了一件来不起的创举,同时学到了如下知识: + +- 绘制自然的光滑曲线和形状 + +- 使用高度和方位角 + +- 实现画图和描影法作图 + +- 添加和使用纹理 + +- 添加橡皮擦功能 + +- 使用预判以及何时撤销它们 + +我也提供了另一个项目示例,它有几个按钮,用于打开/关闭“合并的触摸”和“预判的触摸”,以让你对二者有一个更直观的了解。 + +[苹果的 WWDC 的视频 on Touches](https://developer.apple.com/videos/play/wwdc2015-233/)上专门有一节关于在 60 Hz 的帧率下“合并的触摸”和“预判的触摸”的内容。你可以观看这个视频,视频中描述了如何改善延迟,这种方法在 iOS 8 上能提升 4 帧而 iOS 9.1 上能提升 1.5 帧。内容非常之详实! + +FlexMonkey(即 Simon Gladman) 用苹果铅笔制作了一些非常棒的东东,绝不仅限于用它来作画。你可以看一看他的博客,尤其是 [Pencil Synthesizer](http://flexmonkey.blogspot.com.au/2015/11/pencilsynth-apple-pencil-controlled.html) 和 [FurrySketch](http://flexmonkey.blogspot.com.au/2015/11/furrysketch-hirsute-drawing-with-apple.html)。 + +我希望你能喜欢这篇苹果铅笔教程 —— 我乐于看到有越来越多的 App 集成这个超酷的设备。如果你有疑问或建议,请加入下面的讨论! + + + +