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 4 of 4 of this article. Click here to view the first page.

Immortalizing Your Photo

Now you need to make sure the photo is useful to the player. In this section, you'll add the delegate methods to the image picker view controller to make sure your handsome face saves to the disc, along your high score and total distance flown.

First open RWGameData.h and add a new property:

@property (strong, nonatomic) UIImage* pilotPhoto;

Switch to RWGameData.m and along with the other constants near the top of the file, add this:

static NSString* const SSGameDataPilotPhotoKey = @"pilotPhoto";

This will be the key you use to send the photo to the encoder/decoder.[TODO:I replaced 'encode' with 'send' to reduce redundancy. Please confirm the word choice is suitable] Speaking of which, scroll to encodeWithCoder: and add the following to the end to encode the photo property:

if (self.pilotPhoto) {
  NSData* imageData = UIImagePNGRepresentation(self.pilotPhoto);
  [encoder encodeObject:imageData forKey: SSGameDataPilotPhotoKey];
}

This code is a tiny bit different from how you've encoded the rest of the properties.

Note how you check whether self.pilotPhoto is set, and encode that property only if it is. After all, there's no need to write anything if the user is camera-shy!

Encoding the photo is a bit different when you compare it to the rest of the properties. UIImage does not implement NSCoding, so you have to convert it to a buffer of bytes first.

To do this, you use UIImagePNGRepresentation to convert the image to NSData (which does implement NSCoding), and then you can use encodeObject:forKey: as usual.

Now scroll to initWithCoder:. Look inside the if statement, which is after the code that initializes the other properties; add this code to initialize the photo:

NSData* imageData = [decoder decodeObjectForKey: SSGameDataPilotPhotoKey];
if (imageData) {
  _pilotPhoto = [[UIImage alloc] initWithData:imageData];
}

You're reversing what you did earlier. First, you try to decode the object for the photo key. If it exists, you just create a new UIImage instance out of the data.

Implementing the image picker delegate methods in your scene is the last step to immortalizing the player's photo. Open MyScene.m and at the top, alongside the rest of the imports, add:

#import "UIImage+Mask.h"

First, create a method stub for adding the pilot. You will be adding to it later in the tutorial. Please this code at the bottom of MyScene.m, just before the @end directive.

-(void) setupPilot
{
    // Code to go here.
}

Next, create a new method underneath the previous one to handle a successful photo capture.

-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
  //1
  lockToPortraitOrientation = NO;
    
  //2
  [picker dismissViewControllerAnimated:YES
    completion:^{

    //3
    UIImage* photoTaken = info[UIImagePickerControllerOriginalImage];
    UIImage* pilotImage = [photoTaken imageWithSize: CGSizeMake(25, 25) andMask:[UIImage imageNamed:@"25_mask.png"]];
                                   
    //4
    [RWGameData sharedGameData].pilotPhoto = pilotImage;
    [[RWGameData sharedGameData] save];
                                   
    //5
    [self setupPilot];
    
    //6                            
    self.paused = NO;
  }];
}

This delegate method received the captured photo, so you're good to process and use it. You do this in several steps:

  1. First you reset the screen orientation to landscape only.
  2. Then you dismiss the image picker view controller.
  3. When the view controller is dismissed, you grab the captured photo (located in info[UIImagePickerControllerOriginalImage]), then store a resized and masked version of it in pilotImage.
  4. In the next couple lines you store the photo in RWGameData and then invoke save to make sure the photo saves to the disc immediately.
  5. Then you call setupPilot, which displays the photo on screen.
  6. Finally, you un-pause the scene withsetupPilot, which you'll add in few moments

Now you'll wrap up the image picker part of the code. Add the delegate method, the one called when the user cancels the image capture:

-(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
{
    lockToPortraitOrientation = NO;
    
    [picker dismissViewControllerAnimated:YES
                               completion:^{
                                   self.paused = NO;
                               }];
}

This method just resets the orientation back to landscape, dismisses and image picker view controller, and un-pauses the scene.

You're at the last step before wrapping up! Now you just need to display the photo on the screen. Make sure setupPilot: looks like the following:

-(void)setupPilot
{
  //1
  if ([RWGameData sharedGameData].pilotPhoto) {
    //2
    UIImage* pilotImage = [[RWGameData sharedGameData].pilotPhoto imageWithSize: CGSizeMake(25, 25) andMask:[UIImage imageNamed:@"25_mask.png"]];

    //3
    [[_ship childNodeWithName:@"Pilot"] removeFromParent];
         
    //4
    SKTexture* pilotTexture = [SKTexture textureWithImage:pilotImage];
    SKSpriteNode* pilotSprite = [SKSpriteNode spriteNodeWithTexture: pilotTexture];
    pilotSprite.name = @"Pilot";
    pilotSprite.position = CGPointMake(28, 5);
    [_ship addChild: pilotSprite];
  }
}
  1. First, you check whether there's a pilot's photo. You're going to call this method when the game starts, so you need to check.
  2. Then you make sure the photo is properly sized and masked, because once you persist it via the encoder the masking will actually disappear.
  3. Check for an existing pilot photo node with name of "Pilot" and if found, remove it from the scene.
  4. Create a new texture from the pilot photo and create a sprite node out of the resulting texture. Finally, position the sprite node and add it as a child to the space ship.

Your app calls this method when you capture a new photo, and also when you build the scene and load the game data from a file. If the player sets his or her photo in a previous app launch, you'll need to load the photo when the game starts.

Do that in initWithSize:. Just find the line where you add the ship to the scene: [self addChild:_ship]; and add this above:

[self setupPilot];

Perfect! Now when you start the game the app will check for persisted photo, and if found, it'll display on-screen. If the player takes a new photo - it overwrites and saves the new one to the game data file.

Build and run. You should be able to take your photo and see yourself piloting that space ship! Yeah!

sgd_15_pilot_spaceship

Where To Go From Here?

Here is the example project up to this point.

Here's a recap of what you learned in the part I of this tutorial series:

  • How to encode/decode data to the disc
  • How to encode/decode more complex data (i.e. beyond primitives)
  • How to have optional game data (i.e. can be set or undefined)
  • Bonus: using dynamic photos in your game scene and advanced screen orientation handling

In part two, you'll learn how to prevent malicious users from tinkering with your game's data file and how to share the game data between devices via iCloud.

If you have any questions or comments, please share them with us in the comments below!