开发者必读:游戏中的三角学(下)
原文: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
)
绘制血条
在 GameScene
类中添加以下方法用于绘制血条:
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)
// 创建绘图上下文
UIGraphicsBeginImageContextWithOptions(barSize, false, 0)
let context = UIGraphicsGetCurrentContext()
// 绘制血条的轮廓
borderColor.setStroke()
let borderRect = CGRect(origin: CGPoint.zero, size: barSize)
CGContextStrokeRectWithWidth(context, borderRect, 1)
// 绘制带有颜色的血条矩形
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)
// 提取图像
let spriteImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// 设置精灵的纹理和大小
node.texture = SKTexture(image: spriteImage)
node.size = barSize
}
上述代码用于绘制单个血条。首先,设置血条的填充颜色和边框颜色;然后,创建绘图上下文并绘制两个矩形:一个是背景矩形,大小与血条整体一致;另一个是血条本身,其宽度会根据健康值动态变化。该方法会从绘图上下文中获取一个 UIImage
对象,并将其作为纹理赋值给精灵。
由于绘制血条会消耗较多系统资源,我们不能在每次刷新时都重新绘制。因此,仅当血条的血量发生变化时才调用重绘方法。目前,我们只需调用一次该方法来设置初始血条。在 didMoveToView()
方法中添加以下代码:
updateHealthBar(playerHealthBar, withHealthPoints: playerHP)
updateHealthBar(cannonHealthBar, withHealthPoints: cannonHP)
startMonitoringAcceleration()
运行程序后,你会发现飞船和大炮下方均显示出了血条。
用三角学来监测碰撞
目前,飞船可以直接飞过大炮,仿佛没有任何障碍。为了实现更真实的游戏效果,我们需要在飞船与大炮碰撞时进行检测,并对飞船造成一定伤害。
许多游戏开发者在处理碰撞检测时会考虑使用物理引擎,虽然 SpriteKit
的物理引擎可以轻松实现该功能,但手动进行碰撞检测也并不复杂,尤其是当精灵为简单圆形时。
定义碰撞半径
在 GameScene
类中添加以下两个常量:
let CannonCollisionRadius: CGFloat = 20
let PlayerCollisionRadius: CGFloat = 10
这两个常量分别代表围绕大炮和飞船精灵的圆形碰撞半径。观察精灵会发现,飞船的实际半径略大于定义的半径,这样做是为了给玩家更大的容错空间,避免频繁碰撞障碍物,从而提升游戏体验。
实现碰撞检测方法
添加以下方法用于检测飞船与大炮的碰撞:
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)
}
}
该方法运用了勾股定理计算飞船和大炮中心之间的距离,并将其与两者的半径之和进行比较。若距离小于半径之和,则判定发生碰撞,并播放碰撞音效。
在 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
这两个矢量。我们需要将该矢量的长度调整为半径之和减去两者的实际距离,以确保飞船不会与大炮重叠:
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 <= 0 {
playerSpin = 0
}
}
上述旋转特效会覆盖飞机原有的旋转方向特效,但不影响其速度。旋转效果将在一秒后消失,在旋转过程中,将飞机当前角度赋值给旋转特效角度,以避免旋转结束时飞机角度突变。
运行程序,你将看到碰撞后带有旋转效果的飞船。