7
\$\begingroup\$

This is the second time I have implemented saving and loading for a game using Objective-C. I am using the built in NSCoding methods. I would love to hear opinions about NSCoding and whether or not it is a viable option for saving and loading games. My code does function, however there is a lot of boilerplate code required to make it work. Maybe there is a way to reduce the lines of code required that I don't know about.

I did find a library that supposedly automated part of this process, however that library only encodes the @properties of the class, and I am also encoding private instance variables as well. I also was not planning to use an external library, although I will take any recommendations given.

First, here are the saving and loading methods in the SKScene:

#pragma mark - Save and Load
-(void) saveGameToSlot:(NSString *)saveSlot {
    NSLog(@"Saving game %@", saveSlot);
    [self saveCustomObject:_game key:saveSlot];
    [self closeSaveMenu];
}
-(void) loadGameInSlot:(NSString *)loadSlot {
    NSString *savePath = [[self applicationDocumentsPath] stringByAppendingPathComponent:loadSlot];
    BOOL saveExists = [[NSFileManager defaultManager]fileExistsAtPath:savePath];

    if (saveExists) {
        [self prepareForLoading];

        _game = [self loadCustomObjectWithKey:loadSlot];

        [self createUIAndRenderer];
        [self createSceneElements];

        NSLog(@"Loading game %@", loadSlot);

        [self closeSaveMenu];
    }
}
-(void) saveCustomObject:(DTGame *)object key:(NSString *)key {
    NSString *path = [[self applicationDocumentsPath] stringByAppendingPathComponent:key];
    [NSKeyedArchiver archiveRootObject:object toFile:path];
}
-(DTGame *) loadCustomObjectWithKey:(NSString *)key {
    NSString *path = [[self applicationDocumentsPath] stringByAppendingPathComponent:key];
    DTGame *object = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
    return object;
}
-(void) prepareForLoading {
    [_world removeAllChildren];
    [_world removeFromParent];

    [_mainHud removeAllChildren];
    [_mainHud removeFromParent];

    [_resourceCounters removeAllChildren];
    [_resourceCounters removeFromParent];

    [[self view] removeGestureRecognizer:_panRecognizer];
    [[self view] removeGestureRecognizer:_pinchRecognizer];
    [[self view] removeGestureRecognizer:_swipeLeft];
    [[self view] removeGestureRecognizer:_swipeRight];
}
-(NSString *) applicationDocumentsPath {
    return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
}

Here is the code required in the Game class to make this work:

//in the header file
@interface DTGame : NSObject <NSCoding>

//in the implementation
#pragma mark - NSCoding methods
-(void) encodeWithCoder:(NSCoder *)aCoder {
    [aCoder encodeInt:self.currentCommonResources forKey:@"currentCommonResources"];
    [aCoder encodeInt:self.currentRareResources forKey:@"currentRareResources"];
    [aCoder encodeInt:self.currentFoodResources forKey:@"currentFoodResources"];
    [aCoder encodeInt:_foodStockpileSize forKey:@"foodStockpileSize"];
    [aCoder encodeObject:_towerArray forKey:@"towerArray"];
    [aCoder encodeObject:_gameDwarfArray forKey:@"gameDwarfArray"];
    [aCoder encodeInt:_currentTower forKey:@"currentTower"];
    [aCoder encodeCGSize:_worldSize forKey:@"worldSize"];
}
-(id) initWithCoder:(NSCoder *)aDecoder {
    self = [super init];
    if (self) {
        _currentCommonResources = [aDecoder decodeIntForKey:@"currentCommonResources"];
        _currentRareResources = [aDecoder decodeIntForKey:@"currentRareResources"];
        _currentFoodResources = [aDecoder decodeIntForKey:@"currentFoodResources"];
        _foodStockpileSize = [aDecoder decodeIntForKey:@"foodStockpileSize"];
        _towerArray = [aDecoder decodeObjectForKey:@"towerArray"];
        _gameDwarfArray = [aDecoder decodeObjectForKey:@"gameDwarfArray"];
        _currentTower = [aDecoder decodeIntForKey:@"currentTower"];
        _worldSize = [aDecoder decodeCGSizeForKey:@"worldSize"];
    }
    [self continueGame];
    return self;
}
-(void) continueGame {
    self.isPaused = YES;
    self.hasTowerChanged = YES;
}

And then every class that will be saved and loaded has to be an NSCoding delegate and implement those two methods. Here is an example of how to set this up for a subclass:

#pragma mark - NSCoding methods
-(id) initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        _dwarfMovementState = [aDecoder decodeIntegerForKey:@"dwarfMovementState"];
        _floorList = [aDecoder decodeObjectForKey:@"floorList"];
        _floorListForWork = [aDecoder decodeObjectForKey:@"floorListForWork"];
    }
    return self;
}
-(void) encodeWithCoder:(NSCoder *)aCoder {
    [super encodeWithCoder:aCoder];
    [aCoder encodeInteger:self.dwarfMovementState forKey:@"dwarfMovementState"];
    [aCoder encodeObject:_floorList forKey:@"floorList"];
    [aCoder encodeObject:_floorListForWork forKey:@"floorListForWork"];
}

Is this method of implementing saving and loading reliable and efficient? I hate that I have to use strings to access the properties, because there is no auto correction for the text, so mistakes are likely. If the class names ever change, the strings will not have to change, but they will be unclearly named until they are fixed. And any and every single variable that is added to the classes will need to have this boilerplate code to back it up.

\$\endgroup\$
6
\$\begingroup\$

Is this method of implementing saving and loading reliable and efficient?

It should be reliable. And if saving all of this information is necessary, then this is almost certainly the most efficient way to do it all.

The way to improve efficiency further is to ask yourself what datapoints are absolutely necessary to write and read later, and what datapoints can be derived from the necessary ones without explicitly being saved? If it can be implicitly determined, you don't necessarily need to save it.

I hate that I have to use strings to access the properties, because there is no auto correction for the text

While you do have to use strings for the keys, you can (and should) get auto-complete to help you by defining the keys as named constants. In all of my iOS projects, I always have multiple files where my global constants such as user defaults keys are defined. In this case, these strings could be declared simple within the current file as they're only used in two methods, but nonetheless, it would still be good to do it even if they're only used in two spots.


One other thing to think about is actually using NSUserDefaults. You've already got init/encodeWithConder methods written, so now you can store these to NSUserDefaults.

This won't make loading a game any faster or efficient, as at the end of the day, the loading process would be the same as what you're already doing, however, using NSUserDefaults for at least the current game being played should allow you to take regular snapshots of the current game state and auto-save without the player even noticing.

When you save to NSUserDefaults using setValue:forKey:, at first, you're just setting the value in temporary memory. This is no different from setting a value in a NSMutableDictionary. It's quite quick. You just pointing to a memory address.

You can call synchronize on NSUserDefaults to force it to write everything in the temporary memory to permanent storage, and sometimes this can be appropriate, but you don't want to do this frequently. Instead, NSUserDefaults works in the background and wait till the processor has spare time and then writes the values to permanent storage. I'm pretty sure the only way the data in NSUserDefaults can be lost is if the phone suddenly died unexpectedly. Even if the app is killed or the phone is powered off normally, the data in NSUserDefaults will be saved before it's lost.

What I'd recommend is only saving the currently active game to NSUserDefaults however. Just before a different game becomes active, the data from NSUserDefaults should be saved in the manner you're already using here.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.