standwally

[Box2D] 기본 예제 본문

프로그래밍/Cocos2d-x

[Box2D] 기본 예제

standwally 2013. 12. 6. 11:00

Xcode에서 Box2D용 프로젝트를 만들 수 있다.


그런데 굳이, Xcode을 이용해 Box2D용 프로젝트를 만드는 방법을 이용하지 않아도 Box2D를 사용할 수 있다.

/cocos2d-x-2.1.4/external/Box2D 폴더를 통째로 복사해서 기존 Cocos2d-x 프로젝트에 추가하고, Box2D를 적용하고자 하는 클래스에 Box2D.h 파일을 include 해주면 된다.

Box2D에서 복사해야될 파일과 폴더는

Box2D.h 파일과, Collision, Common, Dynamics, Rope 이 4가지 폴더를 복사하면 된다.

 

Xcode를 이용해 생성된 프로젝트는 Box2D의 간단한 기능이 포함되어 생성된다.

자동으로 생성된 간단한 기능은 왠지 복잡해 보인다.

그래서, HelloWorld.h와 .cpp 파일 내용을 다 지우고 기본 개념 이해를 위한 Box2D의 예제를 만들어 본다.

 

먼저 HelloWorld.h 파일이다.

 

HelloWorld.h
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
#ifndef __HELLO_WORLD_H__
#define __HELLO_WORLD_H__
 
// When you import this file, you import all the cocos2d classes
#include "cocos2d.h"
#include "Box2D.h"
 
#define PTM_RATIO 32
 
using namespace cocos2d;
 
class HelloWorld : public cocos2d::CCLayerColor {
public:
    virtual bool init();
 
 
    static cocos2d::CCScene *scene();
 
    CREATE_FUNC(HelloWorld);
    ~HelloWorld();
 
    virtual void ccTouchesEnded(CCSet *pTouches, CCEvent *event);
    void tick(float dt);
    void addNewSpriteAtPosition(CCPoint location);
 
    CCSize winSize;
    CCTexture2D *texture;
    b2World *_world;
};
 
#endif // __HELLO_WORLD_H__

 

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

 

그럼 이번에는 HelloWorld.cpp 파일이다.

HelloWorld.cpp
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
#include "HelloWorld.h"
 
CCScene* HelloWorld::scene()
{
    CCScene *scene = CCScene::create();
     
    HelloWorld *layer = HelloWorld::create();
     
    scene->addChild(layer);
     
    return scene;
}
 
bool HelloWorld::init()
{
    if (!CCLayerColor::initWithColor(ccc4(255, 255, 255, 255))) {
        return false;
    }
     
    // 터치 활성화
    this->setTouchEnabled(true);
     
    // 윈도우 크기 구함.
    winSize = CCDirector::sharedDirector()->getWinSize();
     
    // 이미지 텍스쳐 구함.
    texture = CCTextureCache::sharedTextureCache()->addImage("CloseNormal.png");
     
    // 월드 생성 시작
    // 중력의 방향 결정
    b2Vec2 gravity = b2Vec2(0.0f, -30.0f);
     
    // 월드 생성
    _world = new b2World(gravity);
 
    // 휴식 상태일 때 포함된 바디들을 멈추게할지 결정함.
    _world->SetAllowSleeping(true);
     
    // 지속적인 물리작용을 할 것인지 결정함.
    _world->SetContinuousPhysics(true);
     
    // 가장자리를 지정해 공간을 만듬.
    // 바디데프에 좌표를 설정함.
    b2BodyDef groundBodyDef;
    groundBodyDef.position.Set(0, 0);
     
    // 월드에 바디데프의 정보(좌표)로 바디를 만듬.
    b2Body *groundBody = _world->CreateBody(&groundBodyDef);
     
    // 가장자리 경계선을 그릴 수 있는 모양의 객체를 만듬.
    b2EdgeShape groundEdge;
    b2FixtureDef boxShapeDef;
    boxShapeDef.shape = &groundEdge;
     
    // 에지 모양의 객체에 Set(점1, 점2)으로 선을 만듬.
    // 그리고 바다(groundBody)에 쉐이프(groundEdge)를 고정시킴.
    // 아래쪽
    groundEdge.Set(b2Vec2(0, 0), b2Vec2(winSize.width/PTM_RATIO, 0));
    groundBody->CreateFixture(&boxShapeDef);
     
    // 왼쪽
    groundEdge.Set(b2Vec2(0, 0), 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()
{
    delete _world;
    _world = NULL;
}
 
void HelloWorld::tick(float dt)
{
    // 물리적 위치를 이용해 그래픽 위치를 갱신함.
 
    // velocityIterations :
    // 바디들을 정상적으로 이동시키는데 필요한 충돌을 반복적으로 계산
    // positionIterations :
    // 조인트 분리와 겹침 현상을 줄이기 위해 바디의 위치를 반복적으로 적용
     
    // 값이 클수록 정확한 연산이 가능하지만 성능이 떨어짐.
    // 프로젝트 생성시 기본값
    // int velocityIterations = 8;
    // int positionIterations = 1;
     
    // 메뉴얼상의 권장값
    int velocityIterations = 8;
    int positionIterations = 3;
     
    // Step : 물리 세계를 시물레이션함.
    _world->Step(dt, velocityIterations, positionIterations);
     
    // 모든 물리 객체는 링크드 리스트에 저장되어 참조할 수 있도록 구현돼 있다.
    // 만들어진 객체만큼 루프를 돌리면서 바디에 붙인 스프라이트를 여기서 제어한다.
    for (b2Body *b = _world->GetBodyList(); b; b=b->GetNext()) {
        if (b->GetUserData() != NULL) {
            CCSprite *spriteData = (CCSprite *)b->GetUserData();
            spriteData->setPosition(ccp(b->GetPosition().x * PTM_RATIO, b->GetPosition().y * PTM_RATIO));
            spriteData->setRotation(-1 * CC_RADIANS_TO_DEGREES(b->GetAngle()));
        }
    }
}
 
void HelloWorld::ccTouchesEnded(CCSet *pTouches, CCEvent *event)
{
    CCSetIterator it = pTouches->begin();
    CCTouch *touch = (CCTouch *)(*it);
    CCPoint touchPoint = touch->getLocation();
    // 터치된 지점에 새로운 물리 객체의 바디와 해당 스프라이트를 추가함.
    addNewSpriteAtPosition(touchPoint);
}
 
void HelloWorld::addNewSpriteAtPosition(CCPoint location)
{
    // 스프라이트를 파라미터로 넘어온 위치에 만듬.
    CCSprite *pSprite = CCSprite::createWithTexture(texture, CCRectMake(0, 0, 37, 37));
    pSprite->setPosition(ccp(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);
}

 

init 함수에서 Box2D의 월드가 구성되는 과정을 간단하게 설명하면,

  1. b2World 객체 생성(중력값 설정)
  2. b2Body 객체를 월드에 생성(생성시 b2BodyDef 객체로 위치 설정)
  3. b2Shape 객체 생성(circle, edge, polygon, chain 등이 있음)
  4. b2FixtureDef 객체로 밀도, 마찰력, 반발력, b2Shape 값을 설정
  5. b2Body 객체에서 CreateFixture 함수를 호출함.(넘겨주는 파라미터는 b2FixtureDef)

이렇게 기본적으로  하나의 월드에 하나의 Body가 생성이 된다.

 

최초에 월드를 생성할 때, 중력값 b2Vec2(0.0f, -30.0f) 이 의미하는 것은,

이차원 벡터값의 차이값이 (0 ~ -30)이 클수록 중력이 크게 작용하며, 일반적으로 Y축 방향의 음수로 설정한다.(위에서 아래로 떨어지는 중력의 개념)

이때, 지구의 땅과 같은 역할을  해 주는 객체를 만들어주지 않으면 화면에 추가되는 객체들이 모두 무한히 낙하하게 될 것이다.

그래서 init 함수에 보면 월드안에 공간을 둘러싸는 가장자리를 설정하고 박스형태의  강체(Body)를 만들어준다.

 

만들어진 공간을 둘러싸는 가장자리 Body와 물리 작용을 하게 될 또 하나의 Body는 addNewSpriteAtPosition함수에서 만들어준다.

동일한 프로세스로 만들어지게 되는데, 한가지 차이점은 눈에 보이는 Body를 만들기 위해서 b2BodyDef의 userData에 텍스처 이미지를 담고 있는 Sprite객체를 넣어준다.

 

그리고 tick함수안에서는 world객체에 Step메서드를 호출해주는데, 이는 만들어진 물리 세계를 시뮬레이션하는 메서드이다.

Step 메서드는 물리 엔진을 한 스텝 진행시킨다. 한 스텝마다 Body들이 정상적으로 이동시키는 데 필요한 충돌들을 반복적으로 계산하고 Body들 간의 겹침 현상을 줄이기 위해 바디의 위치를 반복적으로 적용하는데, 코드에서 지정한 변수값에 의해 반복이 된다.

1
2
3
4
5
6
// 메뉴얼상의 권장값
int velocityIterations = 8;
int positionIterations = 3;
 
// Step : 물리 세계를 시물레이션함.
_world->Step(dt, velocityIterations, positionIterations);

 

dt변수는 Step메서드가 소요되는 시간ㅇ인데, coco2d-x의 schedule함수를 이용하면 평균 1.0f/60.0f의 값으로 넘겨진다.

그런데 고정값으로 1.0f/60.0f를 사용하지 않고 schedule함수를 통해 dt값을 따로 받는 이유는 애플리케이션의 논리적 타이밍과 물리적 타이밍을 맞추기 위해 dt 변수를 사용한다.

 

지금까지 만들어본 간단한 Box2D의 실행 모습이다.