본문 바로가기

cocos2d-x/pyocopang

cocos2d-x 포코팡 류 게임만들기!! - 4 - 게임 Coin!! CCArray

이전 작업들을 통해 게임화면을 어느 정도 준비했다면, 이제부터는 실제 게임에 필요한 코인(딱히 부를만한 이름이 없어서 마음대로 정했습니다. 모양도 동그랗고 해서...)들을 배치하고 터치해서 없애고, 없어진 부분에 새로 코인이 생기게 하는 진짜 게임스러운 부분들을 만들어보겠습니다.

우선 게임 동작에 대한 기본적인 아이디어들을 생각해보고 실제로 구현해보겠습니다.

- 4 - 게임 Coin!! CCArray

처음 구상 부분에서 가볍게 생각해봤지만, 이제는 실제로 구현을 해야되기 때문에... 앞으로의 뻘짓을 줄이기 위해서라도 최대한 자세히 동작들을 구분하고 정리해야겠습니다. 요구사항을 결정하는 것은 정말 중요합니다. 혼자 생각하고 혼자 구현하고 해야되기 때문에 시간낭비를 하지않기 위해서는 준비를 잘 해야겠습니다.

1. 게임동작


최대한 포코팡 게임동작과 유사한 형태로 진행하려고 합니다. 여러가지 최적화 기법들이 많을 것 같지만, 일단 최적화는 할줄도 모르니 나중에 생각하고...^^; 기본적인 기능 구현에 충실하도록 하겠습니다.

1) 게임에 사용되는 코인은 4가지 종류

2) 전체 코인의 수는 : 6 * 7 == 42
 - 한 줄에 6개 코인
 - 7줄로 구성

3) 각 코인의 종류는 랜덤으로 결정

4) 터치 지점의 코인이 선택
 - 터치 후 이동한 위치의 코인이 이전과 동일한 종류일 때만 선택 가능
 - 터치를 떼는 순간 선택된 코인의 수가 3개 이상일 때만 코인 제거가 가능

5) 코인이 제거되면 나머지 코인은 하단의 빈 공간으로 이동
 - 기존의 남은 코인이 밑으로 내려가고 난 자리의 공간에는 새로운 코인이 위에서 부터 내려옴

6) 점수는 그냥 없어진 코인 수 * 100 점


2. GameCoin Class


CCSprite로 이미지를 표시하는데는 전혀 문제가 없지만, Coin의 종류와 상태를 표시하기가 어려워서 CCSprite Class를 상속받아 type과 state를 추가하기로 결정했습니다.

Classes/GameCoin.h

#ifndef __PYOCOPANG_GAMECOIN_H__
#define __PYOCOPANG_GAMECOIN_H__

#include "cocos2d.h"

using namespace cocos2d;

class GameCoin : public CCSprite
{
private:

public:
    enum gameState {
        LIVE,
        DEAD,
        SELECT,
    };  

    CC_SYNTHESIZE(int, _type, Type);
    CC_SYNTHESIZE(int, _state, State);

    static GameCoin* spriteWithFile(const char* pszFileName);

    GameCoin(); 
    ~GameCoin();
};

#endif // __PYOCOPANG_GAMECOIN_H__

위와 같이 CCSprite를 상속받은 class GameCoin을 만듭니다. 게임 상태를 표시할 enum 변수와 이 상태값을 저장할 state 변수, 어떠한 종류의 코인인지 표시하기 위한 type 변수를 만듭니다. 

CC_SYNTHESIZE()라는 매크로가 보이는데요. 이 매크로는 변수를 생성하면서 소위 set, get 함수 또는 세터, 게터 등등으로 불리는 함수를 같이 생성해주는 역할을 합니다. 위의 코드처럼 CC_SYNTHESIZE(int, _type, Type) 으로 선언하면,

1. int _type;
2. int getType() { return _type; }
3. void setType(int type) { _type = type; }

의 역할을 하는 코드를 생성해줍니다(정확히 같은 코드는 아니겠지만요..). 변수 한두개 생성할 때는 모르겠지만, 나중에 변수가 많아지면 아주 편리하게 사용할 수 있을 것 같습니다.

그리고, 이미지 파일 이름을 받아서 class 객체를 생성하는 spriteWithFile()과 생성자, 소멸자 함수를 선언합니다.


Classes/GameCoin.cpp

#include "GameCoin.h"

GameCoin::~GameCoin()
{
}

GameCoin::GameCoin()
:   _type(0),
    _state(GameCoin::LIVE)
{
}

GameCoin* GameCoin::spriteWithFile(const char* pszFileName)
{       
    GameCoin* sprite = new GameCoin();
    if(sprite && sprite->initWithFile(pszFileName)) {
        sprite->autorelease();
        return sprite;
    }
    CC_SAFE_DELETE(sprite);
    return NULL;
}   

cpp 파일에는 실제 함수를 구현해보겠습니다. 소멸자는 일단 뭐할게 없지만, 나중을 위해서... 그리고 생성자에서는 멤버변수의 초기화를 하고, spriteWithFile()에서는 GameCoin 객체를 생성해서 반환해주도록 작성합니다. GameCoin은 CCSprite를 상속받았기 때문에 initWithFile() 또한 사용할 수가 있습니다.

이제 GameCoin을 만들 수 있으니 GameCoin들을 만들어서 화면에 쫙 깔아보겠습니다.


3. GameCoin CCArray


GameCoin을 쉽게 관리하기 위해 배열을 사용하겠습니다. cocos2d-x에서는 CCArray란 class로 쉽게 배열을 관리할 수 있습니다. 원하는 index로 검색하거나 배열 중 원하는 위치에 삽입하거나 현재 개수를 표시하는 등의 다양한 기능을 제공합니다. 자세한 내용은 API Refenrence 문서를 참조해주세요.

다시, 우리 게임의 주가 되는 HelloWorldScene.h, cpp에 코드를 작성하겠습니다. 조금 전에 만든 GameCoin class를 사용하기 위해 GameCoin.h 헤더를 include 합니다. 그리고 CCArray 사용을 위한 _gameCoins 변수를 생성하고, 앞으로 게임에서 많이 사용할 것 같은 값을 enum으로 선언합니다. 2차원 배열로 생각했을 때, 6행 7열의 형태가 되기 때문에 BOARDX 란 값으로 7, BOARDY에는 6을 지정하겠습니다. 그리고 게임에서 총 4 종류의 코인을 사용하기로 했기 때문에 TOTALCOINTYPE를 4로 지정합니다. 나중에 또 자주사용할 값이 필요하면 여기에 추가하도록 하겠습니다.

#include "cocos2d.h"
#include "GameCoin.h"

using namespace cocos2d;

class HelloWorld : public cocos2d::CCLayer
{
private:
    CCSize _screenSize;

    CCAction* _enemyMove;
    CCAction* _treeMove;

    CCArray* _gameCoins;

public:
    enum {
        BOARDX = 7,
        BOARDY = 6,
        TOTALCOINTYPE = 4,
    };  
    // 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);
    void initGameCoin(void);
    GameCoin* createGameCoin(const CCPoint &pos, int type, int state);
};

코인들의 초기화는 initGameCoin()에서 하도록하고, 원하는 위치, 종류, 상태에 맞게 코인을 만들어 주는 createGameCoin()을 선언하겠습니다.

기본적으로 아래와 같은 아이디어를 가지고 코인들을 배치하려고 합니다.

1. 0번 부터, 각 코인에 대해
2. (x, y) 위치 설정
3. 랜덤하게 종류 설정
4. 2, 3번 정보로 코인을 만들어서 array에 추가
5. 이렇게 0~41번까지 계속 반복

각 코인들은 이렇게 만들면 될 것 같습니다. 다음의 코인 이미지를 프로젝트에 추가하고요. 뭔가 포코팡처럼 도형을 추가하고 싶었지만, 귀찮아서 그냥... 나머지는 그냥 알파벳으로...

      


원 모양의 코인이기 때문에 코인들의 간격은 동일해서 0번 코인의 위치만 잘 설정하면 나머지 코인들의 위치는 쉽게 계산할 수 있을 것 같은데요. 한 가지 애로사항이 생겼습니다. 홀수 줄과 짝수 줄의 시작 위치가 조금 차이가 나네요. 예상 위치를 그림으로 한 번 그려보겠습니다.

아. 홀수 줄은 짝수 줄에 비해 코인 간격 반만큼 아래에서 시작하면 될 것 같습니다. 정확한 계산!! 뭐 이런거 없습니다. 조금씩 값을 바꿔가면서 0번 코인의 시작위치와 각 코인의 간격을 대충 찾았습니다. 물론 제 폰 기준으로요...

그럼 이제 지금까지의 아이디어를 바탕으로 initGameCoin() 함수에서 배열을 초기화하고, 각 코인을 배열에 추가하도록 하겠습니다.

Classes/HelloWorldScene.cpp

void HelloWorld::initGameCoin()
{
    int coinXPos = 0;
    int coinYPos = 0;
    int diffX = 104;
    int diffY = 120;
    int initCoinXPos = 72; 
    int initCoinYPos = 778;

    GameCoin* gameCoin;

    // init array
    _gameCoins = CCArray::createWithCapacity(BOARDX * BOARDY);
    _gameCoins->retain();

    // init gameCoin
    for(int xIndex = 0; xIndex < BOARDX; xIndex++) {
        coinXPos = initCoinXPos + (xIndex * diffX);
        coinYPos = initCoinYPos;
        if(xIndex % 2 == 0)
            coinYPos -= diffY/2;

        for(int yIndex = 0; yIndex < BOARDY; yIndex++) {
            gameCoin = createGameCoin(ccp(coinXPos, coinYPos),
                            rand() % TOTALCOINTYPE + 1,
                            GameCoin::LIVE);

            _gameCoins->addObject(gameCoin);

            coinYPos -= diffY;
        }   
    }   
}

6 * 7 총 42개로 배열 크기를 초기화하고, retain()으로 설정합니다. retain() 설정을 했으니 소멸자 함수에 메모리를 해제하는 매크로를 추가해야겠죠.

HelloWorld::~HelloWorld()
{
    CC_SAFE_RELEASE(_enemyMove);
    CC_SAFE_RELEASE(_treeMove);
    CC_SAFE_RELEASE(_gameCoins);
}

이번엔 코인 초기화 부분을 살펴보겠습니다. 위에서 설명한 것과 동일하게 코드를 구현해보았습니다. 각 열 별로 초기 코인위치를 계산하고, 홀수 열인 경우 시작 위치를 코인 간격 반만큼 아래로 조정합니다.

각 코인은 createGameCoin(위치, 종류, 상태) 를 이용해 생성합니다. 앞서 계산된 위치 값을 사용하고, 종류는 랜덤하게 결정합니다. 이 때, rand() 함수로 시작하는 코드 마지막에 +1 을 한 이유는 별 다른 것은 없습니다. 이 값은 코인 종류와 함께 코인 이미지를 결정하기 위해서 사용되는데요. 코인 이미지의 이름이 coin_01.png 로 1부터 시작하기 때문입니다.

최종적으로 생성된 코인을 addObject를 통해 배열에 추가합니다. 각 열에 대한 x값은 모두 동일하기 때문에 y값만 조정하여 다음 코인 위치를 계산하고, 이렇게 모든 코인을 반복하여 42개의 코인을 모두 설정합니다. 

createGameCoin() 함수의 내부 동작은 다음과 같이 구현합니다.

GameCoin* HelloWorld::createGameCoin(const CCPoint &pos, int type, int state)
{
    GameCoin* gameCoin;
    CCString* name;

    name = CCString::createWithFormat("coin_0%i.png", type);
    gameCoin = GameCoin::spriteWithFile(name->getCString());
    gameCoin->setVisible(true);
    gameCoin->setPosition(pos);
    gameCoin->setState(state);
    gameCoin->setType(type);

    this->addChild(gameCoin);

    return gameCoin;
}

type에 맞는 이미지로 GameCoin을 생성하고, 위치, 상태, 종류를 각각 설정합니다. 화면에 이미지가 보여야되니까 일단 addChild()로 현재 코인을 추가하고 이 코인을 반환합니다. 

마지막으로 initGameCoin()을 createGameScene()에 추가하고 빌드하면, 

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

    ...

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

    initGameCoin();
}

다음과 같이!! 각 위치에 만들어진 코인들을 볼 수 있습니다!!

뭔가 설명에서 빼먹은게 있는 것 같은건.. 단지 기분 탓이겠죠..?? 뭔가 안되는게 있으면 바로 댓글로 질문주세요. 글은 자주 못올려도 블로그는 자주 확인합니다!

다음에는 터치에 반응하도록 코드를 추가해보도록 하겠습니다.

... 읽어주셔서 감사합니다. 이번 편 끝.