Cocos2D and the new Retina iPad

Cocos2D and the new Retina iPad

Written by
March 19, 2012 @ 8:08 pm •
Filed under Ramblings

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:

view sourceprint?

001.@implementation CCDirectorIOS (Retina)

002. 

003.CGFloat __ccPointScaleFactor = 1;

004. 

005.// new method call after creating the director in your AppDelegate

006.- (void)makeUniversal

007.{

008.if(UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)

009.{

010.__ccPointScaleFactor = 2;

011.}

012.}

013. 

014.-(void) setContentScaleFactor:(CGFloat)scaleFactor

015.{

016.if( scaleFactor != __ccContentScaleFactor )

017.{

018.__ccContentScaleFactor = scaleFactor;

019. 

020.CGSize glSize = [openGLView_ bounds].size;

021.winSizeInPoints_ = CGSizeMake( glSize.width / __ccPointScaleFactor, glSize.height / __ccPointScaleFactor );

022.winSizeInPixels_ = CGSizeMake( winSizeInPoints_.width * scaleFactor, winSizeInPoints_.height * scaleFactor );

023. 

024.if( openGLView_ )

025.[self updateContentScaleFactor];

026. 

027.// update projection

028.[self setProjection:projection_];

029.}

030.}

031. 

032.-(void) updateContentScaleFactor

033.{

034.// Based on code snippet from: http://developer.apple.com/iphone/prerelease/library/snippets/sp2010/sp28.html

035.if([openGLView_ respondsToSelector:@selector(setContentScaleFactor:)])

036.{

037.CGFloat scaleFactor = (__ccPointScaleFactor == 1) ? __ccContentScaleFactor : (__ccContentScaleFactor / __ccPointScaleFactor);

038.[openGLView_ setContentScaleFactor: scaleFactor];

039. 

040.isContentScaleSupported_ = YES;

041.}

042.else

043.{

044.CCLOG(@"cocos2d: WARNING: calling setContentScaleFactor on iOS < 4. Using fallback mechanism");

045.isContentScaleSupported_ = NO;

046.}

047.}

048. 

049.-(BOOL) enableRetinaDisplay:(BOOL)enabled

050.{

051.// Already enabled ?

052.if( enabled && __ccContentScaleFactor == 2 )

053.returnYES;

054. 

055.// Already disabled

056.if( ! enabled && __ccContentScaleFactor == 1 )

057.returnYES;

058. 

059.// setContentScaleFactor is not supported

060.if(! [openGLView_ respondsToSelector:@selector(setContentScaleFactor:)])

061.returnNO;

062. 

063.// SD device

064.if([[UIScreen mainScreen] scale] == 1.0 && __ccPointScaleFactor == 1.0)

065.returnNO;

066. 

067.float newScale = enabled ? 2 : 1;

068.[self setContentScaleFactor:newScale];

069. 

070.returnYES;

071.}

072. 

073.-(void) reshapeProjection:(CGSize)size

074.{

075.CGSize glSize = [openGLView_ bounds].size;

076.winSizeInPoints_ = CGSizeMake(glSize.width/__ccPointScaleFactor, glSize.height/__ccPointScaleFactor);

077.winSizeInPixels_ = CGSizeMake(winSizeInPoints_.width * __ccContentScaleFactor, winSizeInPoints_.height *__ccContentScaleFactor);

078. 

079.[self setProjection:projection_];

080.}

081. 

082.-(CGPoint)convertToGL:(CGPoint)uiPoint

083.{

084.CGSize s = winSizeInPoints_;

085.float newY = s.height - (uiPoint.y / __ccPointScaleFactor);

086.float newX = uiPoint.x / __ccPointScaleFactor;

087. 

088.returnccp( newX, newY );

089.}

090. 

091.-(CGPoint)convertToUI:(CGPoint)glPoint

092.{

093.CGSize winSize = winSizeInPoints_;

094.int newX = glPoint.x * __ccPointScaleFactor;

095.int newY = (winSize.height - glPoint.y) * __ccPointScaleFactor;

096. 

097.returnccp(newX, newY);

098.}

099. 

100.@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 scaled-down 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 <code>CCDirectorIOS</code> category becomes:

 

view sourceprint?

001.@implementation CCDirectorIOS (My)

002. 

003.&nbsp;

004. 

005.CGFloat __ccPointScaleFactor = 1;

006. 

007.&nbsp;

008. 

009.// new method call after creating the director in your AppDelegate

010. 

011.- (void)makeUniversal

012. 

013.{

014. 

015.if(UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)

016. 

017.{

018. 

019.__ccPointScaleFactor = 2;

020. 

021.}

022. 

023.}

024. 

025.&nbsp;

026. 

027.-(void) setContentScaleFactor:(CGFloat)scaleFactor

028. 

029.{

030. 

031.if( scaleFactor != __ccContentScaleFactor )

032. 

033.{

034. 

035.__ccContentScaleFactor = scaleFactor;

036. 

037.CGSize glSize = [openGLView_ bounds].size;

038. 

039.winSizeInPoints_ = CGSizeMake( glSize.width / __ccPointScaleFactor, glSize.height / __ccPointScaleFactor );

040. 

041.winSizeInPixels_ = CGSizeMake( winSizeInPoints_.width * scaleFactor, winSizeInPoints_.height * scaleFactor );

042. 

043.if( openGLView_ )

044. 

045.[self updateContentScaleFactor];

046. 

047.// update projection

048. 

049.[self setProjection:projection_];

050. 

051.}

052. 

053.}

054. 

055.&nbsp;

056. 

057.-(void) updateContentScaleFactor

058. 

059.{

060. 

061.// Based on code snippet from: <a href="http://developer.apple.com/iphone/prerelease/library/snippets/sp2010/sp28.html">http://developer.apple.com/iphone/prerelease/library/snippets/sp2010/sp28.html<;/a>

062. 

063.if([openGLView_ respondsToSelector:@selector(setContentScaleFactor:)])

064. 

065.{

066. 

067.CGFloat scaleFactor = (__ccPointScaleFactor == 1) ? __ccContentScaleFactor : (__ccContentScaleFactor / __ccPointScaleFactor);

068. 

069.[openGLView_ setContentScaleFactor: scaleFactor];

070. 

071.isContentScaleSupported_ = YES;

072. 

073.}

074. 

075.else

076. 

077.{

078. 

079.CCLOG(@"cocos2d: WARNING: calling setContentScaleFactor on iOS &lt; 4. Using fallback mechanism");

080. 

081.isContentScaleSupported_ = NO;

082. 

083.}

084. 

085.}

086. 

087.&nbsp;

088. 

089.#ifdef CC_RETINA_IPAD_DISPLAY_FILENAME_SUFFIX

090. 

091.&nbsp;

092. 

093.-(BOOL) enableRetinaDisplay:(BOOL)enabled

094. 

095.{

096. 

097.return[self enableRetinaDisplay:enabled onPad:FALSE];

098. 

099.}

100. 

101.&nbsp;

102. 

103.-(BOOL) enableRetinaDisplay:(BOOL)enabled onPad:(BOOL)onPad

104. 

105.{

106. 

107.// Already enabled ?

108. 

109.if( enabled &amp;&amp; __ccContentScaleFactor == 2 )

110. 

111.returnYES;

112. 

113.// Already disabled

114. 

115.if( ! enabled &amp;&amp; __ccContentScaleFactor == 1 )

116. 

117.returnYES;

118. 

119.// setContentScaleFactor is not supported

120. 

121.if(! [openGLView_ respondsToSelector:@selector(setContentScaleFactor:)])

122. 

123.returnNO;

124. 

125.// SD device

126. 

127.CGFloat scale = [[UIScreen mainScreen] scale];

128. 

129.if(scale == 1.0 &amp;&amp; __ccPointScaleFactor == 1.0)

130. 

131.returnNO;

132. 

133.float newScale = 1;

134. 

135.if(onPad) {

136. 

137.newScale = enabled ? (scale * __ccPointScaleFactor) : 1;

138. 

139.} else{

140. 

141.newScale = enabled ? 2 : 1;

142. 

143.}

144. 

145.[self setContentScaleFactor:newScale];

146. 

147.returnYES;

148. 

149.}

150. 

151.&nbsp;

152. 

153.#else

154. 

155.&nbsp;

156. 

157.-(BOOL) enableRetinaDisplay:(BOOL)enabled

158. 

159.{

160. 

161.// Already enabled ?

162. 

163.if( enabled &amp;&amp; __ccContentScaleFactor == 2 )

164. 

165.returnYES;

166. 

167.// Already disabled

168. 

169.if( ! enabled &amp;&amp; __ccContentScaleFactor == 1 )

170. 

171.returnYES;

172. 

173.// setContentScaleFactor is not supported

174. 

175.if(! [openGLView_ respondsToSelector:@selector(setContentScaleFactor:)])

176. 

177.returnNO;

178. 

179.// SD device

180. 

181.if([[UIScreen mainScreen] scale] == 1.0 &amp;&amp; __ccPointScaleFactor == 1.0)

182. 

183.returnNO;

184. 

185.float newScale = enabled ? 2 : 1;

186. 

187.[self setContentScaleFactor:newScale];

188. 

189.returnYES;

190. 

191.}

192. 

193.#endif

194. 

195.&nbsp;

196. 

197.&nbsp;

198. 

199.-(void) reshapeProjection:(CGSize)size

200. 

201.{

202. 

203.CGSize glSize = [openGLView_ bounds].size;

204. 

205.winSizeInPoints_ = CGSizeMake(glSize.width/__ccPointScaleFactor, glSize.height/__ccPointScaleFactor);

206. 

207.winSizeInPixels_ = CGSizeMake(winSizeInPoints_.width * __ccContentScaleFactor, winSizeInPoints_.height *__ccContentScaleFactor);

208. 

209.[self setProjection:projection_];

210. 

211.}

212. 

213.&nbsp;

214. 

215.-(CGPoint)convertToGL:(CGPoint)uiPoint

216. 

217.{

218. 

219.CGSize s = winSizeInPoints_;

220. 

221.float newY = s.height - (uiPoint.y / __ccPointScaleFactor);

222. 

223.float newX = uiPoint.x / __ccPointScaleFactor;

224. 

225.returnccp( newX, newY );

226. 

227.}

228. 

229.&nbsp;

230. 

231.-(CGPoint)convertToUI:(CGPoint)glPoint

232. 

233.{

234. 

235.CGSize winSize = winSizeInPoints_;

236. 

237.int newX = glPoint.x * __ccPointScaleFactor;

238. 

239.int newY = (winSize.height - glPoint.y) * __ccPointScaleFactor;

240. 

241.returnccp(newX, newY);

242. 

243.}

244. 

245.@end

246. 

247.As you can see, there is now a conditional section that can be compiled in ifyou need it. To doso, you need to define somewhere in your header files the <code> CC_RETINA_IPAD_DISPLAY_FILENAME_SUFFIX</code> symbol, e.g.:

248. 

249.<code>#define CC_RETINA_IPAD_DISPLAY_FILENAME_SUFFIX @"-hdpad"</code>

250. 

251.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 <code>CCFileUtils.m</code> file. Here it goes:

252. 

253.[sourcecode]

254. 

255.+(NSString*) getDoubleResolutionImage:(NSString*)path

256.{

257.#ifCC_IS_RETINA_DISPLAY_SUPPORTED

258. 

259.NSString * retinaPath;

260.if( CC_CONTENT_SCALE_FACTOR() == 4 )

261.{

262.if( (retinaPath = [self getPathForSuffix:path suffix:CC_RETINA_IPAD_DISPLAY_FILENAME_SUFFIX]) ) {

263.returnretinaPath;

264.}

265.}

266. 

267.if( CC_CONTENT_SCALE_FACTOR() == 2 )

268.{

269.if( (retinaPath = [self getPathForSuffix:path suffix:CC_RETINA_DISPLAY_FILENAME_SUFFIX]) ) {

270.returnretinaPath;

271.}

272.}

273. 

274.#endif// CC_IS_RETINA_DISPLAY_SUPPORTED

275. 

276.returnpath;

277.}

278. 

279.+(NSString*) getPathForSuffix:(NSString*)path suffix:(NSString*)suffix {

280.NSString *pathWithoutExtension = [path stringByDeletingPathExtension];

281.NSString *name = [pathWithoutExtension lastPathComponent];

282. 

283.// check if path already has the suffix.

284.if( [name rangeOfString:suffix].location != NSNotFound ) {

285. 

286.CCLOG(@"cocos2d: WARNING Filename(%@) already has the suffix %@. Using it.", name, suffix);

287.returnpath;

288.}

289. 

290.NSString *extension = [path pathExtension];

291. 

292.if( [extension isEqualToString:@"ccz"] || [extension isEqualToString:@"gz"] )

293.{

294.// All ccz / gz files should be in the format filename.xxx.ccz

295.// so we need to pull off the .xxx part of the extension as well

296.extension = [NSString stringWithFormat:@"%@.%@", [pathWithoutExtension pathExtension], extension];

297.pathWithoutExtension = [pathWithoutExtension stringByDeletingPathExtension];

298.}

299. 

300.NSString *retinaName = [pathWithoutExtension stringByAppendingString:suffix];

301.retinaName = [retinaName stringByAppendingPathExtension:extension];

302. 

303.if( [__localFileManager fileExistsAtPath:retinaName] )

304.returnretinaName;

305. 

306.CCLOG(@"cocos2d: CCFileUtils: Warning HD file not found (%@): %@", suffix, [retinaName lastPathComponent] );

307. 

308.returnnil;

309.}

In order to factorize this code in a category, we need a way to access __localFileManager. But this variable is unfortunately a static global, so it cannot be accessed from outside the compilation unit where it is defined. One workaround is reimplementing it in the category itself:

view sourceprint?

001.#ifdef CC_RETINA_IPAD_DISPLAY_FILENAME_SUFFIX

002. 

003.@implementation CCFileUtils (Retina_iPad)

004. 

005.NSString *ccRemoveHDSuffixFromFile( NSString *path )

006.{

007.#ifCC_IS_RETINA_DISPLAY_SUPPORTED

008. 

009.if( CC_CONTENT_SCALE_FACTOR() == 2 ) {

010. 

011.NSString *name = [path lastPathComponent];

012. 

013.// check if path already has the suffix.

014.if( [name rangeOfString:CC_RETINA_DISPLAY_FILENAME_SUFFIX].location != NSNotFound ) {

015. 

016.CCLOG(@"cocos2d: Filename(%@) contains %@ suffix. Removing it. See cocos2d issue #1040", path, CC_RETINA_DISPLAY_FILENAME_SUFFIX);

017. 

018.NSString *newLastname = [name stringByReplacingOccurrencesOfString:CC_RETINA_DISPLAY_FILENAME_SUFFIX withString:@""];

019. 

020.NSString *pathWithoutLastname = [path stringByDeletingLastPathComponent];

021.return[pathWithoutLastname stringByAppendingPathComponent:newLastname];

022.} elseif( [name rangeOfString:CC_RETINA_IPAD_DISPLAY_FILENAME_SUFFIX].location != NSNotFound ) {

023. 

024.CCLOG(@"cocos2d: Filename(%@) contains %@ suffix. Removing it. See cocos2d issue #1040", path, CC_RETINA_IPAD_DISPLAY_FILENAME_SUFFIX);

025. 

026.NSString *newLastname = [name stringByReplacingOccurrencesOfString:CC_RETINA_IPAD_DISPLAY_FILENAME_SUFFIX withString:@""];

027. 

028.NSString *pathWithoutLastname = [path stringByDeletingLastPathComponent];

029.return[pathWithoutLastname stringByAppendingPathComponent:newLastname];

030.}

031.}

032. 

033.#endif// CC_IS_RETINA_DISPLAY_SUPPORTED

034. 

035.returnpath;

036. 

037.}

038. 

039.+(NSString*) getDoubleResolutionImage:(NSString*)path

040.{

041.#ifCC_IS_RETINA_DISPLAY_SUPPORTED

042. 

043.NSString * retinaPath;

044.if( CC_CONTENT_SCALE_FACTOR() == 4 )

045.{

046.if( (retinaPath = [self getPathForSuffix:path suffix:CC_RETINA_IPAD_DISPLAY_FILENAME_SUFFIX]) ) {

047.returnretinaPath;

048.}

049.}

050. 

051.if( CC_CONTENT_SCALE_FACTOR() == 2 )

052.{

053.if( (retinaPath = [self getPathForSuffix:path suffix:CC_RETINA_DISPLAY_FILENAME_SUFFIX]) ) {

054.returnretinaPath;

055.}

056.}

057. 

058.#endif// CC_IS_RETINA_DISPLAY_SUPPORTED

059. 

060.returnpath;

061.}

062. 

063.+(NSString*) getPathForSuffix:(NSString*)path suffix:(NSString*)suffix {

064.NSString *pathWithoutExtension = [path stringByDeletingPathExtension];

065.NSString *name = [pathWithoutExtension lastPathComponent];

066. 

067.staticNSFileManager *__localFileManager = [[NSFileManager alloc] init];

068. 

069.// check if path already has the suffix.

070.if( [name rangeOfString:suffix].location != NSNotFound ) {

071. 

072.CCLOG(@"cocos2d: WARNING Filename(%@) already has the suffix %@. Using it.", name, suffix);

073.returnpath;

074.}

075. 

076.NSString *extension = [path pathExtension];

077. 

078.if( [extension isEqualToString:@"ccz"] || [extension isEqualToString:@"gz"] )

079.{

080.// All ccz / gz files should be in the format filename.xxx.ccz

081.// so we need to pull off the .xxx part of the extension as well

082.extension = [NSString stringWithFormat:@"%@.%@", [pathWithoutExtension pathExtension], extension];

083.pathWithoutExtension = [pathWithoutExtension stringByDeletingPathExtension];

084.}

085. 

086.NSString *retinaName = [pathWithoutExtension stringByAppendingString:suffix];

087.retinaName = [retinaName stringByAppendingPathExtension:extension];

088. 

089.if( [__localFileManager fileExistsAtPath:retinaName] )

090.returnretinaName;

091. 

092.CCLOG(@"cocos2d: CCFileUtils: Warning HD file not found (%@): %@", suffix, [retinaName lastPathComponent] );

093. 

094.returnnil;

095.}

096. 

097.@end

098. 

099.#endif

发表评论

电子邮件地址不会被公开。 必填项已用*标注