1

I'm trying to calculate the Angle of reach to hit a target in a scenekit scene. My maths ability is about 3/Potato, but after a few hours on Khan Academy, I think I've managed to tease out the logic of this "Angle of Reach" formula from wikipedia, (Which I stumbled across in this Stack Exchange answer)...

enter image description here

I created a function to returns the SCNVector3 to apply a force to a sphere with a given mass, to hit a target.

As I understand it, the ± in the formula means there are two possible angles, that I'll call angleA and angleB which use plus and minus respectively.

The function I came up with is close, but there are two problems.

  1. angleB is sometimes a teeny tiny bit off the mark.
  2. angleA always falls short.

I can live with angleB being a little bit off sometimes. I'm putting that down to SceneKit's physics engine not being deterministic. Someone please tell me if that's not correct.

But I really want to know what's going wrong with angleA. AngleA should provide the larger angle to produce the tall, sweeping, parabolic trajectory I'm looking for.

Here's the function in full.

func launchVectorFor(
        target: SCNVector3,
        fromPosition origin: SCNVector3, 
        withProjectileMass mass: Float) -> SCNVector3 {

        let g: Float =  -1 * (scene?.physicsWorld.gravity.y)!; //m/s

        // Distance (Displacemet)
        let Dx: Float = (target.x - origin.x)
        let Dy: Float = (target.y - origin.y)

        // Velocity
        // I'm just fudging a Velocity here, using the distance
        // between he origin and the target
        let V: Float= sqrt(Dx * Dx + Dy * Dy)

        // Useful powers
        let V2: Float = pow(V, 2)
        let V4: Float = pow(V, 4)
        let Dx2 = pow(Dx, 2)

        // All the math
        let sqrtInput: Float = V4 - ( g * ( g * Dx2 + 2 * Dy * V2))

        let numeratorPlus: Float = V2 + sqrt(sqrtInput)
        let numeratorMinus: Float = V2 - sqrt(sqrtInput)

        let angleInRadiansA = atan(numeratorPlus / ( g * Dx ) )
        let angleInRadiansB = atan(numeratorMinus / ( g * Dx ) )

        let angleA = angleInRadiansA * (Float(180) / Float.pi)
        let angleB = angleInRadiansB * (Float(180) / Float.pi)
        print("solutions A: \(angleA), -- B \(angleB)")

        // Get X & Y components of force * angle * mass
        let Fx = (V * cos(angleInRadiansA)) * mass
        let Fy = (V * sin(angleInRadiansA)) * mass
        let multiplier: Float = Dx > 0 ? 1 : -1

        return SCNVector3(Fx * multiplier, Fy * multiplier, 0)

    }

And here's the code that goes into calling my function, in case it's of any relevance...

func handleTap(_ gestureRecognize: UIGestureRecognizer) {
        let launchPosition = convertTouchToWorldCoords(location: gestureRecognize.location(in: self.view))
        let target: SCNNode = (scene?.rootNode.childNode(withName: "target", recursively: false))!

        let ball: SCNNode = createBall(atLocation: launchPosition)
        scene?.rootNode.addChildNode(ball)
        let launchVector = launchVectorFor(target: target.position, fromPosition: launchPosition, withProjectileMass: 0.5)
        ball.physicsBody?.applyForce(launchVector, asImpulse: true)
        print(launchVector)
    }

    func createBall(atLocation location: SCNVector3) -> SCNNode {
        let sphere = SCNSphere(radius: 0.5)
        let shape = SCNPhysicsShape(geometry: sphere, options: [:])
        let ball = SCNNode(geometry: sphere);
        let pBody = SCNPhysicsBody(type: .dynamic, shape: shape)
        pBody.restitution = 0.95
        pBody.mass = 0.5
        ball.physicsBody = pBody
        ball.position = location
        ball.castsShadow = true
        return ball;
    }

    func convertTouchToWorldCoords(location: CGPoint) -> SCNVector3 {
        let hits = scnView?.hitTest(location, options: nil)
        if let hitResult = hits?.first {
            return hitResult.worldCoordinates
        }

        return SCNVector3Zero
    }

Any help much appreciated

gargantuan
  • 8,888
  • 16
  • 67
  • 108

1 Answers1

2

You are not supposed to "apply a force", you are supposed to set the initial velocity to the V you used in the formula.

If the vector you are returning is a unit vector (has a magnitude of 1), then multiply each component by V and set the velocity of the object directly.

Also, these formula's don't take damping into account (air friction) -- so to hit the target exactly, set your ball's linearDamping and angularDamping to 0.0 (default is 0.1). You will notice more damping on longer trajectories.

NOTE: if you do this, don't use "mass" in the angle calculation

Lou Franco
  • 87,846
  • 14
  • 132
  • 192
  • Are you sure? I'm applying an impulse, which I think what you're describing. In scenekit, that's done like this `ball.physicsBody?.applyForce(launchVector, asImpulse: true)`. And i can't set 'something' to 'V'. I need to know the X and Y components of V for a given angle, because the impulse is applied as a Vector3. – gargantuan Apr 17 '17 at 13:51
  • Try it. You can set ball.velocity to a 3D vector. Make sure the vector has a magnitude equal to the V you used in the formula. – Lou Franco Apr 17 '17 at 13:55
  • An impulse force is not the same as setting the velocity. It will result in the ball having some velocity, but not necessarily the one you used in the formula to calculate the angle. – Lou Franco Apr 17 '17 at 13:56
  • Thank, Ok. So that's fixed the results for `AngleB`. It's perfectly on target every time. And I also don't need the mass anymore, which cleans things up. But `angleA` is still falling short. I'll update my question to show your suggested changes in action... – gargantuan Apr 17 '17 at 14:04
  • I think you are seeing the affect of damping – Lou Franco Apr 17 '17 at 14:15
  • You're absolutely right! Sorry, didn't see that edit until just now. Thanks, there is absolutely no way I would have discovered that on my own. – gargantuan Apr 17 '17 at 14:17