Play Scene Class

1. PlayScene.h

추후 수정 가능성이 있거나 필요할 것으로 예상되는 부분을 define으로 정의해 두었다.


SCREENX: 스크린의 너비, 아이폰을 세로로 들었을 때의 너비이다.

SCREENY: 스크린의 높이

ENEMYMAKE_INTERVALTIME: 적 기체가 생성되는 시간의 간격

TAG: 각종 상황에서의 관리 및 처리를 위한 꼬리표

DEPTH: TAG와 같은 목적으로 정의한 각 스프라이트 객체의 레이어 층

SCORE_PER_ENEMY1: 적 기체를 파괴했을 때 점수

2. PlayScene.m

(1) init

init에서는 각 스프라이트 객체를 생성하며 스케줄러를 등록한다.


① self.isTouchEnables

터치 이벤트를 활성화한다.


② 플레이어 기체(player) 스프라이트 작업

터치한 곳에 따라 이동하는 플레이어 기체이다.

스프라이트의 이름은 알아보기 쉽게 player로 결정했다.

최대한 빠르고 간단하게 만들기 위해 그래픽 작업 대신 삼각형 모양의 png 이미지를 쓰기로 했다. 플레이어는 정삼각형으로, 적기는 역삼각형으로 했다.

플레이어 기체의 현재 위치를 기준으로, 터치한 곳의 x 좌표를 판단하여 그 쪽으로 이동하게 된다.


③ 게임 스코어 레이블

화면에 스코어를 표시하는 간단한 코드


④ 배열 초기화

플레이어 기체는 화면에 하나만 존재하지만, 적기와 총알은 매우 많이 생성될 수 있다. 이것들의 관리는 배열로 할 것이고, init에서 초기화한다.


⑤ 스케줄러

적 기체의 생성, 충돌 체크는 스케줄러로 등록하여 따로 처리한다.

(2) ccTouchesBegan

터치 이벤트가 발생했을 때 처리할 것들이다.

첫 번째로는 플레이어를 터치한 방향으로 이동시킨다.

두 번째는 미사일을 발사한다.


① UIKit 좌표를 OpenGL 좌표로 변환

OpenGL 좌표로의 변환 공식은 스터디에서 익혔던 내용을 그대로 사용하면 된다.

-(void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)withEvent {
    /// ---------- 이 코드를 수정할 일은 거의 없다.

    // UITouch는 좌상단이 (0,0)이다.
    UITouch *touch=[touches anyObject];

    CGPoint touchPoint;

    // 터치가 발생한 좌표를 얻는다.
    touchPoint = [touch locationInView:[touch view]];

    //GL 좌표로 변환. 좌하단을 (0,0)으로 변환한다.
    touchPoint = [[CCDirector sharedDirector] convertToGL:touchPoint];

    /// ----------
}

OpenGL 좌표 변환 코드로부터 touchPoint(터치한 곳의 좌표)를 얻을 수 있다.

이렇게 얻은 touchPoint 좌표를 플레이어 기체의 이동 액션에 활용하게 된다.


② 플레이어 기체의 x 좌표 수정

터치한 좌표를 기준으로 플레이어 기체를 왼쪽으로 이동시키거나 오른쪽으로 이동시킨다.

이 때 터치한 좌표가 아이폰의 화면 가장자리일 경우, 기체가 화면 밖을 벗어날 수 있는데, x 좌표를 확인하고 있다가 이런 상황이 생길 경우 x 좌표를 수정하면 된다.


왼쪽 스크린샷의 상황이 생기지 않도록 player의 x 좌표를 수정한다.

// SCREENX = 320
// SCREENY = 480

// 터치좌표 x가 (SCREENX-플레이어 너비/2)보다 크면
if (touchPoint.x > SCREENX-player.contentSize.width) {
    // SCREENX - (플레이어 너비/2)
    touchPoint.x = SCREENX-player.contentSize.width/2;
}
// 터치한 좌표가 (플레이어 너비/2)보다 작으면 [예]20
if (touchPoint.x < player.contentSize.width/2) {
    // 현재 플레이어 x좌표를 (플레이어 너비/2)로 수정
    touchPoint.x = player.contentSize.width/2;
}

플레이어 기체의 크기는 임의로 정수 45를 사용했지만 추후에 바뀔 가능성도 있어서, contentSize로 변경했다. 이렇게 하면 크기가 변해도 이 코드를 수정하지 않아도 된다.

터치한 좌표 x가 오른쪽 화면 끝에 가까운 경우, (기체의 우측 부분이 화면 밖으로 삐져 나오는 경우) 가로 화면 크기인 320에서 기체 크기의 반만큼을 뺀 크기를 x 좌표로 수정한다. 이렇게 하면 x 좌표는 항상 화면 내부에 위치하게 된다. 단, 기체의 크기를 너무 크지 않게 해야 뚝뚝 끊기는 느낌이 없을 것이다.

반대로 터치한 곳의 x가 왼쪽 화면에 가까운 곳이라면, 기체 너비의 반 길이를 x로 수정한다. 이렇게 하면 x가 화면 왼쪽을 빠져 나가지 않게 된다.


③ 기체 이동 액션
// 터치한 좌표로 move&action
// y 좌표는 변하지 않아야 함. y 좌표는 (플레이어 높이/2)로 설정
id testPlayerMove = [CCMoveTo actionWithDuration:0.7
                          position:ccp(touchPoint.x,player.contentSize.width/2)];
	
[player runAction:testPlayerMove];

터치했을 때 x 좌표가 지나치게 크거나 작은 경우(화면 밖으로 삐져 나가는 경우)는 앞에서 처리했으므로, 터치한 곳의 x 좌표로 이동하도록 moveTo 액션만 실행해주면 된다.


④ 미사일(총알) 처리

터치할 때마다 미사일을 발사하는 작업이다. 소스 코드에서는 친숙한 표현으로, 총알(bullet)로 명명했다.

// ################## 터치하면, 총알 생성과 액션
bullet = [CCSprite spriteWithFile:@"bullet.png"];

// 총알의 초기 위치는 플레이어의 x좌표
// 플레이어의 높이에 총알 높이를 더한 만큼
bullet.position = ccp(player.position.x, 
                      player.contentSize.height+bullet.contentSize.height);
bullet.tag = TAG_BULLET;
[self addChild:bullet];

// 총알 배열 추가
[_bullets addObject:bullet];	
	
// 총알의 이동은 화면을 벗어나게
id testBulletMove = [CCMoveTo actionWithDuration:0.5 
                              position:ccp(player.position.x,
                                          SCREENY+bullet.contentSize.height)];

// removeBullet 콜백 함수 참조
id callbackBullet = [CCCallFuncN actionWithTarget:self 
                                         selector:@selector(removeBullet:)];

id totalMove = [CCSequence actions:testBulletMove,callbackBullet,nil];
	
[bullet runAction:totalMove];

터치하면 총알 스프라이트가 생성되고, 이동하는 것이 총알 처리의 기본 뼈대가 된다. 그러나 약간의 처리가 더 필요하다.


총알이 생성되는 위치의 공식은 다음과 같다.

• 총알의 x 좌표
Player.position.x (플레이어 기체의 x 좌표와 동일)
• 총알의 y 좌표
player.contentSize.height + bullet.contentSize.height
(플레이어 기체의 높이) + (총알의 높이)

위의 코드로 인해 총알이 생성되는 곳은 플레이어 기체의 삼각형 상단 꼭지점에서 총알 크기만큼 더 높은 위치에서 발사되는 것처럼 보이게 된다.

총알이 날아가는 곳은 화면 최상단이므로, CCMoveTo에서 y 좌표는 화면의 높이인 SCREENY에 총알의 높이를 더해주면 화면 밖으로 벗어날 것이다.

(3) removeBullet

총알의 경우 한 가지 더 신경 써야 할 것이라면, 화면 바깥을 벗어난 것은 제거를 해야 한다는 것이다. 이것은 콜백 함수 removeBullet로 구현해서 따로 제거를 해 주었다.

콜백 함수에서는 node와 함께 총알 스프라이트를 넘겨 받았으므로 바로 삭제해주면 된다.

-(void)removeBullet:(CCNode *)node {
    [_bullets removeObject:node];		// 배열에서 총알 삭제
    [self removeChild:node cleanup:YES];	// 총알 삭제
}

그 외에 총알에 대한 배열이 있는데, 여러 개의 총알 관리를 위해 나중에 구현해 넣은 것이다.

(4) sechduleEnemyMove
① 적 기체의 생성과 이동

적 기체의 생성은 일정 시간 간격마다 행할 예정이므로, 스케줄로 만들었다.

init에서 아래 부분을 이미 추가했었다.

[self schedule:@selector(scheduleEnemyMove) interval:ENEMYMAKE_INTERVALTIME];

ENEMYMAKE_INTERVALTIME이 바로 적기가 생성될 시간 간격이다. #define으로 정의해 두었으므로 수정이 필요하다면 헤더 파일에서 수정하면 된다.


scheduleEnemyMove에서 적기를 본격적으로 생성, 이동하게 된다.

-(void)scheduleEnemyMove {
    // ############### 적 기체 스프라이트 생성
    CCSprite *enemy1 = [CCSprite spriteWithFile:@"enemy1.png"];
    enemy1.anchorPoint = ccp(0,0);
    enemy1.position = ccp(50,490);
    enemy1.tag = TAG_ENEMY1;
    [_enemies addObject:enemy1];    // 적 기체 배열 추가
    [self addChild:enemy1];
    
    // ############### 적 기체 runAction!
    // MoveBy는 현재 좌표를 0,0으로 간주하고 position으로 움직인다.
    id enemyMove = [CCMoveBy actionWithDuration:2 position:ccp(100,-550)];
    id callbackEnemy = [CCCallFuncN actionWithTarget:self
                                            selector:@selector(removeEnemy:)];
    id totalEnemyMove = [CCSequence actions:enemyMove,callbackEnemy,nil];
    [enemy1 runAction:totalEnemyMove];
}

스프라이트를 생성하고 초기 위치는 화면 바깥으로 설정했다. 정상적인 동작을 확인하기 위해 생성 위치를 ccp(50, 490)으로 해두었는데, 추후에 랜덤하게 바꿀 예정이다.

x 좌표를 랜덤으로 바꾸면 화면 상단에서부터 적기들이 생성되어 화면 아래 방향(100,-550)으로 내려올 것이다. 내려오는 방향도 화면 내부의 랜덤한 좌표로 변경할 예정이다.

변경 사항: 실제 소스 코드에는 약간이지만 랜덤한 움직임이 되도록 수정했다. 매우 간단한 코드이므로 설명은 생략한다.

(5) removeEnemy

적기가 등장한 이후 화면을 벗어나면 제거해야 하기 때문에, 콜백 함수 removeEnemy를 만들었다.

-(void)removeEnemy:(CCNode *)node {
    [_enemies removeObject:node];	 // 적 기체를 배열에서 제거
    [self removeChild:node cleanup:YES]; // 적 제거
}

콜백 함수에서는 removeChild를 통해 적 기체를 제거하는 일만 담당한다. 그래서 이름도 removeEnemy이다.

적기에 대한 배열은 여러 개의 적기 관리를 위해 나중에 추가한 것이다.

(6) scheduleEnemyMeetBullet

스프라이트를 생성하는 것까지는 좋았지만, 제거를 해야 할 순간이 언제가 될지 신경 써야 한다. 충돌 체크 자체는 아이폰 SDK에서 제공하는 영역 비교 메소드(CGRectIntersectsRect) 를 사용했다.

미사일과 적 기체가 만났는지 항상 체크해야 하기 때문에 init에서 스케줄로 등록했다.

[self schedule:@selector(scheduleEnemyMeetBullet)];


스케줄 메소드에서 실제 충돌 체크를 하게 된다.

-(void)scheduleEnemyMeetBullet {
/*
총알과 적기의 영역에 해당하는 Rect를 생성하고
CGRectIntersectsRect를 사용해서 교차되었는지를 확인하는 일을 반복한다. 
만약 충돌이 발생하면 그것들을 scene과 배열에서 삭제한다.
이 때 반복 처리되는 동안에는 배열에서 그것들을 제거할 수 없기 때문에 
오브젝트에 toDelete 배열을 추가한다.
*/
    
    NSMutableArray *bulletsToDelete = [[NSMutableArray alloc] init];
    
    for (CCSprite *tempBullet in _bullets)
    {        
        CGRect bulletRect = CGRectMake(tempBullet.position.x,
                                       tempBullet.position.y,
                                       tempBullet.contentSize.width,
                                       tempBullet.contentSize.height);
        
        NSMutableArray *enemiesToDelete = [[NSMutableArray alloc] init];
                
        for (CCSprite *tempEnemy in _enemies) {
            CGRect enemyRect = CGRectMake(tempEnemy.position.x,
                                          tempEnemy.position.y,
                                          tempEnemy.contentSize.width,
                                          tempEnemy.contentSize.height);
            
            // 충돌했다면 임시 적 배열에 적을 넣는다.
            if(CGRectIntersectsRect(bulletRect, enemyRect))
                [enemiesToDelete addObject:tempEnemy];
        }
    
        // 적 배열에서 적을 삭제, 스프라이트도 제거한다.  
        for (CCSprite *tempEnemy in enemiesToDelete) {
            [_enemies removeObject:tempEnemy];
            [self removeChild:tempEnemy cleanup:YES];
        }
    
        if (enemiesToDelete.count > 0) {
            [bulletsToDelete addObject:tempBullet];
        }
    
        [enemiesToDelete release];
    }
    
    // 총알 배열에서 총알 삭제, 스프라이트 제거
    for (CCSprite *tempBullet in bulletsToDelete) {
        [_bullets removeObject:tempBullet];
        [self removeChild:tempBullet cleanup:YES];
    }
        
    [bulletsToDelete release];
}

미사일과 적기의 영역을 각각 bulletRect, enemyRect로 만든 후, 두 영역이 겹치는지 비교를 하게 된다. 스터디에서 익혔던 CGRectIntersectsRect 메소드를 사용했다.

적기와 총알은 여러 개 생성될 것이기 때문에, 배열에 넣어 관리를 하게 된다.

적기나 미사일이 생성되면 배열에 넣고, 충돌했으면 배열에서 삭제하고 스프라이트도 제거한다.

배열은 alloc으로 만들었으므로, 다 쓴 후에는 release로 메모리에서 해제한다.

(7) onEnterTransitionDidFinish

이 메소드를 오버라이드 해 두었으므로 Scene의 로딩이 끝난 직후에 자동으로 호출된다. 특별한 의미가 있다기보다는, 스터디 했던 내용의 복습을 위해 넣은 것이다.

여기에서는 단순히 화면에 HELLO SPACE PROTOTYPE이라는 레이블을 배경으로 출력한다.

(8) dealloc

alloc으로 만든 것들을 반드시 해제해야 한다. 여기서는 메모리에 할당한 것이 배열 뿐이므로 배열만 해제하면 된다.

-(void)dealloc
{
    // ############## 메모리 해제할 것들

    // alloc으로 만든 배열 release
	
    [_enemies release];
    _enemies = nil;
	
    [_bullets release];
    _bullets = nil;
	
    [super dealloc];
}


'개발 > Cocos2d' 카테고리의 다른 글

아이폰 게임 Hello Space 개발 #05  (0) 2012.11.01
아이폰 게임 Hello Space 개발 #03  (0) 2012.11.01
아이폰 게임 Hello Space 개발 #02  (0) 2012.11.01
아이폰 게임 Hello Space 개발 #01  (0) 2012.11.01
cocos2d study #16  (0) 2012.10.26

+ Recent posts