原文:Trigonometry for Games – Sprite Kit and Swift Tutorial: Part 1/2,译者:Dev端

  添加血条

  接第一部分,在本教程中,你会学到如何让炮台向飞船发射导弹,炮台会给飞船带来伤害。为了显示飞船的健康程度,我们给它添加一个血条的sprite。
  在GameScene.swift的最上面申明下面几个新的常量:

let MaxHealth = 100    
let HealthBarWidth: CGFloat = 40   
let HealthBarHeight: CGFloat = 4

  再给GameScene添加下面的属性

let playerHealthBar = SKSpriteNode()    
let cannonHealthBar = SKSpriteNode()
var playerHP = MaxHealth   
var cannonHP = MaxHealth

  现在,把下面的代码添加进didMoveToView(),位置是在startMonitoringAcceleration()之前:

addChild(playerHealthBar)  
addChild(cannonHealthBar) 
cannonHealthBar.position = CGPoint(   
  x: cannonSprite.position.x, 
  y: cannonSprite.position.y - cannonSprite.size.height/2 - 10 
)

  playerHealthBar和cannonHealthBar都是SKSpriteNode,但是你没有给他们任何的图片。你要做的是用CoreGraphics给它们画上血条。
  注意,你在大炮的下面一点放置了血条,但是没有playerHealthBar任何位置信息更新。这是因为大炮并不移动,所以只要给它一次位置的定义,接着就可以忘掉它了。但是飞船不一样,飞船要一直移动,所以血条也会一直跟着移动。这要在updatePlayer()方法里实现:

playerHealthBar.position = CGPoint(    
  x: playerSprite.position.x,
  y: playerSprite.position.y - playerSprite.size.height/2 - 15 
)

  现在,剩下的工作就只有绘制血条了,在你的类型里添加下面的方法:

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 
}

  这些行代码画了一个单独的血条出来。首先设置它的填充颜色和边缘颜色,然后创建绘制环境,并且画两个矩形出来:其一是背景矩形,保持和整体一样的大小;其二是血条本身,会根据健康程度来改变。这个方法之后从绘制环境中提供出来一个UIImage,然后作为纹理赋值给sprite。
  你需要引用这个方法两次,一次是给飞船,一次是给大炮。因为绘制血条会消耗很多系统资源,你不能每一次刷新都重新绘制。所以我们用的办法是,仅仅当血条的血量发生变化的时候才启用重新绘制的方法。暂时的,我们只需要调用它一次来设置初始的血条。添加下面的代码到didMoveToView()
startMonitoringAcceleration()  跑一跑程序,你会发现现在两者下面都有血条了。

  用三角学来监测碰撞

  目前为止,飞船可以飞过大炮,就好像完全没有任何障碍一样。最好我们可以在两者碰触的时候可以知道他们碰了,然后给予飞船一定量的伤害。
  在这个时候,很多游戏开发者会想到“我需要一个物理引擎”,确实SpriteKit的物理引擎可以很轻松地帮你做这件事,但是,你自己做碰撞检测也是很容易的,尤其是当你的sprite是简单的圆的时候。
  (检测两个圆的碰撞是小菜一碟的事:你要做的仅仅是计算两者中心之间的距离和两者半径之和的关系)

  给你的Scene添加下面两个常量
let CannonCollisionRadius: CGFloat = 20    
let PlayerCollisionRadius: CGFloat = 10
  这两者代表围绕着两个sprite的圆的碰撞半径。仔细看你的sprite,你会发现飞船的实际半径要比你定义的半径大一些,这样的做法给了玩家更大的宽容度(他们不会老是碰到障碍物)。你老让他们失败,他们的乐趣就没了。
  事实上,你的飞船形状并不是一个圆。但是一个圆通常可以模拟一个sprite的形状来得到它的碰撞体积。这样的话,可以更简便地计算碰撞。 添加下面的方法来做碰撞检测:
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  
    runAction(collisionSound) 
  } 
}
  你之前已经见过三角学怎么用了。通过勾股定理计算出两者的距离,然后再和他们的半径合做比较。
  在update()方法的末尾加上下面的代码:
checkShipCannonCollision()
  然后再GameScene的上面添加上下面的属性
let collisionSound = SKAction.playSoundFileNamed("Collision.wav", waitForCompletion: false)
  是时候跑一下程序看看了,试试让飞机再靠近大炮吧。

  注意到,当两者发生碰撞的时候,音效就一直响个不停。这是因为飞船飞过大炮的时候,游戏一秒60次地被调用播放音效。
  碰撞检测仅仅是这个问题的前半部分,后半部分是碰撞检测之后的反馈。你不仅想得到音效的反馈,你还想能够得到物理的反馈-飞船应该从大炮身边弹开。在GameScene.swift文件里添加上下面的常量。
let CollisionDamping: CGFloat = 0.8
  在checkShipCannonCollision()里添加这么几行
playerAcceleration.dx = -playerAcceleration.dx * CollisionDamping    
playerAcceleration.dy = -playerAcceleration.dy * CollisionDamping  
playerVelocity.dx = -playerVelocity.dx * CollisionDamping   
playerVelocity.dy = -playerVelocity.dy * CollisionDamping
  这和之前你让飞船在屏幕边上反弹的代码几乎一样。跑一下程序你会发现,飞船如果是很快速度撞击大炮的话,效果不错,但是,如果飞船是很慢的速度撞上去,即便我们让速度和加速度变成相反值,飞船有时候还是会停留在碰撞区域。显然,这样的解决办法存在缺陷。
  与其为飞船设置反弹,你需要给飞船一个物理力来让它远离大炮。
  为了实现之,你需要计算大炮和飞船之间的矢量,之前你计算过两者之间的距离的时候就用到了deltaX和deltaY两个矢量。你要怎么通过这个合起来的矢量来给飞船力呢?那个由deltaX和deltaY组合起来的矢量已经指向了正确的防线,但是长度不对。你需要的长度是半径之和减去两者距离,这样飞船就不会和大炮重叠了。
(CannonCollisionRadius + PlayerCollisionRadius – distance)
  你如何来改变这个距离呢?解决办法是用一个技术叫做规范化(normalization)。通过标准矢量作为中介,执行下面的代码:
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 
)
  跑一下程序,你会发现现在飞船反弹地很正常了。
  这时,你需为飞船扣除一些血量:
playerHP = max(0, playerHP - 20)    
cannonHP = max(0, cannonHP - 5) 
updateHealthBar(playerHealthBar, withHealthPoints: playerHP)   
updateHealthBar(cannonHealthBar, withHealthPoints: cannonHP)

  添加旋转

  为了一个漂亮你效果,你需要在碰撞之后给飞船添加一些旋转。这里的旋转不会影响之前我们设置的飞船的旋转方向,添加下面的常量:
let PlayerCollisionSpin: CGFloat = 180
  这设置了旋转的角度,然后添加另外一个属性:
var playerSpin: CGFloat = 0
  在checkShipCannonCollision()里,添加下面的申明:
playerSpin = PlayerCollisionSpin
  最后,在updatePlayer()添加下面的代码(在playerSprite.zRotation = playerAngle - 90 * DegreesToRadians之前):
if playerSpin > 0 {  
  playerAngle += playerSpin * DegreesToRadians 
  previousAngle = playerAngle 
  playerSpin -= PlayerCollisionSpin * CGFloat(dt) 
  if playerSpin   
  playerSpin = 0 
  }  
}

  旋转特效重写了飞机方向的特效,并没有影响速度。这样的旋转在一秒之后就会消失。在旋转的时候,你把飞机当前的角度赋值为旋转特效的角度,以防止旋转结束的时候,飞机突然变化角度。
  跑一下程序,你就能看到旋转的飞船了!