I am developing a game for iOS with Obj-C and Apple's SpriteKit framework. My primary testing device is an iPad2 (model #MC773X/A running iOS8.1). My game runs smoothly on this device except for a single "stutter" around 60-120s in. At this time the app lags for around 150-300ms / 10-20 frames.
I have tested the same game app on an iPhone6+ (model #MGAJ2X/A running iOS8.4) which doesn't seem to have this problem.
I originally thought it had something to do with my game apps resource management, and then later maybe the debugging link itself... so I created the following testing app. It is simply a blank screen, that logs dt's above 20ms. It has an option to log to the screen (using SKLabelNodes) for testing without a debugging link.
This simple testing app shows the same stutter on the iPad2 without fail at around the 60s mark (built in debug/release, with/without debugging). The iPhone 6+ doesn't show the stuttering behaviour.
Starting with a new SpriteKit game template for iOS (I am using Xcode 6.4 but I saw the same bug with an earlier version - Xcode 6.something):
In GameViewController.m:
//scene.scaleMode = SKSceneScaleModeAspectFill;
scene.scaleMode = SKSceneScaleModeResizeFill;
Replace GameScene.m with:
#import "GameScene.h"
#define LOG_TO_SCREEN
static int dtLogCounter = 1;
#ifdef LOG_TO_SCREEN
static const int dtLogLabelNum = 10; //5;
#endif
#interface GameScene ()
#property (nonatomic) CFTimeInterval prevTime;
#property (nonatomic) CFTimeInterval startTime;
#ifdef LOG_TO_SCREEN
#property (strong, nonatomic) SKLabelNode *timeLabel;
#property (strong, nonatomic) NSArray *dtLogLabels;
#endif
#end
#implementation GameScene
-(void)didMoveToView:(SKView *)view
{
#ifdef LOG_TO_SCREEN
CGFloat screenWidth = view.bounds.size.width;
_timeLabel = [SKLabelNode labelNodeWithFontNamed:#"Courier"];
_timeLabel.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeRight;
_timeLabel.fontSize = 16.0;
_timeLabel.position = CGPointMake(screenWidth - 7.0, 28.0);
[self addChild:_timeLabel];
NSMutableArray *dtLogLabels = [NSMutableArray arrayWithCapacity:dtLogLabelNum];
for (int i = 0; i < dtLogLabelNum; i++)
{
SKLabelNode *dtLogLabel = [SKLabelNode labelNodeWithFontNamed:#"Courier"];
dtLogLabel.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeRight;
dtLogLabel.fontSize = 16.0;
dtLogLabel.position = CGPointMake(screenWidth - 7.0, 100.0 + 25*i);
[self addChild:dtLogLabel];
[dtLogLabels addObject:dtLogLabel];
}
_dtLogLabels = [NSArray arrayWithArray:dtLogLabels];
#endif
_prevTime = _startTime = CACurrentMediaTime();
}
-(void)update:(CFTimeInterval)currentTime
{
CFTimeInterval dt = currentTime - _prevTime;
_prevTime = currentTime;
CFTimeInterval gameTime = currentTime - _startTime;
#ifdef LOG_TO_SCREEN
_timeLabel.text = [NSString stringWithFormat:#"t:%.1f(c)", gameTime];
#endif
if (dt > 20e-3)
{
NSString *logStr = [NSString stringWithFormat:#"#%d:%.1f:%.0f ", dtLogCounter++, gameTime, dt*1000];
#ifdef LOG_TO_SCREEN
for (int i = dtLogLabelNum-1; i > 0; i--)
{
SKLabelNode *dtLogLabel = [_dtLogLabels objectAtIndex:i];
SKLabelNode *prevLabel = [_dtLogLabels objectAtIndex:i-1];
dtLogLabel.text = prevLabel.text;
}
SKLabelNode *firstDtLogLabel = [_dtLogLabels objectAtIndex:0];
firstDtLogLabel.text = logStr;
#else
NSLog(#"%#", logStr);
#endif
}
}
#end
I am getting comfortable with using the time profiler instrument but in this case I don't think it is particularly helpful. It shows no activity for the duration of the stutter.
(image1)
(Unfortunately I'm a brand new poster so can't inline images)
It seems like it is a system process happening at this time which is causing the app to stutter. The activity monitor instrument shows a peak at this time but I don't really know how to interpret the data / nothing obvious stands out.
(image2)
The system usage instrument shows no activity in the I/O activity track.
The file activity instrument doesn't seem to be available.
My question is: how should I approach debugging this issue? Which instrument could I use, and how?
Confirmation that others see this issue on this/other hardware would be appreciated as would suggestions to it's cause/resolution.
Thanks
If you haven't already, I suggest you read Apple's Analyzing CPU Usage in Your App. If that does not yield any positive results for you, peppering your code with NSLog statements can be another way to try to pinpoint the code section involved with this issue.
Do keep in mind that you are probably not seeing the stutter in the iPhone 6+ due to its faster CPU. The iPad 2 is somewhat dated and runs much slower. This itself is a small clue to your issue.
Related
Okay, this is an odd one - and the question has been asked before in various guises and always closed out with the opinion that a) it won't be accurate and b) that it will get less accurate with time.
I understand this - but I'm doing an experiment to see whether that accuracy can be boosted at all by taking into account other evidential sources (for example, if a map has been plotted in advance then the direction of travel from the compass combined with the route could provide another evidence source).
The problem is that my code is clearly rubbish - so I'd welcome your opinion. I suspect that this might be a brown paper bag error!
My ViewController.h looks like this:
#import <UIKit/UIKit.h>
#interface ViewController : UIViewController <UIAccelerometerDelegate> {
UIAccelerometer *accelerometer;
long last_speed;
long distance_travelled;
long lastAccel;
long long lastTime;
IBOutlet UITextField* speedView;
IBOutlet UITextField* distanceView;
}
#end
And my ViewController.m looks like this:
#import "ViewController.h"
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
lastAccel = 0;
last_speed = 0;
distance_travelled = 0;
lastTime = (long)(NSTimeInterval)([NSDate.date timeIntervalSince1970] * 1000.0);
accelerometer = UIAccelerometer.sharedAccelerometer;
accelerometer.updateInterval = 0.1;
accelerometer.delegate = self;
}
- (void)accelerometer:(UIAccelerometer *)meter
didAccelerate:(UIAcceleration *)acceleration
{
long long currentTime = (long)(NSTimeInterval)([NSDate.date timeIntervalSince1970] * 1000.0);
long long deltaTime = currentTime - lastTime;
lastTime = currentTime;
long accel_x = acceleration.x;
long accel_y = acceleration.y;
long accel_z = acceleration.z;
long integrated_acceleration = sqrtl(accel_x*accel_x + accel_y*accel_y + accel_z*accel_z);
long average_acceleration = (lastAccel + integrated_acceleration) / 2;
long speed = last_speed + (average_acceleration * deltaTime);
long average_speed = (speed + last_speed) / 2;
distance_travelled = distance_travelled + (average_speed * deltaTime);
last_speed = speed;
lastAccel = integrated_acceleration;
[speedView setText:[NSString stringWithFormat:#"%ld", speed]];
[distanceView setText:[NSString stringWithFormat:#"%ld", distance_travelled]];
}
#end
When the code is run, the speed and the distance keep going up continually, and deceleration (me slowing down) is never taken into account - so even when I stop, speed and distance keep going up.
Even by the standards of 'this will be a bit inaccurate' that's taking it too far!
All thoughts and opinions gratefully received (well almost - I know that it won't be accurate!)
You can't calculate Speed using UIAccelerometer.
Please find the link for more clarification : How to calculate speed using accelerometer in ios
I have this game when a sprite touches the sides, you get a point. I figured out the code to increment the score each time the sprite touches the side. I get the correct output through the NSLog message.
But, When I try the same code by changing the code from NSLog to SKLabelNode, I get a more than a thousand nodes (which I think will affect the performance and slow the game).Also, when the score gets incremented, it overlaps the old score rather than just increasing the score. I have added the code in the update with frame.
Heres the code:
- (void)printScore {
NSString *text = [NSString stringWithFormat:#"%ld",(long)userScore];
SKLabelNode *score = [SKLabelNode labelNodeWithText:text];
score.fontName = #"chalkduster";
score.fontSize = 45;
score.fontColor = [SKColor whiteColor];
score.position = CGPointMake(CGRectGetMidX(self.frame) + 175 ,CGRectGetMidY(self.frame) + 350);
[self addChild:score];
}
-(void)update:(CFTimeInterval)currentTime {
[self printScore];
}
How do I fix this so that the score gets updated without so many sprites being added?
Sorry if this is a really dumb question, I am noob.
Add just a single property:
#property (nonatomic, strong) SKLabelNode *scoreLabel;
And create it just one time in viewDidLoad o an init method:
_scoreLabel = [SKLabelNode initWithFontNamed:#"chalkduster"];
_scoreLabel.fontSize = 45;
_scoreLabel.fontColor = [SKColor whiteColor];
_scoreLabel.position = CGPointMake(CGRectGetMidX(self.frame) + 175 ,CGRectGetMidY(self.frame) + 350);
[self addChild:_scoreLabel];
Then call the printScore method only when the sprite touches de sides, not on each frame like you are doing in the update method:
- (void)printScore
{
NSString *text = [NSString stringWithFormat:#"%ld",(long)userScore];
self.scoreLabel.text = text;
}
I hope this helps.
The reason you get so many nodes is that you are constantly adding new labels to the scene. Notice that in your printScore method you are create new SKLabelNode instances and then adding that to the scene.
In order to correct that, you will want to maintain a reference to a single SKLabelNode and then update the text of that.
Also, you may want to move the call to printScore to only be called when the actual scoring event happens, as opposed to update which is called for every frame. I'm of course assuming you do not have a scoring event happen every frame.
Your printScore method creates a new SKLabelNode every time it is called and you are calling it 60 times a second via your update method. You should set a BOOL to notify you of the need to call your printScoreMethod only when there is a need to do so (as in the score has changed).
create a global boolean and only add the label the first time:
BOOL firsttime = YES;
- (void)printScore {
NSString *text = [NSString stringWithFormat:#"%ld",(long)userScore];
SKLabelNode *score = [SKLabelNode labelNodeWithText:text];
if( firsttime )
{
score.fontName = #"chalkduster";
score.fontSize = 45;
score.fontColor = [SKColor whiteColor];
score.position = CGPointMake(CGRectGetMidX(self.frame) + 175, CGRectGetMidY(self.frame) + 350);
[self addChild:score];
firsttime = NO;
}
}
-(void)update:(CFTimeInterval)currentTime {
[self printScore];
}
There are a few ways to approach this. I prefer an approach which hides as much of the underlying implementation. Ideally, I would actually create a class which represents the score HUD. From a "rest of the app code" perspective, the other game code's contract with the HUD is through a score property which reflects the score.
Some will argue it is one label and it seems like a hassle. But if you change how it is implemented (for example, say it becomes a score with a fill bar or perhaps you have special effects which accompany a change in score), you will find it makes it super easy to deal with. Or what if you suddenly have a 2 player game and both players track score? You can easily add a new score by adding a new instance of your the score class.
Here is a simple example of what I mean.
ScoreHUD.h
#interface ScoreHUD : NSObject
#property (nonatomic, assign) NSInteger score;
- (instancetype)initWithScene:(SKScene *)scene x:(CGFloat)x y:(CGFloat)y;
#end
ScoreHUD.m
#interface ScoreHUD()
#property (nonatomic, strong) SKLabelNode *scoreLabel;
#end
#implementation ScoreHUD
- (instancetype)initWithScene:(SKScene *)scene x:(CGFloat)x y:(CGFloat)y
{
self = [super init];
if (self) {
_scoreLabel = [SKLabelNode initWithFontNamed:#"chalkduster"];
_scoreLabel.fontSize = 45;
_scoreLabel.fontColor = [SKColor whiteColor];
_scoreLabel.position = CGPointMake(x, y);
[scene addChild:_scoreLabel];
// Setting initial label value to the default value of the score
_scoreLabel.text = [#(_score) stringValue];
}
return self;
}
- (void)setScore:(NSInteger)score
{
if (_score != score) {
_score = score;
self.scoreLabel.text = [#(score) stringValue];
}
}
#end
Note that I haven't tested the code or checked to see if it would compile.
So to use this, you would create an instance of this in your scene like:
// Property in the scene
#property (nonatomic, strong) ScoreHUD *score;
// Initialize where you need to
self.score = [[ScoreHUD alloc] initWithScene:self x:CGRectGetMidX(self.frame) + 175 y:CGRectGetMidY(self.frame) + 350];
You could do this in your scene's initWithSize.
When the score changes, you simply do something like:
self.score.score += 1;
You can always wrap that in another method if it looks nasty to you.
There is a drawback to this method. Say your score get's updated a lot during a frame. This means your text would be set multiple times per frame. Depending on the type of game you have, this may or may not be a concern. There are ways to still have this basic setup and work around it. But for the vast majority of games, it will not be an issue.
My project in cocos2dv3 is throwing ARC Sematic Issue
Multiple methods named 'setRotation:' found with mismatched result, parameter type or attributes
while archiving(release mode). It runs fine while deploying to simulator/device (debug mode).
In release mode compiler gets confused between the implementation of rotation in UIRotationGestureRecognizer and CCNode.
When I got the error in CCBAnimationManager.m , I typecasted the object calling the selector setRotation to (CCNode*) but then the error crept up in CCActionInterval. I'm hoping there is a better solution than typecasting everywhere in cocos2d library.
What am i doing wrong?
Thankyou for your time.
EDIT
#interface CCAction : NSObject <NSCopying> {
id __unsafe_unretained _originalTarget;
id __unsafe_unretained _target;
NSInteger _tag;
}
#property (nonatomic,readonly,unsafe_unretained) id target;
#property (nonatomic,readonly,unsafe_unretained) id originalTarget;
#property (nonatomic,readwrite,assign) NSInteger tag;
in
CCAction.m
#synthesize tag = _tag, target = _target, originalTarget = _originalTarget;
-(void) startWithTarget:(id)aTarget
{
_originalTarget = _target = aTarget;
}
-(void) startWithTarget:(id)aTarget
{
_originalTarget = _target = aTarget;
}
Class Hierarchy
#interface CCActionFiniteTime : CCAction <NSCopying>
#interface CCActionInterval: CCActionFiniteTime <NSCopying>
#interface CCBRotateTo : CCActionInterval <NSCopying>
CCBRotateTo.m {
-(void) startWithTarget:(CCNode *)aTarget
{
[super startWithTarget:aTarget];
startAngle_ = [self.target rotation];
diffAngle_ = dstAngle_ - startAngle_;
}
-(void) update: (CCTime) t
{
[self.target setRotation: startAngle_ + diffAngle_ * t];
}
}
This problem gave me a big headache. Though I've upgraded cocos2d to v2.2 version for my old project (too complex to update to v3), I still got this warning. And any animation I created use rotation in the SpriteBuilder does act oddly, as I described here:
Rotation animation issue on iPhone5S with cocos2d 2.0
Finally I used type casting to solve it as following in CCBAnimationManager.m
#implementation CCBRotateTo
-(void)startWithTarget:(CCNode *)aTarget
{
[super startWithTarget:aTarget];
starAngle_ = [(CCNode *)self.target rotation];
diffAngle_ = dstAngle_ - startAngle_;
}
-(void)update:(ccTime)t
{
[(CCNode *)self.target setRotation: startAngle_ + diffAngle_ * t];
}
With this change, now I can support arm64 too.
update your cocos2dv3 to latest (RC4 for now).
I was using Xcode 5.0 and cocos2dv3 RC1 with no problem.
But updating Xcode to 5.1 I had this problem.
So I updated the cocos2dv3 to RC4 and now it's working fine.
You can find cocos 2d latest versions from here.
Using apple spritekit, how can I uniquely identify nodes if, say 20 nodes, they have same art work and are to be placed randomly on the screen? Is there a faster way like in cocos2d there is a"tag" function to identify?
I'm not the most proficient with sprites, but is this something you're looking for?
Save:
NSArray * nodesArray; // some array of nodes.
for (int x = 0; x < nodesArray.count; x++) {
SKNode * node = nodesArray[x];
node.name = [NSString stringWithFormat:#"%i", x];
}
Retrieve:
int nodeToRetrieveTag = 2; // or whatever
SKNode* nodeToRetrieve = [self.scene childNodeWithName:[NSString stringWithFormat:#"%i", nodeToRetrieveTag]];
I'm usually using the property name:
SKNode *myNode = [[SKNode alloc]init];
myNode.name = #"uniqueName";
[self addChild:myNode];
In the SKScene, to recovery the node you can do:
[self childNodeWithName:#"uniqueName"]; // self is SKScene
If for some reason you don't want to use name, you can always subclass one SKNode and add your personal unique identifier:
MySpriteNode.h
#interface MySpriteNode : SKSpriteNode
#property (nonatomic, strong) NSString *personalIdentifier;
#end
and
MySpriteNode.m
#import "MySpriteNode.h"
#implementation MySpriteNode
#end
with this second option you can:
for (MySpriteNode *sprite in [self children]) personalIdentifier
{
if ([sprite.personalIdentifier isEqualToString:#"something"])
{
//do something
}
}
EDIT 1:
Try to follow these tutorials, I really think they are great. I learned a lot with them:
An iOS 7 Sprite Kit Game Tutorial
Sprite Kit Programming Guide
Sprite Kit Tutorial for Beginners
Are you looking for enumerateChildNodesWithName:usingBlock: method?
I've been writing a simple fahrenheit/celcius converter style app. It works fine in the simulator but when I test the app on my iPhone 4, it is very jerky and slow to update as I move the slider back and forth.
My main view controller looks like this (removed some junk):
#import "MLViewController.h"
#import "MLGradeModel.h"
#import "MLGrade.h"
#interface MLViewController ()
#property (nonatomic, strong) MLGradeModel *gradeModel;
#end
#implementation MLViewController
#synthesize displayLeft = _displayLeft;
#synthesize displayRight = _displayRight;
#synthesize gradeModel = _gradeModel;
#synthesize buttonLeft = _buttonLeft;
#synthesize buttonRight = _buttonRight;
- (IBAction)dragEnter:(UISlider*)sender {
[self sliderUpdate: sender];
}
- (IBAction)sliderInput:(UISlider*)sender {
[self sliderUpdate:sender];
}
- (IBAction) sliderValueChanged:(UISlider *)sender {
[self sliderUpdate:sender];
}
-(IBAction)sliderUpdate:(UISlider*)sender
{
UILabel *myDisplayLeft = self.displayLeft;
UILabel *myDisplayRight = self.displayRight;
float sliderValue = [sender value];
int pos = sliderValue*1000/CONVERSION_SCALE;
NSString *strLeft = [self.gradeModel readGradeFromLeftAtPos:pos];
NSString *strRight = [self.gradeModel readGradeFromRightAtPos:pos];
[myDisplayLeft setText:strLeft];
[myDisplayRight setText:strRight];
// try to redraw, maybe less jerky?
[self.view setNeedsDisplay];
}
-(int) getSliderValue
{
float initialValue = [self.sliderInput value];
return initialValue * 100;
}
- (void)viewDidLoad
{
[super viewDidLoad];
MLGradeModel *model = [MLGradeModel sharedMLGradeModel];
self.GradeModel = model;
}
#end
gradeModel is an NSMutableArray of NSMutableArrays, who contain NSString values. As the slider is dragged, the corresponding position in the arrays should be read. Whose values should then be set to the UILabels.
I thought it should be the most simple thing. Outlets and actions for the UISlider have been dragged in the storyboard.
Edit: Also, when I run the app on the phone, the logs show that the slider input is taken, but the window is not updated at speed "so to say". For example, then I move the slider form left to right the event shows up in the log but the labels are redrawn with 0.5 s delay.
Edit: [self.view setNeedsDisplay]; was added as a test to force a redraw. The program works equally bad/slow when that line is commented away.
Edit: It works equally bad when I change sliderUpdate to:
-(IBAction)sliderUpdate:(UISlider*)sender
{
float sliderValue = [sender value];
int pos = sliderValue*10;
NSString *strFromInt = [NSString stringWithFormat:#"%d",pos];
[self.displayLeft setText:strFromInt];
[self.displayRight setText:strFromInt];
}
-Have I not hooked up all events from the UISlider? I have only set valueChanged to to my viewController's sliderInput (which calls sliderUpdate).
I have this same delay issue with UISlider on an iPhone 4 with my app now i have started targeting iOS 7
On iOS 6 my sliders worked perfectly on an iPhone 4.
The same app works smoothly on a 4s and an ipad mini that i have here
EDIT
What I've just discovered is that in my app there was big difference in the UISlider performance on iPhone 4 under iOS 7 when I built for debug and built for release.
I had a bunch of logging going on in the slider - using DLog instead of NSLog - DLog is a macro that expands to an NSlog on running in debug mode and a no-op in release mode. so it appears that the logging was causing the lagging.
Check to see if you have logging going on in there and either comment them out or, if you are using Dlog, try changing the scheme to release and see if this solves your issue and see if that makes a difference,
To change to release look in Xcode menu Product-Scheme-Edit Scheme)
Made the world of difference to me.
Bit of a relief that !