본문 바로가기

프로그래밍/cocos2d

NSKeyedArchiver로 게임 데이터를 파일에 저장하기

디바이스에서 애플리케이션이 실행되는 중에 홈 버튼이 눌리거나 걸려온 전화를 받으면 실행중인 애플리케이션은 중지 됩니다.

이때, AppDelegate 클래스의

3.x -> - (void)applicationWillTerminate:(UIApplication *)application {}
4.x -> - (void)applicationDidEnterBackground:(UIApplication *)application {}

메서드가 호출 됩니다.
이 메서드에서 게임 데이터를 저장하는 방법을 학습합니다.


어떤 데이터를 저장해야 할까요?

1. 주인공
(1)현재 좌표
(2)방향
(3)에너지 값

2. 적
(1)현재 좌표
(2)방향
(3)걷는 속도

3. 기타
(1)라이프 수
(2)현재 점수




작업할 파일은 다음과 같습니다.

1. 추가할 파일
(1) MyTypes.h
(2) Utils.h
(3) Utils.m
(4) GameData.h
(5) GameData.m
(6) EnemyData.h
(7) EnemyData.m

2. 수정할 파일
(1) GameScene.h
(2) GameScene.m
(3) GameLayer.h
(4) GameLayer.m
(5) EnemySprite.h
(6) EnemySprite.m
(7) GameDemoAppDelegate.h
(8) GameDemoAppDelegate.m





//

//  MyTypes.h

//  GameDemo

//

//  Created by Chang-Min Pak on 6/16/10.

//  Copyright 2010 thefirstgood.com. All rights reserved.

//


#import <Foundation/Foundation.h>



/*@interface MyTypes : NSObject {


}


@end*/


// GameLayer.m 파일에 정의되어 있던 매크로 정의를 이곳으로 옮겼습니다.

#define INIT_NUM_LIFE    3

#define INIT_NUM_ENERGY 10


// 게임 데이터를 저장할 파일 이름

#define GAME_DATA_FILE_NAME     "GameDataArchive"


(1) MyTypes.m 파일은 만들 필요가 없습니다.






//

//  Utils.h

//  GameDemo

//

//  Created by Chang-Min Pak on 6/16/10.

//  Copyright 2010 thefirstgood.com. All rights reserved.

//


#import <Foundation/Foundation.h>



/*@interface Utils : NSObject {


}


@end*/


#import "MyTypes.h"

#import "GameLayer.h"


// 게임 데이터를 저장하는 파일의 경로를 돌려줍니다.

NSString* GameDataFilePath(NSString* fileName);


// GameLayer 데이터를 파일에 저장합니다.

void SaveCurrentGameState(GameLayer *gameLayer);


(1) Utils.h 파일은 수시로 쓰는 메서드를 한 곳에 모아놓은 유틸리티 파일입니다.
(2) 이곳에 정의한 메서드가 C 스타일 함수인 것에 주의하기 바랍니다. CGPointMake 처럼 객체 없이 사용할 수 있습니다.





//

//  Utils.m

//  GameDemo

//

//  Created by Chang-Min Pak on 6/16/10.

//  Copyright 2010 thefirstgood.com. All rights reserved.

//


#import "Utils.h"

#import "GameData.h"

#import "EnemyData.h"

#import "EnemySprite.h"


/*@implementation Utils


@end*/


// document 디렉토리 안에 있는 파일의 경로를 돌려줍니다.

NSString* GameDataFilePath(NSString* fileName) {

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

    NSString *documentDirectory = [paths objectAtIndex:0];

    

    return [documentDirectory stringByAppendingPathComponent:fileName];

}



// GameLayer 데이터로 GameData 오브젝트를 만든 저장합니다.

void SaveCurrentGameState(GameLayer *gameLayer) {

    if(gameLayer == nil)

        return;

    

    GameData *gameData = [[GameData alloc] init];

    

    gameData.score   = gameLayer.scoreValue;

    gameData.numLife = gameLayer.lifeValue;

    gameData.energy  = gameLayer.energyValue;

    

    gameData.princeXPos = gameLayer.princeSprite.position.x;

    gameData.princeYPos = gameLayer.princeSprite.position.y;

    

    gameData.princeFlipX = gameLayer.princeSprite.flipX;

    

    // enemySpriteSheet 들어있는 모든 EnemySprite 대해 EnemyData 오브젝트를 만듭니다.

    if(gameLayer.enemySpriteSheet != nil) {

        NSMutableArray *enemyDataArr = [[NSMutableArray alloc] initWithCapacity:5];

        

        NSArray* enemies = [gameLayer.enemySpriteSheet children];

        for(EnemySprite *enemy in enemies) {

            EnemyData *enemyData = [[EnemyData alloc] initWithWalkingSpeed:enemy.walkingSpeed withPosition:enemy.position flipX:enemy.flipX];

            [enemyDataArr addObject:enemyData];

            [enemyData release];

        }

        

        gameData.enemyDataArr = enemyDataArr;

        [enemyDataArr release];

    }   

    

    // saveGameData 메서드 안에서 데이터가 파일로 저장됩니다.

    [gameData saveGameData];

    [gameData release];

}









//

//  GameData.h

//  GameDemo

//

//  Created by Chang-Min Pak on 6/16/10.

//  Copyright 2010 thefirstgood.com. All rights reserved.

//


#import <Foundation/Foundation.h>


// NSCoding NSCopying 프로토콜을 사용합니다.

//@interface GameData : NSObject {

@interface GameData : NSObject <NSCoding, NSCopying> {

    NSInteger score;

    NSInteger numLife;

    NSInteger energy;

    

    // 주인공의 현재 위치

    CGFloat   princeXPos;

    CGFloat   princeYPos;

    

    // 주인공 방향

    BOOL princeFlipX;

    

    // 여러 명의 적들이 있을 있으므로 배열을 사용합니다.

    NSArray   *enemyDataArr;

}


@property (nonatomic, readwrite) NSInteger score;

@property (nonatomic, readwrite) NSInteger numLife;

@property (nonatomic, readwrite) NSInteger energy;

@property (nonatomic, readwrite) CGFloat   princeXPos;

@property (nonatomic, readwrite) CGFloat   princeYPos;

@property (nonatomic, readwrite) BOOL      princeFlipX;

@property (nonatomic, retain)    NSArray   *enemyDataArr;



// 현재 게임 데이터를 저장한 파일이 있는지 검사합니다.

+ (BOOL) hasSavedGameData;


// 파일에서 데이터를 읽어서 GameData 오브젝트에 넣습니다.

+ (GameData*) loadGameData;


// 게임 데이터 파일을 지웁니다.

+ (void) deleteGameDataFile;


- (id) initWithScore:(NSInteger)scoreVal numLife:(NSInteger)lifeVal energy:(NSInteger)energyVal 

      princePosition:(CGPoint)princePos princeFlipX:(BOOL)flipX;


// GameData 오브젝트의 값을 파일에 저장합니다.

- (void) saveGameData;


@end


(1) GameData 클래스는 NSCoding 과 NSCopying 프로토콜을 사용합니다. NSKeyedArchiver 클래스는 NSCoder 클래스를 상속받는데 NSCoder를 이용하여 저장할 데이터를 인코딩/디코딩 하게 됩니다.






//

//  GameData.m

//  GameDemo

//

//  Created by Chang-Min Pak on 6/16/10.

//  Copyright 2010 thefirstgood.com. All rights reserved.

//


#import "GameData.h"

#import "GameLayer.h"

#import "MyTypes.h"

#import "Utils.h"


@implementation GameData


@synthesize score, numLife, energy;

@synthesize princeXPos, princeYPos, princeFlipX;

@synthesize enemyDataArr;


- (id) init {

    if((self = [super init])) {

        self.score = 0;

        self.numLife = INIT_NUM_LIFE;

        self.energy = INIT_NUM_ENERGY;

    }

    return self;

}


- (id) initWithScore:(NSInteger)scoreVal numLife:(NSInteger)lifeVal energy:(NSInteger)energyVal

      princePosition:(CGPoint)princePos princeFlipX:(BOOL)flipX;

{

    if( (self=[self init]) ) {

        self.score = scoreVal;

        self.numLife = lifeVal;

        self.energy = energyVal;

        

        self.princeXPos = princePos.x;

        self.princeYPos = princePos.y;

        self.princeFlipX = flipX;

    }

    

    return self;

}



#pragma mark Saving/Loading Data

#define GAME_DATA_KEY       @"GameStateData"

+ (BOOL) hasSavedGameData {

    // 파일를 읽어서 NSData 받습니다.

    NSData *data = [[NSMutableData alloc] initWithContentsOfFile:GameDataFilePath(@GAME_DATA_FILE_NAME)];

    if(data == nil) // 저장된 데이터가 없습니다.

        return NO;

    

    [data release];

    

    return YES;

}


+ (GameData*) loadGameData {

    NSData *data = [[NSMutableData alloc] initWithContentsOfFile:GameDataFilePath(@GAME_DATA_FILE_NAME)];

    if(data == nil)

        return nil;

    

    // NSData 들어있는 데이터를 디코드하여 GameData 오브젝트에 넣습니다.

    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];

    GameData *tmpGameData = [unarchiver decodeObjectForKey:GAME_DATA_KEY];

    [unarchiver finishDecoding];

    [unarchiver release];

    [data release];

    

    return tmpGameData;

}


+ (void) deleteGameDataFile {

    @try {

        [[NSFileManager defaultManager] removeItemAtPath:GameDataFilePath(@GAME_DATA_FILE_NAME) error:NULL];

    }@catch (NSException* e) {

        //Should not happen!

    }

}


- (void) saveGameData {

    // NSMutableData key-value형태로 아카이브한 파일에 저장합니다.

    NSMutableData *data = [[NSMutableData alloc] init];

    NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];

    [archiver encodeObject:self forKey:GAME_DATA_KEY];

    [archiver finishEncoding];

    [data writeToFile:GameDataFilePath(@GAME_DATA_FILE_NAME) atomically:YES];

    [archiver release];

    [data release];

}



#pragma mark Keys for encoding/decoding


#define keyScore            @"Score"

#define keyNumLife          @"NumLife"

#define keyEnergy           @"Energy"

#define keyPrinceXPos       @"PrinceXPos"

#define keyPrinceYPos       @"PrinceYPos"

#define keyPrinceFlipX      @"PrinceFlipX"

#define keyEnemyData        @"EnemyData"



#pragma mark NSCoding

- (void) encodeWithCoder:(NSCoder *)encoder {

    [encoder encodeInt:self.score                   forKey:keyScore];

    [encoder encodeInt:self.numLife                 forKey:keyNumLife];

    [encoder encodeInt:self.energy                  forKey:keyEnergy];

    

    [encoder encodeFloat:self.princeXPos            forKey:keyPrinceXPos];

    [encoder encodeFloat:self.princeYPos            forKey:keyPrinceYPos];

    [encoder encodeBool:self.princeFlipX            forKey:keyPrinceFlipX];

    

    [encoder encodeObject:self.enemyDataArr         forKey:keyEnemyData];

}


- (id) initWithCoder:(NSCoder *)decoder {

    if( (self = [super init]) ) {

        self.score                  = [decoder decodeIntForKey:keyScore];

        self.numLife                = [decoder decodeIntForKey:keyNumLife];

        self.energy                 = [decoder decodeIntForKey:keyEnergy];

        

        self.princeXPos             = [decoder decodeFloatForKey:keyPrinceXPos];

        self.princeYPos             = [decoder decodeFloatForKey:keyPrinceYPos];

        self.princeFlipX            = [decoder decodeBoolForKey:keyPrinceFlipX];

        

        self.enemyDataArr           = [decoder decodeObjectForKey:keyEnemyData];

    }

    

    return self;

}


#pragma mark NSCopying

- (id) copyWithZone:(NSZone *)zone {

    GameData *copy = [[[self class] allocWithZone:zone] init];

    copy.score               = self.score;

    copy.numLife             = self.numLife;

    copy.energy              = self.energy;

    

    copy.princeXPos          = self.princeXPos;

    copy.princeYPos          = self.princeYPos;

    copy.princeFlipX         = self.princeFlipX;

    

    copy.enemyDataArr        = self.enemyDataArr;

    

    return copy;

}


@end







//

//  EnemyData.h

//  GameDemo

//

//  Created by Chang-Min Pak on 6/16/10.

//  Copyright 2010 thefirstgood.com. All rights reserved.

//


#import <Foundation/Foundation.h>



//@interface EnemyData : NSObject {

@interface EnemyData : NSObject <NSCoding, NSCopying> {

    CGFloat walkingSpeed;

    CGFloat xPos;

    CGFloat yPos;

    BOOL    flipX;

}


@property (nonatomic, readwrite) CGFloat walkingSpeed;

@property (nonatomic, readwrite) CGFloat xPos;

@property (nonatomic, readwrite) CGFloat yPos;

@property (nonatomic, readwrite) BOOL    flipX;


- (id) initWithWalkingSpeed:(CGFloat)speed withPosition:(CGPoint)position flipX:(BOOL)flip;


@end



(1) GameData 클래스와 같은 방식으로 EnemyData 클래스를 만듭니다.





//

//  EnemyData.m

//  GameDemo

//

//  Created by Chang-Min Pak on 6/16/10.

//  Copyright 2010 thefirstgood.com. All rights reserved.

//


#import "EnemyData.h"



@implementation EnemyData


@synthesize walkingSpeed, xPos, yPos, flipX;


- (id) initWithWalkingSpeed:(CGFloat)speed withPosition:(CGPoint)position flipX:(BOOL)flip {

    if( (self=[self init]) ) {

        self.walkingSpeed = speed;

        self.xPos = position.x;

        self.yPos = position.y;

        self.flipX = flip;

    }

    

    return self;

}


#pragma mark Keys for encoding/decoding


#define keyWalkingSpeed      @"enemyWalkingSpeed"

#define keyPositionX         @"enemyXPos"

#define keyPositionY         @"enemyYPos"

#define keyFlipX             @"enemyFlipX"



#pragma mark NSCoding

- (void) encodeWithCoder:(NSCoder *)encoder {

    [encoder encodeFloat:self.walkingSpeed  forKey:keyWalkingSpeed];

    [encoder encodeFloat:self.xPos          forKey:keyPositionX];

    [encoder encodeFloat:self.yPos          forKey:keyPositionY];

    [encoder encodeBool:self.flipX          forKey:keyFlipX];

}


- (id) initWithCoder:(NSCoder *)decoder {

    if( (self = [super init]) ) {

        self.walkingSpeed   = [decoder decodeFloatForKey:keyWalkingSpeed];

        self.xPos           = [decoder decodeFloatForKey:keyPositionX];

        self.yPos           = [decoder decodeFloatForKey:keyPositionY];

        self.flipX          = [decoder decodeBoolForKey:keyFlipX];

    }

    

    return self;

}


#pragma mark NSCopying

- (id) copyWithZone:(NSZone *)zone {

    EnemyData *copy = [[[self class] allocWithZone:zone] init];

    

    copy.walkingSpeed    = self.walkingSpeed;

    copy.xPos            = self.xPos;

    copy.yPos            = self.yPos;

    copy.flipX           = self.flipX;

    

    return copy;

}


@end








===============================================================================

이상이 새 파일에 대한 설명이었고, 이제 기존에 있던 파일을 수정하겠습니다. 
각 클래스의 private 변수의 값을 외부에서도 읽을 수 있도록 @property를 readonly로 설정하여 getter를 만드는 것이 주된 내용입니다.





//

//  GameLayer.h

//  GameDemo

//

//  Created by cmpak on 5/10/10.

//  Copyright 2010 thefirstgood.com. All rights reserved.

//


@interface GameLayer : CCLayer {

    ...

}


@property (nonatomic, readonly) NSInteger scoreValue;

@property (nonatomic, readonly) NSInteger lifeValue;

@property (nonatomic, readonly) NSInteger energyValue;


//

//  GameLayer.m

//  GameDemo

//

//  Created by cmpak on 5/10/10.

//  Copyright 2010 thefirstgood.com. All rights reserved.

//



@synthesize scoreValue, lifeValue, energyValue;








//

//  EnemySprite.h

//  GameDemo

//

//  Created by Chang-Min Pak on 6/2/10.

//  Copyright 2010 thefirstgood.com. All rights reserved.

//


@interface EnemySprite : CCSprite {

...

}


@property (nonatomic, readonly) CGFloat walkingSpeed;


//

//  EnemySprite.m

//  GameDemo

//

//  Created by Chang-Min Pak on 6/2/10.

//  Copyright 2010 thefirstgood.com. All rights reserved.

//


#import "EnemySprite.h"

#import "GameLayer.h"


@implementation EnemySprite


@synthesize walkingSpeed;









//

//  GameScene.h

//  GameDemo

//

//  Created by cmpak on 5/11/10.

//  Copyright 2010 thefirstgood.com. All rights reserved.

//


#import "cocos2d.h"


@class GameLayer;


@interface GameScene : CCScene {

    GameLayer *gameLayer;

}


@property (nonatomic, readonly) GameLayer *gameLayer;


@end


//

//  GameScene.m

//  GameDemo

//

//  Created by cmpak on 5/11/10.

//  Copyright 2010 thefirstgood.com. All rights reserved.

//


#import "GameScene.h"

#import "GameLayer.h"


@implementation GameScene


@synthesize gameLayer;


- (id) init {

    if( ![super init] )

        return nil;

    

    gameLayer = [GameLayer node];

    [self addChild:gameLayer z:0 tag:0];

    

    return self;

}


@end








//

//  GameDemoAppDelegate.h

//  GameDemo

//

//  Created by cmpak on 5/11/10.

//  Copyright 2010 thefirstgood.com. All rights reserved.

//


#import <UIKit/UIKit.h>


@class GameResumeViewController;

@class GameLayer;


@interface GameDemo_TouchAppDelegate : NSObject <UIApplicationDelegate> {

UIWindow *window;

    

    GameResumeViewController *gameResumeViewController;

    

    GameLayer *gameLayer;

}


@property (nonatomic, retain) UIWindow *window;

@property (nonatomic, readonly) GameResumeViewController *gameResumeViewController;


@end


//

//  GameDemoAppDelegate.m

//  GameDemo

//

//  Created by cmpak on 5/11/10.

//  Copyright 2010 thefirstgood.com. All rights reserved.

//


#import "GameDemo_TouchAppDelegate.h"

#import "cocos2d.h"

#import "GameScene.h"

#import "GameResumeViewController.h"

#import "Utils.h"


@implementation GameDemo_TouchAppDelegate


@synthesize window;

@synthesize gameResumeViewController;


- (void) applicationDidFinishLaunching:(UIApplication*)application

{

...

    

[window makeKeyAndVisible];

    

    GameScene *gameScene = [[GameScene alloc] init];

    gameLayer = gameScene.gameLayer;

    [[CCDirector sharedDirector] runWithScene:(CCScene*)gameScene];

    [gameScene release];

}


- (void)applicationWillTerminate:(UIApplication *)application {

// GameLayer에서 필요한 정보를 모아 파일에 저장합니다.

    SaveCurrentGameState(gameLayer);

[[CCDirector sharedDirector] end];

}


- (void)applicationDidEnterBackground:(UIApplication *)application {

// GameLayer에서 필요한 정보를 모아 파일에 저장합니다.

    SaveCurrentGameState(gameLayer);

[[CCDirector sharedDirector] end];

}






게임 데이터가 파일에 제대로 저장 되었는지 확인해 보겠습니다.
Finder에서 아래 디렉터리로 이동합니다.

/Users/<본인 계정>/Librarary/Application Support/iPhone Simulator/3.0/Applications

날짜와 시간을 확인 후 해당 디렉터리로 이동합니다. 

Documents 디렉터리에 GameDataArchive 파일이 있는지 확인합니다.