SpriteKit框架详细解析(五) —— 基于SpriteKit的游戏编程的三角函数(一)

版本记录

版本号 时间
V1.0 2017.10.19 星期五

前言

SpriteKit框架使用优化的动画系统,物理模拟和事件处理支持创建基于2D精灵的游戏。接下来这几篇我们就详细的解析一下这个框架。相关代码已经传至GitHub - 刀客传奇,感兴趣的可以阅读另外几篇文章。
1. SpriteKit框架详细解析(一) —— 基本概览(一)
2. SpriteKit框架详细解析(二) —— 一个简单的动画实例(一)
3. SpriteKit框架详细解析(三) —— 创建一个简单的2D游戏(一)
4. SpriteKit框架详细解析(四) —— 创建一个简单的2D游戏(二)

开始

首先看一下写作环境

Swift 4, iOS 11, Xcode 9

一个常见的误解是游戏程序员需要了解很多关于数学的知识。 虽然计算距离和角度确实需要数学,但在理解了一些基本概念后,实际上很容易。

在本教程中,您将了解一些重要的三角函数以及如何在游戏中使用它们。 然后,您将通过使用SpriteKit框架开发一个简单的太空射击iOS游戏来应用这些理论。

如果您之前从未使用过SpriteKit或计划为游戏使用不同的框架,请不要担心 - 本教程中涵盖的数学适用于您可能选择使用的任何游戏引擎。

注意:您将在本教程中构建的游戏使用加速度计,因此您需要一个iOS设备和一个开发者帐户。

三角函数(或简称为trig)仅仅意味着用三角形计算(这就是三角函数的由来)。

你可能没有意识到这一点,但游戏中充满了三角形。 例如,假设您有一个太空飞船游戏,并且您想要计算这两艘船之间的距离:

你有每艘船的X和Y位置,但你怎么能找到对角白线的长度?

那么,你可以简单地在每个船的中心点之间画一条线,形成一个这样的三角形:

请注意,此三角形的一个角具有90度的角度。 这被称为直角三角形,本教程中将要处理的三角形类型。

任何时候你可以将游戏中的某些东西表达为具有90度直角的三角形 - 例如图片中两个精灵之间的空间关系 - 你可以使用三角函数对它们进行计算。

例如,在这个太空飞船游戏中,您可能希望:

  • 让一艘船朝另一艘船的方向射击激光
  • 让一艘船开始向另一艘船的方向移动以追逐
  • 如果敌舰距离过近,则播放警告音效

所有这一切,以及更多,你可以用三角函数来完成!


Your Arsenal of Functions

首先,让我们看一下理论。 别担心,我会保持简短,这样你就可以尽快进行有趣的编码。

这些是构成直角三角形的部分:

在上图中,对角线侧称为hypotenuse。 它总是出现在直角形上,是三边中最长的一个。

当从三角形的左下角看时,剩下的两个边被称为相邻边adjacent和相反边opposite

如果从右上角看三角形,则相邻和相对的两侧会切换位置:

Alpha(α)beta(β)是另外两个角度的名称。 您可以将这些角度称为任何您想要的角度(只要它听起来像希腊语!),但通常alpha是感兴趣角的角度,β是对角的角度。 换句话说,您可以相对于alpha标记相对和相邻的边。

很酷的是,如果你只知道两个元素(边和非直角的组合),三角函数允许你使用三角函数正弦,余弦和正切找出所有剩余的边和角度。 例如,如果你知道其中一个边的任何角度和长度,那么你可以很容易地得出其他边和角的长度和角度:

你可以看到正弦,余弦和正切函数(通常缩写为sin,cos和tan)只是比率。 同样,如果你知道其中一个边的alpha和长度,那么sin,cos和tan是将两边和角度联系在一起的比率。

把sin,cos和tan函数想象成“黑盒子” - 你插入数字并取回结果。 它们是标准库函数,几乎可用于包括Swift在内的所有编程语言。

注意:三角函数的行为可以通过将圆形投影到直线上来解释,但是您不需要知道如何导出这些函数以便使用它们。 如果你很好奇,有很多网站和视频可以解释细节;查看Math is Fun网站的一个例子。

1. Know Angle and Length, Need Sides

我们来看一个例子吧。 假设船之间的α角是45度,斜边是10点长。

然后,您可以将这些值插入公式:

sin(45) = opposite / 10

要为斜边解决这个问题,你只需简单地改变这个公式:

opposite = sin(45) * 10

45度的正弦值为0.707(四舍五入到小数点后三位),填充在公式中的结果为:

opposite = 0.707 * 10 = 7.07

2. Know 2 Sides, Need Angle

当你已经知道一个角度时,上面的公式很有用,但情况并非总是如此 - 有时你知道两边的长度并且正在寻找它们之间的角度。 要导出角度,可以使用反三角函数,也称为弧函数:

angle = arcsin(opposite/hypotenuse)
angle = arccos(adjacent/hypotenuse)
angle = arctan(opposite/adjacent)

如果sin(a)= b,那么arcsin(b)= a也是如此。 在这些反向三角函数中,您可能会在实践中使用arc tangent (arctan),因为它可以帮助您找到斜边。 有时这些函数被写成sin-1cos-1tan-1,所以不要让它混淆你。

3. Know 2 Sides, Need Remaining Side

有时你可能知道两个边长,你需要知道第三边长。

这就是几何学的Pythagorean Theorem可以实现的地方:

a2 + b2 = c2

或者,就三角形方面而言:

opposite2 + adjacent2 = hypotenuse2

如果你知道任何两个边,计算第三个只是填写公式并取平方根。 这是游戏中非常常见的事情,您将在本教程中多次执行此操作。

4. Have Angle, Need Other Angle

最后,考虑角度。 如果你知道三角形中的一个非直角,那么找出另一个角是非常简单的事情。 在三角形中,三个角度的总和始终为180度。 因为这是一个直角三角形,它有一个90度角。

alpha + beta + 90 = 180

或者干脆:

alpha + beta = 90

剩下的两个角度必须加起来90度。 所以如果你知道alpha,你可以计算beta,反之亦然。

这些都是你需要知道的公式! 在实践中使用哪一个取决于您已经拥有的部分。 通常你要么有角度,要么至少有一个边长,或者你没有角度,但你有两个边长。

理论已经足够了吧,让我们把这些东西付诸实践。


Begin the Trigonometry! - 开始三角函数

看一下开始starter项目是一个SpriteKit项目。 在iOS设备上构建并运行它。 你会看到有一艘宇宙飞船可以用加速度计和屏幕中心的大炮一起移动。 两个精灵都有一个完整的生命条。

此刻,宇宙飞船在移动时不会旋转。 看到宇宙飞船在移动时所朝向的位置而不是始终指向上方将是有帮助的。 要旋转宇宙飞船,您需要知道旋转它的角度。 但你不知道那是什么;你有速度矢量。 那你如何从矢量中获得一个角度呢?

考虑一下你所知道的。 玩家具有X方向速度长度和Y方向速度长度:

如果你重新排列这些,你可以看到它们形成一个三角形:

在这里你知道相邻的(playerVelocity.dx)和相反的(playerVelocity.dy)边长。

所以基本上,你知道一个直角三角形的两边,你想找到一个角度(Know 2 Sides, Need Angle的情况),所以你需要使用反向三角函数之一:arcsin,arccos或arctan。

您知道的两侧是您需要的角度的相对侧和相邻侧。因此,您将需要使用arctan函数来查找船的旋转角度。请记住,如下所示:

angle = arctan(opposite / adjacent)

Swift标准库包含一个计算反正切的atan()函数,但它有一些限制。首先,x / y产生与-x / -y完全相同的值,这意味着您将获得相反速度的相同角度输出。其次,三角形内部的角度并不完全是您想要的角度 - 您想要相对于一个特定轴的角度,该角度可以是与atan()返回的角度偏移90度,180度或270度。

您可以编写一个四向if语句,通过考虑速度符号来确定角度所在的象限,然后应用正确的偏移量来计算出正确的角度。但是,有一个更简单的方法:

对于这个特定问题,不使用atan(),使用函数atan2(_:_ :)更简单,它将x和y分量作为单独的参数,并正确地确定整体旋转角度。

angle = atan2(opposite, adjacent)

将以下代码添加到GameScene.swiftupdatePlayer(_ :)的末尾:

let angle = atan2(playerVelocity.dy, playerVelocity.dx)
playerSprite.zRotation = angle

请注意,Y坐标先行。 记住第一个参数是opposite边。 在这种情况下,Y坐标与您尝试测量的角度相反。

构建并运行应用程序以进行尝试:

嗯,这似乎没有正常工作。 宇宙飞船肯定会旋转,但它指向的方向与它前进的方向不同!

这里是原因:宇宙飞船精灵图像直指向上,这对应于0度的默认旋转值。 但是按照数学惯例,0度的角度不会指向上方,而是指向右侧,沿X轴:

要解决此问题,请从旋转角度减去90度:

playerSprite.zRotation = angle - 90

Radians, Degrees and Points of Reference - 弧度,度数和参考点

一般人倾向于将角度视为0到360(度)之间的值。 数学家通常用弧度来测量角度,用π表示(希腊字母Pi,听起来像“馅饼”但味道不好)。

一个弧度是沿着圆弧行进半径长度时获得的角度。 你可以做到2π次(大约6.28次),然后再次回到圆圈的开头。

注意半径(直黄线)与弧(红色曲线)的长度相同。 两个长度相等的角是一个弧度!

因此,虽然您可以看到从0到360的角度值,但您也可以从0到2π看到它们。 大多数计算机数学函数都以弧度为单位。 SpriteKit也使用弧度进行所有角度测量。 atan2(_:_ :)函数返回弧度值,但您已尝试将该角度偏移90度。

由于您将使用弧度和度数,因此有一种方法可以轻松地在两者之间进行转换。 转换非常简单。 由于圆中有2π弧度或360度,因此π等于180度。 要将弧度转换为度数,可以除以π并乘以180;要将度数转换为弧度,可以除以180并乘以π。

GameScene上面添加以下两个常量:

let degreesToRadians = CGFloat.pi / 180
let radiansToDegrees = 180 / CGFloat.pi

最后,编辑updatePlayer(_ :)中的旋转代码以使用degreesToRadians相乘:

playerSprite.zRotation = angle - 90 * degreesToRadians

Build并再次运行。 你会看到宇宙飞船终于旋转并面向它前进的方向。


Bouncing Off the Walls

你有一个可以使用加速度计移动的宇宙飞船。 你正在使三角函数使它指向它前进的方向。

让太空飞船卡在屏幕的边缘并不是很令人满意,而你要通过让它从屏幕边框反弹来解决这个问题!

首先,从updatePlayer(_ :)删除这些行:

newX = min(size.width, max(0, newX))
newY = min(size.height, max(0, newY))

替换成下面

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
}

如果记录了碰撞,则反转加速度和速度值,导致船再次反弹。

构建并运行以试用它。

弹跳起作用,但似乎有点精力充沛。 问题在于你不会期望太空船会像橡皮球那样弹跳 - 它会在碰撞时失去大部分能量,并以比预先更低的速度反弹。

let maxPlayerSpeed: CGFloat = 200下面添加另一个常量:

let bordercollisionDamping: CGFloat = 0.4

现在,使用以下内容替换刚添加到updatePlayer(_ :)的代码:

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
}

您现在通过阻尼值bordercollisionDamping来加速。这允许您控制碰撞中损失的能量。在这种情况下,您可以在撞到屏幕边缘后使宇宙飞船仅保留其速度的40%。

为了好玩,使用bordercollisionDamping的值来查看此常量的不同值的效果。如果你使它大于1.0,宇宙飞船实际上从碰撞中获得能量!

您可能已经注意到一个小问题:将太空飞船瞄准屏幕底部,使其继续一遍又一遍地撞入边框,您会看到它在向上和向下之间开始断断续续。

使用反正切来找到一对X和Y分量之间的角度只有在X和Y值相当大的情况下才能正常工作。在这种情况下,阻尼系数将速度降低到几乎为零。当您将atan2(_:_ :)应用于非常小的值时,即使这些值的微小变化也会导致最终角度发生很大变化。

解决此问题的一种方法是在速度非常慢时不改变角度。这听起来是打电话给你的老朋友毕达哥拉斯的一个很好的理由。

现在你实际上并没有存储船的speed。 相反,您存储velocity,它是矢量等效物(参见here有关speedvelocity之间差异的解释),其中一个分量在X方向,一个分量在Y方向。 但是为了得出关于船的speed的任何结论(例如它是否太慢而不值得旋转船),你需要将这些X和Y速度分量组合成一个标量值。

在这里,你在前面讨论过的Know 2 Sides,Need Remaining Side case中。

正如您所看到的,宇宙飞船的真实速度 - 它每秒在屏幕上移动的点数 - 是由X方向的速度和Y方向的速度形成的三角形的斜边。

根据毕达哥拉斯公式:

true speed = √(playerVelocity.dx2 + playerVelocity.dy2)

updatePlayer(_ :)中删除此代码块:

let angle = atan2(playerVelocity.dy, playerVelocity.dx)
playerSprite.zRotation = angle - 90 * degreesToRadians

替换成下面

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
}

建立并运行。 您会看到飞船旋转在屏幕边缘看起来更加稳定。 如果你想知道40的价值来自哪里,答案是:实验。 将print()语句放入代码中以查看飞船通常会到达边界的速度有助于调整此值,直到感觉正确为止。


Blending Angles for Smooth Rotation - 混合角度以实现平滑旋转

当然,修复一件东西会破坏别的东西。 尝试减慢飞船直到它停止,然后翻转设备,以便飞船必须转身并以另一种方式飞行。

以前,这会发生一个很好的动画,你实际看到船转向。 但是因为你刚刚添加了一些防止船只在低速时改变角度的代码,现在转弯非常突然。 这是一个小细节,但正是这些细节才能制作出色的应用和游戏。

修复方法是不立即切换到新角度,而是在一系列连续帧中逐渐将其与先前角度混合。 这会重新引入转动动画,同时防止船舶在没有足够快速移动时旋转。

这种“混合”听起来很奇特,但实际上很容易实现。 它需要您跟踪太空船在更新之间的角度。 在GameScene类中添加以下属性:

var playerAngle: CGFloat = 0

将从rotationThreshold声明开始的代码行替换为updatePlayer(_ :)中最后一个if语句的结尾,代码如下:

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
}

playerAngle通过将它们与混合因子相乘来组合新角度和之前的角度。 换句话说,新角度仅对您在太空船上设置的实际旋转贡献20%。 随着时间的推移,更多的新角度被添加,宇宙飞船最终指向它前进的方向。

构建并运行以验证从一个旋转角度到另一个旋转角度不再发生突然变化。

现在尝试顺时针和逆时针旋转一圈。 你会注意到,在转弯的某个时刻,宇宙飞船突然向相反方向旋转了360度。 它总是发生在圆圈的同一点。 这是怎么回事?

atan2(_:_ :)返回并且+π和-π之间的角度(在+180和-180度之间)。 这意味着如果当前角度非常接近+π,然后再转动一点,它将绕回到-π(反之亦然)。

这实际上相当于圆圈上的相同位置(就像-180和+180度是相同的点),但你的混合算法不够聪明,没有考虑和意识到这一点 - 它认为角度已经跳了整整360度(又名 2π弧度),再近一步,它需要在相反的方向旋转360度以回升。

要修复它,您需要识别角度何时超过该阈值,并相应地调整playerAngle。 向GameScene类添加一个新属性:

var previousAngle: CGFloat = 0

再次,将从rotationThreshold声明开始的代码行替换为update Player(_ :)中最后一个if语句的末尾,替换为:

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)
  
  // did angle flip from +π to -π, or -π to +π?
  if angle - previousAngle > CGFloat.pi {
    playerAngle += 2 * CGFloat.pi
  } else if previousAngle - angle > CGFloat.pi {
    playerAngle -= 2 * CGFloat.pi
  }
  
  previousAngle = angle
  playerAngle = angle * rotationBlendFactor + playerAngle * (1 - rotationBlendFactor)
  playerSprite.zRotation = playerAngle - 90 * degreesToRadians
}

现在,您要检查当前角度和前一个角度之间的差异,以观察阈值0和π(180度)之间的变化。 这应该可以解决问题。

构建并运行。 你应该没有更多的问题来转动你的宇宙飞船!


Using Trig to Find Your Target - 使用三角函数查找目标

这是一个很好的开始 - 你有一艘飞船很顺利地移动! 但到目前为止,小宇宙飞船还是无忧无虑,因为大炮没有做任何事情。 让我们改变这一点。

大炮由两个精灵组成:固定底座和可以旋转以瞄准玩家的炮塔。 你希望大炮的炮塔始终指向玩家。 为了实现这一点,你需要弄清楚炮塔和玩家之间的角度。

为了弄清楚这一点,它将与飞船旋转计算非常相似,以面向其前进方向。 这一次,三角形来自两个精灵的中心:

同样,您可以使用atan2(_:_ :)来计算此角度。 在GameScene中添加以下方法:

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
}

deltaXdeltaY有助于测量玩家精灵和炮塔精灵之间的距离。 您将这些值插入atan2(_:_ :)以获取它们之间的相对角度。

和以前一样,您需要转换此角度以包括与X轴(90度)的偏移,以便精灵正确定向。 请记住atan2(_:_ :)总是给你斜边和0度线之间的角度;它不是三角形内的角度。

将以下代码添加到update(_:)结尾:

updateTurret(deltaTime)

Build并运行。 炮塔现在总是指向宇宙飞船。

挑战:真正的大炮不太可能瞬间移动。 相反,它总是在追赶,略微落后于船的位置。

您可以通过将旧角度与新角度“blending”来完成此操作,就像您使用太空船的旋转角度一样。 混合因子越小,炮塔需要越多时间赶上宇宙飞船。 看看你是否可以自己实现这个。


Using Trig for Collision Detection - 使用三角函数进行碰撞检测

宇宙飞船可以直接飞过大炮而不会产生任何影响。 如果它在与大炮相撞时失去生命,那将更具挑战性(也更现实)。 这是你碰撞检测领域要做的地方。

你可以使用SpriteKit的物理引擎,但自己进行碰撞检测并不困难,特别是如果你用简单的圆圈建模精灵。 检测两个圆是否相交就需要多思考了。 你所要做的就是计算它们之间的距离(毕达哥拉斯),看看它是否小于两个圆的半径之和。

GameScene上方添加两个新常量:

let cannonCollisionRadius: CGFloat = 20
let playerCollisionRadius: CGFloat = 10

这些是大炮和玩家周围的碰撞圈的大小。 看一下精灵,你会看到加农炮图像的实际半径(以像素为单位)略大于你指定的常数(大约25点),但是有一点摆动空间是很好的。 你不希望你的游戏过于无情,或者玩家可能觉得它不是很有趣。

宇宙飞船不是圆形的事实不应该阻止你的实现。 对于任意形状的精灵,圆圈通常是足够近似的。 由于其形状,它具有更简单的三角计算的巨大优势。 在这种情况下,船体的直径大约为20个点(请记住,直径是半径的两倍)。

首先,将此属性添加到GameScene以获取碰撞声效果:

let collisionSound = SKAction.playSoundFileNamed("Collision.wav", waitForCompletion: false)

将以下方法添加到GameScene以检测碰撞:

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)
  guard distance <= cannonCollisionRadius + playerCollisionRadius  else { return }
  run(collisionSound)
}

你已经看过它以前是如何实现的。 首先,计算两个精灵的X位置之间的距离。 其次,计算两个精灵的Y位置之间的距离。 将这两个值视为直角三角形的边,然后可以计算斜边。 斜边是两个精灵之间的距离。 如果该距离小于碰撞半径的总和,则播放声音效果。

update(_:)结束时添加对此新方法的调用:

checkShipCannonCollision()

是时候构建并再次运行。 通过将宇宙飞船飞入大炮,使碰撞逻辑成为一种旋转。

请注意,一旦碰撞开始,声音效果就会无休止地发挥作用。 那是因为,当宇宙飞船飞过大炮时,游戏会一个接一个地记录重复的碰撞。 不只有一次碰撞,每秒有60次,它会为每一次发出声音效果!

碰撞检测只是问题的前半部分。 下半部分是碰撞反应response。 您不仅需要来自碰撞的音频反馈,而且还需要物理响应(physical response) - 宇宙飞船应该从大炮反弹。

将此常量添加到GameScene.swift的顶部:

let collisionDamping: CGFloat = 0.8

然后在checkShipCannonCollision()中的guard语句正下方添加这些代码行:

playerAcceleration.dx = -playerAcceleration.dx * collisionDamping
playerAcceleration.dy = -playerAcceleration.dy * collisionDamping
playerVelocity.dx = -playerVelocity.dx * collisionDamping
playerVelocity.dy = -playerVelocity.dy * collisionDamping

这与您使太空飞船从屏幕边框反弹所做的非常相似。构建并运行以查看其工作原理。

如果太空船在击中大炮时速度很快,那看起来相当不错。但是如果它移动得太慢,那么即使在反转速度之后,船也有时会停留在碰撞半径内并且永远不会离开它。显然,这种解决方案存在一些问题。

不是通过反转其速度将船从大炮上弹开,而是需要通过调整其位置以使半径不再重叠来物理地将船推离大炮。

要做到这一点,你需要计算大炮和宇宙飞船之间的向量。幸运的是,您之前已经计算过它来测量它们之间的距离。那你如何使用那个距离向量来移动船?

deltaXdeltaY形成的向量已经指向正确的方向,但它的长度是错误的。您需要的长度是船舶半径与其当前长度之间的差异。这样,当你将它添加到船的当前位置时,船将不再与大炮重叠。

向量的当前长度是distance,但您需要的长度是:

cannonCollisionRadius + playerCollisionRadius - distance

那么如何改变矢量的长度呢?

解决方案是使用称为normalization的技术。 您可以通过将X和Y分量除以当前标量长度(使用毕达哥拉斯计算)来标准化矢量。 得到的“标准化”向量的总长度为1。

然后,您只需将X和Y乘以所需的长度即可得到宇宙飞船的偏移量。 在您添加到checkShipCannonCollision()的前面代码行下面添加以下代码:

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并运行。 你会看到宇宙飞船现在可以从大炮上正常弹跳。

为了完善碰撞逻辑,你将从宇宙飞船和大炮中减去一些生命值。 然后,更新生命栏。 在run(collisionSound)之前添加以下代码:

playerHP = max(0, playerHP - 20)
cannonHP = max(0, cannonHP - 5)

updateHealthBar(playerHealthBar, withHealthPoints: playerHP)
updateHealthBar(cannonHealthBar, withHealthPoints: cannonHP)

Build并再次运行。 这艘船和大炮每次碰撞时都会失去一些生命值。


Adding Some Spin - 添加一些旋转

为了获得良好的效果,您可以在碰撞后为太空船添加一些旋转。 这种额外的旋转不会影响飞行方向;它只会使碰撞的效果更加深刻(飞行员更加头晕)。 在GameScene.swift的顶部添加一个新常量:

let playerCollisionSpin: CGFloat = 180

这将旋转量设置为每秒半圈,我认为看起来非常好。 现在向GameScene类添加一个新属性:

var playerSpin: CGFloat = 0

checkShipCannonCollision()中,在更新生命栏方法之前添加以下代码:

playerSpin = playerCollisionSpin

最后,在playerSprite.zRotation = playerAngle - 90 * degreesToRadians之前,将以下代码添加到updatePlayer(_ :)

if playerSpin > 0 {
  playerAngle += playerSpin * degreesToRadians
  previousAngle = playerAngle
  playerSpin -= playerCollisionSpin * CGFloat(dt)
  if playerSpin < 0 {
    playerSpin = 0
  }
}

playerSpin在不影响速度的情况下有效地覆盖了旋转持续时间的船舶显示角度。 旋转量随着时间的推移迅速减少,因此船在一秒钟之后就会旋出。 旋转时,更新previousAngle以匹配旋转角度,以便在旋出后船不会突然捕捉到新的角度。

Build并运行并设置该船旋转!

后记

本篇主要讲述了基于SpriteKit的游戏编程的三角函数,感兴趣的给个赞或者关注~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容