2.1a 버전의 메뉴얼이 없어서 적당한 예제 문서가 없다보니 일단 간단하게나마, 각 항목별 예제 코드들을 모아보려고 합니다. 첫번째 예제는 body를 만드는 예제부터.. 만드는 절차는 필요 항목등 세세한 내용은 이미 User Manual 번역문에서 상세히 다뤘으니 못보신 분들은 그쪽을 먼저 참고 하시면 됩니다. (링크는 생략)

이번에 만들게 될 예제입니다.

마우스를 클릭해보시면 사각형이 만들어지고 중력을 받아 자유낙하하는 모습을 보실 수 있습니다. 핑크색 사각형은 깨어있는 상태의 body이고, 이는 시뮬레이션중인 상태라고 생각하시면 됩니다. 시뮬레이션이 필요없는 경우는 흑색으로 바뀝니다. sleep 상태라고 하죠. sleep 상태에서는 성능향상을 위해서 시뮬레이션 스탭에서 제외됩니다. 물론 다른 body의 영향을 받아 깨어날 수 있습니다. 마지막으로 녹색의 사각형들은 static type의 사각형이라고 보면 됩니다. 이는 움직이지 않고 스스로 어떠한 시뮬레이션도 하지 않는 물체라고 보시면 됩니다.

게임에서 빗대어 말하면 static은 지형이나 레벨일 것이고, dynamic은 캐릭터나 총알같은 움직이고 물리적 영향을 받는 것이라고 생각하면 편하겠네요.

이제 소스코드를 들여다 보겠습니다. 액션스크립트 프로젝트를 만들어 메인 클래스가 있을 것입니다. 사이즈는 500 x 400 에 framerate는 30 이라고 하겠습니다. 소스는 메인 클래스 1개 이외의 커스텀 클래스는 없다고 생각하시면 됩니다.

package {

import flash.display.Sprite;

[SWF(width="500", height="400", framerate="30")]

public class MyFirstBox2D extends Sprite {

public function MyFirstBox2D() {

super();

}

}

}


모든 시뮬레이션은 b2World 클래스 객체 단위로 이루어지므로 멤버 변수로 world 객체를 중력 벡터와 함께 선언해줍니다. 또한, 이번 예제에서는 무비클립같은 것들과 완전히 별개로 시뮬레이션만 느껴보기 위해서 디버그 드로잉을 사용할 것입니다. 이는 shape들을 시스템이 임의대로 생성해서 가시적으로 볼 수 있도록 해주는 기능입니다. 원래의 Box2D는 순수하게 물리 시뮬레이션만을 합니다. 때문에 화면에 그려지는것에 대한 것들은 전부 개발자의 몫입니다. 하지만 이 예제에서는 이 부분을 간편하게 디버그 드로잉에 의존할 뿐입니다.

private var world:b2World = new b2World(new b2Vec2(0, 20), true);

private var world_scale :Number = 30;


이렇게 메인 클래스의 멤버 변수로 두개를 선언 했습니다. 중력은 y축으로 20 인 벡터이고 sleep이 가능하게끔 설정했습니다. 또하나의 변수는 디버그 드로잉에 필요한 스케일값입니다. 드로잉이 아닌 시뮬레이션만 담당하는 라이브러리답게 Box2D는 내부적으로 미터(meter)법을 사용하기 때문에 실제로 필셀단위의 디스플레이와 정확하게 매칭 되지 않습니다. 이 스케일값은 디버그 디로잉에만 필요한 값이라고 생각하면 되겠습니다.

다음은 디버그 드로잉을 설정하는 초기화 함수입니다.

private function init():void {

var debug_draw:b2DebugDraw = new b2DebugDraw();

var debug_sprite:Sprite = new Sprite();

debug_draw.SetSprite(debug_sprite);

debug_draw.SetDrawScale(world_scale);

debug_draw.SetFlags(b2DebugDraw.e_shapeBit);

world.SetDebugDraw(debug_draw);

addChild(debug_sprite);

}


이 소스는 2.1a 소스에 포함되어있던 소스코드의 일부이기도 합니다. 디버그 드로잉을 사용하는데에 여기에서 크게 달라질 것이 없어서 일단 동일하게 사용했습니다.

캔버스 역할을 하는 하나의 Sprite 객체를 화면에 붙이고 그 객체를 디버그 드로잉 클래스에 등록하는 과정을 볼 수 있습니다. 앞서 선언했던 스케일값을 셋팅하는 코드도 포함되어있구요. Sprite 객체를 디버그 드로잉 클래스에 등록하고, 그 클래스 객체를 world 객체에 또 등록하네요. 이는 나중에 b2World.DrawDebugData 함수를 통해서 그려내기 위함입니다.

눈여겨볼만한 코드도 한 줄 또 있죠. b2DebugDraw.SetFlags 함수입니다. 이는 화면에 그려낼 정보를 셋팅하는 함수인데요.. b2DebugDraw 클래스의 레퍼런스 문서를 보시면 static 상수가 몇가지 정의되어 있는것을 보실 수 있습니다. 화면에 그릴 옵션들이죠. AABB를 그린다던지, 질량 중심을 그린다던지 그외 joint 나 컨트롤러같은것들까지 설정 할 수 있습니다. 이 예제에서는 body에 첨부된 shape만 그리는 것으로 셋팅되었습니다.

생성자에서 init 함수를 호출해줘야 합니다만 소스는 생략하겠습니다.

자 여기까지 했으면 일단 화면에 출력할 준비가 되었구요. 실제 출력하는 코드를 추가해 보겠습니다. 메인 클래스의 생성자에서 Event.ENTER_FRAME 이벤트 핸들러로 다음의 함수를 붙여줍니다.

private function onEnter(event:Event):void {

world.Step(1/30, 10, 10);

world.ClearForces();

world.DrawDebugData();

}


게임의 핵심 루프에 포함될 내용이 여기 있습니다. 바로 b2World.Step 함수인데요. 이는 world 내에 선언된 모든 정보를 바탕으로 지정해주는 시간만큼 반복해서 시뮬레이션 시키는 함수입니다. 사용자 메뉴얼에 time step을 보시면 더 상세한 내용을 보실 수 있습니다. 이 예제는 frameRate가 30이고 time step도 1/30 으로 지정했습니다. iteration은 10으로.

b2World.ClearForces 함수는 Step으로 시뮬레이팅 한 이후 호출하는 함수라고만 적혀있는데.. sub-step을 또 진행할 것이 아니라면 이 함수를 호출하라고 지시하는것을 보니 아마도 사용된 찌꺼기들 정리하는 느낌으로 보면 될것 같기도 하네요. 이것저것 뒤져보고 혹시 틀린거면 수정하겠습니다. 후훗. 마지막 함수는 말그대로 위에서 지정한 디버그 드로잉 객체에 그려주는 역할을 합니다. 첨부된 shape들을 그리도록 플래그를 설정했으니 아마도 아직까진 그려질것이 없는 상태겠네요.

여기까지는 아무것도 만들어진것도, 시뮬레이션 할 것도 없는 상황입니다. 단지 있다고 가정하고 화면에 그려주고 시뮬레이팅 할 환경만 만들었네요. 다음은 드디어 world 객체에 body를 만드는 함수입니다. world내에 필요한 포인터들은 모두 담겨 있으니 따로 body들의 포인터나 fixture의 포인터를 관리할 필요는 없겠습니다. 단지 b2World.CreateBody 함수의 호출로 만들어주면 world 객체가 내부적으로 관리하고 그려준다고 생각하면 됩니다.

private function createBox

(px:Number, py:Number, angle:Number,

w:Number, h:Number, isDynamic:Boolean):void

{

var _tempBody:b2Body;

var _tempBodyDef:b2BodyDef = newb2BodyDef();

var _tempFixtureDef:b2FixtureDef = newb2FixtureDef();

var _tempPolygonDef:b2PolygonShape = newb2PolygonShape();

_tempPolygonDef.SetAsBox(w/2/world_scale, h/2/world_scale);

_tempFixtureDef.shape = _tempPolygonDef;

_tempFixtureDef.friction = 0.5;

_tempFixtureDef.density = 1;


_tempBodyDef.position.Set(px / world_scale, py / world_scale);

_tempBodyDef.angle = angle / Math.PI * 2;

_tempBodyDef.type = isDynamic ? b2Body.b2_dynamicBody:b2Body.b2_staticBody;

_tempBody = world.CreateBody(_tempBodyDef);

_tempBody.CreateFixture(_tempFixtureDef);

}


사용자 메뉴얼에서도 나와있듯이, body는 world 객체를 통해서 생성합니다. 단지 그 CreateBody를 호출하기 위해선 definition을 지정해야 하고 그렇게 해서 만들어진 body 객체는 그 형태를 띄기 위해서는 fixture라고 하는 재질이나 속성을 다시 할당 해야 합니다. 그리고 기하적 정보는 fixture 객체의 안에 shape를 설정하는것으로 마무리 됩니다. 굉장히 복잡해 보이네요. 하지만 그나마 복잡한 실제 물체들을 시뮬레이팅 하기 위해서 속성을 추상화하는 구조체 치곤 심플하다고 할 수 있습니다.

나중에 다룰 joint와 constrain같은 것들이 추가되기 위해서 이런 구조를 필요로 한다고 생각하면 되겠죠. body를 말 그대로 신체라고 생각한다면 그 안에서 또 얼굴, 팔, 다리, 몸통 같이 세분화 해야할테구요. 그 각각은 속성이 달라질 수도 있으니까 말이죠.

어쨌든 이 함수에서는 생성하고자 하는 사각형의 좌표와 각도, 크기를 받아서 해당 위치에 원하는 크기로 생성해주고 마지막으로 필요한 경우 type을 dynamic 이나 static으로 설정해주는 역할을 합니다.

크기가 위치를 지정할 때 처음에 선언했던 world_scale 값을 꼬박꼬박 나눠서 사용하고 있는데 예상되다시피 디버그 드로잉을 할때 스캐일값만큼 키운것을 보상하기 위한 조치하고 생각하면 되겠습니다. 그 상관관계가 궁금하신 분들은 숫자를 이리 저리 바꾸면서 찾아보시면 되겠죠.

아! 빼먹을뻔 했네요. 잘 보시면 new 명령을 통햇 직접 생성하는 클래스는 Def들 밖에 없습니다. 실제 body와 fixture 는 world와 body에서 제공하는 함수를 통해서만 생성하신다고 생각하면 되겠네요.

이제 이 함수를 사용해서 지형을 만들고 마우스 클릭에 반응해서 동적으로 box들을 만들어보겠습니다. 마우스 클릭 이벤트를 처리할 함수입니다.

private function onMove(event:MouseEvent):void {

createBox(stage.mouseX,stage.mouseY,Math.random()*90, Math.random() * 35 + 15, Math.random() * 35 + 15,true);

}


플래시에 익숙하시다면 마우스 이벤트와 관련된 이야기는 굳이 말하지 않아도 되겠죠? 마우스 클릭이 있을때마다 마우스 위치에 임의의 크기와 각도록 dynamic body를 생성하는 코드입니다. 

그리고 다음은 지형을 만드는 코드죠. 이 코드는 생성자나 초기화 함수에 넣고 한번만 호출될 수 있도록 해야겠죠. 지형이 동적으로 생성될것이 아니라면.

createBox(250,200,0,100,10,false);

createBox(250,390,0,500,10,false);


보시다시피 마지막 매개변수가 false 입니다. static body로 만들라는거죠. static은 중력이나 다른 간섭을 받지 않습니다. 완전히 고정된 body가 됩니다.

자, 이제 최종 소스입니다.

package {

import Box2D.Collision.*;

import Box2D.Common.Math.b2Vec2;

import Box2D.Dynamics.*;


import flash.display.Sprite;

import flash.events.Event;

import flash.events.MouseEvent;

[SWF(width="500", height="400", framerate="30")]

public class MyFirstBox2D extends Sprite {

private var world:b2World = new b2World(new b2Vec2(0, 20), true);

private var world_scale:Number = 30;


public function MyFirstBox2D() {

super();

init();

stage.addEventListener(MouseEvent.CLICK, onMove);

stage.addEventListener(Event.ENTER_FRAME, onEnter);

}

private function init():void {

// debug draw setting

var debug_draw:b2DebugDraw = new b2DebugDraw();

var debug_sprite:Sprite = new Sprite();

debug_draw.SetSprite(debug_sprite);

debug_draw.SetDrawScale(world_scale);

debug_draw.SetFlags(b2DebugDraw.e_shapeBit);

world.SetDebugDraw(debug_draw);

addChild(debug_sprite);


//ground body (static)

createBox(250,200,0,100,10,false);

createBox(250,390,0,500,10,false);

}

private function onMove(event:MouseEvent):void {

createBox(stage.mouseX,stage.mouseY,Math.random()*90, Math.random() * 35 + 15, Math.random() * 35 + 15,true);

}


private function onEnter(event:Event):void {

world.Step(1/30, 10, 10);

world.ClearForces();

world.DrawDebugData();

}

private function createBox(px:Number, py:Number, angle:Number, w:Number, h:Number, isDynamic:Boolean):void {

var _tempBody:b2Body;

var _tempBodyDef:b2BodyDef = new b2BodyDef();

var _tempFixtureDef:b2FixtureDef = new b2FixtureDef();

var _tempPolygonDef:b2PolygonShape = new b2PolygonShape();

_tempPolygonDef.SetAsBox(w/2/world_scale, h/2/world_scale);

_tempFixtureDef.shape = _tempPolygonDef;

_tempFixtureDef.friction = 0.5;

_tempFixtureDef.density = 1;


_tempBodyDef.position.Set(px / world_scale, py / world_scale);

_tempBodyDef.angle = angle / Math.PI * 2;

_tempBodyDef.type = isDynamic ? b2Body.b2_dynamicBody:b2Body.b2_staticBody;

_tempBody = world.CreateBody(_tempBodyDef);

_tempBody.CreateFixture(_tempFixtureDef);

}

}

}



WRITTEN BY
buzzler

,