开发者必读:游戏中的三角学(下)

2015年10月16日 13:15 0 点赞 0 评论 更新于 2017-05-01 18:51

原文: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
)

playerHealthBarcannonHealthBar 均为 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

上述代码与之前让飞船在屏幕边缘反弹的代码类似。但运行程序会发现,当飞船以较慢速度撞击大炮时,即使将速度和加速度取反,飞船有时仍会停留在碰撞区域,说明该解决方案存在一定缺陷。

为了解决这个问题,我们需要为飞船施加一个物理力,使其远离大炮。具体做法是计算大炮和飞船之间的矢量,之前在计算两者距离时已经使用了 deltaXdeltaY 这两个矢量。我们需要将该矢量的长度调整为半径之和减去两者的实际距离,以确保飞船不会与大炮重叠:

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
}
}

上述旋转特效会覆盖飞机原有的旋转方向特效,但不影响其速度。旋转效果将在一秒后消失,在旋转过程中,将飞机当前角度赋值给旋转特效角度,以避免旋转结束时飞机角度突变。

运行程序,你将看到碰撞后带有旋转效果的飞船。

作者信息

洞悉

洞悉

共发布了 515 篇文章