Gravity value in SpriteKit game scene - swift

I'm trying to create a game using Apple's SpriteKit game engine.
While implementing some physics-based calculations in the game, I noticed that the calculated results differ from what effectively then happens to objects.
Example: calculating a body's trajectory through projectile motion's equations causes the body to actually fall down much sooner/quicker than what calculated.
How can I make the physics engine match the real-world physics laws when calculating something gravity-related?

I think I know what's going on with the sample code you have supplied on GitHub, which I'll reproduce here as questions on SO should contain the code:
//
// GameScene.swift
// SpriteKitGravitySample
//
// Created by Emilio Schepis on 17/01/2020.
// Copyright © 2020 Emilio Schepis. All rights reserved.
//
import SpriteKit
import GameplayKit
class GameScene: SKScene {
private var subject: SKNode!
override func didMove(to view: SKView) {
super.didMove(to: view)
// World setup (no friction, default gravity)
// Note that this would work with any gravity set to the scene.
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
physicsBody?.friction = 0
subject = SKShapeNode(circleOfRadius: 10)
subject.position = CGPoint(x: frame.midX, y: 30)
subject.physicsBody = SKPhysicsBody(circleOfRadius: 10)
subject.physicsBody?.allowsRotation = false
// Free falling body (no damping)
subject.physicsBody?.linearDamping = 0
subject.physicsBody?.angularDamping = 0
addChild(subject)
// Set an arbitrary velocity to the body
subject.physicsBody?.velocity = CGVector(dx: 30, dy: 700)
// Inaccurate prediction of position over time
for time in stride(from: CGFloat(0), to: 1, by: 0.01) {
let inaccuratePosition = SKShapeNode(circleOfRadius: 2)
inaccuratePosition.strokeColor = .red
// These lines use the projectile motion equations as-is.
// https://en.wikipedia.org/wiki/Projectile_motion#Displacement
let v = subject.physicsBody?.velocity ?? .zero
let x = v.dx * time
let y = v.dy * time + 0.5 * physicsWorld.gravity.dy * pow(time, 2)
inaccuratePosition.position = CGPoint(x: x + subject.position.x,
y: y + subject.position.y)
addChild(inaccuratePosition)
}
// Actual prediction of position over time
for time in stride(from: CGFloat(0), to: 1, by: 0.01) {
let accuratePosition = SKShapeNode(circleOfRadius: 2)
accuratePosition.strokeColor = .green
// These lines use the projectile motion equations
// as if the gravity was 150 times stronger.
// The subject follows this curve perfectly.
let v = subject.physicsBody?.velocity ?? .zero
let x = v.dx * time
let y = v.dy * time + 0.5 * physicsWorld.gravity.dy * 150 * pow(time, 2)
accuratePosition.position = CGPoint(x: x + subject.position.x,
y: y + subject.position.y)
addChild(accuratePosition)
}
}
}
What you've done is to:
Created an object called subject with a physicsBody and placed it
on screen with a initial velocity.
Plotted predicted positions for an object with that velocity under
gravity via the inaccuratePosition node, using Newton's laws of
motion (v = ut + 1/2at²)
Plotted predicted positions for an object with that velocity under
gravity * 150 via the accuratePosition node, using Newton's laws of
motion
All this is is didMoveTo. When the simulation runs, the path of the node subject follows the accuratePosition path accurately.
I think what's happening is that you are calculating the predicted position using subject's physicsBody's velocity, which is in m/s, but the position is in points, so what you should do is convert m/s into point/s first.
So what's the scale factor? Well from Apple's documentation here; it's.... 150 which is too much of a coincidence 😀, so I think that's the problem.
Bear in mind that you set the vertical velocity of your object to 700m/s - that's 1500mph or 105000 SK point/s. You'd expect it to simply disappear out through the top of the screen at high speed, as predicted by your red path. The screen is somewhere between 1,000 and 2,000 points.

Edit - I created a sample project to demonstrate the calculated paths with and without the multiplier.
https://github.com/emilioschepis/spritekit-gravity-sample
TL;DR - When calculating something gravity-related in SpriteKit multiply the gravity of the scene by 151 to obtain an accurate result.
When trying to solve this issue I first started reading the SpriteKit documentation related to gravity:
https://developer.apple.com/documentation/spritekit/skphysicsworld/1449623-gravity
The documentation says:
The components of this property are measured in meters per second. The default value is (0.0,-9.8), which represent’s Earth’s gravity.
Gravity, however is calculated in m/s^2 and not in m/s.
Thinking that was an error in the implementation of gravity in SpriteKit I began thinking that maybe real-world-based physics laws could not be applied in the scene.
I did, however, come across another documentation page about the linear gravity field that correctly reported that gravity is measured in m/s^2 in SpriteKit.
https://developer.apple.com/documentation/spritekit/skfieldnode/1520145-lineargravityfield
I then setup a simple free falling scene where I applied an initial velocity to a physics body and then calculated the expected trajectory, while comparing it to the actual trajectory.
The x-axis calculations were accurate from the start, suggesting that the only problem was with the gravity's value.
I then tried manually modified the gravity in the scene until the actual trajectory matched the predicted one.
What I found is that there is a "magic" value of ~151 that has to be factored in when using the physics world's gravity property in the game.
Modifying, for example, the y-axis calculations for the trajectory from
let dy = velocity.dy * time + 0.5 * gravity * pow(time, 2)
to
let dy = velocity.dy * time + 0.5 * 151 * gravity * pow(time, 2)
resulted in accurate calculations.
I hope this is useful to anyone who might encounter the same problem in the future.

Related

how to get the force being applied on an object in SpriteKit (Swift)

So I am kinda new to SpriteKit and I am wondering how to get the amount of force on an object and put it into a variable. When you put force on an object, for example, this code,
line.physicsBody?.applyImpulse(CGVector(dx: 0, dy: -30))
it asks you how much in each direction. Well, I want to do the opposite. I want to get the amount of force that the object has and what direction.
An impulse a force that lasts for a single instant. The object can only applies a force on another physics body when it collides relative to its velocity and mass.
If your looking to find your objects current avg speed, you use
sprite.physicsbody?.velocity
or you can check if the object is moving with
physicsbody?.isResting
if you want to calculate the average force being applied on an object between frames you would have to calculate the magnitude of the change in position between the last update cycle and then multiply by the objects mass like so.
var lastPosition: CGPoint?
var avgForce: CGPoint?
func updatedForce(for sprite: SKSprite, timeDelta: Double) {
guard let lastPosition = lastPosition, let currentVelocity = currentVelocity) else {
lastPosition = sprite.position
}
let position = sprite.position
let mass = sprite.physicsbody.mass
var changeInX = position.x - lastPosition.x
var changeInY = position.y - lastPosition.y
avgForce = CGPoint(x: changeInX * mass, y: changeInY * mass)
}`
I apologize for any formatting issues or incorrect spellings, made this from my iPhone and will correct later.

ARKit: Placing an SCNText at a particular point in front of the camera

I've managed to get a cube (SCNNode) placed on a surface where the camera is pointed, however I am finding it very difficult to do the simple (?) task of also placing text in the same position.
I've created the SCNText and subsequent SCNNode, however when I add it to the rootNode the text always seems to be added above my head and off the camera to the right (which tells me thats the global origin point).
Even when I use the exact same values of position I used for the the cube, the SCNText node still gets placed above my head in the same spot.
Apologies if this is a basic question, I've never worked in SceneKit before.
The coordinate center for an SCNGeometry is its center point. But when you are creating a SCNText the center point is somewhere in the bottom left corner:
You need to center the text first. This can be done by checking the bounding box of the node containing your text and setting a pivot transform to change the texts center to its actual center:
func center(node: SCNNode) {
let (min, max) = node.boundingBox
let dx = min.x + 0.5 * (max.x - min.x)
let dy = min.y + 0.5 * (max.y - min.y)
let dz = min.z + 0.5 * (max.z - min.z)
node.pivot = SCNMatrix4MakeTranslation(dx, dy, dz)
}
Edit:
Also note this answer that explains some additional pitfalls:
A text with 16 pts font size is 16 SceneKit units tall. But in ARKit 1 SceneKit units = 1 meter!

Player controlled node ignoring collisions

I have a SKShapeNode controlled by the player. I want to keep this node inside a parent node. So I created edges and I use SKPhysicsBody and collision bit mask to prevent my node from moving outside its parent.
When I try to move the node by updating its position each frame, it just ignores the edges. Here is the function used :
func move(direction: MoveDirection, withTimeInterval timeInterval: TimeInterval) {
var x, y: CGFloat
switch direction {
case .North:
x = 0
y = movementSpeed * CGFloat(timeInterval)
case .South:
x = 0
y = -movementSpeed * CGFloat(timeInterval)
case .East:
x = movementSpeed * CGFloat(timeInterval)
y = 0
case .West:
x = -movementSpeed * CGFloat(timeInterval)
y = 0
}
let sprite = spriteComponent.sprite
sprite.position = CGPoint(x: sprite.position.x + x, y: sprite.position.y + y)
}
The moving works great but the node can go everywhere and just doesn't care about the edges (I turned skView.showPhysics on so I can see them).
But, if I replace the line :
sprite.position = CGPoint(x: sprite.position.x + x, y: sprite.position.y + y)
by :
sprite.physicsBody?.applyForce(CGVector(dx: x, dy: y))
collisions work just fine.
It feels like we have to move objects using physics if we want them to collide. But I didn't see anything about this restriction in Apple's doc. So is this behavior expected? Or did I miss something?
Bonus point :
In the TaskBot game provided by Apple, the player's position is (or seems) changed by setting node.position (the code is a bit...complicated, so not really sure). If someone can give me a hint?
Thank you!
If you move an SKSpriteNode manually, there will be no collisions because you are overriding the physics engine by saying "Put this node there no matter what".
If you want the physics engine to reliably generate collisions, then you'll need to use only the physics engine to move your objects via forces or impulses.
If you manually move a node into a position where it generates a collision, the the physics engine will attempt to move it away, but if you carry on trying to move the node, results will be unpredictable.

How to get a boss character to fire at all directions at once

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
}

How to make a physics impulse move the same percentage of the screen on all devices

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.