Bounce Rays with Enumeratebodies Alongraystart

Bounce rays with enumerateBodies alongRayStart

I guess you are looking for something like this right?

Sample Image

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.

Sample Image

How to place nodes on the ground?

You could cast a ray, at the box's intended x position, from the top of the screen to the bottom, and when the ray hits the ground, place the box directly above the hitpoint. See enumerateBodies(alongRayStart:end:using:). In your case you might want to insert into createObstacle something like:

let topOfScreen = size.height / 2
let bottomOfScreen = -size.height / 2
obstacle.position.x = -768

physicsWorld.enumerateBodies(alongRayStart: CGPoint(x: obstacle.position.x, y: topOfScreen), end: CGPoint(x: obstacle.position.x, y: bottomOfScreen)) { body, point, normal, stop in
if body.node?.name == "ground" {
// body is the ground's physics body. point is the point that an object
// falling from the sky would hit the ground.
// Assuming the obstacle's anchor point is at its center, its actual position
// would be slightly above this point
let positionAboveBody = point + (obstacle.size.height / 2)
obstacle.position.y = positionAboveBody
}
}

how can I determine if a square can reach position without contact with other sprites?

Some observations...

If box B's initial position is to the right of the ending position (in the gap), then the box can successfully move to the ending position without colliding with the other boxes only if theta is a counter-clockwise angle (see Figure below). For this test, use the box B's top-right corner and the bottom-left corner of C.

Sample Image

Similarly, If box B's initial position is to the left of the ending position, then it can successfully move to the ending position without colliding with the other boxes only if theta is a counter-clockwise angle (see Figure below). For this test, use the box B's top-left corner and the bottom-right corner of A.

Sample Image

Some code...

First, extend CGPoint to determine the corners of a box.

extension CGPoint {
func bottomLeftCorner(size:CGSize) -> CGPoint {
return CGPoint (x:x - size.width/2.0, y:y - size.height/2.0)
}

func bottomRightCorner(size:CGSize) -> CGPoint {
return CGPoint(x:x + size.width/2.0, y:y - size.height/2.0)
}

func topLeftCorner(size:CGSize) -> CGPoint {
return CGPoint (x:x - size.width/2.0, y:y + size.height/2.0)
}

func topRightCorner(size:CGSize) -> CGPoint {
return CGPoint(x:x + size.width/2.0, y:y + size.height/2.0)
}
}

The following code allows the user to drop/drag box B. While the user moves the box, the code performs an on-the-fly test to see if the box can move into the gap without colliding with the other boxes.

class GameScene: SKScene {

let size1 = CGSize(width: 100, height: 50)
let size2 = CGSize(width: 50, height: 50)
let size3 = CGSize(width: 100, height: 50)

var boxA:SKSpriteNode!
var boxB:SKSpriteNode!
var boxC:SKSpriteNode!

var center:CGPoint!

override func didMove(to view: SKView) {

// This is box B's ending position
center = CGPoint (x:0,y:0)

// Define and add the boxes to the scene
boxA = SKSpriteNode(color: SKColor.yellow, size: size1)
boxB = SKSpriteNode(color: SKColor.red, size: size2)
boxC = SKSpriteNode(color: SKColor.blue, size: size3)

boxA.position = CGPoint(x: -size1.width, y: 0)
boxB.position = CGPoint(x: 0, y: 0)
boxC.position = CGPoint(x: size3.width, y: 0)

boxB.zPosition = 1

addChild(boxA)
addChild(boxB)
addChild(boxC)
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)

// Allow user to drag box to a new location
boxB.position = location

// Find the appropriate corners
var cornerA:CGPoint!
var cornerB:CGPoint!
var cornerC:CGPoint!
if (boxB.position.x < center.x) {
cornerA = boxA.position.bottomRightCorner(size: boxA.size)
cornerB = boxB.position.topLeftCorner(size: boxB.size)
cornerC = center.topLeftCorner(size: boxB.size)
}
else {
cornerA = center.topRightCorner(size: boxB.size)
cornerB = boxB.position.topRightCorner(size: boxB.size)
cornerC = boxC.position.bottomLeftCorner(size: boxC.size)
}
// Test if box B can move in the gap without colliding
if isCounterClockwise(A: cornerA, B: cornerB, C: cornerC) {
boxB.color = SKColor.green
}
else {
boxB.color = SKColor.red
}
}
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// Move box B to the ending position
let action = SKAction.move(to: center, duration: 2)
boxB.run(action)
}

// Test direction of angle between line segments AB and AC
func isCounterClockwise (A:CGPoint, B:CGPoint, C:CGPoint) -> Bool {
return (C.y-A.y)*(B.x-A.x) > (B.y-A.y)*(C.x-A.x)
}
}

and a video clip...

Sample Image

Box B turns green if it can move into the gap without colliding and turns red if not.



Related Topics



Leave a reply



Submit