HTML5遊戲開發進階 3 :物理引擎基礎

CopperDong發表於2018-02-09

    Box2D最初是有Erin Catto用C++編寫的。很多流行的物理模擬類遊戲都使用了該引擎,包含《憤怒的小鳥》,該引擎已經被轉化為幾種不同語言的版本,包括Java,ActionScript,C#和JavaScript。JS版本又被稱為Box2dWeb。

    http://code.google.com/p/box2dweb

3.1 Box2D基礎

     Box2D使用一些基本物件來定義和模擬遊戲世界,其中最重要的有如下幾個:

  • world:世界,Box2D的主物件,包含世界中的所有物體,對遊戲中的物理現象進行模擬
  • body:剛性的物體,可能由一個或多個形狀組成,這些形狀通過載具新增到物體上。
  • shape:二維形狀,如圓或多邊形這些Box2D中用到的基本形狀。
  • fixture:載具,用來向物體上新增形狀以監測碰撞。載具還包括一些非形狀資料,如摩擦係數、碰撞係數和碰撞閥值。
  • joint:接合點,用來以不同的方式將兩個物體連線在一起。比如,一個旋轉接合點使兩個物體共享一個點,它們可以自由地繞著該點旋轉。

      在遊戲中使用Box2D時,首先要定義遊戲的world物件,然後使用載具新增物體和形狀。接著,逐個操作world物件中的body物件,讓Box2D確定它們的位置和狀態。最後,將所有body物件繪製出來。最複雜的計算(即確定物體的位置和狀態)由Box2D world物件完成。

      引入Box2D:box2d.html

<!DOCTYPE html>
<html>
	<head>
		<meta http-equiv="Content-type" content="text/html; charset=utf-8">
		<title>Box2d Test</title>
		<script src="Box2dWeb-2.1.a.3.min.js" type="text/javascript" charset="utf-8"></script>		
		<script src="box2d.js" type="text/javascript" charset="utf-8"></script>
	</head>
	<body onload="init();">
		<canvas id="canvas" width="640" height="480" style="border:1px solid black;">Your browser does not support HTML5 Canvas</canvas>
	</body>
</html>
     box2d.js

//為了方便,將常用的物件定義為快捷變數
var b2Vec2 = Box2D.Common.Math.b2Vec2;
var b2BodyDef = Box2D.Dynamics.b2BodyDef;
var b2Body = Box2D.Dynamics.b2Body;
var b2FixtureDef = Box2D.Dynamics.b2FixtureDef;
var b2Fixture = Box2D.Dynamics.b2Fixture;
var b2World = Box2D.Dynamics.b2World;
var b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape;
var b2CircleShape = Box2D.Collision.Shapes.b2CircleShape;
var b2DebugDraw = Box2D.Dynamics.b2DebugDraw;
var b2RevoluteJointDef = Box2D.Dynamics.Joints.b2RevoluteJointDef;

     定義World變數:重力加速度

//建立b2World物件
var world;
var scale = 30; //在canvas上的30畫素表示Box2d世界中的1米
function init() {
	// 建立Box2D world物件,該物件將完成大部分物理計算
	var gravity = new b2Vec2(0, 9.8); //重力加速度9.8m/s^2
	var allowSleep = true;  //允許靜止的物體進入休眠狀態,休眠物體不參與物理模擬計算
	world = new b2World(gravity, allowSleep);
}

     新增第一物體: 地面  createFloor()
     在Box2D中建立任何物體都需經過以下步驟:

  1. 使用b2BodyDef宣告一個body的預定義物件。該物件包含了物體的細節,如物體的位置(x,y)和物體的型別(靜態和動態)。靜態物體不會受重力加速度和其他物體的碰撞影響。
  2. 使用b2FixtureDef宣告一個fixture的預定義物件。載具用來給物體指定形狀。該物件也包含了一些其他資訊,如指定形狀的密度、摩擦係數和彈性恢復係數。
  3. 設定載具指定的形狀。這裡使用Box2D中兩種型別的形狀:多邊形(b2polygonshape)和圓(b2circleshape)。
  4. 將物體的預定義物件傳入createBody()方法中,返回一個body物件。
  5. 將載具的預定義物件傳入createFixture()方法中,為物體指定形狀。

      繪製世界:除錯繪圖模式 setupDebugDraw()

      使用DrawDebugData()在給定的canvas上繪製出世界

      動畫:步驟

  • 告訴Box2D為很短時間進行一次模擬。使用world.Step()方法完成這一步
  • 將所有的物體重新繪製在新的位置。使用world.DrawDebugData()或這些物體自身的繪製函式
  • 使用world.ClearForces()方法清除作用在物體上的力。

//為了方便,將常用的物件定義為快捷變數
var b2Vec2 = Box2D.Common.Math.b2Vec2;
var b2BodyDef = Box2D.Dynamics.b2BodyDef;
var b2Body = Box2D.Dynamics.b2Body;
var b2FixtureDef = Box2D.Dynamics.b2FixtureDef;
var b2Fixture = Box2D.Dynamics.b2Fixture;
var b2World = Box2D.Dynamics.b2World;
var b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape;
var b2CircleShape = Box2D.Collision.Shapes.b2CircleShape;
var b2DebugDraw = Box2D.Dynamics.b2DebugDraw;
var b2RevoluteJointDef = Box2D.Dynamics.Joints.b2RevoluteJointDef;

//建立b2World物件
var world;
var scale = 30; //在canvas上的30畫素表示Box2d世界中的1米
function init() {
	// 建立Box2D world物件,該物件將完成大部分物理計算
	var gravity = new b2Vec2(0, 9.8); //重力加速度9.8m/s^2
	var allowSleep = true;  //允許靜止的物體進入休眠狀態,休眠物體不參與物理模擬計算
	world = new b2World(gravity, allowSleep);

	createFloor();
	setupDebugDraw();
	animate();
}
//建立地面
function createFloor() {
	//body的預定義物件,包含建立body剛體需要用到的所有資料
	var bodyDef = new b2BodyDef;
	bodyDef.type = b2Body.b2_staticBody; //靜態 地面不受重力或其他物體碰撞影響
	bodyDef.position.x = 640/2/scale;  // 位置 x=320px, y=450px
	bodyDef.position.y = 450/scale;     
	// fixture用來向body新增shape以實現碰撞檢測
	// fixture的預定義物件,用來建立fixture
	var fixtureDef = new b2FixtureDef;
	fixtureDef.density = 1.0;   //形狀的密度、摩擦係數、彈性恢復係數等屬性
	fixtureDef.friction = 0.5;
	fixtureDef.restitution = 0.2;
	fixtureDef.shape = new b2PolygonShape;  //多邊形
	fixtureDef.shape.SetAsBox(320/scale, 10/scale); //640寬,20高

	var body = world.CreateBody(bodyDef);
	var fixture = body.CreateFixture(fixtureDef);
}
//設定除錯繪圖
var context;
function setupDebugDraw() {
	context = document.getElementById("canvas").getContext('2d');
	var debugDraw = new b2DebugDraw();
	//使用canvas繪圖環境來繪製除錯畫面
	debugDraw.SetSprite(context);
	//設定繪圖比例
	debugDraw.SetDrawScale(scale);
	//填充的透明度為0.3
	debugDraw.SetFillAlpha(0.3);
	//線條的寬度為1
	debugDraw.SetLineThickness(1.0);
	//繪製所有的shape和joint
	debugDraw.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit);
	//設定調製繪圖模式
	world.SetDebugDraw(debugDraw);
}
//設立Box2D動畫迴圈
var timeStep = 1/60;
//按照Box2D手冊建議的迭代數,速度是8,位置是3
var velocityIterations = 8;
var positionIterations = 3;
function animate() {
	world.Step(timeStep, velocityIterations, positionIterations);
	world.ClearForces();
	world.DrawDebugData();
	setTimeout(animate, timeStep);
}

3.2 更多的Box2D元素

     Box2D允許向世界中新增不同種類的元素,包括:

  • 簡單形狀的物體,如矩形、圓、多邊形。
  • 複雜的由多個形狀組成的物體。
  • 接合點,如連線多個物體的旋轉接合點
  • 接觸監聽器,允許我們處理碰撞事件。

     建立矩形物體:createRectangularBody()

     建立圓形物體:createCircularBody()

     建立多邊形:createSimplePolygonBody()

     建立多種形狀的複雜物體:createComplexBody()
     連線物體的接合點:接合點用來將物體與物體之間連線起來。包括滑輪、齒輪、杆、轉動關節和焊接點。

     createRevoluteJoint()函式

3.3 追蹤碰撞與破壞

     在調查一個物體是否被損壞前,需要將該物體與“生命值”或“健康值”聯絡起來。

     建立具有自定義屬性的物體:createSpecialBody()

     接觸監聽器:b2ContactListener物件,有四個事件

  • BeginContact():兩個物體開始接觸時被呼叫
  • EndContact():兩個物體結束接觸時被呼叫
  • PostSolve():求解器完成後呼叫,進行碰撞檢測時很有用
  • PreSolve():在求解器求解前呼叫

     實現: listenForContact()函式

     摧毀物體:

3.4 繪製角色 

//為了方便,將常用的物件定義為快捷變數
var b2Vec2 = Box2D.Common.Math.b2Vec2;
var b2BodyDef = Box2D.Dynamics.b2BodyDef;
var b2Body = Box2D.Dynamics.b2Body;
var b2FixtureDef = Box2D.Dynamics.b2FixtureDef;
var b2Fixture = Box2D.Dynamics.b2Fixture;
var b2World = Box2D.Dynamics.b2World;
var b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape;
var b2CircleShape = Box2D.Collision.Shapes.b2CircleShape;
var b2DebugDraw = Box2D.Dynamics.b2DebugDraw;
var b2RevoluteJointDef = Box2D.Dynamics.Joints.b2RevoluteJointDef;

//建立b2World物件
var world;
var scale = 30; //在canvas上的30畫素表示Box2d世界中的1米
function init() {
	// 建立Box2D world物件,該物件將完成大部分物理計算
	var gravity = new b2Vec2(0, 9.8); //重力加速度9.8m/s^2
	var allowSleep = true;  //允許靜止的物體進入休眠狀態,休眠物體不參與物理模擬計算
	world = new b2World(gravity, allowSleep);

	createFloor();
	//建立一些具有簡單形狀的物體
	createRectangularBody();
	createCircularBody();
	createSimplePolygonBody();
	createComplexBody();
	createRevoluteJoint();

	createSpecialBody();
	listenForContact();

	setupDebugDraw();
	animate();
}
//建立地面
function createFloor() {
	//body的預定義物件,包含建立body剛體需要用到的所有資料
	var bodyDef = new b2BodyDef;
	bodyDef.type = b2Body.b2_staticBody; //靜態 地面不受重力或其他物體碰撞影響
	bodyDef.position.x = 640/2/scale;  // 位置 x=320px, y=450px
	bodyDef.position.y = 450/scale;     
	// fixture用來向body新增shape以實現碰撞檢測
	// fixture的預定義物件,用來建立fixture
	var fixtureDef = new b2FixtureDef;
	fixtureDef.density = 1.0;   //形狀的密度、摩擦係數、彈性恢復係數等屬性
	fixtureDef.friction = 0.5;
	fixtureDef.restitution = 0.2;
	fixtureDef.shape = new b2PolygonShape;  //多邊形
	fixtureDef.shape.SetAsBox(320/scale, 10/scale); //640寬,20高

	var body = world.CreateBody(bodyDef);
	var fixture = body.CreateFixture(fixtureDef);
}
//設定除錯繪圖
var context;
function setupDebugDraw() {
	context = document.getElementById("canvas").getContext('2d');
	var debugDraw = new b2DebugDraw();
	//使用canvas繪圖環境來繪製除錯畫面
	debugDraw.SetSprite(context);
	//設定繪圖比例
	debugDraw.SetDrawScale(scale);
	//填充的透明度為0.3
	debugDraw.SetFillAlpha(0.3);
	//線條的寬度為1
	debugDraw.SetLineThickness(1.0);
	//繪製所有的shape和joint
	debugDraw.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit);
	//設定調製繪圖模式
	world.SetDebugDraw(debugDraw);
}
//設立Box2D動畫迴圈
var timeStep = 1/60;
//按照Box2D手冊建議的迭代數,速度是8,位置是3
var velocityIterations = 8;
var positionIterations = 3;
function animate() {
	world.Step(timeStep, velocityIterations, positionIterations);
	world.ClearForces();
	world.DrawDebugData();
	setTimeout(animate, timeStep);

    //自定義繪製
    if (specialBody) {
    	drawSpecialBody();
    }

	//摧毀耗盡生命值的物體
	if (specialBody && specialBody.GetUserData().life <= 0){
		//world.DestroyBody(specialBody);
		//specialBody = undefined;
		console.log("destroyed");
	}
	setTimeout(animate, timeStep);
}
//建立矩形物體
function createRectangularBody(){
	var bodyDef = new b2BodyDef;
	bodyDef.type = b2Body.b2_dynamicBody;
	bodyDef.position.x = 40/scale;
	bodyDef.position.y = 100/scale;
	var fixtureDef = new b2FixtureDef;
	fixtureDef.density = 1.0;
	fixtureDef.friction = 0.5;
	fixtureDef.restitution = 0.3;
	fixtureDef.shape = new b2PolygonShape;  //多邊形
	fixtureDef.shape.SetAsBox(30/scale, 50/scale); //60寬,100高

	var body = world.CreateBody(bodyDef);
	var fixture = body.CreateFixture(fixtureDef);
}
//建立一個圓形
function createCircularBody(){
	var bodyDef = new b2BodyDef;
	bodyDef.type = b2Body.b2_dynamicBody;
	bodyDef.position.x = 130/scale;
	bodyDef.position.y = 100/scale;
	var fixtureDef = new b2FixtureDef;
	fixtureDef.density = 1.0;
	fixtureDef.friction = 0.5;
	fixtureDef.restitution = 0.7;
	fixtureDef.shape = new b2CircleShape(30/scale);  //

	var body = world.CreateBody(bodyDef);
	var fixture = body.CreateFixture(fixtureDef);
}
//建立多邊形物體
function createSimplePolygonBody(){
	var bodyDef = new b2BodyDef;
	bodyDef.type = b2Body.b2_dynamicBody;
	bodyDef.position.x = 230/scale;
	bodyDef.position.y = 50/scale;

	var fixtureDef = new b2FixtureDef;
	fixtureDef.density = 1.0;
	fixtureDef.friction = 0.5;
	fixtureDef.restitution = 0.2;
	fixtureDef.shape = new b2PolygonShape;  
	//按順時針方向建立一個b2Vec2頂點陣列
	var points = [
	   new b2Vec2(0,0),
	   new b2Vec2(40/scale,50/scale),
	   new b2Vec2(50/scale,100/scale),
	   new b2Vec2(-50/scale,100/scale),
	   new b2Vec2(-40/scale,50/scale),
	];
	fixtureDef.shape.SetAsArray(points, points.length);
	var body = world.CreateBody(bodyDef);
	var fixture = body.CreateFixture(fixtureDef);
}
// 建立多種形狀的複雜物體,由兩個形狀組成的物體
function createComplexBody(){
	var bodyDef = new b2BodyDef;
	bodyDef.type = b2Body.b2_dynamicBody;
	bodyDef.position.x = 350/scale;
	bodyDef.position.y = 50/scale;
	var body = world.CreateBody(bodyDef);

    //建立第一個載具併為物體新增圓形狀
	var fixtureDef = new b2FixtureDef;
	fixtureDef.density = 1.0;
	fixtureDef.friction = 0.5;
	fixtureDef.restitution = 0.7;
	fixtureDef.shape = new b2CircleShape(40/scale);  //
    body.CreateFixture(fixtureDef);

    //建立第二個
	fixtureDef.shape = new b2PolygonShape;  
	//按順時針方向建立一個b2Vec2頂點陣列
	var points = [
	   new b2Vec2(0,0),
	   new b2Vec2(40/scale,50/scale),
	   new b2Vec2(50/scale,100/scale),
	   new b2Vec2(-50/scale,100/scale),
	   new b2Vec2(-40/scale,50/scale),
	];
	fixtureDef.shape.SetAsArray(points, points.length);
	var fixture = body.CreateFixture(fixtureDef);
}
//建立轉動關節
function createRevoluteJoint(){
	//定義第一個物體
	var bodyDef1 = new b2BodyDef;
	bodyDef1.type = b2Body.b2_dynamicBody;
	bodyDef1.position.x = 480/scale;
	bodyDef1.position.y = 50/scale;
	var body1 = world.CreateBody(bodyDef1);

	var fixtureDef1 = new b2FixtureDef;
	fixtureDef1.density = 1.0;
	fixtureDef1.friction = 0.5;
	fixtureDef1.restitution = 0.5;
	fixtureDef1.shape = new b2PolygonShape;  //多邊形
	fixtureDef1.shape.SetAsBox(50/scale, 10/scale); //60寬,100高

	body1.CreateFixture(fixtureDef1);

    //定義第二個物體
	var bodyDef2 = new b2BodyDef;
	bodyDef2.type = b2Body.b2_dynamicBody;
	bodyDef2.position.x = 470/scale;
	bodyDef2.position.y = 50/scale;
	var body2 = world.CreateBody(bodyDef2);

	var fixtureDef2 = new b2FixtureDef;
	fixtureDef2.density = 1.0;
	fixtureDef2.friction = 0.5;
	fixtureDef2.restitution = 0.5;
	fixtureDef2.shape = new b2PolygonShape;  //多邊形
	var points = [
	   new b2Vec2(0,0),
	   new b2Vec2(40/scale,50/scale),
	   new b2Vec2(50/scale,100/scale),
	   new b2Vec2(-50/scale,100/scale),
	   new b2Vec2(-40/scale,50/scale),
	];
	fixtureDef2.shape.SetAsArray(points, points.length);

	body2.CreateFixture(fixtureDef2);

	//建立接合點連線body1和body2
	var jointDef = new b2RevoluteJointDef;
	var jointCenter = new b2Vec2(470/scale, 50/scale);

	jointDef.Initialize(body1, body2, jointCenter);
	world.CreateJoint(jointDef);	
}
//建立具有自定義屬性的物體
var specialBody;
function createSpecialBody(){
	var bodyDef = new b2BodyDef;
	bodyDef.type = b2Body.b2_dynamicBody;
	bodyDef.position.x = 450/scale;
	bodyDef.position.y = 0/scale;

	specialBody = world.CreateBody(bodyDef);
	specialBody.SetUserData({name:"special", life:250});

	var fixtureDef = new b2FixtureDef;
	fixtureDef.density = 1.0;
	fixtureDef.friction = 0.5;
	fixtureDef.restitution = 0.5;
	fixtureDef.shape = new b2CircleShape(30/scale);  //

	var fixture = specialBody.CreateFixture(fixtureDef);
}
//實現接觸監聽器
function listenForContact(){
	var listener = new Box2D.Dynamics.b2ContactListener;
	//引數為接觸和衝擊力(法向和切向衝擊力)
	listener.PostSolve = function(contact, impulse){
		var body1 = contact.GetFixtureA().GetBody();
		var body2 = contact.GetFixtureB().GetBody();
		//如果接觸的兩個物體都具有生命值,則減少其生命值
		if (body1 == specialBody || body2 == specialBody){
			var impulseAlongNormal = impulse.normalImpulses[0];
			//生命值減少
			specialBody.GetUserData().life -= impulseAlongNormal;
			console.log("impulse:", impulseAlongNormal, 
				"life:", specialBody.GetUserData().life);
		}
	};
	world.SetContactListener(listener);
}
//繪製自己的角色
function drawSpecialBody(){
	//獲取body的位置和角度
	var position = specialBody.GetPosition();
	var angle = specialBody.GetAngle();
	//移動並旋轉物體
	context.translate(position.x*scale, position.y*scale);
	context.rotate(angle);
	//繪製實心的圓面
	context.fillStyle = "rgb(200, 150, 250);";
	context.beginPath();
	context.arc(0,0,30,0.2*Math.PI,false);
	context.fill();
	//繪製兩個矩形的眼睛
	context.fillStyle = "rgb(255, 255, 255);";
	context.fillRect(-15, -15, 10, 5);
	context.fillRect(5, -15, 10, 5);
    //繪製向上或向下的圓弧,根據生命值決定是否微笑
    context.strokeStyle = "rgb(255, 255, 255);";
    context.beginPath();
    if (specialBody.GetUserData().life > 100){
    	context.arc(0,0,10,Math.PI,2*Math.PI,true);
    } else {
    	context.arc(0,10,10,Math.PI,2*Math.PI,false);
    }
    context.stroke();
    //移動並旋轉座標軸至最初的位置和角度
    context.rotate(-angle);
    context.translate(-position.x*scale, -position.y*scale);
}


相關文章