这是我们要构建的结果:
光线投射
我们使用的技术称为
光线投射 (Ray Casting),你可以在我已经写过的
类似答案中找到更多细节。
基本上,我们使用SpriteKit物理引擎来确定一个直线并获取该直线与物理体的第一个交点。
代码
当然,你需要:
- 在SpriteKit基础上创建一个空Xcode项目
- 添加一个圆形精灵(ball)
- 给球添加物理体并赋予一些速度
以下是你所需要的全部代码:
扩展功能
将这两个扩展添加到你的项目中:
extension CGVector {
init(angle: CGFloat) {
self.init(dx: cos(angle), dy: sin(angle))
}
func normalized() -> CGVector {
let len = length()
return len>0 ? self / len : CGVector.zero
}
func length() -> CGFloat {
return sqrt(dx*dx + dy*dy)
}
static func / (vector: CGVector, scalar: CGFloat) -> CGVector {
return CGVector(dx: vector.dx / scalar, dy: vector.dy / scalar)
}
func bounced(withNormal normal: CGVector) -> CGVector {
let dotProduct = self.normalized() * normal.normalized()
let dx = self.dx - 2 * (dotProduct) * normal.dx
let dy = self.dy - 2 * (dotProduct) * normal.dy
return CGVector(dx: dx, dy: dy)
}
init(from:CGPoint, to:CGPoint) {
self = CGVector(dx: to.x - from.x, dy: to.y - from.y)
}
static func * (left: CGVector, right: CGVector) -> CGFloat {
return (left.dx * right.dx) + (left.dy * right.dy)
}
}
extension CGPoint {
func length() -> CGFloat {
return sqrt(x*x + y*y)
}
func distanceTo(_ point: CGPoint) -> CGFloat {
return (self - point).length()
}
static func -(left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x - right.x, y: left.y - right.y)
}
}
场景
现在用以下代码替换您在Scene.swift
中的所有代码
class GameScene: SKScene {
lazy var ball: SKSpriteNode = {
return childNode(withName: "ball") as! SKSpriteNode
}()
override func didMove(to view: SKView) {
self.physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
}
override func update(_ currentTime: TimeInterval) {
self.enumerateChildNodes(withName: "line") { (node, _) in
node.removeFromParent()
}
guard let collision = rayCast(start: ball.position, direction: ball.physicsBody!.velocity.normalized()) else { return }
let line = CGVector(from: ball.position, to: collision.destination)
drawVector(point: ball.position, vector: line, color: .green)
var nextVector = line.normalized().bounced(withNormal: collision.normal.normalized()).normalized()
nextVector.dx *= 200
nextVector.dy *= 200
drawVector(point: collision.destination, vector: nextVector, color: .yellow)
}
private func rayCast(start: CGPoint, direction: CGVector) -> (destination:CGPoint, normal: CGVector)? {
let endVector = CGVector(
dx: start.x + direction.normalized().dx * 4000,
dy: start.y + direction.normalized().dy * 4000
)
let endPoint = CGPoint(x: endVector.dx, y: endVector.dy)
var closestPoint: CGPoint?
var normal: CGVector?
physicsWorld.enumerateBodies(alongRayStart: start, end: endPoint) {
(physicsBody:SKPhysicsBody,
point:CGPoint,
normalVector:CGVector,
stop:UnsafeMutablePointer<ObjCBool>) in
guard start.distanceTo(point) > 1 else {
return
}
guard let newClosestPoint = closestPoint else {
closestPoint = point
normal = normalVector
return
}
guard start.distanceTo(point) < start.distanceTo(newClosestPoint) else {
return
}
normal = normalVector
}
guard let p = closestPoint, let n = normal else { return nil }
return (p, n)
}
private func drawVector(point: CGPoint, vector: CGVector, color: SKColor) {
let start = point
let destX = (start.x + vector.dx)
let destY = (start.y + vector.dy)
let to = CGPoint(x: destX, y: destY)
let path = CGMutablePath()
path.move(to: start)
path.addLine(to: to)
path.closeSubpath()
let line = SKShapeNode(path: path)
line.strokeColor = color
line.lineWidth = 6
line.name = "line"
addChild(line)
}
}
它是如何工作的?
这种方法
func drawVector(point: CGPoint, vector: CGVector, color: SKColor)
该功能仅仅从给定的起始点
开始绘制一个向量
,并使用指定的颜色
。
这个
func rayCast(start: CGPoint, direction: CGVector) -> (destination:CGPoint, normal: CGVector)?
使用上面讨论的射线投射技术来查找交点和相关的法向量。
最后在
func update(_ currentTime: TimeInterval)
我们要做的是:
- 删除上一帧绘制的线条
- 使用射线检测获取碰撞点
- 绘制球和碰撞点之间的线条
- 计算球反弹的方向
- 使用前一步得到的方向值,从碰撞点开始绘制一条线
就这些。