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

前言

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


http://www.hightopo.com/demo/...canvas

首先,先完成 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已經有了,如今讓咱們再建立另外三條邊來組成一個框體: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°
  • 方塊有了,如今就該讓它動起來了。設置定時器,使方塊每隔一段時間降低必定距離,並添加鍵盤的監聽事件,以此實現 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,沒法移動時返回: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 模型

在 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街機上玩遊戲的功能。


http://www.hightopo.com/demo/...

拓展

上面只是一個簡單的運用,既然能夠將 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);

總結

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

相關文章
相關標籤/搜索