Box2D は 2D 用物理演算ライブラリです。 これは最も人気のある2Dゲーム用物理演算ライブラリの1つです。多くの言語、およびlibgdxを含む多くのエンジンに移植されています。
libgdx でのBox2D 実装は、C++で書かれたエンジンを軽量のJava ラッパーで内包したものになっています。 そのため、素晴らしい内容の 公式の Box2D マニュアル (PDF)も役立つでしょう。
Box2Dに関する詳細なドキュメントについては、box2d.orgを参照してください。 libgdx に関連しないBox2D 固有の質問についてはそこのフォーラムで問い合わせるのがベストです。
libgdx 1.0以降 Box2D は拡張機能として扱われており、既定ではlibgdx に含まれなくなりました。 したがって、手動でインストールする必要があります。
Box2D を初期化するには、 Box2D.init()
を実行する必要があります。
後方互換性のため最初にWorld
を作成する方法でも同じようにできますが、Box2Dクラスを使用する方法を推奨します。
Box2D を設定する時、まず最初に必要なものは worldです。 world オブジェクトとは基本的には、物理的オブジェクト/ボディを全て保持してそれらの間で発生する作用を再現します。 ですが、オブジェクトの描写は行いません;描写を行うには、libgdx のグラフィック関数を使います。 そうは言っても、libgdx にはBox2D デバッグレンダラーが付属しています。これは物理シミュレーションをデバッグしたり、描写用コードを記述する前にゲームプレイををテストするのに非常に便利です。
world を作成するには、以下のコードを使用します:
World world = new World(new Vector2(0, -10), true);
一つ目の引数は重力の情報を含む2D ベクトルです: 0
は水平方向には重力はないことを示し、 -10は実生活と同じような下向きの力です(y軸は下から上に向かって伸びているものとします)。これらの値は好きに設定できますが、一定の尺度を厳守することを忘れないでください。Box2D では1ユニット = 1メートルです。
world 作成時の二つ目の引数は、オブジェクトをスリープ状態にするかどうかをworld に指示するためのboolean 値です。 一般的にはCPU使用率を節約するためにオブジェクトをスリープさせたいでしょうが、オブジェクトをスリープさせたくない状況もあります。
Box2D で使用しているのと同じ尺度でグラフィックを描写することを推奨します。 つまり、Sprite の横幅/高さの描写はメートル単位で行います。 グラフィックを拡大表示するには、メートル単位でviewportWidth / viewportHeightの設定をしたカメラを使用してください。 例: 横幅が2.0f (2 メートル)のSprite を描写してviewportWidthが20.0fのカメラを使用すると、そのSprite はウィンドウの横幅の1/10を占めます。
次にすることは、デバッグレンダラーの設定です。 一般的にゲームのリリース版ではこれを使用せず、テスト目的のために使用します。設定は以下のようにします:
Box2DDebugRenderer debugRenderer = new Box2DDebugRenderer();
シミュレーション状況を最新の状態に更新するには、world に対してステップ処理を指示する必要があります。
基本的には、ゲーム中はずっとステップ処理でworld オブジェクトを更新し続けます。
step 関数を実行する最適な場所は、 render()
ループ内の最後の行です。
完璧な世界において、あらゆる物のフレームレートは全て同じになります。
world.step(1/60f, 6, 2);
一つ目の引数は、タイムステップもしくはworld で再現させたい時間です。
ほとんどの場合、これは固定タイムステップにしたいでしょう。
libgdx では、1/45f
(1/45秒) か 1/300f
(1/300秒)の使用を推奨しています。
他の二つの引数は、 velocityIterations
と positionIterations
です。
今のところこれらは6
と2
のままにしておきますが、詳細についてはBox2D ドキュメントで読むことができます。
Stepping your simulation is a topic unto itself. 可変時間ステップの使用に関する役立つ議論については、この記事を参照してください。
結果は以下のようになります:
private float accumulator = 0; private void doPhysicsStep(float deltaTime) { // fixed time step // max frame time to avoid spiral of death (on slow devices) float frameTime = Math.min(deltaTime, 0.25f); accumulator += frameTime; while (accumulator >= Constants.TIME_STEP) { WorldManager.world.step(Constants.TIME_STEP, Constants.VELOCITY_ITERATIONS, Constants.POSITION_ITERATIONS); accumulator -= Constants.TIME_STEP; } }
物理演算のステップ処理を実行する前に全てのグラフィック描写を行うことを推奨します。でないと、同期が外れてしまいます。 デバッグレンダラーでこれを行うには、以下のようにします。:
debugRenderer.render(world, camera.combined);
一つ目の引数は Box2D のworld で、二つ目の引数は libgdx のカメラです。
今の状態でゲームを実行しても、何も起こらないとても退屈なものになるでしょう。 world のステップ処理を行ったとしても、操作するものが何もないので何も表示されません。 そのため、ここではいくつかオブジェクトを追加します。
Box2D においてオブジェクトは ボディと呼ばれ、各ボディは1つ以上のフィクスチャによって構成されています。フィクスチャは、ボディ内における固定の位置情報と向き情報を持っています。 フィクスチャをあなたが望む任意の形状にしたり、異なる形状の様々なフィクスチャを組み合わせて望む形状を作ったりできます。
フィクスチャは自身に紐づいている形状、密度、摩擦、反発力といった情報を持っています。 形状については文字通りの意味です。 密度は平方メートル当たりの質量です:ボーリングのボールは密度が非常に高いですが、風船は主に空気で満たされているため密度は非常に低いです。 摩擦は、オブジェクトを何かに対して擦ったり滑ったりした時に発生する逆向きの力の量です:氷のブロックは摩擦が非常に低いですが、ゴムボールは摩擦が高いです。 反発力は、それがどれくらい弾むかです: 岩は反発力がとても低いですが、バスケットボールは反発力がとても高いです。 反発力が0のボディは地面に当たるとすぐに止りますが、反発力が1のボディは同じ高さを保ちながら永久にバウンスしつづけます。
ボディには三つのタイプ: dynamicと kinematic と staticがあります。各タイプについては以下で説明します。
ダイナミックボディとは、動き回ったり、力やその他動的ボディやキネマティックボディやスタティックボディの影響を受けたりするオブジェクトのことです。 ダイナミックボディは動いたり力の影響を受けたりするオブジェクトに適しています。
ボディを構成するフィクスチャについてはもう学んだので、実際にやってみましょう。いくつかボディを作成してそれにフィクスチャを追加してみます!
// First we create a body definition BodyDef bodyDef = new BodyDef(); // We set our body to dynamic, for something like ground which doesn't move we would set it to StaticBody bodyDef.type = BodyType.DynamicBody; // Set our body's starting position in the world bodyDef.position.set(100, 300); // Create our body in the world using our body definition Body body = world.createBody(bodyDef); // Create a circle shape and set its radius to 6 CircleShape circle = new CircleShape(); circle.setRadius(6f); // Create a fixture definition to apply our shape to FixtureDef fixtureDef = new FixtureDef(); fixtureDef.shape = circle; fixtureDef.density = 0.5f; fixtureDef.friction = 0.4f; fixtureDef.restitution = 0.6f; // Make it bounce a little bit // Create our fixture and attach it to the body Fixture fixture = body.createFixture(fixtureDef); // Remember to dispose of any shapes after you're done with them! // BodyDef and FixtureDef don't need disposing, but shapes do. circle.dispose();
ここではオブジェクトとしてボールを作成してworldに追加しています。 このゲームを実行すると、落ちるボールが画面に表示されます。 操作する余地が一切ないので、これではまだとても退屈です。 それでは、ボールがバウンドするための床を作成しましょう。
スタティックボディとは、動かずに力の影響も受けないオブジェクトのことです。 ダイナミックボディはスタティックボディの影響を受けます。スタティックボディは地面や壁や移動する必要がないオブジェクトに最適です。 スタティックボディは必要な計算能力が少なくて済みます。
続いて、床をスタティックボディとして作成してみましょう。 これは先ほどやったダイナミックボディの作成とほとんど同じです。
// Create our body definition BodyDef groundBodyDef = new BodyDef(); // Set its world position groundBodyDef.position.set(new Vector2(0, 10)); // Create a body from the defintion and add it to the world Body groundBody = world.createBody(groundBodyDef); // Create a polygon shape PolygonShape groundBox = new PolygonShape(); // Set the polygon shape as a box which is twice the size of our view port and 20 high // (setAsBox takes half-width and half-height as arguments) groundBox.setAsBox(camera.viewportWidth, 10.0f); // Create a fixture from our polygon shape and add it to our ground body groundBody.createFixture(groundBox, 0.0f); // Clean up after ourselves groundBox.dispose();
FixtureDef
を定義する必要なしにフィクスチャを作成しているのが分かりますか?
形状と密度だけを設定すればいい場合、createFixture
メソッドにはそのための便利なオーバーロードメソッドがあります。
これでゲームを実行すると、ボールが落ちて新たに作成した地面でバウンドする様子が表示されます。 ボールの密度や反発力といった値を異なる値に設定して実行し、どうなるかを見てみてください。
キネマティックボディとは、スタティックボディとダイナミックボディの中間のようなものです。 スタティックボディと同様に力を加えても反応しませんが、ダイナミックボディと同様に移動することができます。 キネマティックボディは、プラットフォームゲームにおける移動する足場のように、あなた(プログラマー)がボディの動きを完全に制御したい場合に最適です。
キネマティックボディの位置を直接設定することは可能ですが、そうではなく通常は速度を設定してBox2Dに位置情報の更新を任せるのが良いでしょう。
前述のダイナミックボディとスタティックボディと全く同じ方法でキネマティックボディを作成できます。作成したら、以下のようにして速度を制御できます:
// Move upwards at a rate of 1 meter per second kinematicBody.setLinearVelocity(0.0f, 1.0f);
Impulses および Forces は、重力や衝突だけではなくボディを移動させるのにも使用されます。
Forcesは時間経過とともに徐々に発生し、ボディの速度を変化させます。 例えば、ロケットはゆっくりと加速を始めるので、ロケットの打ち上げ処理ではゆっくりと力が適用されます。
一方Impulses は即座にボディの速度を変化させます。 例えば、パックマンをプレイするとキャラクターは常に一定のスピードで移動し、移動速度は即座に上がります。
まず最初に、forces/impulsesを適用するダイナミックボディが必要です。上記ダイナミックボディの項目を参照してください。
Forceを適用する
Forces は世界点においてニュートン単位で適用されます。force が物体の中心へ適用されなかった場合、トルクが発生して角速度に影響を与えます。
// Apply a force of 1 meter per second on the X-axis at pos.x/pos.y of the body slowly moving it right dynamicBody.applyForce(1.0f, 0.0f, pos.x, pos.y, true); // If we always want to apply force at the center of the body, use the following dynamicBody.applyForceToCenter(1.0f, 0.0f, true);
Impulseを適用する
Impulses は、ボディの速度を即座に変更するという点を除けばForcesと同じです。 forcesと同様に、impulseが物体の中心へ適用されなかった場合、トルクが発生して角速度が変更されます。 Impulsesはニュートン秒単位、もしくは kg-m/s単位で適用されます。
// Immediately set the X-velocity to 1 meter per second causing the body to move right quickly dynamicBody.applyLinearImpulse(1.0f, 0, pos.x, pos.y, true);
forces や impulses を適用するとボディはスリープ状態が解除されるということを覚えておいてください。 時には、この動作は望ましくない場合もあります。 例えば、安定したforceを適用していて、パフォーマンスを向上させるためにボディをスリープ状態にしたい場合です。 この場合、boolean値であるwake引数にfalseを設定します。
// Apply impulse but don't wake the body dynamicBody.applyLinearImpulse(0.8f, 0, pos.x, pos.y, false);
プレイヤー移動の例
この例では、ソニック・ザ・ヘッジホックのように、プレイヤーを左右に走らせて最大速度まで加速させます。 この例の場合、既に'player'という名前のダイナミックボディを作成しています。 さらにMAX_VELOCITY変数を定義しているので、プレイヤーはこの値を超えて加速することはありません。 今のところはキーを押した時にlinear impulseを適用するだけです。
Vector2 vel = this.player.body.getLinearVelocity(); Vector2 pos = this.player.body.getPosition(); // apply left impulse, but only if max velocity is not reached yet if (Gdx.input.isKeyPressed(Keys.A) && vel.x > -MAX_VELOCITY) { this.player.body.applyLinearImpulse(-0.80f, 0, pos.x, pos.y, true); } // apply right impulse, but only if max velocity is not reached yet if (Gdx.input.isKeyPressed(Keys.D) && vel.x < MAX_VELOCITY) { this.player.body.applyLinearImpulse(0.80f, 0, pos.x, pos.y, true); }
全てのjointは、box2d worldで作成する前に定義を設定しておく必要があります。 initializeメソッドを使うと、全ての jointパラメータを漏れなく設定するのに役立ちます。
ボディを破棄した後にjointを破棄するとクラッシュするので注意してください。 ボディを破棄するとそれに繋がっているjointsも破棄されます。
DistanceJointDef defJoint = new DistanceJointDef (); defJoint.length = 0; defJoint.initialize(bodyA, bodyB, new Vector2(0,0), new Vector2(128, 0)); DistanceJoint joint = (DistanceJoint) world.createJoint(defJoints); // Returns subclass Joint.
Distance joint は、ボディ間の距離を一定にします。
Distance joint の定義では両方のボディのアンカーポイントを定義する必要があり、distance jointのlength 値は0にはできません。
この定義ではローカルアンカーポイントを使用するため、初期設定時にこの制約に少し違反する可能性があります。 これはゲームの保存や読み込みをする時に役立ちます。
lengthには0や小さい値を使用しないでください!
// DistanceJointDef.initialize (Body bodyA, Body bodyB, Vector2 anchorA, Vector2 anchorB) DistanceJointDef defJoint = new DistanceJointDef (); defJoint.length = 0; defJoint.initialize(bodyA, bodyB, new Vector2(0,0), new Vector2(128, 0));
Friction joint is used for top-down friction. 2D の並進摩擦と角摩擦を提供します。
FrictionJointDef jointDef = new FrictionJointDef (); jointDef.maxForce = 1f; jointDef.maxTorque = 1f; jointDef.initialize(bodyA, bodyB, anchor);
gear joint は2つのjoints を1つに繋げるのに使用します。 どちらの joint も revolute joint や prismatic jointにすることができます。 You specify a gear ratio to bind the motions together: coordinate1 + ratio * coordinate2 = constant The ratio can be negative or positive. If one joint is a revolute joint and the other joint is a prismatic joint, then the ratio will have units of length or units of 1/length.
GearJointDef jointDef = new GearJointDef (); // has no initialize
motor joint は二つのボディ間の相対運動を制御するために使用されます。主な用途は、地面に対するダイナミックボディの動きを制御することです。
MotorJointDef jointDef = new MotorJointDef (); jointDef.angularOffset = 0f; jointDef.collideConnected = false; jointDef.correctionFactor = 0f; jointDef.maxForce = 1f; jointDef.maxTorque = 1f; jointDef.initialize(bodyA, bodyB);
mouse joint は、マウスを使ってボディを操作するテストベッドで使用されます。 It attempts to drive a point on a body towards the current position of the cursor. 回転に制限はありません。
MouseJointDef jointDef = new MouseJointDef(); jointDef.target = new Vector2(Gdx.input.getX(), Gdx.input.getY()); MouseJoint joint = (MouseJoint) world.createJoint(jointDef); joint.setTarget(new Vector2(Gdx.input.getX(), Gdx.input.getY()));
prismatic joint を使うと、特定の軸に沿った二つのボディの相対移動が可能になります。 prismatic joint は相対回転を防ぎます。従って、 prismatic joint は1自由度を持ちます。
PrismaticJointDef jointDef = new PrismaticJointDef (); jointDef.lowerTranslation = -5.0f; jointDef.upperTranslation = 2.5f; jointDef.enableLimit = true; jointDef.enableMotor = true; jointDef.maxMotorForce = 1.0f; jointDef.motorSpeed = 0.0f;
PulleyJointは理想の滑車を作成するのに使われます。 PulleyJointでは二つのボディを地面およびお互い同士と繋ぎます。 1つのボディが上がるともう一方のボディは下がります。 滑車のロープの全長は、初期設定に従って保たれます。
JointDef jointDef = new JointDef (); float ratio = 1.0f; jointDef.Initialize(myBody1, myBody2, groundAnchor1, groundAnchor2, anchor1, anchor2, ratio);
revolute joint は、二つのボディに共通のアンカーポイント(しばしばヒンジポイントと呼ばれます)を共有させます。 The revolute joint has a single degree of freedom: the relative rotation of the two bodies. これは関節角と呼ばれます
RevoluteJointDef jointDef = new RevoluteJoint(); jointDef.initialize(bodyA, bodyB, new Vector2(0,0), new Vector2(128, 0)); jointDef.lowerAngle = -0.5f * b2_pi; // -90 degrees jointDef.upperAngle = 0.25f * b2_pi; // 45 degrees jointDef.enableLimit = true; jointDef.enableMotor = true; jointDef.maxMotorTorque = 10.0f; jointDef.motorSpeed = 0.0f;
rope jointは、二つのボディにおける二点間の最大距離を決定します。 それ以外の効果はありません。警告: シミュレーション中にこの最大長を変更しようとした場合、物理的ではない挙動が発生します。 A model that would allow you to dynamically modify the length would have some sponginess, so I chose not to implement it that way. 長さを動的に制御したい場合は、b2DistanceJointを参照してください。
RopeJointDef jointDef = new RopeJointDef (); // has no initialize
weld joint は、本質的には二つのボディを一つに繋げます。 A weld joint may distort somewhat because the island constraint solver is approximate.
WeldJointDef jointDef = new WeldJointDef (); jointDef.initialize(bodyA, bodyB, anchor);
ホイールジョイント。このジョイントは2自由度を提供します: translation along an axis fixed in bodyA and rotation in the plane. You can use a joint limit to restrict the range of motion and a joint motor to drive the rotation or to model rotational friction.このジョイントは車両のサスペンション用に設計されています。
WheelJointDef jointDef = new WheelJointDef(); jointDef.maxMotorTorque = 1f; jointDef.motorSpeed = 0f; jointDef.dampingRatio = 1f; jointDef.initialize(bodyA, bodyB, anchor, axis); // axis is Vector2(1,1) WheelJoint joint = (WheelJoint) physics.createJoint(jointDef); joint.setMotorSpeed(1f);
Coming soon
スプライトやゲームオブジェクトとBox2D 間のリンクを管理する最も簡単な方法は、Box2Dのユーザーデータを使用することです。 ゲームオブジェクトにユーザーデータを設定し、Box2D ボディに基づいてそのオブジェクトの位置を更新できます。
ボディのユーザーデータを設定するのは簡単です
body.setUserData(Object);
これはどんな Java オブジェクトにも設定できます。 自身の物理ボディへの参照を設定できる、ゲーム独自のactor/objectクラスを作成するのも良いでしょう。
同様に、フィクスチャにもユーザーデータを設定できます。
fixture.setUserData(Object);
使用する全てのactors/spritesを最新の状態に更新するには、game/renderループ内でworldが持つ全てのボディをループ処理することで簡単にできます。
// Create an array to be filled with the bodies // (better don't create a new one every time though) Array<Body> bodies = new Array<Body>(); // Now fill the array with all bodies world.getBodies(bodies); for (Body b : bodies) { // Get the body's user data - in this example, our user // data is an instance of the Entity class Entity e = (Entity) b.getUserData(); if (e != null) { // Update the entities/sprites position and angle e.setPosition(b.getPosition().x, b.getPosition().y); // We need to convert our angle from radians to degrees e.setRotation(MathUtils.radiansToDegrees * b.getAngle()); } }
そして、いつものようにlibgdx のSpriteBatch
を使ってスプライトを描写してください。
センサーとは、衝突時(力を適用した場合のような)に勝手に反応しないボディのことです。 これは、二つの形状が衝突した時に発生する事象を完全に制御する必要がある場合に便利です。 例えば、何らかの種類の円形の視界を持ったドローンを考えてみてください。 視界用のボディはドローンに追従して動きますが、ドローンやその他ボディと接触したとしても物理的な反応をしてはいけません。 自身の形状内にターゲットが入った時、それを検知するだけです。
ボディがセンサーになるよう設定するには、'isSensor'フラグにtrueを設定します。以下が例です:
//At the definition of the Fixture fixtureDef.isSensor = true;
このセンサーの接触を検知するには、ContactListenerインタフェースメソッドを実装する必要があります。
Contact Listeners は特定のフィクスチャの衝突イベントを検知します。 検知用のメソッドではContactオブジェクトが渡されます。これには衝突した二つのボディに関する情報が含まれています。 beginContact メソッドは、オブジェクトが他のオブジェクトと重なった時に呼び出されます。 オブジェクトの衝突が終わると、endContactメソッドが呼び出されます。
public class ListenerClass implements ContactListener { @Override public void endContact(Contact contact) { } @Override public void beginContact(Contact contact) { } };
このクラスは、画面のshow()メソッドやinit()メソッド内でworldのコンタクトリスナーとして設定する必要があります。
world.setContactListener(ListenerClass);
接触したフィクスチャからボディに関する情報を取得できます。 アプリケーションの設計によっては、BodyやFixtureのユーザーデータ内のEntityクラスを参照形式にして、Contactメソッド内からそれを使っていくつかの変更を行うことができます(例えば、プレイヤーの健康状態を変更するなど)。
Coming soon
本当に素晴らしいBox2D の情報がたくさんあり、そのコードのほとんどは簡単にlibgdx用へ変換できます。
以下は、box2d とlibgdxで使用するツールの一覧です: