인디 게임 강좌

전체보기

모바일 상단 메뉴

본문 페이지

[코코스2D] [Box2D] #01 - 기본 예제 만들기

아이콘 내폰젤무거워
댓글: 6 개
조회: 6787
추천: 3
2017-07-01 23:01:08


Document Version : V1.3 - 2017.07.01 with cocos2d-x 3.15.1

Document Version : V1.2 - 2015.06.08 with cocos2d-x 3.6

Document Version : V1.1 - 2014.03.14 with cocos2d-x 3.0beta2

Document Version : V1.0 - 2013.07.10 with cocos2d-x 2.1.4


제 책인 "시작하세요! Cocos2d-x 3.0 프로그래밍" 내용을  3.15.1 버전에 맞게 수정하여 올리고 있습니다.

이 글은 네이버카페 "Cocos2d-x 사용자 모임"에 동시에 게재되고 있습니다.

 

개발환경 : 

  • Windows7
  • Visual Studio Community 2017
  • Cocos2d-x 3.15.1
  • 사용 프로젝트 : proj.win32


 

박스2D 코드 재조립 및 분석


cocos2d-x에서 제공되는 cpp-tests 기본예제에 있는 박스2D 코드는 박스2D를 처음 접하는 사람들한테는 너무 복잡해 보이는게 사실입니다. 그래서 해당 코드를 살펴보고 조금이라도 쉬운 형태로 바꿔보도록 하겠습니다.


커맨드창을 열어 원하는 디렉터리로 이동한 후에, 다음과 같이 cocos 명령어를 이용하여 새로운 프로젝트를 생성합니다.

디렉터리 파라미터를 생략했기 때문에 명령어를 입력한 디렉터리에 프로젝트 명으로 프로젝트의 디렉터리가 생성됩니다.

  • 윈도우 환경 : 도스창 (커맨드창) 이용
  • 맥환경 : 터미널 이용

프로젝트명 : Box2dEx01

패키지명 : com.study.box01

랭귀지타입 : cpp

c:> cocos new Box2dEx01 -p com.study.box01 -l cpp  ↵


생성된 프로젝트의 내용을 정리하여 다음과 같이 만들어 줍니다.

다음은 우리가 만들 예제의 프로젝트의 기본형입니다.

이 상태로 실행해보면 배경이 흰 아무것도 없는 화면만 나옵니다.


[ HelloWordlScene.h ]

#ifndef __HELLOWORLD_SCENE_H__

#define __HELLOWORLD_SCENE_H__


#include "cocos2d.h"


class HelloWorld : public cocos2d::Scene

{

public:

    static cocos2d::Scene* createScene();

    virtual bool init();

    CREATE_FUNC(HelloWorld);


};


#endif // __HELLOWORLD_SCENE_H__


HelloWordlScene.cpp ]

#include "HelloWorldScene.h"


USING_NS_CC;


Scene* HelloWorld::createScene()

{

    return HelloWorld::create();

}


bool HelloWorld::init()

{

    if ( !Scene::init() )

    {

        return false;

    }

    

    // 배경을 흰색 레이어로 채운다.

    auto wlayer = LayerColor::create(Color4B(255255255255));

    this->addChild(wlayer);

    /////////////////////////////


    return true;

}




이제 기본형에 다음과 같은 코드를 추가로 입력합니다.


[ HelloWorldScene.h – 박스2D 베이직 ]

#ifndef __HELLOWORLD_SCENE_H__

#define __HELLOWORLD_SCENE_H__


// 윈도우의 경우 한글이 깨지지 않도록 utf-8을 적용한다.

#if (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32)

#pragma execution_character_set("utf-8")

#endif


#include "cocos2d.h"

#include "Box2D/Box2D.h"


#define PTM_RATIO 32


using namespace cocos2d;


class HelloWorld : public cocos2d::Scene

{

public:

    static cocos2d::Scene* createScene();

    virtual bool init();

    CREATE_FUNC(HelloWorld);


   cocos2d::Size winSize;

   cocos2d::Texture2D* texture;

   b2World* _world;


   ~HelloWorld();


   void onEnter();

   void onExit();

   void tick(float dt);


   bool onTouchBegan(Touch* touch, Event* event);

   void addNewSpriteAtPosition(Vec2 location);

};


#endif // __HELLOWORLD_SCENE_H__



박스2D에서 사용하는 물리적인 단위는 미터(m)로서 1이라고 하면 1m를 의미합니다. 그러므로 화면상의 1픽셀을 어느 정도의 크기 정도로 환산해서 계산할 것인지 지정하는 것이 바로 위 코드의 PTM_RATIO입니다. 위 코드에서는 1m를 32pixel로 환산해서 계산할 것이라고 값을 정의했습니다.


Box2D는 0.1m ~ 10m까지의 물체를 계산하기에 최적화되도록 설계되었습니다.

따라서, 1280 x 720의 화면에서는 높이인 720픽셀이 10미터가 되어야 하며

1920 x 1080의 화면에서는 높이인 1080픽셀이 10미터가 되어야 합니다.

그러므로 PTM_RATIO는 [ 화면높이 / 10 ]이 가장 이상적이라고 보시면 됩니다.

여기서 만드는 예제의 화면 크기는 480 pixel  x 320 pixel 입니다. 

PTM_RATIO의 값을 적용하면 화면에서 구현하는 실제 물리 공간은 15 m  x  10 m 의 공간이 될 것입니다.



그럼 이번에는 CPP를 다음과 같이 만듭니다. 이번 포스팅에서는 별도로 자세히 설명하기보다 해당 코드의 주석을 통해 주로 설명하겠습니다. 그러므로 각 코드의 주석을 잘 읽어보기 바랍니다.


[ HelloWorldScene.cpp – 박스2D 베이직 ]

#include "HelloWorldScene.h"


Scene* HelloWorld::createScene()

{

    return HelloWorld::create();

}


bool HelloWorld::init()

{

    if ( !Scene::init() )

    {

        return false;

    }

    

    // 배경을 흰색 레이어로 채운다.

    auto wlayer = LayerColor::create(Color4B(255255255255));

    this->addChild(wlayer);

    /////////////////////////////


    // 윈도우 크기를 구한다.

    winSize = Director::getInstance()->getWinSize();


    // 이미지의 텍스쳐를 구한다.

    texture = Director::getInstance()->getTextureCache()->addImage("SpinningPeas.png");



    // 월드 생성 시작 ---------------------------------------------------------


    // 중력의 방향을 결정한다.

    b2Vec2 gravity = b2Vec2(0.0f, -30.0f);


    // 월드를 생성한다.

    _world = new b2World(gravity);


    // 휴식 상태일때 포함된 바디들을 멈추게(sleep) 할 건지 결정한다.

    _world->SetAllowSleeping(true);


    // 지속적인 물리작용을 할 것인지 결정한다.

    _world->SetContinuousPhysics(true);




    // 가장자리(테두리)를 지정하여 공간(Ground Box)을 만든다.


    // 바디데프에 좌표를 설정한다.

    b2BodyDef groundBodyDef;

    groundBodyDef.position.Set(00);


    // 월드에 바디데프의 정보(좌표)로 바디를 만든다.

    b2Body* groundBody = _world->CreateBody(&groundBodyDef);


    // 가장자리(테두리) 경계선을 그릴 수 있는 모양의 객체를 만든다.

    b2EdgeShape groundEdge;

    b2FixtureDef boxShapeDef;

    boxShapeDef.shape = &groundEdge;


    // 에지모양의 객체에 Set( 점1 , 점2 )로 선을 만든다.

    // 그리고 바디(groundBody)에 모양(groundEdge)을 고정시킨다.


    // 아래쪽.

    groundEdge.Set(b2Vec2(00), b2Vec2(winSize.width / PTM_RATIO, 0));

    groundBody->CreateFixture(&boxShapeDef);


    // 왼쪽

    groundEdge.Set(b2Vec2(00), b2Vec2(0, winSize.height / PTM_RATIO));

    groundBody->CreateFixture(&boxShapeDef);


    // 위쪽

    groundEdge.Set(b2Vec2(0, winSize.height / PTM_RATIO),

                               b2Vec2(winSize.width / PTM_RATIO, winSize.height / PTM_RATIO));

    groundBody->CreateFixture(&boxShapeDef);


    // 오른쪽

    groundEdge.Set(b2Vec2(winSize.width / PTM_RATIO, winSize.height / PTM_RATIO),

                               b2Vec2(winSize.width / PTM_RATIO, 0));

    groundBody->CreateFixture(&boxShapeDef);


    // 월드 생성 끝   ---------------------------------------------------------


    this->schedule(schedule_selector(HelloWorld::tick));




    return true;

}


HelloWorld::~HelloWorld()

{

    // 월드를 C++의 new 로 생성했으므로 여기서 지워준다.

    delete _world;

    _world = nullptr;

}


void HelloWorld::onEnter()

{

    Scene::onEnter();


    // 싱글터치모드로 터치리스너 등록

    auto listener = EventListenerTouchOneByOne::create();

    listener->setSwallowTouches(true);

    listener->onTouchBegan = CC_CALLBACK_2(HelloWorld::onTouchBegan, this);


    _eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);

}


void HelloWorld::onExit()

{

    Scene::onExit();

}


void HelloWorld::tick(float dt)

{

    // 물리적 위치를 이용해서 그래픽 위치를 갱신한다.


    // velocityIterations : 바디들을 정상적으로 이동시키기 위해서 필요한 충돌들을 반복적으로 계산

    // positionIterations : 조인트 분리와, 겹침현상을 줄이기 위해서 바디의 위치를 반복적으로 적용

    // 값이 클수록 정확한 연산이 가능하지만 성능이 떨어진다.


    // 매뉴얼상의 권장값

    int velocityIterations = 8;

    int positionIterations = 3;


    // Step : 물리 세계를 시뮬레이션한다.

    _world->Step(dt, velocityIterations, positionIterations);


    // 모든 물리 객체들은 링크드 리스트에 저장되어 참조해 볼 수 있도록 구현되어 있다.

    // 만들어진 객체 만큼 루프를 돌리면서 바디에 붙인 스프라이트를 여기서 제어한다.

    for (b2Body* b = _world->GetBodyList(); b; b = b->GetNext())

    {

        if (b->GetUserData() != nullptr) {

            Sprite* spriteData = (Sprite *)b->GetUserData();

            spriteData->setPosition(Vec2(b->GetPosition().x * PTM_RATIO,

                                                            b->GetPosition().y * PTM_RATIO));

            spriteData->setRotation(-1 * CC_RADIANS_TO_DEGREES(b->GetAngle()));

        }

    }

}


bool HelloWorld::onTouchBegan(Touch* touch, Event* event)

{

    auto touchPoint = touch->getLocation();


    // 터치된 지점에 새로운 물리 객체의 바디와 해당 스프라이트를 추가한다.

    addNewSpriteAtPosition(touchPoint);


    return true;

}


void HelloWorld::addNewSpriteAtPosition(Vec2 location)

{

    // 스프라이트를 파라미터로 넘어온 위치에 만든다.

    Sprite* pSprite = Sprite::createWithTexture(texture, Rect(003737));

    pSprite->setPosition(Vec2(location.x, location.y));

    this->addChild(pSprite);


    // 바디데프 만들고 속성들을 지정한다.

    b2BodyDef bodyDef;

    bodyDef.type = b2_dynamicBody;

    bodyDef.position.Set(location.x / PTM_RATIO, location.y / PTM_RATIO);

    // 유저데이터에 스프라이트를 붙인다.

    bodyDef.userData = pSprite;


    // 월드에 바디데프의 정보로 바디를 만든다.

    b2Body* body = _world->CreateBody(&bodyDef);


    // 바디에 적용할 물리 속성용 바디의 모양을 만든다.

    // 원형태를 선택하여 지름을 지정한다.

    b2CircleShape circle;

    circle.m_radius = 0.55;


    // 그리고 바디(addedBody)에 모양(circle)을 고정시킨다.


    b2FixtureDef fixtureDef;


    // 모양을 지정한다.

    fixtureDef.shape = &circle;

    // 밀도

    fixtureDef.density = 1.0f;

    // 마찰력 - 0 ~ 1

    fixtureDef.friction = 0.2f;

    // 반발력 - 물체가 다른 물체에 닿았을때 팅기는 값

    fixtureDef.restitution = 0.7f;


    body->CreateFixture(&fixtureDef);

}




보다시피 박스2D 프로젝트에서는 일단 월드를 만듭니다. 그리고 월드의 기본적인 속성을 다음과 같이 지정했습니다.

    // 중력의 방향을 결정한다.

    b2Vec2 gravity = b2Vec2(0.0f, -10.0f);


    // 월드를 생성한다.

    _world = new b2World(gravity);



이제 이 월드 안에서는 중력이 작용합니다. 이차원 벡터(v2Vec2)의 값이 클수록 중력이 크게 작용합니다. 현재 예제의 중력은 Y축 방향의 음수로만 설정되어 있으니 위에서 아래로만 작용합니다.

  • 물체가 떨어지는 속도가 중력이 클수록 더 빨라진다는 이야기입니다.
  • 나중에 -10을 -30으로 변경해서 테스트해 보기 바랍니다.

 

이때 지구의 땅과 같은 역할을 해 주는 객체를 만들어 주지 않으면 화면에 추가되는 객체들은 모두 무한히 낙하하게 될 것입니다. 아래로 떨어지니 화면에서도 사라질 것입니다.


그래서 월드 안에 특정 공간을 둘러싸는 가장자리를 설정하고 박스 형태의 강체를 만들어 둘러싸게 하면 그 공간 안에서는 우리가 월드에 추가한 물체가 경계 밖으로 나가지 않게 됩니다. 물론 중력의 영향을 받아도 더는 떨어지지 않게 됩니다.


이제 화면 안에 물리 객체인 바디를 만들어 넣는 부분을 살펴보겠습니다.

바디를 만들려면 먼저 바디데프를 만들어 속성을 지정해야 합니다. 바디데프에는 해당 바디의 위치, 질량, 마찰력, 탄력 그리고 화면에 실제로 그려져서 눈에 보이는 스프라이트를 지정할 수 있습니다. 이렇게 만들어진 바디는 아직까지 물리 세계에서 모양을 가지고 있지 않습니다. 다만 만들어졌을 뿐입니다.


그러므로 이제 바디의 모양을 만들어야 합니다. 바디는 기본적으로 원 형태(circle), 박스 형태(box), 다각형 형태(polygon)로 만들 수 있습니다. 다각형이 지원되므로 거의 모든 형태를 만들 수 있습니다. 바디의 모양을 만들고 픽스처 객체를 통해 바디와 연결하면 앞에서 지정한 바디데프의 속성을 지닌 물체(바디)가 월드 안에서 모양을 갖추게 됩니다. 이때 화면상으로는 바디데프의 유저데이터에 들어 있는 스프라이트를 통해 우리 눈에 보이게 됩니다.

 

그리고 스케줄러로 tick 메서드를 만듭니다. 이 tick 메서드에서는 다음 코드를 통해 b2World 스텝을 실행합니다.


    // 매뉴얼상의 권장값

    int velocityIterations = 8;

    int positionIterations = 3;


    // Step : 물리 세계를 시뮬레이션한다.

    _world->Step(dt, velocityIterations, positionIterations);



박스2D 월드의 스텝 메서드는 물리 엔진을 한 스텝 진행시킵니다. 한 스텝마다 바디들을 정상적으로 이동시키는 데 필요한 충돌들을 반복적으로 계산하고 조인트 분리와 겹침 현상을 줄이기 위해 바디의 위치를 반복적으로 적용하는데, 위 코드에서 지정한 변수값에 의해 반복이 됩니다.


그러므로 이 변수들의 값을 높게 설정하면 속도는 느려지겠지만 정확한 시뮬레이션이 가능해집니다.


박스2D 매뉴얼에서는 velocityIterations을 8로 설정하고 positionIterations을 3으로 설정하는 것을 권장하고 있습니다.


그리고 애플리케이션의 논리적 타이밍과 물리적 타이밍을 맞추기 위해 dt 변수를 사용하는데, 스텝에 과도한 시간이 소요되면 물리 시스템이 이를 보전하기 위해 재빨리 전진(스텝)하게 됩니다. 이 방식을 가변 타임 스텝(variable time step)이라고 합니다. 이 방식의 대안인 고정 타임 스텝(fixed time step)은 1초의 1/60로 설정됩니다. 



이제 프로젝트를 실행해 볼 차례입니다.

아무것도 표시되지 않는 빈 화면이 나오면 화면의 아무 곳이나 마우스로 클릭해 보기 바랍니다.









위 그림처럼 터치된 곳에서 그림이 나와 중력의 영향을 받고 화면 아래로 떨어지는 것을 볼 수 있을 것입니다. 그리고 바닥에 닿으면 통통 튀는 모습도 볼 수 있을 것입니다. 이는 바디를 추가할 때 탄력 속성을 주었기 때문에 바닥과 추가된 바디가 충돌할 때 반발력이 생기는 것입니다.


  • 마찰력(friction)을 0.0~1.0 사이에서 여러 가지 값으로 바꿔서 테스트해 봅니다.
  • 반발력(restitution)을 0.0~1.0 사이에서 여러 가지 값으로 바꿔서 테스트해 봅니다.
  • 반발력을 1.0으로 설정하면 힘의 손실이 안 생기므로 무한한 운동을 하게 됩니다. 즉, 튀어오르는 동작이 멈추지 않습니다. 자연적으로는 일어날 수 없는 현상의 값인 것입니다.


이 예제를 통해 박스2D의 기본 개념이 이해가 되었습니까? 혹시라도 이해가 되지 않는다면 이해가 될 때까지 예제를 살펴보기 바랍니다. 이어서 나올 내용과 예제는 이 예제를 바탕으로 내용씩 조금씩 확장되기 때문입다.


Lv28 내폰젤무거워

모바일 게시판 하단버튼

댓글

새로고침
새로고침

모바일 게시판 하단버튼

지금 뜨는 인벤

더보기+

모바일 게시판 리스트

모바일 게시판 하단버튼

글쓰기

모바일 게시판 페이징