Making Your iPhone Game Data-Driven

  • strict warning: Non-static method view::load() should not be called statically in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/views.module on line 843.
  • strict warning: Declaration of views_plugin_display::options_validate() should be compatible with views_plugin::options_validate(&$form, &$form_state) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/plugins/views_plugin_display.inc on line 1877.
  • strict warning: Declaration of views_plugin_display_page::options_submit() should be compatible with views_plugin_display::options_submit(&$form, &$form_state) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/plugins/views_plugin_display_page.inc on line 481.
  • strict warning: Declaration of views_plugin_display_block::options_submit() should be compatible with views_plugin_display::options_submit(&$form, &$form_state) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/plugins/views_plugin_display_block.inc on line 193.
  • strict warning: Declaration of views_handler_field_broken::ui_name() should be compatible with views_handler::ui_name($short = false) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/handlers/views_handler_field.inc on line 641.
  • strict warning: Declaration of views_handler_argument::init() should be compatible with views_handler::init(&$view, $options) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/handlers/views_handler_argument.inc on line 745.
  • strict warning: Declaration of views_handler_argument_broken::ui_name() should be compatible with views_handler::ui_name($short = false) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/handlers/views_handler_argument.inc on line 770.
  • strict warning: Declaration of views_handler_sort_broken::ui_name() should be compatible with views_handler::ui_name($short = false) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/handlers/views_handler_sort.inc on line 82.
  • strict warning: Declaration of views_handler_filter::options_validate() should be compatible with views_handler::options_validate($form, &$form_state) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/handlers/views_handler_filter.inc on line 585.
  • strict warning: Declaration of views_handler_filter::options_submit() should be compatible with views_handler::options_submit($form, &$form_state) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/handlers/views_handler_filter.inc on line 585.
  • strict warning: Declaration of views_handler_filter_broken::ui_name() should be compatible with views_handler::ui_name($short = false) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/handlers/views_handler_filter.inc on line 609.
  • strict warning: Declaration of views_handler_filter_boolean_operator::value_validate() should be compatible with views_handler_filter::value_validate($form, &$form_state) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/handlers/views_handler_filter_boolean_operator.inc on line 128.
  • strict warning: Declaration of views_plugin_row::options_validate() should be compatible with views_plugin::options_validate(&$form, &$form_state) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/plugins/views_plugin_row.inc on line 135.
  • strict warning: Declaration of views_plugin_row::options_submit() should be compatible with views_plugin::options_submit(&$form, &$form_state) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/plugins/views_plugin_row.inc on line 135.

Most new game programmers can create a one-level action or puzzle game, but get stuck when it's time to expand the game to multiple levels. OK they say, I'll need a big tree of if(level==2) statements or better yet a switch statement. If they're in an object-oriented frame of mind they'll create a GameLevel class with subclasses for GameLevel1, GameLevel2, etc. Don't laugh, I've seen it done!

A logic-driven or object-driven method works, but the amount of duplicated code is astronomical and only a programmer can update the levels. When you're working alone it's easy enough to tweak constants directly in the code, but when working with a team or a designer they'll want to edit levels without going through you.

The trick to get out of this trap is to make your game "data-driven" - that is, load the info for each stage, level, or other object from a data file. You already do this with images; you don't code the RGB value of every pixel into you program, do you? You save some .pngs and load them at run-time. That's what you want for your game objects too, and Cocoa has some tricks to make it easy.

Here's a class method to load data from a tab delimited file. Tab delimited files are easy to make by exporting or copy/pasting from a spreadsheet into a text file. By the way this will all be Greek to you if you haven't used an NSDictionary before.

//Monster.h

static NSMutableArray *typeList = nil;
+(void)loadTypeList{
   
    //do nothing if list is already loaded
    if(typeList!=nil)
        return;

    typeList = [[NSMutableArray alloc] init];
   
    NSString *path = [[NSBundle mainBundle] pathForResource:@"monsters.txt" ofType:nil];
    NSString *fileString = [NSString stringWithContentsOfFile:path];

    NSArray *keys = nil;
   
    for (NSString *line in [fileString componentsSeparatedByString:@"\n"]) {
       
        //skip blank lines
        if ([line length]==0)
            continue;
       
        //use first non-blank line as the keys
        if(keys==nil){
            keys=[line componentsSeparatedByString:@"\t"];
            continue;
        }
       
        //add the rest to the array
        NSArray *items=[line componentsSeparatedByString:@"\t"];
        NSDictionary *typePropertiesByKey = [NSDictionary dictionaryWithObjects:items forKeys:keys];
        [typeList addObject:typePropertiesByKey];
    }
}

That code converts the entire file into an array of dictionaries; the first line of the file is used as the keys in the dictionaries. This is exactly the format we need to to load the data into objects.

Only one "trick" there. The variable typeList is declared static so that a single instance can be accessed from anywhere in this file. This is Objective-C's closest equivalent to a "class variable." That means we only need to load the typeList once, even if we create a thousand monsters. The rest of the code uses bog-standard string, array, and dictionary methods.

//Monster.m

+(id)monsterWithType:(NSInteger)typeNumber{
   
    //make sure the type list is loaded
    [self loadTypeList];
   
    Monster *newMonster = [[Monster alloc] init];
   
    NSDictionary * typePropertiesByKey = [typeList objectAtIndex:typeNumber];
    [newMonster setValuesForKeysWithDictionary:typePropertiesByKey];
   
    return [newMonster autorelease];
}

The magic here is in setValuesForKeysWithDictionary; it takes each key-value pair in the dictionary and tries to set that propery or instance variable in the object. In our case we have keys for health, attack type, typeID, and other values; our newMonster will get its values from the values in the dictionary. This is a feature of Cocoa called key-value coding.

This listing lacks error checking code, but it will still alert you to some problems in the data file. If any line has the wrong number of tabs then dictionaryWithObjects:forKeys: will fail. If a key doesn't exist in the "Monster" object then setValuesForKeysWithDictionary: will fail. It doesn't give you a *helpful* error message though, so it'd be nice to NSAssert those things ourselves and give the level designer a chance to fix the data file. If the data file is bad you want to bomb as soon as possible - don't let the bad data cause a bug that only shows up in level 13 when using the steam engine tank, etc.

For completeness, here's Monster.h file and the data file:

//Monster.h
#import 

@interface Monster : NSObject {
    //private state
    NSString *typeID;   
    //public state
    float health, attack;
    NSString *AIType, *imageName;
}
@end

and our data file:

typeID	health	attack	AIType	imageName
zombie 10 5 astar zombie.png
crab 5 5 random crab.png
bigcrab 10 10 random bigcrab.png

Again, if the keys in the first line of your data file don't match the properties or instance variables of your class, setValuesForKeysWithDictionary will fail.

Once you grok the data-driven concept you'll see a lot of other places to apply it. Timing of scripted events, level design, conversation trees, and more; even config files. For map-based games you'll want to use a map editor; I suggest "Tiled." It's a little more work to read the format, but worth it for the WYSIWYG editing.

Extra credit: In my example I used a number to look up the monster in an array; that will break if I add a new monster in the middle of the data file. It's nicer to store the types in a dictionary instead and look them up by monsterID. That way you don't have to remember if number three is a giant crab or a zombie, and you don't have to maintain a separate enum.

Extra credit 2: I used stringWithContentsOfFile to load the entire file, and then I split it into lines and then again into data items. It's easy to read but not memory-efficient for larger files. NSScanner could do the job without using up three times the file size in memory.