8

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];
Fredrik Johansson
  • 1,301
  • 1
  • 13
  • 26
  • Correct me if I'm wrong: you want a 'snapshot' of the end scene (maybe part of the scene), and reuse it as the texture of a button. So you will consider taking a real snapshot rather than saving the scene texture. – WangYudong Oct 02 '15 at 04:30
  • The thumbnail image you want to store is the fullscreen contents or a part of the screen? – WangYudong Oct 02 '15 at 09:52
  • Sorry I've been unable to give feedback. The problem is that the match is not always played on screen. As an extreme, the match can be played by two AI players, totally in the CPU. The match is not seen on screen, but I want to take a snapshot anyways. And in a more common case I want to make a special variant of the scene, with for instance bigger fonts, in snapshot mode. This is just a answer to the questions above, I will also comment on your answer. – Fredrik Johansson Oct 02 '15 at 19:23
  • Tested on XCode 11.7 iPhone 8 Simulator (iOS 13.7). texture CGImage is an empty transparent image. The texture itself contains correct image though. Not sure if it is just simulator bug or others. – GeneCode Nov 02 '20 at 12:40

2 Answers2

8

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:

  1. 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.

  2. 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).

  3. 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.

  1. 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.

  • 1
    If you ever write a blog, a book, or anything, I'll contribute cold cash. – Confused Nov 06 '15 at 08:53
  • Russel, I hope to be able to help you back some day. Many thanks... My current plan is to wait for the white 9.2. Maybe it just works then. Lets hope so. I will try to update the post. – Fredrik Johansson Nov 06 '15 at 19:34
  • Russell: I've updated the question with a workaround inspired by your code. Thanks a lot! As you can see I did not have to use UIImage. It works on iOS 9.1/XCode7/Metal. – Fredrik Johansson Nov 07 '15 at 08:58
2

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.

Community
  • 1
  • 1
WangYudong
  • 4,335
  • 4
  • 32
  • 54
  • Thanks. I will reward you Yudong, but I need a real workaround. My real SKScene is much more complicated. I just made an basic example. My main objective is to produce a small storable image of a scene for later use in a sprite. I can see that NSLog returns NULL, but it's also a fact that generateTexture returns some kind of valid texture at line 4 in didMoveToView. Isn't that strange? My general question: how can I quickly store a scaled/cropped snapshot of a non-active scene for later use as a texture in a sprite? – Fredrik Johansson Oct 01 '15 at 17:24
  • @FredrikJohansson Code of line 4 is showing a valid sprite node with null texture and I don't know where does (300 x 300) come from. Would you show some more _complicated_ example of what you are doing? I will take a close look at your problem tomorrow. – WangYudong Oct 01 '15 at 17:34
  • I've updated the question at the end. I mean, my scene is quite complicated with a HUD layer, CTRL layer, world layer etc, and each layer has lots of sprites and stuff. But I guess that only one sprite in the scene is needed to show the problem. I can't even store a texture of a scene with only one sprite! ;) Thanks for the help BTW. – Fredrik Johansson Oct 01 '15 at 19:57
  • Of cause I will reward you, Yudong. Thanks a lot! But I guess that your solution requires the scene to be displayed on screen. Is that correct? And I guess that your feeling is also that the handling of the texture is incorrect? I've reported the behavior to Apple. Let's see if it's corrected. Again, thanks a lot. – Fredrik Johansson Oct 02 '15 at 19:27
  • @FredrikJohansson Yes, my solution needs all the contents to be already on screen. I will keep up with your issue if you drop me a message about the feedback from Apple. BTW, I'm looking forward to your game. Good luck! – WangYudong Oct 03 '15 at 01:18