Sprite Kit - Animations & memory managment - ios

I'm currently messing around with Sprite Kit on iOS to figure out if it would be a fitting framework to make relatively simple 2D game in.
Due to my ActionScript background, i am very comfortable working with Sprite Kit code-wise
But there is something i just can't figure out. Animated nodes with Texture Atlas as a resource are incredibly memory heavy. I've imported an atlas into my project (size of textures is about 35MB). Preloading textures into RAM seems ok but at the moment i run the actual animation, the heap size increases exponentinaly (from about 80MB to 780MB)
Here goes my code:
self.noahFrames = [[NSMutableArray alloc] init];
SKTextureAtlas *noahAtlas = [SKTextureAtlas atlasNamed:#"noahAnimati"];
int imgCount = noahAtlas.textureNames.count;
for (int i=1; i <= imgCount; i++) {
NSString *textureName = [NSString stringWithFormat:#"NoahMainMenuAnimation_%d", i];
SKTexture *temp = [noahAtlas textureNamed:textureName];
[self.noahFrames addObject:temp];
}
SKSpriteNode *noahNode = [self createSpriteWithName:#"noah" imagePath:#"Noah_main_menu_hd" positionXPath:#"MainMenu.Noah.x" positionYPath:#"MainMenu.Noah.y" scalePath:#"MainMenu.Noah.scale"];
[self addChild:noahNode];
//up to this point everything goes fine
[noahNode runAction:[SKAction repeatActionForever:
[SKAction animateWithTextures:self.noahFrames
timePerFrame:0.1f
resize:YES
restore:YES]] withKey:#"animatedNoah"];
So i guess my actual question is why does the application become that insanely memory heavy after calling the SKAction animation ? I must be missing something rather obvious ...

I do know that when a texture is loaded in graphic memory it's loaded without any compression, but I don't think that xcode monitors graphic memory, so it's really strange to me.
I usually load and execute animations just like you do and I don't have such memory behaviour, but i noticed it when testing on the simulator. Are you using iOS simulator for your tests? Does your application crash when you reach those memory levels?

Related

Save generated SKTexture to file

I've now filed a bug for the issue below. Anyone with a good
workaround?
I try to save an SKTexture to file, and load it back again, but I don't succeed. The following code snippet can be copied to GameScene.m in the Xcode startup project.
I use textureFromNode in generateTexture, and that seems to be the root cause of my problem. If I use a texture from a sprite, the code works, and two spaceships are visible.
This code worked in iOS 8 but it stopped working in Xcode7 & iOS 9. I just want to verify that this is a bug before I file a bug report. My worry is that I do something wrong with NSKeyedArchiver.
It happens both in simulator and on device.
#import "GameScene.h"
#implementation GameScene
// Generates a texture
- (SKTexture *)generateTexture
{
SKScene *scene = [[SKScene alloc] initWithSize:CGSizeMake(100, 100)];
SKShapeNode *shapeNode = [SKShapeNode shapeNodeWithRectOfSize:CGSizeMake(50, 50)];
shapeNode.position = CGPointMake(50, 50);
shapeNode.strokeColor = SKColor.redColor;
shapeNode.lineWidth = 10;
[scene addChild:shapeNode];
SKTexture *texture = [self.view textureFromNode:scene];
//SKTexture *texture = [SKSpriteNode spriteNodeWithImageNamed:#"Spaceship"].texture; // This works!
return texture;
}
// Just generate a path
- (NSString *)fullDocumentsPath
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *yourFileName = [documentsDirectory stringByAppendingPathComponent:#"fileName"];
return yourFileName;
}
- (void)didMoveToView:(SKView *)view
{
self.scaleMode = SKSceneScaleModeResizeFill;
// Verify that the generateTexture method indeed produces a valid texture.
SKSpriteNode *s1 = [SKSpriteNode spriteNodeWithTexture:[self generateTexture]];
s1.position = CGPointMake(100, 100);
[self addChild:s1];
// Start with saving the texture.
NSString *fullName = [self fullDocumentsPath];
NSError *error;
NSFileManager *fileMgr = [NSFileManager defaultManager];
if ([fileMgr fileExistsAtPath:fullName])
{
[fileMgr removeItemAtPath:fullName error:&error];
assert(error == nil);
}
NSDictionary *dict1 = [NSDictionary dictionaryWithObject:[self generateTexture] forKey:#"object"];
bool ok = [NSKeyedArchiver archiveRootObject:dict1 toFile:fullName];
assert(ok);
// Read back the texture and place it in a sprite. This sprite is not shown. Why?
NSData *data = [NSData dataWithContentsOfFile:fullName];
NSDictionary *dict2 = [NSKeyedUnarchiver unarchiveObjectWithData:data];
SKTexture *loadedTexture = [dict2 objectForKey:#"object"];
SKSpriteNode *s2= [SKSpriteNode spriteNodeWithTexture:loadedTexture];
NSLog(#"t(%f, %f)", loadedTexture.size.width, loadedTexture.size.height); // Size of sprite & texture is zero. Why?
s2.position = CGPointMake(200, 100);
[self addChild:s2];
}
#end
Update for Yudong:
This might be a more relevant example, but imagine that the scene consists of 4 layers, with lots of sprites. When the game play is over I want to store a thumbnail image of the end scene of the match. The image will be used as a texture on a button. Pressing that button will start a replay movie of the match. There will be lots of buttons with images of old games so I need to store each image on file.
-(SKTexture*)generateTexture
{
SKScene *scene = [[SKScene alloc] initWithSize:CGSizeMake(100, 100)];
SKSpriteNode *ship = [SKSpriteNode spriteNodeWithImageNamed:#"Spaceship"];
ship.position = CGPointMake(50, 50);
[scene addChild:ship];
SKTexture *texture = [self.view textureFromNode:scene];
NSLog(#"texture: %#", texture);
return texture;
}
The solution/work around:
Inspired by Russells code I did the following. It works!
CGImageRef cgImg = texture.CGImage;
SKTexture *newText = [SKTexture textureWithCGImage:cgImg];
I've done a lot of experimenting/hacking with SKTextures. My game utilizes SKTextures. It is written in Swift. Specifically, I've had many problems with textureFromNode and textureFromNode:crop: and creating SKPhysicsBodies from textures. These methods worked fine in ios 8, but Apple completely broke them when they released ios 9.0. In ios 9.0, the textures were coming back as nil. Those nil textures broke SKPhysicsBodies from the textures.
I recently worked on serialization/deserialization of SKTextures.
Some key ideas/clues you might investigate are:
Run ios 9.2. Apple Staff mentioned a lot of issues have been fixed. https://forums.developer.apple.com/thread/17463 I've found ios 9.2 helps with SKTextures but didn't solve every issue especially the serialization issues.
Try PrefersOpenGL (set it to "YES" as a Boolean custom property in your config). Here is a post about PrefersOpenGL in the Apple Dev Forums by Apple Staff. https://forums.developer.apple.com/thread/19683 I've observed that ios 9.x seems to use Metal by default rather than OpenGL. I've found PrefersOpenGL helps with SKTexture issues but still doesn't make my SKShaders work (written in GLSL).
When I tried to serialize/deserialize nodes with SKTextures on ios 9.2, I got white boxes instead of visible textures. Inspired by Apple SKTexture docs that say, "The texture data is loaded when:
The size method on the texture object is called.
Another method is called that requires the texture’s size, such as creating a new SKSpriteNode object that uses the texture object.
One of the preload methods is called (See Preloading the Texture Data.)
The texture data is prepared for rendering when:
A sprite or particle that uses the texture is part of a node tree that is being rendered."
... I've hacked a workaround that creates a secondary texture from the CGImage() call:
// ios 9.2 workaround for white boxes on serialization
let img = texture!.CGImage()
let uimg = UIImage(CGImage: img)
let ntex = SKTexture(image: uimg)
let sprite = SKSpriteNode(texture: ntex, size: texture!.size())
So now my SKSpriteNodes created this way seem to serialize/deserialize fine. BTW, just invoking size() or creating an SKSpriteNode with the original texture does not seem to be enough to reify the texture into memory.
You didn't ask about textureFromNode:crop: but I'm adding observations anyway just in case it helps you: I've found this method in ios 8 worked (although the crop parameters were very tricky and seemed to require normalization with UIScreen.mainScreen().scale) In ios 9.0, this method didn't work at all (returned nil). In ios 9.2 this method now works (it now returns a non-nil texture) however subsequent creation of nodes from the texture do not need the size normalization. And furthermore, to make serialization/deserialization work, I found you ultimately have to do #3 above.
I hope this helps you. I imagine I've struggled more than most with SKTextures since my app is so dependent on them.
I tested your code in Xcode 7 and found texture returned in generateTexture was null. That's the reason why you can't load anything from the file, and you even haven't saved anything.
Try to use NSLog to log the description of your texture or sprite. E.g. add this line in generateTexture:
NSLog(#"texture: %#", texture);
What you will get in console:
texture: '(null)' (300 x 300)
And same for s1 and dict1 in your code:
s1: name:'(null)' texture:[ '(null)'
(300 x 300)] position:{100, 100} scale:{1.00, 1.00} size:{100, 100}
anchor:{0.5, 0.5} rotation:0.00
dict1: {
object = " '(null)' (300 x 300)"; }
You may do these tests on both iOS 8 and iOS 9 and you will probably get different results.
I'm not sure why you add the SKShapeNode to a scene and then save the texture from the scene. One workaround is to set texture for your SKShapeNode, and your code should work fine.
shapeNode.fillTexture = [SKTexture textureWithImageNamed:#"Spaceship"];
SKTexture *texture = shapeNode.fillTexture;
return texture;
Update:
It's quite annoying that textureFromNode doesn't works as expected in iOS 9. I tried to solve it by trial and error but no luck at last. Thus, I asked you if you would consider make a snapshot of the whole screen and set it as your thumbnail. Here's the progress I made today and hope you will get inspired from it.
I created a scene which contained SKLabelNode and SKSpriteNode in didMoveToView. After I clicked anywhere on screen, snapshot would be invoked and the down-scaled screenshot would be saved in the document folder. I used the code here.
- (UIImage *)snapshot
{
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 0.5);
[self.view drawViewHierarchyInRect:self.view.bounds afterScreenUpdates:YES];
UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return snapshotImage;
}
Since the thumbnail is saved as UIImage therefore loading it back for the sprite's texture should be done easily. A sample project demonstrates the whole process and it works on both iOS 8 and 9.

Can I create SpriteKit texture atlas in runtime

I'm developing an os x / iOS crossword game which uses SKLabelNode to display the crossword letters as a child on a SKNode subclass.
So for each letter there is SKNode.SKLabelNode. Without using SKLabelNode the draw counts are in the range of 6-8. With using SKLabelNode they go up to the amount of children in the scene which can be almost 100.
I am now looking for a way to avoid that and came up with the idea to rasterize the SKLabelNode to a texture but this does not lower the draw counts since there are still lots of different textures.
My Idea is now to rasterize these SKNode-Subclasses and to put the textures into a texture atlas.
So question is, is it possible to create a texture atlas in runtime? What to do if a single texture changes? Is it possible to exchange a single texture in the atlas or so I have to rebuild it?
And maybe there is a "best way" to handle lot's of different SKLabelNodes!?
I would go with a letter class which is subclass of SKSpriteNode and an atlas called letters. That way, you will draw all letters in single draw pass(100 draw passes are unnecessary in this situation). Or you don't even have to make a subclass of SKSpriteNode...You can just do this :
SKSpriteNode *letterSprite = [SKSpriteNode spriteNodeWithTexture:[atlas textureNamed:[NSString stringWithFormat:#"%#",character]]];
The limitation of this approach is that letters would have pre-determined size. I doubt that you would need different sized letters in your crossword game. If you need to change size of letters you can still scale them, but I guess some quality loss will theoretically occur because of scaling bitmaps. I say theoretically because in most cases the quality loss is not noticeable.
Here is an example of my TextNode which parses given string (numbers in my case) and create sprites which are drawn in single pass (instead of using SKLabelNode for every single number). Images in atlases should be named like a#2x.png, b#2x.png, or if using numbers 1#2x.png, 2#2x.png etc.
static const float kCharacterDistance = 6.0f;
#import "TextNode.h"
#interface TextNode()
#property (nonatomic,strong) NSMutableArray* characters;
#end
#implementation TextNode
-(instancetype)initWithPosition:(CGPoint)position andText:(NSString*)text{
if(self =[super init]){
self.characters = [[NSMutableArray alloc] initWithCapacity:[text length]];
SKTextureAtlas *atlas = [SKTextureAtlas atlasNamed:#"numbers"];
for(NSUInteger i =0; i < [text length];i++){
NSString *character = [text substringWithRange:NSMakeRange(i, 1)];
SKSpriteNode *characterSprite = [SKSpriteNode spriteNodeWithTexture:[atlas textureNamed:[NSString stringWithFormat:#"%#",character]]];
characterSprite.color = [SKColor yellowColor];
characterSprite.colorBlendFactor = 1.0f;
characterSprite.position = CGPointMake(i*kCharacterDistance,0);
[self.characters addObject:characterSprite];
[self addChild:characterSprite];
}
self.position = position;
}
return self;
}
#end
Hope this helps and will give you the basic idea of how to draw all letters in single draw pass. And note how I colorize letters. In texture atlas images are white, but I colorize them easily to desired color.
As long as your FPS is good, don't worry about anything else. Also, remember that Xcode auto creates texture atlases at runtime so it's not something you have to do yourself.
You can set skView.ignoresSiblingOrder = YES; to improve your performance a bit.

Memory Leak with ARC for fast enumeration

On a background thread checking for intersections I was leaking a large amount of memory. I tracked down where the leak was occurring and it was due to fast enumeration. I tried using an #autorelease but that did not fix it either. What ended up fixing the leak was just using normal iteration, and I have no idea why.
background thread created using dispatch_async, running on ipad mini 2 ios8
// shapes is a NSMutable array
for (RTShape *shape in shapes){
// ... intersection code
}
results in a memory leak, with this fixing it
for (int i = 0; i < shapes.count; i++){
RTShape *shape = [shapes objectAtIndex: i];
// ... same intersection code
}
Does anyone know why this causes a leak?

ios Loading texture from atlas not working

I have an atlas with a bunch of tiles and i am trying to load them into memory using SKTexture and SKTextureAtlas but it is not working. I use the following code to load them:
NSString *atlasName = [NSString stringWithFormat:#"Tiles"];
SKTextureAtlas *tileAtlas = [SKTextureAtlas atlasNamed:atlasName];
NSInteger numberOfTiles = tileAtlas.textureNames.count;
backgroundTiles = [[NSMutableArray alloc] initWithCapacity:numberOfTiles];
for (int y = 0; y < 5; y++) {
for (int x = 0; x < 9; x++) {
int tileNumber = y*9 + x + 1;
NSString *textureName = [NSString stringWithFormat:#"tile%d.png",tileNumber];
SKSpriteNode *tileNode = [SKSpriteNode spriteNodeWithTexture:[tileAtlas textureNamed:textureName]];
CGPoint position = CGPointMake((0.5 + x)*_tileSize - _levelWidth/2,(0.5 - y - 1)*_tileSize + _levelHeight/2);
tileNode.position = position;
tileNode.zPosition = -1.0f;
tileNode.blendMode = SKBlendModeReplace;
[(NSMutableArray *)backgroundTiles addObject:tileNode];
}
}
Then i use this code to add them to my scene:
- (void)addBackgroundTiles
{
for (SKNode *tileNode in [self backgroundTiles]) {
[self addChild: tileNode];
}
}
The problem is it doesnt load the correct texture for a tile or find the texture at all.
What I end up with is this (ignore the blue circle): http://i.stack.imgur.com/g39BF.png
Here is my tile atlas: http://snk.to/f-ctp5yhpz
EDIT: I am using NameChanger(www.mrrsoftware.com/MRRSoftware/NameChanger.html) to rename all my tiles, can it be that program that messes up my pngs? as far as i can see they are in the correct order after i have renamed them.
Solution
Editing my answer to point out that the solution is in the comments below this answer.
It turned out that the issue was caused by Xcode not rebuilding the atlas after the image files were renamed outside of Xcode (presumably by the file changed OP mentioned).
By cleaning and rebuilding the project, all the texture atlases were built again, and OPs code started working.
Original answer
Two things to double-check:
Is your .atlas added to your project as a folder or a group? It must be a folder (blue icon in Xcode, instead of yellow).
After adding Tiles.atlas to your project, you must also enable atlas generation in Xcode settings.
See here for a similar issue: How to create atlas for SpriteKit. I linked to Apple documentation on incorporating texture atlases into your projects which has a detailed step-by-step instruction on enabling atlas generation.
Why the double for loops?
Are you saving the backgroundTiles array as a property?
I've had this occur recently and the only fix that worked was:
[SKSpriteNode spriteNodeWithTexture:[SKTexture textureWithImageNamed:#"someTile.png"]]; The textureWithImageNamed always gets the right one.
So try:
SKSpriteNode *tileNode = [SKSpriteNode spriteNodeWithTexture:[SKTexture textureWithImageNamed:textureName]];

SpriteKit memory management preload cached and fps issue

My question is pretty simple, according to the apple docs you have the ability to preload textures into RAM prior to presenting a scene like so:
SKTextureAtlas * atlas = [SKTextureAtlas atlasNamed:#"effect_circle_explode"];
SKTextureAtlas * atlas2 = [SKTextureAtlas atlasNamed:#"box_explodes"];
SKTextureAtlas * atlas3 = [SKTextureAtlas atlasNamed:#"fence_new"];
SKTextureAtlas * atlas4 = [SKTextureAtlas atlasNamed:#"swipe"];
SKTextureAtlas * atlas5 = [SKTextureAtlas atlasNamed:#"coin"];
SKTextureAtlas * atlas6 = [SKTextureAtlas atlasNamed:#"two_times"];
SKTextureAtlas * atlas7 = [SKTextureAtlas atlasNamed:#"three_times"];
SKTextureAtlas * atlas8 = [SKTextureAtlas atlasNamed:#"gus"];
[SKTextureAtlas preloadTextureAtlases:#[atlas, atlas2, atlas3, atlas4, atlas5, atlas6, atlas7, atlas8] withCompletionHandler:^{
[moron_logo removeFromSuperview];
moron_logo = NULL;
stuff.hidden = NO;
store.hidden = NO;
scroll_view.userInteractionEnabled = YES;
[self present_game_view];
}];
Now would there be any negative effect if later on through out gameplay you also call a preload to the same atlas like so:
-(void)load
{
SKTextureAtlas * atlas = [SKTextureAtlas atlasNamed:#"effect_circle_explode"];
SKTextureAtlas * atlas2 = [SKTextureAtlas atlasNamed:#"coin"];
[SKTextureAtlas preloadTextureAtlases:#[atlas, atlas2] withCompletionHandler:^{
explode_textures = [[NSMutableArray alloc] init];
int numImages = (int)atlas.textureNames.count;
for (int i=0; i <= numImages/2-1; i++)
{
NSString *textureName = [NSString stringWithFormat:#"effect_circle_explode_%d.png", i];
SKTexture *temp = [atlas textureNamed:textureName];
[explode_textures addObject:temp];
}
explodeAnimation = [SKAction animateWithTextures:explode_textures timePerFrame:.05];
idle_textures = [[NSMutableArray alloc] init];
int numImages2 = (int)atlas.textureNames.count;
for (int i=0; i <= numImages2/2-1; i++)
{
NSString *textureName = [NSString stringWithFormat:#"coin_%d.png", i];
SKTexture *temp = [atlas2 textureNamed:textureName];
[idle_textures addObject:temp];
}
idleAnimation = [SKAction animateWithTextures:idle_textures timePerFrame:.05];
[self animate:0];
}];
}
Now if I do not preload the texture again the game will actually crash once in awhile not all the time if I just directly inserted the textures into the SKAction. The crash is an exec_bad_access on the Sprite::update(double) call, so my assumption is that somehow the textures from the first preload were removed from RAM and thats why I preload every single time I create a new node now. It seems to have fixed that error. This leads to another problem though when it comes to performance and hence the reason why I am asking this.
The game runs fine on the 5S and the 5 but as soon you touch an iPod touch 5th gen it barely can go over 15 FPS. I ran instruments and this is what is eating up all the CPU time:
Could this be related to my constant call of the preloadatlas call? Does anyone know why this would be eating my processor time up so badly on older devices? Thanks so much and hopefully someone else might be having a similar problem and this will help them out once I make my way to bottom of it.
Thanks in advance.
Preloading atlases every time you create a new sprite is generally a bad idea.
Your actual problem seems to be that you preload the atlases but you don't keep them around. Unless the atlas variables are global.
As soon as the method that does the preloading returns, the atlas objects are now longer referenced and will be removed from memory automatically. Sprite Kit internally implements a caching system so you won't notice it right away but eventually one or more of the atlases will be gone.
Keep a strong reference to each atlas in your scene so that the atlases remain in memory, and stop preloading at runtime. Whether this helps with fps I don't know.

Resources