本文共 5570 字,大约阅读时间需要 18 分钟。
本节书摘来自异步社区《iOS和tvOS 2D游戏开发教程》一书中的第2章,第2.4节挑战,作者 【美】raywenderlich.com教程开发组,更多章节内容可以访问云栖社区“异步社区”公众号查看
2.4 挑战
本章有3个挑战,它们都很重要。完成这些挑战,能够让你练习使用向量,并且会引入新的数学工具,而在本书的剩下内容中,你将会用到这些工具。同样,如果遇到困难,可以从本章的资源文件中找到解决方案,但是你最好是自己能够解决它。
挑战1:数学工具
你肯定已经注意到了,在开发这款游戏的时候,经常要进行点和向量的计算,例如,把点相加和相减,求取长度值等等。我们还需要在CGFloat和Double之间做很多强制转型。在本章中,到目前为止,我们都是以内嵌的方式自行完成这些计算的。这是做事情的一种很好的方式,但是,在实际工作中,这可能变得很繁琐而且具有重复性;还容易出错。
使用iOSSourceSwift File模板创建一个新的文件,将其命名为MyUtils。然后,使用如下的代码替换MyUtils的内容:
import Foundationimport CoreGraphicsfunc + (left: CGPoint, right: CGPoint) -> CGPoint { return CGPoint(x: left.x + right.x, y: left.y + right.y)}func += (inout left: CGPoint, right: CGPoint) { left = left + right}
在Swift中,可以让+、-、*和/这样的运算符作用于任何想要的类型之上。这里,我们让它们作用于CGPoint之上。
现在,可以像下面这样来把点相加了,但是,不要在任何地方添加这些代码;这里只是给出一个示例:
let testPoint1 = CGPoint(x: 100, y: 100)let testPoint2 = CGPoint(x: 50, y: 50)let testPoint3 = testPoint1 + testPoint2
让我们也覆盖CGPoints上的减法、乘法和除法运算符。在MyUtils.swift的末尾,添加如下的代码:
func - (left: CGPoint, right: CGPoint) -> CGPoint { return CGPoint(x: left.x - right.x, y: left.y - right.y)}func -= (inout left: CGPoint, right: CGPoint) { left = left - right}func * (left: CGPoint, right: CGPoint) -> CGPoint { return CGPoint(x: left.x * right.x, y: left.y * right.y)}func *= (inout left: CGPoint, right: CGPoint) { left = left * right}func * (point: CGPoint, scalar: CGFloat) -> CGPoint { return CGPoint(x: point.x * scalar, y: point.y * scalar)}func *= (inout point: CGPoint, scalar: CGFloat) { point = point * scalar}func / (left: CGPoint, right: CGPoint) -> CGPoint { return CGPoint(x: left.x / right.x, y: left.y / right.y)}func /= (inout left: CGPoint, right: CGPoint) { left = left / right}func / (point: CGPoint, scalar: CGFloat) -> CGPoint { return CGPoint(x: point.x / scalar, y: point.y / scalar)}func /= (inout point: CGPoint, scalar: CGFloat) { point = point / scalar}
现在,可以把一个CGPoint和另一个CGPoint相减、相乘和相除了。还可以将点和标量的CGFloat值相乘和相除,如下所示。同样的,不要在任何地方添加这些代码,这里只是给出一个示例。
let testPoint5 = testPoint1 * 2let testPoint6 = testPoint1 / 10
最后,添加扩展了CGPoint的类,它带有一些辅助方法:
#if !(arch(x86_64) || arch(arm64))func atan2(y: CGFloat, x: CGFloat) -> CGFloat { return CGFloat(atan2f(Float(y), Float(x)))}func sqrt(a: CGFloat) -> CGFloat { return CGFloat(sqrtf(Float(a)))}#endifextension CGPoint { func length() -> CGFloat { return sqrt(x*x + y*y) } func normalized() -> CGPoint { return self / length() } var angle: CGFloat { return atan2(y, x) }}
当这个App在32位架构的机器上运行的时候,#if/#endif语句块为true。在这种情况下,CGFloat和Float具有相同的大小,因此,这段代码编写了接受CGFloat/Float值(而不是默认的Double)的atan2和sqrt版本;这就允许你对CGFloat/Float使用atan2和sqrt,而不会受到设备架构的限制。
接下来,这个类扩展添加了一些方便的方法来获取点的长度,返回该点的一个正规化的版本(即长度为1),并且得到该点的一个角度。
使用这些辅助函数,将会使得代码更加简洁和清晰。例如,来看看moveSprite(velocity:)方法:
func moveSprite(sprite: SKSpriteNode, velocity: CGPoint) { let amountToMove = CGPoint(x: velocity.x * CGFloat(dt), y: velocity.y * CGFloat(dt)) print("Amount to move: \(amountToMove)") sprite.position = CGPoint( x: sprite.position.x + amountToMove.x, y: sprite.position.y + amountToMove.y)}
使用*将velocity和dt相乘,避免了强制转型,简化了第1行代码。此外,使用+=运算符将精灵的位置和移动的量相加,简化了最后一行代码。
最终的结果应该如下所示:
func moveSprite(sprite: SKSpriteNode, velocity: CGPoint) { let amountToMove = velocity * CGFloat(dt) print("Amount to move: \(amountToMove)") sprite.position += amountToMove}
你的挑战是,修改剩下的Zombie Conga以使用新的辅助代码,并且验证游戏仍然能够像预期的那样工作。当你完成之后,应该进行如下的调用,这包括对前面已经提及的两个操作符的调用:
+=运算符:1次调用;
-运算符:1次调用;*运算符:2次调用;normalized:1次调用;angle:1次调用。你将会注意到,当完成了这些工作的时候,代码变得整洁了很多,而且更加易于理解了。在后续的几章中,你将要使用我们所编写的一个数学库,它和这里所创建的数学库非常相似。挑战2:让僵尸停下来
在Zombie Conga,当你点击屏幕的时候,僵尸会朝着点击的位置移动,但是随后,它会继续移动以超过该位置。这是我们想要在Zombie Conga中得到的效果,但是,在其他的游戏中,你可能想要让僵尸在点击的位置停下来。你的挑战是修改游戏以做到这一点。
如下是针对一种可能的实现的一些提示:
创建一个名为lastTouchLocation的可选的属性,并且当玩家触摸场景的时候,更新这个属性。
在update()中,检查最近一次触摸的位置和僵尸的位置之间的距离。如果这个距离小于或等于僵尸将要在当前帧中移动的距离(zombieMovePointsPerSec * dt),那么就把僵尸的位置设置为最近一次触摸的位置,并且将其速度设置为0。否则,正常地调用moveSprite(velocity:)和rotateSprite(direction:)。应该还要调用boundsCheckZombie()。为了实现这些,要用到挑战1中的辅助代码,使用一次-运算符并且调用一次length()。挑战3:平滑移动
目前,僵尸会立即旋转以面朝点击的位置。这可能有点突兀,如果僵尸随着时间的流逝逐渐平滑地旋转以面朝新的方向的话,看上去会好很多。为了做到这一点,需要一个新的辅助程序。将如下代码添加到MyUtils.swift(to typeπ, use Option-p)的末尾。
letπ = CGFloat(M_PI)func shortestAngleBetween(angle1: CGFloat, angle2: CGFloat) -> CGFloat { let twoπ = π * 2.0 var angle = (angle2 - angle1) % twoπ if (angle >= π) { angle = angle - twoπ } if (angle <= -π) { angle = angle + twoπ } return angle}extension CGFloat { func sign() -> CGFloat { return (self >= 0.0) ? 1.0 : -1.0 }}
如果CGFloat大于或等于0,sign()返回1,否则的话,它返回-1。
shortestAngleBetween()返回两个角之间的最短的角度。这并不是将两个角相减那么简单,理由有两个:
1.角度在超过360度(2 * M_PI)之后会“舍入”。换句话说,30度和390度表示相同的角度,如图2-24所示。
2.有时候,两个角之间旋转最短的方式是向左,而有时候又是向右。例如,如果从0度开始,想要转到270度,最短的方式是转-90度,而不是转270度,如图2-25所示。我们不想让僵尸转一大圈,虽然它是僵尸,但是它并不蠢笨。
图2-25
因此,这个程序求得两个角度之间的差,去掉任何比360度大的部分,然后确定是向右旋转还是向左旋转更快。
你的挑战是修改rotateSprite(direction:),以接受并使用一个新的参数,即僵尸每秒应该旋转的弧度数。
定义如下的常量:
let zombieRotateRadiansPerSec:CGFloat = 4.0 * π
并且将该方法的签名修改为如下所示:func rotateSprite(sprite: SKSpriteNode, direction: CGPoint, rotateRadiansPerSec: CGFloat) { // Your code here!}
这里针对这个方法的实现给出一些提示:
使用shortestAngleBetween()找出当前角和目标角之间的距离,称之为shortest。
根据rotateRadiansPerSec和dt计算出在这一帧中要旋转的量,称之为amtToRotate。如果shortest的绝对值小于amtToRotate,使用shortest来替代它。将amtToRotate加到精灵的zRotation中,但是先将其与sign()相乘,以便可以朝着正确的方向旋转。不要忘了在update()中更新对旋转精灵的调用,以便它可以使用新参数。如果你完成了所有这3个挑战,做的真是不错!你真的已经理解了如何使用“经典的”方法随着时间来更新值,从而移动和旋转精灵。然而,经典的方法只是为了便于理解,它总是会让步于现代的方法的。
在第3章中,我们将学习Sprite Kit如何通过神奇的动作,让一些常见的任务变得非常容易。
转载地址:http://burzx.baihongyu.com/