Search code examples
swiftgame-physicsscenekitradiansscnnode

Use PhysicsBody to rotate an SCNNode to match the Thumbstick's radian


I'm working on a top-down space game built using Swift and SceneKit with the following setup:

SCNNode representing a spaceship

  • Rotation is constrained to the y axis; values range from -M_PI_2 to M_PI + M_PI_2
  • Movement is constrained to the x and z axes.

Game controller thumbstick input

  • Values range from -1.0 to 1.0 on the x and y axes.

When the game controller's thumbstick changes position, the spaceship should rotate using the physics body to match the thumbstick's radian.

The target radian of the thumbstick can be calculated with the following:

let targetRadian = M_PI_2 + atan2(-y, -x)

The current radian of the node can be obtained with the following:

let currentRadian = node.presentationNode.rotation.w * node.presentationNode.rotation.y

NSTimeInterval deltaTime provides the time in seconds since the last rotation calculation.

How can the node be rotated using angularVelocity, applyTorque, or another physics method to reach the targetRadian?


Solution

  • The difference between the targetRadian and the currentRadian ranged from 0.0 to -2π depending on the value of currentRadian. This equation will determine the shortest direction to turn, .Clockwise or .CounterClockwise, to reach the targetRadian:

    let turnDirection = (radianDifference + (M_PI * 2)) % (M_PI * 2) < M_PI ? RotationDirection.CounterClockwise : RotationDirection.Clockwise
    

    Using applyTorque, there is a possibility to over-rotate past the targetRadian resulting in a wobbling effect, like a compass magnetizing toward a point, as the rotation changes direction back and forth to reach the targetRadian. The following, while not a perfect solution, dampened the effect:

    let turnDampener = abs(radianDifference) < 1.0 ? abs(radianDifference) : 1.0
    

    The complete solution is thus:

    enum RotationDirection: Double {
        case Clockwise = -1.0
        case CounterClockwise = 1.0
    }
    
    func rotateNodeTowardDirectionalVector(node: SCNNode, targetDirectionalVector: (x: Double, y: Double), deltaTime: NSTimeInterval) {
        guard abs(targetDirectionalVector.x) > 0.0 || abs(targetDirectionalVector.y) > 0.0 else { return }
    
        let currentRadian = Double(node.presentationNode.rotation.w * node.presentationNode.rotation.y)
        let targetRadian = M_PI_2 + atan2(-targetDirectionalVector.y, -targetDirectionalVector.x)
    
        let radianDifference = targetRadian - currentRadian
    
        let π2 = M_PI * 2
        let turnDirection = (radianDifference + π2) % π2 < M_PI ? RotationDirection.CounterClockwise : RotationDirection.Clockwise
    
        let absRadianDifference = abs(radianDifference)
        let turnDampener = absRadianDifference < 1.0 ? absRadianDifference : 1.0
    
        node.physicsBody?.applyTorque(SCNVector4Make(0, CGFloat(turnDirection.rawValue), 0, CGFloat(deltaTime * turnDampener)), impulse: true)
    }