本节书摘来自异步社区《iOS和tvOS 2D游戏开发教程》一书中的第2章,第2.4节挑战,作者 【美】raywenderlich.com教程开发组,更多章节内容可以访问云栖社区“异步社区”公众号查看
2.4 挑战
本章有3个挑战,它们都很重要。完成这些挑战,能够让你练习使用向量,并且会引入新的数学工具,而在本书的剩下内容中,你将会用到这些工具。
同样,如果遇到困难,可以从本章的资源文件中找到解决方案,但是你最好是自己能够解决它。
挑战1:数学工具
你肯定已经注意到了,在开发这款游戏的时候,经常要进行点和向量的计算,例如,把点相加和相减,求取长度值等等。我们还需要在CGFloat和Double之间做很多强制转型。
在本章中,到目前为止,我们都是以内嵌的方式自行完成这些计算的。这是做事情的一种很好的方式,但是,在实际工作中,这可能变得很繁琐而且具有重复性;还容易出错。
使用iOSSourceSwift File模板创建一个新的文件,将其命名为MyUtils。然后,使用如下的代码替换MyUtils的内容:
import Foundation
import CoreGraphics
func + (left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x + right.x, y: left.y + right.y)
}
func += (inout left: CGPoint, right: CGPoint) {
left = left + right
}
在Swift中,可以让+、-、*和/这样的运算符作用于任何想要的类型之上。这里,我们让它们作用于CGPoint之上。
现在,可以像下面这样来把点相加了,但是,不要在任何地方添加这些代码;这里只是给出一个示例:
let testPoint1 = CGPoint(x: 100, y: 100)
let testPoint2 = CGPoint(x: 50, y: 50)
let testPoint3 = testPoint1 + testPoint2
让我们也覆盖CGPoints上的减法、乘法和除法运算符。在MyUtils.swift的末尾,添加如下的代码:
func - (left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x - right.x, y: left.y - right.y)
}
func -= (inout left: CGPoint, right: CGPoint) {
left = left - right
}
func * (left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x * right.x, y: left.y * right.y)
}
func *= (inout left: CGPoint, right: CGPoint) {
left = left * right
}
func * (point: CGPoint, scalar: CGFloat) -> CGPoint {
return CGPoint(x: point.x * scalar, y: point.y * scalar)
}
func *= (inout point: CGPoint, scalar: CGFloat) {
point = point * scalar
}
func / (left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x / right.x, y: left.y / right.y)
}
func /= (inout left: CGPoint, right: CGPoint) {
left = left / right
}
func / (point: CGPoint, scalar: CGFloat) -> CGPoint {
return CGPoint(x: point.x / scalar, y: point.y / scalar)
}
func /= (inout point: CGPoint, scalar: CGFloat) {
point = point / scalar
}
现在,可以把一个CGPoint和另一个CGPoint相减、相乘和相除了。还可以将点和标量的CGFloat值相乘和相除,如下所示。同样的,不要在任何地方添加这些代码,这里只是给出一个示例。
let testPoint5 = testPoint1 * 2
let testPoint6 = testPoint1 / 10
最后,添加扩展了CGPoint的类,它带有一些辅助方法:
#if !(arch(x86_64) || arch(arm64))
func atan2(y: CGFloat, x: CGFloat) -> CGFloat {
return CGFloat(atan2f(Float(y), Float(x)))
}
func sqrt(a: CGFloat) -> CGFloat {
return CGFloat(sqrtf(Float(a)))
}
#endif
extension CGPoint {
func length() -> CGFloat {
return sqrt(x*x + y*y)
}
func normalized() -> CGPoint {
return self / length()
}
var angle: CGFloat {
return atan2(y, x)
}
}
当这个App在32位架构的机器上运行的时候,#if/#endif语句块为true。在这种情况下,CGFloat和Float具有相同的大小,因此,这段代码编写了接受CGFloat/Float值(而不是默认的Double)的atan2和sqrt版本;这就允许你对CGFloat/Float使用atan2和sqrt,而不会受到设备架构的限制。
接下来,这个类扩展添加了一些方便的方法来获取点的长度,返回该点的一个正规化的版本(即长度为1),并且得到该点的一个角度。
使用这些辅助函数,将会使得代码更加简洁和清晰。例如,来看看moveSprite(velocity:)方法:
func moveSprite(sprite: SKSpriteNode, velocity: CGPoint) {
let amountToMove = CGPoint(x: velocity.x * CGFloat(dt),
y: velocity.y * CGFloat(dt))
print("Amount to move: \(amountToMove)")
sprite.position = CGPoint(
x: sprite.position.x + amountToMove.x,
y: sprite.position.y + amountToMove.y)
}
使用*将velocity和dt相乘,避免了强制转型,简化了第1行代码。此外,使用+=运算符将精灵的位置和移动的量相加,简化了最后一行代码。
最终的结果应该如下所示:
func moveSprite(sprite: SKSpriteNode, velocity: CGPoint) {
let amountToMove = velocity * CGFloat(dt)
print("Amount to move: \(amountToMove)")
sprite.position += amountToMove
}
你的挑战是,修改剩下的Zombie Conga以使用新的辅助代码,并且验证游戏仍然能够像预期的那样工作。当你完成之后,应该进行如下的调用,这包括对前面已经提及的两个操作符的调用:
+=运算符: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如何通过神奇的动作,让一些常见的任务变得非常容易。