サイトのトップへ戻る

libGDX ドキュメント 日本語訳

サイト内検索

libGDX 3D 物理演算のBullet ラッパーを使用する - パート1

多くの 3D ゲームでは、3Dオブジェクト間でなんらかの衝突判定が必要です。 場合によっては、いくつかの数学とバウンディングボックスとバウンディングスフィアを使ってこの衝突判定を行うことが可能です。 しかし形状がより複雑になった場合は、同様に使用する数学とコードもより複雑になります。 幸い、 LibGDX には Bullet周りのラッパーが用意されています。 Bullet はオープンソースの3Dの衝突判定および物理演算用ライブラリで、たった数行のコードだけで衝突判定を追加することができます。 このチュートリアルでは、LibGDX の3D 物理演算 Bullet ラッパーの使い方について説明します。

C++で記述されているので、Bullet ライブラリは本当に素晴らしいパフォーマンスで動作します。 これは多くの商用ゲームや映画で使用されています。 しかし、C++で記述されているということは同時に問で題もあります。 LibGDXではコードをJavaで記述しており、Java内から直接C++ライブラリを使用することはできません。 実際、この二つの言語のデザインは非常に異なっており、多くの場合パフォーマンスを低下させずに1対1で翻訳を行うことは不可能です。 そこで "ラッパー"の登場です。 ラッパーとは、BulletライブラリとあなたのJavaアプリケーションとの間に入るレイヤーで(もしくは"ブリッジ"と言い換えてもいいです)、パフォーマンスの維持を行っています。 このため、ラッパーではBullet APIに対していくつかの変更を追加します。

あなたの想像の通り、ラッパーを使った作業は通常のJavaを使った作業とは少し異なります。 このチュートリアルでは、あなたがラッパーとBulletライブラリの両方についてあまり知らないものとして説明を行います。 既にBulletライブラリを使用した経験がある場合でも、このチュートリアルはまだ役に立つかもしれません。 しかし、この wiki ページには目を通しておいた方が良いでしょう。このラッパーに関する重要な変更点について簡単に説明しています。

このチュートリアルでは、あなたが既にLibGDXとその3D apiについてよく理解しているものとして説明をしていきます。 (最初のページから読み始めることが)必須ではありませんが、これまでのチュートリアルを読んでいない場合は、このチュートリアルを読む前に読んでおくことをお勧めします。 特に、以前の視錐台カリングray の取得衝突形状 のチュートリアルが衝突検知へとつながっていきます。

今回は二部構成のチュートリアルになっています。 この第一部では、Bulletラッパーを設定して基本的な衝突判定に使用する方法について見ていきます。 第二部では剛体力学について見ていきます。 私は舞台裏で起こっていることに関する基本的な知識を持つことは大事だと思っているので、 衝突判定を実行した時に実際に何が起こっているのか、どうすればBulletラッパーを使ってパフォーマンスを維持できるのか、といったことを説明していきます。



基本的な3D テストシーンを作成する

それでは、コーディングを開始しましょう! このままLibGDX のプロジェクトを新規に作成します。あなたがプロジェクトの作成について既に理解しているものとして、ここでその説明はしません。 セットアップツールを使用している場合は、gdx-bullet拡張を追加します。 使用していない場合は、後で手動でgdx-bullet拡張を追加する必要があります。 全てのセットアップが完了したら、ApplicationListenerとなるクラスを作成します:


public class BulletTest implements ApplicationListener {
    @Override public void create () {}
    @Override public void render () {}
    @Override public void dispose () {}
    @Override public void pause () {}
    @Override public void resume () {}
    @Override public void resize (int width, int height) {}
}

今度は、 CameraやCameraInputControllerや ModelBatchやEnvironment のような、3Dテストに必要な基本的なものを追加します。


public class BulletTest implements ApplicationListener {
    PerspectiveCamera cam;
    CameraInputController camController;
    ModelBatch modelBatch;
    Array<ModelInstance> instances;
    Environment environment;

    @Override
    public void create () {
        modelBatch = new ModelBatch();

        environment = new Environment();
        environment.set(new ColorAttribute(ColorAttribute.AmbientLight, 0.4f, 0.4f, 0.4f, 1f));
        environment.add(new DirectionalLight().set(0.8f, 0.8f, 0.8f, -1f, -0.8f, -0.2f));

        cam = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
        cam.position.set(3f, 7f, 10f);
        cam.lookAt(0, 4f, 0);
        cam.update();

        camController = new CameraInputController(cam);
        Gdx.input.setInputProcessor(camController);

        instances = new Array<ModelInstance>();
    }

    @Override
    public void render () {
        camController.update();

        Gdx.gl.glClearColor(0.3f, 0.3f, 0.3f, 1.f);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);

        modelBatch.begin(cam);
        modelBatch.render(instances, environment);
        modelBatch.end();
    }

    @Override
    public void dispose () {
        modelBatch.dispose();
    }

    @Override public void pause () {}
    @Override public void resume () {}
    @Override public void resize (int width, int height) {}
}

これらがあなたにとって初めて見るものだった場合は、まずはこのチュートリアルを読むことをお勧めします。

それでは、衝突判定に使用する視覚オブジェクトをいくつか追加します。


public class BulletTest implements ApplicationListener {
    ...
    Model model;
    ModelInstance ground;
    ModelInstance ball;

    @Override
    public void create () {
        ...
        camController = new CameraInputController(cam);
        Gdx.input.setInputProcessor(camController);

        ModelBuilder mb = new ModelBuilder();
        mb.begin();
        mb.node().id = "ground";
        mb.part("box", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, new Material(ColorAttribute.createDiffuse(Color.RED)))
            .box(5f, 1f, 5f);
        mb.node().id = "ball";
        mb.part("sphere", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, new Material(ColorAttribute.createDiffuse(Color.GREEN)))
            .sphere(1f, 1f, 1f, 10, 10);
        model = mb.end();

        ground = new ModelInstance(model, "ground");
        ball = new ModelInstance(model, "ball");
        ball.transform.setToTranslation(0, 9f, 0);

        instances = new Array<ModelInstance>();
        instances.add(ground);
        instances.add(ball);
    }
    ...
    @Override
    public void dispose () {
        modelBatch.dispose();
        model.dispose();
    }
    ...
}

上記コードでは、 ModelBuilder を使って二つのノードを持つモデルを作成します。 一つ目のノードは "ground" という名前で、もう一つは "ball" という名前です(ノードに関する詳細を知りたい場合は、このチュートリアル を読んでください)。 次に各Node のModelInstance を作成し (このチュートリアルで説明したように) 、地面の少し上にボールを移動させます。 これを実行すると以下のようになります:

bullet1

今度は、地面に衝突するまでボールを下へ移動させます。 今のところ、衝突判定にはスタブ(テスト用の仮メソッド)を使用しています:


public class BulletTest implements ApplicationListener {
    ...
    boolean collision;
    ...
    @Override
    public void render () {
        final float delta = Math.min(1f/30f, Gdx.graphics.getDeltaTime());

        if (!collision) {
            ball.transform.translate(0f, -delta, 0f);
            collision = checkCollision();
        }
        ...
    }

    boolean checkCollision() {
        return false;
    }
    ...
}

githubにある全ソースコードを参照してください

collisionという名前のフラグを追加しました。 このフラグがfalseである限り、ボールは1秒間に1単位(例えばメートルなど)の速度で下に移動します。 このためにはGdx.graphics.getDeltaTime() メソッドを使用します。 これを使って最後にrenderメソッドが呼び出されてからこれまでに経過した時間(秒単位)を取得できます。 Math.minを使用してこの時間の最大値を制限しています。 これは "テレポーテーション"を防ぐためです。例えば、何らかの理由で一時停止が発生した時などの。

これを実行すると、ボールがただ落ちるだけで地面に着いても止らないことが分かります。

bullet2



ボールが地面に衝突したかどうかを検知する

実際の衝突判定をいくつか追加しましょう。 まだの場合はgdx-bullet拡張をプロジェクトに追加するようにしてください。 LibGDX セットアップツールで bullet 拡張のチェックボックスをクリックすることで追加できます。 望むのであれば、その拡張を手動で追加することもできます(例えば、jarファイルをjavaのビルドパスに追加して.soファイルを android のlibsフォルダーに追加するなどして)。

Bullet ラッパーについて覚えておくべき最初のことは、初期化するには決してラッパーを使うことはできないということです。 初期化は static メソッド Bullet.init();を使って行えます:


    @Override
    public void create () {
        Bullet.init();
        ...
    }

初期化しないとラッパーが使えないというのは当たり前だと思うかもしれませんが、createメソッドが呼び出される前に(static)メンバが初期化されるということを忘れないでください。 例えば、以下の擬似コードは正常に動作しません:


SomeBulletClass member = new SomeBulletClass();

以下の擬似コードも動作しません:


SomeBulletCallback callback = new SomeBulletCallback() {
    public void theCallback() {
        ...
    }
}

なぜなら、create ()メソッド内のBullet.init();の呼び出しが実行される前に、このコード(コンストラクタ)が呼び出されるからです。 そのため、Bullet 関連のオブジェクトは常に、Bullet ラッパーが初期化されたでインスタンスの作成を行うようにしてください。

上記の行(Bullet.init();)を追加したら、このプロジェクトを実行してライブラリが正常に初期化されることを確認すると良いでしょう。 何らかの理由でライブラリ(例えば windows のDLL ファイルなど)が読み込めない場合は、例外が投げられます。 その場合はプロジェクト構成を確認するか、libgdx セットアップユーティリティを使ってプロジェクトの再作成をしてみてください。

ボールが地面に衝突しているかどうかを確認する前に、それらオブジェクト両方のshape を設定する必要があります。 前回のチュートリアルを読んだのであれば、既に衝突形状について理解していることでしょう。 Bullet では多くの衝突形状が用意されています。 箱型や球体や円柱や円錐やカプセル型のような基本形状から、より一般的な凸形状や最適化された凸包形状やメッシュ形状やそれらの任意の組み合わせまであります。 今のところは、シンプルな箱型や球体だけでテストには十分です:


public class BulletTest implements ApplicationListener {
    ...
    btCollisionShape groundShape;
    btCollisionShape ballShape;

    @Override
    public void create () {
        Bullet.init();
        ...
        ballShape = new btSphereShape(0.5f);
        groundShape = new btBoxShape(new Vector3(2.5f, 0.5f, 2.5f));
    }
    ...
}

btCollisionShape は全ての shapeの基底クラスです。 ボールの場合は btSphereShapeを作成します。これは引数として半径を受け取ります。 ボールの直径は1ユニットなので、半径は 0.5fです。 地面の場合はbtBoxShapeを作成します。これは引数として広さの半分の値を受け取ります。 地面は横幅が5ユニットで高さが1ユニットで深さが5ユニットなので、これらの値を半分にしてから渡す必要があります。 Vector3を使って引数を渡します。

Vector3 は LibGDX のクラスなので注意してください。 Bulletでこれと同等の機能を持つクラスは btVector3 です(これはラッパー内でも使用可能です)。 可能であれば、ラッパーはLibGDXのmath クラスとBullet のmath クラスの間の橋渡しをします。 その場合、あなたに代わっていくつかの最適化処理さえも行います(詳細はのちほど説明します)。

衝突判定を行うには衝突形状だけでは不十分です。 各形状の位置情報(と回転情報)をBullet に通知する必要があるのです。 この通知は衝突オブジェクトを使って行います。:


public class BulletTest implements ApplicationListener {
    ...
    btCollisionObject groundObject;
    btCollisionObject ballObject;

    @Override
    public void create () {
        ...
        groundObject = new btCollisionObject();
        groundObject.setCollisionShape(groundShape);
        groundObject.setWorldTransform(ground.transform);

        ballObject = new btCollisionObject();
        ballObject.setCollisionShape(ballShape);
        ballObject.setWorldTransform(ball.transform);
    }

    @Override
    public void render () {
        ...
        if (!collision) {
            ball.transform.translate(0f, -delta, 0f);
            ballObject.setWorldTransform(ball.transform);

            collision = checkCollision();
        }
        ...
    }
    ...
}

ご覧の通り、衝突オブジェクトとは、衝突形状とそのtransform情報を組み合わせただけです。 今回の場合のtransform 情報は、位置情報と回転情報です。 BoxShapeを作成した際にVector3を使用したのと同様に、ModelInstanceのメンバtransformを使ってtransform 情報を設定できます。 ラッパーは我々に代わって、この Matrix4をbulletにおける同等のクラスであるbtTransformに変換します。 この操作は簡単に行えますが、(bulletに関して言えば)このtransform 情報には位置情報と回転情報しか含まれていないということは忘れないでください。 他のtransformation情報(例えばスケール情報のような)はサポートされていません。 実際のところ、bullet を使用する時は決してスケール情報を直接オブジェクトに適用すべきではないということです。 オブジェクトの拡大/縮小を行うには他にも方法がありますが、一般的には拡大/縮小は避けるようにすることをお勧めします。

オブジェクトが二つあり、それらが衝突しているかを検知したい、というのが今の状況です。 実際の衝突判定を始める前に、いくつかのヘルパークラスが必要です。


public class BulletTest implements ApplicationListener {
    ...
    btCollisionConfiguration collisionConfig;
    btDispatcher dispatcher;

    @Override
    public void create () {
        ...
        collisionConfig = new btDefaultCollisionConfiguration();
        dispatcher = new btCollisionDispatcher(collisionConfig);
    }
    ...
}

これらのオブジェクトの重要性は後で見ていくとして、今のところはただcreate メソッド内でそれらのインスタンスを作成するようにしてください。

そろそろ、今までに出てきた全てのbullet のクラスは頭に"bt"が付いていることに気づいたかもしれません。常にそうなるわけではありませんが、ほとんどの場合はそうなります。

java上でbullet のクラスのインスタンスを作成する度に、ラッパーもネイティブ(C++)ライブラリ上で同じクラスのインスタンスを作成します。 しかし、javaではガベージコレクターがメモリ管理を行って不要になったオブジェクトを解放しますが、C++ではメモリの解放をあなた自身が責任を持って行う必要があります。 同じことが Texture, Model, ModelBatch, Shaderなどの場合でもあったので、この考え方についてはおそらく既に理解しているでしょう。 そのため、オブジェクトが不要になった際にはあなたが手動で破棄する必要があります。


    @Override
    public void dispose () {        
        groundObject.dispose();
        groundShape.dispose();

        ballObject.dispose();
        ballShape.dispose();

        dispatcher.dispose();
        collisionConfig.dispose();

        modelBatch.dispose();
        model.dispose();
    }

ModelBatchとModelについては dispose メソッド内で既に破棄を行っていました。 今回は衝突形状、衝突オブジェクト、ヘルパークラスの dispatchercollisionConfig についても破棄を行います。 このヘルパークラスについては後で説明します。

これでcheckCollision メソッドの実装を開始できます。基本的にここでやりたいのは、球体が箱と衝突しているかどうかの確認です。 Bullet にはまさにこれのために、btSphereBoxCollisionAlgorithmというアルゴリズムがあります。


    boolean checkCollision() {
        CollisionObjectWrapper co0 = new CollisionObjectWrapper(ballObject);
        CollisionObjectWrapper co1 = new CollisionObjectWrapper(groundObject);

        btCollisionAlgorithmConstructionInfo ci = new btCollisionAlgorithmConstructionInfo();
        ci.setDispatcher1(dispatcher);
        btCollisionAlgorithm algorithm = new btSphereBoxCollisionAlgorithm(null, ci, co0.wrapper, co1.wrapper, false); 

        btDispatcherInfo info = new btDispatcherInfo();
        btManifoldResult result = new btManifoldResult(co0.wrapper, co1.wrapper);

        algorithm.processCollision(co0.wrapper, co1.wrapper, info, result);

        boolean r = result.getPersistentManifold().getNumContacts() > 0;

        result.dispose();
        info.dispose();
        algorithm.dispose();
        ci.dispose();
        co1.dispose();
        co0.dispose();

        return r;
    }

githubにある全ソースコードを見てください

ここでまず最初にすることは、各オブジェクト用のCollisionObjectWrapperの作成です。 このクラスは名前が"bt"から始まってないので注意してください。これは、このクラスが libgdx bulletラッパー固有のクラスであるためです (このクラスはbtCollisionObjectWrapper オブジェクトを内包しており、wrapper メンバを使ってこのオブジェクトにアクセス可能です)。

次にbtCollisionAlgorithmConstructionInfo のインスタンスを作成します。 このインスタンスを使って、作成する衝突アルゴリズムに関する情報を設定します。設定は既定のままにしておきましょう。 このインスタンスにはbtDispatcherが必要なので、前に作成した dispatcherを渡します。

その後、btSphereBoxCollisionAlgorithmのインスタンスを作成します。 先ほど作成したオブジェクトを引数としてコンストラクタに渡す必要があります。 これは球体が箱に衝突したかどうかを確認するために使用するアルゴリズムです。 このアルゴリズムを実行するには、追加で btDispatcherInfo (目的のアルゴリズム関する追加情報を渡すためのもの)とbtManifoldResult (結果を受け取るためのもの)が必要です。 このアルゴリズムは、全ての衝突アルゴリズムのスーパークラスであるbtCollisionAlgorithmのインスタンスなので、覚えておいてください。

これで、アルゴリズムのprocessCollision メソッドを呼び出すことでこのアルゴリズムを実際に実行することができます。 結果(接触点)は先ほど咲くほど作成したbtManifoldResult内に保存されます。 接触点の数が0よりも大きい場合、衝突が発生しています。

最後に、以前も見たように、インスタンスを作成した全てのbulletのクラスを破棄することが重要です。

これを実行すると、我々が望んでいたものと全く同じ結果になることが分かるでしょう。 地面に当たるとボールの動きは止ります。

bullet3

少し圧倒されてしまうかもしれませんが、心配ありません。後ほど詳細が分かります。 今のところは、やったことの基本を理解することに努めてください: 衝突形状を二つ作成しました。そして 形状と 位置情報と回転情報を含んだ衝突オブジェクトを二つ作成しました。 二つのオブジェクトが衝突しているかどうかを確認するため、sphere-box衝突を検知するために特別に設計された衝突アルゴリズムを作成しました。 この衝突判定の結果はマニフォールドと呼ばれ、衝突の接触点(接触点が存在すれば)を含んでいます。 これらの接触点には、例えば衝突の距離(めり込み)や方向などの衝突に関する情報が含まれています。



舞台裏を覗いてみる

このまま続ける前に、舞台裏では実際には何が起こっているかを見る良い機会かもしれません。 以下の行を簡単に見てみましょう:


boolean r = result.getPersistentManifold().getNumContacts() > 0;

これは、基本的には以下コードと全く同じになります:


btPersistentManifold manifold = result.getPersistentManifold();
int numContacts = manifold.getNumContacts();
boolean r = (numContacts > 0);

result.getPersistentManifold() を実行すると、 btPersistentManifoldが戻り値として返されます。 今回は、ラッパーがこれの舞台裏で何をしなければならないのかを簡単に見てみましょう。:


  public btPersistentManifold getPersistentManifold() {
    long cPtr = CollisionJNI.btManifoldResult_getPersistentManifold__SWIG_0(swigCPtr, this);
    return (cPtr == 0) ? null : new btPersistentManifold(cPtr, false);
  }

心配しないでください、ここで行われてることを正確に理解する必要はありません。 ですが、自分で記述したコードの影響を包括的に理解することは良い習慣と思います。 最初の行で、ラッパーはネイティブ(C++)オブジェクトのメソッド(getPersistentManifold)を実行します。 このメソッドの戻り値はlong型の値です。 この値はC++ライブラリ内のbtPersistentManifold オブジェクトを参照している"ポインター"です。 ポインターとは、このオブジェクトが存在するメモリ上での位置情報です。 この long 型の値は、後でbtPersistentManifoldオブジェクトのメソッドを実行するのに使われます。

次の行では、ラッパーは同等の機能を持つJavaのbtPersistentManifoldオブジェクトを作成しており、このJavaオブジェクトは簡単に使用可能です。 全てのjava bullet クラスは、同等の機能を持つ C++クラスへのポインタを含んでいます。 全てのjava bulletクラスが持っている getCPointer()メソッドを使用することで、このlong型の値にアクセスできます。

btPersistentManifold オブジェクトを明示的に作成してはいないので(言い換えれば、我々はそのオブジェクトを所有していないので)、 そのオブジェクトを 破棄 する必要もありません。 それをあなたが disposeしたとしても問題はありませんが、ラッパーは、我々がこの裏で動いているC++ オブジェクトを所有していないので破棄することはないと認識しています。 hasOwnership()メソッドを使用して、各java bulletクラスの所有権を照会できます。



衝突ディスパッチャーを使用する

衝突判定のテストに戻りましょう。 たくさんの衝突アルゴリズムがあるであろうとのご想像通り、衝突オブジェクト(shape)同士の考えうる各組み合わせごとに、衝突アルゴリズムが存在します。 各衝突ペアごとの衝突アルゴリズムを手動で作成するのは本当に大変です。 幸いなことに、Bullet は各オブジェクトのペアごとの衝突アルゴリズムを我々に代わって作成します。 以前作成したディスパッチャーでこれを行います。


    @Override
    public void render () {
        ...
            collision = checkCollision(ballObject, groundObject);
        ...
    }

    boolean checkCollision(btCollisionObject obj0, btCollisionObject obj1) {
        CollisionObjectWrapper co0 = new CollisionObjectWrapper(obj0);
        CollisionObjectWrapper co1 = new CollisionObjectWrapper(obj1);

        btCollisionAlgorithm algorithm = dispatcher.findAlgorithm(co0.wrapper, co1.wrapper);

        btDispatcherInfo info = new btDispatcherInfo();
        btManifoldResult result = new btManifoldResult(co0.wrapper, co1.wrapper);

        algorithm.processCollision(co0.wrapper, co1.wrapper, info, result);

        boolean r = result.getPersistentManifold().getNumContacts() > 0;

        dispatcher.freeCollisionAlgorithm(algorithm.getCPointer());
        result.dispose();
        info.dispose();
        co1.dispose();
        co0.dispose();

        return r;
    }

githubにある全ソースコードを見てください

checkCollision シグネチャを少し変更して、これを衝突オブジェクトの任意のペアで使用できるようにしました。 sphere-box衝突アルゴリズムを手動で作成するのではなく、dispatcher.findAlgorithmメソッドを使って、 適切なアルゴリズムを見つけるようdispatcher.findAlgorithmに指示を出しています。 メソッドの残りの部分は、以前と全く同じです。一つのことを除いては: 我々はもうアルゴリズムを所有していないので、それを自分で破棄する必要がないのです。 代わりに、他の衝突判定でディスパッチャを再利用(プール)できるようにするため、ディスパッチャに対してアルゴリズムが完了したことを通知する必要があります。 そのためには、ディスパッチャにはアルゴリズムの現在のメモリ上での位置情報が必要です。 前に見たように、 getCPointer メソッドを使ってこの位置情報を取得できます。



さらにオブジェクトを追加する

ここまでは上手くいきましたね。これで二つのオブジェクトが交差しているかどうかを確認するための一般的な方法が使えるようになりました。 それでは、お互いに比較可能ないくつかのオブジェクトをさらに追加してみましょう。 またそれと一緒に、複数のオブジェクトを使用するためにコードを少し綺麗にしましょう。 そのため、まずはModelInstanceクラスを拡張して、Modelインスタンスに衝突オブジェクトを追加します:


public class BulletTest implements ApplicationListener {
    static class GameObject extends ModelInstance implements Disposable {
        public final btCollisionObject body;
        public boolean moving;
        public GameObject(Model model, String node, btCollisionShape shape) {
            super(model, node);
            body = new btCollisionObject();
            body.setCollisionShape(shape);
        }

        @Override
        public void dispose () {
            body.dispose();
        }
    }
    ...
}

btCollisionObject bodyメンバ(基本的には、衝突形状とtransformation情報)を持つことで、 ゲームオブジェクトの管理が簡単になります。 オブジェクトが地面に接しているかどうかを確認するには、moving メンバを 使います。

コードを少し綺麗にするためのもう一つの良いやり方は、 "factory" クラスを使用することです:


public class BulletTest implements ApplicationListener {
    static class GameObject extends ModelInstance implements Disposable {
        ...
        static class Constructor implements Disposable {
            public final Model model;
            public final String node;
            public final btCollisionShape shape;
            public Constructor(Model model, String node, btCollisionShape shape) {
                this.model = model;
                this.node = node;
                this.shape = shape;
            }

            public GameObject construct() {
                return new GameObject(model, node, shape);
            }

            @Override
            public void dispose () {
                shape.dispose();
            }
        }
    }
}

これで、異なる形状ごとの GameObject.Constructor が用意でき、そのconstruct メソッドを実行して GameObjectが作成できるようになりました。 これとmapを組み合わせると、ゲームオブジェクトのインスタンスを作成するための本当に便利なメソッドが得られます。:


public class BulletTest implements ApplicationListener {
    ...
    Array<GameObject> instances;
    ArrayMap<String, GameObject.Constructor> constructors;

    @Override
    public void create () {
        ...
        camController = new CameraInputController(cam);
        Gdx.input.setInputProcessor(camController);

        ModelBuilder mb = new ModelBuilder();
        mb.begin();
        mb.node().id = "ground";
        mb.part("ground", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, new Material(ColorAttribute.createDiffuse(Color.RED)))
            .box(5f, 1f, 5f);
        mb.node().id = "sphere";
        mb.part("sphere", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, new Material(ColorAttribute.createDiffuse(Color.GREEN)))
            .sphere(1f, 1f, 1f, 10, 10);
        mb.node().id = "box";
        mb.part("box", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, new Material(ColorAttribute.createDiffuse(Color.BLUE)))
            .box(1f, 1f, 1f);
        mb.node().id = "cone";
        mb.part("cone", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, new Material(ColorAttribute.createDiffuse(Color.YELLOW)))
            .cone(1f, 2f, 1f, 10);
        mb.node().id = "capsule";
        mb.part("capsule", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, new Material(ColorAttribute.createDiffuse(Color.CYAN)))
            .capsule(0.5f, 2f, 10);
        mb.node().id = "cylinder";
        mb.part("cylinder", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, new Material(ColorAttribute.createDiffuse(Color.MAGENTA)))
            .cylinder(1f, 2f, 1f, 10);
        model = mb.end();

        constructors = new ArrayMap<String, GameObject.Constructor>(String.class, GameObject.Constructor.class);
        constructors.put("ground", new GameObject.Constructor(model, "ground", new btBoxShape(new Vector3(2.5f, 0.5f, 2.5f))));
        constructors.put("sphere", new GameObject.Constructor(model, "sphere", new btSphereShape(0.5f)));
        constructors.put("box", new GameObject.Constructor(model, "box", new btBoxShape(new Vector3(0.5f, 0.5f, 0.5f))));
        constructors.put("cone", new GameObject.Constructor(model, "cone", new btConeShape(0.5f, 2f)));
        constructors.put("capsule", new GameObject.Constructor(model, "capsule", new btCapsuleShape(.5f, 1f)));
        constructors.put("cylinder", new GameObject.Constructor(model, "cylinder", new btCylinderShape(new Vector3(.5f, 1f, .5f))));

        instances = new Array<GameObject>();
        instances.add(constructors.get("ground").construct());

        collisionConfig = new btDefaultCollisionConfiguration();
        dispatcher = new btCollisionDispatcher(collisionConfig);
    }
    ...
    @Override
    public void dispose () {
        for (GameObject obj : instances)
            obj.dispose();
        instances.clear();

        for (GameObject.Constructor ctor : constructors.values())
            ctor.dispose();
        constructors.clear();

        dispatcher.dispose();
        collisionConfig.dispose();

        modelBatch.dispose();
        model.dispose();
    }
}

ご覧の通り、前回のコードを削除して、GameObject.Constructorクラスを使って衝突形状と衝突オブジェクトのインスタンスを作成するコードと置き換えています。 ModelInstanceインスタンスの配列が、今度はGameObjectインスタンスの配列になっています。 ModelBuilder を使用して各形状ごとのノードを作成します。 次に各形状のGameObject.Constructorごとを作成します。各コンストラクターには btCollisionShapeが引数として渡されています。 各コンストラクタにはmap上での分かりやすい名前が付けられているので、これで constructors.get(name).construct()のようなゲームオブジェクトも作成できます。 もちろん、以前と同じ様に衝突オブジェクトと衝突形状を破棄する必要があります。したがって、同様に dispose メソッドにも少し修正が行われています。

それではrenderメソッドを修正して、 GameObjectオブジェクトを使って1秒ごとに新しいゲームオブジェクトを追加するようにしましょう:


public class BulletTest implements ApplicationListener {
    ...
    float spawnTimer;
    ...
    @Override
    public void render () {
        final float delta = Math.min(1f/30f, Gdx.graphics.getDeltaTime());

        for (GameObject obj : instances) {
            if (obj.moving) {
                obj.transform.trn(0f, -delta, 0f);
                obj.body.setWorldTransform(obj.transform);
                if (checkCollision(obj.body, instances.get(0).body))
                    obj.moving = false;
            }
        }

        if ((spawnTimer -= delta) < 0) {
            spawn();
            spawnTimer = 1.5f;
        }
        ...
    }

    public void spawn() {
        GameObject obj = constructors.values[1+MathUtils.random(constructors.size-2)].construct();
        obj.moving = true;
        obj.transform.setFromEulerAngles(MathUtils.random(360f), MathUtils.random(360f), MathUtils.random(360f));
        obj.transform.trn(MathUtils.random(-2.5f, 2.5f), 9f, MathUtils.random(-2.5f, 2.5f));
        obj.body.setWorldTransform(obj.transform);
        instances.add(obj);
    }
    ...
}

githubにある全ソースコードを見てください

GameObject ごとにそれが移動している状態かを確認し、移動状態の場合は1秒ごとに1ユニットの速度で下へ移動させます。 そしてそのオブジェクトが、配列内で最初にあるゲームオブジェクト(分かっていると思いますが、地面のことです)と衝突しているかを確認し、 衝突している場合はオブジェクトの移動を停止させます。 次にメンバspawnTimerを使って、1.5秒毎にspawn() メソッドを実行します。

spawnメソッド内では、GameObjectのインスタンス(地面は除く)をランダムで新規に作成し、それを地面から上のランダムな位置に配置します。 また、ランダムに回転もさせます。 オブジェクトは回転をしているので、今回はtranslateメソッドではなくtrnメソッドを使ってオブジェクトを変換処理しています。 このtrn メソッドは回転状態を無視してオブジェクトを変換処理するので、オブジェクトは常に指定した位置に移動します。

今回の変更はBullet 関連の内容ではなく、説明すると複雑になってしまうので詳細については省きます。 今回やった内容は、複数のゲームオブジェクトを使うためにコードを少し綺麗にしたということです。

これを実行すると、様々なランダムなオブジェクトが地面に落ちていくのが表示されます:

bullet4



ContactListenerを使用する

二つのオブジェクトが衝突しているかどうかを確認したい場合には、先ほど説明したやり方が最適です。 しかし、複数のオブジェクトが衝突しているかどうかを確認して、なおかつオブジェクトの数が増えるような場合は、先ほどのやり方では大変で動きも遅くなります。 各オブジェクトごとに衝突する可能性のある全ての組み合わせを確認するのではなく、衝突が発生した時に通知を受け取る方がはるかに便利です。 幸いなことに、Bullet には特定の衝突イベント時に呼び出されるコールバックメソッドが用意されています。


public class BulletTest implements ApplicationListener {
    class MyContactListener extends ContactListener {
        @Override
        public boolean onContactAdded (btManifoldPoint cp, btCollisionObjectWrapper colObj0Wrap, int partId0, int index0,
            btCollisionObjectWrapper colObj1Wrap, int partId1, int index1) {
            instances.get(colObj0Wrap.getCollisionObject().getUserValue()).moving = false;
            instances.get(colObj1Wrap.getCollisionObject().getUserValue()).moving = false;
            return true;
        }
    }
    ...
    MyContactListener contactListener;

    @Override
    public void create () {
        ...
        contactListener = new MyContactListener();
    }

    @Override
    public void render () {
        ...
        for (GameObject obj : instances) {
            if (obj.moving) {
                obj.transform.trn(0f, -delta, 0f);
                obj.body.setWorldTransform(obj.transform);
                checkCollision(obj.body, instances.get(0).body);
            }
        }
        ...
    }
    public void spawn() {
        ...
        obj.body.setUserValue(instances.size);
        obj.body.setCollisionFlags(obj.body.getCollisionFlags() | btCollisionObject.CollisionFlags.CF_CUSTOM_MATERIAL_CALLBACK);
        ...
    }
    ...
    @Override
    public void dispose () {
        ...
        contactListener.dispose();
        ...
    }

Githubにある全ソースコードを見てください

上記コードでは ContactListenerを作成しています。これは Bullet のクラスではなく、ラッパー専用に作成されたクラスです。 Bullet は衝突イベントにオブジェクト指向のコールバックを使用しておらず、全てのコールバックはグローバルメソッド (Java の static メソッドのようなもの)です。 Java ではグローバルコールバックメソッドを使用することができないので、 ラッパーはContactListenerを追加してその制御をします。 またContactListenerを使用することをbullet に通知する必要もありません。これはContactListenerのインスタンスを作成した時にラッパーが通知を行うからです。

onContactAdded メソッドは、マニフォールドに接触点が追加される度に呼び出されます。 先ほど見たように、マニフォールドが一つ以上の接触点を持つと、そのタイミングで即ち衝突が発生しています。 そのため、基本的にこのメソッドは二つのオブジェクト間で衝突が発生した時に呼び出されます。

もちろん、Bullet ライブラリは我々のGameObjectクラスを認識していないので、コールバックメソッドでBullet が引数として渡してくるデータを使ってGameObject を取得する方法が必要です。 あなたがbox2dについて詳しいのであれば、box2dのuserDataメンバを使用する方法についてもおそらく知っているでしょう。 Bullet ラッパーも box2dと同様に、btCollisionObjectuserDataメンバをサポートしています。 ですが、それではなくsetUserValue メソッドと getUserValueメソッド を使用します。 これはインスタンス配列内のGameObjectのインデックスを設定するためのint型の値です。 そのため、instances.get(colObj0Wrap.getCollisionObject().getUserValue())を使用して、対応するGameObjectを取得できます。

spawn メソッド内でsetUserValueを使用して、インスタンス配列内のこのオブジェクトのインデックスをこの値に設定します。 そしてCF_CUSTOM_MATERIAL_CALLBACKフラグも追加して、このオブジェクトで受け取りたい衝突イベントをBullet に通知します。 このフラグはonContactAddedメソッドを呼び出すために必要です。

render メソッド内ではもう moving フラグを設定する必要はないので、その部分を削除して checkCollision メソッドを呼び出すだけにします。 いつものようにcontactListenerを破棄する必要があるので、disposeメソッド内に1行追加しました。

これを実行すると前回と全く同じ内容が表示されますが、今回は各衝突の組み合わせをポーリングするのではなく、 contact コールバックを使用しています。

bullet4



頻繁に呼び出されるメソッドを最適化する

先ほど、ラッパーの全てのクラスは基本的には対応するC++オブジェクトへのポインタであるということを見てきました。 これにより、あなたがjava オブジェクトのメソッドを呼び出す時に、ラッパーは C++オブジェクトの適切なメソッドを呼び出すことができます。 しかし、これは一方通行にしかなりません。 どうすれば Bullet C++ コードからJava メソッドを呼び出すことができるのでしょうか? まあ、ほとんどのクラスの場合それはできません。 特にメソッドの上書き拡張のために設計されているクラスのみ、呼び出すことができます。 これはC++からJavaへの橋渡しのオーバーヘッドを減らすためのもので、メソッドを上書きしない場合はパフォーマンスは落ちません。 ContactListener はそうした上書きを前提としたクラスであり、同様の "callback" クラスは他にもいくつかあります。

以前に見たように、ラッパーは必要に応じて各C++オブジェクト用のjava オブジェクトを作成します。 もちろん、必要ないのにオブジェクトを作成するのは避けたいですね(ゲームの場合は特に) 。 そういった理由から、ラッパーではContactListenerメソッド内で本当に必要なオブジェクトをあなたが設定できます。 このため、ContactListenerは上書き可能な同名のメソッドシグネチャを複数持っており、 ラッパーは1つのイベントごとに1つのメソッドのみを呼び出すので、これら同名のメソッドシグネチャの中から1つのみを上書きできます。

ContactListenerを見てみると、我々は btManifoldPointを使用していません。 そのため、引数を含まないメソッドシグネチャを使用するのであれば、ラッパーはそれを作成する必要はありません。:


    class MyContactListener extends ContactListener {
        @Override
        public boolean onContactAdded (btCollisionObjectWrapper colObj0Wrap, int partId0, int index0,
            btCollisionObjectWrapper colObj1Wrap, int partId1, int index1) {
            instances.get(colObj0Wrap.getCollisionObject().getUserValue()).moving = false;
            instances.get(colObj1Wrap.getCollisionObject().getUserValue()).moving = false;
            return true;
        }
    }

btCollisionObjectWrapper はコールバックで頻繁に呼び出されるので、 ラッパーはこれに対して特に注意を払っています。 ラッパーはbtCollisionObjectWrapperのためにプール領域を使用しています。 しかし、我々は実際にはbtCollisionObjectWrapperを使用せずにそれが内包しているbtCollisionObjectのみが必要なので、 代わりにbtCollisionObject が引数として渡されるメソッドシグネチャを使用したほうが良いでしょう。


    class MyContactListener extends ContactListener {
        @Override
        public boolean onContactAdded (btCollisionObject colObj0, int partId0, int index0, btCollisionObject colObj1, int partId1,
            int index1) {
            instances.get(colObj0.getUserValue()).moving = false;
            instances.get(colObj1.getUserValue()).moving = false;
            return true;
        }
    }

btCollisionObject は常にJava内で作成されるので、ラッパーは必要な時には毎回そのインスタンスを必ず使用します。 その際に、ラッパーはLongMap (キーのような C++ ポインタ)を使用します。 もちろん、正しいbtCollisionObjectを探すのに少し負荷はあります。 ですがこれを使うことで、btCollisionObject を拡張してコールバック内で拡張したクラスに常にアクセスすることが可能になります。

しかしながら、我々はコールバック内で必要なのはユーザー値だけであり、この値さえあればインスタンス配列内でのGameObjectの位置を特定するのには十分です。 LongMapを使ってラッパーに衝突オブジェクトを探させる必要はありません。


    class MyContactListener extends ContactListener {
        @Override
        public boolean onContactAdded (int userValue0, int partId0, int index0, int userValue1, int partId1, int index1) {
            instances.get(userValue0).moving = false;
            instances.get(userValue1).moving = false;
            return true;
        }
    }

Githubにある全ソースコードを見てください

ラッパーは我々にuserValueを渡す機能を持っているので、わざわざオブジェクトを作成する必要性は一切なくなります。 今回はプリミティブ型の引数だけを使うコールバックメソッドを作成しました。これは、このメソッド呼び出しのために作成されたオブジェクトや検索用マップはないということです。



CollisionWorldを追加する

これで衝突が発生した時にイベントを受け取るようになりましたが、それでも各オブジェクトが地面に衝突しているかどうかについては手動で確認しています。 確認に使用しているメソッドは非常に上手く機能していますが、最適化されているとは言い難いです。 まず第一に、checkCollision メソッド内でかなり多くのオブジェクトの作成と破棄をしています。 先ほど見てきたように、オブジェクトの作成はあまり頻繁にやらないのがベストです。 そうしないと、ガベージコレクターは数秒かそこらで一時的な動作中断してしまうかもしれません。

しかし、多くのオブジェクトを作成しているのを一旦置いておくとしても、別の問題があります。 我々は専門的な衝突アルゴリズムを毎回使用しています。 前回のチュートリアルからも思い出せるように、専門的な衝突アルゴリズムというのは比較的負荷が高いです。 理想を言えば、まず最初に二つのオブジェクトがお互いに近くにあるかどうかを確認します(例えばバウンディングボックスやバウンディングスフィアを使用して)。 そしてそれらが近くにある場合のみ、より正確で専門的な衝突アルゴリズムを使用します。

このようにして衝突判定で二つのフェイズを使用することは、多くの利点があります。 最初のフェイズ(お互いに近くにある衝突オブジェクトを探すフェイス)はブロードフェイズと呼ばれます。 そして二番目のフェイズ(より正確で専門的な衝突アルゴリズムが使用されるフェイズ)は、 ニアフェイズと呼ばれます。 これまではニアフェイズのみを見てきました。 実際、衝突ディスパッチャーはニアフェイズの場合に使用するクラスです。

ご想像の通り、一般的にブロードフェイズは全ての衝突オブジェクトで呼び出され、ニアフェイズは少数のオブジェクトでのみ呼び出されます。 そのため、ブロードフェイズを特に最適化することが重要です。 Bullet は衝突情報をキャッシュすることで最適化を行うので、衝突情報を毎回再計算する必要はありません。 いくつかの実装方法が選択可能ですが、実際にはこれはツリー形式で行います。 ツリー形式について詳細は説明しませんが、もし知りたい場合は "軸平行バウンディングボックスツリー"もしくは短く"AABB ツリー"と検索してください。

"AABB" という用語は、衝突判定(ブロードフェイズ関連)においてかなり頻繁に使用されます 。 これは単にバウンディングボックスのことを指します。 前回までのいくつかのチュートリアルで使用したバウンディングボックスと同じです。 これは位置情報(中央)と容積でのみ構成されています。 回転情報は含まれていません。これにより、二つのバウンディングボックスが重なっているかどうかを確認するのが非常に簡単(低負荷)になります。

もちろん、ツリーはどこかに保存して、オブジェクトの追加や削除や変形が行われた時に更新を行う必要があります。 幸いBullet には、私達に代わってこれを全て行ってくれる素晴らしいクラスがあります。これはCollisionWorldと呼ばれます。 基本的にはブロードフェイズとニアフェイズどちらを使用したいかをworldに伝え、次に衝突オブジェクトの追加や削除や変形を行い、 衝突が発生した時にはCollision Worldがあなたへ通知します(ContactListenerを介して)。 それでは、ブロードフェイズを含んだCollisionWorldを追加しましょう。:


public class BulletTest implements ApplicationListener {
    ...
    btBroadphaseInterface broadphase;
    btCollisionWorld collisionWorld;

    @Override
    public void create () {
        ...
        collisionConfig = new btDefaultCollisionConfiguration();
        dispatcher = new btCollisionDispatcher(collisionConfig);
        broadphase = new btDbvtBroadphase();
        collisionWorld = new btCollisionWorld(dispatcher, broadphase, collisionConfig);
        contactListener = new MyContactListener();

        instances = new Array<GameObject>();
        GameObject object = constructors.get("ground").construct();
        instances.add(object);
        collisionWorld.addCollisionObject(object.body);
    }

    public void spawn () {
        ...
        instances.add(obj);
        collisionWorld.addCollisionObject(obj.body);
    }

    @Override
    public void render () {
        final float delta = Math.min(1f / 30f, Gdx.graphics.getDeltaTime());

        for (GameObject obj : instances) {
            if (obj.moving) {
                obj.transform.trn(0f, -delta, 0f);
                obj.body.setWorldTransform(obj.transform);
            }
        }

        collisionWorld.performDiscreteCollisionDetection();
        ...
    }

    // Remove the checkCollision method, it's no longer needed

    @Override
    public void dispose () {
        ...
        collisionWorld.dispose();
        broadphase.dispose();
        ...
    }
    ...
}

Githubにある全ソースコードを見てください

上記コードでは、 btBroadphaseInterfacebtCollisionWorldを作成します。 ブロードフェイズの場合は、btDbvtBroadphase 実装を選択します。 これは ダイナミックバウンディングボリュームツリー(dynamic bounding volume tree) 実装のことです。 ほとんどの場合はこの実装で十分でしょう。 次にcollisionWorld.addCollisionObjectメソッドを使って、地面と その他spawnメソッド内で作成するオブジェクトをCollisionWorldに追加する必要があります。 checkCollisionメソッドは削除し、今回は代わりにcollisionWorld.performDiscreteCollisionDetection();を呼び出します。 このメソッドはCollisionWorldに追加した全てのオブジェクト間の衝突を確認し、衝突が発生した時はContactListenerを呼び出します。

なかなか良いですね。これで衝突を検知するためのオブジェクトを作成する必要がなくなったので、ガベージコレクションは実行されません。 また、実際のbullet 関連部分のコードを見ると、わずか数行のコードなのが分かります。 我々が作成したコードのほとんどは、ゲームオブジェクトの描写と管理に関連したものです。

これを実行すると、前回と同じ...ような動作をするのが分かるでしょう:

bullet5



衝突のフィルタリング

CollisionWorldは地面だけではなく全てのオブジェクト間での衝突を検知しているため、 現状ではオブジェクトがお互いに衝突した時も動きが止ってしまいます。 これを修正するのはそんなに難しくはありません:


    class MyContactListener extends ContactListener {
        @Override
        public boolean onContactAdded (int userValue0, int partId0, int index0, int userValue1, int partId1, int index1) {
            if (userValue1 == 0)
                instances.get(userValue0).moving = false;
            else if (userValue0 == 0)
                instances.get(userValue1).moving = false;
            return true;
        }
    }

知っての通り、地面はuserValue に0の値を持っています。 今回は衝突したオブジェクトのうちのどちらかが地面なのかを確認し、地面だった場合はもう一方のオブジェクトの動きを止めます。 オブジェクトのうちどちらかが地面の可能性があるので、両方の値を確認する必要があります。

これはテストでは動作しますが、一般的な解決方法ではありません。 さらに、worldは無視したい組み合わせにおいても、未だに衝突判定のブロードフェイズとニアフェイズの両方を実行する必要があるのです。 worldに対しては、他のオブジェクトに対してはお互いに無視することができ、地面と衝突した場合のみお互いに衝突確認をする必要がある、と指示をしたほうが良いです。 これを行うための非常に効果的な方法の1つは、衝突フラグを使用することです:


public class BulletTest implements ApplicationListener {
    final static short GROUND_FLAG = 1<<8;
    final static short OBJECT_FLAG = 1<<9;
    final static short ALL_FLAG = -1;
    ...
    @Override
    public void create () {
        ...
        collisionWorld.addCollisionObject(object.body, GROUND_FLAG, ALL_FLAG);
    }

    public void spawn () {  
        ...
        collisionWorld.addCollisionObject(obj.body, OBJECT_FLAG, GROUND_FLAG);
    }

Githubにある全ソースコードを見てください

上記コードでは、三つのフラグを作成しています。 一つ目の GROUND_FLAGフラグは9番目のビットにのみ1を設定しています。 二つ目の OBJECT_FLAGフラグは10番目のビットにのみ1を設定しています。 最後の ALL_FLAGフラグは全てのビットに1を設定しています。 そして地面をCollisionWorldへ追加する時に、このオブジェクトで使用するフラグと、このオブジェクトと衝突できるオブジェクトのフラグをBullet へ通知します。 そのため、地面は全てのオブジェクトと衝突します。 spawn メソッド内でオブジェクトを生成する時、そのオブジェクトは地面とのみ衝突するようBullet へ指示を出します。 Bullet はビット単位の比較を使って、二つのオブジェクト間の衝突を検知すべきかどうかを確認します。 ビット単位の比較についてあまり知らない場合、それについて読んでおくことをお勧めします。 ビット単位の比較は非常に便利で高速な方法で、衝突のフィルタリング以外でも使えます。

では、なぜ1番目のビットではなく、9番目のビットから設定を始めているのでしょうか? それは、安全のためです。 ここで説明しているように、Bullet は内部でいくつかのビットを使用しています。 これが問題になることはありませんが、一般的には他で使用されないビットを使用する方が良いです。 フラグは short 型の値なので、ビット数は比較的少ないということに注意してください。 使用するビットは慎重に選ぶことをお勧めします。



次は何をするか

これで LibGDX Bulletラッパーの使用に関する第一部は終了です。 次のパートでは剛体力学について見ていきます。完全な物理シミュレーションのためにBullet を使用します。 例えば、力/重力を適用して各オブジェクトで相互作用をさせます。

当然のことながら、このチュートリアルで衝突判定の全ての側面をカバーすることは不可能です。 例えば、Bullet を使って以前のチュートリアルで見たものよりも正確なrayの取得を行うことも可能です。 Bullet を使って視錐台カリングを行うことさえできます(お勧めはしませんが)。

Bullet マニュアルを読むことを強くお勧めします。これは良い読み物で、Bullet ライブラリを使用する時はここがスタート地点となります。 そのページは LibGDX Bulletラッパーを対象としてはいませんが、内容はLibGDX Bulletラッパーでも役に立ちます。

Bullet ウェブサイトと、特にBullet wiki には多くの実践的な情報があります。 例えば、 様々な衝突形状contact callbacks衝突のフィルタリングなど。 また フォーラム も非常に活発です

この wiki ページには、LibGDX Bullet ラッパーの具体的な多くの情報があります。

次: libGDX 3D 物理演算のBullet ラッパーを使用する - パート2