摘要:2D 的俄羅斯方塊已經被人玩爛了,突發奇想就作了個 3D 的遊戲機,用來玩俄羅斯方塊。。。實現的基本想法是先在 2D 上實現俄羅斯方塊小遊戲,而後使用 3D 建模功能建立一個 3D 街機模型,最後將 2D 小遊戲貼到 3D 模型上。
(ps:最後拓展部分實現將視頻與3D模型的結合)node
http://www.hightopo.com/demo/tetris/canvas
在查看官方文檔的過程當中,瞭解到 HT 的組件參數都是保存在 ht.DataModel() 對象中,將數據模型在視圖中進行加載後呈現各類特效。數組
gameDM = new ht.DataModel(); //初始化數據模型 g2d = new ht.graph.GraphView(gameDM); //初始化2d視圖 g2d.addToDOM(); //在頁面上建立視圖
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已經有了,如今讓咱們再建立另外三條邊來組成一個框體:dom
x:211, y:562, width:22, width:880 x:893, y:562, width:22, width:880 x:552, y:1013, width:704, width:22
獲得效果以下:ide
邊框基本完成,在瀏覽的過程當中發現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 以下圖:spa
在設置旋轉時,只需使用 setRotation 函數對 block 進行旋轉便可:3d
block.setRotation(Math.PI*rotationNum/2); //rotationNum 是一個計數器,保存已經旋轉次數,保證每次都是在上一次的基礎上旋轉90°
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,沒法移動時返回:false function 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 建模文檔中瞭解到,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, ]
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,最終效果以下:
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); }
最終效果以下:
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);
效果以下:
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街機上玩遊戲的功能。
http://www.hightopo.com/demo/tetris/
上面只是一個簡單的運用,既然能夠將 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);
實現上有什麼問題能夠直接留言或者私信或者直接去官網(https://hightopo.com/)上查閱相關的資料。
在 3D 模型上的視頻播放給予了我很大的興趣。若是能將攝像頭的畫面轉移到對應的 3D 場景中,那麼我相信像一些平常的機房監控,智能城市和智能樓宇中的視頻監控將更加的便捷與直觀。