基於 HTML5 的 WebGL 3D 版俄羅斯方塊

基於 HTML5 的 WebGL 3D 版俄羅斯方塊

來自專欄前端外刊評論

前言

摘要:2D 的俄羅斯方塊已經被人玩爛了,突發奇想就做了個 3D 的遊戲機,用來玩俄羅斯方塊。。。實現的基本想法是先在 2D 上實現俄羅斯方塊小遊戲,然後使用 3D 建模功能創建一個 3D 街機模型,最後將 2D 小遊戲貼到 3D 模型上。

(ps:最後拓展部分實現將視頻與3D模型的結合)

3D 街機?

www.hightopo.com

代碼實現

首先,先完成 2D 小遊戲

在查看官方文檔的過程中,了解到 HT 的組件參數都是保存在 ht.DataModel() 對象中,將數據模型在視圖中進行載入後呈現各種特效。

gameDM = new ht.DataModel(); //初始化數據模型g2d = new ht.graph.GraphView(gameDM); //初始化2d視圖g2d.addToDOM(); //在頁面上創建視圖

開始遊戲模型的創建

  • 第一步,先讓我們為遊戲創建一個框體,為遊戲限定範圍。在文檔中,我們可以知道 ht.Node 是 graphView 呈現節點圖元的基礎類,除了可以顯示圖片外,還能支持多種預定義的圖形。所以我打算使用該類創建4個長方形,用它們來做遊戲的範圍限定。

var lineNode = new ht.Node();lineNode.s({ "shape": "rect", //矩形  "shape.background": "#D8D8D8", //設置底色  "shape.border.width": 1, //邊框寬度 1  "shape.border.color": "#979797" // 邊框顏色});lineNode.setPosition(x, y); // 設置圖元展示位置,左上角為0, 0 圖元坐標指向它們的中心位置lineNode.setSize(width, height); // 設置圖元寬、高屬性gameDM.add(lineNode); // 將設置好後的圖元信息加入數據模型中

設置 x:552, y:111, width:704, height:22 後我們可以得到第一個圖形:

邊框的top已經有了,現在讓我們再創建另外三條邊來組成一個框體:

x:211, y:562, width:22, width:880x:893, y:562, width:22, width:880x:552, y:1013, width:704, width:22

得到效果如下:

邊框基本完成,在瀏覽的過程中發現4個邊框可以被拖拽。接下來對邊框初始化的方法進行調整:

lineNode.s({ "shape": "rect", //矩形  "shape.background": "#D8D8D8", //設置底色  "shape.border.width": 1, //邊框寬度 1  "shape.border.color": "#979797", // 邊框顏色  "2d.editable" : false, // 是否可編輯  "2d.movable" : false, //是否可移動  "2d.selectable" : false //是否可選中});

  • 生成方塊,我的想法是生成多個正方形,將它們組合成我們需要的圖形,通過坐標的計算來將它們擺放在相應的位置:

方塊生成後,開始對圖形進行旋轉操作。這其中有兩個方案,第一種是將圖形的翻轉後的圖形坐標按順序保存在數組中,每次改變形狀時取數組中的前一組或後一組坐標來進行改變;第二種是使用 ht.Block() 對象將對應的圖元組合成一個整體,在變形時只需按對應的方向選擇 90° 即可。在這裡,我選擇了第二中方式,代碼如下:

function createUnit(x, y) { var node = new ht.Node(); node.s({ "shape": "rect", "shape.background": "#D8D8D8", "shape.border.width": 1, "shape.border.color": "#979797" }); node.setPosition(x, y); node.setSize(44, 44); gameDM.add(node); return node;}var block = new ht.Block();block.addChild(createUnit(552, 133));block.addChild(createUnit(552, 89));block.addChild(createUnit(508, 133));block.addChild(createUnit(596, 133));block.setAnchor(0.5, 0.75); //設置組合的中心位置, 旋轉時將安裝此點來進行block.setPosition(552, 144);

Block 設置中心點 Anchor 如下圖:

在設置旋轉時,只需使用 setRotation 函數對 block 進行旋轉即可:

block.setRotation(Math.PI*rotationNum/2); //rotationNum 是一個計數器,保存已經旋轉次數,保證每次都是在上一次的基礎上旋轉90°

  • 方塊有了,現在就該讓它動起來了。設置定時器,使方塊每隔一段時間下降一定距離,並添加鍵盤的監聽事件,以此實現 w:翻轉、s:左移動、d:右移動、s:下移的操作,同時為了不使方塊移動出邊界,在每次位移時都將對坐標進行一次驗證:

var offset = 44;var intervalTime = 1000;var topX = 552;var topY = 111;var leftSize = 211,rightSize = 882,bottomSize = 1002;var rotationNum = 0;window.addEventListener(keydown, function(e){ var index = 0; var maxY = null; if(e.keyCode == 87){ // up w  rotationNum ++;  block.setRotation(Math.PI*rotationNum/2); if (!checkRotation(block)) {  rotationNum --;   block.setRotation(Math.PI*rotationNum/2); } } else if (e.keyCode == 65) { // left a   moveBlock(x, -offset, block); } else if (e.keyCode == 68) { // right d   moveBlock(x, offset, block); } else if(e.keyCode == 83){ // down s   moveBlock(y, offset, block); }}, false);setInterval(function(){ if(!moveBlock("y", offset, block)){ //無法進行位移,創建新的方塊  rotationNum = 0; //方塊翻轉次數歸0  block = createNode(blockType); //生成新的方塊  blockType = parseInt(Math.random()*100%5); //下一次生成的方塊圖形 }}, intervalTime); //執行間隔//移動方塊,移動成功時返回:true,無法移動時返回:falsefunction moveBlock(axis, offset, block){ // 移動方塊 var ids = []; var yindexs = []; var indexArr = new Array(); for(var i = 0; i < block.size(); i ++){ var childNode = block.getChildAt(i); var childx = childNode.getPosition().x; var childy = childNode.getPosition().y; if (yindexs.indexOf(childy) == -1) {   yindexs.push(childy);  } if(axis === x){   childx += offset;  }else if (axis === y) {  childy += offset;  } // 驗證方塊的移動是否超出邊界 if(childx < leftSize || childx > rightSize || childy > bottomSize){ return false;  } var obj = new Object();  obj.x = childx;  obj.y = childy;  indexArr.push(obj);  ids.push(childNode.getId()); } //判斷圖形位移過程中是否與其他方塊觸碰 for(var j = 0; j < yindexs.length; j ++){ var indexY = yindexs[j]; if (axis === y) {   indexY += offset;   } //getDatasInRect 方法能獲取到一個範圍中的所有圖元信息 var nodeList = g2d.getDatasInRect({x:233, y:indexY, width:638, height:2}, true, false); if(nodeList.length > 0){ // 觸碰 for(var i = 0; i < nodeList.length; i++){ var x = nodeList.get(i).getPosition().x; var y = nodeList.get(i).getPosition().y; var id = nodeList.get(i).getId(); if (ids.indexOf(id) > -1) { // 位移的圖元 continue;    } for (var k = 0; k < indexArr.length; k++) { var obj = indexArr[k]; if (obj.x === x && obj.y === y){ // 該停下了 return false;     }   }  } }} var blockX = block.getX(); var blockY = block.getY(); if (axis === x) {  blockX += offset; }else if (axis === y) {  blockY += offset;} // 方塊移動到新的坐標 block.setPosition(blockX, blockY); return true;}// 驗證方塊是否可以進行翻轉function checkRotation(block){ for(var i = 0; i < block.getChildren().length; i++){ var node = block.getChildAt(i); var childx = node.getPosition().x; var childy = node.getPosition().y; // 判斷翻轉後的圖形是否會超出範圍 if(childx < leftSize || childx > rightSize || childy > bottomSize){ return false; } } return true;}

  • 在完成方塊的位移與變形之後,我們的小遊戲就只差最後一步了:對填充滿的方塊進行消除。在開始的時候,我們就知道所有的信息都是保存在數據模型當中,所以我們要消除方塊。只需要將它們從數據模型中刪除即可,實現代碼如下:

function deleteBlock(block){ // 消除已經填充滿的方格 var yindexs = []; // 要判斷的y軸坐標 var num = 0; for(var i = 0; i < block.size(); i ++){ var childNode = block.getChildAt(i); var childy = childNode.getPosition().y; var nodeList = g2d.getDatasInRect({x:233, y:childy, width:638, height:2}, true, false); if (nodeList.length == 15) { for(var i = 0; i < nodeList.length; i++){    gameDM.remove(nodeList.get(i)); // 在數據模型中移除對應的圖元 }   num ++;   yindexs.push(childy);  }} if (yindexs.length > 0) { for(var i = 0 ; i < yindexs.length; i++){ // 將被消除圖元上方的圖元進行組合,並整體向下移動一個位置 var yindex = yindexs[i]; var h = yindex - 133 - offset; var moveList = g2d.getDatasInRect({x:233, y:133, width:638, height:h}, true, false); var mblock = new ht.Block(); for(var i = 0; i < moveList.size(); i++){   mblock.addChild(moveList.get(i));  }  moveBlock(y, offset, mblock); } }}

到此,一個簡單的俄羅斯方塊小遊戲就實現了。當然,這個遊戲還有很多可以拓展的地方,比如:更多的方塊類型,遊戲分數的統計,下一步預測窗體,遊戲背景修改等。這些先不考慮,我們先開始下一步。

創建 3D 模型

在 3D 建模文檔中了解到,HT 通過一個個三角形來組合模型。

  • 首先,先將網路上查找到的街機模型進行拆分,將其中的各個模塊拆分成三角形面:

如圖所示,將0所在位置設置為原點(0,0,0),我們打開畫圖工具根據標尺大概估計出每個坐標相對原點的位置,將計算好的坐標數組傳入 vs 中,同時在is頂點索引坐標中將每個三角圖形的組合傳入其中:

ht.Default.setShape3dModel(damBoard, { // 為新模型起名 vs: [ 0, 0, 0, //0  0.23, 0, 0, 0.23, 0.27, 0, 0.27, 0.28, 0, //3  0.27, 0.32, 0, 0.20, 0.33, 0, 0.18, 0.51, 0, // 6  0.27, 0.57, 0, 0.27, 0.655, 0, 0.20, 0.67, 0, // 9  0, 0.535, 0 ], is: [ 0, 1, 2, 0, 2, 5, 2, 3, 4, 4, 2, 5, 5, 0, 10, 10, 5, 6, 6, 7, 8, 8, 6, 9, 9, 10, 6 ]});

與 2D 一樣,我們創建一個 ht.Node() 的基礎圖元,類型設置為我們新註冊的3D模型名稱:

dataModel = new ht.DataModel();g3d = new ht.graph3d.Graph3dView(dataModel);g3d.addToDOM();var node = new ht.Node();node.s({ shape3d: damBoard, shape3d.reverse.flip: true, 3d.movable: false, 3d.editable: false,  3d.selectable: false});node.p3([0, 20, 0]);node.s3([100, 100, 100]);dataModel.add(node);

已經有個側邊了,我們可以將坐標系延z軸移動一定距離後得到另一個側邊的坐標數組同時再根據沒個面的不同,分別設置 is 數組,將所有的面組合起來後,我們就將初步得到一個街機模型:

vs: [ 0, 0, 0, //0  0.23, 0, 0, 0.23, 0.27, 0, 0.27, 0.28, 0, //3  0.27, 0.32, 0, 0.20, 0.33, 0, 0.18, 0.51, 0, // 6  0.27, 0.57, 0, 0.27, 0.655, 0, 0.20, 0.67, 0, // 9  0, 0.535, 0, 0, 0, 0.4, //11  0.23, 0, 0.4, 0.23, 0.27, 0.4, 0.27, 0.28, 0.4, //14  0.27, 0.32, 0.4, 0.20, 0.33, 0.4, 0.18, 0.51, 0.4, // 17  0.27, 0.57, 0.4, 0.27, 0.655, 0.4, 0.20, 0.67, 0.4, // 20  0, 0.535, 0.4,]

  • 模型不夠美觀,我們可以給模型的每個面進行貼圖,參考文檔中對模型 uv 參數的說明,我們可以知道 uv 對應的是模型中每個頂點在圖片中的偏移量,圖片的左上角為(0, 0)右下角為(1,1), 以此我們可以為每個面設置貼圖。如:

ht.Default.setShape3dModel(damBoard, {  vs: vsArr,  is: isArr,  uv: [ 0, 1, 0.81, 1, 0.81, 0.42, 1, 0.4, 1, 0.36,   0.725, 0.34, 0.65, 0.26, 1, 0.16, 1, 0.03, 0.75, 0, 0, 0.22,   , ,   , ,   , ,   , ,   , ,   , ,   , ,   , ,   , ,   , ,   , ,  ], //uv中要將is中有使用到的點的偏移量都進行設值  image: /image/side1.jpg //圖片地址});

同理,為其他面也分別設置 uv,最終效果如下:

  • 3D 模型整體已經建好了, 還需要給模型加上遊戲按鈕。在官方文檔建模函數中,我們可以看到已經有大量封裝完畢的圖形供我們使用。在這裡我選擇使用 createRightTriangleModel 創建直角三角形的方法來創建操作按鈕,使用 createSmoothSphereModel 函數來創建開始按鈕:

ht.Default.setShape3dModel(button, ht.Default.createRightTriangleModel(true, true));ht.Default.setShape3dModel(startButton, ht.Default.createSmoothSphereModel(20, 20, 0, Math.PI * 2, 0, Math.PI));根據註冊好的模型生成按鈕createKeyboard(up, [21.5, 52.5, 26], [0, -Math.PI / 4, 0]);createKeyboard(down, [25.5, 51.75, 26], [0, Math.PI * 3 / 4, 0]);createKeyboard(left, [23.5, 52, 28], [0, Math.PI / 4, 0]);createKeyboard(right, [23.5, 52, 24], [0, Math.PI * 5 / 4, 0]);// 創建開始按鈕function createStartButton() { var node = new ht.Node();  node.setTag(restart);  node.s({ shape3d: startButton, shape3d.reverse.flip: true, shape3d.color: #7ED321, 3d.movable: false, 3d.editable: false  });  node.p3([23.5, 52.5, 11]); // 按擺放位置  node.s3([3, 3, 3]); // 按鈕放大倍數  dataModel.add(node);}// 創建操作按鈕function createKeyboard(tag, p3, r3) { var node = new ht.Node();  node.setTag(tag);  node.s({ shape3d: button, shape3d.reverse.flip: true, shape3d.color: red, 3d.movable: false, 3d.editable: false  });  node.p3(p3); // 按擺放位置  node.s3([1.5, 1.5, 1.5]); // 按鈕放大倍數  node.r3(r3); // 將按鈕按Y軸旋轉,已保存按鈕指向正確 dataModel.add(node);}

最終效果如下:

  • 將 2D 小遊戲貼到3D模型上,在文檔中我們可以發現 setImage 屬性不僅僅是只能設置正常的圖片,還可以使用它來註冊一個 canvas 圖形組件。而2D視圖可以通過 getCanvas() 來獲取畫布信息。

ht.Default.setImage(gameScrn, g2d.getCanvas());ht.Default.setShape3dModel(scrn, {  vs: vsArr,  is: isArr,  uv: scrnUV,  image: gameScrn // 將註冊的2d畫布信息當成屏幕的圖片貼圖信息});// 設置 2d 的畫布大小g2d.getWidth = function () { return 1000; }g2d.getHeight = function () { return 600; }g2d.getCanvas().dynamic = true;//設置這個是為了讓canvas能動態顯示// 設置計時器,讓2d畫布上的每次改變都能及時的在3D模型上進行展示setInterval(function () {  node.iv(); // 每次改變都需要對街機模型進行刷新,刷新時間為下一幀  g2d.validateImpl(); // 立即對2D上的圖元進行刷新}, 10);// 設置500毫秒後,縮放平移整個2D畫布以展示所有的圖元setTimeout(function () {  g2d.fitContent(true);}, 500);

效果如下:

  • 在 2D 畫布上,我們已經為遊戲添加了鍵盤事件,現在我們只需要為 3D 模型上的5個按鈕分別綁定對應方法即可:

g3d.mi(function (e) { // addInteractorListener 交互事件監聽器的縮寫 if (e.kind === clickData) { // 判斷是否為點擊事件 var tag = e.data.getTag(); if (tag === restart) {  gameAgain(node);  } if (start) { if (tag === up) {   block.setRotation(Math.PI * (1 + rotationNum) / 2);    rotationNum++; if (!checkRotation(block)) { // 邊緣變形限制    rotationNum--;    block.setRotation(Math.PI * rotationNum / 2);   }  } else if (tag === down) {  moveBlock(y, offset, block);  } else if (tag === left) {   moveBlock(x, -offset, block);  } else if (tag === right) {  moveBlock(x, offset, block);  } } }});

到此基本完成了在3D街機上玩遊戲的功能。

3D 街機?

www.hightopo.com

拓展

上面只是一個簡單的運用,既然可以將 2D 的 canvas 貼到3D上,那麼是否也可以將視頻貼上去呢。

實現代碼如下:

<video id="video1" width="270" autoplay src="3D交互.mp4" style="display:none"></video>var v = document.getElementById("video1");var node = new ht.Node();node.setSize(2200, 1100);gameDM.add(node);v.addEventListener(play, function () { var i = window.setInterval(function () {    node.setImage(v);//將視頻截圖貼在圖元上    g2d.validateImpl();//刷新2d畫布    g3d.invalidateData(box);//刷新3d圖紙中的街機模型 if (v.ended) {      clearInterval(i)    }  }, 20);}, false);

實現上有什麼問題可以直接留言或者私信或者直接去官網(hightopo.com/)上查閱相關的資料。

總結

在 3D 模型上的視頻播放給予了我很大的興趣。如果能將攝像頭的畫面轉移到對應的 3D 場景中,那麼我相信像一些日常的機房監控,智能城市和智能樓宇中的視頻監控將更加的便捷與直觀。


推薦閱讀:

HTML5學習路線
Cordova 如何實現所有的h5html 來自於遠程伺服器呢?

TAG:HTML5 | WebGL | 遊戲引擎 |