Defining the Achievements
First thing to do was figure out what achievements to make available. In a pinch I created 3 or 4 achievements for getting various levels of score in the game. Next, I put a lot of effort into stereo positioned sound for the game, so to encourage people to experience it I added an achievement for plugging in headphones. I also added a couple of hidden achievements for the "easter eggs" in the game. After that things got tougher, and I ended up just mucking around with the game and stumbling on a few ideas I liked. The full list of achievements I decided upon is as follows:
Name | Description | Hidden | Points | |
Karma | purchased the full game | Yes | 100 | |
Touchdown | used finger on splash screen | Yes | 50 | |
Poke the crab | poked the crab 100 times | Yes | 50 | |
Century Club | earned 100 points in a game | No | 25 | |
High Roller Club | earned 10,000 points in a game | No | 50 | |
100 Grand Club | earned 100,000 points in a game | No | 75 | |
Mile High Club | earned 1,000,000 points in a game | No | 100 | |
Palindrome | lit the signs in order 'B','A','L','L','S','S','L','L','A','B' | No | 50 | |
Stuck on you | cleared three fields of coconuts, hitting only the letter 'A' | No | 100 | |
Double Hitter | turned two signs on in one bounce, two signs off in one bounce, and turned one on and one off in one bounce, all in one game | No | 100 | |
Audiophile | plugged in headphones | No | 100 | |
Nausea | cleared two fields of coconuts, while playing upside down | No | 75 | |
Multiball Master | earned 1,000,000 in games with each of the types of ball | No | 75 | |
Luckily I had created the pelican icons a while back and finally have a use for them.
In total Balls Abound has 10 visible achievements and 3 hidden ones, for players to work towards. iTunes Connect is used to define the achievements in Game Center. Use iTunes Connect to add localizations, point values, icons, and other achievement properties. The identifiers you assign to the achievements defined in iTunes Connect are referenced from your code when obtaining GKAchievement objects, which are used to communicate progress to Game Center.
Tracking the Achievements
I am rushing to get this game finished, and I am the only person touching the codebase. With this in mind, my main goal is not the most beautiful design right now, I just need things to work.
I am already using Core Data to track some of the user settings in the game (muted, etc..) so I decided to model a new class in XCode to track achievements progress. Easy enough, now I have a new managed object that holds floats that represent the progress towards each of the achievements; they mirror the data stored in GameKit's GKAchievement objects. Another require for storing achievement data is, multiple Game Center users might play on the same device. So you need to track achievement progress on a per user basis. Adding a userId attribute to the UserAchievements managed object allows tracking achievements on a per user basis, creating one UserAchievements object for each userId. I create or attach the UserAchievements for the current user to my game settings whenever Game Center authentication is completed. I also use this as an opportunity to load the users current achievement status from Game Center, and reconcile any discrepancies between local and remote progress (using the rule, higher progress always wins).
Now that there is a data model capable of storing user achievement progress, three more things are required.
- an AchievementTracker that will monitor achievements and update the UserAchievements data
- an AchievementBoard UI to listen for UserAchievements changes and put notifications on screen
- the ScoreKeeper must listen for UserAchievements changes and report updates to Game Center
The AchievementTracker class is bound to all the points needed to listen on when detecting achievement progress. The AchievementTracker has the ability to track game state, coconut hits, sign hits, and accelerometer readings. This allows the AchievementTracker to decide when progress towards an achievement is made and update the UserSettings accordingly.
Listening for the Achievements
The ScoreKeeper and AchievementBoard both listen for achievement updates through KVO on the UserAchievements object. In order to achieve a reasonably performant option, I treat achievement progress internally as an int, rather than a double, which helps limit the number of updates that I have to process. Updates to achievement progress are relatively expensive, they trigger a KVO transaction which is followed by a managed context save and possibly network traffic through GameKit reporting through GKAchievement objects.
I decided to create my own UI for displaying achievements, the AchievementBoard. This was largely due to the fact that Apple only provide functionality for displaying achievements in iOS 5.0+ and I wanted to support iOS 4.2+.
I wanted a UI that would animate it's entrance and exit, was capable of handling multiple simultaneous achievement updates, and could display at top and bottom of the screen. The AchievementBoard holds a queue of achievement updates. If progress towards an achievement is made, it will either append the update into the AchievementBoard's queue, or replace the progress in an existing update for the same achievement. The AchievementBoard coordinates the onscreen display of achievement updates that are in it's queue. This system allows multiple achievements to be acted on simultaneously, with no notifications being ignored.
Coming up with graphics that tied into the game took a number of iterations of producing graphics and redeploying the game to test their visuals. I eventually came up with the following, which I think works well.
The Final Touches
Everything for achievements is plugged in and working. Except I'm suddenly down from a nice smooth 60fps to ~40fps in many parts of the game. I'll save the details for another post, but a day or two of work later and I am back to 60fps consistently (my solution involved clipping with glScissor and a hack to CCLabelBMFont). If only everything in life were so straightforward...
With achievements being monitored, updates being communicated to both Game Center and the display, there is just one more task before achievements can be considered fully integrated. There needs to be a button on the high score view so people can view their achievements in Game Center. Ideally I would create an in game view for achievements and leaderboards, but that is out of scope for now. So the quick solution is a simple button that opens a default GKAchievementViewController for displaying achievements. This leads to launching Inkscape and drawing a new icon for viewing achievements and updating the icon that already existed for viewing leaderboards. This in turn means running TexturePacker again, and finall PVRTextTool to generate my pvr with mipmaps. Anyway, achievements are finally present in Balls Abound.
Everything for achievements is plugged in and working. Except I'm suddenly down from a nice smooth 60fps to ~40fps in many parts of the game. I'll save the details for another post, but a day or two of work later and I am back to 60fps consistently (my solution involved clipping with glScissor and a hack to CCLabelBMFont). If only everything in life were so straightforward...