Bounce rays with enumerateBodies alongRayStart

非 Y 不嫁゛ 提交于 2019-12-19 04:05:50

问题


I want to trace the path where a bullet will move in my SpriteKit GameScene. I'm using "enumerateBodies(alongRayStart", I can easily calculate the first collision with a physics body.

I don't know how to calculate the angle of reflection, given the contact point and the contact normal.

I want to calculate the path, over 5 reflections/bounces, so first I:

  1. Cast a ray, get all the bodies it intersects with, and get the closest one.
  2. I then use that contact point as the start of my next reflection/bounce....but I'm struggling with what the end point should be set to....

What I think I should be doing is getting the angle between the contact point and the contact normal, and then calculating a new point opposite to that...

    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:


I guess you are looking for something like this right?

1. Working code

First of all let me post the full working code. Just create a new Xcode project based SpriteKit and

  1. In GameViewController.swift set

    scene.scaleMode = .resizeFill

  2. Remove the usual label you find in GameScene.sks

  3. Replace Scene.swift with the following code

>

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. How does it work?

Lets have a look at what this code does. We'll start from the bottom.

3. CGPoint and CGVector extensions

These are just simple extensions (mainly taken from Ray Wenderlich's repository on GitHub) to simplify the geometrical operations we are going to perform.

4. drawVector(point:vector:color)

This is a simple method to draw a vector with a given color starting from a given point.

Nothing fancy here.

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

This method perform a raycasting and returns the ALMOST closest point where the ray enter in collision with a physics body.

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

What does it mean ALMOST the closets?

It means the the destination point must be at least 1 point distant from the start point

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

Ok but why?

Because without this rule the ray gets stuck into a physics body and it is never able to get outside of it.

6. drawRayCasting(angle)

This method basically keeps the local variables up to date to properly generate 5 segments.

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

The first segment has starting point equals to zero and a direction diving my the angle parameter.

Segments 2 to 5 use the final point and the "mirrored direction" of the previous segment.

update(_ currentTime: TimeInterval)

Here I am just calling drawRayCasting every frame passing the current angle value and the increasing angle by 0.001.

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

6. didMove(to view: SKView)

Finally here I create a physics body around the scene in order to make the ray bounce over the borders.

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

7. Wrap up

I hope the explanation is clear. Should you have any doubt let me know.

Update

There was a bug in the bounced function. It was preventing a proper calculation of the reflected ray. It is now fixed.



来源:https://stackoverflow.com/questions/51819678/bounce-rays-with-enumeratebodies-alongraystart

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!