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

Simple Stats Tracker for iOS Games

Posted on August 14, 2011 by kenc in iDevBlogADay

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!

Bookmark and Share

Comments are closed.

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

  • 2012: The End is Nigh
  • Introducing AppRewardsClub.com
  • Help Others to Help Yourself
  • Pre-Announcing My Next Game: Spellchemy
  • 2011: The Year In Review

iDevBlogADay

RSS Feed

Twitter: @MindJuiceMedia

Bookmark and Share

Support Email: info@mindjuice.net

Bookmark and Share

Pinterest: Mind Juice on Pinterest

Bookmark and Share

© 2012 Mind Juice Media Inc.