=============================================
原文來自: 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 專案