Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Find the path a bouncing SKSpriteNode will take

I'd like to display two lines when a SKSpriteNode is being bounced around the screen — one of where the node is going towards, and second which is in what direction the node is going to bounce off of other sprite nodes.

Essentially, I'd like to do something like this:

enter image description here

I can create the first line fairly easily using the velocity of the ball, and the second line would be fairly easy as well, but only if I know where the line is going to collide with another SKSpriteNode.

For the first line, I'm creating a SKShapeNode and setting its path to the same line as the velocity of the ball, but I'm not sure how to find the path the ball will take after it bounces off of the nearest obstacle.

In short — how can I find the point at which a moving object will collide with another object, and then find the direction in which it will move after colliding with the object? This isn't an issue if I know what object it's going to collide with, but short of doing some insanely inefficient brute-forcing, I'm not sure how to find what object the ball is going to collide with and where.

I'm looking for something like this:

let line: SKSpriteNode // this is the first line
let scene: SKScene // the scene that contains all of the objects

// this doesn't exist, but I'm looking for something that would do this
let collisionPoint: CGPoint = line.firstCollision(in: scene)

For my purposes, there are no environmental factors on the sprite (no gravity, air resistance, etc.), it's moving at a constant velocity, and doesn't lose any velocity in rebounds, but it would be useful if the answer could include a way of calculating the the nth rebound of a sprite in any environment.

like image 941
Jojodmo Avatar asked Aug 10 '18 22:08

Jojodmo


1 Answers

This is the result we are going to build

enter image description here

Ray Casting

The technique used here is called Ray Casting and you can find more details in this similar answer I have already written.

Basically we use the SpriteKit physics engine to determine draw a line and get the first point of intersection between that line and a physics body.

Code

Of course you need to

  • create an empty Xcode project based on SpriteKit
  • add a circular sprite we'll call ball
  • add a physics body to the ball and give it some speed

Here's the full code you'll need

Extensions

Add these 2 extensions to your project

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

Now replace all the code you have in Scene.swift with this one

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

How does it work?

This method

func drawVector(point: CGPoint, vector: CGVector, color: SKColor)

simply draws a vector from a starting point a with a given color.

This one

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

uses the Ray Casting technique discussed above to find the point of intersection and the related normal vector.

Finally in the

func update(_ currentTime: TimeInterval)

we are going to

  • remove the lines drawn during the previous frame
  • get the collision point using the raycasting
  • draw the line between the ball and the collision point
  • calculate the direction the ball is going to bounce
  • draw a line that goes from the collision using the direction value got in the previous step

That's it

like image 108
Luca Angeletti Avatar answered Oct 08 '22 07:10

Luca Angeletti