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

     




저번 포스팅에서 초밥을 쌓는 것 까지 구현했습니다. 이제 터치 이벤트를 넣어서 실제로 냥이가 움직이고 초밥을 날리는 애니메이션을 추가해보도록 하죠.


일단 본견적으로 기능들을 만들기 전에 게임내의 상수를 관리하는 중앙 헤더파일을 만들어 보도록 하죠. 어떤 프로그램을 만들든 공통으로 사용하는 상수를 관리하는 헤더파일은 중요합니다. 


Constant.h라는 헤더파일을 만듭니다.




가장 먼저 선언할 상수는 냥이의 위치입니다. 터치할 때마다 냥이가 왼쪽, 오른쪽 움직이는 것을 구분할 수 있도록 냥이에 위치 변수를 만들어 줍니다. 




이처럼 뭔가 공통된 변수나 연속된 변수를 만들때에는 enum을 주로 사용합니다. 일일이 상수를 지정해 줄 수도 있지만 enum을 사용하면 연속된 상수를 차례대로 입력하게 할 수 있습니다. enum으로 선언하면 처음부터 0, 1 , 2 .... n으로 값을 대입해줍니다. 여기서는 Left = 0, Right = 1, None = 2의 값을 가지고 있습니다. 




냥이의 위치에 대한 상수를 정의했기 때문에 실제로 냥이를 가리키는 Character 클래스에가서 get,set메소드를 만들어줍니다. getter와 setter는 지겹게 만들어 보았으니 어떻게 코드가 구성되는지는 대충 짐작이 가시겠죵?

setSide에는 한가지 기능을 추가해줍니다. 기존에 있던 방향과 반대방향으로 이동하는 기능이죠~! 실제로 냥이가 이동하는 것은 아닌데, 다만 노드의 위치를 바꿔줍니다. 




setSide를 호출할 경우 Side값이 Left일 경우 1.0f를 대입해주고, 반대일 경우 -1.0f를 대입합니다.


자 이제 메인에 Character를 녹여야 합니다. 먼저 헤더에서 Character 포인터를 선언해주고, Cpp에서 터치관련 이벤트를 만들어 봅시다.




메인에서 onEnter()라는 새로운 메소드를 만들었습니다. onEnter() 메소드는 init() 메소드보다 나중에 실행되는데 굳이 터치 메소드를  init에 만들지 않고 onEnter에 만든 이유는 화면에 터치리스너를 적용하기 전에 화면이 생성되지 않는 경우를 방지하기 위해서 입니다. init에서 화면을 제대로 읽기 전에 터치이벤트를 설정하면 에러가 날 수 있기 때문이죠.


위와같이 코딩하고 프로그램을 실행시키면 정상적으로 실행되는 것을 확인할 수 있습니다.


이제 냥이의 방향을 정할 수 있으니, 초밥에 꽃혀있는 젓가락의 방향을 설정해보도록 하겠습니다. 기본적으로 방향을 설정하는 것은 냥이와 비슷한 로직이나, 방향에 따라 그림의 위치를 바꿔주는 것이 아니라 왼쪽이나 오른쪽의 젓가락을 Visible값을 변경하는 것이 포인트입니다.




Piece안에 초밥과 젓가락을 제어하기 위해 getChildByName을 사용해 Sprite를 불러왔습니다. 방향에 맞게 해당 젓가락의 Visible을 설정해 줍니다. 젓가락의 경우 사용자에 의해 방향이 정해지는 것이 아니라 특정 규칙에 의해 정해지는 것이므로 규칙을 만들 필요가 있습니다. Main에서 젓가락 방향이 어떻게 결정될지 규칙을 세워봅시다.




규칙은 간단합니다. 일단 이전에 젓가락이 있다면 다음번 젓가락은 없습니다. 이전에 젓가락이 없다면 45%의 확률로 왼쪽, 45%의 확률로 오른쪽, 나머지 10%가 없는 젓가락입니다. CCRANDOM_0_1()메소드를 사용하면 0.0~1.0 사이의 실수값이 랜덤하게 결정됩니다. 간단한 조건절을 사용해 다음 젓가락의 방향을 결정하고, 설정해주면 끝.


실행하면 화면은 다음과 같습니다.





이제 초밥이 일렬로 내려오는 것을 만들려고 합니다. 지금 현재 초밥이 10개가 한 벡터내에 들어가 있죠. 여기서 제일 아래 초밥을 맨 위에 쌓고, 젓가락의 위치를 바꿔주고, 전체 벡터의 위치를 내리면? 화면에서는 초밥이 아래로 내려간 것으로 보입니다. 다음과 같이 초밥을 움직이는 메소드를 main에 추가합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void MainScene::stepSushi()
{
    //현재 Piece를 읽어온다. 냥이 가장 옆에 있는 초밥을 읽음. init()에서 초기값 0으로 셋팅.
    Piece* currentPiece = this->pieces.at(this->pieceIndex);
    
    //현재 초밥을 맨 위로 올린다.
    currentPiece->setPosition(currentPiece->getPosition() + Vec2(0.0f, currentPiece->getSpriteHeight() / 2.0f * 10.0f));
    
    //현재 초밥을 화면 맨 앞으로 가져온다. 안그러면 맨위에 초밥보다 뒤에있어서 모양이 이상해짐
    currentPiece->setLocalZOrder(currentPiece->getLocalZOrder()+1);
    
    //현재 초밥의 사이드를 결정
    currentPiece->setObstacleSide(this->getNextSide(this->lastSide));
    this->lastSide = currentPiece->getObstacleSide();
    
    //초밥 빌딩 노드를 아래로 내려서 움직인것처럼 보이게함
    this->pieceNode->setPosition(this->pieceNode->getPosition() + Vec2(0.0f, -1.0f * currentPiece->getSpriteHeight() / 2.0f));
    
    //인덱스 재설정
    this->pieceIndex = ( this->pieceIndex + 1 ) % 10;
}
 
cs


짜 이제 stepSushi() 메소드가 실행될때마다 초밥이 한칸씩 아래로 움직일 것입니다. 헤더에  pieceIndex를 추가하는 것을 잊지 마시구요.


테스트를 해보기 위해 onTouchBegan에다가 this->stepSushi를 추가하고 실행시켜봅시다. 터치를 할때마다 초밥이 아래로 내려오는 것을 알 수 있습니다.




응? 근데 이건 뭐죠? ㅋㅋ 계속 눌러보니 젓가락이 양쪽에 달려있습니다. 흠.... 젓가락의 위치를 셋팅할 때 기존 젓가락을 없애주지 않았군요..


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
void Piece::setObstacleSide(Side side)
{
    this->side = side;
    
    Sprite* roll = this->getChildByName<Sprite*>("roll");
    
    Sprite* rightChopstick = roll->getChildByName<Sprite*>("rightChopstick");
    Sprite* leftChoptstick = roll->getChildByName<Sprite*>("leftChopstick");
    
    //기존 젓가락 제거!!!!!!!
    rightChopstick->setVisible(false);
    leftChoptstick->setVisible(false);
    
    switch (this->side) {
        case Side::Left:
            leftChoptstick->setVisible(true);
            break;
        case Side::Right:
            rightChopstick->setVisible(true);
            break;
        case Side::None:
            break;
        default:
            break;
    }
}
cs


기존 젓가락을 제거하는 코드를 넣었습니다.


자 이제 스텝도 다 만들었으니 캐릭터와 냥이가 충돌하는지를 검사해서 게임오버인지를 체크하는 메소드를 만듭니다. 충돌을 감지하는 방법은 여러가지가 있는데 기본적으로는 두 개의 스프라이트가 겹치는 부분이 있는지를 체크하게 됩니다. 하지만~ 이렇게 구현하면 상당히 많은 계산을 하게 되죠... 젓가락 스프라이트의 모든 좌표와 냥이 스프라이트의 모든 좌표가 겹치는지 검사해야하기 때문입니다. 


이 게임에서는 약간 기교를 부려서 충돌체크를 쉽게 할 수 있습니다. 현재 Piece의 Side 값과 냥이의 Side값을 감지해서 같으면? 충돌한 거죠. 이를 판단해서 게임오버 여부를 결정하는 메소드를 만듭니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
 
 
//게임오버 체크
bool MainScene::isGameOver()
{
    if(this->character->getSide() == this->pieces.at(pieceIndex)->getObstacleSide())
        return true;
    else
        return false;
}
 
 
 
 



cs


전 위와같이 만들었습니다. 예제를 보니 다르게 만들었던데.. 뭐 상관없죠 ^^


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
 
//
//  Constant.h
//  SushiNeko
//
//  Created by Cho on 2016. 4. 16..
//
//
 
#ifndef Constant_h
#define Constant_h
 
enum class Side
{
    Left,    //0
    Right,   //1
    None     //2
};
 
enum class GameState
{
    Playing,
    GameOver
};
 
 
#endif /* Constant_h */
 
 
 
 
cs


게임 상태를 확인하는 상수를 만들어줍니다. Constant에 GameState 상수를 만듭니다.

메인 헤더에서도 GameState를 하나 만들어서 게임오버인지 체크하도록 합시다.

init()에서 상태를 Playing상태로 초기화하도록 합시다.

게임오버상태로 바꿔주는 메소드를 하나 만듭니다. 게임오버 상황이 여러가지일 수 있기 때문에 메소드로 만들어서 재사용 하는 것이 좋습니다.


1
2
3
4
5
6
7
8
9
10
11
 
void MainScene::triggerGameOver()
{
    this->gameState = GameState::GameOver;
}
 
void MainScene::triggerPlaying()
{
    this->gameState = GameState::Playing;
}
 
cs

 

만드는 김에 Playing트리거도 만들었습니다. 이제 isGameOver() 메소드를 통해 게임오버 여부를 판단할 수 있고 트리거를 통해 게임오버 or 진행상황을 만들 수 있습니다.


이게임에서 게임오버가 될 수 있는 상황은 냥이가 이동했는데 그곳에 젓가락이 있거나, 냥이 머리 위로 젓가락이 떨어지는 상황입니다. 따라서 냥이가 이동했을 때와 초밥이 아래로 내려올 때마다 게임오버 여부를 체크하면 됩니다. 


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
 
void MainScene::setupTouchHandling()
{
    auto touchListener = EventListenerTouchOneByOne::create();
    
    touchListener->onTouchBegan = [&](Touch* touch, Event* event)
    {
        Vec2 touchLocation = this->convertTouchToNodeSpace(touch);
        
        //게임상태 체크.
        switch (this->gameState) {
        
            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


터치이벤트에서는 또 하나 해줄 것이 있습니다. 바로 게임상태를 체크해서 Playing일 때만 냥이가 움직이는 것이지요. 게임오버됬는데 냥이가 움직이면 이상하겠죠?

게임오버상태에서는 게임을 재시작하는 코드를 추후에 추가해야 할 것 같습니다.


그 다음은 점수와 시간을 설정하는 코드를 추가합니다. 생각보다 이곳저곳에 코드를 추가해서 메인 코드 전체를 보는 것이 빠를 것 같습니다.


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
54
55
56
57
58
59
60
61
62
63
64
65
#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__
 
#include "cocos2d.h"
#include "Piece.hpp"
#include "Constant.h"
#include "ui/CocosGUI.h"
 
class Character;
 
class MainScene : 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();
    void onEnter() override;
    void update(float dt) override;
    float rollHeight;
    
    //이전 젓가락의 위치를 보고 다음 젓가락 위치를 설정함
    Side getNextSide(Side side);
 
    //초밥 움직임 설정
    void stepSushi();
    
    //게임오버 여부 검사
    bool isGameOver();
    
    //게임오버 설정
    void triggerGameOver();
    
    //게임 시작설정
    void triggerPlaying();
    
    //점수 설정
    void setScore(int score);
    
    //타임 레프트 결정
    void setTimeLeft(float timeLeft);
    
    // implement the "static create()" method manually
    CREATE_FUNC(MainScene);
    
    cocos2d::Node* pieceNode;
    cocos2d::Vector<Piece*> pieces;
    cocos2d::ui::Text* scoreLabel;
    cocos2d::Sprite* timeBar;
    
protected:
    
 
    void setupTouchHandling();
    Character* character;
    Side lastSide;
    int pieceIndex;
    GameState gameState;
    int score;
    float timeLeft;
};
 
#endif // __HELLOWORLD_SCENE_H__
 

cs


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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
#include "MainScene.h"
#include "cocostudio/CocoStudio.h"
#include "ui/CocosGUI.h"
#include "CharacterReader.hpp"
#include "PieceReader.hpp"
#include "Character.hpp"
 
USING_NS_CC;
 
using namespace cocostudio::timeline;
 
Scene* MainScene::createScene()
{
    // 'scene' is an autorelease object
    auto scene = Scene::create();
    
    // 'layer' is an autorelease object
    auto layer = MainScene::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 MainScene::init()
{
    //////////////////////////////
    // 1. super init first
    if ( !Layer::init() )
    {
        return false;
    }
    
    CSLoader* instance = CSLoader::getInstance();
    instance->registReaderObject("CharacterReader", (ObjectFactory::Instance) CharacterReader::getInstance);
    instance->registReaderObject("PieceReader", (ObjectFactory::Instance) PieceReader::getInstance);
    
    auto rootNode = CSLoader::createNode("MainScene.csb");
    //버그 땜이 아래 코드 추가!! cocos2d-x 3.7에서도 아직 안고쳐짐!!
    Size size = Director::getInstance()->getVisibleSize();
    rootNode->setContentSize(size);
    ui::Helper::doLayout(rootNode);
    
    
    addChild(rootNode);
 
    this->pieceNode = rootNode->getChildByName("pieceNode");
    this->character = rootNode->getChildByName<Character*>("character");
    
    this->lastSide = Side::Left;
    this->pieceIndex = 0;
    
    //스코어 라벨 불러오기
    this->scoreLabel = rootNode->getChildByName<cocos2d::ui::Text*>("scoreLabel");
    //스코어 초기화
    this->setScore(0);
    
    //라이프 게이지 가져오기
    auto lifeBG = rootNode->getChildByName("lifeBG"); //가져와서 아무것도 안하기 때문에 그냥 바로 선언
    this->timeBar = lifeBG->getChildByName<Sprite*>("lifeBar");
    this->timeLeft = 10.0f;
    for(int i = 0; i < 10++i)
    {
        Piece* piece = dynamic_cast<Piece*>(CSLoader::createNode("Piece.csb"));
        rollHeight = piece->getSpriteHeight();
        piece->setPosition(0.0f, rollHeight / 2.0f * i);
        
        this->lastSide = this->getNextSide(this->lastSide);
        piece->setObstacleSide(this->lastSide);
        this->pieceNode->addChild(piece);
        this->pieces.pushBack(piece);
    }
    
    return true;
}
 
void MainScene::update(float dt)
{
    Layer::update(dt);
    
    if(gameState == GameState::Playing)
    {
        this->setTimeLeft(this->timeLeft - dt);
        
        if(this->timeLeft <= 0.0f)
            this->triggerGameOver();
    }
}
 
void MainScene::setTimeLeft(float timeLeft)
{
    this->timeLeft = clampf(timeLeft,0.0f,10.0f);
    this->timeBar->setScaleX(this->timeLeft/10.0f);
}
 
void MainScene::setScore(int score)
{
    this->score = score;
    this->scoreLabel->setString(std::to_string(this->score));
}
 
void MainScene::triggerGameOver()
{
    this->gameState = GameState::GameOver;
}
 
void MainScene::triggerPlaying()
{
    this->gameState = GameState::Playing;
}
 
//다음 젓가락의 위치를 설정하는 메소드
Side MainScene::getNextSide(Side lastSide)
{
    Side nextSide;
    
    if(lastSide == Side::None)
    {
        float sideSelecter = CCRANDOM_0_1();
        if(sideSelecter <0.45f)
            nextSide = Side::Left;
        else if(sideSelecter < 0.9f)
            nextSide = Side::Right;
        else
            nextSide = Side::None;
    }
    else
    {
        nextSide = Side::None;
    }
    return nextSide;
}
 
//게임오버 체크
bool MainScene::isGameOver()
{
    if(this->character->getSide() == this->pieces.at(pieceIndex)->getObstacleSide())
        return true;
    else
        return false;
}
 
void MainScene::stepSushi()
{
    //현재 Piece를 읽어온다. 냥이 가장 옆에 있는 초밥을 읽음. init()에서 초기값 0으로 셋팅.
    Piece* currentPiece = this->pieces.at(this->pieceIndex);
    
    //현재 초밥을 맨 위로 올린다.
    currentPiece->setPosition(currentPiece->getPosition() + Vec2(0.0f, currentPiece->getSpriteHeight() / 2.0f * 10.0f));
    
    //현재 초밥을 화면 맨 앞으로 가져온다. 안그러면 맨위에 초밥보다 뒤에있어서 모양이 이상해짐
    currentPiece->setLocalZOrder(currentPiece->getLocalZOrder()+1);
    
    //현재 초밥의 사이드를 결정
    currentPiece->setObstacleSide(this->getNextSide(this->lastSide));
    this->lastSide = currentPiece->getObstacleSide();
    
    //초밥 빌딩 노드를 아래로 내려서 움직인것처럼 보이게함
    this->pieceNode->setPosition(this->pieceNode->getPosition() + Vec2(0.0f, -1.0f * currentPiece->getSpriteHeight() / 2.0f));
    
    //인덱스 재설정
    this->pieceIndex = ( this->pieceIndex + 1 ) % 10;
    
    if(this->isGameOver())
    {
        this->triggerGameOver();
    }
}
 
void MainScene::onEnter()
{
    Layer::onEnter();
    
    this->setupTouchHandling();
    
    this->scheduleUpdate();
}
 
void MainScene::setupTouchHandling()
{
    auto touchListener = EventListenerTouchOneByOne::create();
    
    touchListener->onTouchBegan = [&](Touch* touch, Event* event)
    {
        Vec2 touchLocation = this->convertTouchToNodeSpace(touch);
        
        //게임상태 체크.
        switch (this->gameState) {
        
            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


score와 time, update까지 구현한 소스파일입니다.


update()는 시간에 흐름에 따라 자동으로 호출하는 메소드인데 fps를 기반으로 호출하게 됩니다. 정상적이라면 1초에 60번 호출되죠.(60fps의 경우) 자동으로 dt에 시간이 기록되기 때문에 초기값 10초에서 dt값을 빼주면 타이머를 만들 수 있습니다.


만들다보니 깨달은 것이, 자동으로 초밥타워가 내려오는 것이 아니라 제한된 시간내에 많이 클릭해서 초밥타워를 몇개를 이동하느냐가 관건이군요. 이제 이소스로 실행하면 제한된 시간안에 냥이를 최대한 많이 터치해서 초밥 타워를 칠 수 있습니다.


이제 남은건 점수 카운트, 기본적인 게임 UI구성, 초밥이 날아가는 애니메이션 설정이 있겠군요.

다음 포스팅이 마지막이 될 것 같습니다 ^^

반응형

댓글

Designed by JB FACTORY