[cocos2d-x] 간단한 게임 만들기 (4)

     



게임이 거의 다 만들어진줄 알았는데 생각보다 아직 할게 많이 남았더군요. 이번이 마지막 포스팅일 줄 알았는데 ...!!


일단 이번 포스팅의 시작은 시작화면 만들기, 시작 준비 화면, 탭 화면 입니다.


먼저 메인화면을 만들기 위해 코코스 스튜디오로 가서 MainScene.csd에 title.png 파일을 드래그합니다.




앵커 포인트와 위치를 설정해주시면 됩니다. 위치가 위로 가 있는 이유는 아마 위에서부터 내려오는 애니메이션을 넣기 위해서겠죠? 개발할때는 화면 밖에 있는 객체도 보이기 때문에 편하게 작업을 할 수 있습니다.


이제 애니메이션을 설정합니다. 

애니메이션은 노드탐색창 위에 + 모양의 버튼을 누르면 만들 수 있습니다.



이름은 title, frame은 1부터 55로 설정합니다. frame은 장면수를 나타내는 것으로 보통 1초에 60프레임이기 때문에 이 애니메이션은 1초 내에 끝나는 애니메이션이겠네요.




완성하면 타임라인에 줄이 생기면서 에니메이션을 만들 수 있습니다. 해당 프레임 위치에서 객체들의 상태를 지정하고 상태를 저장하면 에니메이션이 자동으로 만들어집니다. 예를들어 50프레임 위치에 가서 title 노드의 위치를 바꿔봅시다.




50프레임을 클릭하면 파란색 화살표가 이동됩니다. 여기서 title스프라이트의 위치를 50%, 80%로 변경합니다. 이렇게 변경하기만 하면 적용되는 것이 아니라 왼쪽 아래 동그라미에 +가 그려진 버튼을 눌려야 상태가 적용됩니다. 1프레임에서도 50% 140% 위치를 저장해야 타임라인에 저장됩니다. 상태가 저장되면 타임라인에 검은색 막대가 생깁니다. 

이제 재생을 누르면 title의 위치가 위에서 아래로 내려가는 것을 알 수 있습니다. 에니메이션 만들기 성공!




같은 방식으로 새로운 애니메이션 ready를 만듭니다. 여기에는 tap_left와 tap_right 를 넣고 61프레임에서는 초기화 (각각 (10%, 8.75% ), (90%, 8.75%) 위치) 90프레임에서는 바깥쪽 방향으로 이동 ( 0%, 8.75%), (100%, 8.75%) 120프레임에서 다시 초기화. 애니메이션을 시작했을때 화살표가 바깥쪽으로 이동했다가 다시 원위치로 이동할 수 있게 해줍니다.


애니메이션 두 개를 만들었으면 코드에서 어떻게 애니메이션을 사용하는지 보러 갑시다!


일단 기존에 GameState가 Playing, GameOver 두가지었는데, Title과 Ready화면을 추가합니다.


1
2
3
4
5
6
7
8
9
 
enum class GameState
{
    Title,
    Ready,
    Playing,
    GameOver
};
 

cs


이제 게임의 상태가 총 4개가 되었습니다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 
 
void MainScene::triggerTitle()
{
    //게임 상태를 타이틀로 변경
    this->gameState = GameState::Title;
    
    //코코스 스튜디오에서 만든 타임라인들을 읽어옴
    cocostudio::timeline::ActionTimeline* titleTimeline = CSLoader::createTimeline("MainScene.csb");
    
    //현재 모든 애니메이션은 멈춤
    this->stopAllActions();
    
    //타임라인을 불러옴
    this->runAction(titleTimeline);
    
    //타임라인에서 설정한 애니메이션을 실행. play("이름", 반복여부)
    titleTimeline->play("title"false);
}
 
cs


위와같이 메인에 Title 셋팅 트리거를 추가합니다. 이제 onEnter()에 triggerTitle메소드를 추가하면~! 게임이 시작되면서 타이틀이 아래로 내려오는 애니메이션이 실행됩니다.


Ready도 똑같은 형태로 만들어줍니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 
 
void MainScene::triggerReady()
{
    this->gameState = GameState::Ready;
 
    //코코스 스튜디오에서 만든 타임라인들을 읽어옴
    cocostudio::timeline::ActionTimeline* readyTimeline = CSLoader::createTimeline("MainScene.csb");
    
    //현재 모든 애니메이션은 멈춤
    this->stopAllActions();
    
    //타임라인을 불러옴
    this->runAction(readyTimeline);
    
    //타임라인에서 설정한 애니메이션을 실행. play("이름", 반복여부)
    readyTimeline->play("ready"true);
    
}
 
cs


트리거를 넣는 부분은 TouchHandle부분입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
 
 
void MainScene::setupTouchHandling()
{
    auto touchListener = EventListenerTouchOneByOne::create();
    
    touchListener->onTouchBegan = [&](Touch* touch, Event* event)
    {
        Vec2 touchLocation = this->convertTouchToNodeSpace(touch);
        
        //게임상태 체크.
        switch (this->gameState) {
                
            case GameState::Title:
                this->triggerReady();
                break;
                
            case GameState::Ready:
                this->triggerPlaying();
        
            case GameState::Playing:
            
                if(touchLocation.x < this->getContentSize().width / 2.0f)
                {
                    this->character->setSide(Side::Left);
                }
                else
                {
                    this->character->setSide(Side::Right);
                }
                
                this->stepSushi();
                
                if(this->isGameOver())
                {
                    this->triggerGameOver();
                    return true;
                }
                
                break;
                
            case GameState::GameOver:
                break;
        }
        return true;
    };
    
    this->getEventDispatcher()->addEventListenerWithSceneGraphPriority(touchListener, this);
}
 
 
 
 
cs


게임 상태가 Title인 상태에서 터치를 하면 Ready가 실행되고 Ready상태라면 Play가 실행됩니다. Ready상태에서 break;가 없는 이유는 터치와 동시에 게임이 Play되게 하기 위함입니다.


이상태로 실행을 했더니 타이틀이 사라지고 게임 라벨과 라이프가 보이지 않더군요.. 아무레도 Ready 애니메이션에서 그 두가지 스프라이트를 보이도록 해야할 것 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 
 
void MainScene::triggerPlaying()
{
    this->gameState = GameState::Playing;
    
    auto scene = this->getChildByName("Scene");
    
    cocos2d::Sprite* tapLeft = scene->getChildByName<cocos2d::Sprite*>("tap_left");
    cocos2d::Sprite* tapRight = scene->getChildByName<cocos2d::Sprite*>("tap_right");
    
    cocos2d::FadeOut* leftFade = cocos2d::FadeOut::create(0.35f);
    cocos2d::FadeOut* rightFade = cocos2d::FadeOut::create(0.35f);
    
    tapLeft->runAction(leftFade);
    tapRight->runAction(rightFade);
 
}
 
cs


또 Play상태에서 탭 스프라이트가 서서히 사라지도록 설정해줍니다. 여기서 각각의 액션을 설정하고 runAction메소드를 실행했는데, 같은 기능이라고 해서 액션을 하나만 만들어 각각 지정해 줄경우 하나의 runAction만 실행되더군요. 아무래도 액션을 동시에 여러군데에서 호출할 수 없는가 봅니다.


자 이제 뭔가 게임처럼 보이는군요.. 인터페이스도 있고..

하지만 타임게이지와 스코어는 아직도 그대로입니다. 터치했을 때 게임오버가 아니라면 타임을 살짝 증가시키고, 스코어도 1 증가시키도록 만듭시다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
 
 
void MainScene::setupTouchHandling()
{
    auto touchListener = EventListenerTouchOneByOne::create();
    
    touchListener->onTouchBegan = [&](Touch* touch, Event* event)
    {
        Vec2 touchLocation = this->convertTouchToNodeSpace(touch);
        
        //게임상태 체크.
        switch (this->gameState) {
                
            case GameState::Title:
                this->triggerReady();
                break;
                
            case GameState::Ready:
                this->triggerPlaying();
        
            case GameState::Playing:
            
                if(touchLocation.x < this->getContentSize().width / 2.0f)
                {
                    this->character->setSide(Side::Left);
                }
                else
                {
                    this->character->setSide(Side::Right);
                }
                
                this->stepSushi();
                
                if(this->isGameOver())
                {
                    this->triggerGameOver();
                    return true;
                }
                //냥이 회피 성공시 라이프 약간 증가. 스코어 1 증가.
                this->setTimeLeft(this->timeLeft + 0.3f);
                this->setScore(this->score +1);
                break;
                
            case GameState::GameOver:
                this->resetGameState();
                this->triggerReady();
                break;
        }
        return true;
    };
 
 
cs


터치 핸들링을 위와같이만 바꿔주면 됩니다.



이제 냥이와 스시가 날아가는 애니메이션을 만들어 봅시다.

먼저 스시가 날아가는 애니메이션은 타이틀 애니메이션과 비슷합니다. 왼쪽으로 날아가는 것과 오른쪽으로 날아가는 것을 구분해서 애니메이션을 만들어 줍니다.




디폴트에서는 roll의 위치를 초기화. moveRight는 1프레임에서 초기화 48프레임에서는 800px, 200px의 위치로 해주고 로테이션을 120값을 줍니다. 




그럼 roll이 저렇게 돌아갑니다. chopstick들은 roll안에 포함되어 있기 때문에 roll이 움직이거나 돌아가면 같이 움직이거나 돌아갑니다.


moveRight와 Left애니메이션을 만들면 실행했을때 다음과 같이 나옵니다.





냥이의 움직임 애니메이션은 좀 다릅니다. 프레임마다 그림을 바꿔줘야 되거든요.

기본 모습 애니메이션과 chop 애니메이션 두개를 만듭니다.




디폴트에서는 기본위치를 지정해 주시고, chop에서는 프레임별로 냥이의 그림을 바꿔줍니다.




이미지는 냥이를 클릭하고 이미지 리소스에서 오른쪽 클릭, Set Image메뉴를 통해 바꿔줍니다. 이미지를 바꾸고 상태를 저장하는 것 잊지마세요!.


프레임 1에선 기본, 2는 character2.png, 3은 character3.png, 4는 다시 character2.png, 5는 character1.png 로 설정해줍니다. 이제 애니메이션을 시작하면 냥이가 퍼퍼퍽! 손을 휘두릅니다. 너무 빠르다고 생각되면 FPS를 30으로 설정해줍니다.


이제 새로운 애니메이션을 퍼블리싱하고 Xcode로 갑니다.

냥이의 애니메이션을 추가하기 위해 Charater 소스와 헤더를 수정합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
 
 
bool Character::init()
{
    if(! Node::init())
    {
        return false;
    }
    
    this->side = Side::Left;
    
    this->timeline = CSLoader::createTimeline("Character.csb");
    
    this->timeline->retain();
    
    return true;
}
 
void Character::onExit()
{
    this->timeline->release();
    
    Node::onExit();
}
 
void Character::runChopAnimation()
{
    this->stopAllActions();
    
    this->runAction(this->timeline);
    
    this->timeline->play("chop"false);
}
 
 
cs


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
class Character : public cocos2d::Node
{
public:
    CREATE_FUNC(Character);
    
    void setSide(Side side);
    Side getSide();
    
    bool init() override;
    void onExit() override;
    void runChopAnimation();
    
protected:
    Side side;
    cocostudio::timeline::ActionTimeline* timeline;
    
cs


헤더와 소스에 위와같이 추가합니다. 메인에서 타이틀 애니메이션을 적용한 것과 똑같습니다. runChopAnimation 메소드를 실행할 때마다 냥이가 펀치를 하겠죠? 이부분은 메인의 터치핸들 부분에서 호출해줘야 할 것 같군요.


그전에 스시가 날아가는 애니메이션을 넣어봅시다. 실제 타워에 있는 스시에게 애니메이션을 적용하면 어떻게 될까요? 실제 초밥은 탑 아래서 위로 계속 무한 루프를 돌고 있기 때문에 애니메이션을 적용하면 스시의 위치가 엉망이 될 것입니다. 따라서 애니메이션을 적용시킬 가상의 스시를 만들고, 애니메이션을 적용시켜줍니다. 


정리하면 

1. 가상의 스시를 만든다.

2. 위치는 pieceNode.

3. 냥이가 젓가락을 피해서 스시를 날리면 애니메이션 적용.

4. 만든 가상의 스시 릴리즈.


정도로 만들면 되겠군요.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 
void MainScene::onHitSushi(Side sushiSide)
{
    //가상의 스시를 만든다.
    Piece* flyingPiece = dynamic_cast<Piece*>(CSLoader::createNode("Piece.csb"));
    
    //가상의 스시의 사이드를 정한다. (인풋값을 이용해서)
    flyingPiece->setObstacleSide(sushiSide);
    
    //화면에 추가한다. 위치는 pieceNode의 위치를 받아온걸 사용
    flyingPiece->setPosition(this->flyingPiecePosition);
    this->addChild(flyingPiece);
    
    //애니메이션을 불러온다.
    cocostudio::timeline::ActionTimeline* rollTimeline = CSLoader::createTimeline("Piece.csb");
    Side characterSide = this->character->getSide();
    
    //스시위 위치에 따라 애니메이션을 바꿔준다.
    std::string animationName = (characterSide == Side::Left) ? std::string("moveRight") : std::string("moveLeft");
    
    flyingPiece->runAction(rollTimeline);
    
    rollTimeline->play(animationName, false);
    
    //마지막 프레임에서 객체를 릴리즈한다.
    rollTimeline->setLastFrameCallFunc([this, &flyingPiece](){
        this->removeChild(flyingPiece);
    });
    
}
 
cs


메인에 위와같이 onHitSushi를 만듭니다.

물론 이 애니메이션이 실행되는건 터치 핸들러겠죠?


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
            case GameState::Playing:
            
                if(touchLocation.x < this->getContentSize().width / 2.0f)
                {
                    this->character->setSide(Side::Left);
                }
                else
                {
                    this->character->setSide(Side::Right);
                }
                
                //냥이 펀치 애니메이션
                this->character->runChopAnimation();
                
                this->stepSushi();
                
                if(this->isGameOver())
                {
                    this->triggerGameOver();
                    return true;
                }
                //냥이 회피 성공시 라이프 약간 증가. 스코어 1 증가.
                this->setTimeLeft(this->timeLeft + 0.3f);
                this->setScore(this->score +1);
                
                //스시 날리는 애니메이션
                this->onHitSushi(this->pieces.at(this->pieceIndex)->getObstacleSide());
                break;
 
cs


터치 핸들러에서 위와같이 애니메이션 부분을 추가합니다.

이제 게임을 시작하면 터치할때마다 냥이가 펀치를 하면서 스시가 날라갈겁니다. 

하지만 여기서 더 나은 효과를 주기위해 스시타워가 움직이는 애니메이션을 넣어봅시다.


1
2
3
4
    //초밥 빌딩 노드를 아래로 내려서 움직인것처럼 보이게함
    cocos2d::MoveBy* moveAction = cocos2d::MoveBy::create(0.15f, Vec2(0.0f, -1.0f * currentPiece->getSpriteHeight() / 2.0f));
    this->pieceNode->runAction(moveAction);
 
cs



기존에 단지 초밥타워의 위치만 바꿔주던 코드를 애니메이션으로 바꿔봤습니다. 이제 게임을 실행하면 제법? 그럴듯하게 실행됩니다.





이제 마지막으로 게임오버화면을 만들어줍니다.

게임오버화면은 일단 코코스튜디오에서 화면을 구성하고 애니메이션으로 위에서 아래로 내려오도록 만듭니다.

게임오버 스코어라벨을 만들어주고 현재 종료된 시점의 점수를 입력합니다.

또 재시작 버튼을 만들고 게임오버되면 버튼이 보이게 설정합니다.

터치핸들러에서 게임오버시 화면을 터치할 때가 아니라 버튼을 클릭했을때 재시작되도록 수정합니다.


이부분은 각자 알아서.. 응용해서 하시면 좋겠네요 ^^... 

버튼같은 경우는 이번 게임에서 처음 쓰는거기때문에 코드를 참조하시는게 좋을 것 같습니다.





전체 소스는 아래와 같습니다.


네코스시.zip


간단할줄알았지만 복잡했던 게임 만들기 끝!!

반응형

댓글

Designed by JB FACTORY