![]() |
RuTh's
RuThLEss
HomEpAgE
|
|
* 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 FilesThe NSCoder protocolCocoa 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 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:
The next chapter is an example of how to correctly implement the Encoding and Decoding objectsThe data will stored in an NSCoder object named 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 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 The line 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 MYGameWorldNSArchiver 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 MYGameControllerOpen 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 GUITo actually connect these actions to menu items in the GUI, we use the InterfaceBuilder Application.
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 |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
http://www.ruthless.zathras.de/ |