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?
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)
}
Related
I am currently facing the problem that I want to calculate the angle in radians from the camera's position to a target position. However, this calculation needs to take into account the heading of the camera as well.
For example, when the camera is facing away from the object the function should return π. So far the function I have written works most of the time. However when the user gets close to the X and Z axis the arrow does not point to the target any more, rather it points slightly to the left or right depending if you are at positive or negative X and z space.
Currently, I'm not sure why my function does not work. The only explanation I would have for this behavior is gimbal lock. However I'm not quite sure how to implement the same function using quaternions.
I also attached some photos to this post that the issue is a little bit more clear.
Here is the function I'm using right now:
func getAngle() -> Float {
guard let pointOfView = self.sceneView.session.currentFrame else { return 0.0 }
let cameraPosition = pointOfView.camera.transform.columns.3
let heading = getUserVector()
let distance = SCNVector3Make(TargetPosition.x - cameraPosition.x ,TargetPosition.y - cameraPosition.y - TargetPosition.y,TargetPosition.z - cameraPosition.z)
let heading_scalar = sqrtf(heading.x * heading.x + heading.z * heading.z)
let distance_scalar = sqrtf(distance.z * distance.z + distance.z * distance.z)
let x = ((heading.x * distance.x) + (heading.z * distance.z) / (heading_scalar * distance_scalar))
let theta = acos(max(min(x, 1), -1))
if theta < 0.35 {
return 0
}
if (heading.x * (distance.z / distance_scalar) - heading.z * (distance.x/distance_scalar)) > 0{
return theta
}
else{
return -theta
}
}
func getUserVector() -> (SCNVector3) { // (direction)
if let frame = self.sceneView.session.currentFrame {
let mat = SCNMatrix4(frame.camera.transform) // 4x4 transform matrix describing camera in world space
let dir = SCNVector3(-1 * mat.m31, -1 * mat.m32, -1 * mat.m33) // orientation of camera in world space
print(mat)
return dir
}
return SCNVector3(0, 0, -1)
}
Consider the following image as an example. The arrow in the top right corner should be pointing straight up to follow the line to the center object but instead it is pointing slightly to the left. As I am aligned with the z-axis the same behavior happens when aligning with the x-axis.
I figured out the answer to my problem the solution was transforming the object into the prospective of the camera and then simply taking the atan2 to get the angle in between the camera and object hope this post will help future readers!
func getAngle() -> Float {
guard let pointOfView = self.sceneView.session.currentFrame else { return 0.0 }
let cameraPosition = pointOfView.camera.transform
let targetPosition = simd_float4x4(targetNode.transform)
let newTransform = simd_mul(cameraPosition.inverse, targetPosition).columns.3
let theta = atan2(newTransform.z, newTransform.y)
return theta + (Float.pi / 2)
}
I'm new to SpriteKit game development. I'm trying give a boss character the ability to cast fireballs in multiple directions (16 fireballs all at once, 360 degree/16 = 22.5 degree apart).
I know how to get him to fire at a certain position by providing the player's current position, but how to get him to fire at 16 different angles regardless of player's position?
Thanks for any help in advance.
First, set up a loop over the angles
let numAngles = 16
var angle:CGFloat = 0
var angleIncr = CGFloat(2 * M_PI) / CGFloat(numAngles)
let strength:CGFloat = 50
for _ in 0..<numAngles {
...
angle += angleIncr
}
In the loop, convert the angle to the corresponding vector components and then create a vector
let dx = strength * cos (angle)
let dy = strength * sin (angle)
let vector = CGVectorMake (dx, dy)
and create a new fireball and apply an impulse to its physics body
let fireball = ...
fireball.position = player.position
fireball.zRotation = angle
// Add a physics body here
fireball.physicsBody?.appyImpulse (vector)
I'm not sure what code you have in place. for shooting. but ill give this a shot. angles in spritekit are in radians and a there are 2*pi radians in a circle. so you just need to do something like this
let fireballs = 16
let threeSixty = CGFloat(M_PI*2)
for i in 1...fireballs {
let angle = (CGFloat(i) / CGFloat(fireballs)) * threeSixty
// do something useful with your angle
}
I would like to apply a rotation to multiple nodes (selected nodes in my game) using the UIRotationGesture, based on the center point of all those nodes. I can already rotate a single node simply changing it's zRotation.
The problem with multiple nodes is that it changes position and zRotation based on a center node, and I cannot seem to understand how to manage that.
I would like something like this:
What I have to rotate a single node is this:
During the rotation gesture
theRotation = CGFloat(sender.rotation) + self.offset
theRotation = theRotation * -1
node.rotation = theRotation
After the rotation gesture
self.offset = theRotation * -1
Would you have an idea on how set the correct position and angle for my nodes during the rotation?
What I tried:
I tried to add a node in the center (where the white dot is in my pictures, which represents the center) and change the parent of my nodes to be this one, then apply the zRotation on this node, and then replace the right parents. This did not work as I cannot seem to change a parent (my nodes disappear), this is another one of my Stack Questions.
I tried to change the anchor point of my nodes to fit the center point and than rotate them using theRotation. It did not work as I cannot seem to set the anchor point at the center position (that I have). I tried changing the coordinates system of the center's position to fit the node's one, but this is still not working. node.convertPoint(center, fromNode: Self) gives me coordinated like -58;-74 when it's about -1;-.5 (or something like that). I do not understand this.
So now I am thinking to calculate the position and rotation myself, as those did not work, but I would need an idea on how to calculate those as I am not very good with trigonometry/linear algebra, sadly enough.
Thank you for you help!
How I calculate my center:
var maxX = nodesSelected[0].position.x
var minX = nodesSelected[0].position.x
var maxY = nodesSelected[0].position.y
var minY = nodesSelected[0].position.y
for node in nodesSelected{
if node.position.x > maxX{
maxX = node.position.x
}
if node.position.x < minX{
minX = node.position.x
}
if node.position.y > maxY{
maxY = node.position.y
}
if node.position.y > maxY{
minY = node.position.y
}
}
return CGPoint(x: (maxX-minX)/2+minX, y: (maxY-minY)+minY/2)
How I calculate the radius of the rotation (distance between a node and the center):
extension CGPoint {
func distance(point: CGPoint) -> CGFloat {
return abs(CGFloat(hypotf(Float(point.x - x), Float(point.y - y))))
}
How I get my rotation:
sender.rotation
Given a rotationAngle, you can calculate the new position of each node with the code below, you need to know a bit of trigonometry to understand the code.
Here I have an array of SKShapeNode that I called dots (It would be the equivalent of your green nodes in the image). And the centralDot would be your central SKSpriteNode.
for dot in dots {
let dx = dot.position.x - centralDot!.position.x // Get distance X from center
let dy = dot.position.y - centralDot!.position.y // Get distance Y from center
let current_angle = atan(dy / dx) // Current angle is the arctan of dy / dx
let next_angle = current_angle - rotationAngle // Sum how much you want to rotate in radians
// the new x is: center + radius*cos(x)
// the new y is: center + radius*sin(y)
// if dx < 0 you need to get the oposite value of the position
let new_x = dx >= 0 ? centralDot!.position.x + rotationRadius * cos(next_angle) : centralDot!.position.x - rotationRadius * cos(next_angle)
let new_y = dx >= 0 ? centralDot!.position.y + rotationRadius * sin(next_angle) : centralDot!.position.y - rotationRadius * sin(next_angle)
let new_point = CGPoint(x: new_x, y: new_y)
let action = SKAction.moveTo(new_point, duration: 0.2)
dot.runAction(action)
}
Hope this helps
Update:
The first code didn't helped, so I tried another one. This one worked better on my tests.
for i in 0..<dots.count {
let dot = dots[i]
let angle = rotationAngle + CGFloat(M_PI_2 * Double(i))
let new_x = rotationRadius * cos(angle) + centralDot!.position.x
let new_y = rotationRadius * sin(angle) + centralDot!.position.y
let new_point = CGPoint(x: new_x, y: new_y)
let action = SKAction.moveTo(new_point, duration: 1/60)
dot.runAction(action)
}
rotationRadius is a constant, the distance you want between the center and the green node.
So what I am trying to do is make it so that a physics impulsee seems to have the same effect on all devices. So basically if I can figure out A way to do the following I will be able to accomplish my goal.
First lets simplify things by taking out all gravity.
Basically I need to calculate the impulse it will take to get a physics object on the far left of the screen to get to the far right of the screen in the same amount of time no matter how big the screen size is.
The reason I ask is I am making a movement system based on the magnitude and angle of a swipe. However I want it to play the same way on every device. I am calculating magnitude by
(distance (in virtual points)) / (Time spent making gesture)
Then i am applying it as a physics impulse.
This is the code I am working with:
func Jump(angle: CGFloat, strength: CGFloat)
{
if (Ready == true)
{
var rangle:CGFloat = angle * CGFloat(M_PI / 180)
var translate:CGPoint = CGPoint(x: 1, y: 0)
var vx:CGFloat = ((translate.x * cos(rangle)) - (translate.y * sin(angle)))
var vy:CGFloat = ((translate.y * cos(rangle)) + (translate.x * sin(rangle)))
vx *= width
vy *= height
vx *= (strength)
vy *= (strength)
vx /= 4000
vy /= 4000
print("Applying Impulse VX: ")
print(vx)
print(" , VY: ")
print(vy)
println(" )")
var velx = Cavity.physicsBody?.velocity.dx
var vely = Cavity.physicsBody?.velocity.dy
Cavity.physicsBody?.velocity = CGVector(dx: CGFloat(velx!) / 2, dy: CGFloat(vely!) / 2)
Cavity.physicsBody?.applyImpulse(CGVectorMake(vx, vy))
//Cavity.physicsBody?.applyImpulse(CGVectorMake(1000 / width, 1000 / height))
}
}
So basically I want it to be so that if a strength of 1 or 2 is passed it will make the same looking result on all devices.
What you can do is make the strength relative to the screen size.
strengthAdjustment = (1/375*UIScreen.mainScreen().bounds.width)
This uses the iPhone 6 screen (4.7") width (375 pts) to make the strength = 1.
With an iPhone 5s the screen will be only 320 pts which and will only require 0.8533 of the impulse strength to move the width of the screen in the same amount of time.
Hopefully this helps you out.
In a iOS prototype I use a combination of CMDeviceMotion.deviceMotion.yaw and CLHeading.trueHeading to make stable compass heading that is responsive and accurate. This works well when the iPhone is held flat, where I have a graphical arrow that point to a stable compass heading.
The problem appear when the iPhone is held vertical in portait mode. The UIDeviceOrientation constantly changes from UIDeviceOrientationFaceDown to UIDeviceOrientationFaceUp and back. This makes the yaw value to skip back and forth +/-180 degrees based on small changes of the pitch. Is it possible to lock the device to one orientation that gives a stable yaw value, predict the change without glitches or compute the gyro yaw (or roll in this orientation) in other ways?
This poor guy have the same problem, with no answers. Double points possible people! :)
https://stackoverflow.com/questions/10470938/euler-angle-yaw-not-working-when-iphone-orientation-changes
I was just searching for an answer to this problem. It broke my heart a bit to see that you posted this over a year ago, but I figured maybe you or someone else could benefit from the solution.
The issue is gimbal lock. When pitch is about 90 degrees, yaw and roll match up and the gyro loses a degree of freedom. Quaternions are one way of avoiding gimbal lock, but I honestly didn't feel like wrapping my mind around that. Instead, I noticed that yaw and roll actually match up and can simply be summed to to solve the problem (assuming you only care about yaw).
SOLUTION:
float yawDegrees = currentAttitude.yaw * (180.0 / M_PI);
float pitchDegrees = currentAttitude.pitch * (180.0 / M_PI);
float rollDegrees = currentAttitude.roll * (180.0 / M_PI);
double rotationDegrees;
if(rollDegrees < 0 && yawDegrees < 0) // This is the condition where simply
// summing yawDegrees with rollDegrees
// wouldn't work.
// Suppose yaw = -177 and pitch = -165.
// rotationDegrees would then be -342,
// making your rotation angle jump all
// the way around the circle.
{
rotationDegrees = 360 - (-1 * (yawDegrees + rollDegrees));
}
else
{
rotationDegrees = yawDegrees + rollDegrees;
}
// Use rotationDegrees with range 0 - 360 to do whatever you want.
I hope this helps someone else!
If somebody is interested in the implementation in iOS Swift the code is given below:
let queue = NSOperationQueue()
motionManager.startDeviceMotionUpdatesToQueue(queue) {
[weak self] (data: CMDeviceMotion!, error: NSError!) in
var yawDegrees: Double = self!.motionManager.deviceMotion.attitude.yaw * (180.0 / M_PI)
var pitchDegrees: Double = self!.motionManager.deviceMotion.attitude.pitch * (180.0 / M_PI)
var rollDegrees: Double = self!.motionManager.deviceMotion.attitude.roll * (180.0 / M_PI)
if(rollDegrees < 0 && yawDegrees < 0){
self!.rotationDegrees = 360 - (-1 * (yawDegrees + rollDegrees))
}
else {
self!.rotationDegrees = yawDegrees + rollDegrees
}
}
However I am having some problems and I hope #blkhp19 can help me with this because at certain points the angles go into negative values which then messes up the entire calculation and I can't figure out what the problem is.
The problem is a bit confusing because there are at least two different ways to think about Yaw. One is from the phone's perspective, and one from the world perspective.
I'll use this image from Apple to explain further:
If the phone is flat on a table:
Rotations along the phone's yaw (or Z axis): change the compass heading.
Rotations along the phone's roll (or Y axis): do not change compass heading.
Rotations along the phone's pitch (or X axis): do not change compass heading.
If the phone is flat against a wall:
Rotations along the phone's yaw (or Z axis): change the compass heading.
Rotations along the phone's roll (or Y axis): change the compass heading.
Rotations along the phone's pitch (or X axis): do not change compass heading.
For the remainder of this answer, I'll assume the phone is upright and yaw, pitch, and roll refer to exactly what's in the photo above.
Yaw
You'll need to use atan2 and inspect gravity as in this example.
let yaw = -Angle(radians: .pi - atan2(motion.gravity.x, motion.gravity.y))
Pitch
Similar to the above, I primarily just swapped x and z and it seems to be returning the correct values:
let pitch = Angle(radians: .pi - atan2(motion.gravity.z, motion.gravity.y))
Roll (aka Compass Heading)
Use blkhp19's code above which sums up the attitude yaw and roll. If you import SwiftUI, you can leverage the Angle struct to make radian + degrees conversion easier:
func roll(motion: CMDeviceMotion) -> Angle {
let attitudeYaw = Angle(radians: motion.attitude.yaw)
let attitudeRoll = Angle(radians: motion.attitude.roll)
var compassHeading: Angle = attitudeYaw + attitudeRoll
if attitudeRoll.degrees < 0 && attitudeYaw.degrees < 0 {
compassHeading = Angle(degrees: 360 - (-1 * compassHeading.degrees))
}
return compassHeading
}
Also note that if you don't need the actual angle, and all you need is the relationship (e.g. isPhoneUpright), you can simply read gravity values for those.
extension CMDeviceMotion {
var yaw: Angle {
-Angle(radians: .pi - atan2(gravity.x, gravity.y))
}
var pitch: Angle {
Angle(radians: .pi - atan2(gravity.z, gravity.y))
}
var roll: Angle {
let attitudeYaw = Angle(radians: attitude.yaw)
let attitudeRoll = Angle(radians: attitude.roll)
var compassHeading: Angle = attitudeYaw + attitudeRoll
if attitudeRoll.degrees < 0 && attitudeYaw.degrees < 0 {
compassHeading = Angle(degrees: 360 - (-1 * compassHeading.degrees))
}
return compassHeading
}
}