您在這裡

如何用 HTML5 與 Box2D 建立遊戲

1 篇文章 / 0 新
如何用 HTML5 與 Box2D 建立遊戲

=============================================
原文來自: Build your First Game with HTML5
作者: Anderson Rodrigues
難度: 入門
=============================================

HTML5 是一個目前成長快速、令人眼睛為之一亮的 Internet 技術規格,各種應用領域都在此發展專業且強大的解決方案,包括遊戲製作技術。本文會帶大家運用知名的物理引擎 Box2D 和 HTML5 的 canvas 標籤來實做第一個簡單的 HTML 遊戲。

下載原始碼 | Live DEMO

  • 第一步 - 設定專案

開始實作之前,先從此處下載 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 專案