Showing posts with label cocos2d. Show all posts
Showing posts with label cocos2d. Show all posts

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:

Monday, June 13, 2011

Improved CCSlider included with Cocos2d iPhone Extensions

I was happy to get an email from Stepan Generalov asking for permission to publish an improved version he made of CCSlider under an MIT license, which I approved immediately. Good work Stepan!
You can find the improved CCSlider and other cool stuff here:
http://www.cocos2d-iphone.org/archives/1506

Wednesday, January 5, 2011

Creating a Slider Control in cocos2d

I wanted to add a little slider control to allow the user to set the background music level in a game I am working on. So I created this little CCSliderControl class that I think is cute and useful.

The CCSliderControl is sub-classed from CCLayer. Here is the .h file for it:

 @protocol CCSliderControlDelegate  
 - (void) valueChanged: (float) value tag: (int) tag;  
 @end  
 @interface CCSliderControl : CCLayer {  
      float value;  
      id<CCSliderControlDelegate> delegate;  
      float minX;  
      float maxX;  
 }  
 @property (nonatomic, assign) float value;  
 @property (nonatomic, retain) id<CCSliderControlDelegate> delegate;  
 @end  

In the init of this class instance, I add two sprites - one for the background, and one for the slider thumb:

 -(id) init  
 {  
      if ((self = [super init]))  
      {  
           CCLOG(@"init %@", self);  
           self.isTouchEnabled = YES;  
           value = 0;  
           // add the slider background  
           CCSprite *bg = [CCSprite spriteWithFile:@"slider_background.png"]; //- TODO: add this to some texture atlas  
           [self setContentSize:[bg contentSize]];  
           bg.position = CGPointMake([bg contentSize].width / 2, [bg contentSize].height / 2);  
           [self addChild:bg];  
           // add the slider thumb  
           CGSize thumb_size;  
           CCSprite *thumb = [CCSprite spriteWithFile:@"slider_thumb.png"]; //- TODO: add this to some texture atlas  
           thumb_size = [thumb contentSize];  
           minX = thumb_size.width / 2;  
           maxX = [self contentSize].width - thumb_size.width / 2;  
           thumb.position = CGPointMake(minX, [self contentSize].height / 2);  
           [self addChild:thumb];  
      }  
      return self;  
 }  

The setValue method that sets the value property of the instance also updates the thumb sprite position:

- (void) setValue:(float) newValue
{
    if (newValue < 0) newValue = 0;
    if (newValue > 1.0) newValue = 1.0;
    value = newValue;
    CCSprite *thumb = (CCSprite *)[[self children] objectAtIndex:1];
    CGPoint pos = thumb.position;
    pos.x = minX + newValue * (maxX - minX);
    thumb.position = pos;
    
}

Next step is to register for touch events:

-(void) registerWithTouchDispatcher
{
    [[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:-1 swallowsTouches:YES];
}


and then handle the touch events:

-(CGPoint) locationFromTouch:(UITouch *)touch
{
    CGPoint touchLocation = [touch locationInView: [touch view]];
    touchLocation = [[CCDirector sharedDirector] convertToGL:touchLocation];
    CGRect bbox = [self boundingBox];
    touchLocation.x -= bbox.origin.x;
    touchLocation.y -= bbox.origin.y;
    return touchLocation;
}

-(bool) isTouchForMe:(CGPoint)touchLocation
{
    CCSprite *bg = (CCSprite *)[[self children] objectAtIndex:0];
    return CGRectContainsPoint([bg boundingBox], touchLocation);
}

-(BOOL) ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event
{
    CGPoint location = [self locationFromTouch:touch];
    bool isTouchHandled = [self isTouchForMe:location];
    if (isTouchHandled) {
        CCSprite *thumb = (CCSprite *)[[self children] objectAtIndex:1];
        thumb.color = ccYELLOW;
        CGPoint pos = thumb.position;
        pos.x = location.x;
        thumb.position = pos;
    }
    return isTouchHandled; // YES for events I handle
}

-(void) ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event
{
    CGPoint location = [self locationFromTouch:touch];
    if ((location.x < minX) || (location.x > maxX))
        return;

    CCSprite *thumb = (CCSprite *)[[self children] objectAtIndex:1];
    CGPoint pos = thumb.position;
    pos.x = location.x;
    thumb.position = pos;
}

-(void) ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event
{
    CCSprite *thumb = (CCSprite *)[[self children] objectAtIndex:1];
    thumb.color = ccWHITE;
    value = (thumb.position.x - minX) / (maxX - minX);
    [delegate valueChanged:value tag:self.tag];
}


And the result looks like this:



You can get the source code from bitbucket by using mercurial:


$ hg clone https://iroth_net@bitbucket.org/iroth_net/ccslider

Saturday, December 25, 2010

Cocos2D support new and old devices textures

Got a new iPhone game project going - an opportunity to learn some new things which is always fun.
I am using the cocos2d framework, which really saves me a lot of work.
I still would like to get deeper into OpenGL ES some time later, but for this project it will be enough to have cocos2d handle the low level details.

One issue I struggled with is that I need to create fairly detailed animation of the scene background.

Due to the limit on OpenGL ES texture size (especially on older devices where the texture is limited to 1024x1024 pixels), I am faced with two options:

  • Limit the number of frames (In a 1024x1024 texture I can get 9 frames of size 320x340 which is the size of my background area in the game).
  • Construct the background area from multiple animations and compose them programmatically. While possible, this makes the design work more cumbersome as the designer needs to supply the needed graphics and their location.
Using a larger texture size of 2048x2048 gives me enough room for 36 frames, which is sufficient for quite a nice background animation loop, but should I give up all older devices (prior to iPhone 3Gs)?

There are millions of those!

So this leads me to the current solution I am going to use, which is:

Use a 2048x2048 texture for newer devices, and substitute that for a smaller 1024x1024 texture on older devices in run time.

But how should I decide which texture to use?

Well, I will save you all my google searches, as the answer was found right under my nose:

Cocos2d CCConfiguration class!

Here's the code:

    NSString *plistFile;
    int numFrames;
    if ([[CCConfiguration sharedConfiguration] maxTextureSize] < 2048)
    {
        plistFile = @"highQualityAnimation.plist"];
        numFrames = 36;
    }
    else
    {
        plistFile = @"highQualityAnimation.plist"];
        numFrames = 9;
    }
   
    // cache the frames to the sharedSpriteFrameCache
   
    CCSpriteFrameCache * cache = [CCSpriteFrameCache sharedSpriteFrameCache];
    [cache addSpriteFramesWithFile:plistFile];

    // create the animation
    CCAnimation * animation = [[CCAnimation alloc] initWithName:@"animation" delay:0.1];    // set the frame rate as needed
    for ( int i=1; i <= numFrames; ++i )
    {
        NSString * fname = [NSString stringWithFormat:@"animation_frame_%i.png", i];
        [animation addFrame:[cache spriteFrameByName: fname]];
    }

    CCSprite *backgroundSprite = [CCSprite spriteWithSpriteFrameName:[NSString stringWithFormat:@"animation_frame_1.png"]];
    id action = [CCRepeatForever actionWithAction:[CCAnimate actionWithAnimation:animation]];
    [backgroundSprite runAction:action];

    // add the sprite to the scene layer

    [self addChild:backgroundSprite];

That's easy enough, gives good animation on newer devices, and still reasonably support for older devices.