序
JUCEには物理エンジンライブラリ、box2Dが実装されているが、実際にどうやって使うの?と思っても、少なくともJUCEのリファレンスはないに等しく、サンプルも提供されておりません。
一方、box2D自体のサンプルは、OpenGLなどを使って結果を描写するというものはあっても、JUCEで結果をどう表示させるか?が見つからず、box2D未経験者としては困りました。
仕方なく、box2Dのサンプルを学び→JUCEでの実装を研究する、という手順を追いました。
マウスで画面をクリックすると四角いオブジェクトを生成し、それが重力に従って落下。
他のオブジェクトとぶつかりあって、あんなことやこんなことが起こるプログラムを作ろう...
破
重力を表現する単純なサンプルを使います。(box2DのHello World的なサンプルです。)
JUCEでbox2Dを使用する際は、Projucerでプロジェクト生成時にbox2Dにチェックを入れる必要があります。
まずはお決まりの"MainComponent"に以下のメソッドを追加し、box2Dの物理ワールドを作ります。
void MainComponent::initBox()
{
//重力を作ります。 (重力加速度 - 9.81)
b2Vec2 gravity(0.0, -9.81);
//物理ワールド生成
this->world = std::make_unique<b2World>(gravity);
//地面を設定する
b2BodyDef groundBodyDef;
//地面の位置
groundBodyDef.position.Set(0.0, -10);
//地面を作る(world内に保管される)
b2Body* groundBody = this->world->CreateBody(&groundBodyDef);
//地面のfixtureを設定する
b2PolygonShape groundBox;
//横2000m 高20m (数値は倍になる)
groundBox.SetAsBox(1000.0f, 10.f);
//地面を作る(world内に保管される)
groundBody->CreateFixture(&groundBox, 0.0f);// density
}
worldのCreateBodyで地面を生成することで、データはworldに保管されます。
これから、様々なオブジェクトを作って行きますが、全てworldに保管される形をとります。
このメソッドをコンストラクタで一度読んでください。
MainComponent::MainComponent()
{
setSize (600, 400);
initBox();
}
次に、どんどん追加していく四角いオブジェのクラスを作ります。
# include <JuceHeader.h>
class Box : public Component {
public:
Box(float scale);
~Box();
void init(b2World* world, Point<float> position, Point<float> dimensions);
void paintBox(Graphics& g, int height);
void updatePosition(int height);
Point<float> getDimensions() { return this->dimensions; }
private:
void paint(Graphics& g) override;
void resized() override;
float ratio = 1.0;
b2Body* body = nullptr;
b2Fixture* fixture = nullptr;
Point<float>dimensions;
};
この中でbox2Dに関係する箇所は initメソッドです。
void Box::init(b2World* world, Point<float> position, Point<float> dimensions)
{
this->dimensions = dimensions;
float hX = dimensions.x / 2.0;
float hY = dimensions.y / 2.0;
//ボディを定義
b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;
bodyDef.position.Set(position.x - hX, position.y + hY);
//ボディ作る
this->body = world->CreateBody(&bodyDef);
//形を定義
b2PolygonShape boxShape;
boxShape.SetAsBox(hX, hY);
//fixtureを定義
b2FixtureDef fixtureDef;
fixtureDef.shape = &boxShape;
fixtureDef.density = 1.0f;
fixtureDef.friction = 0.7f;
this->fixture = body->CreateFixture(&fixtureDef);
}
これでbox2Dの設定はおしまいです。
Q
次は、どうやって結果をJUCEで表示する?です。
実は、2通り試しました。
- Componentの座標をコントロールし、Component自体を動かすケース
- Graphicsで図形を描写し、その座標をコントロールするケース
個人的に、2の方が綺麗に動いているように思いました。
しかしながら、1の方が一つ一つのオブジェクトの挙動などを制御したり、マウスイベントなどを活用したりしやすく、どちらも捨てがたいように思います。
ここでは両方紹介したいと思います。
以下はGraphicsでペイントするケース
void Box::paintBox(Graphics& g, int height)
{
auto b = this->body->GetPosition();
g.setColour(Colours::white);
g.fillRect(b.x * scale,
(float)height - b.y * scale,
this->dimensions.getX() * scale,
this->dimensions.getY() * scale);
}
注意点としては、JUCEの座標系はトップが0、ボトムに向かうにつれて値が大きくなります。
box2Dは逆で、高い位置の値が大きく、地面方面に向かって値が小さくなります。
ですので、Componentの高さheightからy座標を引くことでy座標をadjustしています。
次に、以下はComponentのBoundsを制御するケース
ペイントのケースでは面倒でやっていないのですが、こちらではbox2Dによって吐き出される角度も実装しています。
void Box::updatePosition(int height)
{
auto b = this->body->GetPosition();
setBounds(b.x * scale,
(float)height - b.y * scale,
this->dimensions.getX() * scale,
this->dimensions.getY() * scale);
float angle = this->body->GetAngle();
Point<int> centrePos = Point<int>(getX() + getWidth()/2,
getY() + getHeight()/2);
setTransform(AffineTransform().translation(-centrePos.getX(), -centrePos.getY()).rotated(angle).translated(centrePos.getX(), centrePos.getY()));
}
角度の実装は少し工夫が必要で、オブジェクトの座標を回転の中心に持ってきた後、回転、元の位置に戻す、という工程が必要です。
その際、単純にgetX() getY()ではズレが生じ、ちゃんとオブジェクトのWidthとHeightも考慮する必要があります。(centrePosの箇所です。)
Repeat
次に、オブジェクトの生成方法を簡単に紹介します。
//MainComponent.h
std::vector<Box*> boxes;
//MainComponent.cpp
void MainComponent::createBox(int x, int y)
{
Box* newBox = new Box(this->box2DRatio);
newBox->init(this->world.get(),
Point<float>((float)x / this->box2DRatio,
(float)(getHeight() - y) / this->box2DRatio),
Point<float>(1, 1));
this->boxes.push_back(newBox);
addAndMakeVisible(newBox);
}
マウスをクリックするとオブジェクトが生成されるようにします。
//MainComponent.h
void mouseDown(const MouseEvent& e) override;
//MainComponent.cpp
void MainComponent::mouseDown(const MouseEvent& e)
{
auto pos = e.getPosition();
createBox(pos.getX(), pos.getY());
}
描写・座標変更はここ
座標を更新する際は、物理ワールドの更新が必要です。
今回は、フレームレート60 (float timeStep = 1.0 / 60.0)、int velocityIterations = 6, int positionIterations = 2で行っています。
//MainComponent.h
std::unique_ptr<b2World> world;
//MainComponent.cpp
void MainComponent::paint (juce::Graphics& g)
{
g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));
g.setFont (juce::Font (16.0f));
g.setColour (juce::Colours::white);
g.drawText ("Hello World!", getLocalBounds(), juce::Justification::centred, true);
this->world->Step(this->timeStep,
this->velocityIterations,
this->positionIterations);
for(auto box : this->boxes)
{
//box->paintBox(g, getHeight()); //どっちか片方ね
box->updatePosition(getHeight());
}
}
リピート
最後にTimerを継承して、リピートしましょう。
void MainComponent::timerCallback()
{
repaint();
}
私は startTimer(60); で行っています。
終わり
以上、雑で申し訳ないですが、ざっと書きました。
書き漏れ、不明瞭な箇所ありましたら、ご連絡いください。
可能な限りお答えします。
最後まで見ていただき、ありがとうございました。