=============================================
原文來自: Build your First Game with HTML5
作者: Anderson Rodrigues
難度: 入門
=============================================
HTML5 是一個目前成長快速、令人眼睛為之一亮的 Internet 技術規格,各種應用領域都在此發展專業且強大的解決方案,包括遊戲製作技術。本文會帶大家運用知名的物理引擎 Box2D 和 HTML5 的 canvas 標籤來實做第一個簡單的 HTML 遊戲。
- 第一步 - 設定專案
開始實作之前,先從此處下載 HTML5 移植版本的 Box2D,然後建立一個新的 index.html 檔案,並將壓縮檔解開後的 js 與 lib 兩個子目錄複製到你的專案目錄。

現在可以把 box2d 相關的函式庫引用至你的 html 檔案內。
<!--[if IE]><script src="lib/excanvas.js"></script><![endif]--> <script src="lib/prototype-1.6.0.2.js"></script> <!-- box2djs --> <script src='js/box2d/common/b2Settings.js'></script> <script src='js/box2d/common/math/b2Vec2.js'></script> <script src='js/box2d/common/math/b2Mat22.js'></script> <script src='js/box2d/common/math/b2Math.js'></script> <script src='js/box2d/collision/b2AABB.js'></script> <script src='js/box2d/collision/b2Bound.js'></script> <script src='js/box2d/collision/b2BoundValues.js'></script> <script src='js/box2d/collision/b2Pair.js'></script> <script src='js/box2d/collision/b2PairCallback.js'></script> <script src='js/box2d/collision/b2BufferedPair.js'></script> <script src='js/box2d/collision/b2PairManager.js'></script> <script src='js/box2d/collision/b2BroadPhase.js'></script> <script src='js/box2d/collision/b2Collision.js'></script> <script src='js/box2d/collision/Features.js'></script> <script src='js/box2d/collision/b2ContactID.js'></script> <script src='js/box2d/collision/b2ContactPoint.js'></script> <script src='js/box2d/collision/b2Distance.js'></script> <script src='js/box2d/collision/b2Manifold.js'></script> <script src='js/box2d/collision/b2OBB.js'></script> <script src='js/box2d/collision/b2Proxy.js'></script> <script src='js/box2d/collision/ClipVertex.js'></script> <script src='js/box2d/collision/shapes/b2Shape.js'></script> <script src='js/box2d/collision/shapes/b2ShapeDef.js'></script> <script src='js/box2d/collision/shapes/b2BoxDef.js'></script> <script src='js/box2d/collision/shapes/b2CircleDef.js'></script> <script src='js/box2d/collision/shapes/b2CircleShape.js'></script> <script src='js/box2d/collision/shapes/b2MassData.js'></script> <script src='js/box2d/collision/shapes/b2PolyDef.js'></script> <script src='js/box2d/collision/shapes/b2PolyShape.js'></script> <script src='js/box2d/dynamics/b2Body.js'></script> <script src='js/box2d/dynamics/b2BodyDef.js'></script> <script src='js/box2d/dynamics/b2CollisionFilter.js'></script> <script src='js/box2d/dynamics/b2Island.js'></script> <script src='js/box2d/dynamics/b2TimeStep.js'></script> <script src='js/box2d/dynamics/contacts/b2ContactNode.js'></script> <script src='js/box2d/dynamics/contacts/b2Contact.js'></script> <script src='js/box2d/dynamics/contacts/b2ContactConstraint.js'></script> <script src='js/box2d/dynamics/contacts/b2ContactConstraintPoint.js'></script> <script src='js/box2d/dynamics/contacts/b2ContactRegister.js'></script> <script src='js/box2d/dynamics/contacts/b2ContactSolver.js'></script> <script src='js/box2d/dynamics/contacts/b2CircleContact.js'></script> <script src='js/box2d/dynamics/contacts/b2Conservative.js'></script> <script src='js/box2d/dynamics/contacts/b2NullContact.js'></script> <script src='js/box2d/dynamics/contacts/b2PolyAndCircleContact.js'></script> <script src='js/box2d/dynamics/contacts/b2PolyContact.js'></script> <script src='js/box2d/dynamics/b2ContactManager.js'></script> <script src='js/box2d/dynamics/b2World.js'></script> <script src='js/box2d/dynamics/b2WorldListener.js'></script> <script src='js/box2d/dynamics/joints/b2JointNode.js'></script> <script src='js/box2d/dynamics/joints/b2Joint.js'></script> <script src='js/box2d/dynamics/joints/b2JointDef.js'></script> <script src='js/box2d/dynamics/joints/b2DistanceJoint.js'></script> <script src='js/box2d/dynamics/joints/b2DistanceJointDef.js'></script> <script src='js/box2d/dynamics/joints/b2Jacobian.js'></script> <script src='js/box2d/dynamics/joints/b2GearJoint.js'></script> <script src='js/box2d/dynamics/joints/b2GearJointDef.js'></script> <script src='js/box2d/dynamics/joints/b2MouseJoint.js'></script> <script src='js/box2d/dynamics/joints/b2MouseJointDef.js'></script> <script src='js/box2d/dynamics/joints/b2PrismaticJoint.js'></script> <script src='js/box2d/dynamics/joints/b2PrismaticJointDef.js'></script> <script src='js/box2d/dynamics/joints/b2PulleyJoint.js'></script> <script src='js/box2d/dynamics/joints/b2PulleyJointDef.js'></script> <script src='js/box2d/dynamics/joints/b2RevoluteJoint.js'></script> <script src='js/box2d/dynamics/joints/b2RevoluteJointDef.js'></script>
js 函式庫的檔案數量相當可觀,但不用擔心,接著在專案目錄內的 js 子目錄裡面,建立兩個新檔案 "box2dutils.js" 和 "game.js"。
將以下代碼複製到 "box2dutils.js",文章稍後會逐步解釋內容。
function drawWorld(world, context) {
for (var j = world.m_jointList; j; j = j.m_next) {
drawJoint(j, context);
}
for (var b = world.m_bodyList; b; b = b.m_next) {
for (var s = b.GetShapeList(); s != null; s = s.GetNext()) {
drawShape(s, context);
}
}
}
function drawJoint(joint, context) {
var b1 = joint.m_body1;
var b2 = joint.m_body2;
var x1 = b1.m_position;
var x2 = b2.m_position;
var p1 = joint.GetAnchor1();
var p2 = joint.GetAnchor2();
context.strokeStyle = '#00eeee';
context.beginPath();
switch (joint.m_type) {
case b2Joint.e_distanceJoint:
context.moveTo(p1.x, p1.y);
context.lineTo(p2.x, p2.y);
break;
case b2Joint.e_pulleyJoint:
// TODO
break;
default:
if (b1 == world.m_groundBody) {
context.moveTo(p1.x, p1.y);
context.lineTo(x2.x, x2.y);
}
else if (b2 == world.m_groundBody) {
context.moveTo(p1.x, p1.y);
context.lineTo(x1.x, x1.y);
}
else {
context.moveTo(x1.x, x1.y);
context.lineTo(p1.x, p1.y);
context.lineTo(x2.x, x2.y);
context.lineTo(p2.x, p2.y);
}
break;
}
context.stroke();
}
function drawShape(shape, context) {
context.strokeStyle = '#000000';
context.beginPath();
switch (shape.m_type) {
case b2Shape.e_circleShape:
{
var circle = shape;
var pos = circle.m_position;
var r = circle.m_radius;
var segments = 16.0;
var theta = 0.0;
var dtheta = 2.0 * Math.PI / segments;
// draw circle
context.moveTo(pos.x + r, pos.y);
for (var i = 0; i < segments; i++) {
var d = new b2Vec2(r * Math.cos(theta), r * Math.sin(theta));
var v = b2Math.AddVV(pos, d);
context.lineTo(v.x, v.y);
theta += dtheta;
}
context.lineTo(pos.x + r, pos.y);
// draw radius
context.moveTo(pos.x, pos.y);
var ax = circle.m_R.col1;
var pos2 = new b2Vec2(pos.x + r * ax.x, pos.y + r * ax.y);
context.lineTo(pos2.x, pos2.y);
}
break;
case b2Shape.e_polyShape:
{
var poly = shape;
var tV = b2Math.AddVV(poly.m_position, b2Math.b2MulMV(poly.m_R, poly.m_vertices[0]));
context.moveTo(tV.x, tV.y);
for (var i = 0; i < poly.m_vertexCount; i++) {
var v = b2Math.AddVV(poly.m_position, b2Math.b2MulMV(poly.m_R, poly.m_vertices[i]));
context.lineTo(v.x, v.y);
}
context.lineTo(tV.x, tV.y);
}
break;
}
context.stroke();
}
function createWorld() {
var worldAABB = new b2AABB();
worldAABB.minVertex.Set(-1000, -1000);
worldAABB.maxVertex.Set(1000, 1000);
var gravity = new b2Vec2(0, 300);
var doSleep = true;
var world = new b2World(worldAABB, gravity, doSleep);
return world;
}
function createGround(world) {
var groundSd = new b2BoxDef();
groundSd.extents.Set(1000, 50);
groundSd.restitution = 0.2;
var groundBd = new b2BodyDef();
groundBd.AddShape(groundSd);
groundBd.position.Set(-500, 340);
return world.CreateBody(groundBd)
}
function createBall(world, x, y) {
var ballSd = new b2CircleDef();
ballSd.density = 1.0;
ballSd.radius = 20;
ballSd.restitution = 1.0;
ballSd.friction = 0;
var ballBd = new b2BodyDef();
ballBd.AddShape(ballSd);
ballBd.position.Set(x,y);
return world.CreateBody(ballBd);
}
function createBox(world, x, y, width, height, fixed, userData) {
if (typeof(fixed) == 'undefined') fixed = true;
var boxSd = new b2BoxDef();
if (!fixed) boxSd.density = 1.0;
boxSd.userData = userData;
boxSd.extents.Set(width, height);
var boxBd = new b2BodyDef();
boxBd.AddShape(boxSd);
boxBd.position.Set(x,y);
return world.CreateBody(boxBd)
}- 第二步 - 撰寫遊戲程式
編輯 index.html,把一個解析度 600x400 的 canvas 元件,如下方代碼加到 HTML body 區域內,我們會藉由 canvas 使用 HTML5 繪圖 API。
<canvas id="game" width="600" height="400"></canvas>
同樣的我們也需要在 index.html 內部引用自己寫的 js 檔案。
<script src='js/box2dutils.js'></script> <script src='js/game.js'></script>
HTML 相關檔案和函式庫的準備工作到此就緒,開始弄點 JavaScript 吧!
打開 game.js,貼入下方代碼。
// 此 DEMO 引用到的變數
var initId = 0;
var player = function(){
this.object = null;
this.canJump = false;
};
var world;
var ctx;
var canvasWidth;
var canvasHeight;
var keys = [];
// 網頁載入事件發生時,觸發這段功能
Event.observe(window, 'load', function() {
world = createWorld(); // box2DWorld
ctx = $('game').getContext('2d'); // 2
var canvasElm = $('game');
canvasWidth = parseInt(canvasElm.width);
canvasHeight = parseInt(canvasElm.height);
initGame(); // 3
step();
window.addEventListener('keydown',handleKeyDown,true);
window.addEventListener('keyup',handleKeyUp,true);
});OK,來解釋上面代碼的意思。
Box2DWorld 是 Box2D 核心內的一個重要類別,它負責彙整所有 Box2D 的功能,在註解 box2DWorld 處程式取得了 Box2DWorld 物件實體,裡面包含遊戲內所有物體的定義以及碰撞偵測管理器。
現在從 box2dutils.js 檔案內找到 createWorld() 函式,它創造 Box2DWorld 的物件。
function createWorld() {
// 在這裡我們建立世界物件並設定碰撞偵測
var worldAABB = new b2AABB();
worldAABB.minVertex.Set(-1000, -1000);
worldAABB.maxVertex.Set(1000, 1000);
// 設定重力參數
var gravity = new b2Vec2(0, 300);
var doSleep = true;
// 初始化世界物件然後回傳
var world = new b2World(worldAABB, gravity, doSleep);
return world;
}前一段代碼有個註解編號 2 的地方,當時取出的 canvas 物件稍後繪圖階段會用到,而註解編號 3 程式呼叫了有趣的函式 initGame(),它建立整個遊戲邏輯的場景。
關於 initGame(),將下列代碼貼至 game.js
function initGame(){
// 建立兩個較大平台
createBox(world, 3, 230, 60, 180, true, 'ground');
createBox(world, 560, 360, 50, 50, true, 'ground');
// 建立兩個較小的平台
for (var i = 0; i < 5; i++){
createBox(world, 150+(80*i), 360, 5, 40+(i*15), true, 'ground');
}
// 放顆球進去
var ballSd = new b2CircleDef();
ballSd.density = 0.1;
ballSd.radius = 12;
ballSd.restitution = 0.5;
ballSd.friction = 1;
ballSd.userData = 'player';
var ballBd = new b2BodyDef();
ballBd.linearDamping = .03;
ballBd.allowSleep = false;
ballBd.AddShape(ballSd);
ballBd.position.Set(20,0);
player.object = world.CreateBody(ballBd);
}記得之前 box2dutils.js 裡的代碼有個函式叫做 createBox(),負責建立矩形物體。
function createBox(world, x, y, width, height, fixed, userData) {
if (typeof(fixed) == 'undefined') fixed = true;
//1
var boxSd = new b2BoxDef();
if (!fixed) boxSd.density = 1.0;
//2
boxSd.userData = userData;
//3
boxSd.extents.Set(width, height);
//4
var boxBd = new b2BodyDef();
boxBd.AddShape(boxSd);
//5
boxBd.position.Set(x,y);
//6
return world.CreateBody(boxBd)
}
這段代碼有六個步驟:
1. 建立一個矩形形狀 (Box),設定它的密度 (與移動或滾動的容易度有關)
2. 此範例中我們用字串代表該矩形。
3. 設定矩形的大小。
4. 定義 Body 物件 (b2BodyDef),並把矩形塞給它。
5. 設定 Body 物件的位置
6. 建立 Body 物件的實體,並回傳之。
而之前貼到 game.js 的代碼建立玩家操控的球
var ballSd = new b2CircleDef(); ballSd.density = 0.1; ballSd.radius = 12; ballSd.restitution = 0.5; ballSd.friction = 1; ballSd.userData = 'player'; var ballBd = new b2BodyDef(); ballBd.linearDamping = .03; ballBd.allowSleep = false; ballBd.AddShape(ballSd); ballBd.position.Set(20,0); player.object = world.CreateBody(ballBd);
相當類似矩形的建立流程
1. 建立圓形 (b2CircleDef) 以及它的物理參數
2. 定義 Body 物件
3. 把圓形加入 Body 物件
4. 建立 Body 的實體!
就這樣,一些球狀物體和矩形物體都被加入了遊戲世界。跑跑看 DEMO,會看到球可從螢幕左方移動、墜落。
但手上的程式至此仍然是一片空白畫面,這是因為 Box2D 只負責物理運算,不包含繪製,我們得自己來!
- 第三步 - 繪圖程式
把剛剛建立的物理世界都畫出來。首先打開 game.js 加入這段程式。
function step() {
var stepping = false;
var timeStep = 1.0/60;
var iteration = 1;
// 1
world.Step(timeStep, iteration);
// 2
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
drawWorld(world, ctx);
// 3
setTimeout('step()', 10);
}
它有三個步驟:
1. 讓物理世界進行模擬。
2. 清除畫布,然後重繪一次。
3. 10ms 之後再次執行 step() 函式
這麼一來,我們便能顯示像這樣的畫面
當中 drawWorld() 函式的內容如下,它描述了繪製每個物體的工作流程。
function drawWorld(world, context) {
for (var j = world.m_jointList; j; j = j.m_next) {
drawJoint(j, context);
}
for (var b = world.m_bodyList; b; b = b.m_next) {
for (var s = b.GetShapeList(); s != null; s = s.GetNext()) {
drawShape(s, context);
}
}
}第一個迴圈畫出所有 joints (關節),在本範例中並沒有真正發揮 joints 的機制,所以先行略過。
第二個迴圈畫出所有定義過的物體。
function drawShape(shape, context) {
context.strokeStyle = '#000000';
context.beginPath();
switch (shape.m_type) {
case b2Shape.e_circleShape:
{
var circle = shape;
var pos = circle.m_position;
var r = circle.m_radius;
var segments = 16.0;
var theta = 0.0;
var dtheta = 2.0 * Math.PI / segments;
// 畫圓
context.moveTo(pos.x + r, pos.y);
for (var i = 0; i < segments; i++) {
var d = new b2Vec2(r * Math.cos(theta), r * Math.sin(theta));
var v = b2Math.AddVV(pos, d);
context.lineTo(v.x, v.y);
theta += dtheta;
}
context.lineTo(pos.x + r, pos.y);
// 畫出圓的半徑
context.moveTo(pos.x, pos.y);
var ax = circle.m_R.col1;
var pos2 = new b2Vec2(pos.x + r * ax.x, pos.y + r * ax.y);
context.lineTo(pos2.x, pos2.y);
}
break;
case b2Shape.e_polyShape:
{
var poly = shape;
var tV = b2Math.AddVV(poly.m_position, b2Math.b2MulMV(poly.m_R, poly.m_vertices[0]));
context.moveTo(tV.x, tV.y);
for (var i = 0; i < poly.m_vertexCount; i++) {
var v = b2Math.AddVV(poly.m_position, b2Math.b2MulMV(poly.m_R, poly.m_vertices[i]));
context.lineTo(v.x, v.y);
}
context.lineTo(tV.x, tV.y);
}
break;
}
context.stroke();
}作為範例,drawShape() 函式用線條掃描每個頂點來畫出物體 (context.moveTo 與 context.lineTo)。但在務實面,我們常會運用更高階的繪製功能,因此畫物體的時候比較關心物體的位置,不用像此範例反覆遊走物體邊緣的每個頂點。
- 第四步 - 互動操作
遊戲想用方向鍵來控制球的移動,為此需要加點程式到 game.js 內。
function handleKeyDown(evt){
keys[evt.keyCode] = true;
}
function handleKeyUp(evt){
keys[evt.keyCode] = false;
}
// 避免方向鍵操作瀏覽器捲軸 :)
document.onkeydown=function(event){
return event.keyCode!=38 && event.keyCode!=40
}我們在 handleKeyDown 和 handleKeyUp 內紀錄哪些鍵被按到,另外也在 document.onkeydown 攔截瀏覽器內的鍵盤事件,避免方向鍵的功能作用在瀏覽器捲軸上,使得遊戲畫面區域被捲軸影響到。
然後在 step() 內的開始加入一個函式
handleInteractions();
定義如下
function handleInteractions(){
// 上方向鍵
// 1
var collision = world.m_contactList;
player.canJump = false;
if (collision != null){
if (collision.GetShape1().GetUserData() == 'player' || collision.GetShape2().GetUserData() == 'player'){
if ((collision.GetShape1().GetUserData() == 'ground' || collision.GetShape2().GetUserData() == 'ground')){
var playerObj = (collision.GetShape1().GetUserData() == 'player' ?
collision.GetShape1().GetPosition() :
collision.GetShape2().GetPosition());
var groundObj =
(collision.GetShape1().GetUserData() == 'ground' ?
collision.GetShape1().GetPosition() :
collision.GetShape2().GetPosition());
if (playerObj.y < groundObj.y){
player.canJump = true;
}
}
}
}
// 2
var vel = player.object.GetLinearVelocity();
// 3
if (keys[38] && player.canJump){
vel.y = -150;
}
// 4
// 左右方向鍵
if (keys[37]){
vel.x = -60;
}
else if (keys[39]){
vel.x = 60;
}
// 5
player.object.SetLinearVelocity(vel);
}上述 5 段註解的第 1 段最複雜,程式寫各種條件來檢查碰撞的情形。檢查 shape1 或 shape2 是不是玩家物體,然後檢查 shape1 或 shape2 是不是其他物體。 如果玩家有和其他物體保持接觸,遊戲才允許玩家進行跳躍。
在註解 2 取得玩家的直線移動速度
註解 3 與 4 改變遊戲數值,方向鍵被按下的時候,改變了物體的速度。
最後註解 5 之處,把速度設回給玩家物體。
如此便完成了互動操作的功能,現在球可以跳來跳去啦。
- 第五步 - "You Win" 訊息
把這些程式寫在 step() 內最前面
if (player.object.GetCenterPosition().y > canvasHeight){
player.object.SetCenterPosition(new b2Vec2(20,0),0)
}
else if (player.object.GetCenterPosition().x > canvasWidth-50){
showWin();
return;
}
第一個 if 判斷玩家有沒有掉到無底洞,若有則把位置擺回重生點。
第二個 if 檢察玩家由沒有走到終點,若有就呼叫 showWin 函式顯示成功訊息。
最後加上 showWin 函式顯示訊息
function showWin(){
ctx.fillStyle = '#000';
ctx.font = '30px verdana';
ctx.textBaseline = 'top';
ctx.fillText('Ye! you made it!', 30, 0);
ctx.fillText('thank you, andersonferminiano.com', 30, 30);
ctx.fillText('@andferminiano', 30, 60);
}
以上! 我們用 Box2D 完成了第一個 HTML5 的小遊戲,恭喜!!
完整代碼可參考 github 專案



