miércoles, 4 de septiembre de 2013

Resources framework for "universal" apps

When I started programming for iOS, the projects I were involved in were all for iPad devices only (only a pack of image resources), but when I started coding I did it in such a way that with the same code it was valid for iPad and iPhone. It was a great idea because later on the iPad apps were turned into universal apps and the changes in the code were just a few details. The idea is the following:
  1. Initialize the scale of the background with respect to the current screen size and the resources dimensions and save this ratio.
  2. Scale the background and images with the previously calculated ratio. As the iPhone has not the same dimensions of the iPad, it uses scaleX and scaleY, and the images get a bit stretched.
  3. Never use points for positions, but use logic of dynamic positions with respect to the screen: top-left corner of the screen is (size.width*0, size.height*1) and the center-right point is (size.width*1, size.height*0.5).
One of the problems I found in my way was the memory constraint, the iPad 1 couldn't load big files of images. I had to change the resources target from iPad retina to iPad non-retina screen. Enabling retina display but without the -hd resources makes the graphics on iPad retina not blurred nor with pixels, so it was a good solution and saved a lot of mega bytes.

Here you have the framework and an example of the usage:
//
// ResourcesManagerStretched.h
//
// Created by Elena Vielva on 31/04/12.
// Copyright (c) 2012 Elena Vielva. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "cocos2d.h"
@interface ResourcesManagerStretched : NSObject {
float scaleX;
float scaleY;
CGSize size;
CCSpriteFrameCache *frameCache;
}
@property (readonly) float scaleX;
@property (readonly) float scaleY;
@property (readonly) CGSize size;
+ (ResourcesManagerStretched *) sharedResourcesManager;
- (CCSprite *) addBackground:(NSString*)bg To:(CCLayer*)layer;
- (CCSprite *) addSpriteWithName:(NSString*)name Position:(CGPoint)pos Layer:(int)z To:(CCNode*)layer Tag:(int) tag;
- (CCAnimate*) generateAnimationWithName:(NSString*)name NumberOfImages:(int)n Delay:(float) delay Repeticion:(BOOL) rep Restore:(BOOL)restore;
@end
//
// ResourcesManagerStretched.m
//
// Created by Elena Vielva on 31/04/12.
// Copyright (c) 2012 Elena Vielva. All rights reserved.
//
#import "ResourcesManagerStretched.h"
static ResourcesManagerStretched *shared;
@implementation ResourcesManagerStretched
@synthesize scaleX = scaleX;
@synthesize scaleY = scaleY;
@synthesize size = size;
+ (ResourcesManagerStretched *) sharedResourcesManager {
if (shared) {
return shared;
}
shared = [[ResourcesManagerStretched alloc] init];
return shared;
}
+ (id)alloc {
NSAssert(shared == nil, @"Attempted to allocate a second instance of a singleton.");
return [super alloc];
}
-(id) init {
self = [super init];
if (self) {
size = [[CCDirector sharedDirector] winSize];
frameCache = [CCSpriteFrameCache sharedSpriteFrameCache];
}
return self;
}
/** ADDBACKGROUND:TO
* Adds a background image to the scene. For this:
* - Gets the file from the name
* - Scales it
* - Places it in the center of the screen
* - Adds it to the z-height -1 (to make sure its in the background)
* Input: NSString -> name of the background image
* CCLayer -> parent layer to add the background to
* Output: CCSprite -> the sprite
*/
- (CCSprite *) addBackground:(NSString *)bg To:(CCLayer *)layer {
CCSprite *background = [CCSprite spriteWithFile:bg];
if ((scaleX==0) || (scaleY==0)) {
scaleX = size.width / background.contentSize.width;
scaleY = size.height / background.contentSize.height;
}
background.scaleX = scaleX;
background.scaleY = scaleY;
NSAssert(background!=nil,@"Error couldn't load background");
[background setPosition:ccp(size.width/2, size.height/2)];
[layer addChild:background z:-1];
return background;
}
/** ADDSPRITEWITHFRAME:POSITION:LAYER:TO:TAG
* Adds the sprite to the scene. For this:
* - Gets the frame from the name
* - Gets the sprite from the frame
* - Scales the sprite according with the current device
* - Places it in the given position
* - Adds it to the parent node
* Input: NSString -> name of the file
* CGPoint -> position where the sprite is placed
* int -> z-layer (height) where the sprite is placed
* CCNode -> parent node where the sprite is added to
* Int -> tag
* Output: CCSprite -> the sprite
*/
- (CCSprite *) addSpriteWithName:(NSString*)name Position:(CGPoint)pos Layer:(int)z To:(CCNode *)layer Tag:(int) tag{
CCSpriteFrame * frame = [frameCache spriteFrameByName:name];
NSAssert(frame!=nil,@"Frame es nil");
CCSprite *sprite = [CCSprite spriteWithSpriteFrame:frame];
if ((scaleX==0) || (scaleY==0)) {
scaleX = size.width / sprite.contentSize.width;
scaleY = size.height / sprite.contentSize.height;
}
if (sprite!=nil) {
sprite.scaleX = scaleX;
sprite.scaleY = scaleY;
[sprite setPosition:pos];
[layer addChild:sprite z:z tag:tag];
}
return sprite;
}
/** GENERATEANIMATIONWITHNAME:NUMBEROFIMAGES:DELAY:REPETITION:TO {:RESTORE}
* The name of the sprites range from name0001.png to name0023.png for example
* Generates a cartoon-style animation. For this:
* - For each image of the animation: "calculates its name" and adds it to an array
* - When all the images are added to the array, creates the animation
* - It the animation should be a loop, makes the animation repeat forever
* Input: NSString -> Base name of the animations (before 0001.png)
* int -> Number of images of the animation
* float -> Delay between the animation frames
* BOOL -> Whether the animation is a loop or not
* BOOL -> Whether the animation should restore to the first frame when finished or not
* Output: CCAnimate -> The animation
*/
- (CCAnimate*) generateAnimationWithName:(NSString*)name NumberOfImages:(int)n Delay:(float) delay Repeticion:(BOOL) rep Restore:(BOOL)restore {
NSMutableArray *animFrames = [NSMutableArray array];
for (int i=1; i<=n; i++) {
NSString * spriteName;
if (i<10) {
spriteName = [NSString stringWithFormat:@"%@000%d.png",name, i];
} else if (i<100) {
spriteName = [NSString stringWithFormat:@"%@00%d.png",name, i];
} else if (i<1000) {
spriteName = [NSString stringWithFormat:@"%@0%d.png",name, i];
} else if (i<10000) {
spriteName = [NSString stringWithFormat:@"%@%d.png",name, i];
}
CCSpriteFrame * frame = [frameCache spriteFrameByName: spriteName];
[animFrames addObject: frame];
}
CCAnimation *animation = [CCAnimation animationWithFrames:animFrames delay:delay];
CCAnimate *action = [CCAnimate actionWithAnimation:animation restoreOriginalFrame:restore];
if (rep) {
action = [CCRepeatForever actionWithAction:action];
}
return action;
}
@end
+(CCScene *) scene {
CCScene *scene = [CCScene node];
TestLayer *layer = [TestLayer node];
[scene addChild: layer];
return scene;
}
- (id) init {
self = [super init];
if (self) {
ResourcesManagerStretched *helper = [ResourcesManagerStretched sharedResourcesManager];
[helper addBackground:@"background.jpg" To:self];
size = helper.size;
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"resources.plist"
textureFile:@"resources.pvr.ccz"];
sp1 = [helper addSpriteWithName:@"spName.png" Position:ccp(size.width*0.5,size.height*0.18) Layer:5 To:self];
sp2 = [helper addSpriteWithName:@"otherSprite.png" Position:ccp(size.width*0.1,size.height*0.1) Layer:5 To:self];
}
return self;
}
view raw TestLayer.mm hosted with ❤ by GitHub

When the iPhone 5 ready apps were mandatory for new submissions (and updates), it was also possible to keep stretching the backgrounds and sprites, but now our characters have got too much fat. I had to think a new way of using only a pack of images and taking advance of all screen sizes. Pen and paper and a calculator for operations, I draw something like this:


The small rectangle should have all the important information for the app (buttons, information, ...) but the background have to fill all the space, in order to take advance of the iPhone screens. The reference size is the dimension of this rectangle in the different devices: in the iPad is the same as its size, while in the iphone will fulfill that 4*iphoneHeight = 3*iphoneWidth. Thus, the left reference point is the (screen size - reference size) / 2 and the point most on the right would be refLeft+refWidht*1.0.

Here there is the new framework and an example of how to use it:
//
// ResourcesManager.h
//
// Created by Elena Vielva on 15/04/13.
// Copyright (c) 2012 Elena Vielva. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "cocos2d.h"
#import "Constants.h"
@interface ResourcesManager : NSObject {
float _scale;
CGSize _size;
CGSize _refSize;
CCSpriteFrameCache *frameCache;
}
@property (readonly) float scale;
@property (readonly) CGSize size;
@property (readonly) CGSize refSize;
+ (ResourcesManager *) sharedResourcesManager;
- (CCSprite *) addBackground:(NSString*)bg To:(CCLayer*)layer;
- (CCSprite *) addSpriteWithName:(NSString*)name Position:(CGPoint)pos Layer:(int)z To:(CCNode*)layer Tag:(int) tag;
- (CCAnimate*) generateAnimationWithName:(NSString*)name NumberOfImages:(int)n Delay:(float) delay Repeticion:(BOOL) rep Restore:(BOOL)restore;
@end
//
// ResourcesManager.m
//
// Created by Elena Vielva on 15/04/13.
// Copyright (c) 2012 Elena Vielva. All rights reserved.
//
#import "ResourcesManager.h"
static ResourcesManager *shared;
@implementation ResourcesManager
@synthesize scale = _scale;
@synthesize size = _size;
@synthesize refSize = _refSize;
+ (ResourcesManager *) sharedResourcesManager {
if (shared) {
return shared;
}
shared = [[ResourcesManager alloc] init];
return shared;
}
+ (id)alloc {
NSAssert(shared == nil, @"Attempted to allocate a second instance of a singleton.");
return [super alloc];
}
-(id) init {
self = [super init];
if (self) {
_size = [[CCDirector sharedDirector] winSize];
frameCache = [CCSpriteFrameCache sharedSpriteFrameCache];
[self initRefSizes];
}
return self;
}
/** INITREFSIZES
* Initializes the reference sizes (dimensions) for the current device
* Reference size is the dimensions of the screen in which will be the information and important elements of the scene
* - iPads have as reference size the iPad non-retina dimension
* - iPhone reference size: keep the height; the width is calculated from its height with the ratio of iPad screen dimensions
*/
- (void) initRefSizes {
CGSize pixelSize = [[CCDirector sharedDirector] winSizeInPixels];
if (pixelSize.width == 1024) {
// iPad non-retina
_refSize = CGSizeMake(1024, 768);
}else if (pixelSize.width == 2048) {
// iPad retina
_refSize = CGSizeMake(1024, 768);
}else if (pixelSize.width == 1136) {
// iPhone 5
_refSize = CGSizeMake(427, 320);
}else if (pixelSize.width == 960) {
// iPhone retina
_refSize = CGSizeMake(427, 320);
}else if (pixelSize.width == 480) {
// iPhone non-retina
_refSize = CGSizeMake(427, 320);
}
_scale = pixelSize.height/768;
}
/** ADDBACKGROUND:TO
* Adds a background image to the scene. For this:
* - Gets the file from the name
* - Scales it
* - Places it in the center of the screen
* - Adds it to the z-height -1 (to make sure its in the background)
* Input: NSString -> name of the background image
* CCLayer -> parent layer to add the background to
* Output: CCSprite -> the sprite
*/
- (CCSprite *) addBackground:(NSString *)bg To:(CCLayer *)layer {
CCSprite *background = [CCSprite spriteWithFile:bg];
background.scale = _scale;
NSAssert(background!=nil,@"Error al cargar el fondo");
[background setPosition:ccp(_size.width/2, _size.height/2)];
[layer addChild:background z:-1];
return background;
}
/** ADDSPRITEWITHFRAME:POSITION:LAYER:TO:TAG
* Adds the sprite to the scene. For this:
* - Gets the frame from the name
* - Gets the sprite from the frame
* - Scales the sprite according with the current device
* - Places it in the given position
* - Adds it to the parent node
* Input: NSString -> name of the file
* CGPoint -> position where the sprite is placed
* int -> z-layer (height) where the sprite is placed
* CCNode -> parent node where the sprite is added to
* Int -> tag
* Output: CCSprite -> the sprite
*/
- (CCSprite *) addSpriteWithName:(NSString*)name Position:(CGPoint)pos Layer:(int)z To:(CCNode *)layer Tag:(int) tag{
CCSpriteFrame * frame = [frameCache spriteFrameByName:name];
NSAssert(frame!=nil,@"Frame es nil");
CCSprite *sprite = [CCSprite spriteWithSpriteFrame:frame];
if (sprite!=nil) {
sprite.scale = _scale;
[sprite setPosition:pos];
[layer addChild:sprite z:z tag:tag];
}
return sprite;
}
/** GENERATEANIMATIONWITHNAME:NUMBEROFIMAGES:DELAY:REPETITION:TO {:RESTORE}
* The name of the sprites range from name0001.png to name0023.png for example
* Generates a cartoon-style animation. For this:
* - For each image of the animation: "calculates its name" and adds it to an array
* - When all the images are added to the array, creates the animation
* - It the animation should be a loop, makes the animation repeat forever
* Recibe: NSString -> Base name of the animations (before 0001.png)
* int -> Number of images of the animation
* float -> Delay between the animation frames
* BOOL -> Whether the animation is a loop or not
* BOOL -> Whether the animation should restore to the first frame when finished or not
* Devuelve: CCAnimate -> The animation
*/
- (CCAnimate*) generateAnimationWithName:(NSString*)name NumberOfImages:(int)n Delay:(float) delay Repeticion:(BOOL) rep Restore:(BOOL)restore {
NSMutableArray *animFrames = [NSMutableArray array];
for (int i=1; i<=n; i++) {
NSString * spriteName;
if (i<10) {
spriteName = [NSString stringWithFormat:@"%@000%d.png",name, i];
} else if (i<100) {
spriteName = [NSString stringWithFormat:@"%@00%d.png",name, i];
} else if (i<1000) {
spriteName = [NSString stringWithFormat:@"%@0%d.png",name, i];
} else if (i<10000) {
spriteName = [NSString stringWithFormat:@"%@%d.png",name, i];
}
CCSpriteFrame * frame = [frameCache spriteFrameByName: spriteName];
[animFrames addObject: frame];
}
CCAnimation *animation = [CCAnimation animationWithSpriteFrames:animFrames delay:delay];
CCAnimate *action = [CCAnimate actionWithAnimation:animation restoreOriginalFrame:restore];
if (rep) {
action = [CCRepeatForever actionWithAction:action];
}
return action;
}
- (void) dealloc {
NSLog(@"Deallocating %@",self);
[super dealloc];
}
@end
+ (CCScene *) scene{
CCScene *scene = [CCScene node];
SomeLayer *layer = [SomeLayer node];
[scene addChild:layer];
return scene;
}
- (id) init{
self = [super init];
if (self) {
// ask director for the window size
CGSize size = [[CCDirector sharedDirector] winSize];
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"sceneResources.plist"];
ResourcesManager *helper = [ResourcesManager sharedResourcesManager];
float refWidth = helper.refSize.width;
float refLeft = size.width/2-refWidth/2;
[helper addBackground:@"sceneBackground.jpg" To:self];
sp1 = [helper addSpriteWithName:@"spriteName.png" Position:ccp(refLeft+refWidth*0.5, size.height*0.18) Layer:2 To:self];
sp2 = [helper addSpriteWithName:@"otherSprite.png" Position:ccp(refLeft+refWidth*0.1, size.height*0.1) Layer:2 To:self];
}
return self;
}
view raw SomeLayer.mm hosted with ❤ by GitHub