How to make a sprite jump to a specific height with SpriteKit? - ios

I am trying to use applyImpulse to make a sprite jump to a specific height. In the example code below, the dynamic black circle jumps to the same height as the static red circle, but it only works with a kludge.
If my physics is right, the required initial vertical momentum to launch a projectile to height Y is given by mass * sqrt(2 * gravity * Y). Yet this formula results in the black circle moving very little.
Through trial and error, I have discovered that I can make the red circle jump more or less accurately by multiplying the vertical component of the impulse vector by 12.3, as illustrated in the code below.
This seems completely arbitrary and is driving me crazy. I am obviously doing something wrong. What's the right way to do this?
Here's what I think is the relevant bit of code:
let dy = mass * sqrt(
2 * -self.physicsWorld.gravity.dy
* (fixedCircle.position.y-jumpingCircle.position.y))
* 12.3
jumpingCircle.physicsBody?.applyImpulse(CGVectorMake(0, CGFloat(dy)))
Here's the GameScene.swift class in its entirety, should you wish to copy and paste...
import SpriteKit
class GameScene: SKScene {
var jumpingCircle = SKShapeNode()
var fixedCircle = SKShapeNode()
override func didMoveToView(view: SKView) {
self.scaleMode = .AspectFit
// Create an exterior physical boundary
self.physicsBody = SKPhysicsBody(edgeLoopFromRect:self.frame)
self.physicsWorld.gravity = CGVectorMake(0, -5);
// Create the jumping circle
jumpingCircle = SKShapeNode(circleOfRadius: 50)
jumpingCircle.position = CGPoint(x: 100,y: 0)
jumpingCircle.strokeColor = .blackColor()
jumpingCircle.physicsBody = SKPhysicsBody(circleOfRadius: 50)
jumpingCircle.physicsBody?.linearDamping = 0.0
self.addChild(jumpingCircle)
// Create the fixed circle
fixedCircle = SKShapeNode(circleOfRadius: 50)
fixedCircle.position = CGPoint(x: 100,y: 384)
fixedCircle.strokeColor = .redColor()
self.addChild(fixedCircle)
}
override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
// to reach certain height, initial y velocity should by sqrt(2 * gravity * Y)
// momentum required to reach this velocity is mass * velocity
// therefore, vertical impulse should be given by mass * sqrt(2 * gravity * Y)
if let mass = jumpingCircle.physicsBody?.mass{
// calculate vertical impulse
// this formula "should work" but does not
// let dy = mass * sqrt(2 * -self.physicsWorld.gravity.dy * (fixedCircle.position.y-jumpingCircle.position.y))
// this formula "works", but has the arbitrary multiplier
let dy = mass * sqrt(2 * -self.physicsWorld.gravity.dy * (fixedCircle.position.y-jumpingCircle.position.y)) * 12.3
// apply the Impulse
jumpingCircle.physicsBody?.applyImpulse(CGVectorMake(0, CGFloat(dy)))
}
}
}

Figured it out.
SpriteKit apparently has an undocumented pixel-to-"meter" ratio of 150.
I discovered this when I realized the mass SpriteKit was automatically calculating for my circles was not 50*pi*r^2 as it should be. I worked backward from mass to calculate the radius SpriteKit was using, and it was 7500, which happens to be 50*150.
And 12.3? It just happens to be (approximately) the square root of 150.
So to make these physics simulations work, you have to consider this ratio. I'm calling it "pixel to unit" (PTU) because it has nothing to do with meters in spite of Apple's insistence that SpriteKit uses SI units. But because it's undocumented it seems possible to change, so I'm kicking off my simulation using the following line of code to determine the true PTU:
let ptu = 1.0 / sqrt(SKPhysicsBody(rectangleOfSize: CGSize(width:1,height:1)).mass)
This is an expensive operation, so it should only be called once. I'm calling it when setting up the initial Scene.
My impulse calculation is now as follows:
let dy = mass * sqrt(2
* -self.physicsWorld.gravity.dy
* (fixedCircle.position.y-jumpingCircle.position.y
* ptu))
And now the black circle jumps to perfect alignment with the red circle and I can go back to sleeping at night.

Instead of applyImpulse use velocity. Velocity will be more specific.
jumpingCircle.physicsBody?.velocity = CGVectorMake(jumpingCircle.physicsBody.velocity.dx, 300.0f);
Code not tested.

Related

Using CATransform3DRotate with perspective: how to correct the 2D size increase?

I'm trying to create a paper folding effect in Swift using CALayers and CATransform3DRotate. There are some libraries out there, but those are pretty outdated and don't fit my needs (they don't have symmetric folds, for example).
My content view controller will squeeze to the right half side of the screen, revealing the menu at the left side.
Everything went well, until I applied perspective: then the dimensions I calculate are not correct anymore.
To explain the problem, I created a demo to show you what I'm doing.
This the content view controller with three squares. I will use three folds, so each square will be on a separate fold.
The even folds will get anchor point (0, 0.5) and the odd folds will get anchor point (1, 0.5), plus they'll receive a shadow.
When fully folded, the content view will be half of the screen's width.
On an iPhone 7, each fold/plane will be 125 points unfolded and 62.5 points fully folded when looked at.
To calculate the rotation needed to achieve this 62.5 points width, we can use a trigonometric function. To illustrate, look at this top-down view:
We know the original plane size (125) and the 2D width (62.5), so we can calculate the angle α using arccos:
let angle = acos(width / originalWidth)
The result is 1.04719755 rad or 60 degrees.
When using this formula with CATransform3DRotate, I get the correct result:
Now for the problem: when I add perspective, my calculation isn't correct anymore. The planes are bigger. Probably because of the now different projection.
You can see the planes are now overlapping and being clipped.
I reconstructed the desired result on the right by playing with the angle, but the correction needed is not consistent, unfortunately.
Here's the code I use. It works perfectly without perspective.
// Loop layers
for i in 0..<self.layers.count {
// Get layer
let layer = self.layers[i]
// Get dimensions
let width = self.frame.size.width / CGFloat(self.numberOfFolds)
let originalWidth = self.sourceView.frame.size.width / CGFloat(self.numberOfFolds)
// Calculate angle
let angle = acos(width / originalWidth)
// Set transform
layer.transform = CATransform3DIdentity
layer.transform.m34 = 1.0 / -500
layer.transform = CATransform3DRotate(layer.transform, angle * (i % 2 == 0 ? -1 : 1), 0, 1, 0)
// Update position
if i % 2 == 0 {
layer.position = CGPoint(x: (width * CGFloat(i)), y: layer.position.y)
} else {
layer.position = CGPoint(x: (width * CGFloat(i + 1)), y: layer.position.y)
}
}
So my question is: how do I achieve the desired result? Do I need to correct the angle, or should I calculate the projected/2D width differently?
Thanks in advance! :)

Mysterious factor of 150 on SpriteKit's Physics. Gravity and Forces

I wanted an object to float on the screen, resisting gravity, not moving at all.
This is the gravity setting of the view.
self.physicsWorld.gravity = CGVector(dx: 0, dy: 5.0)
it's set to 5m/s^2 upwards. So object gets accelerated by 5m upwards per second.
Mass of the object is set to 1.0kg
self.physicsBody?.mass = 1.0
I applied a force to the object so it can resist the gravity. So I did the following.
func update(delta: TimeInterval) {
...
let force = CGVector(dx: 0.0, dy: -5.0)
self.physicsBody?.applyForce(force)
}
I applied -5N because I thought the gravitational force applied to the object is 1kg * 5m/s^2 = 5N. Applying -5N will make the object gets accelerated by -5m/s^2, floating on the screen as a result with the gravity.
But it did not work. Instead I had to do this.
let force = CGVector(dx: 0.0, dy: -5.0 * 150.0)
-5 multiplied by 150 is -750. So, where does this 150 come from? Why do I have to apply -750N instead of -5N to make the object resist gravity?
I also tested out different masses and forces on different gravity settings.
self.physicsBody?.mass = 2.0
let force = CGVector(dx: 0.0, dy: -5.0 * 150.0 * 2)
self.physicsWorld.gravity = CGVector(dx: 0, dy: 15.0)
self.physicsBody?.mass = 2.0
let force = CGVector(dx: 0.0, dy: -15.0 * 150.0 * 2)
and they all worked find. F=ma.
Question is the mysterious factor of 150. Where the hell does it come form?
OK, so it was all about wrong documentation of Apple.
Here's the truth of 150.
this seems to be little bit stupid, but applyForce is measured in ((points * kilo) / sec ^ 2), but the gravity acceleration is in Newtons ((kilo * meter)/ sec ^ 2) (despite the fact it's described as meters per second in documentation. Meters per second! Acceleration!). Multiply it by mass and get the force.
https://stackoverflow.com/a/31868380/5752908
Come on, Apple... It's been out there for 4 years.

Apply rotation around axis defined by touched point

I have an object displayed using OpenGL ES on an iPad. The model is defined by vertices, normals and indexes to vertices. The origin of the model is 0,0,0. Using UIGestureRecognizer I can detect various gestures - two-fingered swipe horizontally for rotation about y, vertically for rotation about x. Two-fingered rotate gesture for rotation about y. Pan to move the model around. Pinch/zoom gesture to scale. I want the viewer to be able to manipulate the model to see (for example) the reverse of the model or the whole thing at once.
The basic strategy comes from Ray Wenderlich's tutorial but I have rewritten this in Swift.
I understand quaternions to be a vector and an angle. The vectors up, right and front represent the three axes:
front = GLKVector3Make(0.0, 0.0, 1.0)
right = GLKVector3Make(1.0, 0.0, 0.0)
up = GLKVector3Make(0.0, 1.0, 0.0)
so the quaternion apples a rotation around each of the three axes (though only one of dx, dy, dz has a value, decided by the gesture recognizer.)
func rotate(rotation : GLKVector3, multiplier : Float) {
let dx = rotation.x - rotationStart.x
let dy = rotation.y - rotationStart.y
let dz = rotation.z - rotationStart.z
rotationStart = GLKVector3Make(rotation.x, rotation.y, rotation.z)
rotationEnd = GLKQuaternionMultiply(GLKQuaternionMakeWithAngleAndVector3Axis(dx * multiplier, up), rotationEnd)
rotationEnd = GLKQuaternionMultiply(GLKQuaternionMakeWithAngleAndVector3Axis(dy * multiplier, right), rotationEnd)
rotationEnd = GLKQuaternionMultiply((GLKQuaternionMakeWithAngleAndVector3Axis(-dz, front)), rotationEnd)
state = .Rotation
}
Drawing uses the modelViewMatrix, calculated by the following function:
func modelViewMatrix() -> GLKMatrix4 {
var modelViewMatrix = GLKMatrix4Identity
// translation and zoom
modelViewMatrix = GLKMatrix4Translate(modelViewMatrix, translationEnd.x, translationEnd.y, -initialDepth);
// rotation
let quaternionMatrix = GLKMatrix4MakeWithQuaternion(rotationEnd)
modelViewMatrix = GLKMatrix4Multiply(modelViewMatrix, quaternionMatrix)
// scale
modelViewMatrix = GLKMatrix4Scale(modelViewMatrix, scaleEnd, scaleEnd, scaleEnd);
// rotation
return modelViewMatrix
}
And mostly this works. However everything is relative to the origin.
If the model is rotated then the pivot is always an axis passing through the origin - if zoomed in looking at the end of the model away from the origin and then rotating, the model can rapidly swing out of view. If the model is scaled then the origin is always the fixed point with the model growing larger or smaller - if the origin is off-screen and scale is reduced the model can disappear from view as it collapses toward the origin...
What should happen is that whatever the current view, the model rotates or scales relative to the current view. For a rotation around the y axis that would mean defining the y axis around which the rotation occurs as passing vertically through the middle of the current view. For a scale operation the fixed point of the model would be in the centre of the screen with the model shrinking toward or growing outward from that point.
I know that in 2D the solution is always to translate to the origin, apply rotation and then apply the inverse of the first translation. I don't see why this should be different in 3D, but I cannot find any example doing this with quaternions only matrices. I have tried to apply a translation and its inverse around the rotation but nothing has an effect.
So I tried to do this in the rotate function:
let xTranslation : Float = 300.0
let yTranslation : Float = 300.0
let translation = GLKMatrix4Translate(GLKMatrix4Identity, xTranslation, yTranslation, -initialDepth);
rotationEnd = GLKQuaternionMultiply(GLKQuaternionMakeWithMatrix4(translation) , rotationEnd)
rotationEnd = GLKQuaternionMultiply(GLKQuaternionMakeWithAngleAndVector3Axis(dx * multiplier, up), rotationEnd)
rotationEnd = GLKQuaternionMultiply(GLKQuaternionMakeWithAngleAndVector3Axis(dy * multiplier, right), rotationEnd)
rotationEnd = GLKQuaternionMultiply((GLKQuaternionMakeWithAngleAndVector3Axis(-dz, front)), rotationEnd)
// inverse translation
let inverseTranslation = GLKMatrix4Translate(GLKMatrix4Identity, -xTranslation, -yTranslation, -initialDepth);
rotationEnd = GLKQuaternionMultiply(GLKQuaternionMakeWithMatrix4(inverseTranslation) , rotationEnd)
The translation is 300,300 but there is no effect at all, it still pivots around where I know the origin to be. I've searched a long time for sample code and not found any.
The modelViewMatrix is applied in update() with:
effect?.transform.modelviewMatrix = modelViewMatrix
I could also cheat by adjusting all of the values in the model so that 0,0,0 falls at a central point - but that would still be a fixed origin and would be only marginally better.
The problem is in the last operation you made, you should swap the inverseTranslation with rotationEnd :
rotationEnd = GLKQuaternionMultiply(rotationEnd, GLKQuaternionMakeWithMatrix4(inverseTranslation))
And I think the partial rotation(dx, dy, dz) should follow the same rule.
In fact, if you want to change the pivot, this is how your matrix multiplication should be done:
modelMatrix = translationMatrix * rotationMatrix * inverse(translationMatrix)
and the result in homogeneous coordinates will be calculated as follows:
newPoint = translationMatrix * rotationMatrix * inverse(translationMatrix) * v4(x,y,z,1)
Example
This is a 2D test example that you can run in a playground.
let v4 = GLKVector4Make(1, 0, 0, 1) // Point A
let T = GLKMatrix4Translate(GLKMatrix4Identity, 1, 2, 0);
let rot = GLKMatrix4MakeWithQuaternion(GLKQuaternionMakeWithAngleAndVector3Axis(Float(M_PI)*0.5, GLKVector3Make(0, 0, 1))) //rotate by PI/2 around the z axis.
let invT = GLKMatrix4Translate(GLKMatrix4Identity, -1, -2, 0);
let partModelMat = GLKMatrix4Multiply(T, rot)
let modelMat = GLKMatrix4Multiply(partModelMat, invT) //The parameters were swapped in your code
//and the result would the rot matrix, since T*invT will be identity
var v4r = GLKMatrix4MultiplyVector4(modelMat, v4) //ModelMatrix multiplication with pointA
print(v4r.v) //(3,2,0,1)
//Step by step multiplication using the relation described above
v4r = GLKMatrix4MultiplyVector4(invT, v4)
v4r = GLKMatrix4MultiplyVector4(rot, v4r)
v4r = GLKMatrix4MultiplyVector4(T, v4r)
print(v4r.v) //(3,2,0,1)
As for the scale, if I understand correctly what you want, I would recommend to do it like it's done here: https://gamedev.stackexchange.com/questions/61473/combining-rotation-scaling-around-a-pivot-with-translation-into-a-matrix

How to apply force at an speicific angle in spriteKit?

I want to give Force to my SKSpriteNode at specific angle.
So, How to treat my CGVector for give force at specific angle?
I had searched for it but unfortunately not getting any good way.
What i wants to achieve :
My SKSpriteNode moving towards the screen. There are buttons on top like 30,45,60.
So if user press button(i.e. that Button contain "30") then i had to move my SKSpriteNode to 30 degree with same speed.
Please help me towards it if any of you can help me regarding this.
First, you will need to convert the angle in degrees to radians by multiplying it by pi / 180
CGFloat angleInRadians = angleInDegrees * M_PI / 180;
You can then determine the vector components in that direction by
CGFloat dx = cosf(angleInRadians);
CGFloat dy = sinf(angleInRadians);
and finally apply a force to the sprite with
[sprite.physicsBody applyForce:CGVectorMake(dx*scale, dy*scale)];
where scale determines how much force is applied.
Optionally, you can rotate the sprite to face in the same direction as its motion by
sprite.zRotation = angleInRadians + offset;
where offset is the difference in angle, in radians, between your sprite's image and zero degrees. For example, if your sprite is facing up when zRotation is zero, offset should be -M_PI_2.

SpriteKit move rotated physicsBody with applyImpulse

I want to move a physicsBody with the applyImpulse method in a direction based on the physicsBody rotation.
Foe example, the physicsBody is a square in shape, I call a "move" which will apply an impulse to make it move up vertically. I then call a method to rotate the physicsBody 45 degrees right. If I call the "move" method again, the physicsBody will move diagonally right and up.
I suggest that you follow Sprite Kit’s coordinate and rotation conventions. Specifically, your sprite image should be facing right at zero degrees (the default value), and a positive value is a counter-clockwise rotation. That said, here's one way to apply an impulse in the direction a sprite is facing:
// Specify the force to apply to the SKPhysicsBody
CGFloat r = 5;
// Create a vector in the direction the sprite is facing
CGFloat dx = r * cos (sprite.zRotation);
CGFloat dy = r * sin (sprite.zRotation);
// Apply impulse to physics body
[sprite.physicsBody applyImpulse:CGVectorMake(dx,dy)];
UPDATED:
Fixed with the below thanks to #0x141E
-(void)characterJump {
CGFloat radianFactor = 0.0174532925;
CGFloat rotationInDegrees = _body.zRotation / radianFactor;
CGFloat newRotationDegrees = rotationInDegrees + 90;
CGFloat newRotationRadians = newRotationDegrees * radianFactor;
CGFloat r = 500;
CGFloat dx = r * cos(newRotationRadians);
CGFloat dy = r * sin(newRotationRadians);
[_body.physicsBody applyImpulse:CGVectorMake(dx, dy)];
}

Resources