使用enumerateBodies沿着射线反弹光线

3
我想在我的SpriteKit GameScene中跟踪子弹路径。 我正在使用“enumerateBodies(alongRayStart”,可以轻松计算与物理体的第一次碰撞。
我不知道如何计算反射角度,给定接触点和接触法线。
我想计算5次反射/弹跳后的路径,所以首先:
  1. 投掷光线,获取其相交的所有物体,并获取最近的一个。
  2. 然后将该接触点用作下一个反射/弹跳的起点...但我在确定终点时遇到了困难...
我认为应该做的是获取接触点和接触法线之间的角度,然后计算一个相反的新点...
    var points: [CGPoint] = []
    var start: CGPoint = renderComponent.node.position
    var end: CGPoint = crossHairComponent.node.position

    points.append(start)

    var closestNormal: CGVector = .zero

    for i in 0...5 {

        closestNormal = .zero
        var closestLength: CGFloat? = nil
        var closestContact: CGPoint!

        // Get the closest contact point.
        self.physicsWorld.enumerateBodies(alongRayStart: start, end: end) { (physicsBody, contactPoint, contactNormal, stop) in

            let len = start.distance(point: contactPoint)

            if closestContact == nil {
                closestNormal = contactNormal
                closestLength = len
                closestContact = contactPoint
            } else {
                if len <= closestLength! {
                    closestLength = len
                    closestNormal = contactNormal
                    closestContact = contactPoint
                }
            }
        }


        // This is where the code is just plain wrong and my math fails me.
        if closestContact != nil {

            // Calculate intersection angle...doesn't seem right?
            let v1: CGVector = (end - start).normalized().toCGVector()
            let v2: CGVector = closestNormal.normalized()
            var angle = acos(v1.dot(v2)) * (180 / .pi)

            let v1perp = CGVector(dx: -v1.dy, dy: v1.dx)
            if(v2.dot(v1perp) > 0) {
                angle = 360.0 - angle
            }

            angle = angle.degreesToRadians


            // Set the new start point
            start = closestContact

            // Calculate a new end point somewhere in the distance to cast a ray to, so we can repeat the process again
            let x = closestContact.x + cos(angle)*100
            let y = closestContact.y + sin(-angle)*100
            end = CGPoint(x: x, y: y)  

            // Add points to array to draw them on the screen
            points.append(closestContact)
            points.append(end)
        }

    }
1个回答

2

我猜你正在寻找这样的内容对吗?

enter image description here

1. 工作代码

首先,让我发布完整的工作代码。只需创建一个基于SpriteKit的新Xcode项目,并进行以下操作:

  1. 在GameViewController.swift中设置

    scene.scaleMode = .resizeFill

  2. 删除您在GameScene.sks中找到的通常标签

  3. 使用以下代码替换Scene.swift

>

import SpriteKit

class GameScene: SKScene {

    override func didMove(to view: SKView) {
        self.physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
    }

    var angle: CGFloat = 0

    override func update(_ currentTime: TimeInterval) {
        removeAllChildren()
        drawRayCasting(angle: angle)
        angle += 0.001
    }

    private func drawRayCasting(angle: CGFloat) {
        let colors: [UIColor] = [.red, .green, .blue, .orange, .white]

        var start: CGPoint = .zero
        var direction: CGVector = CGVector(angle: angle)

        for i in 0...4 {
            guard let result = rayCast(start: start, direction: direction) else { return }
            let vector = CGVector(from: start, to: result.destination)

            // draw
            drawVector(point: start, vector: vector, color: colors[i])

            // prepare for next iteration
            start = result.destination
            direction = vector.normalized().bounced(withNormal: result.normal.normalized()).normalized()
        }
    }

    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
        addChild(line)
    }
}


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

}

2. 它是如何工作的?

让我们来看看这段代码是如何工作的。我们从底部开始。

3. CGPointCGVector 扩展

这些只是简单的扩展(主要来自于GitHub上Ray Wenderlich的存储库),用于简化我们将要执行的几何操作。

4. drawVector(point:vector:color)

这是一个简单的方法,用于绘制给定颜色向量,从给定的开始。

这里没有花哨的东西。

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
    addChild(line)
}

5. rayCast(start:direction) -> (destination:CGPoint, normal: CGVector)?

该方法执行射线投射并返回最接近的点,该点与物理体发生碰撞。

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

“ALMOST the closest”是什么意思?

这意味着目标点必须至少距离起始点1个点的距离。

guard start.distanceTo(point) > 1 else {
    return
}

好的,但为什么?

因为没有这个规则,光线会被卡在物理体内,永远无法走出它。

6. drawRayCasting(angle)

这个方法基本上会保持局部变量的更新,以正确生成5个线段。

private func drawRayCasting(angle: CGFloat) {
    let colors: [UIColor] = [.red, .green, .blue, .orange, .white]

    var start: CGPoint = .zero
    var direction: CGVector = CGVector(angle: angle)

    for i in 0...4 {
        guard let result = rayCast(start: start, direction: direction) else { return }
        let vector = CGVector(from: start, to: result.destination)

        // draw
        drawVector(point: start, vector: vector, color: colors[i])

        // prepare next direction
        start = result.destination
        direction = vector.normalized().bounced(withNormal: result.normal.normalized()).normalized()
    }
}

第一段的起点为零,方向由角度参数决定。

第二到五段使用前一段的终点和“镜像方向”。

update(_ currentTime: TimeInterval)

在这里,我每帧都调用drawRayCasting函数,传递当前角度值和增加0.001的角度。

var angle: CGFloat = 0
override func update(_ currentTime: TimeInterval) {
    removeAllChildren()
    drawRayCasting(angle: angle)
    angle += 0.001
}

6. didMove(to view: SKView)

最后,我在场景周围创建物理体,使光线能够在边界上反弹。

override func didMove(to view: SKView) {
    self.physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
}

7. 总结

我希望解释得很清楚。 如果您有任何疑问,请让我知道。

更新

bounced函数中存在一个错误。它阻止了反射光线的正确计算。 现在已经修复。

enter image description here


@Chris 它可能与物理身体的近似有关。尝试使物理身体可见,这应该可以解释其行为。 - Luca Angeletti
物理体是可见的这里是图片 - 或许与alpha通道有关? - Chris
它在第一张图片上,是蓝色的线,非常淡。我发现如果你为了调试而绘制法线,法线只是从中心向外绘制,在某些情况下,线条会反射成与您想象的相反的方向似乎不正确 - 在这张图片中,绿色的线应该向下反射。 - Chris
这张图片更容易看出来,我认为我添加的蓝线应该是反射:编辑后的图片 - Chris
@Chris 我知道了。我明天会看一下。 - Luca Angeletti
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接