Sunday, May 13, 2012

Background loading a Cocos2d Sprite from URL

Based on Steffen Itterheim's excellent article here, I created a CCSprite class to download itself in the background. You create such a sprite giving it a default image:

 LGNetworkSprite *myImage = [LGNetworkSprite spriteWithFile:@"fbDefault.png"];
 myImage.position = ccp(_screenSize.width * 0.75, _screenSize.height * 0.6);
 [self addChild:myImage];

Then, you can set the URL of the image to download, and it will do the magic in the background:
Note: the local file name should be unique if you download different images.

 [myImage loadFromURLString:imgUrl withLocalFileName:myID];


The code itself is quite simple (once you learn Steffen's teachings...):
//
//  LGNetworkSprite.h
//  CatchTheBall
//
//  Created by Israel Roth on 5/12/12.
//  Based on Steffen Iterheim code:
//  http://www.learn-cocos2d.com/2012/02/cocos2d-webcam-viewer-part-2-asynchronous-texture-loading/
//  Copyright 2012 Labgoo LTD. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "cocos2d.h"

@interface LGNetworkSprite : CCSprite {
}

- (void) loadFromServerAddress: (NSString*) serverAddr fileName: (NSString *) fileName;
- (void) loadFromURLString: (NSString *) urlString withLocalFileName: (NSString *) fileName;

@end


And the m file:
//
//  LGNetworkSprite.m
//  CatchTheBall
//
//  Created by Israel Roth on 5/12/12.
//  Based on Steffen Iterheim code:
//  http://www.learn-cocos2d.com/2012/02/cocos2d-webcam-viewer-part-2-asynchronous-texture-loading/
//  Copyright 2012 Labgoo LTD. All rights reserved.
//

#import "LGNetworkSprite.h"
#import "AsyncFileDownloadData.h"

@implementation LGNetworkSprite

-(NSString*) cacheDirectory
{
 NSString* cacheDirectory = nil;
 NSArray* pathArray = NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
                                                                 NSUserDomainMask, YES);
 if ([pathArray count] > 0)
 {
  cacheDirectory = [pathArray objectAtIndex:0];
 }
 return cacheDirectory;
}

- (void) loadFromURLString: (NSString *) urlString withLocalFileName: (NSString *) fileName {
 NSString* localFile = [[self cacheDirectory] stringByAppendingPathComponent:fileName];
 AsyncFileDownloadData* afd = [[[AsyncFileDownloadData alloc] init] autorelease];
 afd.url = [NSURL URLWithString:urlString];
 afd.localFile = localFile;
 [self performSelectorInBackground:@selector(downloadFileFromServerInBackground:) withObject:afd];
}

-(void) logError:(NSError*)error {
 if (error) {
  CCLOG(@"%@: %@", error, [error localizedDescription]);
 }
}

-(void) downloadFileFromServerInBackground:(AsyncFileDownloadData*)afd
{
 NSError* error = nil;
 NSData* data = [NSData dataWithContentsOfURL:afd.url options:NSDataReadingMappedIfSafe error:&error];
 [self logError:error];
 
 [data writeToFile:afd.localFile options:NSDataWritingAtomic error:&error];
 [self logError:error];
 
 // wait until done in this case means that the background thread waits for completion of the task
 // manipulating or creating sprites must be done on the main thread
 [self performSelectorOnMainThread:@selector(updateTexturesWithAsyncData:) withObject:afd waitUntilDone:NO];
}

-(void) updateTexturesWithAsyncData:(AsyncFileDownloadData*)afd
{
 [self updateTexturesFromFile:afd.localFile];
}

-(void) updateTexturesFromFile:(NSString*)file
{
 CCTextureCache* texCache = [CCTextureCache sharedTextureCache];
 [texCache addImageAsync:file 
      target:self
       selector:@selector(asyncTextureLoadDidFinish:)];
}

-(void) asyncTextureLoadDidFinish:(CCTexture2D*)texture
{
 CCTextureCache* texCache = [CCTextureCache sharedTextureCache];
 [texCache removeTexture:self.texture];
 CGSize prevSize = [self contentSize];
 self.texture = texture;
 CGSize size = [texture contentSize];
 [self setTextureRect:CGRectMake(0.0f, 0.0f, size.width,size.height)];
 [self setScale:prevSize.width / size.width];
}

@end

Note: Since I am updating the sprite itself, I do not need to keep the sprite tag. Also, I am using the Library/Caches directory to keep the local file. I think it is the right thing to do (in face, Apple will reject your app if you save to the Documents directory files that should not be there unless you mark them specifically).
Thank you Steffen, and to all of you doing cocos2d - go get yourself some help by purchasing Steffen's book here:

3 comments:

  1. Hi I noticed,

    loadFromServerAddress
    and #import "AsyncFileDownloadData.h" is missing...

    But thanks!

    ReplyDelete
  2. there are a few mistakes in the code, but they are obvious after pasting them...
    anyway, what i a good thing to add (which I did) is to create the image to load based on the device - this way you can use a template image on the device (4 sizes for 4 devices) and it will load the correct image to replace it. otherwise you might get unexpected behavior in image sizes (like stretching)

    if( UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
    {
    if( CC_CONTENT_SCALE_FACTOR() == 2 )
    // ipadhd
    urlString = [NSString stringWithFormat:@"%@ipadhd",urlString];
    else
    // ipad
    urlString = [NSString stringWithFormat:@"%@ipad",urlString];
    }
    else
    {
    if( CC_CONTENT_SCALE_FACTOR() == 2 )
    // iphone hd
    urlString = [NSString stringWithFormat:@"%@hd",urlString];
    else
    // iphone
    urlString = [NSString stringWithFormat:@"%@",urlString];
    }

    ReplyDelete
  3. Thanks for your comments. The import of AsyncFileDownloadData is from Stefan's original post mentioned in the text above.

    ReplyDelete