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]];
Related
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.
In my game I initially create sprites using this code:
- (void)addSpritesForCookies:(NSSet *)cookies {
for (BBQCookie *cookie in cookies) {
NSString *directory = [NSString stringWithFormat:#"sprites/%#.png", [cookie spriteName]];
CCSprite *sprite = [CCSprite spriteWithImageNamed:directory];
sprite.position = [self pointForColumn:cookie.column row:cookie.row];
[self.cookiesLayer addChild:sprite];
cookie.sprite = sprite;
}
}
This works perfectly fine, and the sprites display properly. All of the textures that I need are in a smart sprite sheet that I created in spritebuilder. Here is a screenshot of the structure in Spritebuilder, since I don't have enough reputation to post images here: https://www.evernote.com/shard/s30/sh/e1ea553f-a26c-4ecd-866b-551b50f14bd7/cc338b5ea3e557777a9a3acdeb0cd5ac
Then later on I need to change the texture on some of the sprites. I'm using the following code:
CCActionCallBlock *changeSprite = [CCActionCallBlock actionWithBlock:^{
NSString *directory = [NSString stringWithFormat:#"sprites/%#.png", [combo.cookieB spriteName]];
CCTexture *texture = [CCTexture textureWithFile:directory];
combo.cookieB.sprite.texture = texture;
}];
However, I just get these messages logged:
2015-01-17 12:22:09.294 BbqBlitz[65732:4875151] -[CCFileUtils fullPathForFilename:contentScale:] : cocos2d: Warning: File not found: sprites/Cupcake.png
2015-01-17 12:22:09.294 BbqBlitz[65732:4875151] cocos2d: Couldn't find file:sprites/Cupcake.png
What's weird is that I'm using exactly the same file path to initially create the sprites, which works fine. But now that I'm trying to create a texture its not working, and the sprites are just turning into black squares in my game.
I've already read this similar question: SpriteBuilder image file not found but from what I can tell the directory I'm using is correct.
What am I missing here? Thanks so much for any help!
Try to load it without folder name
NSString *directory = [NSString stringWithFormat:#"%#.png", [combo.cookieB spriteName]];
It could be a part of sprite sheet after spriteBuilder, I suppose.
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.
I'm using spritekit on Xcode 5. I created a folder called images with ".atlas" extension and these images inside: bear1.png, bear2.png and bear3.png
During the simulation this SKTextureAtlas is founded by “[SKTextureAtlas atlasNamed:#"images”]” but when i try to recovery some image using “[ursoAtlas textureNamed:#"bear1"]" it doesn't find the image. I printed the variable temp and it returns "MissingResource.png" and numImages in the code is 0. i expected that numImages was 3.
SKTextureAtlas *ursoAtlas = [SKTextureAtlas atlasNamed:#"images"];
SKTexture *temp = [ursoAtlas textureNamed:#"bear1"];
NSLog(#"%#", temp);
int numImages = ursoAtlas.textureNames.count;
NSLog(#"%d", numImages);
Everything look fine.
Did you drag images.atlas into the project?
or you can try to create new test_image.atlas and drag into project.
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?