HTML5:用Web Workers API執行大計算量任務

出版圈郭志敏發表於2012-03-19

我們一直致力於尋求一種高效能的圖形渲染方法,以便將其用於最終的遊戲。而路徑查詢則是一個非常有用的功能,可以用於建立道路或顯示角色從A 點到B 點的過程。簡言之,路徑查詢演算法就是要在n 維(通常是2D 或3D)空間中找出兩點間的最短路線。

通常,只有少數人才能實現準確的路徑查詢;換句話說,很多人(但願不包括我們)都無法保證計算結果的準確性。可以說這是一項計算量很大的工作,而最有效的解決方案就是根據我們的產品修改演算法,以便得到最合用的方法。

處理路徑查詢的一種最佳演算法叫做A*,是迪傑斯特拉(Dijkstra)演算法的變體。路徑查詢(或者類似的計算時間超過數毫秒的操作)的問題在於,它們會導致JavaScript 產生一種名為“介面鎖定”的效果,也就是在操作完成以前,瀏覽器將一直被凍結。

幸運的是,HTML5 規範也提供了一個名為Web Workers 的新API。Web Workers(通常稱為“worker”)可以讓我們在後臺執行計算量相對較大以及執行時間較長的指令碼,而不會影響瀏覽器的主使用者介面。

worker 不是銀彈,不能魔幻般地讓原來吃掉100% 的CPU 處理能力的任務變得輕而易舉。只要是常規手段下計算量大的任務,使用worker 可能照樣還是計算量大,最終還是會影響到使用者體驗。不過,如果是隻消耗了30%的CPU 處理能力的任務,利用worker 並行來處理還是可以把使用者介面的影響降到最低的。

當然,也有一些限制:

  • 由於每個worker 都執行在與執行它們的頁面完全獨立、執行緒安全的環境下(也• 叫“沙盒”),因此它們不能訪問DOM 和window 物件;

  • 儘管已有的worker 可以再產生新的worker(谷歌的Chrome 不支援這個功能),但使用起來必須多加小心,因為這樣一來,如果產生bug 將會很難除錯。

要建立worker,可以使用以下語法:

var worker = new Worker(PATH_TO_A_JS_SCRIPT);

其中的PATH_TO_A_JS_SCRIPT 可以是一個指令碼檔案, 比如astar.js。在建立了worker 之後,任何時候呼叫worker.close() 都可以終止它的執行。如果終止了一個worker,然後又需要執行一個新操作,那麼就得再建立一個新的worker 物件。

Web Workers 之間的通訊是通過在worker.onmessage 事件的回撥函式中呼叫worker.postMessage(object) 來實現的。此外,還可以通過onerror 事件處理程式來處理worker 的錯誤。與普通的網頁類似,Web Workers 也支援引入外部指令碼,使用的是importScripts()函式。這個函式接受零個或多個引數,如果有引數,每個引數都應該是一個JavaScript檔案。

本書線上程式碼庫的ex17-grid-astar.html 中有一個用JavaScript 實現的A* 演算法,其中使用了Web Worders。圖4-2 展示了這個示例的執行效果。程式示例4-4 和程式示例4-5 展示了網頁及A* 演算法的JavaScript 實現。

enter image description here

程式示例4-4 路徑查詢HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Example 17 - (A* working on a grid with unset indexes using
web workers)</title>
<script>
window.onload = function () {
var tileMap = [];
var path = {
start: null,
stop: null
}
var tile = {
width: 6,
height: 6
}
var grid = {
width: 100,
height: 100
}
var canvas = document.getElementById('myCanvas');
canvas.addEventListener('click', handleClick, false);
var c = canvas.getContext('2d');
// 隨機生成1000 個元素
for (var i = 0; i < 1000; i++) {
generateRandomElement();
}
// 繪製整個網格
draw();
function handleClick(e) {
// 檢測到滑鼠單擊後,把滑鼠座標轉換為畫素座標
var row = Math.floor((e.clientX - 10) / tile.width);
var column = Math.floor((e.clientY - 10) / tile.height);
if (tileMap[row] == null) {
tileMap[row] = [];
}
if (tileMap[row][column] !== 0 && tileMap[row][column] !== 1) {
tileMap[row][column] = 0;
if (path.start === null) {
path.start = {x: row, y: column};
} else {
path.stop = {x: row, y: column};
callWorker(path, processWorkerResults);
path.start = null;
path.stop = null;
}
draw();
}
}
function callWorker(path, callback) {
var w = new Worker('astar.js');
w.postMessage({
tileMap: tileMap,
grid: {
width: grid.width,
height: grid.height
},
start: path.start,
stop: path.stop
});
w.onmessage = callback;
}
function processWorkerResults(e) {
if (e.data.length > 0) {
for (var i = 0, len = e.data.length; i < len; i++) {
if (tileMap[e.data[i].x] === undefined) {
tileMap[e.data[i].x] = [];
}
tileMap[e.data[i].x][e.data[i].y] = 0;
}
}
draw();
}
function generateRandomElement() {
var rndRow = Math.floor(Math.random() * (grid.width + 1));
var rndCol = Math.floor(Math.random() * (grid.height + 1));
if (tileMap[rndRow] == null) {
tileMap[rndRow] = [];
}
tileMap[rndRow][rndCol] = 1;
}
function draw(srcX, srcY, destX, destY) {
srcX = (srcX === undefined) ? 0 : srcX;
srcY = (srcY === undefined) ? 0 : srcY;
destX = (destX === undefined) ? canvas.width : destX;
destY = (destY === undefined) ? canvas.height : destY;
c.fillStyle = '#FFFFFF';
c.fillRect (srcX, srcY, destX + 1, destY + 1);
c.fillStyle = '#000000';
var startRow = 0;
var startCol = 0;
var rowCount = startRow + Math.floor(canvas.width / tile.
width) + 1;
var colCount = startCol + Math.floor(canvas.height / tile.
height) + 1;
rowCount = ((startRow + rowCount) > grid.width) ? grid.width :
rowCount;
colCount = ((startCol + colCount) > grid.height) ? grid.height :
colCount;
for (var row = startRow; row < rowCount; row++) {
for (var col = startCol; col < colCount; col++) {
var tilePositionX = tile.width * row;
var tilePositionY = tile.height * col;
if (tilePositionX >= srcX && tilePositionY >= srcY &&
tilePositionX <= (srcX + destX) &&
tilePositionY <= (srcY + destY)) {
if (tileMap[row] != null && tileMap[row][col] != null) {
if (tileMap[row][col] == 0) {
c.fillStyle = '#CC0000';
} else {
c.fillStyle = '#0000FF';
}
c.fillRect(tilePositionX, tilePositionY, tile.width,
tile.height);
} else {
c.strokeStyle = '#CCCCCC';
c.strokeRect(tilePositionX, tilePositionY, tile.width,
tile.height);
}
}
}
}
}
}
</script>
</head>
<body>
<canvas id="myCanvas" width="600" height="300"></canvas>
<br />
</body>
</html>

程式示例4-5 A* 演算法的JavaScript 實現

// 這個worker 處理負責aStar 類的例項
onmessage = function(e){
var a = new aStar(e.data.tileMap, e.data.grid.width, e.data.grid.height,
e.data.start, e.data.stop);
postMessage(a);
}
// 基於非連續索引的tileMap 調整後的A* 路徑查詢類
/**
* @param tileMap: A 2-dimensional matrix with noncontiguous indexes
* @param gridW: Grid width measured in rows
* @param gridH: Grid height measured in columns
* @param src: Source point, an object containing X and Y
* coordinates representing row/column
* @param dest: Destination point, an object containing
* X and Y coordinates representing row/column
* @param createPositions: [OPTIONAL] A boolean indicating whether
* traversing through the tileMap should
* create new indexes (default TRUE)
*/
var aStar = function(tileMap, gridW, gridH, src, dest, createPositions) {
this.openList = new NodeList(true, 'F');
this.closedList = new NodeList();
this.path = new NodeList();
this.src = src;
this.dest = dest;
this.createPositions = (createPositions === undefined) ? true :
createPositions;
this.currentNode = null;
var grid = {
rows: gridW,
cols: gridH
}
this.openList.add(new Node(null, this.src));
while (!this.openList.isEmpty()) {
this.currentNode = this.openList.get(0);
this.currentNode.visited = true;
if (this.checkDifference(this.currentNode, this.dest)) {
// 到達目的地 :)
break;
}
this.closedList.add(this.currentNode);
this.openList.remove(0);
// 檢查與當前節點相近的8 個元素
var nstart = {
HTML5聲音及處理優化 | 219
x: (((this.currentNode.x - 1) >= 0) ? this.currentNode.x - 1 : 0),
y: (((this.currentNode.y - 1) >= 0) ? this.currentNode.y - 1 : 0),
}
var nstop = {
x: (((this.currentNode.x + 1) <= grid.rows) ? this.currentNode.
x + 1 : grid.rows),
y: (((this.currentNode.y + 1) <= grid.cols) ? this.currentNode.
y + 1 : grid.cols),
}
for (var row = nstart.x; row <= nstop.x; row++) {
for (var col = nstart.y; col <= nstop.y; col++) {
// 在原始的tileMap 中還沒有行,還繼續嗎?
if (tileMap[row] === undefined) {
if (!this.createPositions) {
continue;
}
}
// 檢查建築物或其他障礙物
if (tileMap[row] !== undefined && tileMap[row][col] === 1) {
continue;
}
var element = this.closedList.getByXY(row, col);
if (element !== null) {
// 這個元素已經在closedList 中了
continue;
} else {
element = this.openList.getByXY(row, col);
if (element !== null) {
// 這個元素已經在closedList 中了
continue;
}
}
// 還不在任何列表中,繼續
var n = new Node(this.currentNode, {x: row, y: col});
n.G = this.currentNode.G + 1;
n.H = this.getDistance(this.currentNode, n);
n.F = n.G + n.H;
this.openList.add(n);
}
}
}
while (this.currentNode.parentNode !== null) {
this.path.add(this.currentNode);
this.currentNode = this.currentNode.parentNode;
}
}
aStar.prototype.checkDifference = function(src, dest) {
return (src.x === dest.x && src.y === dest.y);
}
aStar.prototype.getDistance = function(src, dest) {
return Math.abs(src.x - dest.x) + Math.abs(src.y - dest.y);
}
function Node(parentNode, src) {
this.parentNode = parentNode;
this.x = src.x;
this.y = src.y;
this.F = 0;
this.G = 0;
this.H = 0;
}
var NodeList = function(sorted, sortParam) {
this.sort = (sorted === undefined) ? false : sorted;
this.sortParam = (sortParam === undefined) ? 'F' : sortParam;
this.list = [];
this.coordMatrix = [];
}
NodeList.prototype.add = function(element) {
this.list.push(element);
if (this.coordMatrix[element.x] === undefined) {
this.coordMatrix[element.x] = [];
}
this.coordMatrix[element.x][element.y] = element;
if (this.sort) {
var sortBy = this.sortParam;
this.list.sort(function(o1, o2) { return o1[sortBy] - o2[sortBy]; });
}
}
NodeList.prototype.remove = function(pos) {
this.list.splice(pos, 1);
}
NodeList.prototype.get = function(pos) {
return this.list[pos];
}
NodeList.prototype.size = function() {
return this.list.length;
}
NodeList.prototype.isEmpty = function() {
return (this.list.length == 0);
}
NodeList.prototype.getByXY = function(x, y) {
if (this.coordMatrix[x] === undefined) {
return null;
} else {
var obj = this.coordMatrix[x][y];
if (obj == undefined) {
return null;
} else {
return obj;
}
}
}
NodeList.prototype.print = function() {
for (var i = 0, len = this.list.length; i < len; i++) {
console.log(this.list[i].x + ' ' + this.list[i].y);
}
}

本文摘自《深入HTML5應用開發》

相關文章