Sunday, October 21, 2012

Tutorial 22 - Building a Universal App in Codea

Figure 1.

22.1 Going Universal in Xcode


A universal application is defined as one targeted at both the iPhone/iPod touch and iPad. If you want to build a universal iOS application then you have to use the runtime and Xcode. There is currently no way to target iPhone development from within Codea.

This is handy, since we plan to use our old staple Mine Sweeper to demonstrate how to turn your Codea App into one which can run on an iPhone (or iPod Touch for that matter). As we have added Game Center code to MineSweeper, all future development of this game will have to occur in Xcode.

In principle this is a simple three step process:
  1. Target the application to iPhone/iPad in the Xcode deployment build settings;
  2. Add the relevant iPhone graphical assets (icons, default images etc.);
  3. Use a bunch of if (device is an iPhone) then do one thing else do another thing. Step 2 is simplified by Apple providing a new macro called UI_USER_INTERFACE_IDIOM() that will tell you what device your code is currently running on. There are two values defined, UIUserInterfaceIdiomPhone and UIUserInterfaceIdiomPad, and this macro will return the appropriate value based on the current device.
It would be a very short tutorial if that was all there was to it. If your App has any sort of complexity then there are a few things that you need to watch out for.

22.2 Step 1: Targeting iPhone and iPad in Xcode


As with the previous few tutorials, we are assuming that you have already got your Codea App up and humming in the Xcode iPad simulator using the runtime. If you haven't got to this state then go back and have a look at Tutorial 12.

Targeting your App for iPhone and iPad is easy one you have found the setting. Open up Xcode with your App. You need to have the Project Navigator open (top left hand tool bar item - it looks like a file folder). Click on the item at the very top of the Navigator called "Codea Template."

When you create a new project, it includes one or more targets, where each target specifies one build product and the instructions for how that product is to be built. You can use the project editor to specify every aspect of the build, from the version of the SDK to specific compiler options.

In the next column over you will see two items, Project (CodeaTemplate) and Targets (MineSweeper - in our case, this will be the name of your App). 

Selecting the project file opens the project settings editor. The project settings editor is Xcode 4′s replacement for Xcode 3′s project and target inspectors. Select the target from the project settings editor. 

Figure 2. Setting your App to Universal.

To specify the device families on which you want your app to be able to run:

  1. In the project navigator, select the project. 
  2. From the target list in the project editor, select the target that builds your app, and click Summary. 
  3. From the Devices pop-up menu, choose Universal (to target both families - iPhone and iPad are the other two options - see Figure 2.).
And that is it. You should now be able to select either iPhone or iPad from the simulator. It is unlikely that the iPhone version will work but you can give it a shot if you want. MineSweeper certainly crashed and burned. The app should still compile and run ok as an iPad app.

22.3 Step 2: Adding iPhone Graphical Assets


Once you have made your app universal, Xcode will allow you to set up the icon and default images for your iPhone. Before we do this, here is a quick refresher on the screen resolutions that we need to consider. 

iPad:
  • 1024 x 768 px; and
  • 2048 x 1536 px for Retina displays (@2x).
iPhone:
  • 320 x 480 px;
  • 640 x 960 px for Retina displays (@2x); and
  • 640 x 1136 px for the iPhone 5 and iPod touch (5th Generation).
You will also need an icon in the 57 x 57 pixel size and 114 x 114 pixels (@2x for the retina version). These are fairly easy to scale from your iPad icons.

You can also use your  default iPad images for your iPhone but they won't scale exactly (you will need to crop either the width or height) if you want to maintain the aspect ratio.

The easiest way to add the iPhone versions of your icon and default load images is to drag them from Finder onto the deployment information page in Xcode. To bring up this page, select the CodeaTemplate at the top of the left hand Project Navigator pane. Click on Targets (Minesweeper in our case) in the middle pane and Summary in the far right pane (see Figure 3.)

Figure 3. Adding the iPhone Graphical Assets

Once you have the screen shown in Figure 3, you can drag across the appropriate image files from Finder. If necessary, Xcode will rename the files and copy them into your project. You should now be able to run your application in the iPhone simulator and see the default image load and when you exit the app using the home button you will be able to see the appropriate icon. That may be all you need to do, but probably not.

22.4 Step 3: Code Modifications


To get our App running on the iPhone we need to make some changes. What you have to watch out for is any hard coded view sizes. Some of our graphical assets will have to be redone to suit the dimensions of the iPhone screen. Let's start by getting the splash screen to work.

In the CodifyAppDelegate of the runtime the base app window is created when the application finishes launching. The good news is that the following line of code will create a window which will fit either an iPhone or iPad.

self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];

So there is nothing we need to do from a runtime perspective (unless you are using Game Center - see below). If we did need to modify anything in the Objective C portion of our code we could use the following pattern:

// Check if device is an iPhone or iPad
// We now also need to check whether we have a iPhone 5 
// or 5th Generation iPod touch.
    
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
        NSLog(@"App Delegate - iPad device detected.");
    else if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
    {
        CGSize result = [[UIScreen mainScreen] bounds].size;
        if (result.height == 480)
        {
            NSLog(@"App Delegate - iPhone/iPod Touch detected (< v5).");
        }
        if (result.height == 568)
        {
            NSLog(@"App Delegate - iPhone/iPod Touch detected (> v5).");
        }
    }

Paste a copy of this in the - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions method of the CodifyAppDelegate.m file if you want to try it out.

Now we do have to modify our Lua splash screen class as it loads background images which are too large for the iPhone screen. To assist with the conditional coding we need to add, we wrote a new helper function for the Lua portion of the code to determine what device we are running on. This was added to the Main tab, so that we know it will be available throughout the rest of the code. Depending on what resolution the forthcoming iPad mini ends up with, we might need to update this in the next couple of weeks. 

-- This function will detect the Device which is in use, 
-- required as our app is now universal

function deviceIsIpad()

    if CurrentOrientation == LANDSCAPE_LEFT 

      or CurrentOrientation == LANDSCAPE_RIGHT then
        if WIDTH == 1024 then
            return true
        else
            return false
        end
    else
        if WIDTH == 768 then
            return true
        else
            return false
        end
    end
    
end

As the device isn't going to change mid program, you can call this function once and assign it to a global boolean if you are worried about performance. Performance isn't an issue for MineSweeper so we wont bother.

Using this function we can now conditionally load the background image depending on the device. In the function SplashScreen:init(splashText, fadeSpeed) we have modified the sprite loading as follows:

    if deviceIsIpad() then
        landscapeImage = readImage("Dropbox:MS_TitleScreen_1024x768")
        portraitImage = readImage("Dropbox:MS_TitleScreen_768x1024")
    else
        landscapeImage = readImage("Dropbox:Default-Landscape~iphone")
        portraitImage = readImage("Dropbox:Default-Portrait~iphone")
    end

Remember that if you need to add new images to your Dropbox folder on the Mac then you will have to do a clean build for Xcode to recognise them. If you don't do this you will get a sprite loading error. To find the folder on your Mac to add the images, right click on the Drop Box sprite pack folder in the Xcode Project Navigator (Frameworks -> Codea -> SpritePacks) and select "Show in Finder". You can then drag across the new images that you want to use.

The other change that we need to make is in the function SplashScreen:draw().

        if deviceIsIpad() then
            sprite(logoImage, WIDTH - logoImage.width - 20, 20)
            text(self.splashText, WIDTH/2, HEIGHT/2 + 250 + portraitTextOffset)
            fill(whiteColor)
            fontSize(20)
            text("Version: "..version, WIDTH/2, HEIGHT/2 + 200 + portraitTextOffset)
        else
            fontSize(42)
            text(self.splashText, WIDTH/2, HEIGHT - 80)
            fill(whiteColor)
            fontSize(14)
            text("Version: "..version, WIDTH/2, 50)
        end

You will probably need to make similar adjustments in font size and composition to suit the smaller iPhone screen We also don't load the Reefwing Software logo if the device is an iPhone due to the space constraints. Make sure that the resulting view looks okay in both landscape and portrait orientations (command right arrow to change the orientation of the simulator in Xcode). As an aside you should find that the Fader class works as expected regardless of the device.


Now that you have the splash screen sorted we can turn our attention to the Menu screen. For us the changes here are minimal for the portrait orientation. We load up a smaller MineSweeper logo to fit on the iPhone screen and once again there is no room for the Reefwing and Codea logos so we dispense with them. The RoundBorder class works on all devices without any changes.

In the Main setup function where we load images we added:

-- Load the MineSweeper Text Logo
    
    if deviceIsIpad() then
        msTextLogo = readImage("Dropbox:minesweeper_logo 400x76px")
    else
        msTextLogo = readImage("Dropbox:minesweeper_logo 200x38px")
    end

And then in drawMenu() we shuffle the button positions and eliminate the "Medium" and "Hard" game options since those grid sizes wont fit on an iPhone screen (see section 22.5 Lessons Learned). The upside of removing the "Medium" and "Hard" buttons is that the remaining three buttons fit easily on the iPhone screen in both portrait and landscape orientations. Since there is only one game difficulty available on the iPhone we also changed the button text from "Easy" to "Start".


The achievement and leader board Game Center functionality works fine on an iPad but due to the way these modal views are displayed, on an iPhone the Lua draw() loop gets stopped if you tap the "Achievements" or "High Score" buttons. This results in the app still running but the screen never gets refreshed. To prevent this from occurring we need to modify the aGameCenter_Codea.m file that we introduced in the previous Game Center tutorial. The fix is to call [[SharedRenderer renderer] startAnimation] when the Game Center view controller is dismissed. This method is in the BasicRendererViewController of the runtime. An example  for dismissing achievements is shown below. The same is required for dismissing leader boards.

- (void)achievementViewControllerDidFinish:(GKAchievementViewController *)viewController
{
    [[SharedRenderer renderer] dismissViewControllerAnimated:YES completion:nil];
    [[SharedRenderer renderer] startAnimation];
}

This isn't a problem on the iPad because the Game Center view doesn't cover the whole screen (which causes the viewWillDisappear method to be called in the rendering code and stops animating the draw loop).

We have two more screens that we need to tweak and then we are done. Firstly the High Score screen which shows the scores stored locally is not particularly useful on the iPhone version (and is somewhat redundant as we are now using Game Center). So in the function scoreButtonPressed() we wont show this screen if we are an iPhone using the following code.

if deviceIsIpad() then
        gameState = stateScore
end

Remember that the gameState determines what the draw() loop displays. By not setting this to stateScore if the device is an iPhone, this screen wont be displayed.


Our last job is to sort out the game playing screen. The grid display is platform agnostic so it is only the button and text placement that we need to worry about. In the function drawCellsLeftDisplay() we do some more conditional placement.

    local iPhoneHeightAdjustment = 0

    if CurrentOrientation == LANDSCAPE_LEFT
      or CurrentOrientation == LANDSCAPE_RIGHT then
        iPhoneHeightAdjustment = 50
    end



    if deviceIsIpad() then
        text(cellsLeft, w + 50, HEIGHT - 100)
        text(math.round(gameTime), WIDTH - 150, HEIGHT - 100)
    else 
        text(cellsLeft, w - 30, HEIGHT - 100 + iPhoneHeightAdjustment)
        text(math.round(gameTime), WIDTH - 70, HEIGHT - 100 + iPhoneHeightAdjustment)
    end

The buttons on the iPhone screen will need to be placed based on the current orientation due to the space constraints. We do this in the drawGameButtons() function.

function drawGameButtons()
    
    -- These are the buttons visible on the game run screen
    -- Adjust the button position based on the device and orientation.
    -- In particular, we need to move the button location
    -- if the current device is an iPhone/iPod touch.
    
    local mButtonSize = vec2(100, 50)
    local mLocX, mLocY = 0, 0

    if CurrentOrientation == LANDSCAPE_LEFT 
      or CurrentOrientation == LANDSCAPE_RIGHT then
        if deviceIsIpad() then
            mLocX = WIDTH - 200
            mLocY = HEIGHT - 195
        else
            mLocX = WIDTH - 200 + 90
            mLocY = HEIGHT - 195 - 100
        end
    else
        if deviceIsIpad() then
            mLocX = WIDTH - 125
            mLocY = HEIGHT - 320
        else
            mLocX = WIDTH - 120
            mLocY = HEIGHT - 320 - 140
        end
    end
    
    flagButton.x, flagButton.y = mLocX, mLocY
    
    if CurrentOrientation == LANDSCAPE_LEFT 
      or CurrentOrientation == LANDSCAPE_RIGHT then
        if deviceIsIpad() then
            mLocY = 110 + mButtonSize.y*2
        else
            mLocY = 110 + mButtonSize.y
        end
    else
        if deviceIsIpad() then
            mLocY = 240 + mButtonSize.y*2
        else
            mLocX = 20
        end
    end
    
    newGameButton.x, newGameButton.y = mLocX, mLocY
    
    if CurrentOrientation == LANDSCAPE_LEFT 
      or CurrentOrientation == LANDSCAPE_RIGHT then
        if deviceIsIpad() then
            mLocY = 110
        else
            mLocY = 90
        end
    else
        if deviceIsIpad() then
            mLocY = 240
        else
            mLocX = WIDTH / 2 - mButtonSize.x/2
            mLocY = HEIGHT - mButtonSize.y - 10
        end
    end    
        
    menuButton.x, menuButton.y = mLocX, mLocY
    
    flagButton:draw()
    newGameButton:draw()
    menuButton:draw()
    
end



22.5 Lessons Learned


Converting to a universal App was easier than we were expecting. The Codea runtime works out of the box without modification so it is only your Lua code and graphical assets which need to be modified.

For us the most important lesson is that if you want you App to eventually be universal then design it to work on an iPhone/iPod sized screen (320 x 480 pixels) first. It is much easier to convert this to work on a larger screen than vice versa.

The other lesson is to avoid using hard coded co-ordinates if possible and use positions relative to the WIDTH and HEIGHT constants. This will make conversion between different screen sizes much easier and in many cases wont require any changes to your code at all.

Finally if your app includes Game Center functionality then you need to modify the methods which dismiss the Game Center view controllers to restart the Lua draw() loop.



22.6 Download the Code


You can download the updated code below. For simplicity we have provided all the classes even if they haven't changed from a previous version. You should be able to tell from the version number whether you need to download the code or can use a previous version (v1.6 indicates code that was modified for this tutorial).

Lua Code:
  1. Main v1.6
  2. Cell v1.5
  3. Colors v1.1
  4. Physics v1.0
  5. PhysicsDebugDraw v1.0
  6. RoundBorder v1.2
  7. ScoreScreen v1.0
  8. SplashScreen v1.6
  9. Button v1.3
  10. Fader v1.1
  11. IconImages v1.1
  12. TextBox v1.0
  13. Twinkle v1.1
Runtime (Objective C) Code:

Friday, October 19, 2012

Tutorial 21 - Integrating Game Center (Part 3)



21.1 On the Shoulders of Giants...


Juan Bel√≥n (@juaxix over on the Codea Forums) has already done a lot of the hard work required to get Game Center up and running on your Codea App. If all you need is to submit scores to a single leaderboard then just follow Juan's instructions at this post. Juan has also provided the ability to play and stop mp3's from a Codea App.

We want to be able to submit scores to multiple leaderboards (Easy, Medium and Hard) and incorporate achievements so we need to add to the code that Juan has generously contributed. To this end we will fork his Game Center class on GitHub.

We will start by updating the Codea Runtime Objective C code.

21.2 Modifications to LuaState


In Xcode, make sure that you are showing the Project Navigator (top left button on the tool bar). Open up the Frameworks group, then Codea -> Backend. In Backend, click on the LuaState.m file. Within this file you will see a method called - (void)create{ }. To this method add the following:

//  Game Center Functions:
    LuaRegFunc(aGameCenter_start);
    LuaRegFunc(aGameCenter_isGameCenterAvailable);
    LuaRegFunc(aGameCenter_showEasyLeaderboard);
    LuaRegFunc(aGameCenter_showMediumLeaderboard);
    LuaRegFunc(aGameCenter_showHardLeaderboard);
    LuaRegFunc(aGameCenter_reportEasyScore);
    LuaRegFunc(aGameCenter_reportMediumScore);
    LuaRegFunc(aGameCenter_reportHardScore);
    LuaRegFunc(aGameCenter_reportAchievementIdentifier);
    LuaRegFunc(aGameCenter_showAchievements);
    LuaRegFunc(aGameCenter_resetAchievements);

//  Play Music
    LuaRegFunc(playMusic);
    LuaRegFunc(stopMusic);


21.3 Modifications to OSCommands


In the same directory as LuaState you will see OSCommands.h and OSCommands.m, click on the header file OSCommands.h and after int alert(struct lua_State *L) add the following:

//  Game Center:
    int aGameCenter_start(struct lua_State *state);
    int aGameCenter_isGameCenterAvailable(struct lua_State *state);
    int aGameCenter_showEasyLeaderboard(struct lua_State *state);
    int aGameCenter_showMediumLeaderboard(struct lua_State *state);
    int aGameCenter_showHardLeaderboard(struct lua_State *state);
    int aGameCenter_reportEasyScore(struct lua_State *state);
    int aGameCenter_reportMediumScore(struct lua_State *state);
    int aGameCenter_reportHardScore(struct lua_State *state);
    int aGameCenter_reportAchievementIdentifier(struct lua_State *state);
    int aGameCenter_showAchievements(struct lua_State *state);
    int aGameCenter_resetAchievements(struct lua_State *state);
  //  Play Music
    int playMusic(struct lua_State *L);
    int stopMusic(struct lua_State *L);

Then in the implementation file, OSCommands.m add the following:

static aGameCenter_Codea *CodeaGameCenter;

int aGameCenter_start(struct lua_State *state){
    NSLog(@"Starting Game Center");
    if (CodeaGameCenter==nil){
        CodeaGameCenter = [[[aGameCenter_Codea alloc] init] autorelease];
    }

    [CodeaGameCenter start];
    return 0;
}

int aGameCenter_isGameCenterAvailable(struct lua_State *state){
    return [CodeaGameCenter isGameCenterAvailable];
}

int aGameCenter_showLeaderboard(struct lua_State *state) {
    [CodeaGameCenter showLeaderboard];
    return 0;
}

int aGameCenter_reportEasyScore(struct lua_State *state) {
    [CodeaGameCenter reportEasyScore:lua_tonumber(state,1)];
    return 0;
}

int aGameCenter_reportMediumScore(struct lua_State *state) {
    [CodeaGameCenter reportMediumScore:lua_tonumber(state,1)];
    return 0;
}

int aGameCenter_reportHardScore(struct lua_State *state) {
    [CodeaGameCenter reportHardScore:lua_tonumber(state,1)];
    return 0;
}

int aGameCenter_reportAchievementIdentifier(struct lua_State *state) {
    [CodeaGameCenter reportAchievementIdentifier:lua_tonumber(state,1)];
    return 0;
}

int aGameCenter_showAchievements(struct lua_State *state) {
    [CodeaGameCenter showAchievements];
    return 0;
}

int aGameCenter_resetAchievements(struct lua_State *state) {
    [CodeaGameCenter resetAchievements];
    return 0;
}

int playMusic(struct lua_State *L){
    [CodeaGameCenter playMusic:lua_tonumber(L,1)];
    return 0;
}

int stopMusic(struct lua_State *L){
    [CodeaGameCenter stopMusic];
    return 0;
}


21.4 Modifications to the aGameCenter_Codea Class


The final change in Xcode is to add the updated version of the aGameCenter_Codea class. You can download the forked version of Juan's code here. Add these two files to the Supporting Files group in the runtime.

21.5 Modifications to your Codea App

  
Once you start adding the Game Center functionality you wont be able to compile and run your App in Codea anymore (since these functions are only defined in the runtime). Consequently, debug all of your code apart from the Game Center specific parts before you make the following modifications. 

After you are in Xcode and using the runtime, you can still update your Lua code but you need to force the runtime to reload the Lua files into the documents directory. You can do this by changing the version number in the Lua info.plist which can be found in Project.codea. 

This number just needs to be different (not necessarily larger) to what it was the last time you compiled, to force an upload. Alternatively, if you get sick of doing this each run-test cycle (like we did), you can go into the CodifyAppDelegate.m file, find the - (BOOL) migrateProjectAtPath:(NSString*)path toPath:(NSString*)destPath method and comment out the following code as shown below.
  
// NSString* oldVersion = [self versionForProjectAtPath:destPath];
// NSString* newVersion = [self versionForProjectAtPath:path];

// if ([oldVersion isEqualToString:newVersion]) 

//    {
//        return YES;
//    }

Should you need to add sprites, you can save these directly into the dropbox folders in the runtime but you may need to do a clean build (select Product from the menu bar and then Clean) before Xcode will see them.

As part of the Game Center update of MineSweeper (v1.5) we took the opportunity to tweak the code based on feedback from the Codea Forum. In particular:
  1. There is a new MineSweeper logo on the Menu screen courtesy of @derhannes (you can see their web site at: http://www.boba-soft.com).
  2. The falling mines on the Menu Screen now start dropping from above the screen which makes the animation look smoother. Thanks to @Fred for this suggestion.
  3. The textBox used to enter the players name, if a new high score is achieved, now handles a changing orientation (e.g. Portrait to Landscape). Thanks to @West for finding this bug.

Once you have added all the code above, the actual Game Center implementation in Lua is very simple. In the Setup() function in Main add the following code:


-- Initialise Game Center if it is available (requires a device running iOS 4.1
-- or later and the App needs to be running within the Codea run time).
    
    aGameCenter_start()
    if aGameCenter_isGameCenterAvailable() then
        print("Game Center Started.")
    else
        print("Game Center not available")
    end

Then you just need to report your scores and achievements as they happen. For example in MineSweeper when the game is won we do the following achievement checks:

-- Check if any Achievements were completed once game
-- has been won.
        
        if gameDifficulty == stateEasy then
            if readLocalData("easyWinner") == nil then
                saveLocalData("easyWinner", "YES")
                aGameCenter_reportAchievementIdentifier(1)
                print("Easy Winner Achievement.")
            end
        end
        
        if gameDifficulty == stateMedium then
            if readLocalData("mediumWinner") == nil then
                saveLocalData("mediumWinner", "YES")
                aGameCenter_reportAchievementIdentifier(2)
                print("Medium Winner Achievement.")
            end
        end
        
        if gameDifficulty == stateHard then
            if readLocalData("hardWinner") == nil then
                saveLocalData("hardWinner", "YES")
                aGameCenter_reportAchievementIdentifier(3)
                print("Hard Winner Achievement.")
            end
        end
        
        if readLocalData("gamesPlayed") == nil then
            gamesPlayed = 0
        else
            gamesPlayed = readLocalData("gamesPlayed")
        end
        
        gamesPlayed = gamesPlayed + 1
        saveLocalData("gamesPlayed", gamesPlayed)
        if gamesPlayed == 10 then
            if readLocalData("decathlon") == nil then
                saveLocalData("decathlon", "YES")
                aGameCenter_reportAchievementIdentifier(6)
                print("Decathlon Achievement.")
            end
        end
        
        if gamesPlayed == 100 then
            if readLocalData("centurion") == nil then
                saveLocalData("centurion", "YES")
                aGameCenter_reportAchievementIdentifier(7)
                print("Centurion Achievement.")
            end
        end

To save high scores onto the relevant leader board we updated the saveHighScore function as shown below.

function saveHighScore(d)
    
    -- Build the high score data into a string which is saved
    -- using saveLocalData(key, value). Also save gameTime as
    -- the score on Game Center for the appropriate leader board
    -- (easy, medium or hard).
    --
    -- n = playerName
    -- t = gameTime
    -- d = os.date() [current date] not used in this version
    
    playerName = textBox.text
    print("New High Score by: "..playerName)
    
    local hsDataString = string.format("return {\"%s\", %d}", playerName, gameTime)
    
    if gameDifficulty == stateEasy then
        saveLocalData("easyHighScore", hsDataString)
        aGameCenter_reportEasyScore(gameTime)
    elseif gameDifficulty == stateMedium then
        saveLocalData("mediumHighScore", hsDataString)
        aGameCenter_reportMediumScore(gameTime)
    elseif gameDifficulty == stateHard then
        saveLocalData("hardHighScore", hsDataString)
        aGameCenter_reportHardScore(gameTime)
    end
    
    hideKeyboard()
    highScoreSaved = true
    
end

Thursday, October 4, 2012

Tutorial 20 - Integrating Game Centre (Part 2)

20.1 Scores and Achievements


Before getting too much further we need to decide what scores and achievements that you want to have for your game. For MineSweeper we will keep things fairly simple. We will set up a leader board for each game difficulty (easy, medium and hard) and keep track of the following achievements:
  • Easy Winner (25 points) - Won an Easy Difficulty Game;
  • Medium Winner (50 points) - Won a Medium Difficulty Game;
  • Hard Winner (100 points) - Won a Hard Difficulty Game;
  • Boom (25 points) - tapped a mine;
  • Bad Luck (50 points) - tapped a mine on your first move;
  • Decathlon (50 points) - play 10 games of any difficulty; and
  • Centurion (100 points) - play 100 games of any difficulty.

20.2 iTunes Connect - Leaderboards



To set up the metadata for your App, log into your iOS developer account and go to iTunes Connect. We are assuming that you have already set up your App ID and provisioning profiles. If you haven't, look at Tutorials 12 and 13. 

Click on the Manage your Applications link and then click on the App icon that you want to set up for Game Center. On the App Information screen you will see a button in the top right called Manage Game Center. Click on this and then enable your game (as either a single game or part of a group of games which shares scores and achievements). For MineSweeper we will just enable it as a single game, you can always change it to a group game later if you wish.

From the Game Center screen you can add leader boards and achievements. Remember that Leader boards which are live for any app version cannot be removed. This is true for achievements as well.

Click on the Add Leaderboard Button and then select add Single Leaderboard (You cannot create a combined leaderboard until you have two or more single leaderboards with the same score format type and sort order).


Figure 1. Add Language Screen.

Fill out the information for your leader board. The leaderboard reference name is an internal name that you must provide for each leaderboard. It is the name you should use if you search for the leaderboard within iTunes Connect. We used "Easy Difficulty" for our first leaderboard reference name.

The Leaderboard ID is a unique alphanumeric identifier that you create for this leaderboard. It can contain periods and underscores. We used the reverse URL style for ours (e.g. au.com.reefwing.minesweeper.easyDifficulty).

Then choose the score format for this app leaderboard and choose "High to Low" if you want highest scores displayed first or choose "Low to High" if you want the lowest scores displayed first. We select "Low to High" because a lowering your time to solve the grid is the objective.

Optionally, you can define the score range using 64-bit signed integers. The values must be between the long min (-2^63) and long max (2^63 - 1). Any scores outside of this range will be deleted. We don't define a score range for MineSweeper.

You must add at least one language for your leader board. For each language, you will have to provide a score format and a leaderboard name. Click on the Add Language button to bring up the screen to do this (see Figure 1).

The score format suffix will be added to the end of scores displayed on your leaderboard. Use this field to specify a singular suffix. This is optional, and is useful for clarifying the type of score your app uses. Examples include "point", "coin", or "hit".

Finally, you can assign an image to your leaderboard. The image must be a .jpeg, .jpg, .tif, .tiff, or .png file that is 512x512 or 1024x1024 pixels, at least 72 DPI, and in the RGB color space. Click the Save button and you are done.


Figure 2. Add an Achievement.

20.3 iTunes Connect - Achievements


Creating achievements is very similar to leaderboards. Click on the "Add Achievement" button to get started (Figure 2).

The Achievement Reference Name is an internal name that you must provide for each achievement. It is the name you should use if you search for the achievement within iTunes Connect (e.g. Easy Winner).

The Achievement ID is a unique alphanumeric identifier that you create for this achievement. It can contain periods and underscores. Once again we used the reverse URL style for ours (e.g. au.com.reefwing.minesweeper.easyWinner).

Point Value is the amount of points your achievement is worth. There is a maximum of 100 points per achievement and 1000 points for all achievements combined. The points we assigned to each achievement are shown in section 20.1. You will see that we have left lots of spare points in case we want to add additional achievements in the future.

Achievements marked as Hidden will remain hidden on Game Center until a player has achieved them. We wont hide any of our achievements.

For the "Achievable More Than Once" field, If you select Yes, users can accept Game Center challenges for achievements they have already earned.

Once you have filled out the above metadata for your App you need to add at least one language. For each language you need to add:

  • Title: The localized title of this achievement as you would like it to appear in Game Center (e.g. Easy Winner).
  • Pre-earned Description: The description of your achievement as it will appear to a Game Center user before they have earned it (e.g. Win at least one Mine Sweeper game on Easy Difficulty).
  • Earned Description: The description of your achievement as it will appear to a Game Center user after they have earned it (e.g. Won at least one Mine Sweeper game on Easy Difficulty).
  • An image for your achievement. The image must be a .jpeg, .jpg, .tif, .tiff, or .png file that is 512x512 or 1024x1024 pixels, at least 72 DPI, and in the RGB color space.
Click the Save Button and you are finished. Rinse and repeat for each one of your achievements.

In part 3 of the tutorial we will detail the code required in Lua and Objective C to tie these leaderboards and achievements to your game.