cocos2d의 액션과 이벤트를 테스트해 보았습니다. 관련 문서들은 cocos2d 위키 번역해서 올린 것으로 대강 정리되었고 이 포스트에 참조할 문서는
이 정도가 되겠습니다.(API reference 번역은 불가능이예요. doxygen으로 만들어진 문서라 -_-;)
제가 만든 소스는 ActionTest.zip으로 압축해 놓았으니 다운받아서 참조하시고요, 그 소스의 설명이 이 포스트의 전부입니다.
cocos2d는 다양한 액션을 제공하는데, 이동, 크기변환 등의 기본 액션(특이하게도 점프 액션이 따로 있답니다.)과 기본 액션들을 결합하거나 반복하는 액션이 제공됩니다.(자세한 내용은 Basic Action와 Composition actions를 참조하세요.) 보통 순서는 레이어를 만들고, 레이어에 스프라이트와 액션을 만들고, 스프라이트에 액션을 동작시키는 순입니다.
먼저 이번 테스트를 위한 구상은 동그란 이미지 하나를 스프라이트로 만든 뒤에 화면을 터치하면 액션하는 것입니다. 이미지는 이걸 사용했죠.

XCode와 iPhone SDK, 그리고 cocos2d의 templete을 설치(자세한 것은 프로그래밍 가이드를 참조하세요) 하셨으면 새 프로젝트에 cocos2d templete이 보일 겁니다. 새 프로젝트를 ActionTest라고 만드셨다면 ActionTestAppDelegate.h, ActionTestAppDelegate.m, HelloWorldScene.h, HelloWorldScene.m(요 두개는 기억이 가물가물해서..)가 생성됩니다. HelloWorldScene 두 파일을 LabelsLayerScene으로 바꾸시고, FireElementScene라는 새 Objective-c 파일을 추가하세요. LabelsLayerScene은 그냥 화면에 배경 글자를 표시하게 하고, 실제 액션은 FireElementScene 파일에서 작업할 겁니다.
Classes 파일 중 ActionTestAppDelegate 코딩을 살펴보죠.
ActionTestAppDelegate.h
#import <UIKit/UIKit.h>
@interface ActionTestAppDelegate : NSObject <UIApplicationDelegate, UIAlertViewDelegate, UITextFieldDelegate> {
UIWindow *window;
}
@property (nonatomic, retain) UIWindow *window;
@end
템플릿에서 생성한 것을 거의 그대로 쓰는데 UIAlertViewDelegate, UITextFieldDelegate 두 개의 프로토콜을 추가해 줍니다.(UITextField야.. 화면에 찍어보려고 한 거고, UIAlertView도 좀 해보려고 했는데 이번엔 사용하지 않았네요.) 이것 말고는 더 한 것이 없어서 다음 .m 파일로 가죠.
ActionTestAppDelegate.m
#import "ActionTestAppDelegate.h"
#import "cocos2d.h"
#import "LabelsLayerScene.h"
#import "FireElementScene.h"
@implementation ActionTestAppDelegate
@synthesize window;
- (void) applicationDidFinishLaunching:(UIApplication*)application
{
window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
[window setUserInteractionEnabled:YES];
[window setMultipleTouchEnabled:YES];
if( ! [CCDirector setDirectorType:CCDirectorTypeDisplayLink] )
[CCDirector setDirectorType:CCDirectorTypeDefault];
[[CCDirector sharedDirector] setPixelFormat:kPixelFormatRGBA8888];
[CCTexture2D setDefaultAlphaPixelFormat:kTexture2DPixelFormat_RGBA8888];
[[CCDirector sharedDirector] setDeviceOrientation:CCDeviceOrientationLandscapeLeft];
[[CCDirector sharedDirector] setAnimationInterval:1.0/60];
[[CCDirector sharedDirector] setDisplayFPS:YES];
[[CCDirector sharedDirector] attachInView:window];
[window makeKeyAndVisible];
CCScene *scene = [CCScene node];
CCLayer *labelLayer = [LabelsLayer node];
[scene addChild:labelLayer];
CCLayer *fireElementLayer = [FireElement node];
[scene addChild:fireElementLayer];
[[CCDirector sharedDirector] runWithScene: scene];
}
- (void)applicationWillResignActive:(UIApplication *)application {
[[CCDirector sharedDirector] pause];
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
[[CCDirector sharedDirector] resume];
}
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
[[CCTextureCache sharedTextureCache] removeUnusedTextures];
}
- (void)applicationWillTerminate:(UIApplication *)application {
[[CCDirector sharedDirector] end];
}
- (void)applicationSignificantTimeChange:(UIApplication *)application {
[[CCDirector sharedDirector] setNextDeltaTimeZero:YES];
}
- (void)dealloc {
[[CCDirector sharedDirector] release];
[window release];
[super dealloc];
}
@end
여기에 몇가지 중요한 코드들이 있습니다. 각 메소드 골격은 그대로 유지하고, app이 동작을 시작하기 직전에 applicationDidFinishLaunching 메소드가 호출(자동으로)되게 되는데요, 이 메소드가 거의 모든 초기화를 담당하거나 다른 초기화 메소드를 호출하게 됩니다.
[window setUserInteractionEnabled:YES];
[window setMultipleTouchEnabled:YES];
setUserInteractionEnabled이 YES이면 터치가 가능하고 setMultipleTouchEnabled이 YES이면 멀티터치가 가능해집니다. 통상 UIView 기반의 app들은 Controller 클래스에 동작을 정의하고 뷰의 nib에 정의된 UI를 아웃렛이 정의된 Delegate로 연결해서 상호동작하게 하는데, cocos2d에서는 그저 속성을 셋팅하면 사용자의 UI 이벤트(터치나 흔들기 등)에 따라서 미리 정의된 이름의 메소드를 호출합니다. 아웃렛과 델리게이트의 마우스질을 안하게 되지요.(제가 좋아하는 스타일이기도 합니다. ㅎㅎ) setUserInteractionEnabled을 셋팅하였기 때문에 화면을 터치하였을 때 ccTouchesEnded가 호출될 수 있는 것이죠.
CCScene *scene = [CCScene node];
CCLayer *labelLayer = [LabelsLayer node];
[scene addChild:labelLayer];
CCLayer *fireElementLayer = [FireElement node];
[scene addChild:fireElementLayer];
위의 코드가 cocos2d의 Scene-Layer-Sprite의 핵심입니다. (자세한 것은 cocos2d 기본 컨셉를 참조하세요) 전체적으로 여러 노드를 포함하는 트리구조를 이루어 Scene과 Layer는 node가 되고, 각 Sprite는 Layer의 child가 되는 방법이지요. 위의 코드는 LabelsLayerScene으로 Scene을 생성하고(실행까지), 그 scene에 FireElementScene을 붙이는 겁니다. (저도 처음에는 Scene, Layer, Node가 영 헷갈렸는데 직접 코딩해 보니깐 이해가 가더군요.) 액션과 이벤트는 FireElement에 정의하게 되지만 그 이벤트가 일어나기 위해서는 위에서 언급한 setUserInteractionEnabled를 셋팅하여야 했습니다.
LabelsLayerScene.h
#import "cocos2d.h"
@interface LabelsLayer : CCLayer
{
}
+(id) scene;
@end
LabelsLayerScene 는 CCLayer를 상속한 것 말고는 거의 한 게 없어요.
LabelsLayerScene.m
#import "LabelsLayerScene.h"
@implementation LabelsLayer
+(id) scene
{
CCScene *scene = [CCScene node];
LabelsLayer *layer = [LabelsLayer node];
[scene addChild: layer];
return scene;
}
-(id) init
{
if( (self=[super init] )) {
CCLabel* label = [CCLabel labelWithString:@"Action Test" fontName:@"Marker Felt" fontSize:64];
CGSize size = [[CCDirector sharedDirector] winSize];
label.position = ccp( size.width /2 , size.height/2 );
[self addChild: label];
}
return self;
}
- (void) dealloc
{
[super dealloc];
}
@end
물론 “Action Test”라는 CCLabel 인스턴스를 만들어서 레이어(자기자신)에 붙인 건 있죠.
FireElementScene.h
#import "cocos2d.h"
@interface FireElement : CCLayer {
}
+(id) scene;
@end
enum {
kTagSprite = 1,
};
kTagSprite를 정의한 enum 타입은 Sprite를 편하게 찾아오기 위해서입니다. 일단 태그처럼 enum 타입을 하나 정해놓고 sprite 인스턴스를 layer에 붙일 때 알려주면 다른 메소드가 해당 레이어를 사용할 때 스프라이트를 알아낼 수 있죠.
FireElementScene.m
#import "FireElementScene.h"
@implementation FireElement
+(id) scene
{
CCScene *scene = [CCScene node];
FireElement *layer = [FireElement node];
[scene addChild: layer];
return scene;
}
-(id) init
{
if( (self=[super init] )) {
self.isTouchEnabled = YES;
CCSprite *sprite = [CCSprite spriteWithFile:@"element_fire.png"];
CGSize size = [[CCDirector sharedDirector] winSize];
sprite.position = ccp(size.width / 2, size.height / 2);
[self addChild:sprite z:1 tag:kTagSprite];
}
return self;
}
-(BOOL)ccTouchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
UITouch *touch = [touches anyObject];
if(touch) {
CGPoint location = [touch locationInView:[touch view]];
CGPoint convertedPoint = [[CCDirector sharedDirector] convertToGL:location];
CCNode *sprite = [self getChildByTag:kTagSprite];
[sprite stopAllActions];
id action1 = [CCMoveTo actionWithDuration:0.4f position:convertedPoint];
id action2 = [CCJumpTo actionWithDuration:0.6f position:ccp(convertedPoint.x + 80.0f, convertedPoint.y) height:50.0f jumps:1];
id action3 = [CCRotateBy actionWithDuration:0.6f angle:90.0f];
id action4 = [CCBlink actionWithDuration:1.0f blinks:3];
[sprite runAction:[CCSequence actions:action1, action2, action3, action4, nil]];
}
}
- (void) dealloc
{
[super dealloc];
}
@end
[self addChild:sprite z:1 tag:kTagSprite];
이 코드가 layer에 sprite를 붙일 때 tag를 넣어주는 것이고
CCNode *sprite = [self getChildByTag:kTagSprite];
이 코드가 layer에서 sprite를 찾아오는 코드입니다.
id action1 = [CCMoveTo actionWithDuration:0.4f position:convertedPoint];
id action2 = [CCJumpTo actionWithDuration:0.6f position:ccp(convertedPoint.x + 80.0f, convertedPoint.y) height:50.0f jumps:1];
id action3 = [CCRotateBy actionWithDuration:0.6f angle:90.0f];
id action4 = [CCBlink actionWithDuration:1.0f blinks:3];
[sprite runAction:[CCSequence actions:action1, action2, action3, action4, nil]];
액션을 정의하고 동작시키는 부분입니다. ccTouchesEnded 메소드는 터치할 때 마다 호출 될 거고(그래서 [sprite stopAllActions]으로 동작하고 있는 액션을 터치했을 때 일단 멈춰주는 거죠. 안그러면 하던 액션을 다 하고 나서 터치된 새 액션이 실행되니까요.) CCSequence 액션이 실행됩니다. Sequence 액션은 전달받은 액션들을 순서대로 실행하는 이해하기 쉬운 액션이죠.
또 주의할 점이 iPhone SDK의 그래픽은 Top-Left(좌상단)가 원점이 되는 좌표계를 쓰지만, cocos2d는 OpenGL 기반이라 좌하단이 원점이 되는 좌표계를 사용합니다. 그래서 위치를 계산하기 전에 convertToGL 메소드로 좌표들을 변환해야 합니다.
CGPoint convertedPoint = [[CCDirector sharedDirector] convertToGL:location];
이 코드가 변환하는 코드입니다.
기본 액션(위에서는 Move, Jump, Rotate)들은 To와 By가 있는데요, To는 쉽게 생각되죠. 이동을 원하는 지점이 (80, 80)이면 [CCMoveTo position:ccp(80,80)]이라고 하면 되니깐요. By는 해당 node가 있는 지점에서 얼마큼 더 가느냐 하는 겁니다. To를 By로 바꾸시거나 By를 To로 바꾸시면서 동작을 확인해보시면 쉽게 이해하실 수 있을 겁니다.