Mind Juice
  • Email
  • Twitter
  • Rss
  • About
  • Blog
Home» iDevBlogADay » Simple Stats Tracker for iOS Games

Simple Stats Tracker for iOS Games

Posted on August 14, 2011 by kenc in iDevBlogADay 15 Comments

This week I wrote some code to add statistics to Unicorn Rush.  Basically I wanted to keep track of things like distance run, number of jumps, number of fireballs shot, etc.  I will use these stats later to present the user with specific objectives within the game and reward him/her for reaching them.

Conceptually, tracking stats is pretty simple:

  • Keep a collection of some numbers
  • Give a way to change the numbers
  • Be able to track these number on a per-game basis and to roll the game stats into a set of total stats
  • Provide a persistence mechanism to save and load the numbers between games

It seemed simple enough, so I didn’t do any design before I started coding.  I just dove write into Xcode and started typing.  As you might expect, this was a big mistake, and I know better than to do this, but there you have it.

Anyway, I’ll share with you the process I went through below, and you can download the Stats code (under the very permissive MIT license), including a simple demo project, at the end of this post.

The Long And Winding Road

I ended up going through three near-complete rewrites before I was happy with the code.

The first time, I tried to combine objectives and statistics into a single class and things just got very ugly and messy.  Also, I had done something silly and created functions for every stat like getNumJumps, getNumGames, getNumShotsFired, etc.  Clearly this would not result in a nice reusable class, so I threw it all away and started again.  Kind of embarrassed that I even thought about this.

The second time, I ensured that the Stats class was generic.  The developer would call registerStat to add a new statistic, and then call beginTracking once all stats had been registered.  The developer could also #define a string for each stat to be used as a dictionary key and a tag name in the plist file used for persistence.

#define STAT_JUMPS    @"Jumps"

The main thing I didn’t like in this revision was that I had combined the stats for the current game with total stats across all games.  This resulted in an API with methods like get and getTotalStat, reset and resetTotal, etc.  I did not like this duplication of methods, and realized that many games might choose to use stats in a different way.  This led me to refactor the code so that current stats are tracked with one instance of the Stats class and total stats are tracked with a second instance.

After that refactoring, I realized that I had inadvertently introduced a somewhat nasty requirement due to the fact that I wanted the developer to be able to access the stats as efficiently as possible by a direct lookup in array of stat values.

The developer would need to write code to register each statistic and give each one a unique string name, which was fine.  However, in order to access the statistic using a method like the following:

- (void) incrementByIndex:(int)statIndex
{
    statValues[statIndex] += 1;
}

the developer would also need to create a #define whose value corresponded to the order in which the stats were declared or create a variable and get the index of the stat at runtime.

The first stat would be #defined as 0, the second as 1, etc.  I really did not want to have to write two #defines for each stat like the following:

#define STAT_JUMPS           0
#define STAT_SCORE           1

#define STAT_JUMPS_KEY_NAME  @"Jumps"
#define STAT_SCORE           @"Score"

To avoid the need to do this, I decide to using the string stat name to manage all updates to the stat, at least as the nice simple default.  Here, for example, is the increment method. Note that you pass in statName as a string, not an index.

- (void) increment:(NSString*)statName
{
    int statIndex = [self getIndex:statName];
    if (statIndex        return;

    statValues[statIndex] += 1;
}

Enough Already! Show Me the Final API!

OK, OK…here is the main interface of Stats.h:

// Initialization
- (id) initWithFilename:(NSString*)theFilename;
- (void) registerStat:(NSString*)name;
- (void) beginTracking;

// Persistence
- (void) save;
- (void) load;
- (void) reset;

// Stats modifiers by Name
- (void) increment:(NSString*)statName;
- (void) decrement:(NSString*)statName;
- (void) add:(int64_t)amount toStat:(NSString*)statName;
- (void) set:(int64_t)value toStat:(NSString*)statName;
- (int64_t) get:(NSString*) statName;

// Stats modifiers by Index
- (void) incrementByIndex:(int)statIndex;
- (void) decrementByIndex:(int)statIndex;
- (void) add:(int64_t)amount byIndex:(int)statIndex;
- (void) set:(int64_t)value byIndex:(int)statIndex;
- (int64_t) getByIndex:(int)statIndex;

// Iteration interface
- (int) getNumStats;
- (int) getIndex:(NSString*)statName;
- (int64_t) getByIndex:(int)statIndex;
- (NSString*) getNameByIndex:(int)statIndex;

Here is an example of simulating a game and changing some stats. The gameOver method currently saves the statistics, but your stats class can save it whenever you prefer.

- (void) testStats
{
    [TestStats initialize];
    [TestStats dump];
    NSLog(@"\n");
    NSLog(@"Simulating a Game...");
    NSLog(@"\n");

    // Simulate some game events
    [TestStats gameStarted];
    [TestStats increment:STAT_JUMPS];
    [TestStats increment:STAT_SHOTS_FIRED];
    [TestStats increment:STAT_SHOTS_FIRED];
    [TestStats increment:STAT_SHOTS_FIRED];
    [TestStats increment:STAT_JUMPS];
    [TestStats add:100 toStat:STAT_DISTANCE];
    [TestStats set:1235 toStat:STAT_SCORE];
    [TestStats decrement:STAT_SCORE];
    [TestStats gameOver];
    [TestStats dump];
}

If you don’t like the overhead of converting the string stat name to an integer, you can use the getIndex method once to find out the index associated with a given statistic, and then use the “ByIndex” versions of the stats methods like incrementByIndex, decrementByIndex, add:byIndex, etc. There is also an iteration interface you can use to get the values of all stats.

Debugging Help

In order to help debug your game stats, I defined a dump method to output the stats with NSLog.

Here is an example of calling the dumpWithHeader:andPrefix method to show the stats from both the gameStats and totalStats objects in the TestStats.m class.

2011-08-13 22:17:08.958 StatsTest[31774:207] Game Stats
2011-08-13 22:17:08.959 StatsTest[31774:207]   Score=0
2011-08-13 22:17:08.960 StatsTest[31774:207]   Jumps=0
2011-08-13 22:17:08.960 StatsTest[31774:207]   ShotsFired=0
2011-08-13 22:17:08.960 StatsTest[31774:207]   Distance=0
2011-08-13 22:17:08.961 StatsTest[31774:207]
2011-08-13 22:17:08.961 StatsTest[31774:207] Total Stats
2011-08-13 22:17:08.961 StatsTest[31774:207]   Score=6170
2011-08-13 22:17:08.962 StatsTest[31774:207]   Jumps=5
2011-08-13 22:17:08.962 StatsTest[31774:207]   GamesPlayed=5
2011-08-13 22:17:08.962 StatsTest[31774:207]   ShotsFired=3
2011-08-13 22:17:08.963 StatsTest[31774:207]   Distance=500
2011-08-13 22:17:08.963 StatsTest[31774:207]
2011-08-13 22:17:08.963 StatsTest[31774:207] Simulating a Game...
2011-08-13 22:17:08.964 StatsTest[31774:207]
2011-08-13 22:17:08.965 StatsTest[31774:207] Game Stats
2011-08-13 22:17:08.965 StatsTest[31774:207]   Score=1234
2011-08-13 22:17:08.965 StatsTest[31774:207]   Jumps=2
2011-08-13 22:17:08.966 StatsTest[31774:207]   ShotsFired=3
2011-08-13 22:17:08.966 StatsTest[31774:207]   Distance=100
2011-08-13 22:17:08.966 StatsTest[31774:207]
2011-08-13 22:17:08.966 StatsTest[31774:207] Total Stats
2011-08-13 22:17:08.967 StatsTest[31774:207]   Score=7404
2011-08-13 22:17:08.967 StatsTest[31774:207]   Jumps=7
2011-08-13 22:17:08.967 StatsTest[31774:207]   GamesPlayed=6
2011-08-13 22:17:08.968 StatsTest[31774:207]   ShotsFired=6
2011-08-13 22:17:08.968 StatsTest[31774:207]   Distance=600

Rollover Beethoven

One of the requirements I mentioned above was for the current game stats to be added onto the total at the end of a game.  In the past, when I’ve done this, I used the term “rollover” to indicate that the current stats are being rolled over into the totals.

In my first version of the code, the rollover method was part of Stats.m.  When I made the code generic, I decided that it was too complex to attempt to rollover one set of arbitrarily-named statistics into another set of arbitrarily-named statistics, so I moved the rollover method into the game-specific stats class (TestStats.h in the example).

The game-specific stats class knows exactly what “rollover” means to that game, and it can choose to include or exclude whichever stats it wants.  For example, maybe there is a stat that only needs to be tracked within a game and not as a total.  The rollover method can exclude it.

Here is the rollover method from TestStats.h:

+ (void) rollover
{
	[totalStats add:[gameStats get:STAT_DISTANCE] toStat:STAT_DISTANCE];
	[totalStats add:[gameStats get:STAT_SCORE] toStat:STAT_SCORE];
	[totalStats add:[gameStats get:STAT_SHOTS_FIRED] toStat:STAT_SHOTS_FIRED];
	[totalStats add:[gameStats get:STAT_JUMPS] toStat:STAT_JUMPS];
}

That’s Great, but I Just Came Here for the Code!

No problem.  I understand completely!  Here is a link to a ZIP file with the Stats.mm class and an Xcode project with some example usage.

Enjoy!  Oh…and let me know if you find any bugs.  This code has only undergone a couple of hours of basic testing.  I need to get back into my unit testing mode on the next project!

  • http://twitter.com/mikeparlee Mike Parlee

    Hi Ken, thanks for posting this. I just have a couple of questions. Forgive me if I’m missing something obvious.

    First, why not just use an NSMutableDicitonary instead of an array and forgo the String index conversion? It would also make it easy to keep the rollover function in your Stats class.

    Second, why .mm? I didn’t notice any C++ code in there at all.

    Thanks,
    Mike

  • Pingback: Mind Juice » Simple Stats Tracker for iOS Games » Beliebte Suchbegriffe

  • http://twitter.com/MindJuiceMedia Ken Carpenter

    Hi Mike,

    Actually, I started out with that idea too.

    The reason I changed is that you need to store objects in an NSMutableDictionary, like NSNumber, and NSNumber is immutable, so I would have had to create new object instances and reinsert them into the dictionary every time I updated a stat, which is considerably higher overhead.

    Regarding .mm, it’s just my habit as I use libraries like OpenFeint that require it. I got tired of all the compile errors, so I ended up just doing it all the time by default. You can certainly change it.

    Ken

  • Scott Rapson

    Hi, Thanks for the great class. Mine didn’t scale very well.

    Im getting a build error in Stats.mm in the dealloc method. 

    delete[] statNames;  Throws a “Cannot delete expression of type ‘NSMutableDictionary *”

    Do you know how I can rectify this? I got rid of the error by commenting it out for [statNames release], but I don’t know if this is safe or not in the long run.

    Thanks

  • http://twitter.com/MindJuiceMedia Ken Carpenter

    Hi Scott,

    Ooops…Thanks for noticing that!  It looks like a leftover line of code from a previous version of the class where I used arrays for everything.

    Also, yes, your fix is correct and safe.

    FYI, I recently rewrote my Stats code though while I was adding support for Objectives.  I’ll do a post on the new version fairly soon.  The performance is basically the same as this version, but it is a cleaner design.

  • Scott Rapson

    Thats all good then. Looking forward to the new version.

    As a suggestion, you could include a more explicit example for retrieving the stats for use on a stats page. I just figured it out by common sense, but less experienced programmers may not spot how to do it. An example like,

    NSString *totalJumps = [NSString stringWithFormat:@"Total Jumps %i", [TestStats getTotal:STAT_JUMPS]];

    CCLabelTTF *Label = [CCLabelTTF labelWithString:totalJumps …etcThanks!

  • Scott Rapson

    Ive had it working for the last few days, but in search of an elusive slowdown, I found that the +(void) initialize { method in TestStats.mm is leaking quite badly, causing these slowdowns… See attached screencap.

    Any ideas? I don’t see anything deallocated. Maybe Im missing something?

  • Pingback: Platformer Progress Update 4 | 26oclock.com

  • Pingback: cool caravans

  • Pingback: Hanna

  • Pingback: young women's clothing

  • Pingback: Back Acne

  • Pingback: enfermedades de perros

  • Pingback: fat burning foods

  • Pingback: disney hotel discounts codes

Support

Email us at: info@mindjuice.net

Please be sure to let us know which app you are asking about. Thanks!

Our Games & Apps







Recent Posts

  • Pre-Announcing My Next Game: Spellchemy
  • 2011: The Year In Review
  • Charmed: RewardsDen Edition Lets Players Earn Free Gift Cards
  • How to Fix an App that Crashes in Release but not Debug
  • Towards a Solution to App Store Clutter

Blog Archive

iDevBlogADay

RSS Feed

Login

Login

Mind Juice Media is a game development company formed by industry veteran Ken Carpenter. Mind Juice is currently focused on the mobile market, and in particular, the iPhone, iPod touch and iPad.

Info coming soon.

More info here.

© 2012 Mind Juice Media Inc.