Box2d是一個2D遊戲物理引擎,由Erin Catto開發,於2007年發佈。很多2D遊戲都用過Box2d,其中最有名的自然是憤怒的小鳥。Box2d本身是C++編寫,但在不同平臺都有它的衍生版本,像Flash版的Box2dFlash,JS版的Box2dJS和Box2dWeb。最近偶然看到一篇使用Box2dFlash模擬箭矢飛行效果的文章:
http://www.emanueleferonato.com/2012/12/10/flying-arrows-simulation-with-box2d/
很有意思,想嘗試使用下Box2d。
之前從沒接觸過Flash,選擇JS版的Box2d,而Box2dJS已經很久沒更新,所以使用Box2dWeb重寫箭矢飛行效果。
網上有不少Box2d教程,不過介紹其應用的多些。對於Box2d基本概念和原理,推薦阿蕉的博客,他將Box2d C++的系列教程譯成中文。雖然C++和JS不同,但是Box2d原理是相通的,可以參考。
http://blog.csdn.net/wen294299195/article/category/1227604
首先下載Box2dWeb
https://code.google.com/p/box2dweb/downloads/list
壓縮包裏只有四個文件,這裏只需要Box2dWeb-2.1.a.3.min.js(也可以用Box2dWeb-2.1.a.3.js,方便了解Box2DWeb的各個函數)。
按照下面的目錄結構創建各個文件即可:
|-js/
| |-Box2dWeb-2.1.a.3.min.js
| |-game.js
|-arrow.html
編輯arrow.html,引用javascript文件並創建一個canvas標籤,代碼如下:
<!DOCTYPE HTML>
<html>
<head>
<title>Box2DWeb Test</title>
<scripttype="text/javascript"src="js/Box2dWeb-2.1.a.3.min.js"></script>
<scripttype="text/javascript" src="js/game.js"></script>
</head>
<bodyonload="init();">
<canvasid="canvas" width="640" height="480"style="background-color:#333333;"></canvas>
</body>
</html>
接下來編輯game.js。從arrow.html中可以看到,頁面載入後調用init方法模擬整個過程,添加下面的代碼:
function init() {
// Commen code for usingBox2D object.
var b2Vec2 =Box2D.Common.Math.b2Vec2;
var b2AABB =Box2D.Collision.b2AABB;
var b2BodyDef =Box2D.Dynamics.b2BodyDef;
var b2Body =Box2D.Dynamics.b2Body;
varb2FixtureDef = Box2D.Dynamics.b2FixtureDef;
var b2Fixture =Box2D.Dynamics.b2Fixture;
var b2World =Box2D.Dynamics.b2World;
varb2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape;
var b2DebugDraw =Box2D.Dynamics.b2DebugDraw;
/* 下文添加 */
};
以上代碼是爲了方便使用Box2d中的對象。接着就是設置全局屬性,像canvas的大小、Box2d的世界參數、鼠標消息響應方法等。
// Get canvas fordrawing.
var canvas =document.getElementById("canvas");
var canvasPosition =getElementPosition(canvas);
var context =canvas.getContext("2d");
// World constants.
var worldScale = 30;
var dragConstant=0.05;
var dampingConstant = 2;
var world = newb2World(new b2Vec2(0, 10),true);
document.addEventListener("mousedown",onMouseDown);
debugDraw();
window.setInterval(update,1000/60);
設置好Box2d的世界之後就可以放置各種模型。接下來的代碼創建四面牆壁來封閉區域:
// Create bottom wall
createBox(640,30,320,480,b2Body.b2_staticBody,null);
// Create top wall
createBox(640,30,320,0,b2Body.b2_staticBody,null);
// Create left wall
createBox(30,480,0,240,b2Body.b2_staticBody,null);
// Create right wall
createBox(30,480,640,240,b2Body.b2_staticBody,null);
functioncreateBox(width,height,pX,pY,type,data){
var bodyDef = new b2BodyDef;
bodyDef.type = type;
bodyDef.position.Set(pX/worldScale,pY/worldScale);
bodyDef.userData=data;
var polygonShape = new b2PolygonShape;
polygonShape.SetAsBox(width/2/worldScale,height/2/worldScale);
var fixtureDef = new b2FixtureDef;
fixtureDef.density = 1.0;
fixtureDef.friction = 0.5;
fixtureDef.restitution = 0.5;
fixtureDef.shape = polygonShape;
var body=world.CreateBody(bodyDef);
body.CreateFixture(fixtureDef);
}
希望點擊鼠標時,從左下角向鼠標點擊的位置發射一支箭。所以在鼠標點擊消息的響應方法中調用createArrow方法,根據傳入的座標生成一支箭,並賦給它初速度,代碼如下:
function onMouseDown(e) {
var evt = e||window.event;
createArrow(e.clientX-canvasPosition.x,e.clientY-canvasPosition.y);
}
function createArrow(pX,pY) {
// Set the left corner as the originalpoint.
var angle = Math.atan2(pY-450, pX);
// Define the shape of arrow.
var vertices = [];
vertices.push(new b2Vec2(-1.4,0));
vertices.push(new b2Vec2(0,-0.1));
vertices.push(new b2Vec2(0.6,0));
vertices.push(newb2Vec2(0,0.1));
var bodyDef = new b2BodyDef;
bodyDef.type = b2Body.b2_dynamicBody;
bodyDef.position.Set(40/worldScale,400/worldScale);
bodyDef.userData = "Arrow";
var polygonShape = new b2PolygonShape;
polygonShape.SetAsVector(vertices,4);
var fixtureDef = new b2FixtureDef;
fixtureDef.density = 1.0;
fixtureDef.friction = 0.5;
fixtureDef.restitution = 0.5;
fixtureDef.shape = polygonShape;
var body = world.CreateBody(bodyDef);
body.CreateFixture(fixtureDef);
// Set original state of arrow.
body.SetLinearVelocity(newb2Vec2(20*Math.cos(angle), 20*Math.sin(angle)));
body.SetAngle(angle);
body.SetAngularDamping(dampingConstant);
}
接下來就是系統方法,繪製物體並定時更新:
function debugDraw() {
var debugDraw = new b2DebugDraw();
debugDraw.SetSprite(document.getElementById("canvas").getContext("2d"));
debugDraw.SetDrawScale(worldScale);
debugDraw.SetFillAlpha(0.5);
debugDraw.SetLineThickness(1.0);
debugDraw.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit);
world.SetDebugDraw(debugDraw);
}
function update() {
world.Step(1/60,10,10);
world.ClearForces();
for(var b = world.m_bodyList; b != null; b = b.m_next){
if(b.GetUserData() === "Arrow") {
updateArrow(b);
}
}
world.DrawDebugData();
}
注意上面update方法中調用的updateArrow方法,它負責模擬箭矢在空中運動形態,讓整個過程更加真實。
functionupdateArrow(arrowBody) {
// Calculate arrow's fligth speed.
var flightSpeed =Normalize2(arrowBody.GetLinearVelocity());
// Calculate arrow's pointingdirection.
var bodyAngle = arrowBody.GetAngle();
var pointingDirection = new b2Vec2(Math.cos(bodyAngle),-Math.sin(bodyAngle));
// Calculate arrow's flightingdirection and normalize it.
var flightAngle =Math.atan2(arrowBody.GetLinearVelocity().y,arrowBody.GetLinearVelocity().x);
var flightDirection = newb2Vec2(Math.cos(flightAngle), Math.sin(flightAngle));
// Calculate dot production.
var dot = b2Dot( flightDirection,pointingDirection );
var dragForceMagnitude = (1 -Math.abs(dot)) * flightSpeed * flightSpeed * dragConstant *arrowBody.GetMass();
var arrowTailPosition =arrowBody.GetWorldPoint(new b2Vec2( -1.4, 0 ) );
arrowBody.ApplyForce( newb2Vec2(dragForceMagnitude*-flightDirection.x,dragForceMagnitude*-flightDirection.y),arrowTailPosition );
}
function b2Dot(a, b) {
return a.x * b.x + a.y * b.y;
}
function Normalize2(b) {
return Math.sqrt(b.x * b.x + b.y *b.y);
}
最後是getElementPosition方法,用於獲得canvas的偏移座標:
//http://js-tut.aardon.de/js-tut/tutorial/position.html
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};
}
程序的運行結果(Chrome 25.0.1323.1下測試)如下圖所示: