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:
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.
This is the result we are going to build
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.
Of course you need to
Here's the full code you'll need
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)
}
}
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)
}
}
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
That's it
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With