基於HTML5 的WebGL 3D 版俄羅斯方塊
前言
摘要:2D的俄羅斯方塊已經被人玩爛了,突發奇想就做了個3D的遊戲機,用來玩俄羅斯方塊。 。 。實現的基本想法是先在2D 上實現俄羅斯方塊小遊戲,然後使用3D 建模功能創建一個3D 街機模型,最後將2D 小遊戲貼到3D 模型上。
(ps:最後拓展部分實現將視頻與3D模型的結合)

代碼實現
首先,先完成2D 小遊戲
在查看官方文檔的過程中,了解到HT 的組件參數都是保存在ht.DataModel() 對像中,將數據模型在視圖中進行加載後呈現各種特效。
gameDM=newht.DataModel();//初始化数据模型
g2d=newht.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° 即可。在這裡,我選擇了第二中方式,代碼如下:
functioncreateUnit(x,y){varnode=newht.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);returnnode;}varblock=newht.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:下移的操作,同時為了不使方塊移動出邊界,在每次位移時都將對坐標進行一次驗證:
varoffset=44;varintervalTime=1000;vartopX=552;vartopY=111;varleftSize=211,rightSize=882,bottomSize=1002;varrotationNum=0;window.addEventListener('keydown',function(e){varindex=0;varmaxY=null;if(e.keyCode==87){// up w rotationNum++;
block.setRotation(Math.PI*rotationNum/2);if(!checkRotation(block)){
rotationNum--;
block.setRotation(Math.PI*rotationNum/2);}
}elseif(e.keyCode==65){// left a moveBlock('x',-offset,block);
}elseif(e.keyCode==68){// right d moveBlock('x',offset,block);
}elseif(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,無法移動時返回:falsefunctionmoveBlock(axis,offset,block){//移動方塊varids=[];varyindexs=[];varindexArr=newArray();for(vari=0;i<block.size();i++){varchildNode=block.getChildAt(i);varchildx=childNode.getPosition().x;varchildy=childNode.getPosition().y;if(yindexs.indexOf(childy)==-1){
yindexs.push(childy);
}if(axis==='x'){
childx+=offset;
}elseif(axis==='y'){
childy+=offset;
}//驗證方塊的移動是否超出邊界if(childx<leftSize||childx>rightSize||childy>bottomSize){returnfalse;
}varobj=newObject();
obj.x=childx;
obj.y=childy;
indexArr.push(obj);
ids.push(childNode.getId());}//判斷圖形位移過程中是否與其他方塊觸碰for(varj=0;j<yindexs.length;j++){varindexY=yindexs[j];if(axis==='y'){
indexY+=offset;
}//getDatasInRect方法能獲取到一個範圍中的所有圖元訊息varnodeList=g2d.getDatasInRect({x:233,y:indexY,width:638,height:2},true,false);if(nodeList.length>0){//觸碰for(vari=0;i<nodeList.length;i++){varx=nodeList.get(i).getPosition().x;vary=nodeList.get(i).getPosition().y;varid=nodeList.get(i).getId();if(ids.indexOf(id)>-1){//位移的圖元continue;
}for(vark=0;k<indexArr.length;k++){varobj=indexArr[k];if(obj.x===x&&obj.y===y){//該停下了returnfalse;
}
}
}
}}varblockX=block.getX();varblockY=block.getY();if(axis==='x'){
blockX+=offset;}elseif(axis==='y'){
blockY+=offset;}//方塊移動到新的坐標block.setPosition(blockX,blockY);returntrue;}//驗證方塊是否可以進行翻轉functioncheckRotation(block){for(vari=0;i<block.getChildren().length;i++){varnode=block.getChildAt(i);varchildx=node.getPosition().x;varchildy=node.getPosition().y;//判斷翻轉後的圖形是否會超出範圍if(childx<leftSize||childx>rightSize||childy>bottomSize){returnfalse;}}returntrue;}
- 在完成方塊的位移與變形之後,我們的小遊戲就只差最後一步了:對填充滿的方塊進行消除。在開始的時候,我們就知道所有的訊息都是保存在數據模型當中,所以我們要消除方塊。只需要將它們從數據模型中刪除即可,實現代碼如下:
functiondeleteBlock(block){//消除已經填充滿的方格varyindexs=[];//要判斷的y軸坐標varnum=0;for(vari=0;i<block.size();i++){varchildNode=block.getChildAt(i);varchildy=childNode.getPosition().y;varnodeList=g2d.getDatasInRect({x:233,y:childy,width:638,height:2},true,false);if(nodeList.length==15){for(vari=0;i<nodeList.length;i++){
gameDM.remove(nodeList.get(i));//在數據模型中移除對應的圖元}
num++;
yindexs.push(childy);
}}if(yindexs.length>0){for(vari=0;i<yindexs.length;i++){//將被消除圖元上方的圖元進行組合,並整體向下移動一個位置varyindex=yindexs[i];varh=yindex-133-offset;varmoveList=g2d.getDatasInRect({x:233,y:133,width:638,height:h},true,false);varmblock=newht.Block();for(vari=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=newht.DataModel();g3d=newht.graph3d.Graph3dView(dataModel);g3d.addToDOM();varnode=newht.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]);//創建開始按鈕functioncreateStartButton(){varnode=newht.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);}//創建操作按鈕functioncreateKeyboard(tag,p3,r3){varnode=newht.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(){return1000;}g2d.getHeight=function(){return600;}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'){//判斷是否為點擊事件vartag=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);
}
}elseif(tag==='down'){
moveBlock('y',offset,block);
}elseif(tag==='left'){
moveBlock('x',-offset,block);
}elseif(tag==='right'){
moveBlock('x',offset,block);
}}}});
到此基本完成了在3D街機上玩遊戲的功能。

拓展
上面只是一個簡單的運用,既然可以將2D 的canvas 貼到3D上,那麼是否也可以將視頻貼上去呢。
實現代碼如下:
<videoid="video1"width="270"autoplaysrc="3D交互.mp4"style="display:none"></video>varv=document.getElementById("video1");varnode=newht.Node();node.setSize(2200,1100);gameDM.add(node);v.addEventListener('play',function(){vari=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 場景中,那麼我相信像一些日常的機房監控,智能城市和智能樓宇中的視頻監控將更加的便捷與直觀。