지난번 연재 5-1, 5-2와 이어집니다. 그림번호는 새로 시작됩니다.
이제 Cocos2d-x를 사용하여 본격적으로 게임 개발을 시작하도록 하겠습니다. 먼저 새로운 Scene을 추가해보겠습니다. 앞으로 새로운 클래스는 멀티 플랫폼 적용을 위해 모두 이렇게 추가해야 합니다.
솔루션 탐색기에서 minigame 아래의 src 경로를 오른쪽 클릭한 뒤 ‘클래스 마법사’를 선택합니다.
Figure 5‑1 클래스 마법사
클래스 마법사 창이 뜨면 좌측의 ‘프로젝트’를 minigame으로 선택합니다. 그다음 우측의 [클래스 추가] 버튼을 선택합니다.
Figure 5‑2 클래스 마법사
‘클래스 이름’에 StartScene이라고 입력하면 .h 파일(헤더 파일)과 .cpp 파일명(클래스 파일)이 자동으로 입력됩니다. 여기서부터 중요합니다. .h 파일과 .cpp파일의 위치를 Classes 폴더로 지정해주어야 합니다. .h 파일 부분에서 파일명 […] 버튼을 선택합니다.
Figure 5‑3 클래스 마법사
이때 ‘StartScene.h 파일이 없습니다. 파일을 만드시겠습니까?’라는 경고창이 뜨면 [아니요] 버튼을 선택합니다. 다른 폴더를 지정할 것이기 때문입니다.
Figure 5‑4 파일 경고창
파일 선택창을 보면 기본 폴더가 proj.win32로 되어 있을 것입니다. 기본 설정대로 이곳에 클래스 파일을 만들면 윈도우용 빌드 시에만 사용할 수 있으므로, minigame\Classes 폴더로 이동합니다. 파일이 없어 생성하겠느냐는 경고창이 뜬다면 [예] 버튼을 선택합니다. 파일 이름이 StartScene.h이 맞는지 마지막으로 확인한 뒤 [열기] 버튼을 선택합니다. 이때도 경고창이 뜬다면 [예] 버튼을 선택합니다.
Figure 5‑5 파일 선택창에서 경로 확인
.h 파일의 위치가 설정된 것을 확인할 수 있습니다.
Figure 5‑6 .h 파일 경로 설정한 모습
같은 방법으로 .cpp 파일도 .h 파일과 같은 위치를 지정하여 파일을 생성해줍니다. 설정이 끝났으면 다음 그림과 같을 것입니다. .h 파일과 .cpp 파일의 위치를 모두 설정하였으면 하단의 [마침] 버튼을 선택하여 창을 닫습니다.
Figure 5‑7 .cpp 파일 경로 설정한 모습
이제 클래스 마법사 창으로 돌아와 ‘클래스 이름’을 보면 방금 생성한 StartScene이 있는 것을 확인할 수 있습니다. 하단의 [확인] 버튼을 눌러 클래스 마법사 창을 닫습니다.
Figure 5‑8 클래스 마법사
이처럼 멀티 플랫폼 적용을 위해선 Proj.win32 상위 폴더인 minigame폴더의 Classes 폴더에 클래스를 생성해야 합니다. 이렇게 지정해주지 않으면 다른 플랫폼 빌드 시 클래스를 찾지 못합니다.
이제 솔루션 탐색기를 보면 StartScene.h과 StartScene.cpp가 생성된 것을 볼 수 있습니다.
Figure 5‑9 솔루션 탐색기에 추가된 클래스 및 헤더 파일
이 두 파일을 src 경로로 드래그해 이동합니다. 이는 프로젝트 내에서 링크를 연결하는 것으로, 실제 파일이 이동되진 않습니다.
Figure 5‑10 드래그해서 이동한 모습
다음으로 StartScene.h 파일과 StartScene.cpp 파일의 코드를 수정해보겠습니다. 헤더 파일은 HelloWorldScene.h 파일 내용을 전체 복사하여 붙여넣습니다. 그다음 아래와 같이 두 군데 노란색 부분에서 클래스명을 수정하고, 1~2행 및 마지막 행의 #ifndef, #define, #endif 같은 매크로도 삭제해야 합니다. 중복 정의를 막기 위해 기본 파일에 들어 있는 매크로이므로 이를 삭제하지 않으면 실습 시 파일을 복사해서 사용할 수 없습니다.
--------------------------StartScene.h--------------------------
#include "cocos2d.h"
class StartScene : public cocos2d::Layer
{
public:
// there's no 'id' in cpp, so we recommend returning the class instance pointer
static cocos2d::Scene* createScene();
// Here's a difference. Method 'init' in cocos2d-x returns bool, instead of returning 'id' in cocos2d-iphone
virtual bool init();
// a selector callback
void menuCloseCallback(cocos2d::Ref* pSender);
// implement the "static create()" method manually
CREATE_FUNC(StartScene);
};
다음으로 StartScene.cpp 파일 역시 HelloWorldScene.cpp 파일을 전체 복사하여 필요 없는 부분은 삭제하고 클래스명을 수정합니다. 다음 코드에서 노란색이 삭제 또는 수정한 부분입니다.
--------------------------StartScene.cpp--------------------------
#include "StartScene.h"
USING_NS_CC;
Scene* StartScene::createScene()
{
// 'scene' is an autorelease object
auto scene = Scene::create();
// 'layer' is an autorelease object
auto layer = StartScene::create();
// add layer as a child to scene
scene->addChild(layer);
// return the scene
return scene;
}
// on "init" you need to initialize your instance
bool StartScene::init()
{
//////////////////////////////
// 1. super init first
if (!Layer::init())
{
return false;
}
// 삭제
// 추후 코드 넣을 곳
return true;
}
// 이하 삭제
StartScene.h 파일과 StartScene.cpp 파일을 수정하였으면 프로그램 시작 시에 호출하는 클래스를 StartScene으로 변경하겠습니다. 기존에는 HelloWorld를 호출하게 되어 있었습니다. AppDelegate.cpp 파일을 열어 먼저 2행에서 헤더 파일명을 변경해줍니다. 즉 #include "HelloWorldScene.h" 라인을 #include "StartScene.h"로 수정합니다. 그다음 HelloWorld가 아니라 StartScene을 호출하도록 auto scene = HelloWorld::createScene(); 라인도 auto scene = StartScene::createScene();으로 수정합니다.
이렇게 프로그램 시작 시 HelloWorld 클래스를 호출하는 것을 StartScene로 수정한 다음 디버거를 다시 실행해보면, 에러 없이 다음과 같이 실행되어야 합니다. 현재 StartScene 안에는 아무 내용도 없으므로 아무것도 없는 검은 화면이 나타나면 정상입니다.
Figure 5‑11 시작 클래스 변경 후 실행 화면
이제 StartScene 화면의 UI를 작성하도록 하겠습니다. StartScene.cpp 파일에서 init() 메서드를 찾아 다음과 같이 수정합니다. ‘init’는 initialize의 약자로 초기화한다는 뜻입니다.
----------StartScene.cpp----------
// on "init" you need to initialize your instance
bool StartScene::init()
{
//////////////////////////////
// 1. super init first
if (!Layer::init())
{
return false;
}
// 추후 코드 넣을 곳
/*****디바이스의 크기를 가져옵니다.*****/
//Director를 가져옵니다.
auto director = Director::getInstance();
//OpenGLView를 가져옵니다.
auto glview = director->getOpenGLView();
//OpenGLView에서 DesignResolutionSize를 가져옵니다.
auto winSize = glview->getDesignResolutionSize();
/*****배경 이미지 back을 넣습니다.*****/
//Sprite를 생성하여 이미지를 불러옵니다.
auto back = Sprite::create("title_bg.png");
//back을 해당 포인트에 위치시킵니다. 화면의 정중앙에 위치하도록 했습니다.
back->setPosition(Point(winSize.width / 2, winSize.height / 2));
//this에 back을 자식 노드로 추가하였습니다.
this->addChild(back);
return true;
}
init()를 수정하였으면 디버거를 실행합니다. 배경(title_bg.png)이 추가된 것을 확인할 수 있습니다.
Figure 5‑12 배경을 추가한 실행 화면
위 소스 중 중요한 부분은 auto 키워드, create(), setPosition(), addChild() 부분입니다. 그리고 코드에 직접 나오지는 않지만 노드(Node)에 대해서도 살펴보고, 이후 사용할 setAnchorPoint()도 미리 알아보고 넘어가도록 하겠습니다. 앞으로도 계속 나오는 부분이니 꼭 숙지하기 바랍니다.
auto 키워드는 C++ 11 버전부터 사용할 수 있는 키워드입니다. Cocos2d-x 3.0 버전에서부터 C++ 11을 지원합니다. auto 키워드를 사용하지 않아도 에러는 발생하지 않으나, auto 키워드를 사용하면 매우 편하고 소스 코드도 간결해지므로 꼭 숙지하고 사용하도록 합시다.
auto 키워드가 없는 기존의 방식은, 예를 들어 Sprite 객체의 경우 반드시 Sprite로 선언해야 했습니다. 즉 이런 식으로 선언해야만 했습니다.
Sprite *test = Sprite::create("test.png");
auto 키워드는 생성된 객체의 클래스에 따라 자료형을 자동으로 정해주기 때문에 매우 간편하게 사용할 수 있습니다.
auto test = Sprite:::create("test.png");
주의할 점은, auto 키워드는 선언과 초기화를 동시에 하는 경우에만 사용할 수 있다는 점입니다. 따라서 다음과 같이 사용할 수는 없다는 것을 알아두도록 합니다. 기존의 2.x 버전을 사용했던 분들은 헷갈릴 수 있으므로 참고하기 바랍니다.
//이렇게 사용하면 안 됩니다.
auto test;
test = Sprite:::create("test.png");
노드(Node)는 Scene을 구성하는 기본 객체입니다. cocos2d-x의 Scene을 구성하는 대부분의 객체들은 이 Node를 상속받습니다. 예를 들어 화면을 구성하는 Sprite나 Layer 등 화면에 나타나야 하는 클래스들이 이 Node를 상속받습니다.
create()는 노드나 액션 등 Cocos2d-x에서 사용하는 대다수의 클래스를 생성하는 메서드입니다. create()를 사용하여 객체를 생성하?메모리 해제를 수동으로 해주지 않아도 됩니다. 내부에서 자동으로 메모리를 해제해주는 autorelease() 코드가 들어 있기 때문입니다.
create()는 객체를 생성할 때 사용하는 메서드로 각 클래스마다 매개변수가 다르므로 형식에 맞추어 사용해야 합니다. Sprite의 경우 파일명을 매개변수로 넘깁니다.
setPosition()은 노드의 위치를 지정해주는 메서드입니다. 노드마다 사용하므로 꼭 알고 넘어갑시다. setPosition()은 Point를 매개변수를 받으며 Point는 x좌표와 y좌표를 가질 수 있습니다. 좌측 상단을 원점으로 잡는 언어들도 있지만, Cocos2d-x의 좌표계는 좌측 하단이 (0, 0)이고, 우측 상단이 (320, 480)이 됩니다.
(320, 480) |
(0, 0) |
Figure 5‑13 Cocos2d-x 좌표계
setAnchorPoint()는 앵커 포인트를 설정한다는 뜻이며, 앵커 포인트란 노드가 화면에 표시될 때 기준이 되는 점을 말합니다. 노드는 기본적으로 노드의 가운데를 기준으로 위치됩니다. x 및 y가 0에서 1 사이인 Point를 매개변수로 받습니다. Point(0.5, 0.5)는 노드의 정중앙입니다. 대부분의 노드들은 이 Point(0.5, 0.5)를 초기값으로 갖습니다. 하지만 Layer는 Point(0, 0)을 기본으로 갖습니다. 이 이유는 씬은 Layer를 상속받아 만들어집니다. 이 씬은 좌측 하단을 기준으로 위치하기에 Point(0, 0)을 기본값으로 갖습니다. 우리는 앞에서 배경 이미지(title_bg.png)를 화면 정가운데에 위치시켰습니다. 이 그림은 크기가 정확히 320 * 480인데, 어떻게 화면에 딱 맞게 들어갔을까요? back의 앵커 포인트가 기본값인 Point(0.5, 0.5)로서 이미지의 정중앙이므로, setPosition()을 사용하여 Point(winSize.width / 2, winSize.height / 2)로 위치시켰을 때 화면에 딱 맞게 표시된 것입니다.
//back을 해당 포인트에 위치시킵니다. 화면의 정중앙에 위치하도록 했습니다.
back->setPosition(Point(winSize.width / 2, winSize.height / 2));
이 부분은 뒤에 실전에서 사용하는 부분에서 다시 한 번 설명하겠습니다.
addChild() 메서드의 매개변수는 세 가지 종류가 있습니다.
virtual void addChild(Node *child);
virtual void addChild(Node *child, int localZOrder);
virtual void addChild(Node *child, int localZOrder, int tag);
하나씩 살펴보겠습니다. 첫 번째로 Node만 받는 경우입니다. 이때는 해당 노드만 자식으로 추가됩니다.
addChild(child);
두 번째는 Node와 ZOrder를 받습니다. 일반적으로 ZOrder를 입력하지 않은 경우 나중에 추가된 자식 노드가 위로 올라오게 되지만 ZOrder를 사용하면 순서를 조절할 수 있습니다.
addChild(child, 1);
세 번째는 Node, ZOrder, Tag를 입력으로 받습니다. Tag의 경우 setTag()를 이용하여 추가할 수도 있는데, 이렇게 addChild() 시 추가할 수도 있습니다. Tag 사용법은 추후에 설명하도록 하겠습니다.
addChild(child, 1, 3);
이상 세 가지를 필요한 상황에 맞추어 사용할 것입니다.
UI 작업을 진행하다 보면 경로나 파일명이 잘못됐을 경우 에러 메시지를 볼 수 있습니다. 이런 자주 발생하는 에러의 경우 비주얼 스튜디오의 출력창을 살펴보면 아래와 같은 메시지가 있을 겁니다. ErrorTest.png는 에러가 난 파일명입니다.
Get data from file(ErrorTest.png) failed
위와 같은 에러가 나오면 해당 경로나 파일명이 잘못되진 않았는지 확인해봅시다.
배경 이미지만 있는 메인 화면은 썰렁하죠? 이제 init() 메서드 내부를 수정하여 타이틀과 캐릭터를 추가하겠습니다.
----------StartScene.cpp----------
//this에 back을 자식 노드로 추가하였습니다.
this->addChild(back);
/*****타이틀 title을 넣습니다.*****/
//Sprite를 생성하여 이미지를 불러옵니다.
auto title = Sprite::create("title_text.png");
//앵커 포인트를 Point(0.5f, 1)로 변경
title->setAnchorPoint(Point(0.5f, 1));
//title을 해당 포인트에 위치시킵니다. 화면의 가로 중앙에 위치하도록 했습니다.
title->setPosition(Point(winSize.width / 2, winSize.height - 30));
//this에 title을 자식 노드로 추가하였습니다.
this->addChild(title);
/*****캐릭터 character을 넣습니다.*****/
//Sprite를 생성하여 이미지를 불러옵니다.
auto character = Sprite::create("title_character.png");
//character를 해당 포인트에 위치시킵니다. 화면의 정중앙에 위치하도록 했습니다.
character->setPosition(Point(winSize.width / 2, winSize.height / 2));
//this에 character를 자식 노드로 추가하였습니다.
this->addChild(character);
return true;
}
배경 이미지와 마찬가지로 Sprite를 이용하여 생성해주었습니다. 여기서는 title만 setAnchorPoint()로 앵커 포인트를 기본값인 정중앙 대신 Point(0.5f, 1)로 변경해봤습니다. setAnchorPoint()로 즉 다음 그림에서 화살표로 표시된 부분으로 앵커 포인트를 변경한 것입니다. 여기서는 차이를 확인하기 어렵지만 뒤에서 타이틀을 회전할 때는 앵커 포인트에 따른 차이를 시각적으로 확인할 수 있습니다.
Figure 5‑14 앵커 포인트
앵커 포인트의 매개변수는 0에서 1 사이의 값을 사용하며 0은 처음, 1은 끝을 나타냅니다. 0.5는 물론 그 가운데를 나타냅니다. 이 값을 퍼센트라고 생각하면 이해하기 쉽습니다. 1=100%, 0=0%, 0.5=50%처럼 말이죠.
Figure 5‑15 앵커 포인트
title->setPosition을 보면 y 위치가 winSize.height에서 30만큼 뺀 곳입니다. 이것은 곧 화면 위에서 30만큼 아래로 떨어진 곳에 위치하라는 뜻입니다.
이제 디버거를 실행해보면 배경 이미지 위에 타이틀과 캐릭터가 추가된 것을 볼 수 있습니다.
Figure 5‑16 타이틀과 캐릭터를 넣은 실행 화면
지금까지 본 실행 화면 좌측 하단을 살펴보면 GL verts, GL calls, 그리고 숫자가 있습니다. GL verts란 3D에서 사용하는 버텍스의 개수 입니다. 버텍스란 폴리곤의 꼭짓점이 만나는 곳을 뜻하며, 이 버텍스가 18개가 사용되었다는 의미입니다. GL calls란 OpenGL이 호출되는 횟수입니다. 즉 노드의 개수라고 볼 수 있습니다. 맨 아래 숫자는 FPSframes per second, 즉 초당 화면이 갱신되는 횟수입니다. 60.1이란 60 FPS라는 뜻이죠. 디버깅 시 이 숫자를 모니터링하며 현재 화면의 노드 개수와 FPS를 확인하고 퍼포먼스에 문제가 없는지 살펴볼 수 있습니다.
물론 프로그램을 배포할 때는 저런 표시가 남아 있으면 안 되겠죠. 이를 표시하지 않기 위해서는 AppDelegate.cpp 파일을 수정해야 합니다. 다음 라인을 찾아 true를 false로 바꿔주면 됩니다.
// turn on display FPS
director->setDisplayStats(false);
이번엔 누를 수 있는 [캐릭터 만들기] 버튼을 구현해보겠습니다. 뒤에 다른 버튼을 추가할 것이므로 버튼의 이름은 button1이라고 하죠. StartScene.cpp 파일에 아래와 같이 버튼 부분을 구현하도록 합니다.
----------StartScene.cpp----------
//this에 character를 자식 노드로 추가하였습니다.
this->addChild(character);
//버튼 추가
auto button1 = MenuItemImage::create("title_btn_1.png", "");
button1->setPosition(Point(winSize.width / 2, 100));
//생성된 버튼을 메뉴에 추가한다.
auto menu = Menu::create(button1, NULL);
menu->setPosition(Point::ZERO);
this->addChild(menu);
return true;
}
이번에 새로 사용한 클래스는 MenuItemImage와 Menu입니다. MenuItemImage는 MenuItem, 즉 메뉴 항목의 한 종류이며, Cocos2d-x에서는 버튼을 메뉴 항목이라고 부른다고 이해하면 됩니다. 이러한 버튼은 Menu 클래스에 담아야만 화면에 붙일 수 있습니다.
MenuItem에는 다시 MenuItemSprite 등의 하위 클래스가 있고, MenuItemSprite 아래에 MenuItemImage가 있습니다. 간단히 말해 MenuItemImage는 파일명을 받아 MenuItem을 생성하고, MenuItemSprite는 Sprite를 받아 MenuItem을 생성합니다.
MenuItem은 Menu에 추가되어야 정상적으로 동작합니다. 따라서 MenuItem을 생성한 다음에는 Menu도 생성하고 앞에서 생성한 MenuItem을 반드시 Menu에 추가하여 사용하도록 합니다.
Menu의 create()에는 여러 개의 MenuItem이 들어갈 수 있습니다. 따라서 마지막 항목을 가리키기 위해 마지막에 NULL을 입력하는 습관을 들여야 합니다. 만약 NULL로 끝나지 않을 경우 에러가 발생하거나, 에러가 발생하지 않더라도 iOS에서는 동작하나 안드로이드에서는 오작동하는 등 디버깅이 어려워질 수도 있습니다. 여러 개의 항목을 Menu에 한 번에 담기 위해선 create(Item1, Item2, item3, …, NULL) 이런 식으로 사용할 수 있습니다.
Menu를 붙일 때는 setPoint(Point::ZERO)를 꼭 해주어야 터치 좌표가 정확하게 나옵니다(Point::ZERO는 Point(0, 0)과 동일한 의미입니다). 개별 버튼의 위치는 setPosition()에서 지정합니다.
이제 디버거를 실행해보도록 합시다. [캐릭터 만들기]라는 버튼이 생성되었습니다.
Figure 5‑17 버튼 추가 후 실행 화면
하지만 이 버튼이 눌려지는 것인지 아닌지는 알 수 없습니다. 따라서 버튼을 눌렀을 때 눌렸는지 알 수 있도록 변경해보도록 하겠습니다. 다음과 같이, 앞에서 추가한 코드 중 MenuItemImage의 create()에 두 번째 매개변수를 추가합니다.
//버튼 추가
auto button1 = MenuItemImage::create("title_btn_1.png", "title_btn_1_on.png");
button1->setPosition(Point(winSize.width / 2, 100));
두 번째 매개변수는 selectedImage, 즉 눌렸을 때 이미지입니다. 참고로 첫 번째 매개변수는 normalImage라고 합니다. 이렇게 수정하고 디버거를 실행하고, 버튼을 눌러보면 이미지가 바뀌는 것을 확인할 수 있습니다.
Figure 5‑18 눌렀을 때 이미지 추가 후
MenuItemImage의 create()에는 normalImage와 selectImage 외에도 disabledImage를 매개변수로 추가할 수 있습니다. disabledImage란 버튼이 사용 가능하지 않을 때(누를 수 없을 때) 표시될 이미지를 뜻합니다. 다음과 같이, 앞의 코드에 매개변수 두 개를 추가하고, 그 아래 한 행을 추가합니다.
//버튼 추가
auto button1 = MenuItemImage::create("title_btn_1.png", "title_btn_1_on.png", "title_btn_1_dis.png", [&](Ref *sender) {});
button1->setEnabled(false);
button1->setPosition(Point(winSize.width / 2, 100));
여기서 MenuItemImage의 네 번째 매개변수 [&](Ref *sender) {}를 콜백 메서드라고 합니다. 이 버튼을 눌렀을 때 자동으로 실행되는 메서드라고 이해하면 됩니다. 여기에서는 별도의 메서드를 생성하지 않고 이름 없는 메서드를 즉석에서 생성하여 등록했습니다. 이를 람다lambda 방식이라고 부릅니다. 람다는 이름없는 메서드입니다. 쉽게 말해 따로 선언하지 않고 바로 등록하여 사용하는 방식입니다. 특정 작업을 여러 번 사용하지 않는 경우 람다를 사용하는 것이 좋지만, 여러 번 사용한다면 따로 메서드를 생성하는 것이 좋습니다. 람다나 콜백 메서드에 대해서는 다음 절에서 설명하기로 하고 일단 넘어가겠습니다.
그 아래 행에 추가한 setEnabled() 메서드는 버튼을 누를 수 있게 할지 없게 할지를 설정하는 메서드입니다. 버튼은 기본값으로 누를 수 있지만 이 메서드에 false를 넣으면 누를 수 없게 되고, 따라서 표시되는 이미지도 nomalImage가 아니라 disabledImage로 바뀝니다. 여기에서는 누를 수 없는 버튼 이미지가 표시되는지 확인하기 위해 false를 넣었습니다.
이 코드를 실행해보면 [캐릭터 만들기] 버튼이 title_btn_1_dis.png로 바뀌어 있고, 해당 버튼은 누를 수 없습니다.
Figure 5‑19 사용 불가 이미지 추가 후
이번에는 [게임 하기] 버튼을 아래에 추가해볼 것입니다. 그 전에 앞에서 추가했던 button1->setEnabled(false); 행은 삭제합시다. 기본값이 true이므로 이 행을 지우면 버튼은 누를 수 있는 버튼으로 바뀔 것입니다. 다음과 같이 새로운 버튼 button2의 코드를 추가하고, Menu에도 이 버튼을 추가해줍니다.
----------StartScene.cpp----------
//버튼 추가
auto button1 = MenuItemImage::create("title_btn_1.png", "title_btn_1_on.png", "title_btn_1_dis.png", [&](Ref *sender) {});
button1->setPosition(Point(winSize.width / 2, 100));
auto button2 = MenuItemImage::create("title_btn_2.png", "title_btn_2_on.png");
button2->setPosition(Point(winSize.width / 2, 40));
//생성된 버튼을 메뉴에 추가한다.
auto menu = Menu::create(button1, button2, NULL);
menu->setPosition(Point::ZERO);
this->addChild(menu);
return true;
}
button1 부분에서 setEnabled() 행을 삭제했고, button2를 생성하는 코드를 추가했으며, Menu의 create()에 button2를 추가해주었습니다. 만약 버튼을 생성했다고 해도 Menu에 추가하지 않으면 화면에 보이지 않습니다. 버튼을 생성했는데 화면에 보이지 않는다면 Menu에 추가하지 않았는지 확인해봅시다.
이제 디버거를 실행해볼까요?
Figure 5‑20 버튼 두 개 추가 후
[게임 하기] 버튼이 추가되었고, 두 버튼 모두 눌렀을 때 이미지가 변하는 것을 볼 수 있어야 정상입니다.
지금은 버튼이 생성되었고 눌러지기만 할 뿐이지 눌렀을 때 이벤트가 발생하진 않습니다. button1에는 람다 콜백 메서드를 등록했지만 하는 일이 없으며, button2에는 아무 콜백 메서드도 등록하지 않았죠. 따라서 버튼을 눌러도 아무 일도 일어나지 않는 것입니다.
그럼 이제 이벤트가 발생되게 해보겠습니다. StartScene.h 파일을 열고, button2를 눌렀을 때 실행될 콜백 메서드를 선언하겠습니다.
----------StartScene.h----------
// implement the "static create()" method manually
CREATE_FUNC(StartScene);
void onClickButton2(Ref *object);
};
단순히 생성자 마지막에 한 줄 추가한 것이 전부입니다. 다음으로 StartScene.cpp를 열고 아래와 같이 파일 제일 끝에 콜백 메서드를 정의합니다.
----------StartScene.cpp----------
return true;
}
void StartScene::onClickButton2(Ref *object) {
log("onClickButton2");
}
이것이 콜백 메서드를 선언하고 정의하는 일반적 방법입니다. 이 콜백 메서드가 하는 일은 단순히 로그를 출력하는 것뿐입니다(log()는 콘솔의 출력창에 메시지를 출력하는 간단한 메서드입니다).
그럼 이 콜백 메서드를 button2에 등록하려면 어떻게 할까요? 앞 절에서 이미 살펴봤습니다. 바로 MenuItemImage를 생성할 때 추가 매개변수로 넣으면 됩니다. 이때 알아야 할 문법을 잠깐 살펴보겠습니다.
cocos2d-x가 3.0으로 버전업되면서 메서드 부분에도 많은 변화가 생겼습니다. 다음과 같이 네 가지의 콜백 메서드 등록하는 방법이 있습니다.
// in v2.1
CCMenuItemImage *item = CCMenuItemImage::create("normal.png", this, menu_selector(MyClass::callback));
// in v3.0 (short version)
auto item = MenuItemImage::create("normal.png", CC_CALLBACK_1(MyClass::callback, this));
// in v3.0 (long version)
auto item = MenuItemImage::create("normal.png", std::bind(&MyClass::callback, this, std::placeholders::_1));
// in v3.0 you can use lambdas or any other "Function" object
auto item = MenuItemImage::create("normal.png",
[&](Ref *sender) {
// do something. Item "sender" clicked
});
① 방식은 menu_selector()를 이용한 방법입니다. 2.x 버전에서 사용하던 방식이므로 몰라도 괜찮습니다. 나머지는 3.0 버전에서 사용할 수 있는 방식인데, 이 중 ③ long 버전은 따로 알 필요가 없습니다. ② short 버전과 동일하기 때문입니다. 따라서 short 버전과 ④ 람다에 대해서만 알아보도록 하겠습니다.
// in v3.0 (short version)
auto item = MenuItemImage::create("normal.png", CC_CALLBACK_1(MyClass::callback, this));
short 버전의 콜백 메서드 등록부는 CC_CALLBACK_1(selector, target)으로 이루어집니다. CC_CALLBACK_1(__selector__, __target__)는 std::bind(&__selector__, __target__)와 동일한 의미입니다. selector는 호출될 메서드를 뜻하고 target은 위치를 뜻합니다. target에 this라고 입력하면 자기 자신을 뜻합니다.
// in v3.0 you can use lambdas or any other "Function" object
auto item = MenuItemImage::create("normal.png",
[&](Ref *sender) {
// do something. Item "sender" clicked
});
람다는 미리 정의한 콜백 메서드를 등록하는 게 아니라 콜백을 즉석에서 입력해버리는 방식입니다. 예제의 경우, 버튼, 즉 sender가 클릭되면 { // do something… } 에 정의된 코드를 실행하라는 의미입니다.
두 가지 방법은 사용 환경에 따라 어느 한 쪽이 편리할 수 있습니다. selector를 사용하는 방식은 동일한 메서드를 서로 다른 객체에서 여러 번 콜백 메서드로 등록할 경우 편리합니다. 람다 방식의 경우, 다른 곳에서 사용되지 않을 간단한 동작을 하는 메서드를 선언 없이 사용할 수 있으므로 코드 작성이 편리해질 수 있습니다. 상황에 맞추어 두 가지 방식 모두 편리하게 사용하면 됩니다.
3.0의 콜백 메서드 등록 방법을 보면 selectImage나 disabledImage 매개변수를 지정하지 않고 바로 콜백 메서드를 매개변수로 적은 것을 볼 수 있습니다. 콜백 메서드 등록을 위해 필요한 매개변수는 normalImage 하나뿐이며, 나머지는 선택적입니다.
앞에서 button1의 람다 내부에는 아무것도 넣지 않았습니다. button1의 람다에도 로그 메서드를 추가하고, button2에는 방금 정의한 콜백 메서드를 등록해보겠습니다.
----------StartScene.cpp----------
//버튼 추가
auto button1 = MenuItemImage::create("title_btn_1.png", "title_btn_1_on.png", "title_btn_1_dis.png", [&](Ref *sender) { log("onClickButton1"); });
button1->setPosition(Point(winSize.width / 2, 100));
auto button2 = MenuItemImage::create("title_btn_2.png", "title_btn_2_on.png", CC_CALLBACK_1(StartScene::onClickButton2, this));
button2->setPosition(Point(winSize.width / 2, 40));
button1의 람다에는 로그 메서드를 추가했고, button2에는 short 버전의 방식으로 콜백 메서드를 등록하였습니다. button2는 별도의 disabledImage를 지정하지 않았습니다. 이제 디버거를 실행한 다음 각 버튼을 눌러보고, 로그가 출력되는지 확인해봅시다. 로그는 출력창에서 볼 수 있습니다.
Figure 5‑21 로그 확인
[5.3.1]을 참고하여 CharacterScene이라는 이름의 클래스를 만듭니다.
Figure 5‑22 CharacterScene 클래스를 추가한 모습
CharacterScene.h 파일과 CharacterScene.cpp 파일을 생성하였으면 StartScene.h 파일과 StartScene.cpp 파일의 내용을 붙여넣고, Cocos2d-x에서 사용할 수 있도록 빈 Scene을 만듭니다.
먼저 헤더 파일을 보면, 다음과 같이 클래스명을 두 군데 수정하고, onClickButton2() 선언은 삭제합니다.
----------CharacterScene.h----------
#include "cocos2d.h"
class CharacterScene: public cocos2d::Layer
{
public:
// there's no 'id' in cpp, so we recommend returning the class instance pointer
static cocos2d::Scene* createScene();
// Here's a difference. Method 'init' in cocos2d-x returns bool, instead of returning 'id' in cocos2d-iphone
virtual bool init();
// a selector callback
void menuCloseCallback(cocos2d::Ref* pSender);
// implement the "static create()" method manually
CREATE_FUNC(CharacterScene);
// 삭제
};
cpp 파일 역시 다음과 같이 클래스명을 수정하고, 사용하지 않는 코드 및 콜백 메서드 onClickButton2()는 삭제합니다.
----------CharacterScene.cpp----------
#include "CharacterScene.h"
USING_NS_CC;
Scene* CharacterScene::createScene()
{
// 'scene' is an autorelease object
auto scene = Scene::create();
// 'layer' is an autorelease object
auto layer = CharacterScene::create();
// add layer as a child to scene
scene->addChild(layer);
// return the scene
return scene;
}
// on "init" you need to initialize your instance
bool CharacterScene::init()
{
//////////////////////////////
// 1. super init first
if (!Layer::init())
{
return false;
}
// 추후 코드 넣을 곳
return true;
}
// 삭제
이와 같이 두 파일을 수정했으면 StartScene에서 [캐릭터 만들기] 버튼을 눌렀을 때 CharacterScene을 호출하도록 추가해야 합니다. StartScene.cpp 파일을 아래와 같이 수정합니다.
----------StartScene.cpp----------
#include "StartScene.h"
#include "CharacterScene.h"
…중략…
//버튼 추가
auto button1 = MenuItemImage::create("title_btn_1.png", "title_btn_1_on.png", "title_btn_1_dis.png", [&](Ref *sender) {
log("onClickButton1");
auto Scene = CharacterScene::createScene();
Director::getInstance()->pushScene(Scene);
});
클레스를 호출하기 위해서는 먼저 그 헤더 파일을 #include해야 합니다(①). 그리고 button1의 람다 부분에 CharacterScene을 호출하는 코드를 추가했습니다(②).
여기서 pushScene()은 해당 Scene을 추가하는 메서드입니다. replaceScene()을 사용할 수도 있습니다. pushScene()은 기존의 Scene 위에 Scene을 추가하는 방식이고, replaceScene()은 기존 Scene을 없애고 Scene을 추가하는 방식이라는 점이 다릅니다.
이제 디버거를 실행하고, [캐릭터 만들기] 버튼을 누르면 검정 화면으로 이동할 것입니다. 우리의 예상대로, 이 아무것도 없는 검정 화면이 바로 CharacterScene입니다.
->
Figure 5‑23 캐릭터 만들기 화면으로 이동
화면이 그냥 전환되는 것보다 화면 전환에 효과를 추가하는 것이 좀 더 화려하게 보일 것입니다.
화면 전환에 애니메이션을 추가해보겠습니다. CharacterScene을 호출하는 곳, 즉 button1의 람다 부분에서 auto Scene = CharacterScene::createScene();를 아래와 같이 수정합니다.
auto Scene = TransitionCrossFade::create(0.5f, CharacterScene::createScene());
TransitionCrossFade 클래스를 사용하여 크로스페이딩 애니메이션을 추가하였습니다. TransitionCrossFade::create()의 첫 번째 매개변수는 시간(초)이고, 두 번째 매개변수는 호출할 메서드입니다.
디버거를 실행하고 [캐릭터 만들기] 버튼을 눌러보면 화면 전환에 애니메이션 효과가 추가된 것을 볼 수 있습니다.
Figure 5‑24 애니메이션 추가 화면
크로스페이딩 애니메이션을 위한 TransitionCrossFade 클래스 외에도 TransitionFlipX, TransitionFlipY, TransitionJumpZoom 등 여러 가지 애니메이션 클래스가 있습니다. 앞에서 TransitionCrossFade 클래스를 사용한 곳에 다른 클래스를 넣으면 눈으로 확인할 수 있습니다.
auto Scene = TransitionFlipX::create(0.5f, CharacterScene::createScene());
auto Scene = TransitionFlipY::create(0.5f, CharacterScene::createScene());
auto Scene = TransitionFlipZumpZoom::create(0.5f, CharacterScene::createScene());
예를 들어 이와 같은 세 가지 애니메이션을 적용해보면 다음과 같은 화면 전환 효과를 확인할 수 있을 것입니다.
Figure 5‑25 화면 전환 애니메이션 예시(TransitionFlipX, TransitionFlipY, TransitionJumpZoom)
다른 애니메이션을 추가하고 싶다면 레퍼런스 문서를 찾아보는 것이 편리하고 정확합니다. 레퍼런스 문서는 Cocos2d-x 사이트(http://www.cocos2d-x.org/)에서 버전별로 찾아볼 수 있습니다. 이 책에서 다루는 3.4 버전의 경우 레퍼런스 메인 페이지 주소는 http://www.cocos2d-x.org/reference/native-cpp/V3.4/index.html입니다. 여기에서 우측 상단의 검색창에 ‘transition’을 입력하면 관련 클래스들을 모두 볼 수 있습니다(검색 결과 페이지 http://bit.ly/1QNpi1a).
타이틀 화면의 UI 구현이 끝났습니다. 이제 마지막으로 타이틀 화면을 좀 더 시각적으로 돋보이게 하는 애니메이션 효과를 추가해보겠습니다.
StartScene.cpp 파일에서 init() 메서드를 아래와 같이 수정합니다.
----------StartScene.cpp----------
//생성된 버튼을 메뉴에 추가한다.
auto menu = Menu::create(button1, button2, NULL);
menu->setPosition(Point::ZERO);
this->addChild(menu);
//타이틀 스프라이트에 애니메이션 추가
auto to = title->getPosition();
auto from = Point(to.x, to.y + 100);
//title 스프라이트의 시작 위치를 지정
title->setPosition(from);
//MoveTo로 지정된 노드를 해당 위치로 움직이는 액션을 만든다.
auto action = MoveTo::create(0.5f, to);
//title 스프라이트에 액션 지정
title->runAction(action);
return true;
}
먼저 title의 위치를 가져와서 to라는 포인트에 추가해주고, from은 애니메이션이 시작할 위치, 즉 100픽셀 위로 지정했습니다(①).
그다음 액션을 담을 변수 action을 생성했는데, create()의 매개변수로는 시간과 이동할 위치를 지정해줍니다(②). 액션이란 쉽게 말해 에니메이션이라고 생각하면 됩니다. 바로 이어서 자세히 설명하도록 하겠습니다. 바로 앞에서 살펴봤던 Transition 계열 클래스처럼, 대부분의 애니메이션은 생성 시 첫 번째 매개변수로 시간을 받습니다.
이제 디버거를 실행해보면 타이틀이 상단에서부터 아래로 내려오는 애니메이션이 추가되었습니다(금방 지나가므로 실행 직후 잘 지켜봐야 합니다).
Figure 5‑26 타이틀에 애니메이션 적용
액션에 관련된 클래스인 Action에는 여러 가지 애니메이션이 있습니다. 기본적인 이동, 회전, 크기 변경 등의 애니메이션이 있으며 이러한 애니메이션을 여러 개 섞어 사용할 수도 있습니다.
여러 애니메이션을 사용할 때 사용되는 클래스 중에는 Spawn과 Sequence가 있습니다. Spawn은 애니메이션을 동시에 실행하며, Sequence는 순차적으로 실행한다는 차이가 있습니다. 간단히 살펴보겠습니다. 해당 부분을 이렇게 고쳐봅시다.
//MoveTo로 지정된 노드를 해당 위치로 움직이는 액션을 만든다.
auto action = Sequence::create(MoveTo::create(0.5f, to), RotateTo::create(0.5f, 720), NULL);
앞에서 언급한 적이 있지만, 매개변수를 여러 개 받을 수 있는 메서드는 마지막에 NULL을 추가하여 끝을 알려주어야 합니다.
그럼 디버거를 실행하여 확인해볼까요? MoveTo 애니메이션이 끝난 다음에 RotateTo(회전) 애니메이션이 실행됩니다.
Figure 5‑27 복합 애니메이션 적용
만약 Sequence 클래스 대신 Spawn 클래스를 사용하면 MoveTo 애니메이션과 RotateTo 애니메이션이 동시에 적용되는 것을 확인할 수 있을 것입니다. 두 개뿐만 아니라 세 개, 네 개, 그 이상의 애니메이션을 추가하여 사용할 수도 있습니다.
여기서 회전 애니메이션이 동작하는 것을 잘 살펴보면 앵커 포인트로 지정한 곳을 기준으로 회전하는 것을 볼 수 있습니다. 우리는 앞에서(5.3.5) title의 앵커 포인트를 Point(0.5f, 1), 즉 가로 중간, 세로 상단으로 지정했습니다. 다음과 같이 title의 앵커 포인트를 정중앙으로 수정하면 어떻게 될까요? 앵커 포인트가 아래로 내려옴에 따라 타이틀의 위치 역시 아래로 더 내렸습니다.
//앵커 포인트를 Point(0.5f, 0.5f)로 변경
title->setAnchorPoint(Point(0.5f, 0.5f));
//title을 해당 포인트에 위치시킵니다. 화면의 가로 중앙에 위치하도록 했습니다.
title->setPosition(Point(winSize.width / 2, winSize.height - 70));
디버거를 실행해보면, 회전 애니메이션의 기준점이 바뀐 것을 눈으로 잘 확인할 수 있습니다.
Figure 5‑28 기준점 변경 후 회전 애니메이션