Cocos2D and the new Retina iPad

The introduction of the new iPad, a.k.a Retina-iPad, or iPad 3, has made developing universal applications supporting all of iOS devices definitely more complex. This is mostly due to the requirement of providing artwork for 4 (four!) different resolutions:

  • original iPhone: 320×480;
  • retina display (iPhone 4, and iPod Touch 4g): 640×960;
  • iPad 1/2: 768×1024;
  • retina iPad: 1536×2048.

Apple has done its best to support dealing with the different resolutions as transparently and effortlessly as possible, but still the problem remains of generating and managing 4 different versions for each piece of artwork you use in your app.

In fact, even before the new iPad was launched, things were not so easy and straigth-forward, since developers already had to provide artworks in 3 different sizes. For this reason, one common pattern to reduce effort and still provide a great user experience was to “reuse” on the iPad artworks tailored to the retina iPhone display (640×960 vs. 768×1024). This pattern was so a common one that the cocos2D community even provided some patch to the CCDirectorIOS class to directly support it “out-of’the-box”. Lately, I have created a CCDirectorIOS category encapsulating all that needs to be done to patch the director:


//////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////
@implementation CCDirectorIOS (SDSRetina)

CGFloat __ccPointScaleFactor = 1;

//////////////////////////////////////////////////////////////////////////////////////////////
// new method call after creating the director in your AppDelegate
- (void)makeUniversal{
	if(UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
		__ccPointScaleFactor = 2;
}

//////////////////////////////////////////////////////////////////////////////////////////////
-(void) setContentScaleFactor:(CGFloat)scaleFactor {
	if( scaleFactor != __ccContentScaleFactor ) {
		__ccContentScaleFactor = scaleFactor;

		CGSize glSize = [openGLView_ bounds].size;
		winSizeInPoints_ = CGSizeMake( glSize.width / __ccPointScaleFactor, glSize.height / __ccPointScaleFactor );
		winSizeInPixels_ = CGSizeMake( winSizeInPoints_.width * scaleFactor, winSizeInPoints_.height * scaleFactor );
        
		if( openGLView_ )
			[self updateContentScaleFactor];
        
		// update projection
		[self setProjection:projection_];
	}
}


//////////////////////////////////////////////////////////////////////////////////////////////
-(void) updateContentScaleFactor {
	// Based on code snippet from: http://developer.apple.com/iphone/prerelease/library/snippets/sp2010/sp28.html
	if ([openGLView_ respondsToSelector:@selector(setContentScaleFactor:)])
	{
		CGFloat scaleFactor = (__ccPointScaleFactor == 1) ? __ccContentScaleFactor : (__ccContentScaleFactor / __ccPointScaleFactor);
		[openGLView_ setContentScaleFactor: scaleFactor];
        
		isContentScaleSupported_ = YES;
	}
	else
	{
		CCLOG(@"cocos2d: WARNING: calling setContentScaleFactor on iOS < 4. Using fallback mechanism");
		isContentScaleSupported_ = NO;
	}
}

//////////////////////////////////////////////////////////////////////////////////////////////
-(BOOL) enableRetinaDisplay:(BOOL)enabled {
	// Already enabled ?
	if( enabled && __ccContentScaleFactor == 2 )
		return YES;
    
	// Already disabled
	if( ! enabled && __ccContentScaleFactor == 1 )
		return YES;
    
	// setContentScaleFactor is not supported
	if (! [openGLView_ respondsToSelector:@selector(setContentScaleFactor:)])
		return NO;
    
	// SD device
	if ([[UIScreen mainScreen] scale] == 1.0 && __ccPointScaleFactor == 1.0)
		return NO;
    
	float newScale = enabled ? 2 : 1;
	[self setContentScaleFactor:newScale];
    
	// Load Hi-Res FPS label
    //	[self createFPSLabel];
    
	return YES;
}

//////////////////////////////////////////////////////////////////////////////////////////////
-(void) reshapeProjection:(CGSize)size {
	CGSize glSize = [openGLView_ bounds].size;
	winSizeInPoints_ = CGSizeMake(glSize.width/__ccPointScaleFactor, glSize.height/__ccPointScaleFactor);
	winSizeInPixels_ = CGSizeMake(winSizeInPoints_.width * __ccContentScaleFactor, winSizeInPoints_.height *__ccContentScaleFactor);
    
	[self setProjection:projection_];
}

//////////////////////////////////////////////////////////////////////////////////////////////
-(CGPoint)convertToGL:(CGPoint)uiPoint {
	CGSize s = winSizeInPoints_;
	float newY = s.height - (uiPoint.y / __ccPointScaleFactor);
	float newX = uiPoint.x / __ccPointScaleFactor;
    
	return ccp( newX, newY );
}

//////////////////////////////////////////////////////////////////////////////////////////////
-(CGPoint)convertToUI:(CGPoint)glPoint {
	CGSize winSize = winSizeInPoints_;
	int newX = glPoint.x * __ccPointScaleFactor;
	int newY = (winSize.height - glPoint.y) * __ccPointScaleFactor;
    
	return ccp(newX, newY);
}

@end

Just copy/paste the above category in a file of its own and add it to your project, and magics will happen: all iPad (1/2) artwork will be shown in the higher resolution version you provided for the retina iPhone. You’ll have to bear with a couple of warnings due to calling some CCDirectorIOS private methods, but the methods are there.

Welcome Retina iPad!

The new iPad has made things better, indeed exciting, on the one hand, but worse on the other, meaning: more work to create and manage your artworks. The trick above will not be of much help, since as a side effect of having to support the new 1536x1024p resolution, what makes sense is natively supporting the 768x1024p resolution with properly scaled artworks.

One important point to understand is that existing apps that do use that trick will still work correctly on the new iPad, but they will break if you try to recompile them with the newer iOS SDK (have a look at this discussion on the cocos2d forum) without adapting them to the new retina iPad. This is something that cannot be circumvented in the end, but if you are in a hurry and need to quickly publish a new version of your app, still cannot afford to update all of its artworks now, well, there is a patch to the patch…

(The code that I am going to display here is a slight adaptation of the original one coming from Taco Graveyard blog.)

So, the CCDirectorIOS category becomes:

 


//////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////
@implementation CCDirectorIOS (SDSRetina)

CGFloat __ccPointScaleFactor = 1;

//////////////////////////////////////////////////////////////////////////////////////////////
// new method call after creating the director in your AppDelegate
- (void)makeUniversal{
	if(UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
		__ccPointScaleFactor = 2;
}

//////////////////////////////////////////////////////////////////////////////////////////////
-(void) setContentScaleFactor:(CGFloat)scaleFactor {
	if( scaleFactor != __ccContentScaleFactor ) {
		__ccContentScaleFactor = scaleFactor;

		CGSize glSize = [openGLView_ bounds].size;
		winSizeInPoints_ = CGSizeMake( glSize.width / __ccPointScaleFactor, glSize.height / __ccPointScaleFactor );
		winSizeInPixels_ = CGSizeMake( winSizeInPoints_.width * scaleFactor, winSizeInPoints_.height * scaleFactor );
        
		if( openGLView_ )
			[self updateContentScaleFactor];
        
		// update projection
		[self setProjection:projection_];
	}
}


//////////////////////////////////////////////////////////////////////////////////////////////
-(void) updateContentScaleFactor {
	// Based on code snippet from: http://developer.apple.com/iphone/prerelease/library/snippets/sp2010/sp28.html
	if ([openGLView_ respondsToSelector:@selector(setContentScaleFactor:)])
	{
		CGFloat scaleFactor = (__ccPointScaleFactor == 1) ? __ccContentScaleFactor : (__ccContentScaleFactor / __ccPointScaleFactor);
		[openGLView_ setContentScaleFactor: scaleFactor];
        
		isContentScaleSupported_ = YES;
	}
	else
	{
		CCLOG(@"cocos2d: WARNING: calling setContentScaleFactor on iOS < 4. Using fallback mechanism");
		isContentScaleSupported_ = NO;
	}
}

#ifdef CC_RETINA_IPAD_DISPLAY_FILENAME_SUFFIX
//////////////////////////////////////////////////////////////////////////////////////////////
-(BOOL) enableRetinaDisplay:(BOOL)enabled {
	return [self enableRetinaDisplay:enabled onPad:FALSE];
}

-(BOOL) enableRetinaDisplay:(BOOL)enabled onPad:(BOOL)onPad {
	// Already enabled ?
	if( enabled && __ccContentScaleFactor == 2 )
		return YES;
    
	// Already disabled
	if( ! enabled && __ccContentScaleFactor == 1 )
		return YES;
    
	// setContentScaleFactor is not supported
	if (! [openGLView_ respondsToSelector:@selector(setContentScaleFactor:)])
		return NO;
    
	// SD device
    CGFloat scale = [[UIScreen mainScreen] scale];
	if (scale == 1.0 && __ccPointScaleFactor == 1.0)
		return NO;
    
    float newScale = 1;
    if (onPad) {
        newScale = enabled ? (scale * __ccPointScaleFactor) : 1;
    } else {
        newScale = enabled ? 2 : 1;
    }
	[self setContentScaleFactor:newScale];
    
	// Load Hi-Res FPS label
	//[self createFPSLabel];
    
	return YES;
}

#else

//////////////////////////////////////////////////////////////////////////////////////////////
-(BOOL) enableRetinaDisplay:(BOOL)enabled {
	// Already enabled ?
	if( enabled && __ccContentScaleFactor == 2 )
		return YES;
    
	// Already disabled
	if( ! enabled && __ccContentScaleFactor == 1 )
		return YES;
    
	// setContentScaleFactor is not supported
	if (! [openGLView_ respondsToSelector:@selector(setContentScaleFactor:)])
		return NO;
    
	// SD device
	if ([[UIScreen mainScreen] scale] == 1.0 && __ccPointScaleFactor == 1.0)
		return NO;
    
	float newScale = enabled ? 2 : 1;
	[self setContentScaleFactor:newScale];
    
	// Load Hi-Res FPS label
    //	[self createFPSLabel];
    
	return YES;
}
#endif


//////////////////////////////////////////////////////////////////////////////////////////////
-(void) reshapeProjection:(CGSize)size {
	CGSize glSize = [openGLView_ bounds].size;
	winSizeInPoints_ = CGSizeMake(glSize.width/__ccPointScaleFactor, glSize.height/__ccPointScaleFactor);
	winSizeInPixels_ = CGSizeMake(winSizeInPoints_.width * __ccContentScaleFactor, winSizeInPoints_.height *__ccContentScaleFactor);
    
	[self setProjection:projection_];
}

//////////////////////////////////////////////////////////////////////////////////////////////
-(CGPoint)convertToGL:(CGPoint)uiPoint {
	CGSize s = winSizeInPoints_;
	float newY = s.height - (uiPoint.y / __ccPointScaleFactor);
	float newX = uiPoint.x / __ccPointScaleFactor;
    
	return ccp( newX, newY );
}

//////////////////////////////////////////////////////////////////////////////////////////////
-(CGPoint)convertToUI:(CGPoint)glPoint {
	CGSize winSize = winSizeInPoints_;
	int newX = glPoint.x * __ccPointScaleFactor;
	int newY = (winSize.height - glPoint.y) * __ccPointScaleFactor;
    
	return ccp(newX, newY);
}

@end

As you can see, there is now a conditional section that can be compiled in if you need it. To do so, you need to define somewhere in your header files the CC_RETINA_IPAD_DISPLAY_FILENAME_SUFFIX symbol, e.g.:

#define CC_RETINA_IPAD_DISPLAY_FILENAME_SUFFIX @"-hdpad"

There is one more change that you need to make. In this case, it cannot be easily factorized in a category, but you can directly patch the code in the CCFileUtils.m file. Here it goes:


//////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////
@implementation CCFileUtils (SDSRetina)

//////////////////////////////////////////////////////////////////////////////////////////////
+ (NSString*)getDoubleResolutionImage:(NSString*)path {

#if CC_IS_RETINA_DISPLAY_SUPPORTED
    
    NSString * retinaPath;
	if( CC_CONTENT_SCALE_FACTOR() == 4 ) {
		if ( (retinaPath = [self getPathForSuffix:path suffix:CC_RETINA_IPAD_DISPLAY_FILENAME_SUFFIX]) ) {
            return retinaPath;
        }
	}
    
    if( CC_CONTENT_SCALE_FACTOR() == 2 ) {
        if ( (retinaPath = [self getPathForSuffix:path suffix:CC_RETINA_DISPLAY_FILENAME_SUFFIX]) ) {
            return retinaPath;
        }
	}
	
#endif // CC_IS_RETINA_DISPLAY_SUPPORTED
    
	return path;
}

//////////////////////////////////////////////////////////////////////////////////////////////
+ (NSString*)getPathForSuffix:(NSString*)path suffix:(NSString*)suffix {
    
    NSString *pathWithoutExtension = [path stringByDeletingPathExtension];
    NSString *name = [pathWithoutExtension lastPathComponent];
    
    //-- check if path already has the suffix.
    if( [name rangeOfString:suffix].location != NSNotFound ) {
        
        CCLOG(@"cocos2d: WARNING Filename(%@) already has the suffix %@. Using it.", name, suffix);			
        return path;
    }
    
    
    NSString *extension = [path pathExtension];
    
    if( [extension isEqualToString:@"ccz"] || [extension isEqualToString:@"gz"] )
    {
        // All ccz / gz files should be in the format filename.xxx.ccz
        // so we need to pull off the .xxx part of the extension as well
        extension = [NSString stringWithFormat:@"%@.%@", [pathWithoutExtension pathExtension], extension];
        pathWithoutExtension = [pathWithoutExtension stringByDeletingPathExtension];
    }
    
    
    NSString *retinaName = [pathWithoutExtension stringByAppendingString:suffix];
    retinaName = [retinaName stringByAppendingPathExtension:extension];
    
    if( [__localFileManager fileExistsAtPath:retinaName] )
        return retinaName;
    
    CCLOG(@"cocos2d: CCFileUtils: Warning HD file not found (%@): %@", suffix, [retinaName lastPathComponent] );
    
    return nil;
}

In order to factorize this code in a category, we need two things:

  1. a way to access __localFileManager;
  2. redefining/wrapping ccRemoveHDSuffixFromFile

As to point 1, the problem is that this variable is a static global, so it cannot be directly accessed from outside the compilation unit where it is defined. One workaround is reimplementing it in the category. Point 2 is trickier, since overriding a C function is not that straight. In any case, there are several methods available that you can read here about (by the way, what an impressive post!).

With all this, the category files can be found on my github.

Summing up

If you want to make your cocos2D app work on a retina iPad with full resolution images, simply use the above category and don’t forget to call (e.g., in your app delegate):

[director setProjection:kCCDirectorProjection2D];

On the other hand, if you want to use the “universal” mode (which will “trick” your cocos2D into thinking that an iPad has a 384×512 resolution), call makeUniversal.

This workaround has been tested with Cocos2D 0.95.

Copyright © Labs Ramblings

Built on Notes Blog by TDH
Powered by WordPress

That's it - back to the top of page!