suyama
suyama

Reputation: 262

Sprite kit: Stuttering during movement with constant velocity

I’m developing a simple flight game with sprite kit. Once the space ship reaches the max height and speed, it flies on with constant velocity. I noticed that the space ship stutters randomly during the constant flight. I’ve read here all the posts about this issue but nothing really helped to fix it 100%.

For testing, I wrote a very simple game which contains only a space ship and one simple cloud (the code is below). But the space ship still stutters even in this very simple game. The LOG shows that the space ship movement is randomly not constant even when the space ship flies with constant velocity. And this is the reason for the stuttering.

Hopefully someone can help me to fix this issue. Thank you for any Ideas.


Sprite kit, Objective c, Xcode 8.0, Test devices: iPhone 6 - iOS 8.3, iPhone 4s - iOS 9.3.5

CPU: max 21%, Memory: max 8 MB, FPS: permanent 60 FPS


Here my code (for simplicity I put all the code in the scene class)


FlightScene.h

#import <SpriteKit/SpriteKit.h>

@interface FlightScene : SKScene <SKPhysicsContactDelegate>

@end

FlightScene.m

#import "FlightScene.h"

#define  HERO_FLIGHT_LOG    1
//#define  HERO_DEBUG_OVERLAY 1

static const CGFloat kMaxHeroVelocityY = 100.0f;
static const CGFloat kMaxHeroVelocityX = 200.0f;

@implementation FlightScene
{
    SKNode *_world;
    SKSpriteNode *_hero;
    SKSpriteNode *_cloud;

    CGPoint _heroStartPosition;
    CGSize _cloudSize;
    CGFloat _xAdj;

    BOOL _hasBegun;

    // debug
    CGFloat _oldHeroX;
    CGFloat _oldHeroY;
    int _frame;

}

- (void)didMoveToView:(SKView *)view
{
    // Setup your scene here

    [super didMoveToView:view];

    _hasBegun = NO;        
    _cloudSize = CGSizeMake(120, 80);
    _xAdj = _cloudSize.width;
    _heroStartPosition = CGPointMake(60, self.size.height/2);

    [self addWorld];
    [self addHero];
    [self addCloud];

    // debug
    _frame = 0;
    _oldHeroX = 0;
    _oldHeroY = 0;
}

#pragma mark - hero

- (void)addHero
{
    _hero = [SKSpriteNode spriteNodeWithImageNamed:@"Spaceship2"];

    _hero.size = CGSizeMake(80.0f, 70.0f);
    _hero.position = _heroStartPosition;
    _hero.zPosition = 1;

    _hero.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:self.size.width/2.0f];
    _hero.physicsBody.affectedByGravity = NO;
    _hero.physicsBody.dynamic = YES;
    _hero.physicsBody.allowsRotation = NO;
    _hero.physicsBody.mass = 1.0f;
    _hero.physicsBody.linearDamping = 0.0f;
    _hero.physicsBody.friction = 0.0f;

    [_world addChild:_hero];
}

- (void)updateFlying
{
    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;
    if(_hero.position.y > maxHeight)
        _hero.position = CGPointMake(_hero.position.x, maxHeight);
}

- (void)updateFlight
{
    // move hero with constant velocity
    [self updateFlying];

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

#pragma mark - game world

- (void)addWorld
{
    _world = [SKNode new];
    [self addChild:_world];
}

- (void)addCloud
{
    _cloud = [SKSpriteNode spriteNodeWithColor:[SKColor lightGrayColor] size:_cloudSize];

    _cloud.anchorPoint = CGPointMake(0, 1); // top left
    _cloud.position = CGPointMake(self.size.width + _cloudSize.width, self.size.height + _cloudSize.height/2);
    _cloud.zPosition = -1;

    [_world addChild:_cloud];
}

#pragma mark - update world

- (void)updateCloud
{
    // reposition the cloud
    if(_world.position.x + _xAdj < -(_cloudSize.width + self.size.width))
    {
        _xAdj += _cloudSize.width + self.size.width;

        CGFloat y = arc4random_uniform(_cloudSize.height - 10);
        _cloud.position = CGPointMake(_xAdj + self.size.width, self.size.height + y);
    }
}

- (void)updateWorld
{
    // move the world
    CGFloat worldX = -(_hero.position.x - _heroStartPosition.x);
    _world.position = CGPointMake(worldX, _world.position.y);

    [self updateCloud];
    [self flightLog];
}

-(void)update:(CFTimeInterval)currentTime
{
    // Called before each frame is rendered

    if(!_hasBegun)
        return;

    _frame++;

    // update hero movement
    [self updateFlight];
}

- (void)didFinishUpdate
{
    if(!_hasBegun)
        return;

    // update world movement
    [self updateWorld];        
}

#pragma mark - touches

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

    _hasBegun = YES;
    [self updateFlight];
}


#pragma mark - debug

- (void)flightLog
{

#ifdef HERO_FLIGHT_LOG

    CGFloat newHeroX = _hero.position.x - _heroStartPosition.x;;
    CGFloat diffX = newHeroX - _oldHeroX;

    CGFloat newHeroY = _hero.position.y;
    CGFloat diffY = newHeroY - _oldHeroY;

    NSLog(@"oldHeroY:%f, newHeroY:%f, diffY:%f", _oldHeroY, newHeroY, diffY);
    NSLog(@"oldHeroX:%f, newHeroX:%f, diffX:%f\n\n", _oldHeroX, newHeroX, diffX);

    if(diffX > 3.5f)
    {
        //NSLog(@"\t -> frame:%d fast oldHeroY:%f, newHeroY:%f, diffY:%f", _frame, _oldHeroY, newHeroY, diffY);
        NSLog(@"\t -> frame:%d fast oldHeroX:%f, newHeroX:%f, diffX:%f\n\n", _frame, _oldHeroX, newHeroX, diffX);
    }
    else if(diffX < 3.0f)
    {
        //NSLog(@"\t -> frame:%d fast oldHeroY:%f, newHeroY:%f, diffY:%f", _frame, _oldHeroY, newHeroY, diffY);
        NSLog(@"\t -> frame:%d slow oldHeroX:%f, newHeroX:%f, diffX:%f\n\n", _frame, _oldHeroX, newHeroX, diffX);
    }

    _oldHeroX = newHeroX;
    _oldHeroY = newHeroY;

#endif

}

@end

LOG:


.
.
.
// no stuttering .. FPS: 60

2016-10-02 17:27:19.164 TestFlight[11009:1774440] oldHeroY:301.666534, newHeroY:301.666534, diffY:0.000000
2016-10-02 17:27:19.165 TestFlight[11009:1774440] oldHeroX:263.002899, newHeroX:266.335968, diffX:3.333069

2016-10-02 17:27:19.181 TestFlight[11009:1774440] oldHeroY:301.666534, newHeroY:301.666534, diffY:0.000000
2016-10-02 17:27:19.182 TestFlight[11009:1774440] oldHeroX:266.335968, newHeroX:269.669067, diffX:3.333099

// stuttering .. FPS: 60

2016-10-02 17:27:24.584 TestFlight[11009:1774440] oldHeroY:301.666687, newHeroY:302.500031, diffY:0.833344
2016-10-02 17:27:24.585 TestFlight[11009:1774440] oldHeroX:1346.335083, newHeroX:1351.335083, diffX:5.000000

2016-10-02 17:27:24.585 TestFlight[11009:1774440]    -> frame:413 fast oldHeroX:1346.335083, newHeroX:1351.335083, diffX:5.000000

2016-10-02 17:27:24.600 TestFlight[11009:1774440] oldHeroY:302.500031, newHeroY:300.833344, diffY:-1.666687
2016-10-02 17:27:24.601 TestFlight[11009:1774440] oldHeroX:1351.335083, newHeroX:1353.001709, diffX:1.666626

2016-10-02 17:27:24.601 TestFlight[11009:1774440]    -> frame:414 slow oldHeroX:1351.335083, newHeroX:1353.001709, diffX:1.666626

2016-10-02 17:27:24.617 TestFlight[11009:1774440] oldHeroY:300.833344, newHeroY:301.666687, diffY:0.833344
2016-10-02 17:27:24.618 TestFlight[11009:1774440] oldHeroX:1353.001709, newHeroX:1356.335083, diffX:3.333374

2016-10-02 17:27:24.634 TestFlight[11009:1774440] oldHeroY:301.666687, newHeroY:301.666687, diffY:0.000000
2016-10-02 17:27:24.634 TestFlight[11009:1774440] oldHeroX:1356.335083, newHeroX:1359.668457, diffX:3.333374

2016-10-02 17:27:24.650 TestFlight[11009:1774440] oldHeroY:301.666687, newHeroY:301.666687, diffY:0.000000
2016-10-02 17:27:24.651 TestFlight[11009:1774440] oldHeroX:1359.668457, newHeroX:1363.001831, diffX:3.333374

2016-10-02 17:27:24.667 TestFlight[11009:1774440] oldHeroY:301.666687, newHeroY:302.500031, diffY:0.833344
2016-10-02 17:27:24.668 TestFlight[11009:1774440] oldHeroX:1363.001831, newHeroX:1368.001831, diffX:5.000000

2016-10-02 17:27:24.668 TestFlight[11009:1774440]    -> frame:418 fast oldHeroX:1363.001831, newHeroX:1368.001831, diffX:5.000000

2016-10-02 17:27:24.684 TestFlight[11009:1774440] oldHeroY:302.500031, newHeroY:300.833344, diffY:-1.666687
2016-10-02 17:27:24.684 TestFlight[11009:1774440] oldHeroX:1368.001831, newHeroX:1369.668457, diffX:1.666626

2016-10-02 17:27:24.685 TestFlight[11009:1774440]    -> frame:419 slow oldHeroX:1368.001831, newHeroX:1369.668457, diffX:1.666626

2016-10-02 17:27:24.700 TestFlight[11009:1774440] oldHeroY:300.833344, newHeroY:301.666687, diffY:0.833344
2016-10-02 17:27:24.701 TestFlight[11009:1774440] oldHeroX:1369.668457, newHeroX:1373.001831, diffX:3.333374

2016-10-02 17:27:24.717 TestFlight[11009:1774440] oldHeroY:301.666687, newHeroY:302.500031, diffY:0.833344
2016-10-02 17:27:24.718 TestFlight[11009:1774440] oldHeroX:1373.001831, newHeroX:1378.001831, diffX:5.000000

2016-10-02 17:27:24.718 TestFlight[11009:1774440]    -> frame:421 fast oldHeroX:1373.001831, newHeroX:1378.001831, diffX:5.000000

2016-10-02 17:27:24.734 TestFlight[11009:1774440] oldHeroY:302.500031, newHeroY:301.666687, diffY:-0.833344
2016-10-02 17:27:24.734 TestFlight[11009:1774440] oldHeroX:1378.001831, newHeroX:1381.335205, diffX:3.333374

// no stuttering  .. FPS: 60

2016-10-02 17:27:24.750 TestFlight[11009:1774440] oldHeroY:301.666687, newHeroY:301.666687, diffY:0.000000
2016-10-02 17:27:24.751 TestFlight[11009:1774440] oldHeroX:1381.335205, newHeroX:1384.668579, diffX:3.333374

2016-10-02 17:27:24.767 TestFlight[11009:1774440] oldHeroY:301.666687, newHeroY:301.666687, diffY:0.000000
2016-10-02 17:27:24.768 TestFlight[11009:1774440] oldHeroX:1384.668579, newHeroX:1388.001953, diffX:3.333374

2016-10-02 17:27:24.784 TestFlight[11009:1774440] oldHeroY:301.666687, newHeroY:301.666687, diffY:0.000000
2016-10-02 17:27:24.784 TestFlight[11009:1774440] oldHeroX:1388.001953, newHeroX:1391.335327, diffX:3.333374

2016-10-02 17:27:24.801 TestFlight[11009:1774440] oldHeroY:301.666687, newHeroY:301.666687, diffY:0.000000
2016-10-02 17:27:24.801 TestFlight[11009:1774440] oldHeroX:1391.335327, newHeroX:1394.668701, diffX:3.333374

2016-10-02 17:27:24.817 TestFlight[11009:1774440] oldHeroY:301.666687, newHeroY:301.666687, diffY:0.000000
2016-10-02 17:27:24.818 TestFlight[11009:1774440] oldHeroX:1394.668701, newHeroX:1398.002075, diffX:3.333374

2016-10-02 17:27:24.834 TestFlight[11009:1774440] oldHeroY:301.666687, newHeroY:301.666687, diffY:0.000000
2016-10-02 17:27:24.834 TestFlight[11009:1774440] oldHeroX:1398.002075, newHeroX:1401.335449, diffX:3.333374

2016-10-02 17:27:24.850 TestFlight[11009:1774440] oldHeroY:301.666687, newHeroY:301.666687, diffY:0.000000
2016-10-02 17:27:24.851 TestFlight[11009:1774440] oldHeroX:1401.335449, newHeroX:1404.668823, diffX:3.333374

2016-10-02 17:27:24.867 TestFlight[11009:1774440] oldHeroY:301.666687, newHeroY:301.666687, diffY:0.000000
2016-10-02 17:27:24.868 TestFlight[11009:1774440] oldHeroX:1404.668823, newHeroX:1408.002197, diffX:3.333374

// stuttering  .. FPS: 60

2016-10-02 17:27:24.883 TestFlight[11009:1774440] oldHeroY:301.666687, newHeroY:300.833344, diffY:-0.833344
2016-10-02 17:27:24.884 TestFlight[11009:1774440] oldHeroX:1408.002197, newHeroX:1409.668823, diffX:1.666626

2016-10-02 17:27:24.885 TestFlight[11009:1774440]    -> frame:431 slow oldHeroX:1408.002197, newHeroX:1409.668823, diffX:1.666626

2016-10-02 17:27:24.901 TestFlight[11009:1774440] oldHeroY:300.833344, newHeroY:302.500031, diffY:1.666687
2016-10-02 17:27:24.902 TestFlight[11009:1774440] oldHeroX:1409.668823, newHeroX:1414.668823, diffX:5.000000

2016-10-02 17:27:24.902 TestFlight[11009:1774440]    -> frame:432 fast oldHeroX:1409.668823, newHeroX:1414.668823, diffX:5.000000

2016-10-02 17:27:24.917 TestFlight[11009:1774440] oldHeroY:302.500031, newHeroY:300.833344, diffY:-1.666687
2016-10-02 17:27:24.918 TestFlight[11009:1774440] oldHeroX:1414.668823, newHeroX:1416.335449, diffX:1.666626

2016-10-02 17:27:24.918 TestFlight[11009:1774440]    -> frame:433 slow oldHeroX:1414.668823, newHeroX:1416.335449, diffX:1.666626

2016-10-02 17:27:24.934 TestFlight[11009:1774440] oldHeroY:300.833344, newHeroY:302.500031, diffY:1.666687
2016-10-02 17:27:24.935 TestFlight[11009:1774440] oldHeroX:1416.335449, newHeroX:1421.335449, diffX:5.000000

2016-10-02 17:27:24.935 TestFlight[11009:1774440]    -> frame:434 fast oldHeroX:1416.335449, newHeroX:1421.335449, diffX:5.000000

2016-10-02 17:27:24.950 TestFlight[11009:1774440] oldHeroY:302.500031, newHeroY:301.666687, diffY:-0.833344
2016-10-02 17:27:24.951 TestFlight[11009:1774440] oldHeroX:1421.335449, newHeroX:1424.668823, diffX:3.333374

// no stuttering for a while (17 seconds .. long time) .. FPS: 60

2016-10-02 17:27:24.967 TestFlight[11009:1774440] oldHeroY:301.666687, newHeroY:301.666687, diffY:0.000000
2016-10-02 17:27:24.968 TestFlight[11009:1774440] oldHeroX:1424.668823, newHeroX:1428.002197, diffX:3.333374
.
.
.

2016-10-02 17:27:41.559 TestFlight[11009:1774440] oldHeroY:301.666687, newHeroY:301.666687, diffY:0.000000
2016-10-02 17:27:41.559 TestFlight[11009:1774440] oldHeroX:4742.992188, newHeroX:4746.325684, diffX:3.333496

// stuttering .. FPS: 60 

2016-10-02 17:27:41.575 TestFlight[11009:1774440] oldHeroY:301.666687, newHeroY:300.833344, diffY:-0.833344
2016-10-02 17:27:41.576 TestFlight[11009:1774440] oldHeroX:4746.325684, newHeroX:4747.992188, diffX:1.666504

2016-10-02 17:27:41.576 TestFlight[11009:1774440]    -> frame:1432 slow oldHeroX:4746.325684, newHeroX:4747.992188, diffX:1.666504

2016-10-02 17:27:41.592 TestFlight[11009:1774440] oldHeroY:300.833344, newHeroY:302.500031, diffY:1.666687
2016-10-02 17:27:41.593 TestFlight[11009:1774440] oldHeroX:4747.992188, newHeroX:4752.992188, diffX:5.000000

2016-10-02 17:27:41.593 TestFlight[11009:1774440]    -> frame:1433 fast oldHeroX:4747.992188, newHeroX:4752.992188, diffX:5.000000

2016-10-02 17:27:41.609 TestFlight[11009:1774440] oldHeroY:302.500031, newHeroY:301.666687, diffY:-0.833344
2016-10-02 17:27:41.609 TestFlight[11009:1774440] oldHeroX:4752.992188, newHeroX:4756.325684, diffX:3.333496

2016-10-02 17:27:41.625 TestFlight[11009:1774440] oldHeroY:301.666687, newHeroY:301.666687, diffY:0.000000
2016-10-02 17:27:41.626 TestFlight[11009:1774440] oldHeroX:4756.325684, newHeroX:4759.659180, diffX:3.333496

2016-10-02 17:27:41.642 TestFlight[11009:1774440] oldHeroY:301.666687, newHeroY:301.666687, diffY:0.000000
2016-10-02 17:27:41.643 TestFlight[11009:1774440] oldHeroX:4759.659180, newHeroX:4762.992676, diffX:3.333496
.
.
.
// and so on ..

EDIT:


I would be grateful for any other Ideas.

Upvotes: 3

Views: 1108

Answers (3)

Confused
Confused

Reputation: 6288

This is because SpriteKit's default physics and motion/transform handling is not corrected to/by delta time. Instead it stutters whenever the frame rate drops due to system calls or other interruptions. These things happen a LOT on iOS devices. The iOS system is constantly checking networks and monitoring other states of all sorts of background activities, and responding to information coming to it.

There are two ways to deal with it.

  1. Turn on Airplane mode, close all background updating apps and severely restrict just about all apps from doing background processes in their settings; this reduces OS noise and overhead.

  2. Roll your own adjustments and compensation mechanisms using delta time so things appear to continue moving at their correct rate to anticipated destinations, even when a frame or two are skipped.


If you're not limited to SpriteKit, or not needing its physics, you can include another physics engine and use that. This will ultimately mean you could use SceneKit, Core Animation or even Metal to do your rendering.... or SpriteKit

The two most sensible options are Box2D and Chipmunk2D.

Here's a tutorial on adding Box2D to iOS: https://www.raywenderlich.com/2061-liquidfun-tutorial-with-metal-and-swift-part-1

Here's a Swift wrapper for Chipmunk2D: https://github.com/jakubknejzlik/ChipmunkSwiftWrapper

Upvotes: 5

crashoverride777
crashoverride777

Reputation: 10674

I am fighting the same issues with my game too. It seems SpriteKit doesn't particularly like constant movement.

First thing that you can try is to use

applyForce

instead of directly manipulating the velocity property of your player.

I seem to be getting the best result when I move the player in the regular

override func update(_ currentTime: TimeInterval) { ... }

method.

I update the camera in

override func didFinishUpdate() { ... }

In Apples sample game DemoBots they use constraints for the camera. Constraints are getting updated after DidSimulatePhysics so didFinishUpdate seems to be the best spot when you do it manually.

Also remember that you will gain some performance by doing the following things

1) Disconnecting your phone from your mac when your test. Just having your phone running via xCode while plugged in reduces performance.

2) When you test your app it is in Debug mode and not in Release mode which will reduce performance.

To change this manually when testing go to your scheme (the button next to the button you press to run your app). Click edit scheme and in Info change build configuration to Release.

Remember that you should change this back when you finished that particular test. You want to have the debug stuff running when you test your app.

3) Make sure your draw calls are very low. You do that by making sure that

ignoresSiblingOrder 

is set to true in your GameViewController, which is should by default.

Than give all your sprites different zPositions depending on the layer they are on.

e.g

background = 1
sun = 2
clouds = 3
traps, obstacles, objects = 4
player = 5

4) Make as many physics bodies as possible Circles as they use the least amount of performance.

5) Have as little code as possible in the update methods.

6) Cache textures, particularly when using texture animations.

7) Pool certain nodes if you are instantiating them a lot.

Upvotes: 1

suyama
suyama

Reputation: 262

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];
}

Upvotes: 1

Related Questions