Constant Movement in Spritekit

Sprite kit: Stuttering during movement with constant velocity

I'll answer my own question.

Flight phases:

  • Take-off
  • Once the space ship reaches the max height and speed, it flies on
    with constant velocity

Problem:

  • The space ship stutters randomly during the constant flight

Solution in this case:

  • Since the stuttering occurs during the constant flight, define a variable (_maxReached) to control the end of the take-off phase.

  • Once the space ship reaches the max height and speed, set _maxReached = YES.

  • Update the constant flight by updating the x-position (updatePosition).

Updated code:

.
.
// new:
static const CGFloat kMaxSpeed = 5.0f;

@implementation FlightScene
{
.
.
.
// new:
BOOL _maxReached;
}

// new: rename
//- (void)updateFlying
- (void)takeOff
{
if(!_hasBegun)
return;

CGVector oldVel = _hero.physicsBody.velocity;
CGVector newVel = oldVel;

// increase the velocity
newVel.dx += (kMaxHeroVelocityX - newVel.dx) / 10.0f;
newVel.dy += (kMaxHeroVelocityY - newVel.dy) / 10.0f;

// ensure velocity doesn't exceed maximum
newVel.dx = newVel.dx > kMaxHeroVelocityX ? kMaxHeroVelocityX : newVel.dx;
newVel.dy = newVel.dy > kMaxHeroVelocityY ? kMaxHeroVelocityY : newVel.dy;

_hero.physicsBody.velocity = newVel;
}

- (void)limitHeight
{
const CGFloat maxHeight = self.size.height * 0.8f;

// new
if(_hero.position.y >= maxHeight)
{
if(_hero.physicsBody.velocity.dy == kMaxHeroVelocityY)
_maxReached = YES;

if(_hero.position.y > maxHeight)
_hero.position = CGPointMake(_hero.position.x, maxHeight);
}
}

// new: move the hero with constant velocity
- (void)updatePosition
{
CGFloat newX = _hero.position.x + kMaxSpeed;
_hero.position = CGPointMake(newX, _hero.position.y);
}

- (void)updateFlight
{
if(_maxReached) // new
{
[self updatePosition]; // move the hero with constant velocity
}
else
{
[self takeOff];

// ensure height doesn't exceed maximum
[self limitHeight];
}
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
if(_hasBegun)
return;

_hasBegun = YES;
_maxReached = NO; // new
[self updateFlight];
}

Constant speed orbit around point with SKNode

The slowing down behavior is often to be expected when dealing with physics engines. They are not perfect, especially in more complex scenarios like constraints. You should not rely on Sprite Kit to provide perfect continuos motion simulation because we have no clue what the physic's engine is doing under-the-hood; there are so many factors involved.

In cases where you need continuous motion, (especially something like constant centripetal motion) it is always best to calculate and preserve these values yourself. There is simply no escaping writing calculations in your update method for behaviors like this.

So I wrote a quick example project that calculates the necessary velocity needed to orbit a particular point. You simply specify a period, orbit position and orbit radius. Note that because I am calculating everything, there is no need for any SKJoints, so that makes this implementation also more lightweight. I highly recommend you do your orbiting this way, as it gives you total control over how you want your nodes to orbit each other (i.e. you could have nodes orbit moving nodes, you could have oval orbiting paths, you could adjust the orbit during key moments in your game, etc.)

import SpriteKit

class GameScene: SKScene {
var node1: SKShapeNode!
var node2: SKShapeNode!
var node2AngularDistance: CGFloat = 0

override func didMoveToView(view: SKView) {
physicsWorld.gravity = CGVector(dx: 0, dy: 0)
node1 = SKShapeNode(circleOfRadius: 10)
node1.position = CGPoint(x: self.size.width/2.0, y: self.size.height/2.0)
node1.physicsBody = SKPhysicsBody(circleOfRadius: 10)
node2 = SKShapeNode(circleOfRadius: 10)
node2.position = CGPoint(x: self.size.width/2.0+50, y: self.size.height/2.0)
node2.physicsBody = SKPhysicsBody(circleOfRadius: 10)
self.addChild(node1)
self.addChild(node2)
}
override func update(currentTime: NSTimeInterval) {
let dt: CGFloat = 1.0/60.0 //Delta Time
let period: CGFloat = 3 //Number of seconds it takes to complete 1 orbit.
let orbitPosition = node1.position //Point to orbit.
let orbitRadius = CGPoint(x: 50, y: 50) //Radius of orbit.

let normal = CGVector(dx:orbitPosition.x + CGFloat(cos(self.node2AngularDistance))*orbitRadius.x ,dy:orbitPosition.y + CGFloat(sin(self.node2AngularDistance))*orbitRadius.y);
self.node2AngularDistance += (CGFloat(M_PI)*2.0)/period*dt;
if (fabs(self.node2AngularDistance)>CGFloat(M_PI)*2)
{
self.node2AngularDistance = 0
}
node2.physicsBody!.velocity = CGVector(dx:(normal.dx-node2.position.x)/dt ,dy:(normal.dy-node2.position.y)/dt);
}
override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
node1.position = (touches.first! as! UITouch).locationInNode(self)
}
}

Below is a gif showing the centripetal motion. Notice that because it is completely dynamic we can actually move the centripetal point as it is orbiting.

enter image description here


If however you really want to use your current SKJoints implementation for some reason then I have another solution for you. Simply keep updating your node's linear velocity so that it never slows down.

override func update(currentTime: NSTimeInterval) {
let magnitude = sqrt(secondNode.physicsBody!.velocity.dx*secondNode.physicsBody!.velocity.dx+secondNode.physicsBody!.velocity.dy*secondNode.physicsBody!.velocity.dy)
let angle = Float(atan2(secondNode.physicsBody!.velocity.dy, secondNode.physicsBody!.velocity.dx))
if magnitude < 500 {
secondNode.physicsBody!.velocity = CGVector(dx: CGFloat(cosf(angle)*500), dy: CGFloat(sinf(angle)*500))
}
}


Regardless of the solution you pick (and once again I highly recommend the first solution!), you can choose to apply the velocity over time rather than instantaneously for a more realistic effect. I talk more about this in my answer here

Hopefully I have provided you with enough information to resolve your issue. Let me know if you have any questions. And best of luck with your game!

Sprite Kit camera smooth movement with delay

An easier way to keep your play in bounds is to set a physics body to the player, and a physics body for the bounds.

var player: SKSpriteNode! //player variable
var map: SKSpriteNode! //this simple node called map is a variable that represents the bounds area, for the example i made it the size of the scene (inside GameScene.sks)

override func didMove(to view: SKView) {
player = childNode(withName: "player") as! SKSpriteNode // initializing the player from the scene file.
player.physicsBody = SKPhysicsBody(rectangleOf: player.size) // initializing player physics body(in this example the player is a simple rectangle).

map = childNode(withName: "map") as! SKSpriteNode // initializing the map from the scene file.
map.physicsBody = SKPhysicsBody(edgeLoopFrom: map.frame) // instead of assigning a physics body to the scene it self, we created the map variable and assign the physics body to it, the edgeLoopFrom physics body is a static volume-less body, so the player will not be able to pass through.

setupCamera() // for the second part of your question we create this method and call it right here in viewDidLoad().
}

instead of updating the camera position constantly in the update() method, you simply need to add the camera constraints. (I added the camera in the scene file as well)

func setupCamera() {
guard let camera = camera, let view = view else { return } // make sure we have a camera and a view or else return

let zeroDistance = SKRange(constantValue: 0)
let playerConstraint = SKConstraint.distance(zeroDistance,
to: player) // as the name suggest this is a simple constraint for the player node.
//next part of the method will assign a second constraint to the camera which will prevent the camera from showing the dark area of the scene in case the player will go to the edge. you don't have to add this part but it is recommended.
let xInset = min(view.bounds.width/2 * camera.xScale,
map.frame.width/2)
let yInset = min(view.bounds.height/2 * camera.yScale,
map.frame.height/2)

let constraintRect = map.frame.insetBy(dx: xInset,
dy: yInset)

let xRange = SKRange(lowerLimit: constraintRect.minX,
upperLimit: constraintRect.maxX)
let yRange = SKRange(lowerLimit: constraintRect.minY,
upperLimit: constraintRect.maxY)

let edgeConstraint = SKConstraint.positionX(xRange, y: yRange)
edgeConstraint.referenceNode = map

camera.constraints = [playerConstraint, edgeConstraint] //finally we add the constraints we created to the camera, notice that the edge constraint goes last because it has a higher priority.
}


Related Topics



Leave a reply



Submit