Logo
RuTh's  RuThLEss  HomEpAgE

 

sTaRt
 
 
 
 
 
 
 
 
FuN
 
 
 
 
fAcTs
 
 
>
3D Game DesignEnglish
>
PlanetrisEnglishDeutschCesky
>
PHP-SkripteDeutsch
>
Programming Links
 
 
 
 
 
 
* INDEX * 3D game to-do list * 3D Engine to-do list ( chapter 1, 2, 3, 4, 5, 6, 7 ) *
* projection formula * transformation matrices * Bresenham algorithm * Scanline Polygonfill algorithm *
* Spieldesign / game design * Troubleshooting 3D * Irrlicht 3D engine * Blender for Beginners *

Chapter 3 -- Loading From and Saving To Files

The NSCoder protocol

Cocoa offers methods to save and load objects and variables, and assists us with low-level data storage. Unfortunately, these methods only work with data types known to Cocoa. If we want Cocoa to read and write custom objects (like MYGameWorld), we need to tell it explicitly how to do that. Especially, we want to define which temporary variables to skip when writing data to a file, and we need to provide default values for the same temporary variables when reading the files back in and recreating the object.

Telling Cocoa how to encode/decode custom data types is done by implementing the NSCoder protocol in our custom objects. Implementing a protocol is like implementing an interface in Java. There are special coder methods that Cocoa expects to be in every load- and saveable object, and it's our job to make sure they are there and do the right thing.

The NSCoder protocol includes the two methods -(void)encodeWithCoder:(NSCoder*)coder for saving and -(id)initWithCoder:(NSCoder*)coder for loading data. The NSCoder protocol is very handy: After you implement it, archiving the top-level object (in our case, the game world) will cause a chain reaction in all the embedded objects (in our case, down to the vertices); Cocoa uses the coders to learn how to read and write our strange custom objects, treating them smoothly just like any Objective-C data type.

For the chain reaction to work, each member of the chain needs to respond to the protocol. How do you identify the objects that need the protocol? Start with the top-level custom object that we want to save, MYGameWorld. The rule is: If one of our custom objects contains another custom object, we need to define the protocol for the embedded custom objects, too. That's how we proceed:

  1. We indentify the objects in need of the NSCoder protocol: Our game world includes an array of Entities, which consist of colors and polygons, of which the latter consist of vertices, which consist of floating point numbers. That means, our custom objects MYGameworld, MYEntity, MYPolygon, MYVertex need the proctol implemented. NSColor, int and float belong to Cocoa and have it already.
  2. For each of those custom objects that we want to load from a file, we implement the initWithCoder: method as an alternative init method.
  3. For each of those custom objects that we want to save to a file, we implement the encodeWithCoder: method.
  4. We add un/archiver methods to the game world level, and implement load and save methods on the controller level.
  5. In the last step, we connect the controller's load and save actions to the corresponding menu items to make them accessible from the GUI.

The next chapter is an example of how to correctly implement the initWithCoder: and encodeWithCoder: methods. It works the same for MYGameworld, MYEntity, MYPolygon, and MYVertex.

Encoding and Decoding objects

The data will stored in an NSCoder object named coder. First, you store instance variables with a primitive C data type such as ints and floats: The line [coder encodeValueOfObjCType:@encode(int) at:&size] tells Cocoa, that it should store the instance variable size and remember it's an integer value. Do that for each of your indispensable instance variables like I do for xVar, yVar, zVar in my example.

Next, look at what other objects your custom object uses. For example that might be an NSMutableArray, NSArray, NSColor etc. Even if you don't have to define how to encode Cocoa's data types, you still have to specify that you want to encode them. So if you have an NSMutableArray named content, you encode it with the line [coder encodeObject:content]. Do that for each indispensable object.

Why do I keep saying 'indispensable'? Well, there probably are some temporary variables in your code: Just leave out all reconstructable data, it will save you disk space.

- (void) encodeWithCoder:(NSCoder*)coder
{
    /* archive primitive variables */
    [coder encodeValueOfObjCType:@encode(int)   at:&size];
    [coder encodeValueOfObjCType:@encode(float) at:&xVar];
    [coder encodeValueOfObjCType:@encode(float) at:&yVar];
    [coder encodeValueOfObjCType:@encode(float) at:&zvar];
    /* archive objects */
    [coder encodeObject:content];
    [coder encodeObject:color];
    /* leave out all temporary variables! */
}

Decoding works in a similar fashion. The object simply gets an alternative init method which will initialize it from the contents of a file. The line [coder decodeValueOfObjCType:@encode(int) at:&size] tells Cocoa to read in an integer sized primitive value from the coder, and store it in the instance variable named size. Decode and restore all indispensible primitive variables that way.

The line color = [coder decodeObject]; [color retain]; tells Cocoa to read in an object, store it under the name color and retain it. Decode and restore all indispensible objects that way. Don't forget to set temporary variables of your newly initialized object to useful default values!

Important: The order in which you decode data from the object must be the same in which you encoded it! I use a sequential coder, that means, it just jumps to the next block of data by the typical size reserved for this data type (Alternatively, you could use a keyed coder.) It does not matter however, in which order temporary variables are set.

- (id) initWithCoder:(NSCoder*)coder
{
    self=[super init];
    if(self){
	/* unarchive primitive variables */
	[coder decodeValueOfObjCType:@encode(int)   at:&size];
	[coder decodeValueOfObjCType:@encode(float) at:&xVar];
	[coder decodeValueOfObjCType:@encode(float) at:&yVar];
	[coder decodeValueOfObjCType:@encode(float) at:&zVar];
	/* unarchive and retain objects: */
	content = [coder decodeObject];	[content retain];
	color   = [coder decodeObject]; [color retain];
        /* restore default values for all temporary variables! */
        int truth = 42;
    }
    return self;
}

Okay! Now we have prepared our custom data types for the excitement of being transmitted from RAM to hard disk and back. W00t! Next we need to set up MYGameWorld with a means of initiating the chain reaction necessary for the transmission.

Initiating De- and Encoding from MYGameWorld

NSArchiver and NSUnarchiver are subclasses of NSCoder. Open your MYGameWorld files and add saveTo: and loadFrom: methods to it, which initiate archiving/unarchiving our game world data using the de- and encoders we defined before.
- (MYGameWorld*) loadFrom:(NSString*)path
{
    NSData* data = [[[NSData alloc] initWithContentsOfFile:path] autorelease];
    MYGameWorld* tempworld = [NSUnarchiver unarchiveObjectWithData:data];
    return tempworld;
}
- (BOOL) writeTo:(NSString*)path
{
    NSData *data = [NSArchiver archivedDataWithRootObject:self];
    BOOL  result = [data writeToFile:path atomically:YES];
    return result;
}

Our goal is to connect the loadFrom: and saveTo: methods to menu items in the executable application. In order to achieve that, we need methods in the controller that call loadFrom: and saveTo: each time the GUI tells the controller that the user activated the save or open menu item. Methods in the controller that are accessible from the GUI are refered to as actions.

Adding Actions to MYGameController

Open MYGameController and add the following loadGameWorld: and saveGameWorld: methods. These two methods are the actions that will be called when somebody chooses open or save from the menu in your application. The actions will open dialog panels and ask the user for a file path. Then they call the saveTo: or loadFrom: methods (respectively) that we just implemented in MYGameWorld.

- (void) saveGameWorld:(id)sender
{
    /* pop up a dialog window */
    NSSavePanel *saveDialog = [NSSavePanel savePanel];
    [saveDialog setRequiredFileType:@"xyz"];
    [saveDialog setDirectory:[self pathForDataFile:@""]];
    int runResult = [saveDialog runModal];
    if (runResult == NSOKButton) {
        /* save the stuff under the filename entered by the user */
	if ( ![world writeTo:[saveDialog filename]] ) NSBeep();
    }
    [saveDialog release];
}
- (void) loadGameWorld:(id)sender 
{
    /* pop up  a dialog window */
    NSArray *fileTypes = [NSArray arrayWithObject:@"xyz"];
    NSOpenPanel *loadDialog = [NSOpenPanel openPanel]; 
    [loadDialog setAllowsMultipleSelection:NO];
    [loadDialog setDirectory:[self pathForDataFile:@""]];
    int result = [loadFrom runModalForTypes:fileTypes];
    if (result == NSOKButton) {
        /* replace old game world by new empty one */
	[world release]; 
	world = [[MYGameWorld alloc] init]; 
        /* load stuff from the file selected by the user */
        NSString *fileToOpen = [loadDialog filename];
        [worldView setWorldTo:[world lesen:fileToOpen]];
	[fileToOpen release];
    }
    [loadDialog release]; [fileTypes release];
}

The method pathForDataFile: constructs the default path where you want to save your game data. The most obvious place is the "Application Support" folder in the user's Library. Replace the string XYZ by the name of your 3D-engine, and replace xyz by the suffix you chose for your data files.

- (NSString *) pathForDataFile:(NSString*)name
{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSString *folder = @"~/Library/Application Support/XYZ/";
    folder = [folder stringByExpandingTildeInPath];
    if ([fileManager fileExistsAtPath:folder] == NO)
    {
	[fileManager createDirectoryAtPath:folder attributes:nil];
    }
    return [folder stringByAppendingPathComponent:name];
}

Adding and Connecting Menu Actions to the GUI

To actually connect these actions to menu items in the GUI, we use the InterfaceBuilder Application.

  1. Open your project's .nib file in the InterfaceBuilder.
  2. Select the MYGameController icon in the Instances tab and switch to the Classes tab.
  3. Choose Classes > Add Action from the menu and type in saveGameWorld: as the name of the action.
  4. Next, we want to connect the action the its menu item. In the Instances tab, double click the Main Menu icon to edit the menu.
  5. To connect the action saveGameWorld: to its menu item, CTRL-click the menu item "Save As..." and drag the mouse to the MYGameController icon in the Instances tab. A golden line will appear and connect both items.
  6. Released the mouse. The Connections palette pops up, offering you a list of available actions.
  7. In the Actions list, select saveGameWorld: and click the Connect Button. A little dot will appear to show you the action in MYGameController is now connected to this menu item in the GUI.

Repeat these steps to connect the loadGameWorld: action to the "Open..." menu item, and you're done! :-)

A Level Editor?

A Level editor is an additional GUI you write to manipulate you game world while designing its contents. Of course, a real level editor would allow you to select and manipulate objects, polygons and vertices by mouse drag&drop — we start small and first only add some GUI elements like controls or text fields to get and set some overall game world values, and some buttons or pop-ups to easily load prepared test entities and save them.

Useful Links

 
   
2008.08.26

http://www.ruthless.zathras.de/