More Pixels Equals More Panic

The March 2012 Apple Keynote brought no real surprises. The new iPad (hereinafter iPad 3 despite protestations) has a Retina Display with a whopping 1.572864 million pixels. I have some worries about whether the device will have enough oomph to push those pixels, but we will burn that bridge when we get to it.

Like a good little Apple dev, I downloaded the newest XCode and iOS Simulator to test out my apps. The rest of the cocos2d community was doing the same and that is when the excrement came in contact with the bladed cooling device. Devs started to report that their apps were showing a black screen on the iPad 3 simulator. More reports were coming in of other strange behavior.

I had only tested out my current project when I first heard the buzz. It hadn't shown any problems but I was worried about my apps that were already for sale. I fired up every project and tested them one by one. They all behaved exactly as expected, but why? What was the difference between my apps and all of the apps that were having problems?

The Issue

The root of the problem was that the iPad 3 was behaving exactly as it should. Universal cocos2d applications that support the Retina Display on the iPhone often have this snippet of code in the app delegate:

The enableRetinaDisplay method attempts to turn on the Retina Display if the hardware supports it. Turns out that the iPad 3, with its Retina Display, activates the Retina Display when asked to do so. The problem is that since the rest of cocos2d and our apps aren't expecting this to happen on an iPad all sorts of craziness happens.

The good news is that Apple didn't go crazy and that things are behaving as one might expect. The bad news is that we have code built around an assumption that is now wrong. So why did my code work? Clearly I must not have had that handy snippet of Retina enabling code from above. WHAT?!?! I did? Oh, something else must be going on then.

Pixel Pushers

The solution to the problem was a bit of an accident. I wanted to create universal apps that took advantage of the iPad's resolution without needing new assets. With careful asset management and a few hacks it is easy to use your Retina iPhone assets on the iPad. This helps you reduce your app's footprint and take advantage of additional pixels. DOUBLE RAINBOW!

The method that I use is partially outlined in this forum post. It adds a makeUniversal method to CCDirector which you call immediately after instantiation of the director in your app delegate. Here is the code:

This method sets the __ccPointScaleFactor to 2. This sets the scaling between points and pixels. This also causes an escape to be skipped in enableRetinaDisplay display which causes the content scale factor to be updated to 2. The content scale factor is used by cocos2d to determine if the "-hd" assets should be used.

This has another side effect due to a check in updateContentScaleFactor which is indirectly called by the enableRetinaDisplay method:

If __ccContentScaleFactor is 2, then the method forces the display's scaleFactor to 1. This means that we don't actually activate the Retina Display even though we call enableRetinaDisplay. We are now safely using iPhone Retina assets on all (existing) models of the iPad. WOOOOOOOT!

But All Those Pixels!

You are right, this does not take advantage of all the extra pixels that the iPad 3 has available. You will need higher resolution assets to fully take advantage of the new iPad, so we need another approach. We can go over that in another post. (Edit: In fact, I just wrote up a post on how to make a truly universal iOS application here.)

Hey Cocos2d Noob - A Letter To Past Me: Part 3

If you missed them, catch Part 1 here, and Part 2 here.

Wow, it's been a long time. I shouldn't have left you. Without a new post to review. I was sucked into a new project that started off as a Ludum Dare Jam and then turned into a full game (check it out after you read this). I learned a lot going through the process and I recommend a game jam for any aspiring game dev. Unfortunately, adding a new project into the mix meant the next installment of this series was delayed. Time to dig back into my archive of horrible mistakes...

Filename@2x.png

When the retina display came out Apple gave us a simple way to automagically load retina assets instead of "SD" assets. Adding "@2x" to the retina version of the asset solved all our woes.

When I first came to cocos2d I was already in the habit of adding "@2x" to all of my retina assets, so I continued to do so. This is not the recommended naming convention for cocos2d because Apple's special handling of the "@2x" images can cause subtle bugs when interacting with cocos2d. Instead we should add "-hd" to all retina display images. Do this from the start and avoid having to unleash your command-line-fu to rename all your images. It is amazing how many issues can be avoided by just reading the documentation.

Another bonus of the "-hd" suffix is that it works for other asset types such as plists, fonts, and TMX files. If you need to load different assets for retina than for sd, then just slap an "-hd" on the end of the retina version.

Ticking Away The Moments

Within the main game loop a monster lies in wait. It will devour noob game developers indiscriminately. It doesn't matter if you are using cocos2d, another framework, or no framework at all. This monster's name: Time.

A very important thing to recognize is that not every step through the game loop will take the same amount of time. This is especially true as you move from one device for another.

A Story: You have carefully tweaked your monster speed to charge at your player at a rate of 10.698 units per update. The time through your update loops are variable, but you haven't caught on because they are always similar enough for the effect to go unnoticed. Now you load the game on your friend's shiny new iPhone 5 with it's awesomely powerful processor. Holy badunks! You can no longer react fast enough to escape the monsters! This game is impossible!

Since you used a fixed delta per game loop iteration and since the faster device can iterate more times per second, you have ended up with a game that no longer works as intended on a faster device. Don't feel too bad about it, many older games built for a specific platform suffer the same fate when run on newer hardware.

Luckily, this is an easy to solve problem if you address it from the start. Take the delta into account with each update and you will avoid this problem entirely. If you have a step method that looks like this:

-(void) step: (ccTime) delta;

Multiply the time delta by a velocity to determine the positional delta.

If you are using a physics system like chipmunk, then the delta should be passed into the step method (cpSpaceStep). The physics engine will then take care of all of the grunt work for you, but you still need to be careful with anything you update from outside the physics system.

Aside: There are two ways that physics engines can be updated, fixed step or variable step. There are pros and cons to each approach. Engines will facilitate one or both approaches. A good starting place to learn more is this thread on gamedev.stackexchange.

Shuffling

The last tip of the day comes in the form of some FREE code. The internet is just full of this stuff! (Always check the attached licenses) Odds are you will eventually need to shuffle the elements of an array. An easy way to do this is to create a category for NSMutableArray. This will give you a highly reusable piece of code, and then you won't have to write new shuffling code everywhere you need to randomize array elements.

A discussion on shuffling an NSMutable array using a category can be found over on StackOverflow. I have included my own slightly modified version below for ease of Copy-Paste.

52616e646f6d; 52616e646f6d

If you want to randomize an array the first time you generate it, and then want to be able to regenerate that same sequence again in the future then you can make a few modifications to the code above (or add a new method).

Replace:

int n = (arc4random() % nElements) + i;

With:

int n = (rand() % nElements) + i;

Then before calling shuffle, call srand() and pass in your saved seed value.

// Our first play of Taco Madness

srand(423452345);

[tacos shuffle];

// resulting order => beef, chicken, fish, pork, brain

 

// Replaying Taco Madness later using the same seed

srand(423452345);

[tacos shuffle];

// resulting order => beef, chicken, fish, pork, brain

// same results as first run

 

// Starting a new game of Taco Madness and therefore using a new seed

srand(76864432);

[tacos shuffle];

// resulting order => pork, chicken, brain, fish, beef

// new results due to new seed

This will guarantee that shuffling will return the same result as the last time you shuffled the same array with the same seed. They call them pseudo-random for reason.

Next Time

In the next installment I am going to go through an entirely new class of errors related to process of releasing a game on the App Store. It will be chock full of mistakes you will absolutely want to avoid.

Editor's Notes:

  1. Glad to see that tacos were used in an example. I was beginning to grow worried.
  2. A video of the Melbourne Shuffle… really?
  3. I am imaginary.
  4. Speaking of tacos: