问题
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:
- Cast a ray, get all the bodies it intersects with, and get the closest one.
- 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
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.
来源:https://stackoverflow.com/questions/51819678/bounce-rays-with-enumeratebodies-alongraystart