Managing Local High Scores and Online Leaderboard for your iPhone Games Part 1/2

September 6, 2009

Why high scores? Because the stamp of public achievement rewards players for investing their time and skill in your game; and because the one-upsmanship of ranking promotes a competitive social awareness which can make a title more enduring.

iPhone Leaderboard

With the body of iPhone gaming so often reminiscent of the arcade era, an online leaderboard seems natural to the platform and something that adds a breath of life to an otherwise perishable experience. In this two-part tutorial, I’ll explain how to store and load local and online high scores for a game using only the native iPhone libraries and a Rails web service.

This first part will cover modeling and storing local high scores, and in part two we’ll build the leaderboard web service.

Existing Frameworks

A number of iPhone frameworks for leaderboards, analytics, and social media have been appearing recently and certainly warrant investigation if you’re really looking to dress up your game with community features.

Scoreloop
http://corporate.scoreloop.com

A free service, “adding the Scoreloop SDK to your game gives your users access to features such as challenges, buddies, and high score lists.”

OpenFeint
http://www.openfeint.com

An extremely feature-rich and free service offering social challenges, offline support, in-app friending, high scores, and social media APIs.

iGetScores
http://www.igetscores.com

Free and open source (code.google.com/p/igetscores) “online high scores / ranking service which is convenient for developers and transparent to game players.”

Agon Online
http://developer.agon-online.com
“AGON Online is a complete social platform for iPhone and iPod Touch games. It is a location-aware online high score system, complete with profiles, friends, awards etc. Think Xbox LIVE on-the-go.”

Geocade
http://geocade.com/company.html
Also free, “Geocade is the largest location aware social gaming platform with gaming communities in thousands of cities and towns across the world.”

Requirements

Although each of those services is promising in its own right, we’re going to develop our own leaderboard server here for a few practical reasons.

  1. Avoid dependencies on external libraries
  2. Minimize codebase and build size by staying in the iPhone SDK
  3. Control encryption and avoid App Store export restrictions
  4. Build a system that does precisely what’s needed, no more or less
  5. Keep our user analytics and personal data 2nd party
  6. Learn by doing!!!

Before moving into the code, let’s take stock of the requirements for local high score support.

First, we’ll need a high score class that can be serialized for storage. We’ll need to be able to store and retrieve the records on the iPhone, and we’ll also need to display the records in a UITableView. Finally, we’ll need to process new high scores so that they’re ranked while all low scores are rejected.

Modeling the High Score Classes

HighScoreRecord.h

#import <Foundation/Foundation.h>

@interface HighScoreRecord : NSObject <NSCoding, NSCopying> {
	NSString *name;
	NSNumber *totalScore;

	NSDate *dateRecorded;
}

- (id) initWithScore:(NSString *)name TotalScore:(NSNumber *)totalScore;

@property (nonatomic, retain) NSString *name;
@property (nonatomic, retain) NSNumber *totalScore;

@property (nonatomic, retain) NSDate *dateRecorded;
- (NSComparisonResult) compare:(id)other;
@end

Aside from the obvious properties, the HighScoreRecord class will need to implement NSCoding and NSCopyingfor serialization, and the NSComparisonResult method compare: so that it can be easily ranked against other HighScoreRecord instances.

HighScoreRecord.m

#import "HighScoreRecord.h"

@implementation HighScoreRecord

@synthesize name;
@synthesize totalScore;
@synthesize dateRecorded;

- (id) initWithScore:(NSString *)playerName TotalScore:(NSNumber *)score {
    if (self = [super init])
	{
		name = playerName;
		totalScore = score;

		dateRecorded = [NSDate date];
	}
    return self;
}

- (NSComparisonResult) compare:(id)other {
	return [self.totalScore compare:other];
}

#pragma mark NSCoding
- (void)encodeWithCoder:(NSCoder *)encoder {
	[encoder encodeObject:name forKey:@"Name"];
	[encoder encodeObject:totalScore forKey:@"TotalScore"];
	[encoder encodeObject:dateRecorded forKey:@"DateRecorded"];
}

- (id)initWithCoder:(NSCoder *)decoder {
	if(self = [super init]) {
		self.name = [decoder decodeObjectForKey:@"Name"];
		self.totalScore = [decoder decodeObjectForKey:@"TotalScore"];
		self.dateRecorded = [decoder decodeObjectForKey:@"DateRecorded"];
	}
	return self;
}

#pragma mark -
#pragma mark NSCopying
- (id)copyWithZone:(NSZone *)zone {
	HighScoreRecord *copy = [[[self class] allocWithZone:zone] init];
	name = [self.name copy];
	totalScore = [self.totalScore copy];
	dateRecorded = [self.dateRecorded copy];

	return copy;
}
#pragma mark -

- (void)dealloc {
	[name release];
	[totalScore release];
	[dateRecorded release];

    [super dealloc];
}

@end

We’ll also need a managing class that is responsible for storing and retrieving the scores, ranking them against each other, and, ultimately, communicating with the leaderboard server.

HighScores.h

#import <Foundation/Foundation.h>
#import "HighScoreRecord.h"

@interface HighScores : NSObject {}

+ (void)addNewHighScore:(HighScoreRecord *)score;
+ (void)saveLocalHighScores:(NSArray *)highScoreArray;

+ (NSString *)highScoresFilePath;
+ (NSMutableArray *)getLocalHighScores;
+ (NSMutableArray *)sortHighScoreDictionaryArray:(NSMutableArray *)highScoreArray;
@end

Look over the following implementation, and then we’ll walk through each of the methods in turn.

HighScores.m

#import "HighScores.h"

@implementation HighScores

const int HIGH_SCORE_COUNT = 10;

+ (void)addNewHighScore:(HighScoreRecord *)score {
	NSMutableArray *locals = [HighScores getLocalHighScores];

	int totalScore = [score.totalScore intValue];
	if (locals.count < HIGH_SCORE_COUNT){
		[locals addObject:score];
		NSMutableArray *sortedLocals = [HighScores sortHighScoreDictionaryArray:locals];
		[HighScores saveLocalHighScores:sortedLocals];
		[sortedLocals release];
	} else {
		NSUInteger lastIdx = HIGH_SCORE_COUNT-1;
		HighScoreRecord *lastRecord = [locals objectAtIndex:lastIdx];
		if (totalScore > [lastRecord.totalScore intValue]){
			[locals addObject:score];
			NSMutableArray *sortedLocals = [HighScores sortHighScoreDictionaryArray:locals];
			[sortedLocals removeLastObject];

			[HighScores saveLocalHighScores:sortedLocals];

			[sortedLocals release];
		}
	}
}

+ (NSString *)highScoresFilePath {
	NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
	NSString *documentsDirectory = [paths objectAtIndex:0];
	return [documentsDirectory stringByAppendingPathComponent:@"HighScoresFile"];
}

+ (NSMutableArray *)getLocalHighScores {
	NSData *data = [[NSMutableData alloc] initWithContentsOfFile:[HighScores highScoresFilePath]];
	NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];

	NSArray *highScores = [unarchiver decodeObjectForKey:@"HighScores"];

	return [[[NSMutableArray alloc] initWithArray:highScores copyItems:NO] autorelease];
}

+ (void)saveLocalHighScores:(NSArray *)highScoreArray {

	NSMutableData *data = [[NSMutableData alloc] init];
	NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];

	[archiver encodeObject:highScoreArray forKey:@"HighScores"];
	[archiver finishEncoding];

	[data writeToFile:[HighScores highScoresFilePath] atomically:YES];
	[archiver release];
	[data release];
}

+ (NSMutableArray *)sortHighScoreDictionaryArray:(NSMutableArray *)highScoreArray {

	NSString *SORT_KEY = @"totalScore";
	NSSortDescriptor *scoreDescriptor = [[[NSSortDescriptor alloc] initWithKey:SORT_KEY ascending:NO selector:@selector(compare:)] autorelease];
	NSArray *sortDescriptors = [NSArray arrayWithObjects:scoreDescriptor, nil];

	NSArray *sortedArray = [highScoreArray sortedArrayUsingDescriptors:sortDescriptors];

	return [[NSMutableArray alloc] initWithArray:sortedArray copyItems:NO];
}

@end

With our classes modeled, we can now examine how the storage and retrieval process actually works.

Storing the Data

In order to locally store our high scores, we’ll use the application’s NSDocumentDirectory, the recommended source for file management on iPhone apps. The records will simply be serialized to an NSArray of HighScoreRecord objects, which we can load and manipulate as needed.

Adding a score is called with addNewHighScore, which takes a HighScoreRecord and saves it if the score is high enough. To save a player’s score, have a method like so:

- (void)saveHighScore {
	int score = [somePlayerObject.score intValue];

	HighScoreRecord *highScore = [[HighScoreRecord alloc] initWithScore:name TotalScore:[NSNumber numberWithInt:score]];
	[HighScores addNewHighScore:highScore];

}

The addNewHighScore method is responsible for the controlling persistence logic, which includes a few simple rules. First, we should only store a maximum of HIGH_SCORE_COUNT records locally. Because of this constraint, only records that exceed the score of the last record in the collection will be saved; otherwise, they’re ignored. Once a new HighScoreRecord has been added to the high scores collection, it needs to be resorted and saved back to the file system.

The sortHighScoreDictionaryArray method is responsible for sorting the scores after a new record has been added to the collection. Using an NSSortDescriptor and the HighScoreRecord’s compare method, the scores are sorted on the totalScore property and a sorted array is returned.

Finally, saveLocalHighScores is called to persist the high scores collection. Its chief responsibility is serializing the HighScoreRecord objects to the NSDocumentDirectory.

Loading the Data

In the most common scenario you’ll be loading the high scores into a UITableView. The process is straightforward, amounting to deserializing the high scores file we’ve been saving.

The code in HighScoresViewController, which implements UITableViewDelegate and UITableViewDataSource, is quite standard. All the high score logic is handled by the other models, and populating a high scores display is simply a matter of making the right calls.

Note, since there won’t always be available local high score, cellForRowAtIndexPath first checks if a row exists and adds placeholder text if there aren’t enough scores to fill the table. In this case, we’ll set the UITableView to always show 10 high score slots with numberOfRowsInSection.

HighScoresViewController.h

#import <UIKit/UIKit.h>

#import "HighScoreCell.h"
#import "HighScoreRecord.h"
#import "HighScores.h"

@interface HighScoresViewController : UIViewController <UITableViewDelegate, UITableViewDataSource> {
	IBOutlet UITableView *highScoreTable;
	NSArray *highScoreData;

}
@property (nonatomic, retain) UITableView *highScoreTable;
@property (nonatomic, retain) NSArray *highScoreData;

- (void) loadLocalHighScores;

@end

HighScoresViewController.m

#import "HighScoresViewController.h"

@implementation HighScoresViewController

@synthesize scoreTableSelector;
@synthesize highScoreTable;
@synthesize highScoreData;

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
        [self loadLocalHighScores];
    }
    return self;
}

. . .

- (void)loadLocalHighScores {
	highScoreData = [[HighScores getLocalHighScores] retain];
}

. . . 

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
	return 10;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
	HighScoreCell *cell = (HighScoreCell *)[tableView dequeueReusableCellWithIdentifier: @"HighScoreCellIdentifier"];
	if (cell == nil) {
		NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"HighScoreCell" owner:self options:nil];
		cell = (HighScoreCell *)[nib objectAtIndex:0];
	}
	NSUInteger row = [indexPath row];
	cell.rankLabel.text = [NSString stringWithFormat:@"%d", row+1];

	if (row < highScoreData.count){
		HighScoreRecord *record = (HighScoreRecord *)[highScoreData objectAtIndex:row];

		cell.nameLabel.text = record.name;
		cell.scoreLabel.text = [NSString stringWithFormat:@"%@", record.totalScore];

		NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
		[dateFormat setDateFormat:@"yyyy-MM-dd"];

		cell.dateLabel.text = [dateFormat stringFromDate:record.dateRecorded];
	} else {
		cell.nameLabel.text = @"-";
		cell.scoreLabel.text = @"-";
		cell.dateLabel.text = @"-";
	}

	return cell;
}

. . . 

@end

You can note again that the responsibility for loading and deserializing the local high scores file rests with the HighScores class’s getLocalHighScores method. This is called by the HighScoresViewController in loadLocalHighScores.

+ (NSMutableArray *)getLocalHighScores {
	NSData *data = [[NSMutableData alloc] initWithContentsOfFile:[HighScores highScoresFilePath]];
	NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];

	NSArray *highScores = [unarchiver decodeObjectForKey:@"HighScores"];

	return [[[NSMutableArray alloc] initWithArray:highScores copyItems:NO] autorelease];
}


Part 2

In the second part of this tutorial, I walk through setting up a simple web service in Rails that will act as an online leaderboard, and we’ll apply some basic authentication to secure the high score submissions. And with git and heroku.com’s free Rails hosting, deploying the service will be about as frictionless as web development gets.

Tags: , ,

13 Responses

  1. [...] Read more here: Managing Local High Scores and Online Leaderboard for your iPhone … [...]

  2. [...] post:  Managing Local High Scores and Online Leaderboard for your iPhone … Make Your Baby Shower Games Standout: Free Printable Baby Shower …watch nfl football games [...]

  3. [...] This post was Twitted by PoppiLee00 [...]

  4. [...] rest is here:  Managing Local High Scores and Online Leaderboard for your iPhone … iPhone Games analysis ave-imperator creativity data development game game-development history [...]

  5. Great article! This was just the ideas I was looking for. Thanks.

  6. This is great – gave me some ideas about how to do this with a game we’re developing. Looking forward to more – specifically I’m interested in “best practice” with encryption and the App Store. Thanks again!

  7. [...] the first part this tutorial I demonstrated how to save player scores for an iPhone game. In this post, I’ll explain how to [...]

  8. Is there something missing the “#import “HighScoreCell.h”??

    Also, once added to our project, how do we call this set of functions?

  9. Hi Travis,

    Is there someplace to download the source for this tutorial? Also
    as noted above there seems to be a file missing.

    HighScoreCell.h

    Thanks.

    PS:
    Great Tutorial

  10. Apologies for omitting the HighScoreCell classes. It’s just there as a placeholder in case you wanted to do any fancy effects with the xib.

    * * *

    @interface HighScoreCell : UITableViewCell {
    IBOutlet UILabel *rankLabel;
    IBOutlet UILabel *nameLabel;
    IBOutlet UILabel *scoreLabel;
    IBOutlet UILabel *dateLabel;
    }

    @property (nonatomic, retain) UILabel *rankLabel;
    @property (nonatomic, retain) UILabel *nameLabel;
    @property (nonatomic, retain) UILabel *scoreLabel;
    @property (nonatomic, retain) UILabel *dateLabel;

    @end

    * * *

    #import “HighScoreCell.h”

    @implementation HighScoreCell

    @synthesize rankLabel;
    @synthesize nameLabel;
    @synthesize scoreLabel;
    @synthesize dateLabel;

    - (void)dealloc {
    [rankLabel release];
    [nameLabel release];
    [scoreLabel release];
    [dateLabel release];

    [super dealloc];
    }

    @end

  11. it gives me this error:
    Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: ‘UITableView dataSource must return a cell from tableView:cellForRowAtIndexPath:’

  12. And I wish there was sample source code to understand it better.

  13. Travis,

    I tried getting it to load into the table view and had nothing load but the “-” that would load if scores weren’t there. I look at the HighScoresFile and scores and names are there. Just won’t load in the table view. Can you help?

    http://itouchr.googlepages.com/Code.png
    http://itouchr.googlepages.com/HighScoresFile.png

    Thanks.

Leave a Reply

Powered by WP Hashcash