JBox2dを使ってみた。

いわゆる物理エンジンというやつですね。物理エンジンと言っても色々あるわけだけれども、ここではbox2dっていうやつのjava版、つまりJBOX2Dってのを使ってみました。描画はappletです。いまさら。

これまで何度かやってみようと思って取り掛かった事はあるのだけれど、何から何までわからない事だらけで、いつも途中で投げ出してました。今回、なんか動くのが出来たので、記事にしてみようと思ったわけです。
ハッキリ言って、参考になる情報はいくらでも転がっているんですが、バージョンがちょっと違うだけでずいぶん書き方が違ったりするみたいなので、私のような初心者(かつ、めんどくさがり)で、あめりか語を読めない人には易しくありません。

というわけなので、コードと解説をかるぅーくやっておこうかなと言う気になりました。
基本的な考え方とか、お作法は「このバージョンだから」とか「Javaだから」とかで変わるものではないと思いますので、色々と参考になるサイトはあると思います。

この辺のサイトが参考になります。→ http://www.google.com/

まあ、冗談です。
Box2DFlashAS3 の単純なサンプルと使い方 (2.0.2版) - てっく煮ブログさんとかが、とてもわかりやすくて参考になりました。
もっと言うとTM's Workspace - JBox2Dを試してみた。さんが、今から私がやろうとしている以上の事をやってくださっていたりします。

さて、はじめるにあたって、ですが、環境を整えなければなりません。
とにかくなんと言っても絶対必要なのは以下の2つです。

それぞれ準備しましょう。eclipsejdkって言うとアレなので、詳細は割愛します。

で、サンプルを動かすだけならhttp://jbox2d.googlecode.com/svn/trunk/をチェックアウトしてくれば、まあ大体それだけで動きます。今回はJBox2dをライブラリとして使用して自分で何か作りたいわけですから、そういう準備をします。
ここから持ってきましょう。→
http://code.google.com/p/jbox2d/downloads/list
で、もう一つ必要になるのが、slf4jってやつです。これがナニモノかについては頑張ってください。

私が今回準備できたバージョンは以下の通りでした。

  • jbox2d 2.1.2.1
  • slf4j 1.6.4

さて、早速eclipseでプロジェクトを作ったら、jbox2dの中にあるjbox2d-library-2.1.2.1-SNAPSHOT.jarとslf4j-api-1.6.4.jarを参照ライブラリに加えます。これで準備OKです。
後は大体他のサイトに書いてる内容と一緒になってしまいます。

なのでいきなりコードどーーーーん!

import java.applet.Applet;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.util.Random;

import org.jbox2d.collision.shapes.PolygonShape;
import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.Body;
import org.jbox2d.dynamics.BodyDef;
import org.jbox2d.dynamics.BodyType;
import org.jbox2d.dynamics.FixtureDef;
import org.jbox2d.dynamics.World;

public class JBox2dTest2 extends Applet implements Runnable,
MouseListener, MouseWheelListener, MouseMotionListener {

private static final long serialVersionUID = 1L;

public static final int FPS = 60;

private World _world = null;
private boolean _loop = true; // 処理を続行するか
private Random r = new Random();

// 表示に関する情報
private int _scale = 20; // 表示倍率
private float _cameraX = 5.0f; // カメラの注視店
private float _cameraY = 5.0f; // カメラの注視店
private int _mouseX = 0; // 現在のマウス位置
private int _mouseY = 0; // 現在のマウス位置


// ダブルバッファリング用
private Image _image;
private Graphics2D _graph;

public void mouseWheelMoved(MouseWheelEvent e) {
_scale -= (e.getWheelRotation() * 2);
if (_scale <= 10) _scale = 10;
if (_scale >= 50) _scale = 50;
}

public void mousePressed(MouseEvent e) {
_mouseX = e.getX();
_mouseY = e.getY();
}

public void mouseClicked(MouseEvent e) {}

public void mouseEntered(MouseEvent e) {}

public void mouseExited(MouseEvent e) {}

public void mouseReleased(MouseEvent e) {}

public void mouseDragged(MouseEvent e) {
_cameraX += (float)(_mouseX - e.getX()) / _scale;
_cameraY += (float)(_mouseY - e.getY()) / _scale;

_mouseX = e.getX();
_mouseY = e.getY();
}

public void mouseMoved(MouseEvent e) {}

@Override
public void init() {
super.init();

// アプレットのための初期化
_image = createImage(getWidth(), getHeight());
_graph = (Graphics2D)_image.getGraphics();
_graph.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);

addMouseWheelListener(this);
addMouseListener(this);
addMouseMotionListener(this);

// jbox2dの初期化
Vec2 gravity = new Vec2(0, 10f);
_world = new World(gravity, true);

// 地面の定義
BodyDef bd = new BodyDef();
bd.position.set(5.0f, 9.0f);
bd.angle = (float)Math.PI / 180 * 15;

float w = 8.0f;
float h = 1.0f;

Body body = _world.createBody(bd);

PolygonShape ps = new PolygonShape();
ps.setAsBox(w / 2, h / 2);

body.createFixture(ps, 0f);
body.setUserData(new Rect(w, h));

// 描画スレッド開始
new Thread(this).start();
}

@Override
public void paint(Graphics g) {
// 回転を戻す
AffineTransform at = new AffineTransform();
_graph.setTransform(at);

// 初期化
_graph.clearRect(0,0,getWidth(),getHeight());

// 再描画
draw(_graph);
g.drawImage(_image,0,0,null);
}

@Override
public void update(Graphics g) {
paint(g);
}

@Override
public void destroy() {
_loop = false;
}

public void createObj() {
float w = 1f;
float h = 1.5f;

BodyDef bd = new BodyDef();
bd.type = BodyType.DYNAMIC;
bd.position.set(r.nextInt(getWidth()) / _scale, -h);
bd.angle = (float) (r.nextFloat() * Math.PI);

Body body = _world.createBody(bd);

PolygonShape ps = new PolygonShape();
ps.setAsBox(w / 2, h / 2);

FixtureDef fd = new FixtureDef();
fd.shape = ps;
fd.density = 0.5f; // 密度
fd.friction = 0.2f; // 摩擦
fd.restitution = 0.1f; // 反発
body.createFixture(fd);

body.setUserData(new Rect(w, h));
}

public void draw(Graphics2D g) {
for (Body body = _world.getBodyList(); body != null; body =
body.getNext()) {
try {
Vec2 position = body.getPosition();
Rect obj = (Rect)body.getUserData();

// カメラの左上座標(ピクセル)
int cxb = (int)(_cameraX * _scale - getWidth() / 2);
int cyb = (int)(_cameraY * _scale - getHeight() / 2);

// オブジェクトの基準座標(ピクセル)
int oxc = (int)(position.x * _scale);
int oyc = (int)(position.y * _scale);

// オブジェクトの左上座標(ピクセル)
int oxb = (int)((position.x - obj._width / 2.0f) * _scale);
int oyb = (int)((position.y - obj._height / 2.0f) * _scale);

// オブジェクトのサイズ(ピクセル)
int ow = (int)(obj._width * _scale);
int oh = (int)(obj._height * _scale);

AffineTransform at = new AffineTransform();
at.setToRotation(body.getAngle(), oxc - cxb, oyc - cyb);
g.setTransform(at);

g.drawRect(oxb - cxb, oyb - cyb, ow, oh);
} catch(RuntimeException e) {
}
}
}

public void run() {
long pt = System.nanoTime();
long ct;
try {
while(_loop) {
// 実際に経過した時間だけ処理を進める
ct = System.nanoTime();
long course = (ct - pt);
_world.step(course / 1000f / 1000f / 1000f, 8, 8);

repaint();

// ランダムで新しいオブジェクトの生成
if (r.nextInt(100) > 95) {
createObj();
}

pt = ct;
Thread.sleep(1000 / FPS);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

// 表示情報を管理するクラス
class Rect {
public float _width = 0;
public float _height = 0;
Rect(float w, float h) {
_width = w;
_height = h;
}
}
}

細かい説明はしませんが、大まかな作りとしてはAppletをextendsしてRunnableをimplementsしたthisをnew
Thread()してstart()するのでOverrideしたrun()で定期的に再描画してます。まあこの辺については"applet
アニメーション"とかでググるといっぱい出てくると思うます。
init()でjbox2dの初期化、ぢめん(?)の生成。各フレームでjbox2dに対して演算してもらいつつ、たまに新しいオブジェクトを生成してます。あとは、各種リスナーを登録し、ホイール操作で拡大・縮小、ドラッグで注視点の移動ができるようにしています。
生成したBodyのUserDataには、Rectって言うクラスを設定して描画の時に使ってます。とりあえず四角のみの前提で作ってるってことです。実際に何か作ろうと思ったらここらへんは変えていくつもりです。

内部的な計算は基本的にfloatなのに、Graphics2Dでの描画はintなので、変なところで丸められてとてつもなく不信な動きをしてたものの、描画が悪いのか、jbox2dの使い方が悪いのかも判らず困ってた時期もありましたが、そこにだけ気をつければここまで作るのに大した問題はありませんでした。

なんかもっと色々書くつもりだったけどめんどくさくなったので、この辺にしておきます。