How to Save your Game’s Data: Part 1/2

This tutorial will walk you through how to save your game data – locally on the device and also up in the cloud. By Marin Todorov.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

More about NSCoding

Are you mumbling something under your breath? Did you say something to the effect of, "Yeah, when you need to save two double numbers it's all too easy"? Well, you're on the right track. There is more you can do with NSCoding, but the concept is remarkably similar to what you just did.

As I mentioned earlier, NSCoding includes helper methods to encode various types of primitives:

  • encodeBool:forKey:
  • encodeInt:forKey:
  • encodeInt32:forKey:
  • encodeInt64:forKey:
  • encodeFloat:forKey:

You can encode any object that implements NSCoding with this:

  • encodeObject:forKey:

If you can convert something to either NSData or a series of bytes, you can encode it with these methods:

  • encodeBytes:length:forKey:
  • encodeDataObject:forKey:

And finally, there are a few utility methods to ease the pain whenever you need to encode certain common structures:

  • encodeCGPoint:forKey:
  • encodeCGRect:forKey:
  • encodeCGSize:forKey:
Note:If you're curious to see more, check the headers for NSCoder and NSCoding. Also, check out our tutorial: NSCoding Tutorial for iOS: How To Save Your App Data

Say "Space Cheese"!

Are you ready to try something more interesting than storing numbers? Good! Next, you'll add the player's photo to RWGameData and save it to the device.

To work with images you'll need some extra assets unrelated to this tutorial, so download them now.

Unzip it and drag the ImageMasking folder into your Xcode project. Be sure you select both of the following: Copy items into destination group's folder (if needed) and Create groups for any added folders. You should see three new files in your project navigator:

NewFiles

The files are as follows:

  • 25_mask.png: This is a black and white mask matching the shape of the front window of the spaceship.
  • UIImage+Mask.h: This is a category on UIImage that provides a method to resize and mask a UIImage instance.
  • UIImage+Mask.m: This is the implementation of the method mentioned above.

What you're about to do is let the player take a photo, scale it, mask it and display it in the space ship's window.

Now, add the code for taking a photo. Open MyScene.m and in the file contents, close to the @implementation line, add a new instance variable under the rest of the ones you added earlier:

SKLabelNode* _takePhoto;

This instance variable will hold your new HUD button for making photos. Now scroll down to setupHUD: and add this code at the end of the method body:

_takePhoto = [[SKLabelNode alloc] initWithFontNamed:@"Futura-CondensedMedium"];
_takePhoto.name = @"TakePhotoButton";
_takePhoto.fontSize = 15.0;
_takePhoto.text = @"Pilot Photo";
_takePhoto.position = CGPointMake(self.size.width-40, self.size.height-20);
_takePhoto.fontColor = [SKColor yellowColor];
[self addChild:_takePhoto];

This will create a new label along the rest of the HUD, but this label will act like a button - just like the label that acts like a button to restart the game. You also need to add the code to handle touches.

Scroll to touchesBegan:withEvent:, and add this new chunk of code to handle touches on the Pilot photo button. Check the comments inside the code below to make sure you're adding the right code:

for (UITouch *touch in touches) {
    SKNode *n = [self nodeAtPoint:[touch locationInNode:self]];
    if (n != self && [n.name isEqual: @"restartLabel"]) {
        //[self.theParentView restartScene];
        [[self childNodeWithName:@"restartLabel"] removeFromParent];
        [[self childNodeWithName:@"winLoseLabel"] removeFromParent];
        [self startTheGame];
        return;
    }
  
    // here new code begins
    if (n != self && [n.name isEqualToString:@"TakePhotoButton"]) {
      [self takePhoto];
      return;
    }
    // new code ends
}

What this new code does is to check whether the name of the node being tapped equals to "TakePhotoButton", and if so invokes the takePhoto: of your scene, which you'll add to the class implementation now:

-(void)takePhoto
{
  //1
  self.paused = YES;
  
  //2
  UIImagePickerController* imagePickerController = [[UIImagePickerController alloc] init];

  //3
  [imagePickerController setSourceType:UIImagePickerControllerSourceTypeCamera];
  imagePickerController.delegate = self;
    
  //4  
  UIWindow* appWindow = [UIApplication sharedApplication].windows.firstObject;
  [appWindow.rootViewController presentViewController:imagePickerController animated:YES completion:nil];
}

This code does a number of things:

  1. First you pause the game to avoid killing the player while he or she takes a selfie.
  2. Then you create a new UIImagePickerController instance - this is a view controller that allows the user to take a photo.
  3. You set the source type for the image picker to be the camera and the delegate to be the scene class.
  4. Finally, you hold the app's window and present the image picker view controller from the window's top view controller. [TODO: Please check this last point for clarity and accuracy]

Finally, to remove those compiler warnings, you need to conform to a couple of protocols, required by UIImagePickerController.

Open MyScene.m and replace the @interface directive with the following:

@interface MyScene : SKScene<UIImagePickerControllerDelegate, UINavigationControllerDelegate>

The warnings will now disappear with the next project build.

This should be enough to see the camera view controller, so give it a try! Build and run, then tap the Pilot photo button on the top of the scene.

Note: Unfortunately, you can only run this portion of the code on a device. If you try to run it in the simulator, it will crash with a NSInvalidArgumentException since the camera is not available.

Oooops! You should see an error in the console like this:

Terminating app due to uncaught exception 'UIApplicationInvalidInterfaceOrientation', 
reason: 'Supported orientations has no common orientation with the application, 
and shouldAutorotate is returning YES'

This was (almost) unexpected. So, to find the trouble-maker, click on the project file and check the allowed screen orientations:

sgd_12_orientation

Bingo! The image picker view controller only supports portrait orientation, but the app doesn't support portrait - so this seems easy enough to correct, right? Just check the portrait checkbox and build and run.

sgd_13_orientationPort

Hmm…now when you tilt the device in a certain position the whole scene rotates and the display looks off-kilter. As it turns out, your game can't support both portrait and landscape orientations.

Now you're going to implement a little trickery to support only landscape while the player plays the game, and only portrait while they are taking a photo.

Open ViewController.h and add a new static variable above the @interface line:

static BOOL lockToPortraitOrientation = NO;

This is a static variable, which you'll set to YES when you present the image picker and set back to NO when the player wants to continue playing.

Switch to MyScene.m and add along the rest of the imports at the top of the file:

#import "ViewController.h"

Add the following as the first line of code in the takePhoto:

lockToPortraitOrientation = YES;

Most of your master plan is now completed, the only thing remaining is to make ViewController respect the value of lockToPortraitOrientation. Open ViewController.m and find the method called supportedInterfaceOrientations. Replace the contents of the method with the following:

if (lockToPortraitOrientation) {
  return UIInterfaceOrientationMaskPortrait;
} else {
  return UIInterfaceOrientationMaskLandscapeRight;
}

The code is straight forward; if lockToPortraitOrientation is YES the view controller allows only portrait orientation; otherwise it allows landscape only. Could something so simple actually work? Give it a try!

sgd_14_cameraview

The camera view shows up and you can snap a picture, nice! But did you notice there's another problem? update: doesn't pause while you take a photo. There's a quick fix for that too, just open MyScene.m and as first line of code in update: add:

if (self.paused) {
  return;
}

This stops all node actions while you're taking your mug shot. That's more like it!