今回はbox2dwebでマウスジョイントをやります。
今回のスクリーンショットです。
円をマウスドラッグ&ドロップで放り投げれます
正直実際に弄ってみないと感覚が掴めないと思います。
今回の全ソースコードです。
<canvas id="canvas" width="600px" height="420px" style="background-color:#333333;"></canvas>
<script type="text/javascript" src="Box2dWeb-2.1.a.3.min.js"></script>
<script type="text/javascript">
// Box2Dオブジェクトを取得
var b2Vec2 = Box2D.Common.Math.b2Vec2 // 2Dベクトル
, b2BodyDef = Box2D.Dynamics.b2BodyDef // Body定義
, b2Body = Box2D.Dynamics.b2Body // Body
, b2FixtureDef = Box2D.Dynamics.b2FixtureDef // Fixture定義
, b2Fixture = Box2D.Dynamics.b2Fixture // Fixture
, b2World = Box2D.Dynamics.b2World // 物理世界
, b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape // 衝突オブジェクトの形状(ポリゴン)
, b2CircleShape = Box2D.Collision.Shapes.b2CircleShape // 衝突オブジェクトの形状(円)
, b2MouseJointDef = Box2D.Dynamics.Joints.b2MouseJointDef // マウスジョイント
, b2AABB = Box2D.Collision.b2AABB // AABB(axis-aligned bounding box)
, b2DebugDraw = Box2D.Dynamics.b2DebugDraw // デバッグ描画
// 世界を作る
var world = new b2World(new b2Vec2(0,10), true);
////////////////////////////////////
var bodyDef = new b2BodyDef;
bodyDef.type = b2Body.b2_dynamicBody;
var fixDef = new b2FixtureDef;
fixDef.density = 30;
fixDef.friction = 10;
fixDef.restitution = 0.1;
fixDef.shape = new b2CircleShape(1);
// 円
bodyDef.position.Set(8,5);
var wheel1 = world.CreateBody(bodyDef);
wheel1.CreateFixture(fixDef);
bodyDef.position.Set(11,5);
var wheel2 = world.CreateBody(bodyDef);
wheel2.CreateFixture(fixDef);
// 地面
var bodyDef = new b2BodyDef;
bodyDef.type = b2Body.b2_staticBody;
bodyDef.position.Set(0,12);
var fd = new b2FixtureDef;
fd.shape = new b2PolygonShape;
fd.shape.SetAsBox(20,1);
var holder = world.CreateBody(bodyDef);
holder.CreateFixture(fd);
////////////////////////////////////
// デバッグ描画の設定
var debugDraw = new b2DebugDraw();
debugDraw.SetSprite ( document.getElementById ("canvas").getContext ("2d"));
debugDraw.SetDrawScale(30); //描画スケール
debugDraw.SetFillAlpha(0.3); //半透明値
debugDraw.SetLineThickness(1.0);//線の太さ
debugDraw.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit);// 何をデバッグ描画するか
world.SetDebugDraw(debugDraw);
window.setInterval(update,1000/60);
// マウス
var mouseX, mouseY, mousePVec, isMouseDown, selectedBody, mouseJoint;
var canvasPosition = getElementPosition(document.getElementById("canvas"));
function update() {
// マウスが押されていて、マウスジョイントが作成されていない場合
if(isMouseDown && (!mouseJoint)) {
var body = getBodyAtMouse();
if(body) {
// マウスジョイント作成
var md = new b2MouseJointDef();
md.bodyA = world.GetGroundBody(); // 物理世界
md.bodyB = body; // ひも付け対象のbody
md.target.Set(mouseX, mouseY); // マウスジョイント位置
md.collideConnected = true; // 二つのボディの衝突判定を行う
md.maxForce = 300.0 * body.GetMass(); // マウスジョイントが引っ張る力
mouseJoint = world.CreateJoint(md);
body.SetAwake(true);// スリープ状態解除
}
}
if(mouseJoint) {
// マウスドラッグしたとき
if(isMouseDown) {
mouseJoint.SetTarget(new b2Vec2(mouseX, mouseY));
}
// ドロップしたとき
else {
world.DestroyJoint(mouseJoint);
mouseJoint = null;
}
}
world.Step(1 / 60, 10, 10); // 物理世界を更新する
world.DrawDebugData(); // デバック描画
world.ClearForces(); // 物理世界上の力をリセットする
}
document.addEventListener("mousedown", function(e) {
isMouseDown = true;
handleMouseMove(e);
document.addEventListener("mousemove", handleMouseMove, true);
}, true);
document.addEventListener("mouseup", function() {
document.removeEventListener("mousemove", handleMouseMove, true);
isMouseDown = false;
mouseX = undefined;
mouseY = undefined;
}, true);
// マウス位置取得
function handleMouseMove(e) {
mouseX = (e.clientX - canvasPosition.x) / 30;
mouseY = (e.clientY - canvasPosition.y) / 30;
};
// マウスの位置にあるBodyを取得する
function getBodyAtMouse() {
mousePVec = new b2Vec2(mouseX, mouseY);
// 当たり判定用のAABB作成
var aabb = new b2AABB();
aabb.lowerBound.Set(mouseX - 0.001, mouseY - 0.001);// ボックスの左上座標
aabb.upperBound.Set(mouseX + 0.001, mouseY + 0.001);// ボックスの右下座標
selectedBody = null;
// 指定のAABB領域内のBodyを探索する
world.QueryAABB(getBodyCB, aabb);
return selectedBody;
}
// AABB領域内のBodyを探索する関数
function getBodyCB(fixture) {
//console.log(fixture);
// 静的Bodyじゃない場合(動的Bodyの場合)
if(fixture.GetBody().GetType() != b2Body.b2_staticBody) {
// マウスとBodyの当たり判定
if(fixture.GetShape().TestPoint(fixture.GetBody().GetTransform(), mousePVec)) {
selectedBody = fixture.GetBody();// マウス位置にあるBodyを取得
return false;// 探索を中止する
}
}
return true; // AABB領域内の次のfixtureを探す
}
// ヘルパー関数
// canvas位置取得用
function getElementPosition(element) {
var elem=element, tagname="", x=0, y=0;
while((typeof(elem) == "object") && (typeof(elem.tagName) != "undefined")) {
y += elem.offsetTop;
x += elem.offsetLeft;
tagname = elem.tagName.toUpperCase();
if(tagname == "BODY")
elem=0;
if(typeof(elem) == "object") {
if(typeof(elem.offsetParent) == "object")
elem = elem.offsetParent;
}
}
// タグの位置を返す
return {x: x, y: y};
}
</script>
今回は、マウスジョイント用のb2MouseJointDefオブジェクトと
マウス当たり判定用のb2AABBオブジェクトを使います
var b2MouseJointDef = Box2D.Dynamics.Joints.b2MouseJointDef // マウスジョイント
var b2AABB = Box2D.Collision.b2AABB // AABB(axis-aligned bounding box)
マウスジョイントの定義です。
自作のgetBodyAtMouse関数(後述)でマウス位置のBodyを取得します。
Bodyが存在するならばマウスジョイントを作成します。
var body = getBodyAtMouse();
if(body) {
// マウスジョイント作成
var md = new b2MouseJointDef();
md.bodyA = world.GetGroundBody(); // 物理世界
md.bodyB = body; // ひも付け対象のbody
md.target.Set(mouseX, mouseY); // マウスジョイント位置
md.collideConnected = true; // 二つのボディの衝突判定を行う
md.maxForce = 300.0 * body.GetMass(); // マウスジョイントが引っ張る力
mouseJoint = world.CreateJoint(md);
body.SetAwake(true);// スリープ状態解除
}
bodyAには普通は物理世界を指定します。
bodyBは引っ張りたいオブジェクトを指定します
collideConnectedは二つのボディ同士の衝突判定を行うフラグです。
デフォルトではfalseですが、今回のように片方のBodyが物理世界の場合、trueにしないと引っ張れません。
maxForceはマウスジョイントが引っ張る力を指定します。
SetAwake関数は
強制的にスリープ状態を解除しています
body.SetAwake(true);// スリープ状態解除
Box2dではBodyが動かなくなると灰色になり
他のオブジェクトがぶつかったりして動くまで、物理計算処理の対象から除外されます(計算負荷軽減のためです)
この処理はスリープ状態を強制的に解除するための処理です。
マウスドラッグで移動したときに
マウスジョイントの位置を動かすことでジョイントしたBodyを引っ張ることが出来ます
マウスをドロップした(離した)ときにマウスジョイントを破壊することでBodyを放り投げることが出来ます
if(mouseJoint) {
// マウスドラッグしたとき
if(isMouseDown) {
mouseJoint.SetTarget(new b2Vec2(mouseX, mouseY));
}
// ドロップしたとき
else {
world.DestroyJoint(mouseJoint);
mouseJoint = null;
}
}
マウス位置内にあるBodyを取得するために
AABBを作成して当たり判定を行います(getBodyAtMouse関数)
// マウスの位置にあるBodyを取得する
function getBodyAtMouse() {
mousePVec = new b2Vec2(mouseX, mouseY);
// 当たり判定用のAABB作成
var aabb = new b2AABB();
aabb.lowerBound.Set(mouseX - 0.001, mouseY - 0.001);
aabb.upperBound.Set(mouseX + 0.001, mouseY + 0.001);
selectedBody = null;
// 指定のAABB領域内のBodyを探索する
world.QueryAABB(getBodyCB, aabb);
return selectedBody;
}
AABB(axis-aligned bounding box)は軸に平行な包括ボックスで当たり判定などでよく使われます。
今回はマウス位置に対して擬似的なAABBを作成してます。
ちなみに、軸に対して回転を許す包括ボックスにOBB(oriented bounding box)というものもあります。
こちらは向きも考慮するため、
包括したときの隙間がAABBよりも少なく精度が良いのですが、その分計算コストが高くなります。
aabbが作成できたら
QueryAABB関数で
AABB領域内のBodyの探索を行います
getBodyCB関数は自作の関数です。
// 指定のAABB領域内のBodyを探索する
world.QueryAABB(getBodyCB, aabb);
getBodyCB関数は次のように作成しています。
戻り値bool型の関数です。
引数はfixtureを指定します。
// AABB領域内のBodyを探索する関数
function getBodyCB(fixture) {
//console.log(fixture);
// 静的Bodyじゃない場合(動的Bodyの場合)
if(fixture.GetBody().GetType() != b2Body.b2_staticBody) {
// マウスとBodyの当たり判定
if(fixture.GetShape().TestPoint(fixture.GetBody().GetTransform(), mousePVec)) {
selectedBody = fixture.GetBody();// マウス位置にあるBodyを取得
return false;// 探索を中止する
}
}
return true; // AABB領域内の次のfixtureを探す
}
fixtureのGetBody関数でBodyを取得します。
静的Bodyはジョイントしても動かせないため、除外しています。
この関数は、戻り値trueでAABB領域内の次のfixtureを探し(再度getBodyCBがコールバックされます)、
マウス領域内にBodyがあれば取得し、戻り値falseで探索を中止します。
今回でbox2dwebのジョイントは終わりで
次回からはより実践的な使い方をしていきます。
(キー入力での制御、衝突検知、センサー等)