I'm working on a top-down space game built using Swift and SceneKit with the following setup:
SCNNode representing a spaceship
-M_PI_2
to M_PI + M_PI_2
Game controller thumbstick input
-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
?
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)
}