Bounce rays with enumerateBodies alongRayStart
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
In GameViewController.swift set
scene.scaleMode = .resizeFill
Remove the usual label you find in
GameScene.sks
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.
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.
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.
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...
Box B turns green if it can move into the gap without colliding and turns red if not.
Related Topics
How to Open/View iOS Oslogs Stored on Device
Swift String VS. String! VS. String
How to Create a Noop Block for a Switch Case in Swift
How to Convert Int32 to Int in Swift
Use 'Self' as a Default Parameter
Target Parameter in Dispatchqueue
Where Is the .Camera Anchorentity Located
Smooth Transition Between 2 Scrollviews
Nsundomanager Casting Nsundomanagerproxy Crash in Swift Code
How to Get Next Case of Enum(I.E. Write a Circulating Method) in Swift 4.2
Custom Bullets from Images on Uitextview or Uilabel Swift
Swift: Unable to Decompose Tuple in Certain Closures (E.G., Reduce with Enumerate)
Get Index in Foreach in Swiftui
How to Disambiguate a Type and a Module With the Same Name