如何使用畫布、SVG 和多點觸控建立磚塊式拼圖遊戲

本教學課程從介紹基本概念開始,最後討論使用畫布和 SVG 的多點觸控影像拼圖遊戲,以漸進方式說明如何處理指標事件,例如從滑鼠、手寫筆或單指或多指 (多點觸控) 產生的事件。

附註  指標事件需要 Windows 8 或更新版本。
附註  自為 Internet Explorer 10 撰寫本文之後,Internet Explorer 指標事件實作有了些微變更。如需如何更新及滿足程式碼未來需要的詳細資訊,請參閱指標事件更新

簡介

在 Internet Explorer 10 和使用 JavaScript 的 Windows 市集應用程式中,開發人員可以使用一種稱為「指標」的輸入類型。這個內容中的指標可以是滑鼠、手寫筆、單指或多指與螢幕接觸的任一點。本教學課程先說明如何開始使用指標,然後逐步實作使用畫布和 SVG 的多重指標影像拼圖遊戲:

使用畫布和 SVG 的多點觸控拼圖影像

如果您已經知道如何使用滑鼠事件,那麼指標事件對您應該不陌生:MSPointerDownMSPointerMoveMSPointerUpMSPointerOver 以及 MSPointerOut 等。處理滑鼠和指標事件很簡單,如下列的範例 1 所示。此外,對多點觸控裝置而言,範例 1 在裝置能夠處理的任何同時接觸點 (手指) 數量都可正常運作。因為指標事件會針對每個螢幕接觸點觸發,所以可行。因此,類似下列基礎繪圖範例的應用程式不需要任何的特殊編碼,即可支援多點觸控:

範例 1 - 基礎繪圖應用程式


<!DOCTYPE html>
<html>

<head>
  <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
  <title>Example 1</title>
  <style>
    html {
      -ms-touch-action: double-tap-zoom;  
    }    
  </style>
</head>

<body>
  <canvas id="drawSurface" width="500" height="500" style="border: 1px black dashed;"></canvas>
  <script>
    var _canvas = document.getElementById("drawSurface");
    var _context = _canvas.getContext("2d");
    
    _context.fillStyle = "rgba(255, 0, 0, 0.5)";
    
    if (navigator.msPointerEnabled) {
      _canvas.addEventListener('MSPointerMove', paintCanvas, false);
    }
    else {
      _canvas.addEventListener('mousemove', paintCanvas, false);
    }
        
    function paintCanvas(evt) {
      _context.fillRect(evt.clientX, evt.clientY, 5, 5);
    }
  </script>
</body>

</html>

瀏覽器通常會針對本身用途而使用觸控事件。例如,若要向上捲動網頁,使用者可以觸控螢幕 (不觸碰到連結) 並往下拉。或者,若要放大頁面,可以使用兩指張開的捏合動作。在範例 1 中,我們不希望發生這些預設行為,否則以單指或多指建立繪圖時,反而會導致移動瀏覽 (或縮放) 頁面。為允許這些事件流向您的 JavaScript 程式碼,我們使用下列 CSS:


html {
  -ms-touch-action: double-tap-zoom;  
}

這會指示瀏覽器「忽略」點兩下 (拉近頁面) 以外的所有觸控動作。換句話說,您的 JavaScript 程式碼現在可以使用所有觸控事件,只是無法擷取點兩下事件。-ms-touch-action 的其他可能值為 autononemanipulation 以及 inherit,如建置觸控友善之網站的指導方針

addEventListener 觀點而言,必須注意 Windows Internet Explorer 指標事件和傳統的滑鼠事件是互斥的,意味著當指標事件可用時,它們也會包含滑鼠事件。 換句話說,您不能同時以 mousemoveMSPointerMove 登錄 paintCanvas 事件接聽程式:



// DO NOT DO THE FOLLOWING:
_canvas.addEventListener('mousemove', paintCanvas, false);
_canvas.addEventListener('MSPointerMove', paintCanvas, false);

而是如果 MS 指標事件 (也會回報滑鼠事件) 可用,我們會使用它們。否則的話,我們就會使用傳統的滑鼠事件:


if (window.navigator.msPointerEnabled) {
  _canvas.addEventListener('MSPointerMove', paintCanvas, false);
}
else {
  _canvas.addEventListener('mousemove', paintCanvas, false);
}

因此,上一個程式碼片段便允許繪圖應用程式使用觸控或傳統 (非觸控) 裝置。

了解指標事件的基本概念後,就可以繼續更實際的用法 - 實作影像拼圖遊戲:

顯示使用畫布和 SVG 之多點觸控拼圖的影像

切割影像

建立這種遊戲的第一步,是建立影像片段或影像磚。canvas API 的 drawImage 方法可以讓您輕鬆地將來源影像切割成畫布,方法是設定要複製或顯示的影像部分。語法是 drawImage(image, i_x, i_y, i_width, i_eight, c_x, c_y, c_width, c_height)下面兩個圖例會顯示如何選取 <img> 元素的部分並在畫布上顯示:

顯示您可以將影像分割或切割成片段或磚的影像

來源影像

顯示您可以將影像分割或切割成片段或磚的影像

畫布

將來源拼圖影像分割成一系列的資料列和資料欄 (表格),drawImage 方法就可以套用到每個表格儲存格,以產生所需的個別影像磚:

顯示所有個別的磚如何拼湊出一個完整拼圖的影像

範例 2 將示範這個磚產生程序。若要檢視範例 2 的原始程式碼,用滑鼠右鍵按一下範例 2 頁面,選擇 [檢視原始檔]。範例 2 的討論分成下列兩節:

X-UA 相容的中繼標籤

由於範例 2 是使用 Internet Explorer 在近端內部網路上開發的,因此使用 <meta http-equiv="X-UA-Compatible" content="IE=10"> 標記來確保 Internet Explorer 已設定為正確的瀏覽器和文件模式。如需詳細資訊,請參閱定義文件相容性。一般而言,這個標記應在頁面實際使用之前移除。

影像切割

為了將上述 400 x 400 的拼圖影像轉換成實用的拼圖片段 (或磚),我們先建立一個影像的記憶體內部影像物件以待切割 (400x400.png),然後在影像完全載入後再叫用匿名函式:


var image = new Image();
image.src = "images/400x400.png";
image.onload = function () { ... }

依據來源影像的大小 (image.widthimage.height) 和需要的資料列和資料欄數目 (NUM_COLSNUM_ROWS) 來分割影像,我們在匿名的函式中計算所需的磚大小:


var dx = _canvas.width = image.width / NUM_COLS;
var dy = _canvas.height = image.height / NUM_ROWS;

我們將畫布寬度和高度設定為與磚相同的大小,因為我們將會從這個畫布建立所有這樣的磚。個別的磚影像會建立如下:


for (var row = 0; row < NUM_ROWS; row++) {
  for (var col = 0; col < NUM_COLS; col++) {
    ctx.drawImage(image, dx * col, dy * row, dx, dy, 0, 0, dx, dy);
    ctx.strokeRect(0, 0, dx, dy); // Place a border around each tile.
    slicedImageTable.push( _canvas.toDataURL() );
  }
}

為了更清楚這個雙重巢狀的 for 迴圈,可以假設我們想要的是 5 個資料列乘以 5 個資料欄的拼圖,而變數 rowcol 目前分別是 2 與 3。也就是說,我們目前是在來源影像的表格儲存格 (2, 3):

顯示 5 個資料列乘以 5 個資料欄的拼圖影像 (其中已選取表格儲存格座標 (2, 3))

如果來源影像的大小是 400 x 400 像素,則:


dx = canvas.width = 400 / 5 = 80
dy = canvas.height = 400 / 5 = 80

這會導致:


ctx.drawImage(image, 80*3, 80*2, 80, 80, 0, 0, 80, 80)

或:


ctx.drawImage(image, 240, 160, 80, 80, 0, 0, 80, 80)

換句話說,我們在 (240, 160) 位置擷取來源影像的 80 x 80 快照:

顯示已選取表格儲存格座標 (240, 160) 的拼圖快照影像

然後將快照放在 80px x 80px 畫布的左上角:

只顯示表格儲存格座標 (240, 160) 之快照的影像

這個畫布之後會轉換成資料 URL 影像,並儲存在切割的影像陣列中,如下所示:


slicedImageTable.push( _canvas.toDataURL() );

範例 2 的其餘部分會以取得 (切割) 個別磚的相同順序來顯示它們,確保拼圖影像順利切割。

將影像切片轉換成 SVG

既然我們可以產生影像切片 (即影像磚),接下來要研究如何將這些資料 URL 影像磚轉換成 SVG 影像物件。範例 3 將示範這個程序。若要檢視範例 3 的輸出,必須開啟您的瀏覽器偵錯程式主控台視窗。如需詳細資訊,請參閱如何使用 F12 開發人員工具偵錯您的網頁。範例 2 與 3 的主要差異在於 SVG 影像元素的建立和設定方法,如下所示:


for (var row = 0; row < NUM_ROWS; row++) {
  for (var col = 0; col < NUM_COLS; col++) {
    ctx.drawImage(image, dx*col, dy*row, dx, dy, 0, 0, dx, dy);
    ctx.strokeRect(0, 0, dx, dy); // Place a border around each tile.

    var svgImage = document.createElementNS('http://www.w3.org/2000/svg', 'image');
    svgImage.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", _canvas.toDataURL());
    svgImage.setAttribute('width', dx);
    svgImage.setAttribute('height', dy);
    svgImage.setAttribute('x', dx * col);
    svgImage.setAttribute('y', dy * row);
    svgImage.correctX = dx * col;
    svgImage.correctY = dy * row;
    slicedImageTable.push(svgImage);
  }
}

因為 SVG 是 XML 格式,在建立 SVG 元素時必須指定命名空間 (至少從 SVG 物件模型的外部):


var svgImage = document.createElementNS('http://www.w3.org/2000/svg', 'image')

相對於 src 屬性 (與非 SVG <img> 元素搭配使用),SVG 影像元素使用 href 屬性。此外也請注意,若是 Internet Explorer,svgImage.setAttribute('href', _canvas.toDataURL()) 可以用來設定 SVG 影像元素的 href 屬性。不過,其他瀏覽器可能需要 XLink 語法,這就是為什麼改用下列程式碼的理由:


svgImage.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", _canvas.toDataURL());

SVG 影像預設的 widthheight0,因此我們必須明確地設定這些值:


svgImage.setAttribute('width', dx);
svgImage.setAttribute('height', dy);

最後,對於和 SVG 檢視區相關的座標系統 (請參閱 SVG 座標轉換),我們建立並設定兩個自訂屬性 (correctXcorrectY),以記錄每個磚在正確 (即獲勝) 的拼圖中的位置。

顯示 SVG 影像

既然我們已將定位的 SVG 影像儲存在磚陣列中,下一個工作就是在螢幕上顯示它們。為輕鬆完成這個工作,我們將 SVG 元素 <svg> 和一些其他的增強指定新增到網頁,如以下三節和範例 4 (按一下滑鼠右鍵以檢視原始檔) 中說明和示範的內容。

流動性 SVG

除了簡化新增 SVG 元素外,我們也使用一些 CSS 屬性,使整個 SVG 檢視區具有流動性 (或流暢性)。第一個要考量的項目是 SVG 元素本身:


<svg width="75%" height="75%" viewbox="0 0 400 400"></svg>

這裡的正方形 SVG 檢視區是最小的瀏覽器檢視區維度的 75%,並且套用 400 x 400 單位的座標系統。為使流動性 SVG 如預期般作用,請確定已套用下列 CSS 規則:

  • htmlbody 元素的 height 需為 100%
    
    html, body {
      margin: 0;
      padding: 0;
      height: 100%
    }
    
    

    現在,因為瀏覽器的檢視區變小,因此 SVG 檢視區的內容也變小。請注意,固有的 400 x 400 單位的 SVG 檢視區座標系統不會變更 - 只有座標單位的大小會變更。

  • SVG 元素與 <img> 元素同樣是內嵌元素。因此,為了將它放在瀏覽器檢視區的中央,我們將 display 屬性設定為 block,並將其 leftright 邊界設定為 auto
    
    svg {
      display: block; 
      margin: 0 auto;
    }
    
    

功能偵測

由於我們正在使用 addEventListener、畫布及 SVG,因此在嘗試顯示拼圖之前,讓我們先偵測這些功能。範例 4 會執行這項工作,如下所示:


if (window.addEventListener) {
  window.addEventListener('load', main, false);
} else {
  document.getElementsByTagName('body')[0].innerHTML = "<h1>The addEventListener method is not supported - please upgrade your browser and try again.</h1>";
} // if-else

function main() {
  var game = {};
  game.canvas = document.createElement('canvas');
  game.hasCanvas = !!game.canvas.getContext;
  game.svg = document.getElementsByTagName('svg')[0];
  game.hasSVG = game.svg.namespaceURI == "http://www.w3.org/2000/svg";
  
  .
  .
  .

  if (!game.hasCanvas) {
    document.getElementsByTagName('body')[0].innerHTML = "<h1>Canvas is not supported - please upgrade your browser and try again.</h1>";
    return;
  }

  if (!game.hasSVG) {
    document.getElementsByTagName('body')[0].innerHTML = "<h1>SVG is not supported - please upgrade your browser and try again.</h1>";
    return;
  } 
        
  .
  .
  .
  
} // main

如您所見,如果支援 addEventListener 方法,我們只輸入 main 函式 (順道一提,如果不支援 addEventListener,畫布或 SVG 就不太可能是其中一項)。

當我們在 main 函式中之後,我們建立一個 game 變數,以包含遊戲所有相關的「全域」變數與狀態。如您所見,如果使用者的瀏覽器不支援所有必要的功能,就會顯示指出問題的訊息。

當我們確定所有必要的功能都可支援後,我們繼續在範例 3 中使用的相同方法,不同的是某些變數名稱已變更,例如 NUM_ROWS 變更為 game.numRows,而 slicedImageTable 變更為 game.tiles。接下來,當我們在雙重巢狀的 for 迴圈中建立所有 SVG 影像元素 (磚) 之後,我們要顯示這些元素,如下一節所述。

顯示磚

在計算和設定每個磚的位置後,只要將它們附加到 SVG 元素,就可以顯示 SVG 磚影像:


function displaySvgImages() {
  for (var i = 0; i < game.tiles.length; i++) {
    game.svg.appendChild(game.tiles[i]);
  }
 }

為確認 SVG 磚的流動性 (或流暢性),變更瀏覽器視窗的大小。請注意 SVG 檢視區如何始終維持瀏覽器檢視區最小維度 (寬度或高度) 的 75%。

移動磚

既然我們已經顯示拼圖磚,我們的下一個挑戰便是讓使用者使用指標裝置移動它們。我們首先將檢驗如何移動三個 SVG 圓圈以探索這個問題,如範例 5 所示 (按一下滑鼠右鍵以檢視原始檔)。

我們先建立一個全域陣列,以包含目前使用中的這些圓圈。也就是如 mousedownMSPointerDown 事件所指,已經「按一下」的這些圓圈:


var _activeCircles = [];

為協助說明下列程式碼片段,如果 mousemove 事件處理常式是直接附加到 SVG 圓圈 (似乎很合理),它 (很可惜的是) 會因為讓使用者移動滑鼠的速度太快而失去一個圓圈。也就是說,SVG 圓圈的移動無法跟上使用者非常快速的滑鼠移動。假設 mousemove 事件處理常式負責在螢幕中移動圓圈,在滑鼠和圓圈實際分開後,mousemove 事件處理常式就會停止執行,因此圓圈移動就會停止。這就是我們將 mousemove 事件處理常式附加到 window 物件 (而非 SVG 圓圈) 的理由 - 無論使用者移動滑鼠的速度多快,都絕不會失去無所不在的 window 物件:


if (navigator.msPointerEnabled) {
  window.addEventListener('MSPointerMove', handlePointerEvents, false);
} else {
  window.addEventListener('mousemove', handlePointerEvents, false);
}

如上一個程式碼片段所示,如果適用,我們登錄一個 MSPointerMove 指標事件處理常式 (它也會處理傳統的滑鼠事件),如果不適用,便登錄一個 mousemove 事件處理常式。請回想範例 1 中所提到的,您無法在同一個物件上同時登錄事件類型 MSPointerMovemousemove

接下來,我們在每個 SVG 圓圈元素上登錄指標向下與指標向上事件處理常式:


var svgCircles = document.getElementsByTagName('circle');
for (var i = 0; i < svgCircles.length; i++) {
  if (navigator.msPointerEnabled) {
    svgCircles[i].addEventListener('MSPointerDown', handlePointerEvents, false);
    svgCircles[i].addEventListener('MSPointerUp', handlePointerEvents, false);
  } else {
    svgCircles[i].addEventListener('mousedown', handlePointerEvents, false);
    svgCircles[i].addEventListener('mouseup', handlePointerEvents, false);
  }
} // for

請特別注意 handlePointerEvents 函式,這是接下來所要討論的:


function handlePointerEvents(evt) {
  var activeCircle; 
  var activeCircleIndex = evt.pointerId || 0;

  switch (evt.type) {
    case "mousedown":
    case "MSPointerDown":
      _svgElement.removeChild(evt.target);
      _svgElement.appendChild(evt.target);
      if (evt.pointerId) {
        evt.target.msSetPointerCapture(evt.pointerId);
      }
      _activeCircles[activeCircleIndex] = evt.target;
      break;
    case "mousemove":
    case "MSPointerMove":
      activeCircle = _activeCircles[activeCircleIndex];
      if (activeCircle) {
        var svgPoint = _svgElement.createSVGPoint();
        svgPoint.x = evt.clientX;
        svgPoint.y = evt.clientY;

        var ctm = activeCircle.getScreenCTM();
        svgPoint = svgPoint.matrixTransform(ctm.inverse());
        activeCircle.setAttribute('cx', svgPoint.x);
        activeCircle.setAttribute('cy', svgPoint.y);
      } // if
      break;
    case "mouseup":
    case "MSPointerUp":
      if (evt.pointerId) {
        _activeCircles[activeCircleIndex].msReleasePointerCapture(evt.pointerId);
      }              
      delete _activeCircles[activeCircleIndex];
      break;
    default:
      alert("Error in handlePointerEvents on: " + evt.type);
  } // switch
} // handlePointerEvents

為協助說明 handlePointerEvents,我們將逐步說明兩個案例 - 移動單一圓圈,然後同時移動兩個圓圈。

單一圓圈移動

有三個要處理的事件:向下、移動及向上。

向下事件

當使用者觸碰單一圓圈 (使用滑鼠、手寫筆或手指) 時,向下事件就會觸發,以叫用 handlePointerEvents。如果支援指標事件,evt.pointerId 將為非 Null,而 activeCircleIndex 將等於 evt.pointerId;否則 activeCircleIndex 將為 0 (由於 evt.pointerId || 0)。如果 evt.pointerIdnull,則一次只能有一個使用中的圓圈 (即 _activeCircles[0]),當滑鼠是唯一允許的指標裝置時,這是唯一的可能性。

接下來,switch 陳述式會檢查 evt.type 並將控制流程轉向 mousedown/MSPointerDown 子句。為確保使用中圓圈永遠在其他所有圓圈的上方,我們僅將它移除並附加到 DOM (最後附加的元素永遠是最後/最上方轉譯的元素)。

接著,如果 evt.pointerId 已定義,我們在 evt.target 上呼叫 msSetPointerCapture (也就是使用中的圓圈),讓圓圈可以繼續接收所有登錄的事件。這樣可讓圓圈實際移開又回到瀏覽器檢視區。

最後,我們將觸碰的圓圈記錄在使用中圓圈清單中:


_activeCircles[activeCircleIndex] = evt.target;

移動事件

當指標裝置移動時 (在 window 物件上),mousemove/MSPointerMove 事件處理常式 (handlePointerEvents) 就會執行,它會將控制流程轉向 switch 陳述式的 mousemove/MSPointerMove 子句。如果某個圓圈先前未被觸碰,_activeCircles 陣列將為空白,而 activeCircle = _activeCircles[activeCircleIndex] 將為 null。在這種情況下,我們直接跳到 break 陳述式,並結束 switch 陳述式。

另一方面,如果 activeCircle 不是 null (也就是有一個應該移動的使用中圓圈),我們就要將與瀏覽器檢視區相對的指標裝置座標 (evt.clientX, evt.clientY) 轉換成 400 x 400 SVG 座標系統。這是透過座標轉換矩陣 (CTM) 完成的。如需詳細資訊,請參閱 SVG 座標轉換

最後,我們將圓圈的中心點 (cx, cy) 移至轉換後的座標值:


activeCircle.setAttribute('cx', svgPoint.x);
activeCircle.setAttribute('cy', svgPoint.y);

向上事件

如果向上事件在某個圓圈上觸發,控制流程會轉向 switch 陳述式的 mouseup/MSPointerUp 子句。activeCircleIndex 所指示的觸控/按一下圓圈會從使用中圓圈清單移除,而且會透過 msRelasePointerCapture 釋放其擷取事件要求 (如果適用的話)。

兩個圓圈移動

如同單一圓圈案例,有三個要處理的事件:向下、移動及向上。

向下事件

在這個案例中,兩個 (幾乎同時) 圓圈向下事件物件流向 switchmousedown/MSPointerDown 子句。_activeCircles 陣列現在包含兩個圓圈物件 (用來存取它們的索引是相關的 evt.pointerId 值)。

移動事件

當兩個 (幾乎同時) 視窗移動事件物件流向 switchmousemove/MSPointerMove 子句時,就會依次移動每個圓圈,正如單一圓圈案例一樣。

向上事件

當每個 (幾乎同時) 圓圈向上事件物件流向 mouseup/MSPointerUp 子句時,就會依次清除每個圓圈,正如單一圓圈案例一樣。

影像拼圖

我們現在已經有建立功能完整 (但不見得出色) 的觸控式影像拼圖遊戲所需的所有基本片段。我們將這個冗長的討論拆成數個容易理解的部分。我們將從範例 6,也就是遊戲的架構開始 (完成的遊戲在範例 7 中提供,將於稍後說明)。

架構

相較於進階 SVG 動畫中提供的遊戲,拼圖的架構相對簡單,如範例 6 (按一下滑鼠右鍵以檢視原始檔) 所示。範例 6 的核心標記如下:


<table>
  <tr>
    <td colspan="3"><h1>A Multi-Touch Enabled Image Puzzle Using Canvas &amp; SVG</h1></td>
  </tr>
  <tr>
    <td id="playButtonWrapper"><button id="playButton">Play</button></td>
    <td id="messageBox">Always carefully study the image before clicking the play button!</td>
    <td id="scoreBox"><strong>Score:</strong> 0</td>
  </tr>
</table>
<svg width="75%" height="75%" viewBox="0 0 400 400">
  <rect width="100%" height="100%" style="fill: black; stroke: black;" />
</svg>

<rect> 元素用來將整個 SVG 檢視區塗成黑色 (無論其目前大小為何)。這為拼圖的磚提供「遊戲場」。這個效果可以在以下範例 6 的螢幕擷取畫面中看到:

顯示拼圖磚遊戲場架構的螢幕擷取畫面

範例 6 其餘的架構包含下列 JavaScript:


if (window.addEventListener) {
  window.addEventListener('load', main, false);
} else {
  document.getElementsByTagName('body')[0].innerHTML = "<h1>The addEventListener method is not supported - please upgrade your browser and try again.</h1>";
} // if-else

function main() {
  var imageList = ["images/puzzle0.png", "images/puzzle1.png", "images/puzzle2.png", "images/puzzle3.png", "images/puzzle4.png", "images/puzzle5.png"]; // Contains the paths to the puzzle images (and are cycled through as necessary).
  var game = new Game(2, imageList);

  function Game(size_in, imageList_in) {
    var gameData = {};
    gameData.elements = {};
    gameData.elements.canvas = document.createElement('canvas');
    gameData.elements.svg = document.getElementsByTagName('svg')[0];
    
    this.hasCanvas = !!gameData.elements.canvas.getContext;
    this.hasSVG = gameData.elements.svg.namespaceURI == "http://www.w3.org/2000/svg";

    this.init = function() { alert("The init() method fired.") };
  } // Game

  if (!game.hasCanvas) {
    document.getElementsByTagName('body')[0].innerHTML = "<h1>Canvas is not supported - please upgrade your browser and try again.</h1>";
    return;
  } 
  
  if (!game.hasSVG) {
    document.getElementsByTagName('body')[0].innerHTML = "<h1>SVG is not supported - please upgrade your browser and try again.</h1>";
    return; 
  } 

  // Assert: The user's browser supports all required features.

  game.init(); // Start the game.
} // main

如同前面的功能偵測一節所述,如果 addEventListener 可用,我們會觸發 main 函式。接著我們初始化 imageList 陣列,其包含遊戲中要使用的影像路徑 (也就是要分割為磚、要任意排列及要解題的影像)。如果使用者到了第七關,就會開始重複使用第一個影像 (puzzle0.png)。

我們接著叫用 Game 建構函式。第一個參數 2 會指示建構函式產生含有兩個資料欄和兩個資料列的 game 物件。當然,最後一個參數是要循環使用 (如有需要的話) 的拼圖影像清單。

在建構函式內,我們將其所有「全域」變數放到便於使用的 gameData 變數中。如果您不熟悉 this 關鍵字,this.hasCanvas = !!game.elements.canvas.getContext 陳述式會在建構函式建構的物件 (game 變數) 上建立並設定一個稱為 hasCanvas 的屬性。雙重否定 (!!) 只會將 game.elements.canvas.getContext 運算式強制為布林值 true (如果支援畫布的話)。

同樣地,this.init = function() { … } 會為建構函式建立的所有物件 (只有一個這種物件:game) 定義一個稱為 init 的方法。叫用其中的 game.init(),啟動遊戲。

多點觸控影像拼圖遊戲

我們現在可以將上述所有資訊合併到功能完整的多點觸控影像拼圖遊戲,如範例 7 所述。與範例 7 有關的原始程式碼看起來應該很熟悉,並且有完整的註解,不過,下列兩個部分可能需要額外說明才會更清楚:

磚隨機排列

createAndAppendTiles 函式中,我們產生 SVG 磚影像物件 (如範例 4 所述)。但在將它們附加到 SVG 元素之前,我們要將它們隨機排列,並確定產生的隨機排列樣式不會恰巧與原始的 (完整) 拼圖影像相符 (這只是第一關的 4 個磚會遇到的問題):


var randomizationOK = false; 
while (!randomizationOK) {
  coordinatePairs.sort(function() { return 0.5 - Math.random(); });
  for (var i = 0; i < gameData.tiles.length; i++) {
    if (gameData.tiles[i].correctX != coordinatePairs[i].x || 
        gameData.tiles[i].correctY != coordinatePairs[i].y) {
      randomizationOK = true;
      break;
    } // if
  } // for
} // while

為輕鬆地隨機排列磚,我們將它們相關的座標組放在陣列 (coordinatePairs) 中,並使用下列 JavaScript 陣列排序方法:



coordinatePairs.sort(function() { return 0.5 - Math.random(); });

如同 sort 方法 (JavaScript) 中所述,並且假設 Math.random() 會傳回一個介於 01 之間的值,這個匿名的 sort 函式會隨機排序陣列的元素。

擴充的指標向上子句

switch 陳述式內的向下與移動子句與先前的範例幾乎相同。不過,向上子句則大幅擴充:



case "mouseup":
case "MSPointerUp":
activeTile = gameData.activeTiles[activeTileIndex];
var currentX = activeTile.getAttribute('x');
var currentY = activeTile.getAttribute('y');

for (var i = 0; i < gameData.tiles.length; i++) {
  var correctX = gameData.tiles[i].correctX;
  var correctY = gameData.tiles[i].correctY;

  if (currentX >= (correctX - gameData.snapDelta) && currentX <= (correctX + gameData.snapDelta) && 
      currentY >= (correctY - gameData.snapDelta) && currentY <= (correctY + gameData.snapDelta)) {
    activeTile.setAttribute('x', correctX);
    activeTile.setAttribute('y', correctY);
    break;
  } // if
} // for

if (evt.pointerId) {
  gameData.activeTiles[activeTileIndex].msReleasePointerCapture(evt.pointerId);
} 

delete gameData.activeTiles[activeTileIndex];

if (gameData.inProgress) {
  for (var i = 0; i < gameData.tiles.length; i++) {
    currentX = Math.round(gameData.tiles[i].getAttribute('x'));
    currentY = Math.round(gameData.tiles[i].getAttribute('y'));

    correctX = Math.round(gameData.tiles[i].correctX);
    correctY = Math.round(gameData.tiles[i].correctY);

    if (currentX != correctX || currentY != correctY) {
      return;
    } // if
  } // for

  // Assert: The user has solved the puzzle.

  gameData.inProgress = false;

  gameData.score += gameData.size * gameData.size;
  gameData.elements.scoreBox.innerHTML = "<strong>Score:</strong> " + gameData.score;

  ++(gameData.size);

  var randomIndex = Math.floor(Math.random() * gameData.congrats.length);
  document.getElementById('messageBox').innerHTML = gameData.congrats[randomIndex];

  for (var i = 0; i < gameData.tiles.length; i++) {
    gameData.elements.svg.removeChild(gameData.tiles[i])
  }
  
  createAndAppendTiles();

  gameData.elements.playButton.innerHTML = "Play";
} // if
break;

向上子句第一個要討論的部分是磚貼齊。對許多拼圖遊戲 (包括這個遊戲) 而言,在適當移動使用中的拼圖片段 (磚) 時,使磚貼齊至靠近正確位置是必備的條件。用於執行這項工作的程式碼 (從先前的範例複製的) 顯示如下:


activeTile = gameData.activeTiles[activeTileIndex];
var currentX = activeTile.getAttribute('x');
var currentY = activeTile.getAttribute('y');

for (var i = 0; i < gameData.tiles.length; i++) {
  var correctX = gameData.tiles[i].correctX;
  var correctY = gameData.tiles[i].correctY;

  if (currentX >= (correctX - gameData.snapDelta) && currentX <= (correctX + gameData.snapDelta) && 
      currentY >= (correctY - gameData.snapDelta) && currentY <= (correctY + gameData.snapDelta)) {
    activeTile.setAttribute('x', correctX);
    activeTile.setAttribute('y', correctY);
    break; // We've positioned the active tile correctly, so exit the FOR loop now.
  } // if
} // for

在這個程式碼片段中,我們取得使用中的磚 (當向上事件觸發時) 的位置,然後逐一查看所有磚以決定所有磚的正確位置。如果使用中的磚的位置已經非常靠近這些正確位置,我們就將它貼齊到位置並立即中斷 for 迴圈 (不需再查看任何其他的正確位置)。if 子句會隱含地在正確位置中心的周圍繪製一個小型的衝突偵測方塊,如果使用中的磚的位置落在其中,該子句會變成 true,並透過下列兩種 setAttribute 方法呼叫將磚貼齊到位置:


if (currentX >= (correctX - gameData.snapDelta) && currentX <= (correctX + gameData.snapDelta) && 
    currentY >= (correctY - gameData.snapDelta) && currentY <= (correctY + gameData.snapDelta)) {
  activeTile.setAttribute('x', correctX);
  activeTile.setAttribute('y', correctY);
  break;
} // if

請注意,隨著 gameData.snapDelta 增加,衝突偵測方塊的大小也會增加,使得磚貼齊將較不敏感。

接下來,如果遊戲目前正在進行,我們會逐一查看所有磚並檢查   「暴力解法」,確認使用中的磚的放置位置是否是最終位置,而且是正確的位置:


if (gameData.inProgress) {
  for (var i = 0; i < gameData.tiles.length; i++) {
    currentX = Math.round(gameData.tiles[i].getAttribute('x'));
    currentY = Math.round(gameData.tiles[i].getAttribute('y'));

    correctX = Math.round(gameData.tiles[i].correctX);
    correctY = Math.round(gameData.tiles[i].correctY);

    if (currentX != correctX || currentY != correctY) {
      return;
    } // if
  } // for

如果並非所有磚都在正確位置,我們會立即結束 handlePointerEvents,並等待下一個指標事件觸發 handlePointerEvents。否則的話,使用者已解出拼圖,就會執行下列程式碼:


gameData.inProgress = false;

gameData.score += gameData.size * gameData.size;
gameData.elements.scoreBox.innerHTML = "<strong>Score:</strong> " + gameData.score;

++(gameData.size);

var randomIndex = Math.floor(Math.random() * gameData.congrats.length);
document.getElementById('messageBox').innerHTML = gameData.congrats[randomIndex];

for (var i = 0; i < gameData.tiles.length; i++) {
  gameData.elements.svg.removeChild(gameData.tiles[i])
}

createAndAppendTiles();

gameData.elements.playButton.innerHTML = "Play";

由於已通過這個遊戲關卡,我們會將 gameData.inProgress 設定為 false 並顯示新分數。如果遊戲目前的大小 (資料列與資料欄數目) 也用來指示目前的遊戲關卡 (也就是使用者目前為止解出的拼圖數目),而且因為遊戲的困難度與資料列的正方形計數 (或資料欄計數,因為它們是相同的) 成正比,分數會依據遊戲目前的正方形大小而增加:


gameData.score += gameData.size * gameData.size;

我們接著增量遊戲關卡 Proxy (gameData.size),並從可能的「您贏了」句子的陣列中顯示隨機的句子:


++(gameData.size);

var randomIndex = Math.floor(Math.random() * gameData.congrats.length);
document.getElementById('messageBox').innerHTML = gameData.congrats[randomIndex];

最後,我們移除附加到 SVG 元素的任何既存的 SVG 影像元素 (磚),以準備下一回合 (不同) 的 SVG 影像元素 (將透過 createAndAppendTiles 以建立並附加到 SVG 元素):


for (var i = 0; i < gameData.tiles.length; i++) {
  gameData.elements.svg.removeChild(gameData.tiles[i])
}

createAndAppendTiles();
gameData.elements.playButton.innerHTML = "Play";

 

我們此處的目標,是透過本教學課程來說明如何在合理的現實案例 (實作觸控式拼圖遊戲) 中處理多點觸控事件。這裡的指導原則應能提供在一些情況下 (可能包含涉及畫布和 SVG 的情況) 處理觸控事件的足夠知識。

相關主題

Contoso 影像中心
手勢事件
建置觸控友善之網站的指導方針
如何在觸控裝置模擬游標暫留
HTML5 圖形
Internet Explorer 範例和教學課程
指標事件
指標事件更新

 

 

顯示:
© 2015 Microsoft