Reputation: 262
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.
Upvotes: 3
Views: 1108
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.
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.
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
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
Reputation: 262
I'll answer my own question.
Flight phases:
Problem:
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