프로그래밍/Cocos2D2010. 7. 2. 17:55
충돌 검사
이전 포스트까지의 내용대로 개발된 게임은 아무리 총알을 발사해도 타겟을 통과할 뿐이다. 이 문제를 해결하기 위해 충돌 검사라는 것이 필요하다.

케익에 맞은 맘모스는 게임 화면에서 사라져와 한다. 물론 잘 만들어진 게임이라면 총알 맞는 효과 같은 것들이 부수적으로 필요할 것이다. 

그러나 간단한 게임 예제라는 전제로 이런 부분들은 배제하고 간단히 총알과 타겟이 만났을 경우만 처리하는 코드를 작성할 것이다. 게임 물리의 거리, 운동 등의 다양한 면을 담당하는 게임 엔진의 한 부분인 물리 엔진은 사용되지 않는다.

먼저 다음 코드를 MammothHuntingScene.h에 추가하자.

NSMutableArray *_targets;

NSMutableArray *_projectiles;


그리고 MammothHuntingScene.m의 init 메소드의 self.isTouchEnabled = YES; 아래에 다음 코드를 추가한다.

_targets = [[NSMutableArray allocinit];

_projectiles = [[NSMutableArray allocinit];


마지막으로 메모리 관리를 위해 MammothHuntingScene.m의 dealloc 메소드에 다음 코드를 추가한다.

[_targets release];

_targets = nil;

[_projectiles release];

_projectiles = nil;


이제 addTarget 메소드르 수정할 차례이다. 위에서 선언한 _targets 배열에 새로운 target를 추가하는 것이다. 제일 아랫 부분에 다음처럼 추가하자. 새로 설정한 tag는 나중에 사용될 것이다.

target.tag = 1;

[_targets addObject:target];


그리고 ccTouchesEnded 메소드 역시 _projectiles 배열에 새로운 projectile을 추가하게 끔 다음 코드를 메소드 마지막에 추가한다. 새로 설정한 tag는 나중에 사용될 것이다.

projectile.tag = 2;

[_projectiles addObject:projectile];


마지막으로 spriteMoveFinished 메소드에 위에서 설정한 tag를 기반으로 스프라이트를 삭제하는 코드를 추가한다.

if (sprite.tag == 1) { // 타겟.

[_targets removeObject:sprite];

else if (sprite.tag == 2) { // 총알.

[_projectiles removeObject:sprite];

}


빌드앤런 해보자. 그런데 이전과 마찬가지로 실제 사냥이 되지는 않을 것이다. 당연하다. 충돌 검사 코드가 아직 작성되지 않았다. 다음 update: 메소드를 MammothHuntingScene.m에 추가해야 한다.

- (void)update:(ccTime)dt {

NSMutableArray *projectilesToDelete = [[NSMutableArray allocinit];

for (CCSprite *projectile in _projectiles) {

CGRect projectileRect = CGRectMake(

   projectile.position.x - (projectile.contentSize.width/2), 

   projectile.position.y - (projectile.contentSize.height/2), 

   projectile.contentSize.width

   projectile.contentSize.height);

NSMutableArray *targetsToDelete = [[NSMutableArray allocinit];

for (CCSprite *target in _targets) {

CGRect targetRect = CGRectMake(

   target.position.x - (target.contentSize.width/2), 

   target.position.y - (target.contentSize.height/2), 

   target.contentSize.width

   target.contentSize.height);

if (CGRectIntersectsRect(projectileRect, targetRect)) {

[targetsToDelete addObject:target];

}

}

for (CCSprite *target in targetsToDelete) {

[_targets removeObject:target];

[self removeChild:target cleanup:YES];

}

if (targetsToDelete.count > 0) {

[projectilesToDelete addObject:projectile];

}

[targetsToDelete release];

}

for (CCSprite *projectile in projectilesToDelete) {

[_projectiles removeObject:projectile];

[self removeChild:projectile cleanup:YES];

}

[projectilesToDelete release];

}


update: 메소드는 루프를 돌면서 총알과 타겟이 교차하는 지  CGRectIntersectsRect를 통해 검사한다. 만약 교차한다면 화면과 배열에서 제거해 버린다. 
여기서 한 가지 주의할 것이 있다. 충돌 검사 루프를 하는 동안은 배열의 객체를 삭제할 수 없으므로 제거 대상을 배열에 넣었다가 삭제한다.

정말 마지막으로 다음의 코드를 init 메소드 하단에 추가하고 빌드앤런하자. 이제 맘모스를 사냥할 수 있을 것이다.

[self schedule:@selector(update:)];


서두에 간단히 물리 엔진에 대해서 이야기 했던 것처럼, 간단한 그래픽 기반 게임 조차 어느 정도 물리 코드가 필요하다. 이 말은 충돌 검사의 다양한 방법이 있다는 것이다. 다음 기회에 Cocos2D의 물리 엔진인 Box2d와 Chipmunk를 공부할 항목에 추가해야 겠다.

---

Posted by windship
프로그래밍/Cocos2D2010. 7. 2. 17:54
MammothHunting은 우에서 좌로 움직이는 맘모스를 잡는 게임이다. 이제 타겟인 맘모스와 맘모스를 사냥하기 위한 총알 발사에 관해 살펴 보자.

타겟 추가
타겟은 화면 우측의 랜덤한 Y축을 기준으로 화면 좌측으로 이동한다. MammothHuntingScene.m에 다음의 addTarget 메소드를 init 메소드 이전에 추가한다.

-(void)addTarget {

CCSprite *target = [CCSprite spriteWithFile:@"target.png" rect:CGRectMake(007070)]; 

// 타겟(맘모스) Y 위치 결정.

CGSize winSize = [[CCDirector sharedDirectorwinSize];

int minY = target.contentSize.height/2;

int maxY = winSize.height - target.contentSize.height/2;

int rangeY = maxY - minY;

int actualY = (arc4random() % rangeY) + minY;

// 오른쪽  화면 밖에 X 위치와 위에서 결정한 Y축으로 타켓의 포지션 결정.

target.position = ccp(winSize.width + (target.contentSize.width/2), actualY);

[self addChild:target];

// 타겟의 속도 결정.

int minDuration = 2.0;

int maxDuration = 4.0;

int rangeDuration = maxDuration - minDuration;

int actualDuration = (arc4random() % rangeDuration) + minDuration;

// 액션 생성.

id actionMove = [CCMoveTo actionWithDuration:actualDuration position:ccp(-target.contentSize.width/2, actualY)];

id actionMoveDone = [CCCallFuncN actionWithTarget:self selector:@selector(spriteMoveFinished:)];

[target runAction:[CCSequence actions:actionMove, actionMoveDone, nil]];

}


위 메소드 중 새로 등장하는 액션에 대해서 잠깐 정리하고 넘어가자. Cocos2D는 다양한 액션을 제공한다. 액션 클래스는 CocosNode 클래스를 상속한 클래스로 애니메이션 처리를 위한 클래이다. 대부분 스프라이트 클래스의 애내메이션 처리를 위해 사용된다.

액션 클래스를 이용한 애니메이션은 시간과 동작에 따라 나눌 수 있다. 우선 시간에 따라서는 다음과 같에 세 가지로 구분된다.

InstanceAction: 한 번만 실행된다.
IntervalAction: 주어진 시간 동안 같은 동작을 반복해서 실행한다.
RepeatAction: 일정한 시간 동안 일정한 패턴을 반복한다.

동작에 따른 구분은 다음과 같다.

Transformation Action: Move, Rotate, Scale, Jump 등.
Composable Action: Sequence, Spawn, Repeat, Revers.
Ease Action: Exp, Sin, Cubic 등.
Etc Action: CallFun, Orbit.

Transformation Action은 스프라이트의 이동, 회전 등과 같은 움직임의 일회성 처리나 효과 처리를 위한 액션이다. Composable Action은 Transformation Action 여러 개가 순차적으로 또든 동시에  애니메이션의 실행이 필요한 경우 사용한다. Ease Action은 애니메이션을 구현하는 속도와 관련된 액션들이다. Transformation Action과 Composable Action이 기본 속도 1로 동작한다면, Ease Action은 애니메이션을 점점 빠르게 하거나, 갑자기 빠르게 바꾸는 것과 같은 처리를 할 수 있다. 그 외에 콜백 메소드를 호출해서 실행하는 CallFunc나 궤도를 따라 움직이도록 할 수 있는 Orbit과 같은 액션들이 있다. CallFunc의 콜백 기능을 이용하면 애니메이션이 끝난 다음에 액션을 발생시키는 등의 처리를 쉽게 할 수 있다.

다음은 액션을 생성한 부분의 CCCallFuncN에 생성한 콜백 함수인 spriteMoveFinished 메소드를 addTarget 메소드 이전에 추가해야 한다.

-(void)spriteMoveFinished:(id)sender {

CCSprite *sprite = (CCSprite *)sender;

[self removeChild:sprite cleanup:YES];

}


spriteMoveFinished 메소드는 화면 밖으로 사라진 스프라이트를 제거하는데  사용된다. 메모리 릭과 관련하여 더 좋은 방법을 선택해야 하지만, 간단한 게임이므로 생략한다.

마지막으로, 실제 타겟을 추가하기 위해 다음 구문을 init 메소드 마지막(if 구문의...)에 추가하고, 실제 gameLogic 메소드를 addTarget 메소드 다음에 구현한다.

[self schedule:@selector(gameLogic:) interval:1.0];


-(void)gameLogic:(ccTime)dt {

[self addTarget];

}


다시 빌드앤런 하면 다음과 같이 맘모스가 우에서 좌를 이동하는 화면을 볼 수 있다.




총알 발사
실제 맘모스를 잡기위해서는 꽤 묵직한 총과 총알이 필요하겠지만, 간단한 예제 이므로, 맘모스는 케익을 던져 잡는 것으로 처리하기로 한다. 물론 케익은 고릴라가 던질 것이다.

케익을 던지는 방법은 여러 가지가 있겠지만, MammothHunting 게임에서는 화면을 터치하는 방식을 선택했다. 총알 발사 처리를 위해서는 간단한 수학 공식이 필요하다. 우선 다음 그림을 보자.


총알 발사 처리를 위해 CCMoveTo 액션을 사용할 것이다. CCMoveTo 액션의 API를 보면 액션 지정을 위해 시간과 거리가 필요하다. 플레이어(고릴라)가 발사(터치)한 총알(케익)까지의 거리를 계산하기 위해 먼저 한 가지 사실을 가정해야 한다. 발사된 총알은 터치한 지점을 지나 화면 밖까지 이동한다는 점이다. 이 내용을 기초로 그림을 그려 본 것이 위의 그림이다. 그림의 내용을 상기하면서 다음 단계로 넘어 가자. 작은 직삼각형과 같은 비율의 큰 직삼각형을 기억하라.

우선 터치로 총알을 발사하기 위해 다음 코드를 init 메소드(if 구문 상단에...)에 추가하자. 레이어에 터치 이벤트를 활성화 한 것이다.

self.isTouchEnabled = YES;


그리고 터치 이벤트를 처리하기 위한 콜백 메소드(ccTouchesEnded)를 다음과 같이 구현한다.

-(void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {

// 처리를 위한 터치 선택.

UITouch *touch = [touches anyObject];

CGPoint location = [touch locationInView:[touch view]];

location = [[CCDirector sharedDirectorconvertToGL:location];

// 총알(케익) 초기 위치 설정.

CGSize winSize = [[CCDirector sharedDirectorwinSize];

CCSprite *projectile = [CCSprite spriteWithFile:@"projectile.png" rect:CGRectMake(004040)];

projectile.position = ccp(20, winSize.height/2);

// 총알 위치의 오프셋(offset) 측정.

int offX = location.x - projectile.position.x;

int offY = location.y - projectile.position.y;

// 총알 발사를 / 수직이나 뒤로  경우 탈출함(발사되지 않음).

if (offX <= 0return;

// 포지션 이중 체크.

[self addChild:projectile];

// 총알을 발사할 위치 측정.

int realX = winSize.width + (projectile.contentSize.width/2);

float ratio = (float) offY / (float) offX;

int realY = (realX * ratio) + projectile.position.y;

CGPoint realDest = ccp(realX, realY);

// Determine the length of how far we're shooting

// 발사한 총알의 길이(궤적) 얼마인지 측정.

int offRealX = realX - projectile.position.x;

int offRealY = realY - projectile.position.y;

float length = sqrtf((offRealX*offRealX)+(offRealY*offRealY));

float velocity = 480/1// 480pixels/1sec

float realMoveDuration = length/velocity;

// 총알을 종점(endpoint, 화면 ...)까지 이동 시킴.

[projectile runAction:[CCSequence actions:

   [CCMoveTo actionWithDuration:realMoveDuration position:realDest],

   [CCCallFuncN actionWithTarget:self selector:@selector(spriteMoveFinished:)], 

   nil]];

}


ccTouchesEnded 메소드의 내용의 첫 번째 부분은 처리할 하나의 터치를 선택하여 현재 뷰에서 위치(location)을 확인한다. 그리고 현재 화면 모드가 landscape이므로 convertToGL 메소드로 좌표를 변환한다. 이 것은 이전 포스트에서 언급했던 것으로 주의해야 할 부분이다. 왜냐하면, UIKit(Quartz)의 좌표계 Top-Left(좌상단)가 원점이 되는 좌표계를 쓰지만, Cocos2d는 OpenGL 기반이라 좌하단이 원점이 되는 좌표계를 사용하기 때문이다. 그래서 위치를 계산하기 전에 convertToGL 메소드로 좌표들을 꼭 변환해야 한다.

다음은 총알(케익) 스프라이트를 초기화 한다. 그리고 위에서 언급했던 수학으로 이동 시킬 총알의 위치를 계산해야 한다. 여기서 필요한 것이 바로 피타고라스의 정리이다.

임의의 직각삼각형에서 빗변을 한 변으로 하는 정사각형의 넓이는 다른 두 변을 각각 한 변으로 하는 정사각형의 넓이의 합과 같다.

물론 이 알고리즘은 총알을 발사하는 방법 중 가장 간단한 것의 하나일 뿐이다. 본 예제는 아주 단순한 경우이므로 이정도로 만족하고 넘어간다.

여기에서 중요한 또 한 가지의 내용은, 총알이 발사되는 시간과 속도의 관계를 이해하는 것이다. 학창 시절 배운 물리 법칙과 몇 가지 사항을 기억해 내야 한다.
  • 거리(distance)는 스칼라(scalar) 이고 변위(displacement)는 벡터(vector) 이다.
  • 벡터는 화살표를 사용해 나타낼 수 있다.
  • 거리 = 속도 * 시간
게임 개발을 하려면 기본적으로 수학과 물리학이 필수이다. 그렇다고 다시 교과서를 꺼내 볼 수도 없고... 이 문제를 해결하기 위해 다음 두 권의 책을 추천한다. 한 권은 마침 번역이 되어 있다.

어쨌든 알고리즘 같지 않은 알고리즘을 적용한 것이 액션을 추가하기 전까지의 소스 내용이다. 마지막으로 총알에 액션을 추가하고, 빌드앤런 하자. 이제 화면을 터치하면 총알이 발사될 것이다.


다음 포스트에서는 게임 개발 시 가장 중요한 부분인 충돌 검사에 대해 다룰 것이다. 두 번째 포스트를 마무리하기 전에 [MammothHunting] - Cocos2D를 사용한 아이폰 게임 개발 튜토리얼 1 에서 언급했던 Director, Scene, Layer, Sprite에 대해 정리해 보자.

Cocos2D의 모든 클래스는 CocosNode를 상속한다. CocosNode 클래스는 위치, 색상, 투명도, 크기, 회전, 가시성 여부, 카메라, 그리드 속성을 가지고 있으며 이들 속성을 필요에 따라 변경이 가능하다. CocosNode의 또 다른 특징은 자식 노드를 가질 수 있다는 점이다. 이는 객체들의 계층 구조를 만들 수 있음을 의미한다. 계층 구조가 가능함으로써 부모의 속성이 변할 때 자식의 속성까지 쉽게 변경할 수 있다.

Cocos2D에서 게임 화면을 구성하기 위해 제공되는 네 개의 클래스가 바로 CocosNode를 상속한 Director, Scene, Layer, Sprite 이다.

Director
Director는 싱글톤 클래스이다. Cocos2D는 게임의 화면을 Scene이라는 장면 단위로 구성하여 처리할 수 있는 기능을 제공한다. Director는 바로 이들 장면을 관리한다.

Scene
Scene은 게임에서 하나의 장면을 표현한다. 영화의 장면과 유사하다. 게임 내에서 장면 변경이 필요한 경우, 생성하여 각 장면마다 장면의 Scene 클래스로 처리한다.

Layer
Layer는 Sprite와 함께 Scene을 구성한다. Scene과 Layer의 가장 큰 차이점은, Scene은 사용자의 처치 이벤트를 받아서 처리할 수 없지만 Layer는 TouchEventsDelegate 프로토콜로 사용자의 처치 이벤트를 처리할 수 있다는 점이다. 다음 그림을 보면 이해하기 쉬울 것이다.


Sprite 
Director, Scene, Layer가 추상적이라면, Sprite는 실제로 화면에 출력되는 구체적 개념이다. Sprite는 컴퓨터 그래픽스에서 화면을 구성하는 정적인 이미지나 애니메이션이 되는 일련의 이미지를 가리킨다. 보통 게임에 사용되는 이미지라고 이해하면 된다. 일반적인 이미지와 구별되는 가장 큰 특징 중 하나는 투명색을 가진다는 점이다. 
실제 게임 개발 시 개발의 대부분의 시간이 Sprite를 생성하고 출력한 다음 사용자 입력이나, 기타 이벤트가 발생할 때마다 이들 이벤트에 대응하는 처리를 하는데 소요된다.

---

Posted by windship
프로그래밍/Cocos2D2010. 7. 2. 17:53
Cocos2D-iPhone 엔진은 원래 Python으로 작성된 2D 기반의 게임 라이브러리 인데 이 것을 아이폰 용으로 포팅한 것이 바로 Cocos2D-iPhone이다. 아이폰 게임 개발에 관심을 갖게 된다면 가장 먼저 접합게 되는 게임 엔진일 것이다. Cocos2D는 모바일 환경에서 그래픽 출력을 위한 OpenGL ES 및 사운드 출력을 위한 OpenAL 라이브러리를 손쉽게 게임 개발에 사용할 수 있도록 래핑한 것이다. 물론 다양한 기능을 지원한다. 이에 대한 내용은 사이트에서 확인 바란다.

단 0.8.X  버전에서 0.99.0(cocos2d for iPhone v0.99.0 Release Notes) 버전으로 업그레이드 되면서 CC라는 네임스페이스를 사용하기 시작했다는 점에 주의해야 한다. 0.99.0 이전의 인터넷 자료를 참고한다면 샘플 소스에서 차이가 날 것이다.

이와 관련된 내용과 기본적인 사용법(Hello World, Hello Actions, Hello Events 등등) 등을 공부하고 나면 바로 다음 단계가 궁굼해 진다. "그럼, 어떻게 게임을 만들지?", 물론 사이트에 샘플 게임 소스가 공개되어 있긴 하지만 이제 막 게임 개발에 입문한 개발자에게는 그리 녹녹치 않다.

부분적으로 API를 훓어 본다고 해결될 문제도 아니다. 이에 대한 가장 좋은 해결책은 최대한 단순한 게임 형태를 살펴보는 것이 전체적인 사용법과 이해도를 높이는데 훨씬 많은 도움이 될 것이다. 

본 포스트는 "How To Make A Simple iPhone Game with Cocos2D Tutorial"의 내용을 한글로 재구성 한 것이며, 좋은 글을 공개해준 저자에게 감사 드린다. 이외에도 게임 개발과 관련된 유용한 정보가 상당히 많다. 기회가 된다면 다른 내용도 이와 유사한 형태로 포스팅해 볼 계획이다.

* 부분적으로 설명이 생략된 부분은 마지막 포스트 하단에 첨부한 샘플 프로젝트로 설명을 갈음한다.

우선 사전 준비부터 시작하자.
1. Cocos2D 다운로드 및 설치(현재 안정 버전: 0.99.1)

프로젝트 생성
MammothHunting이라는 이름으로 신규 프로젝트 생성. 물론 Cocos2D 템플릿을 이용한다.


이 상태에서 빌드앤런 하면 다음 화면을 볼 수 있을 것이다. 이미 알고 있는 것처럼 Hello World 예제이다.


이제 게임 개발에 관심을 갖는 단계라면 Xcode의 사용법은 익숙할 것이다. 자세한 설명은 생략한다.

Sprite 추가
* Director, Scene, Layer, Sprite 네 개의 클래스는 Cocos2D에서 게임 화면의 구성을 위한 가장 중요한 것 들이다. 이와 관련된 기본 내용은 숙지하자. 다행히도 미리 고생해 준 분이 계시다.

스프라이트를 추가하기 전에 다음 세 단계를 먼저 진행한다. 물론 기존 템플릿 코드로 진행해도 되지만 생애 최초의 게임을 만드는 마당에 네이밍도 중요하지 않은가? 

첫째, 다음 세개의 그림을 Resources에 추가한다.



둘째, MammothHuntingAppDelegate.m 파일을 열어 applicationDidFinishLaunching: 메소드에서 템플릿이 생성해준 맨 아래의 [[CCDirector sharedDirector] runWithScene: [HelloWorld scene]]; 코드를 다음과 같이 수정한다.

CCScene *scene = [CCScene node];

CCLayer *layer = [MammothHuntingScene node];

[scene addChild:layer];

[[CCDirector sharedDirectorrunWithScene: scene];


세째, HelloScene.h와 HelloScene.m 클래스 파일을 삭제한다. 그리고 NSObject의 서브 클래스로 MammothHuntingScene.h와 MammothHuntingScene.m 클래스를 생성한다. 다음은 MammothHuntingScene.h 클래스에서 "cocos2d.h"를 임포트 하고 CCLayer를 상속하도록 수정한다. MammothHuntingScene.m에 다음 init 메소드를 추가하고, 다시 빌드앤런!

-(id) init {

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

CGSize winSize = [[CCDirector sharedDirectorwinSize];

CCSprite *player = [CCSprite spriteWithFile:@"player.png" rect:CGRectMake(005055)];

player.position = ccp(player.contentSize.width/2, winSize.height/2);

[self addChild:player];

}

return self;

}


아마도 문제가 없다면, 다음 화면을 접할 수 있을 것이다.


잘 보이지 않겠지만, 고릴라이다. 인터넷에서 무료로 구할 수 있는 아이콘을 활용했다. 원하는 그림으로 변경해도 튜토리얼을 진행하는 데는 전혀 지장이 없다. 단 그림 사이즈와 rect 사이즈는 동일해야 한다.

여기서 잠깐, 좌표계에 대해서 공부하고 다음 단계로 넘어가자. 화면의 위치를 이동하거나 지정할 때 Cocos2D에서는 좌표계에 주의해야 한다. UIView를 사용하는 일반적인 아이폰 앱은 UIView 좌표계를 사용하며 UIView에서는 좌측 상단(0, 0)이 원점이다. 즉, 오른쪽으로 가면서 x 값이 증가하고 아래로 갈 수록 y 값이 증가한다. 이와는 달리 Cocos2D는 OpenGL을 사용하므로 좌표계의 원첨이 좌측 하단 위로 갈수록 y 값이 증가한다.

위 내용을 그림으로 표현하면 다음과 같다. 나중에 좌표계를 변환해야 하므로 기억해 두어야 한다.


좌표계를 기준으로 플레이어(고릴라)의 위치는 init 메소드에 다음과 같이 설정했다.

player.position = ccp(player.contentSize.width/2, winSize.height/2);


여기서 플레이어(고릴라)의 스프라이트의 앵커포인트(anchorPoint: 고정점)의 x, y 좌표를 위와 같이 설정했다. 앵커 포인트는 모든 변환 및 위치 조작의 중심점인데, 상위 레이어에 부착한 레이어 상의 핀으로 생각하면 된다. 

레이어 관련하여 프레임(frame), 바운드(bounds), 위치(position), 고정점(anchorPoint), 모서리 반경(cornerRadius), 레이어 높낮이(zPosition) 등의 기하 관련 속성에 대한 이해가 전제되어야 하는데, 이는 Core Animation의  Layer Geometry and Transforms 부분을 참고하라.

다음 작업은 배경화면 변경이다. 검정색 배경을 흰색으로 변경할 것이다.
프로젝트에서 MammothHuntingScene.h 클래스 파일을 열어 다음과 같이 CCColorLayer를 상속 받도록 수정한다.

#import "cocos2d.h"



@interface MammothHuntingScene : CCColorLayer {


}


@end


그리고 MammothHuntingScene.m 클래스의 init 메소드의 if 조건을 다음과 같이 수정한다.

if ((self=[super initWithColor:ccc4(255,255,255,255)])) {


이제 다시 빌드앤런 하면 다음 그림과 같이 흰색 배경화면을 볼 수 있을 것이다.


"Cocos2D를 사용한 아이폰 게임 개발 튜토리얼 1"은 이것으로 마무리 하고 "Cocos2D를 사용한 아이폰 게임 개발 튜토리얼 2"에서는 타켓과 총알 발사에 관해서 살표 보겠다. 하나의 포스트로 진행하기에는 내용이 너무 많은 것 같아 4 단계 정도로 나누어서 포스팅할 계획이다.

---

Posted by windship
기반지식/C/C++2010. 7. 1. 19:54
☆ 가상함수와 다형성

#include <iostream.h>
#include <string.h>

class Com
{
public:
virtual void draw()  //<-- 부모 클래스 Com의 원래 함수 draw()를 virtual로 가상함수로 만든다
{
cout << "Com-draw\n";
}
};

/////////////////////////////////////////////////

class Circle:public Com  // <-- 부모 클래스 Com으로부터 상속받음
{
public:
void draw()           // <-- draw() 함수를 원을 그리는 데에 사용한다
{
cout << "원을 그린다\n";
}
};

/////////////////////////////////////////////////

class Rect:public Com   // <-- 부모 클래스 Com으로부터 상속받음
{
public:
void draw()          // <-- draw() 함수를 사각형을 그리는 데에 사용한다
{
cout << "사각형을 그린다\n";
}
};

/////////////////////////////////////////////////

void main()
{
Com *p;
int n;
while(1)
{
cout << "1. Circle  2. Rect  3. Exit\n";
cin >> n;
switch(n)
{
case 1:
p = new Circle();
break;
case 2:
p = new Rect();
break;
default:
return;
}
p->draw();
}
}

  - 순수 가상함수가 포함되어 있는 클래스를 추상(abstract) 클래스라 한다. 
  - 추상 클래스로부터 상속받은 클래스는 반드시 순수 가상함수를 오버라이딩해야 한다. 
  

☆ 예제 2

#include <iostream.h>

class Command
{
public:
virtual void write() = 0;  // 순수 가상 함수
};

////////////////////////////////////////////

class List:public Command
{
void write()
{
cout << "게시판 목록을 출력합니다 \n";
}
};

////////////////////////////////////////////

class Insert:public Command
{
void write()
{
cout << "게시판에 글을 올립니다\n";
}
};

/////////////////////////////////////////////

class Edit:public Command
{
void write()
{
cout << "게시판의 글을 수정합니다\n";
}
};

///////////////////////////////////////////////

/////////////////////////////////////////////

class Delete:public Command
{
void write()
{
cout << "게시판의 글을 삭제합니다\n";
}
};

///////////////////////////////////////////////

void main()
{
int n;
Command *p;

while(1)
{
cout << "1. 추가  2. 삭제  3. 수정  4. 출력  5. 종료\n";
cin >> n;

switch(n)
{
case 1:
p = new Insert;
break;

case 2:
p = new Delete;
break;

case 3:
p = new Edit;
break;

case 4:
p = new List;
break;

default:
return;
}
p->write();
}
}

  - 

☆ 파일 입출력

#include <stdio.h>

void main()
{
        FILE *fp = fopen("c:\\data1.txt", "w"); // w : 쓰기 모드로 파일을 연다. 
                                                                          파일이 없으면 새로 만들고, 있어도 새로 만들어 덮어쓴다.

putc('A', fp);  // putc : 파일에 한글자 출력
putc('B', fp);

fclose(fp);

puts("탐색기에서 확인하세요");
int n;
FILE *fp;

char ch;
while(1)
{
puts("1. 문자 입력 후 Ctrl+Z  2. 파일출력  3. 종료");
scanf("%d", &n);

switch(n)
{
case 1:
fp = fopen("c:\\paper.txt", "a"); // 추가 모드로 파일 열기.
while(1)
{
if((ch=getchar())==EOF)
{
fprintf(fp, "======================\n");   
break;
}
fputc(ch, fp);      // 키보드에서 한글자를 입력받아 파일에 기록.
}
fclose(fp);
break;

case 2:
fp = fopen("c:\\paper.txt", "r"); // 읽기 모드로 파일 열기.
puts("=== paper.txt 파일 읽기 ===");

while(!feof(fp))       // feof : 파일의 끝인가를 검사한다.
{
ch = fgetc(fp);
putchar(ch);
}
puts("\n=== paper.txt 파일 읽기 끝 ===");
fclose(fp);
break;

default:
puts("탐색기에서 파일을 확인하세요");
return;
}
}
}


☆ 파일 입출력(구조체를 사용하는 방법)

#include <iostream>
#include <fstream>

using namespace std;

struct data
{
char name[20];
int age;
char address[20];
};

/*
int main()                                // 파일에 쓰는 부분
{
data man[5]={
{"박문석", 50, "서울"},
{"이순신", 40, "아산"},
{"김유신", 55, "대구"},
{"강감찬", 34, "서울"},
{"을지문덕", 25, "마산"}
};

ofstream fout;

fout.open("c:\\data.txt");
fout.write((char *)man, sizeof(data)*5);
fout.close();

return 0;
}
*/

int main()                         // 파일로부터 읽어오기
{
data man;
ifstream fin;
fin.open("c:\\data.txt");
if(fin.fail())
{
printf("Error: file open error\n");
return 0;
}

cout << " *** 파일에서 읽어온 데이터 ***\n";
cout << "-------------------------------\n";
cout << "이름 나이 주소\n";
cout << "-------------------------------\n";

while(fin.read((char *) &man, sizeof(man)) )
{
cout << man.name << " " << man.age << " " << man.address << endl;
}

fin.close();
return 0;
}

--
Posted by windship
기반지식/C/C++2010. 6. 30. 22:18
☆ 상속과 오버라이딩

#include <iostream.h>
#include <string.h>

class Base
{
protected:
char name[10];
int age;
public:
Base(char *n="이승기", int a=20);
};

Base::Base(char *n, int a):age(a)
{
cout << "Base 생성자 호출\n";
strcpy(name, n);
}

////////////////////////////////////////////////////////////////

class Child:public Base // 부모클래스 Base에서 protected와 public 부분을 물려받아 자식 클래스 Child를
                            // 만든다.
{
public:
Child();
Child(char *n, int a);
void write();
};

Child::Child()
{
cout << "Child 생성자 호출\n";
}

Child::Child(char *n, int a)
{
strcpy(name, n);
age = a;
}

void Child::write()
{
cout << "이름 : " << name << "\n나이 : " << age << endl;
}

////////////////////////////////////////////////////////////////

void main()
{
Child ch;
Child ch2("한예슬", 30);

ch.write();
ch2.write();
}


--
Posted by windship
기반지식/C/C++2010. 6. 30. 21:08
☆ 클래스와 오버로딩, 생성자

#include <iostream.h>
#include <string.h>

class Sales
{
private:
char *pummok;
int su, dan;
public:
Sales(char *p="우유", int s=1, int d=1000);
void Disp() const;
int GetKum() const;
};

Sales::Sales(char *p, int s, int d):su(s), dan(d)
{
pummok = new char[strlen(p)+1];
strcpy(pummok, p);
}

int Sales::GetKum() const
{
return su*dan;
}

// const 함수에서 일반 함수를 호출하지 못한다. 같은 const여야만 호출 가능.
void Sales::Disp() const
{
cout << "품목 : " << pummok << endl;
cout << "수량 : " << su << endl;
cout << "단가 : " << dan << endl;
cout << "총금액 : " << GetKum() << endl;
}

void main()
{
/*
Sales s1;
Sales s2("사과");
Sales s3("포도",5);
Sales s4("수박",3,9000);

s1.Disp();
s2.Disp();
s3.Disp();
s4.Disp();
*/

/*
Sales s[4]={Sales(), Sales("쥬스",7), Sales("오렌지", 3, 1300)};
for(int i=0;i<4;i++)
{
s[i].Disp();
}
*/
Sales *s[3];
s[0]=new Sales();
s[1]=new Sales("apple",3);
s[2]=new Sales("Banana",10,700);

for(int i=0;i<3;i++)
{
s[i]->Disp();
}
}

  - 클래스 Sales 하나를 선언하고 여러가지 형식으로 사용하는 예


☆ 예제 2

#include <iostream.h>
#include <string.h>
#include <iomanip.h>

// static 멤버 변수
// 1. 같은 클래스로 생성되는 모든 인스턴스에서 공유한다.
// 2. 호출할 때 클래스 명을 이용해서 직접 호출 가능
// 3. 초기값은 클래스 밖에서 준다. 안에서는 못 줌.
// 4. this 포인터가 없어서 주소가 없다. static 변수만 접근 가능하다.

class List
{
private:
char name[10]; // 직원 이름
char address[30]; // 집 주소
        static char address2[30];         // 회사 주소
int age; // 직원 나이
public:
List(char *n="", char *add="", int a=0);
void SetHome(char *add);
static void SetOffice(char *add2);
void Disp();
};

char List::address2[30]="강남구 역삼동"; // static 변수 초기화

List::List(char *n, char *add, int a):age(a) // class 내에서 초기화를 못하므로 age의 초기화를 밖에서 해준다.
                                                            // 원래는 함수 안에서 초기화하지만 숫자는 이렇게 바로 해줄수있음.
{
strcpy(name, n);
strcpy(address, add);
}

void List::SetHome(char *add)
{
strcpy(this->address, add);
}

void List::SetOffice(char *add2) // static 함수
{
strcpy(address2, add2);
}

void List::Disp()
{
cout << setw(10) << name << setw(20) << address << setw(20) << List::address2 << setw(7) << age << endl;
}

void write(List *p)
{
for(int i=0;i<3;i++,p++)
{
p->Disp();
}
}

void main()
{
List em[3]={List("박철민", "송파구", 34), List("이승기", "강동구", 21), List("강호동", "서대문구", 34)};

cout << "초기값 출력 \n";

write(em);

cout << "변경된 값 출력(0번 변경해 보기)\n";

em[0].SetHome("하와이");
em[0].SetOffice("스위스");

write(em);
}

  -


☆ 프렌드 함수중복

#include <iostream.h>
#include <string.h>

class Point
{
private:
int x, y;
public:
Point(int x=0, int y=0);
friend void Disp(Point &po); //전역함수를 친구로 지정하는 것. 멤버가 아님!
};

Point::Point(int x, int y)
{
this->x=x;
this->y=y;
}

void Disp(Point &po)
{
cout << "x: " << po.x << ", y=" << po.y << endl;
}

void main()
{
Point a(100, 200);
Disp(a);
}

  - 

Posted by windship
기반지식/C/C++2010. 6. 28. 21:28
☆ 생성자와 소멸자

  - 생성자 : 객체가 생성되는 시점에서 자동으로 수행되는 멤버 함수. 오버로딩이 가능.
  - 소멸자 : 객체가 소멸되는 시점에서 자동으로 호출되는 멤버 함수. 오버로딩 불가.
  - 둘 다 사용자가 호출하는게 아니며 자동적으로 호출된다
  - 리턴값이 없다
  - 함수의 이름이 클래스의 이름과 동일하다
  - 주로 멤버 변수의 초기화나 메모리 할당에 사용된다

#include <iostream.h>
#include <string.h>

class Sinsang
{
private:
//char name[10];   // 클래스 안에서 구체적으로 배열의 길이를 선언해주고 있지만 이것이 안될 경우
char *name;          // 배열을 사용해서 임의의 길이로 문자열을 설정할 수 있도록 한다.
int age;
public:
Sinsang(); //생성자
Sinsang(char *n); // 윗줄과 이름은 똑같지만 파라미터가 틀리므로 오버로딩임.
Sinsang(char *n, int age);

~Sinsang(); //소멸자. 앞에 ~ 가 붙으면 소멸자이다
void Disp() const;
};

Sinsang::Sinsang()
{
// cout << "디폴트 생성자 호출 \n";
name = new char[10]; // 생성자 안에서 배열(문자열)의 길이를 설정한다.
strcpy(name,"디폴트");
age=0;
}

Sinsang::~Sinsang()
{
cout << "소멸자 호출 -> " << name << "\n";
delete name;
}

Sinsang::Sinsang(char *n)
{
// cout << n << "생성자 호출\n";
name = new char[strlen(n)+1]; // 생성자의 호출 타입이 오버로딩으로 달라져 있으므로 
                                                    // 메모리 할당도 다르게 된다. NULL 문자 때문에 문자열 길이 +1이 된다.
strcpy(name,n);
age=0;
}

Sinsang::Sinsang(char *n, int age)
{
name = new char[strlen(n)+1];
strcpy(name,n);
this->age=age;
}

void Sinsang::Disp() const
{
cout << "이름 : " << name << endl;
cout << "나이 : " << age << endl;
}

void main()
{
/*
Sinsang s; // 디폴트 생성자 호출
Sinsang s2("송혜교");
Sinsang s3("한은정",23);
s.Disp();
s2.Disp();
s3.Disp();
*/

Sinsang *s=new Sinsang();
Sinsang *s2=new Sinsang("송혜교");
Sinsang *s3=new Sinsang("한은정",23);

s->Disp();
s2->Disp();
s3->Disp();

delete s3;
delete s2;
delete s;
}


☆ 정적 데이터 멤버



Posted by windship
기반지식/C/C++2010. 6. 25. 19:53
☆ 구조체와 동적 메모리 할당 예제

#include <stdio.h>
#include <malloc.h>

typedef struct Student
{
int no;
char name[10];
int age;

}ST;

void datain(ST *st,int n) // <-- 이 부분 잘 모르겠음... 
{
for(int i=0;i<n;i++,st++)
{
printf("%d번 학생정보 입력\n",i+1);
printf("\t번호 : ");

scanf("%d",&st->no);

printf("\t이름 : ");
scanf("%s",&st->name);

printf("\t나이 : ");
scanf("%d",&st->age);

}
}

void dataout(ST *st, int n)
{
puts("=== 입력결과 출력 ===");
printf("번호\t이름\t나이\n");
for(int i=0;i<n;i++,st++)
{
printf("%-5d%-10s%6d\n",st->no,st->name,st->age);
}
}

void main()
{
int inwon;
ST *st;
printf("입력할 인원수는?");
scanf("%d",&inwon);
st=(ST *)malloc(sizeof(ST)*inwon);

datain(st,inwon);
dataout(st,inwon);

free(st);  // 동적 메모리 해제
}

  - 자료 수를 입력받아 정하고, 그에 맞게 동적 메모리를 할당하여 자료를 입력받고 결과를 출력한다.


☆ 클래스와 접근 지정자

#include <iostream.h>

class Point            // C의 구조체(struct)와 거의 같으나 기본 접근지정자가 private이다.
{

//public:     // <- public으로 할 것이라면 구조체를 쓰는 것과 차이가 없다. (굳이 class를 쓸 필요가 없음)
private:  // <- 안 써줘도 어차피 public임 ㅋㅋ

int x;
int y;

public:       // class 전체는 private이지만 이렇게 필요한 함수 영역에만 public을 지정해 
             // 외부에서 값을 참조할 수 있다.

void SetX(int xx);
void SetY(int yy);

int GetX(); // 아래의 리턴 타입이 int이므로 함수도 int로 선언
{
return x;   // <-- x의 리턴 타입이 int
}

int GetY()
{
return y;
}
};

void Point :: SetX(int xx)
{
x=xx;
}

void Point :: SetY(int yy)
{
y=yy;
}

int Point :: GetX()
{
return x;
}

int Point :: GetY()
{
return y;
}


void main()
{
Point p;

p.SetX(100); 
p.SetY(200);

cout << "X 좌표 : " << p.GetX() << endl;
cout << "Y 좌표 : " << p.GetY() << endl;

p.x=100;  // 이런 식의 직접 접근은 private에서는 불가능
p.y=80;
cout << "X좌표 : " << p.x << endl;
cout << "Y좌표 : " << p.y << endl;

}

  - class는 보통 프로그램 자체에 포함시키지 않고 헤더 파일로 뺀다.
  - 이 경우 #include "파일이름.h"와 같이 인클루드시킨다. (<파일이름.h>가 아님!)
  - class 내부에서 함수가 정의될 경우, 이것 역시 선언만 시키고 class 외부에서 정의할 수 있다.


☆ 클래스와 멤버함수 예제

#include <iostream.h>
#include <string.h>

class Person
{

private:

char name[10];
int kor, eng;

public:
void setName(char *n);
void setScore(int k, int e);
char *getName();  // <-- 요 부분 몰랐음...
int getKor();
int getEng();
int getTot();
};

void Person :: setName(char *n)
{
strcpy(name,n);  // <-- 요 부분도 몰랐음
}

void Person :: setScore(int k, int e)
{
kor = k;
eng = e;
}

char *Person :: getName()   // <-- 문자열을 받는 함수는 기본적으로 포인터로 취급한다고 생각하자.
{
return name;
}

int Person :: getKor()
{
return kor;
}

int Person :: getEng()
{
return eng;
}

int Person :: getTot()
{
return kor + eng;
}


void main()
{
Person p;
p.setName("이수진");
p.setScore(90,100);

cout << "이름 : " << p.getName() << endl;
cout << "국어 : " << p.getKor() << endl;
cout << "영어 : " << p.getEng() << endl;
cout << "총점 : " << p.getTot() << endl;
}

---
Posted by windship
프로그래밍/Objective-C2010. 6. 25. 00:37
이전에 포스팅한 "NSXMLParser로 RSS 읽어오기"와 유사한 방법으로 구글 날씨 RSS를 가져오는 것을 만들어 보았습니다. 그런데 한글이 깨져나와 확인해 보니 문자셋이 euc-kr이었습니다. 문자셋을 확인하는 방법은 URLConnection의 델리게이트 메소드에서 확인할 수 있습니다.
  1. - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
  2.     NSLog(@"Encoding: %@"[response textEncodingName]);
  3. }

전송이 끝난 후에 아래와 같이 NSData를 euc-kr을 utf-8로 변환하여 사용할 수 있습니다. 변경된 data를 NSXMLParser의 initWithData의 인자로 사용하면 됩니다.
  1. - (void)connectionDidFinishLoading:(NSURLConnection *)connection {
  2.     NSString *str = [[NSString alloc] initWithData:receiveData encoding:0x80000000 + kCFStringEncodingDOSKorean];
  3.     NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];
  4.    
  5.     NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
  6. .
  7. .
  8. .
  9. }

한가지 이상한 점은 웹브라우저에서 확인하면 같은 URL이지만 utf-8로 넘어 옵니다. 아마 서버에서 헤더를 검사에서 각각 다른 인코딩으로 넘겨주는 것이 아닌가 하는 생각이 듭니다. 헤더의 항목들을 변경해서 보았는데 User-Agent를 설정해서 보내보니 euc-kr이 아닌 utf-8로 넘어 왔습니다.
  1.     NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURLURLWithString:@"http://www.google.com/ig/api?weather=seoul"]];
  2.      
  3.     [request addValue:@"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; ko; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2" forHTTPHeaderField:@"User-Agent"];
  4.  
  5.     xmlConnection = [[NSURLConnection alloc]
  6.                      initWithRequest:request
  7.                      delegate:self];


구글의 날씨 API에서는 이와 같이 User-Agent를 보내면 utf-8로 보내기때문에 위와같이 인코딩의 변환이 필요하지 않습니다. 아마 예측가능한 User-Agent는 utf-8로 보내고 그외에는 euc-kr로 보내는 것 같습니다. 이는 영문도 마찬가지이며 http://www.google.com/ig/api?weather=seoul와 같이 co.kr에서 com으로 변경하면 문자셋이 iso-8859-1로 넘어 옵니다. User-Agent를 추가하면 역시 utf-8로 넘어 옵니다.



이전부터 그냥 복사해서 올렸는데 오늘 보니 아래와 같이 나오는 건 너무 보기가 힘든 것 같아서, 예제코드를  Quick Highlighter를 사용해서 정리해 보았습니다.
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.google.com/ig/api?weather=seoul"]];
     
    [request addValue:@"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; ko; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2" forHTTPHeaderField:@"User-Agent"];

    xmlConnection = [[NSURLConnection alloc] 
                     initWithRequest:request 
                     delegate:self];

보기도 조금 나아지지만 해당 클래스에 대한 애플의 문서로 바로 링크가 되는 것도 좋은 것 같습니다.

Posted by windship
프로그래밍/Objective-C2010. 6. 25. 00:32
아이폰 3.0 SDK 부터는 accelerometer를 사용하지 않고도 UIResponder에 추가된 motion 이벤트 처리 메소드를 구현함으로써 간단하게 사용자의 흔들기 동작을 체크할 수 있습니다. 저도 처음 사용해 보면서 간단한 내용들을 정리해 보았습니다.

1. First responder 되기
사용자의 흔들기 이벤트를 처리할 ViewController는 그 자신이 First responder가 되어야 합니다. becomFirstResponder 메소드를 호출하고 canBecomeFirstResponder 메소드에서 YES를 반환합니다.

  1. - (void)viewDidAppear:(BOOL)animated {
  2.     [super viewDidAppear:animated];
  3.     [self becomeFirstResponder];
  4. }
  5.  
  6. - (BOOL)canBecomeFirstResponder {
  7.     return YES;
  8. }

viewDidAppear는 코드에서 서브뷰로 추가될 때만 호출됩니다. IB에서 바로 Window에 View를 추가하였으면 awakeFromNib등의 메소드에서 becomFirstResponder를 호출하셔야 합니다.

2. motion 메소드 구현
이후로는 간단합니다. 사용자의 흔들기가 시작되면 해당 motionBegan이 호출되고 종료될 때 motionEnded가 호출됩니다. 지나치게 많이 흔들거나 하여 유효하지 않은 흔들기로 판단될 때는 motionCancelled가 호출됩니다.

  1. - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event {
  2.     NSLog(@"Shaking start");
  3. }
  4.  
  5. - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event {
  6.     NSLog(@"Shaking end");
  7. }
  8.  
  9. - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event {
  10.     NSLog(@"Shaking cancel");  
  11. }
  12.  

motionEnded 메소드에 사용자의 흔들기가 끝난 후 실행할 코드를 추가하면, 간단하게 흔들기를 지원할 수 있습니다.

Posted by windship