본문 바로가기

cocos2d-x/pyocopang

cocos2d-x 포코팡 류 게임만들기!! - 3 - 액션 Action!!

지난 번에 CCSprite 로 이미지를 화면에 추가하는 방법을 알아봤습니다. 배경화면, 나무, 적 캐릭터를 화면에 추가해봤는데요.

이번에는 나무와 적 캐릭터가 너무 심심하게 가만히 있는 것 같아서 좌우로 움직이도록 만들어 보겠습니다.

- 3 - 액션 Action!!

Action은 CCAction Class를 이용합니다. CCAction은 여러가지 움직임을 조합해서 다양한 동작을 만드는 것이 가능합니다. 예를 들어, "왼쪽으로 10미터 갔다가" > "한 바퀴 회전하고" > "크기를 10%로 줄였다가" > "오른쪽으로 20미터 갔다가" 이런 식으로 움직임을 만들 수 있다는 것이지요. 

이렇게 복잡하게는 아니고, 저는 나무를 좌우로만 움직이게 해보려고 합니다. "왼쪽으로 10도" > "오른쪽으로 10도" 이 동작을 무한히 반복되게 하면, 계속 흔들흔들 움직이는 것처럼 보일 것 같습니다.

1. Action 추가

createGameAction() 이란 함수를 하나 만들어서 이 안에서 게임에 필요한 모든 액션을 생성하도록 하겠습니다. header와 cpp 파일에 다음과 같이 추가합니다. 액션을 생성하고, 이 생성된 액션을 CCSprite->runAction()으로 추가하여 동작시켜야하기 때문에, 지난 번에 만든 createGameScene() 전에 초기화가 될 수 있도록 createGameAction()을 위치시킵니다. 그리고, 나무의 액션과 적 캐릭터의 액션을 설정할 CCAction 변수를 추가합니다. 마지막으로, 클래스의 소멸자도 추가해줍니다.

Classes/HelloWorldScene.h

class HelloWorld : public cocos2d::CCLayer                                               
{                                                                                        
private:
    CCSize _screenSize;                                                                  
                                                                                         
    CCAction* _enemyMove;
    CCAction* _treeMove;                                                                 
    
public:                                                                                  
    // Here's a difference. Method 'init' in cocos2d-x returns bool, instead of returning 'id' in cocos2d-iphone
    virtual bool init();   
    
    // there's no 'id' in cpp, so we recommend returning the class instance pointer      
    static cocos2d::CCScene* scene();                                                    
    
    // implement the "static node()" method manually
    CREATE_FUNC(HelloWorld);
    
    ~HelloWorld(void);
    
    void createGameScene(void);                                                          
    void createGameAction(void);
}; 


Classes/HelloWorldScene.cpp

HelloWorld::~HelloWorld()
{
    CC_SAFE_RELEASE(_enemyMove);
}
bool HelloWorld::init()
{
    //////////////////////////////
    // 1. super init first
    if ( !CCLayer::init() )
    {
        return false;
    }

    createGameAction();
    createGameScene();

    return true;
}

cocos2d-x 의 액션은 여기서 모두 열거하기 어려울만큼 다양하게 제공되고 있습니다. 공식홈페이지의 wiki 문서에 간단한 예제와 기본액션들이 소개되고 있는데, 이 문서에도 간단히 언급되고 있는 액션 중 CCEaseInOut과 CCRotateTo를 사용해보도록 하겠습니다.

http://www.cocos2d-x.org/wiki/Actions

void HelloWorld::createGameAction()
{
    CCFiniteTimeAction* enemySwing = CCSequence::create(
            CCEaseInOut::create(CCRotateTo::create(1.2f, -10), 2),
            CCEaseInOut::create(CCRotateTo::create(1.2f, 10), 2), NULL);

    _enemyMove = CCRepeatForever::create((CCActionInterval*)enemySwing);
    _enemyMove->retain();
}

createGameAction() 내에 좌우로 흔들거리는 액션을 추가해보았습니다. cpp 또는 class를 사용하는 객체지향언어에 익숙하지 않으신 분들은 이해가 어려울 수도 있을 것 같습니다. 분명히 _enemyMove가 CCAction으로 선언되었는데, CCFiniteTimeAction 이란 class로 만든 변수에 CCSequence로 생성된 변수를 대입하고, 그걸 또 CCActionInterval*로 형변환해서 CCRepeatForever create에 사용되고 이걸 또 _enemyMove에 넣고... 이게 도대체 뭐하는 짓인지 알 방법이 없지요.

물론 한 번에 다 이해할 필요는 없습니다. 처음부터 이해하는게 오히려 더 이상하지요. 어떤 언어라도 상관없습니다. 상속의 개념을 이해하고, reference api문서에서 제공되는 CCAction class와 다른 class의 관계를 살펴보면 차차 이해가 될 것이라고 생각됩니다.

http://www.cocos2d-x.org/reference/native-cpp/V2.2.2/d7/d12/classcocos2d_1_1_c_c_action.html

class 이름을 통해 충분히 코드 내용과 동작을 유추할 수는 있습니다. 괄호들을 파고 들어가서 가장 안쪽에 있는 CCRotateTo 부터 살펴보겠습니다.

1) CCRotateTo::create(1.2f, -10) : 1.2 초 동안 -10도 만큼 회전합니다.

2) CCEaseInOut::create(CCRotateTo::..., 2) : 천천히 움직이기 시작해서 점점 빠르게, 또는 그 반대로 동작하게 해줍니다. 입력 인자인 2값을 바꿔가면서 테스트 해보세요.

3) CCSequence::create(CCEaseInOut::create(...), CCEaseInOut::create(...), NULL) : 여러 동작을 연결해줍니다. 첫번째 동작부터 순서대로 실행합니다. 즉, "1.2 초 동안 -10도 위치까지 회전" 그리고 "1.2 초 동안 10도 위치까지 회전" 그리고 "NULL, 끝!!" 입니다.

4) CCRepeatForever::create(CCSequence::create(...)) : 3)의 동작을 무한히 반복

이렇게 만들어진 _enemyMove를 enemy sprite에 action으로 추가하면 원하는 동작으로 움직이는 것을 확인할 수 있습니다. 그전에, _enemyMove->retain() 이라는 코드가 보입니다. C++ 에서는 new, delete로 메모리 관리를 사용자가 할 수 있는데, cocos2d-x에서는 reference count 방식을 사용한 retain, release, autorelease 등의 메모리 관리 기법을 별도로 제공합니다. retain과 release 쌍을 잘 맞춰서 카운트를 해줘야하는데요. 소멸자에서 CC_SAFE_RELEASE() 매크로를 사용함으로써 간단하고 안전하게 메모리 해제를 할 수 있습니다. 소멸자 함수에 CC_SAFE_RELEASE(_enemyMove) 코드를 추가한 것도 그 이유입니다.

그럼 이제 enemy sprite에 action을 추가해보겠습니다.

    // enemy image
    CCSprite* enemy = CCSprite::create("enemy_01.png");
    enemy->setPosition(ccp(_screenSize.width * 0.7f, _screenSize.height * 0.8f));
    enemy->runAction(_enemyMove);
    this->addChild(enemy);
}

CCSprite->runAction() 을 이용해서 간단하게 액션추가를 할 수 있습니다. 그럼 나무의 action도 추가해보겠습니다. 동작은 enemy와 동일하기 때문에 변수 값만 조금씩 바꿔서 어떻게 달라지는지 확인해볼 수 있을 것 같습니다.

void HelloWorld::createGameAction()
{
    CCFiniteTimeAction* enemySwing = CCSequence::create(
            CCEaseInOut::create(CCRotateTo::create(1.2f, -10), 2),
            CCEaseInOut::create(CCRotateTo::create(1.2f, 10), 2), NULL);

    _enemyMove = CCRepeatForever::create((CCActionInterval*)enemySwing);
    _enemyMove->retain();

    CCFiniteTimeAction* treeSwing = CCSequence::create(
            CCEaseInOut::create(CCRotateTo::create(2.0f, -5), 2),
            CCEaseInOut::create(CCRotateTo::create(2.0f, 5), 2), NULL);

    _treeMove = CCRepeatForever::create((CCActionInterval*)treeSwing);
    _treeMove->retain();
}

enemy 때와 동일하게 코드를 작성하고, 움직이는 시간과 각도값만 변경해보았습니다. _treeMove->retain() 을 추가했으니 소멸자 함수에 메모리 해제 코드도 추가해야겠습니다. enemy와 달리 tree는 동일한 액션을 다른 sprite에 적용해야 하기 때문에 Action을 복사해서 사용합니다. copy()를 통해 복사를 하고, autorelease()로 앞서 말씀드린 메모리관리를 위한 카운터 값을 자동으로 설정합니다.

void HelloWorld::createGameScene()
{
    _screenSize = CCDirector::sharedDirector()->getWinSize();

    // back ground image
    CCSprite* background = CCSprite::create("game_back.png");
    background->setPosition(ccp(_screenSize.width * 0.5f, _screenSize.height * 0.5f));
    this->addChild(background);

    // tree image
    CCSprite* tree = CCSprite::create("tree_01.png");
    tree->setPosition(ccp(_screenSize.width * 0.2f, _screenSize.height * 0.8f));
    tree->runAction((CCAction*)_treeMove->copy()->autorelease());
    this->addChild(tree);

    tree = CCSprite::create("tree_02.png");
    tree->setPosition(ccp(_screenSize.width * 0.35f, _screenSize.height * 0.8f));
    tree->runAction((CCAction*)_treeMove->copy()->autorelease());
    this->addChild(tree);

    tree = CCSprite::create("tree_03.png");
    tree->setPosition(ccp(_screenSize.width * 0.8f, _screenSize.height * 0.8f));
    tree->runAction((CCAction*)_treeMove->copy()->autorelease());
    this->addChild(tree);

    // enemy image
    CCSprite* enemy = CCSprite::create("enemy_01.png");
    enemy->setPosition(ccp(_screenSize.width * 0.7f, _screenSize.height * 0.8f));
    enemy->runAction(_enemyMove);
    this->addChild(enemy);
}
HelloWorld::~HelloWorld()
{
    CC_SAFE_RELEASE(_enemyMove);
    CC_SAFE_RELEASE(_treeMove);
}

빌드해서 동작을 확인해보겠습니다. 잘 움직시나요? 저는 잘 움직이는데요. 원하는 움직임은 아닙니다. 오뚜기처럼 아래를 중심축으로 해서 흔들흔들거리는 동작은 원했는데. 가운데를 중심으로 돌아가는 모양이네요. 마음에 들지 않습니다. 중심축을 변경해서 원하는 움직임을 만들어봐야겠습니다.

2. 회전 중심 변경. Anchor Point

책과 검색을 통해 Anchor Point를 변경하면 된다는 사실을 알았습니다. CCSprite->setAnchorPoint() 라는 함수가 있는데 레퍼런스 가이드 문서를 보니 이미지의 왼쪽 하단을 (0, 0), 오른쪽 상단을 (1, 1) 기준으로 변경할 수 있다고 되어 있습니다. 저는 중앙 하단을 중심축으로 하고 싶으니 값을 (0.5, 0)으로 설정하면 될 것 같습니다. 코드에 Anchor Point 설정 부분을 추가해보겠습니다.

void HelloWorld::createGameScene()
{
    _screenSize = CCDirector::sharedDirector()->getWinSize();

    // back ground image
    CCSprite* background = CCSprite::create("game_back.png");
    background->setPosition(ccp(_screenSize.width * 0.5f, _screenSize.height * 0.5f));
    this->addChild(background);

    // tree image
    CCSprite* tree = CCSprite::create("tree_01.png");
    tree->setPosition(ccp(_screenSize.width * 0.2f, _screenSize.height * 0.8f));
    tree->setAnchorPoint(ccp(0.5f, 0));
    tree->runAction((CCAction*)_treeMove->copy()->autorelease());
    this->addChild(tree);

    tree = CCSprite::create("tree_02.png");
    tree->setPosition(ccp(_screenSize.width * 0.35f, _screenSize.height * 0.8f));
    tree->setAnchorPoint(ccp(0.5f, 0));
    tree->runAction((CCAction*)_treeMove->copy()->autorelease());
    this->addChild(tree);

    tree = CCSprite::create("tree_03.png");
    tree->setPosition(ccp(_screenSize.width * 0.8f, _screenSize.height * 0.8f));
    tree->setAnchorPoint(ccp(0.5f, 0));
    tree->runAction((CCAction*)_treeMove->copy()->autorelease());
    this->addChild(tree);

    // enemy image
    CCSprite* enemy = CCSprite::create("enemy_01.png");
    enemy->setPosition(ccp(_screenSize.width * 0.7f, _screenSize.height * 0.8f));
    enemy->setAnchorPoint(ccp(0.5f, 0));
    enemy->runAction(_enemyMove);
    this->addChild(enemy);
}

빌드해서 동작을 보니 오~! 원하는대로 잘 움직입니다. 그런데 좀 이상한 점이 있네요. 위치가 원래보다 위로 올라갔네요. 뭔가 놓친 부분이 있는 것 같습니다. 다시 검색... 해도 잘 안나오네요. 하지만 값을 변경시키면서 동작을 이해할 수 있었습니다. 결론적으로, 중심점 위치는 이미지의 한 가운데로 정해져 있고, Anchor Point를 변경하면 그 값에 맞게 이미지 위치를 변경하는 것이었습니다. 설명이 어려워서 그림으로 살펴보면,

이렇게 되는 것이었습니다. 중심축을 설정했으니 각 이미지들을 원하는 위치로 다시 변경하겠습니다. setPosition()에서 0.8f 를 0.75f 정도로 저는 바꿨습니다. 마지막으로 빌드해서 동작을 확인해보니 어느 정도 원하는 대로 움직입니다! 마음에 드네요.

다음 번에는 실제 게임에 필요한 coin들을 화면에 배치해보겠습니다.

.... 글 끝!