닌자의 공격! 퓻퓻!
Cocos2D는 아이폰 게임을 만들 때 많은 시간을 절약할 수 있게 해 주는 파워풀한 라이브러리이다. 스프라이트 지원, 멋진 그래픽펙트, 애니메이션, 물리 충돌, 사운드 엔진, 기타 등등 다양한 기능들을 지원한다.
필자도 Cocos2D를 배우기 시작한 지 얼마 되지 않았기 때문에, 쓸만한 튜터리얼을 찾기 위해 많이 헤매고 다녔다. 하지만 정말 내가 원하는 그런 튜터리얼 - 간단하지만 애니메이션, 충돌, 오디오 등의 기능에 대한 명료한 설명이 있는 - 을 찾기가 무척 어려웠다. 마침내 스스로 간단한 게임을 만들어냈을 때, 나는 내 경험을 바탕으로 하여 다른 초보자들에게 도움이 될 만한 튜터리얼 문서를 쓰기로 마음먹었다.
이 튜터리얼에서는 Cocos2D로 간단한 아이폰 게임을 만드는 과정을 처음부터 끝까지 훑어볼 것이다. 여러분은 튜터리얼을 그대로 따라올 수도 있고, 아니면 글 마지막에 있는 샘플 프로젝트를 바로 시험해봐도 된다. 보시다시피, 닌자 게임이 될 것이다.
Cocos2D 다운로드와 설치
구글의 Cocos2D 코드 페이지에서 Cocos2D를 다운받을 수 있다. 이 글이 쓰여질 당시의 최신 버전은 0.99.9-final이다. 이 튜터리얼에서도 이 버전을 사용할 것이다.
Cocos2D를 다운받았으면 템플릿을 설치해야 한다. 터미널 창을 열고 Cocos2D를 다운로드한 디렉토리로 간 뒤, 다음 명령을 입력한다.
./install_template.sh
만약 여러분이 맥에 SDK 버전을 여러 개 설치했다든가 해서, XCode가 기본 위치가 아닌 다른 곳에 설치되었다면 명령 뒤쪽에 뭔가 파라미터를 더 붙여줘야 할 수도 있다.
Hello, Cocos2D!
그러면 간단한 Hello World 프로젝트로 방금 설치한 Cocos2D 템플릿을 실행해 보자. XCode를 실행하고 cocos2d-0.99.0 Application 템플릿을 골라, 새 Cocos2D 프로젝트를 만든다. 프로젝트 이름은 “Cocos2DSimpleGame” 으로 하겠다.
프로젝트가 열렸으면 Build and run 을 눌러 빌드하고 실행시키자. 모두 잘 되었다면 다음처럼 시뮬레이터에 실행되는 화면이 나타날 것이다.
Cocos2D는 게임의 레벨이나 스크린 등을 "씬"(장면)이라는 개념으로 만들어놓고 있다. 예를 들어, 하나의 게임은 크게 보아 첫 메뉴가 나오는 부분, 게임의 주된 플레이가 이루어지는 부분, 그리고 게임오버 되었을 때의 부분 등으로 나눌 수 있을 것이다. 실제 Cocos2D의 씬에서는, 여러분은 여러 개의 레이어(포토샵의 그 레이어와 비슷하다고 생각하면 된다)를 다루게 되고, 각각의 레이어는 스프라이트, 레이블, 메뉴 등등의 "노드"를 포함하게 된다. 노드는 다른 노드를 포함할 수도 있다(예를 들어 스프라이트는 그 안에 자식 스프라이트를 가질 수 있다).
만약 여러분이 샘플 프로젝트를 보고 있다면, HelloWorldScene 이라는 1개의 씬만이 사용되고 있는 것을 볼 수 있다. 이제 우리는 바로 거기에다 게임 플레이 부분을 만들게 될 것이다. 소스를 열어 보면 씬에 "Hello World"를 보여주는 레이블을 넣는 메소드, 그리고 그 메소드를 초기화하는 코드가 있다. 우리는 이 코드를 없애 버리고, 대신에 스프라이트를 넣을 것이다.
스프라이트 넣기
스프라이트를 넣기 전에, 당연히 스프라이트를 구성할 그림이 필요해진다. 여러분이 이 그림들을 직접 만들 수도 있지만, 나의 사랑하는 아내가 만들어준 그림들(플레이어, 표창, 적)을 쓸 수도 있을 것이다.
그림을 준비했다면, 그것들을 드래그해서 XCode의 Resources 폴더에 넣자. 그리고 “Copy items into destination group’s folder (if needed)” 부분이 체크되어 있는지를 확인하자.
이제 그림이 준비됐다. 우리는 이제 플레이어가 위치할 곳을 생각해봐야 한다. Cocos2D에서는 왼쪽 아래 지점이 (0,0)이며 거기서부터 오른쪽 위로 갈 수록 X와 Y 좌표가 증가한다는 것을 기억하라. 우리는 이 프로젝트를 가로 모드로 할 것이므로, 가장 오른쪽 위의 좌표는 (480, 320)이 될 것이다.
또한 우리가 오브젝트의 위치를 정할 때, 그림의 중심을 기준으로 삼는다는 것을 기억하라. 캐릭터를 스크린 왼쪽 끝에서 오른쪽 끝으로 이동시킨다고 할 때, 이동의 위치는 이 중심점을 기준으로 사용하게 될 것이다. 즉,
- 가로 좌표를 주기 위한 가로 기준점은 [그림의 가로 크기]/2
- 세로 좌표를 주기 위한 세로 기준점은 [그림의 세로 크기]/2
가 된다는 뜻이다.
좀 더 이해를 쉽게 하기 위해 다음 그림을 보자.
그럼 이제 실제로 만들어 보자. XCode에서 옆의 Classed 폴더를 열고 HelloWorldScene.m 파일을 선택한 뒤, 코드 편집창에서 init 메소드 부분을 다음과 같이 변경한다.
-(id) init { if( (self=[super init] )) { CGSize winSize = [[CCDirector sharedDirector] winSize]; CCSprite *player = [CCSprite spriteWithFile:@"Player.png" rect:CGRectMake(0, 0, 27, 40)]; player.position = ccp(player.contentSize.width/2, winSize.height/2); [self addChild:player]; } return self; } |
컴파일하고 실행해 보면 스프라이트가 잘 나타날 것이다. 하지만 아직 배경이 까맣게 나타나고 있다는 점도 알 수 있다. 우리의 닌자 캐릭터를 잘 보이게 하기 위해서는 아무래도 하얀 배경이 훨씬 나을 것이다. Cocos2D에서 이것을 간단하게 처리하고 싶으면 CCColoredLayer 클래스를 이용해 레이어의 배경색을 원하는 색으로 바꾸어 주면 된다. 바로 실험해보자. HelloWorldScene.h 파일을 열고 HelloWorld 인터페이스를 아래와 같이 선언한다.
@interface HelloWorld : CCColorLayer |
그 다음 HelloWorldScene.m 파일을 열고 init 메소드를 조금만 더 수정해주면 배경을 흰색으로 만들 수 있다.
if( (self=[super initWithColor:ccc4(255,255,255,255)] )) { |
수정이 끝났으면 빌드하고 실행해 보자. 이제 스프라이트가 흰 배경에 서 있는 모습이 보일 것이다. 우리의 닌자는 이제 싸울 준비가 됐다!
목표물 움직이기
다음으로 우리는 목표물, 즉 적들이 화면에 나타나 우리의 닌자를 공격해오도록 해야 한다. 이게 재미있게 느껴지려면 당연히 적들이 움직여야 할 것이다. 자, 그러면 적들이 화면 오른쪽에서 나타나 왼쪽으로 이동해 오도록 만들어 보자.
다음 코드를 init 메소드 바로 앞에 넣는다.
-(void)addTarget { CCSprite *target = [CCSprite spriteWithFile:@"Target.png" rect:CGRectMake(0, 0, 27, 40)];
// 적들이 어떤 Y 좌표에서 나타날 것인지를 결정한다 CGSize winSize = [[CCDirector sharedDirector] winSize]; int minY = target.contentSize.height/2; int maxY = winSize.height - target.contentSize.height/2; int rangeY = maxY - minY; int actualY = (arc4random() % rangeY) + minY;
// 적들을 화면 오른쪽 끝에서 나타나게 만든다 // 위쪽에서 계산한 결과에 따라 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는 이동, 점프, 페이드(사라지거나 나타날 때) 등등, 우리가 스프라이트를 애니메이션시킬 때에 사용할 수 있는 액션을 미리 많이 준비해 놓고 있다. 우리는 여기에서 그 중의 3가지 액션을 적들에게 시킬 것이다.
- CCMoveTo: CCMoveTo 액션은 오브젝트를 스크린 밖에서부터 왼쪽으로 곧바로 움직이게끔 한다. 전체 움직임이 다 끝날 때까지 걸리는 시간도 지정할 수 있는데, 우리는 2~4초 사이에서 랜덤하게 결정되게끔 할 것이다.
- CCCallFuncN: CCCallFuncN 함수는 액션이 다 끝났을 때 오브젝트에게 일어나는 콜벡을 정의한다. 여기에서는 "spriteMoveFinished"라는 콜백을 정의할 것인데, 지금은 아직 그 부분을 쓰지 않았으므로 나중에 더 설명한다.
- CCSequence: CCSequence 액션은 몇 가지 액션을 정해놓은 순서에 따라 행하게끔 만들 수 있다. 우리는 먼저 CCMoveTo 액션을 행하고, 그것이 끝나면 CCCallFuncN 액션을 행하도록 할 것이다.
이제 위에서 설명한 콜백 함수를 넣는다. addTarget 바로 전 부분에 다음 메소드를 넣자.
-(void)spriteMoveFinished:(id)sender { CCSprite *sprite = (CCSprite *)sender; [self removeChild:sprite cleanup:YES]; } |
알겠지만 이 함수의 목적은 화면 밖으로 나간 스프라이트를 제거하기 위한 것이다. 이 부분은 화면에 보이지 않는 알 수 없는 스프라이트들 때문에 메모리를 낭비하지 않게 하기 위해 매우 중요하다. 물론 스프라이트의 배열을 다시 사용해서 이 문제를 해결하는 다른 좋은 방법도 있지만, 필자의 이 초보자용 트터리얼에서는 위와 같은 간단한 방법을 취할 것이다.
적을 등장시키기 위해 마지막으로 해야 할 일이 있다.
One last thing before we go. We need to actually call the method to create targets! And to make things fun, let’s have targets continuously spawning over time. We can accomplish this in Cocos2D by scheduling a callback function to be periodically called. Once per second should do for this. So add the following call to your init method before you return:
[self schedule:@selector(gameLogic:) interval:1.0]; |
And then implement the callback function simply as follows:
-(void)gameLogic:(ccTime)dt { [self addTarget];} |
That’s it! So now if you compile and run the project, now you should see targets happily moving across the screen:
Shooting Projectiles
At this point, the ninja is just begging for some action – so let’s add shooting! There are many ways we could implement shooting, but for this game we are going to make it so when the user taps the screen, it shoots a projectile from the player in the direction of the tap.
I want to use a CCMoveTo action to implement this to keep things at a beginner level, but in order to use this we have to do a little math. This is because the CCMoveTo requires us to give a destination for the projectile, but we can’t just use the touch point because the touch point represents just the direction to shoot relative to the player. We actually want to keep the bullet moving through the touch point until the bullet goes off-screen.
Here’s a picture that illustrates the matter:
So as you can see, we have a small triangle created by the x and y offset from the origin point to the touch point. We just need to make a big triangle with the same ratio – and we know we want one of the endpoints to be off the screen.
Ok, so onto the code. First we have to enable touches on our layer. Add the following line to your init method:
self.isTouchEnabled = YES; |
Since we’ve enabled touches on our layer, we will now receive callbacks on touch events. So let’s implement the ccTouchesEnded method, which is called whenever the user completes a touch, as follows:
- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { // Choose one of the touches to work with UITouch *touch = [touches anyObject]; CGPoint location = [touch locationInView:[touch view]]; location = [[CCDirector sharedDirector] convertToGL:location]; // Set up initial location of projectile CGSize winSize = [[CCDirector sharedDirector] winSize]; CCSprite *projectile = [CCSprite spriteWithFile:@"Projectile.png" rect:CGRectMake(0, 0, 20, 20)]; projectile.position = ccp(20, winSize.height/2); // Determine offset of location to projectile int offX = location.x - projectile.position.x; int offY = location.y - projectile.position.y; // Bail out if we are shooting down or backwards if (offX <= 0) return; // Ok to add now - we've double checked position [self addChild:projectile]; // Determine where we wish to shoot the projectile to 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; // Move projectile to actual endpoint [projectile runAction:[CCSequence actions: [CCMoveTo actionWithDuration:realMoveDuration position:realDest], [CCCallFuncN actionWithTarget:self selector:@selector(spriteMoveFinished:)], nil]]; } |
In the first portion, we choose one of the touches to work with, get the location in the current view, then call convertToGL to convert the coordinates to our current layout. This is important to do since we are in landscape mode.
Next we load up the projectile sprite and set the initial position as usual. We then determine where we wish to move the projectile to, using the vector between the player and the touch as a guide, according to the algorithm described previously.
Note that the algorithm isn’t ideal. We’re forcing the bullet to keep moving until it reaches the offscreen X position – even if we would have gone offscreen in the Y position first! There are various ways to address this including checking for the shortest length to go offscreen, having our game logic callback check for offscreen projectiles and removing rather than using the callback method, etc. but for this beginner tutorial we’ll keep it as-is.
The last thing we have to do is determine the duration for the movement. We want the bullet to be shot at a constant rate despite the direction of the shot, so again we have to do a little math. We can figure out how far we’re moving by using the Pythagorean Theorem. Remember from geometry, that is the rule that says the length of the hypotenuse of a triangle is equal to the square root of the sum of the squares of the two sides.
Once we have the distance, we just divide that by the velocity in order to get the duration. This is because velocity = distance over time, or in other words time = distance over velocity.
The rest is setting the actions just like we did for the targets. Compile and run, and now your ninja should be able to fire away at the oncoming hordes!
Collision Detection
So now we have shurikens flying everywhere – but what our ninja really wants to do is to lay some smack down. So let’s add in some code to detect when our projectiles intersect our targets.
There are various ways to solve this with Cocos2D, including using one of the included physics libraries: Box2D or Chipmunk. However to keep things simple, we are going to implement simple collision detection ourselves.
To do this, we first need to keep better track of the targets and projectiles currently in the scene. Add the following to your HelloWorldScene class declaration:
And initialize the arrays in your init method:
And while we’re thinking of it, clean up the memory in your dealloc method:
[_targets release];_targets = nil;[_projectiles release];_projectiles = nil; |
Now, modify your addTarget method to add the new target to the targets array and set a tag for future use:
target.tag = 1;[_targets addObject:target]; |
And modify your ccTouchesEnded method to add the new projectile to the projectiles array and set a tag for future use:
projectile.tag = 2;[_projectiles addObject:projectile]; |
Finally, modify your spriteMoveFinished method to remove the sprite from the appropriate array based on the tag:
if (sprite.tag == 1) { // target [_targets removeObject:sprite]; } else if (sprite.tag == 2) { // projectile [_projectiles removeObject:sprite]; } |
Compile and run the project to make sure everything is still working OK. There should be no noticeable difference at this point, but now we have the bookkeeping we need to implement some collision detection.
Now add the following method to HelloWorldScene:
- (void)update:(ccTime)dt { NSMutableArray *projectilesToDelete = [[NSMutableArray alloc] init];
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 alloc] init];
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]; } |
The above should be pretty clear. We just iterate through our projectiles and targets, creating rectangles corresponding to their bounding boxes, and use CGRectIntersectsRect to check for intersections. If any are found, we remove them from the scene and from the arrays. Note that we have to add the objects to a “toDelete” array because you can’t remove an object from an array while you are iterating through it. Again, there are more optimal ways to implement this kind of thing, but I am going for the simple approach.
You just need one more thing before you’re ready to roll – schedule this method to run as often as possible by adding the following line to your init method:
[self schedule:@selector(update:)]; |
Give it a compile and run, and now when your projectiles intersect targets they should disappear!
Finishing Touches
We’re pretty close to having a workable (but extremely simple) game now. We just need to add some sound effects and music (since what kind of game doesn’t have sound!) and some simple game logic.
If you’ve been following my blog series on audio programming for the iPhone, you’ll be extremely pleased to hear how simple the Cocos2D developers have made it to play basic sound effects in your game.
First, drag some background music and a shooting sound effect into your resources folder. Feel free to use the cool background music I made or my awesome pew-pew sound effect, or make your own.
Then, add the following import to the top of your HelloWorldScene.m:
#import "SimpleAudioEngine.h" |
In your init method, start up the background music as follows:
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"background-music-aac.caf"]; |
0.99-final update: It seems there might be a minor bug in 0.99-final where the background music will only play once (whereas it should loop) – either that or I’m doing something wrong. For a workaround, see the comments at the end of this article.
And in your ccTouchesEnded method play the sound effect as follows:
[[SimpleAudioEngine sharedEngine] playEffect:@"pew-pew-lei.caf"]; |
Now, let’s create a new scene that will serve as our “You Win” or “You Lose” indicator. Click on the Classes folder and go to File\New File, and choose Objective-C class, and make sure subclass of NSObject is selected. Click Next, then type in GameOverScene as the filename, and make sure “Also create GameOverScene.h” is checked.
Then replace GameOverScene.h with the following code:
#import "cocos2d.h"
@interface GameOverLayer : CCColorLayer { CCLabel *_label; }
@property (nonatomic, retain) CCLabel *label;
@end
@interface GameOverScene : CCScene { GameOverLayer *_layer; }
@property (nonatomic, retain) GameOverLayer *layer;
@end |
Then replace GameOverScene.m with the following code:
#import "GameOverScene.h" #import "HelloWorldScene.h"
@implementation GameOverScene
@synthesize layer = _layer;
- (id)init { if ((self = [super init])) { self.layer = [GameOverLayer node]; [self addChild:_layer]; } return self; }
- (void)dealloc { [_layer release]; _layer = nil; [super dealloc]; } @end
@implementation GameOverLayer
@synthesize label = _label;
-(id) init { if( (self=[super initWithColor:ccc4(255,255,255,255)] )) { CGSize winSize = [[CCDirector sharedDirector] winSize]; self.label = [CCLabel labelWithString:@"" fontName:@"Arial" fontSize:32]; _label.color = ccc3(0,0,0); _label.position = ccp(winSize.width/2, winSize.height/2); [self addChild:_label]; [self runAction:[CCSequence actions: [CCDelayTime actionWithDuration:3], [CCCallFunc actionWithTarget:self selector:@selector(gameOverDone)], nil]]; } return self; }
- (void)gameOverDone { [[CCDirector sharedDirector] replaceScene:[HelloWorld scene]]; }
- (void)dealloc { [_label release]; _label = nil; [super dealloc]; }
@end |
Note that there are two different objects here: a scene and a layer. The scene can contain any number of layers, however in this example it just has one. The layer just puts a label in the middle of the screen, and schedules a transition to occur 3 seconds in the future back to the HelloWorld scene.
Finally, let’s add some extremely basic game logic. First, let’s keep track of the projectiles the player has destroyed. Add a member variable to your HelloWorld class in HelloWorldScene.h as follows:
int _projectilesDestroyed; |
Inside HelloWorldScene.m, add an import for the GameOverScene class:
#import "GameOverScene.h" |
Increment the count and check for the win condition in your update method inside the targetsToDelete loop right after removeChild:target:
_projectilesDestroyed++;if (_projectilesDestroyed > 30) { GameOverScene *gameOverScene = [GameOverScene node]; [gameOverScene.layer.label setString:@"You Win!"]; [[CCDirector sharedDirector] replaceScene:gameOverScene];} |
And finally let’s make it so that if even one target gets by, you lose. Modify the spriteMoveFinished method by adding the following code inside the tag == 1 case right after removeChild:sprite:
GameOverScene *gameOverScene = [GameOverScene node]; [gameOverScene.layer.label setString:@"You Lose :["]; [[CCDirector sharedDirector] replaceScene:gameOverScene]; |
Go ahead and give it a compile and run, and you should now have win and lose conditions and see a game over scene when appropriate!
Gimme The Code!
And that’s a wrap! Here’s the full code for the simple Cocos2D iPhone game that we developed thus far.
Where To Go From Here?
This project could be a nice basis for playing around some more with Cocos2D by adding some new features into the project. Maybe try adding in a bar chart to show how many more targets you have to destroy before you win (check out the drawPrimitivesTest sample project for examples of how to do that). Maybe add cooler death animations for when the monsters are destroyed (see ActionsTest, EffectsTest, and EffectsAdvancedTest projects for that). Maybe add more sounds, artwork, or gameplay logic just for fun. The sky’s the limit!
If you want to keep going with this tutorial series, check out How To Add A Rotating Turret to this game!
Also, if you’d like to keep learning more about Cocos2D, check out my tutorials on how to create buttons in Cocos2D, intro to Box2D, or how to create a simple Breakout game.
Feel free to chime in if you know of any better ways to do various things with this project or if there are any problems – like I said this is the first time I’ve played with Cocos2D so I have a lot left to learn!
---
출처 : http://www.raywenderlich.com/352/how-to-make-a-simple-iphone-game-with-cocos2d-tutorial