Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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];
like image 357
Fredrik Johansson Avatar asked Sep 21 '15 17:09

Fredrik Johansson


1 Answers

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.

like image 174
Russell Okamoto Avatar answered Sep 22 '22 00:09

Russell Okamoto