序文
要約:2Dテトリスは、テトリスをプレイするために使用され、3Dゲーム機を行うために腐った、気まぐれを果たしてきました。。。基本的な考え方は、2Dでのテトリスゲームの実現を達成した後、3Dモデリング機能のアーケードを使用して3Dモデルを作成し、最終的に3Dモデルに2Dのゲームに固執することです。
(シモンズ:実装の最後の部分は、結合されたビデオと3Dモデルを拡大します)
http://www.hightopo.com/demo/tetris/
コードの実装
まず、最初の完全な2Dゲーム
公式文書化プロセスの観点から、我々は、オブジェクト)(HT成分パラメータがht.DataModelに格納されている学習し、ビュー内のデータ・モデルは、ローディング後に様々な効果を示しました。
gameDM =新しいht.DataModel(); //データモデルを初期化します G2D =新しいht.graph.GraphView(gameDM); //ビューの2Dを初期化します g2d.addToDOM(); //は、ページ上のビューを作成
ゲームを開始するモデルを作成します。
- 最初のステップは、のは、ゲーム、ゲームの制限のためのフレームを作成してみましょう。文書では、我々はht.Nodeは基礎クラスgraphViewで知ることができる画像を表示することに加えて、原始的なノードに表示されますが、また、事前に定義されたグラフィックの様々なサポートしています。だから私は自分の限られた範囲でゲームをプレイする、4つの長方形を作成するには、このクラスを使用するつもりです。
VAR lineNode =新しいht.Node(); lineNode.s({ "形状": "RECT"、//矩形 "Shape.background": "#D8D8D8"、//背景色を設定 "Shape.border.width":1、//境界線の幅1 "Shape.border.color": "#979797" //境界線の色 }); lineNode.setPosition(X、Y); //セット配置要素、0の左上隅、0プリミティブ座標点中心位置それら lineNode.setSize(幅、高さ); //設定プリミティブ幅と高さの属性 gameDM.add(lineNode); //設定された情報要素は、良好なデータ・モデルを追加しました
我々は最初のパターンを取得した後に22:552、Y:111、幅704、高さXを設定します。
上部の境界線は現在のフレームを形成するために、他の三辺を作成してみましょうています:
X:211、Y:562、幅:22、幅:880 X:893、Y:562、幅:22、幅:880 X:552、Y:1013幅:704、幅:22
次のような効果を得ます:
ボーダーは基本的に完了し、4つの境界線は、ブラウジングの過程でドラッグすることができることを見出しました。次に、調整して国境を初期化する方法:
lineNode.s({ "形状": "RECT"、//矩形 "Shape.background": "#D8D8D8"、//背景色を設定 "Shape.border.width":1、//境界線の幅1 "Shape.border.color": "#979797"、//境界線の色 「2d.editable」:偽、//編集可能かどうか 可動かどうか//、偽: "2d.movable" 「2d.selectable」:偽//オプションか });
- ボックスを生成し、私の考えでは、適切な場所にそれらを配置する座標を算出することで、私たちに必要なパターンにそれらを組み合わせて、複数の正方形を生成することです:
方块生成后,开始对图形进行旋转操作。这其中有两个方案,第一种是将图形的翻转后的图形坐标按顺序保存在数组中,每次改变形状时取数组中的前一组或后一组坐标来进行改变;第二种是使用 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,无法移动时返回: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/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(ボックス); // 3D描画でアーケードモデルをリフレッシュ IF(v.ended){ clearInterval(I) } }、20); }、偽)。
問題は、メッセージまたはプライベートの手紙や直接Quguanネットワーク(指示することができるものの実現がありhttps://hightopo.com/の関連情報へのアクセスには)。
概要
3Dモデル上のビデオ再生は私に多くの関心を与えました。カメラは、いくつかの毎日の部屋の監視として、私は信じて、対応する3Dシーンに画像を転送することができた場合は、スマートシティやインテリジェントビルのビデオ監視は、より便利で直感的になります。