HUD (Head-up Display)


대부분 게임에는 시간이나 점수를 보여주는 HUD가 있다. 우리 게임에도 간단하게 HUD를 추가하도록 하자. 

단순하게 움직인 거리를 나타내는 것만 표시할 것이다. 적을 죽여서 점수에 반영하는 것은 숙제로 남기기로 하겠다. 


우선 CCLayer를 상속받아서 HUDLayer이름으로 클래스 하나를 생성한다. 

그리고 HUDLayer.h에 아래 코드와 같이 작성한다. 


@interface HUDLayer : CCLayer {

    CCLabelTTF *scoreLabel;

}


-(void)setScoreText:(int)score;


점수(거리)를 보여주는 Label을 인스턴스 변수로 추가한다. 그리고 외부에서 점수를 메시지로 보낼 수 있도록 메소드를 하나 생성한다. 


HUDLayer.m으로 이동해서 초기 설정을 위해 init 메소드에 아래와 같이 추가한다. 

- (id)init

{

    self = [super init];

    if (self) {

        //레이블의 초기값을 0M 으로 한다. 시스템 폰트 사용

        scoreLabel = [CCLabelTTF labelWithString:@"0M" fontName:@"Arial" fontSize:20];

        //위치를 정한다.

        scoreLabel.position = ccp(280, 450);

        //화면에 뿌리기위해 자식노드에 추가 

        [self addChild:scoreLabel];

    }

    return self;

}

시스템 폰트를 사용해서 문자열의 초깃값을 "0M"로 설정한다. 그리고 오른쪽 위에 배치한 후 자식 노드로 추가해서 화면에 보여준다.


그리고 다른 클래스에서 텍스트를 변경할 수 있도록 메소드를 작성한다. 

-(void)setScoreText:(int)score{

    //레이블을 위한 스트링

    NSString *scoreString = [NSString stringWithFormat:@"%dM", score];

    //레이블의 스트링에 할당한다.

    scoreLabel.string = scoreString;

}

점수값을 받으면 스트링으로 변환한다. 그리고 레이블의 스트링값에 넣어준다. 


기본적인 HUDLayer를 작성하였다.


GameLayer에 적용을 위해서 GameLayer.h로 이동을 한다. 

점수를 저장할 인스턴스 변수 하나를 아래와 같이 추가한다. 

    int score;


그리고 프로퍼티를 설정한다. 

@property (nonatomicweakHUDLayer *hud;


GameLayer.m으로 이동해서 init 함수에서 아래와 같이 코드를 추가한다.

        //점수를 위한 초기화

        score = 0;

시작할 때 점수를 0으로 초기화한다. 


그리고 시간이 흘러감에 따라서 점수를 올려줄 수 있게 아래와 같이 코드를 추가한다.

//일정 시간마다 점수를 올려준다.

-(void)updateScore:(ccTime)dt{

    [_hud setScoreText:score++];

}

움직이는 거리를 계산하기보단 간단하게 시간을 거리로 두면 간단하게 계산이 된다. 한번 호출될 때 마다 1M씩 점수를 올려준다. 


그리고 onEnter 메소드에 아래와 같이 코드를 추가한다.


- (void)onEnter {

    [super onEnter];

...

    //점수를 위한 스케쥴

    [self schedule:@selector(updateScore:) interval:0.01f];

}


0.01초마다 updateScore: 메소드를 호출을 하는 스케쥴러를 추가한다. 


끝난 것처럼 보인다. 하지만 HUDLayer를 자식 노드에 추가하지 않아서 지금은 화면에 보이지 않는다. 


GameScene.h으로 이동해서 HUDLayer를 추가하도록 하자.


#import "HUDLayer.h"

...

@property (nonatomic, weak) HUDLayer *hudLayer;

HUDLayer를 임포트 하고 프로퍼티로 설정을 한다. 


GameScene.m으로 이동해서 init 메소드를 아래코드로 변경을 한다. 

- (id)init

{

    self = [super init];

    if (self) {

        //HUD 레이어 추가하기

        _hudLayer = [HUDLayer node];

        [self addChild:_hudLayer z:1];

        //Game 레이어 추가하기

        _gameLayer = [GameLayer node];

        [self addChild:_gameLayer z:0];

      

        //게임 레이어의 HUD HUD레이어 전달

        self.gameLayer.hud = _hudLayer;

    }

    return self;

}

HUDLayer를 자식노드로 추가를 하고 GameLayer의 hud에 hudLayer를 전달한다. 


Cmd(⌘) + R을 눌러서 실행을 해보자. 

시간이 지남에 따라서 오른쪽 위에 거리를 나타내는 점수가 갱신면서 보여지는 것을 확인할 수 있다. 




다음 장에서는 적이 터질때 터지는 애니메이션을 구현하도록 하겠다. 

Posted by KraZYeom

댓글을 달아 주세요

  1. 김성우 2013.02.10 14:32  댓글주소  수정/삭제  댓글쓰기

    배경음악과 사운드 입력에 대해서도 알려주세요 ^^;;

    • Favicon of https://www.appilogue.kr BlogIcon KraZYeom 2013.02.11 06:06 신고  댓글주소  수정/삭제

      넵, 공개된 음원을 구하거나..
      제가 만들어놔야하는데... 개을러서 찾질 못 했습니다.
      찾으면 바로 추가하도록 하겠습니다.
      다다음 파트에 들어갈 것 같아요

기본적인 게임 기능은 다 구현되었지만 밋밋한 화면을 역동적으로 하기 위해서 애니메이션을 추가하도록 하겠다. 이번 장에서는 플레이어와 적의 날갯짓 애니메이션을 구현하겠다. 


날갯짓 애니메이션

적의 날갯짓을 하기 위해서는 기본적인 날개 스프라이트를 왼쪽 날개는 오른쪽에 오른쪽 날개는 왼쪽에 축을 두고 위아래로 살짝씩만 회전을 하면 된다. 


우선 enemy.h 로 이동을 해서 아래 코드를 변수로 추가한다.

    BOOL wingDown;

날개가 내려갔는지 올라갔는지를 체크하기 위한 변수이다. 


그리고 본격적으로 애니메이션을 구현하기 위해 enemy.m 파일로 이동한다. 

특정 시간마다 반복적으로 메소드를 호출해야 하기때문에 스케쥴러를 위한 메소드 -(void)updateWings:(ccTime)dt 를 하나 만들어 아래와 같이 코드를 추가한다.

-(void)updateWings:(ccTime)dt {

    //왼쪽 오른쪽 날개짓을 위환 회전

    CCRotateTo *leftWingDown = [CCRotateTo actionWithDuration:0.2 angle:-80];

    CCRotateTo *leftWingUp = [CCRotateTo actionWithDuration:0.2 angle:0];

    CCRotateTo *rightWingDown = [CCRotateTo actionWithDuration:0.2 angle:80];

    CCRotateTo *rightWingUp = [CCRotateTo actionWithDuration:0.2 angle:0];


    //번갈아 가면서 날개짓을 한다.

    if ( (wingDown = !wingDown) ){

        [_leftWing runAction:leftWingDown];

        [_rightWing runAction:rightWingDown];

    }else{

        [_leftWing runAction:leftWingUp];

        [_rightWing runAction:rightWingUp];

    }

}

회전을 위한 액션인 CCRotateTo을 날개가 내려갔을 때, 올라갔을 때 그리고 왼쪽, 오른쪽 날개 4개의 액션을 생성한다. 시간은 0.2초, 회전 각도는 80도로 한다. 그리고 내려갔을 경우는 올라가는 애니메이션을 실행하고, 올라갔을 경우는 내려가는 애니메이션을 실행한다. 


그리고 실행하면 바로 애니메이션을 호출하기 위해서 onEnter 메소드에 아래와 같이 스케줄러 코드를 추가한다.

-(void)onEnter{

...

    //날개짓을 위한 0.2 마다 메소드 호출

    [self schedule:@selector(updateWings:) interval:0.2];

}



Cmd(⌘) + R을 눌러서 실행을 해보자. 




적의 날개를 추가하는 방법과 같은 방법으로 플레이어 캐릭터에도 적용을 하도록 하자. 


Player.h 파일로 이동해서 날갯짓을 위한 변수를 하나 만든다.

    BOOL wingDown;


구현을 위해 Player.m 파일로 이동한다. 


Enemy.m 과 똑같은 방법으로 특정 시간마다 반복적으로 메소드를 호출해야 하므로 스케쥴러를 위한 -(void)updateWings:(ccTime)dt 메소드를 만들어 아래와 같이 코드를 추가한다.

-(void)updateWings:(ccTime)dt{

    //왼쪽 날개 에니메이션을 위한 날개 내렸다 올리기

    CCRotateTo *leftWingDown = [CCRotateTo actionWithDuration:0.2 angleX:-30 angleY:60];

    CCRotateTo *leftWingUp = [CCRotateTo actionWithDuration:0.2 angleX:0 angleY:0];

    //오른쪽 날개 에니메이션을 위한 날개 내렸다 올리기

    CCRotateTo *rightWingDown = [CCRotateTo actionWithDuration:0.2 angleX:30 angleY:-60];

    CCRotateTo *rightWingUp = [CCRotateTo actionWithDuration:0.2 angleX:0 angleY:0];

    //날개짓을 번갈아 가기 위해

    if ( (wingDown = !wingDown) ){

        [_leftWing runAction:leftWingDown];

        [_rightWing runAction:rightWingDown];

    }else{

        [_leftWing runAction:leftWingUp];

        [_rightWing runAction:rightWingUp];

    }

}


적의 날갯짓 애니메이션과 다른 점이 보일 것이다. 적은 단순히 고정축인 Z축으로 으로 회전을 했다면, 플레이어 캐릭터의 날개는 X축과 Y축으로 회전을 해서 약간 비트는 느낌이 들게 한다. 회전을 위한 액션인 CCRotateTo을 내려갔을 때, 올라갔을 때 그리고 왼쪽, 오른쪽 날개 4개의 액션을 생성한다. 시간은 0.2초, x축 회전 각도는 30도, y축 회전 각도는 60도로 한다. 그리고 내려갔을 경우는 올라가는 애니메이션을 실행하고, 올라갔을 경우는 내려가는 애니메이션을 실행한다. 


그리고 실행하면 바로 애니메이션을 호출하기 위해서 onEnter 메소드에 아래와 같이 스캐쥴러 코드를 추가한다.

-(void)onEnter{

...

    //0.2초에 한번씩 날개짓을 한다.

    [self schedule:@selector(updateWings:) interval:0.2];

}


0.2초에 한번씩 날갯짓을 한다.


Cmd(⌘) + R을 눌러서 실행을 해보자. 


정적인 느낌의 게임이 날갯짓 애니메이션 하나를 추가하니, 좀더 생동감이 넘치게 되었다. :-)


다음은 화면에 점수를 표시하는 레이어인 HUD를 추가하도록 할 예정이다. 



Posted by KraZYeom

댓글을 달아 주세요

  1. 김성우 2013.02.10 14:28  댓글주소  수정/삭제  댓글쓰기

    잘 보고 있습니다. 정말 도움이 많이 되었어요

이번 파트에서는 총알과 플레이어 충돌,  적과 플레이어 충돌 그리고 적의 에너지 게이지 추가를 구현하겠다. 


총알과 플레이어 케릭터 충돌처리


총알과 플레이어 케릭터의 충돌처리는 아주 간단하다. 플레이어 스프라이트의 바운딩 박스 영역과 총알 스프라이트의 바운딩 박스의 영역이 겹치면 충돌했다고 처리하면 된다. 물론 더 세세한 충돌처리를 위해서는 케릭터의 모양을 여러 개의 영역으로 나눠서 충돌했는지를 체크하거나, 더 정확하게 하려면 플레이어 케릭트 외곽선을 전부 따서 하는 방법도 있다. 여러가지 방법이 있긴 하지만 가장 간단한 방법을 선택하도록 한다. 

gameLayer.h 에 이동하여 충돌하였는지를 체크 할수 있게 변수하나를 추가한다. 

    BOOL isCollision;


충돌 처리를 위해서 gameLayer.m으로 이동해서 - (void)update:(ccTime)dt  메소드에 아래 코드를 추가한다.   

  //총알 플레이어와 케릭터 충돌을 위해서 배열에서 적을 하나 꺼낸다.

for (Enemy *enemy in enemysArray) {

    //적이 죽은 상태이면 그냥 넘어간다.

    if (!enemy.state) continue;

    

    //총알을 하나 배열에서 꺼낸다

    for (Bullet *bullet in bulletsArray) {

        //총알이 적에 맞아서 없어진 상태면 그냥 넘어간다.

        if (!bullet.visible) continue;

        

        //총알과 적이 충돌이 나는지를 체크

        if (!isCollision && CGRectIntersectsRect(bullet.boundingBox, enemy.boundingBox)){

            //총알을 없애고

            bullet.visible = NO;

            //싸운드 효과를 재생한다.

            //미사일로 적을 공격해서 0점을 받아오는지를 체크

            if (![enemy attackedWithPoint:[bullet bulletType]]){

                //싸운드 효과를 재생한다.

                //적이 폭파되면 먼지를 뿌려주기위한 에니메이션

            }

        }

    }

}
적을 담고 있는 배열에서 적을 꺼내서 죽었는지 상태 체크를 하고, 죽지 않았으면 총알의 배열에서 총알을 꺼내서 총알이 맞아서 없어진 상태인지를 체크 한다. 그리고 CGRectIntersectsRect(CGRect rect1, CGRect rect2)메소드를 통해서 두개의 영역이 겹쳤는지를 체크 한다. 충돌이 일어났으면 총알을 없애고, 총알 타입에 따른 적이 얼마나 피해를 입었는지를 체크 한다. 

총알에 따라서 적이 얼마나 피해를 입었는지 체크를 위해서 Enemy.h 로 이동해서 아래 코드를 추가한다. 

-(NSInteger)attackedWithPoint:(NSInteger)point;


Enemy.m으로 이동해서 아래 코드를 추가한다. 

-(NSInteger)attackedWithPoint:(NSInteger)point {

    //공격 받은 숫자 만큼 에너지를 줄여준다.

    _energy -= point;

    //0미만으로 되면 0 아니면 현재 에너지를 반환

    if ( _energy <= 0 ) {

        //죽으면 숨긴다

        [self destroy];

    }

    return _energy;

}


-(void)destroy {

    //적이 죽으면 상태 값을 변경

    _state = kDestoryed;

    //에너지는 0으로 한다.

    _energy = 0;

    //그리고 숨긴다.

    self.visible = NO;

}

미사일의 포인트 만큼 적의 에너지를 줄이고, 0이하가 되면 적을 죽인다. 상태값을 죽은 상태로 하고 에너지는 0, 그리고 숨긴다.  

적과 플레이어 케릭터 충돌처리

적과 플레이어 케릭터 충돌처리도 간단하다. 총알과 적과 충돌처리처럼 겹치는 영역이 있으면 충돌처리를 하고 게임을 끝나게 하면 된다. 


gameLayer.m 에서 위에 추가한 for (Enemy *enemy in enemysArray) {} 괄호 안에서 마지막에 아래 코드를 추가한다. 


//적과 플레이어 케릭터가 충돌하는지를 체크

if (!isCollision && CGRectIntersectsRect(enemy.boundingBox, _player.boundingBox)) {

    isCollision = YES;

    if (isCollision){

        _player.visible = NO;

        // 충돌하게 되면 총알을 다 없앤다.

        [self unschedule:@selector(updateBullet)];

        for (Bullet *bullet in bulletsArray) {

            bullet.visible = NO;

            [bullet removeFromParentAndCleanup:YES];

        }

        

        CCCallBlock *allStop = [CCCallBlock actionWithBlock:^{

            //터치 이벤트를 더이상 받지 않는다.

            self.isTouchEnabled = NO;

        }];

        //딜레이를 위한 엑션

        CCDelayTime *delay = [CCDelayTime actionWithDuration:2.0f];

        //딜레이후 메뉴로 나가기 위한 엑션 블럭

        CCCallBlock *block = [CCCallBlock actionWithBlock:^{

            //메뉴 레이어로 돌아간다.

            [[CCDirector sharedDirector] replaceScene:[MenuLayer scene]];

        }];

        //엑션을 순서대로 준비.

        CCSequence *seq = [CCSequence actions:allStop, delay, block, nil];

        //엑션 실행

        [self runAction:seq];

    }

}

메소드가 짧은 시간에 여러번 호출 되기 때문에 충돌이 났는지 나지 않았는지를 체크한다. 충돌이 없는 상태에서 영역이 겹치게 되면, 충돌 체크를 하고 총알 스케줄러를 멈춰서 더 이상 총알이 나가지 않게 한다. 콜백 블럭에 터치를 받지 않게 처리하고, 바로 끝나면 이상하므로 2초간 게임화면이 보이게 한다. 그리고 다시 콜백 블럭으로 메뉴 화면으로 빠져 나가게 한다. 이 3개의 엑션들을 순차적으로 액션처리 한다. 

모든 게임 기본 기능은 다 구현 되었다. 그런데 적이 총알을 맞았는데 얼마만큼 에너지가 줄었는지 체크를 해야 한다. 적의 하단에 에너지 게이지를 추가 하도록 하겠다. 

CCNode를 상속받아서 EnegyGauge라는 클래스를 추가한다 
그리고 EnergyGauge.h에 아래 코드를 추가한다. 

@interface EnergyGauge : CCNode {

    CGFloat maxValue;

    CGFloat currentValue;

    CGSize maxSize;

}

+ (id)initWithMaxSize:(CGSize)size maxValue:(CGFloat)_maxValue;

- (id)initWithMaxSize:(CGSize)size maxValue:(CGFloat)maxVal;

- (void)updateBar:(CGFloat)_currentValue;

게이지의 최대값, 현재값 그리고 최대 크기를 저장하는 변수를 생성하고, 생성하는 메소드와 외부에서 게이지바를 변경할 수 있게 해주는 메소드를 추가한다. 

EnergyGauge.m으로 이동해서 생성 관련 메소드를 아래와 같이 추가하여 작성한다. 

+(id)initWithMaxSize:(CGSize)size maxValue:(CGFloat)_maxValue{

     return [[self alloc] initWithMaxSize:size maxValue:_maxValue];

}


- (id)initWithMaxSize:(CGSize)size maxValue:(CGFloat)_maxValue{

    self = [super init];

    if ( self ){

        maxSize = size;

        currentValue = maxValue = _maxValue;

    }

    return self;

}



에너지 게이지를 스프라이트를 사용해서 그릴수도 있지만 간단하게 OpenGL을 이용해서 그리도록 한다. 아래 코드를 복사, 붙여 넣기한다. 

- (void)draw{

    //최대값 기준으로 % 환산한 너비

    CGFloat width = (currentValue / maxValue) * (float)maxSize.width;

    //라인의 두깨는 높이값이다.

    glLineWidth(maxSize.height);

    //100% 때는 Green

    if (currentValue == maxValue) {

        ccDrawColor4B(255, 255, 0, 255);

    // 외에는 Red

    } else {

        ccDrawColor4B(255, 0, 0, 255);

    }

    //선을 그린다.

    ccDrawLine(ccp(0, 0), ccp(width, 0));

}

너비 값을 구하고, 게이지의 두깨를 설정한다. 그리고 최대 값일때는 노란색(배경색이 초록색이여서 노락색으로 변경함), 아닐 경우에는 빨간색으로 설정한다. 그리고 길이, 두깨 만큼 선을 그린다. 

외부에서 게이지바의 업데이트 하기 위해 아래 코드를 추가한다. 

- (void)updateBar:(CGFloat)_currentValue{

    //값을 값을 현재 값으로 할당한다.

    currentValue = _currentValue;

    //0 이하면 0으로, 최대값을 초과하면 최대값으로 보정한다.

    if (currentValue < 0 ) currentValue = 0;

    else if (currentValue > maxValue) currentValue = maxValue;

}

현재 값을 할당하고, 0미만이면 0으로 설정하고 최대 값을 벗어 나면 최대값으로 설정한다. 

적에게 게이지를 추가하기 위해서 Enemy.h로 이동해서 프로퍼티를 아래와 같이 추가한다. 

@property (nonatomic, strong) EnergyGauge *gauge;


그리고 - (id)init 메소드 아래에 다음 코드를 추가한다. 

        //에너지 게이지 객채를 생성한다

        _gauge = [EnergyGauge initWithMaxSize:CGSizeMake(self.boundingBox.size.width, 10) maxValue:100];

        //에너지 게이지를 자식으로 추가.

        [self addChild:_gauge];

게이지 객체를 너비와 높이값 그리고 최대 값으로 생성한다. 그리고 자식으로 추가한다. 

적이 공격을 받았을 경우 처리를 하기 위해서 -(NSInteger)attackedWithPoint:(NSInteger)point 메소드 안 _energy -= point; 코드 아래에 다음 코드를 추가한다.

    //게이지 업데이트

    [_gauge updateBar:_energy];



그리고 적이 초기화 되었을 때 에너지 게이지를 초기화 하기 위해서  -(void)reset 메소드 가장 아래 부분에 다음 코드를 추가한다 

    //죽어서 안보이던 적을 다시 보여준다.

    [self setVisible:YES];

    //상태값을 일반 상태로 변경 한다.

    _state = kNormal;



    //게이지 바를 초기화 한다.

    [_gauge updateBar:_energy];



Cmd(⌘) + R 해서 Run을 해보자. 총알과 적이 충돌을 하면 에너지가 줄어드는 모습을 볼 수 있고, 이리저리 움직이다가 적과 충돌을 하게 되면 게임이 종료되는 것을 확인 할 수 있다. 

기본적 게임 기능은 모두 추가 되었다. 다음 파트에서는 점수를 나타내는 HUD 추가와 폭파할때 일어나는 에니메이션 등을 구현 하도록 하겠다. 


시뮬레이터 상에서 스프라이트 이미지가 많이지면서 프레임 레이트가 20 밑으로 뚝뚝 떨어지는 것을 볼 수 있다. 실제 기기 상에서는 문제 없이 나온다. 시뮬레이터와 단말기에서 사용하는 OpenGL 이 달라서 그렇다. 

Posted by KraZYeom

댓글을 달아 주세요

이번 파트에서는 총알을 생성하고 총알과 적의 충돌처리 하는 것을 구현하겠다. 


총알 추가

플레이어 케릭터에서는 특별한 액션이 필요하지 않고 끊임없이 총알이 발사 된다. 레벨에 따라서 총알의 모양, 강도만 다를 뿐이다. 


총알 스프라이트를 구현하기 위해서 Bullet 스프라이트를 CCSprite를 상속받아서 생성한다. 그리고 Bullet.h 이동한다. 아래 코드를 추가한다. 


//총알의 종류와 강도

typedef enum {

    //총알의 강도를 나타낸다.

    kFirst = 20

} BulletType;


@interface Bullet : CCSprite {

}


@property (nonatomic, readwrite) BulletType bulletType;

총알의 종류와 강도를 나타내는 변수와  총알 타입을 설정 할 수 있는 프로퍼티도 설정한다. 


Bullet.m 으로 이동해서 init 메소드를 아래와 같이 작성한다. 

- (id)init

{

    // 번째 총알 이미지

    self = [super initWithSpriteFrameName:@"bullet_01_01.png"];

    if (self) {

    //총알 타입을 정한다.

        _bulletType = kFirst;

    }

    return self;

}

스프라이트에 이미지를 넣고 총알 강도를 나타내는 타입을 지정한다. 이번 따라하기 시리즈에서는 레벨이 개념이 없으므로 그냥 한 개의 타입 밖에 없다. 


총알이 앞으로 나아가게 하기 위해 - (void)updateBullet:(ccTime)dt 메소드를 추가하고 아래 코드를 추가한다.  

- (void)updateBullet:(ccTime)dt {

    CGSize winSize = [[CCDirector sharedDirector] winSize];

    //총알의 움직임 가속도

    CGPoint bulletVel = ccp(0, 300);

    //총알의 현재위치

    CGPoint currentPostion = self.position;

    //화면 밖으로 넘어가면 총알을 숨긴다.

    if (currentPostion.y > winSize.height){

        self.visible = NO;

    } else{

        //아니면 계속해서 앞으로 보낸다.

        self.position = ccpAdd(ccpMult(bulletVel, dt), self.position);

    }

}

이전 적, 배경과 처럼, 총알의 속도를 정하고 현재 위치를 확인해서 화면 밖으로 넘어가면 숨김 처리한다. 아니면 계속해서 앞으로 나아가게 한다. 


updateBullet: 메소드를 호출을 위해서 onEnter 메소드에 아래 코드를 추가한다.

-(void)onEnter{

    [super onEnter];

    //스케쥴로 호출

    [self schedule:@selector(updateBullet:) interval:0.05];

}

0.05초 마다 updateBullet: 메소드를 호출한다.


기본적인 총알 스프라이트 구성은 완성되었다. GameLayer에 추가하여서 화면에 보이도록 하자.


GameLayer.h 화일로 이동해서 아래 코드를 추가한다. 

    //총알을 담을 배열

    CCArray *bulletsArray;

    //마지막 총알 확인용

    int lastBullet;


@property (nonatomic, weak) CCSpriteBatchNode *batchNode;


총알을 재사용하기 위해 배열을 사용하고 마지막으로 사용된 총알확인을 위한 변수를 하나 추가한다. 그리고 총알의 이미지 하나를 사용해서 여러개의 총알을 생성하기 위해서 배치노드를 사용한다. 배치노드 사용 성능에 관해서는 CCSpriteBatchNode 성능 테스트를 참조하도록 한다. 


GameLayer.h이동해서 상단에 총알 클래스를 임포트하는 코드를 아래와 같이 추가한다.

#import "Bullet.h"


init 메소드로 이동해서 아래 코드를 스프라이트 캐쉬위에 추가한다. 

//마지막 총알 번호를 위해 초기화

lastBullet = 0;


//총알등을 위해 배치노드 사용

_batchNode = [CCSpriteBatchNode batchNodeWithFile:@"dragonRideSprite.pvr.ccz"];

[self addChild:_batchNode];


//스프라이트 프레임 케쉬에 스프라이트를 저장한다.

[[CCSpriteFrameCache sharedSpriteFrameCacheaddSpriteFramesWithFile:@"dragonRideSprite.plist"];

마지막 총알 위치를 초기화하고 배치노드를 추가한다. 


총알을 생성하기 위해서 initBullet메소드를 생성하고 아래 코드를 추가한다.

#define kMaxBullet 30


-(void)initBullet {

    //총알 갯수만큼 배열을 만든다.

    bulletsArray = [[CCArray alloc] initWithCapacity:kMaxBullet];

    //총알 갯수만큼 배열에 넣는다.

    for (int i = 0; i < kMaxBullet; i++) {

        //총알 노드를 생성

        Bullet *bullet = [Bullet node];

        //처음에는 보이는 상태로 만든다.

        bullet.visible = NO;

        //총알의 위치는 플레이어 케릭터의 앞에 위치.

        bullet.position = ccp(_player.position.x, _player.position.y + _player.boundingBox.size.height / 2);

        //배치노드에 넣는다.

        [_batchNode addChild:bullet z:99];

        //충돌등 계산을 쉽게 하기 위해 배열에 넣는다

        [bulletsArray addObject:bullet];

    }

}

총알의 갯수는 30개로 하며 배력에 넣고 재사용한다. 이때 GameLayer에 바로 추가하는게 아니라 방금전에 생성했던 배치노드에 추가한다. 그렇게 되면 하나의 동일한 이미지로 효율성 있게 재사용하게 된다. 


init 메소드로 이동해서 아래 코드를 추가한다. 

        //총알 초기화

        [self initBullet];


기본적인 생성은 끝났으니 사용해보도록 구현하겠다. 


-(void)updateBullet:(ccTime)dt메소드를 생성하고 아래 코드를 추가한다. 

-(void)updateBullet:(ccTime)dt {

    //배열에서 하나씩 총알을 꺼낸다.

    Bullet *bullet = (Bullet*)[bulletsArray objectAtIndex:lastBullet];

    //움직일때는 보이게 설정

    bullet.visible = YES;

    //총알의 위치는 플레이어 케릭터의 앞에 위치.

    bullet.position = ccp(_player.position.x, _player.position.y + _player.boundingBox.size.height / 2);

    //마지막 총알이 배열의 마지막이면 다시 초기화 한다.

    if (++lastBullet == kMaxBullet) {

        lastBullet = 0;

    } 

}

배열에서 마지막으로 사용했던 총알위치에서 총알을 꺼내고 보이게 설정한다. 총알의 위치는 플레이어 케릭터의 위이다. 


onEnter메소드에 위에 생성했던 메소드를 스케쥴러를 통해서 호출하기 위해서 아래 코드를 추가한다. 

    [self schedule:@selector(updateBullet:) interval:0.05f];


Cmd(⌘) + R을 해서 Run을 해보자. 그러면 총알이 플레이어 케릭터에서 마구 나가는 모습을 확인 할 수 있다. 





Posted by KraZYeom

댓글을 달아 주세요

  1. 지르미 2013.01.29 16:46  댓글주소  수정/삭제  댓글쓰기

    이걸보고나니 코코스가 이해가 정말 잘되요
    드래곤 플라이트편 끝나고 다른방식의 게임도 풀이 부탁합니다 ^^

* 혹시 안되는것이 있거나 하면 바로 리플로 알려주세요. 구현하면서 글을 작성해야 좋은데, 구현 해놓고 역으로 글을 쓰니까 코드가 살짝 꼬이는것이 있네요. 생각하면서 작성을 하고 있긴 하지만 이상한 곳이 많을 것 같습니다. 



이번 파트에서는 적 케릭터를 추가하고 적이 없어지고 다시 생성되는 것을 구현하겠다.


적 케릭터 만들기 

드래곤 플라이트에서 적은 적의 레벨에 따라서 여러 종류의 색상으로 달라진다. 하얀색이 약한 놈, 노란색은 조금더 강한 놈 ... 점점 강하게 된다. 적의 스프라이트를 상세하게 분석하면 몸통, 기본과 공격 받을때가 달라지는 양쪽 눈, 날개 짓 하는 양쪽 날개 그리고 에너지 상태를 나타내는 에너지 바로 구분 할 수 있다. 

그리고 다섯 마리의 적들이 한 줄로 나란하게 나온다. 아래로 움직여서 내려오고 화면 밖으로 벗어나면 다시 상단에서 생성되어 내려온다. 계속해서 반복한다. 


이런 컨셉으로 적을 만들어 보도록 하겠다. 


새로운 클래스의 이름을 Enemy로 CCSprite를 상속받아서 만든다. 


그리고 Enemy.h 화일에 적의 스프라이트를 생성을 위한 프로퍼티를 아래와 같이 추가한다. 

//두종류의

typedef enum {

    kRed = 0,

    kWhite

} EnemyType;



//적의 상태를 나타내기 위해 

typedef enum {

    kDestoryed = 0,

    kNormal = 1

} State;


@interface Enemy : CCSprite {

    BOOL wingDown;

}


//적의 종류 : 약한놈과 강한놈으로 가지로만 구분

@property (nonatomic) EnemyType enemyType;

//현재 상태 : 죽었는지, 살았는지로 구분

@property (nonatomic) State state;

//에너지

@property (nonatomic) NSInteger energy;

//왼쪽 날개

@property (nonatomic, weak) CCSprite *leftWing;

//오른쪽 날개

@property (nonatomic, weak) CCSprite *rightWing;

//왼쪽

@property (nonatomic, weak) CCSprite *leftEye;

//오른쪽

@property (nonatomic, weak) CCSprite *rightEye;


-(void)reset;

그리고 init 메소드에 다음 코드를 추가 한다. 

- (id)init

{

    //몬스터 스프라이트를 위한 기본 이미지를 추가한다.

    self = [super initWithSpriteFrameName:@"dragon_01_body.png"];

    if (self) {

        //적의 기본 에너지는 100

        _state = kNormal;

        _energy = 100;

        //에너지 게이지를 만든다.

        wingDown = NO;

        

        //왼쪽 날개 스프라이트

        _leftWing = [CCSprite spriteWithSpriteFrameName:@"dragon_01_wing.png"];

        _leftWing.anchorPoint = ccp1.00 );

        //중간 정도로 위치 시킨다.

        _leftWing.position = ccp(10self.boundingBox.size.height / 2);

        [self addChild:_leftWing];

        

        //오른쪽 날개 스프라이트

        _rightWing = [CCSprite spriteWithSpriteFrameName:@"dragon_01_wing.png"];

        //동일한 이미지를 반대도 회전 시켜서 사용

        _rightWing.flipX = YES;

        _rightWing.anchorPoint = ccp0.00 );

        //중간 정도로 위치 시킨다.

        _rightWing.position = ccp(self.boundingBox.size.width - 10self.boundingBox.size.height / 2);

        [self addChild:_rightWing];

        

        //왼쪽  스프라이트

        _leftEye = [CCSprite spriteWithSpriteFrameName:@"dragon_eye_1.png"];

        //왼쪽 눈의 위치를 설정한다

        _leftEye.position = ccp(1030);

        [self addChild:_leftEye];


        //오른쪽  스프라이트

        _rightEye = [CCSprite spriteWithSpriteFrameName:@"dragon_eye_1.png"];

        //왼쪽 눈의 위치를 설정한다

        _rightEye.position = ccp(3030);

        [self addChild:_rightEye];

    }

    return self;

}


적 노드가 생성될때 몸에 해당하는 스프라이트를 추가하고, 상태를 기본상태로 설정하고 에너지는 100으로 설정한다. 에너지는 적의 종류에 따라 추후 변경된다. 그리고 왼쪽, 오른쪽 날개 스프라이트를 추가한다. 나중에 날개짓 에니메이션을 위해서 엥커포인트를 조정한다. 그리고 왼쪽, 오른쪽 눈 스프라이트를 추가한다. 


일정시간 마다 적을 아래로 움직이게 하기 위해서 init 메소드 아래에 다음 코드를 추가한다.

-(void)update:(ccTime)dt {

    //적의 움직임 가속도 

    CGPoint enemyScrollVel = ccp(0, -250);

    //현재 적의 위치 

    CGPoint enemyPos = [self position];

    //화면 아래로 벗어나는지 체크

    if (enemyPos.y + self.boundingBox.size.height / 2 <= 0) {

        //벗어나면 리셋 한다.

        [self reset];

        //아닐 경우

    } else{

        //움직이게 위치값을 아래로 이동

        enemyPos = ccpAdd(ccpMult(enemyScrollVel, dt), enemyPos);

        //위치 시킨다.

        [self setPosition:enemyPos];

    }

}

시작점으로 부터 일정 주기마다 자신의 현재 위치에서 계속 아래로 내려온다. 그리고 자신의 바운딩 박스 크기가 영역을 벗어나면 초기 위치로 이동한다. 


영역을 벗어나면 초기 위치로 이동하고, 적의 레벨을 랜덤으로 변경한다. 아래 코드를 추가하자. 

#define kMaxEnemyType 2


-(void)reset{

    CGSize winSize = [[CCDirector sharedDirectorwinSize];

    //적의 위치를 화면 상단으로 부터 시작한다.

    self.position = ccpself.position.x, winSize.height + self.boundingBox.size.height / 2 - 1 );

    //적의 이미지를 바꿔주기 위해 랜덤  생성

    int random = ( (float)arc4random() / (float)0xffffffff ) * kMaxEnemyType;

    

    switch (random) {

        case kWhite:

            //에너지를 100으로 할당

            _energy = 100;

            //적의 타입설정

            _enemyType = kWhite;

            //이미지 변경을위해 프레임에 파일이름으로 스프라이트 프레임 케쉬로 부터 찾아서 할당

            [self setDisplayFrame:[[CCSpriteFrameCache sharedSpriteFrameCachespriteFrameByName:@"dragon_01_body.png"]];

            [_leftWing setDisplayFrame:[[CCSpriteFrameCache sharedSpriteFrameCachespriteFrameByName:@"dragon_01_wing.png"]];

            [_rightWing setDisplayFrame:[[CCSpriteFrameCache sharedSpriteFrameCachespriteFrameByName:@"dragon_01_wing.png"]];

            _rightWing.flipX = YES;

            break;

        case kRed:

            //에너지를 200으로 할당

            _energy = 200;

            //적의 타입설정

            _enemyType = kRed;

            //프레임에 파일이름으로 스프라이트 프레임 케쉬로 부터 찾아서 할당

            [self setDisplayFrame:[[CCSpriteFrameCache sharedSpriteFrameCachespriteFrameByName:@"dragon_02_body.png"]];

            [_leftWing setDisplayFrame:[[CCSpriteFrameCache sharedSpriteFrameCachespriteFrameByName:@"dragon_02_wing.png"]];

            [_rightWing setDisplayFrame:[[CCSpriteFrameCache sharedSpriteFrameCachespriteFrameByName:@"dragon_02_wing.png"]];

            _rightWing.flipX = YES;

            break;

        default:

            break;

    }

}


화면의 상단 밖으로 적의 스프라이트를 위치하고, 랜덤으로 적의 종류를 선택한다. 적의 종류에 따라서 에너지와 관계된 스프라이트의 이미지를 변경을 한다. 


-(void)onEnter{

    [super onEnter];

    //스케쥴러 호출

    [self scheduleUpdate];

}

-(void)update:(ccTime)dt;  스케쥴러 메소드를 호출하기 위해서 onEnter 되었을 때 [self scheduleUpdate];를 호출 한다.


적이 완성되었으니 이제는 적을 게임 화면에 추가 할 차례이다. GameLayer.h로 이동해서 아래 코드를 추가 하도록 하자.

    CCArray *enemysArray; //적을 저장할 배열

적을 배열에 넣고, 재사용을 할 목적의 배열을 생성한다. 


GameLayer.m으로 이동해서 아래 코드를 추가한다. 

#import "Enemy.h"

상단에 아래와 같이 적을 불러올 수 있게 임포트 해준다.


그리고 적을 생성하기 위해서 위해서 아래코드를 추가한다. 

#define kMaxMonster //기본 적의 수는 5마리


- (void)initEnemys {

    //적을 저장할 배열을 생성한다.

    enemysArray = [[CCArray allocinitWithCapacity:kMaxMonster];

    //화면을 균등하게 나눈다.

    float width = winSize.width / kMaxMonster;

    //적의 최대 갯수 만큼 화면에 나타내고배열에 저장

    for ( int i = 0; i < kMaxMonster ; i++ ) {

        // 노드를 생성.

        Enemy *enemy = [Enemy node];

        //화면에 위치 시킨다.

        [self addChild:enemy z:98];

        //균등하게 나눈 화면에서 가운데에 위치 시킨다.

        enemy.position = ccp( i * width + width / 2winSize.height + enemy.boundingBox.size.height / 2);

        //그리고 나중에 충돌 등을 위해 배열에 넣는다.

        [enemysArray addObject:enemy];

    }

}

배열을 생성하고 화면에서 균등하게 나눈 위치에 적을 위치시키고 배열에 저장한다. 배열에 넣는 이유는 재사용하기도 좋고, 나중에 충돌 등 각각을 다루는 것 보다 배열에서 꺼내서 사용하는게 더 편리 하기 때문이다. 


- (void)initEnemys호출을 위해서 init 메소드에서 [self initPlayer];  바로 아래에 다음 코드를 추가한다.

        [self initEnemys];


Cmd(⌘) + R으로 Run을 해보자. 아래 동영상 처럼 적들이 아래로 내려가고, 화면에서 없어지면 위에서 또 생성되서 나온다. :]





다음 파트에서는 플레이어가 총알을 쏘고, 적을 없애는 것을 다루도록 하겠다. 



* 혹시 안되는것이 있거나 하면 바로 리플로 알려주세요. 구현하면서 글을 작성해야 좋은데, 구현 해놓고 역으로 글을 쓰니까 코드가 살짝 꼬이는것이 있네요. 생각하면서 작성을 하고 있긴 하지만 이상한 곳이 많을 것 같습니다. 


Posted by KraZYeom

댓글을 달아 주세요

  1. 표독이 2013.01.24 08:55  댓글주소  수정/삭제  댓글쓰기

    드디어 올라 왔네요!! 감사합니다.

  2. Favicon of https://red-submarine.tistory.com BlogIcon 빨간잠수함。 2013.02.26 23:22 신고  댓글주소  수정/삭제  댓글쓰기

    안녕하세요 뒤늦게 정주행중에 매의눈으로 발견한 오타입니다
    책으로 내실때 수정해주세요 ㅎㅎ

    kDestoryed -> kDestroyed

    //오른쪽 눈 스프라이트
    _rightEye = [CCSprite spriteWithSpriteFrameName:@"dragon_eye_1.png"];
    //왼쪽 눈의 위치를 설정한다 ->오른쪽 눈의 위치를 설정한다
    _rightEye.position = ccp(30, 30);

    잘보고 있습니다 튜토리얼 많이 만들어주세요 ㅎㅎㅎ

  3. Favicon of https://red-submarine.tistory.com BlogIcon 빨간잠수함。 2013.04.13 16:30 신고  댓글주소  수정/삭제  댓글쓰기

    후... 두달전에 cocos2d로 따라할때도 따라하다가 버그 있어서 제잘못인줄 알고... 때려쳤...었는데..
    이번에 cocos2d-x 공부하면서 krazyeom님의 튜토리얼을 cocos2d-x로 포팅하면서 공부하는중에 원인을 발견해서 코멘트합니다
    reset 메서드에서
    self.visible = YES;
    _state = kNormal;
    두줄이 빠져서 한번 죽은자리는 안나오는 현상이 나옵니다ㅎㅎ