Search code examples
iosobjective-csprite-kitgame-physics

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 added this to the addHero: method

    _hero.physicsBody.angularDamping = 0.0f;
    
  • I changed the touchesBegan: so that the space ship flies immediately with max velocity without take-off.

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        if(_hasBegun)
            return;
    
        _hasBegun = YES;
        _hero.physicsBody.velocity = CGVectorMake(kMaxHeroVelocityX, kMaxHeroVelocityY);
    }
    
    • I run the code without the updateFlying: method. The space ship flies immediately with max velocity without take-off. It flies on with constant velocity so that the update of velocity is not necessary.

    • I tried other things, like putting the updates in didSimulatePhysics: .. Turn on Airplane mode .. as @Confused suggested.. But nothing helped to fix this issue. It still not be 100% perfect as @crashoverride777 says.

I would be grateful for any other Ideas.


Solution

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