html5 开发的的俄罗斯方块实例源码
我们先来看看效果图:
上面的数字是得分,游戏没有考虑兼容性,只在chrome上测试过,不过大部分现代浏览器还是可以玩的。
先上HTML代码
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>俄罗斯方块</title> <style type="text/css"> body { padding: 0; margin-top: 40px; text-align: center; } .tetris { border: 4px solid black; } </style> </head> <body> <div id="score"></div> <canvas id="tetris" width="240" height="420"></canvas> <script type="text/javascript" src="js/tetris2.js"></script> </body> </html>
html代码就不解释了哈,重点来说一下js的实现方式。
首先是加载图片的函数:
function dlImg(img) { // 返回一个img对象 var oimg = new Image(); oimg.src = 'images/' + img; return oimg; } /** * @param img img对象数组 * @param sw 屏幕适应的一个比值 * @param fun 程序入口函数 */ function loadAllImg(img, sw, fun) { var l = img.length, i,h = 0; for(i = 0; i < l; i ++){ oimgarr[img[i]] = dlImg(img[i]); oimgarr[img[i]].onload = function () { // 加载是异步的 this.width = this.width * sw; this.height = this.height * sw; h ++; h >= l && fun(); // 所有图片加载成功后调用fun函数 } } }
oimgarr是一个全局变量,暂存着游戏用到的img对象。
然后就是主函数的代码了,包含俄罗斯方块主要的逻辑功能。
主函数内有2个对象,Block对象是形状对象,参数type是形状的类型(上、l、L、田、转形状),Blocks是指整个俄罗斯盘(不知道用什么词形容。。。)的对象
下面是Block对象的实现:
function Block(type) { this.type = type; this.i = -1; this.j = 6; this.speed = 100; this.defer = 0; switch (this.type) { case 1: // l字 this.outline = [{i: this.i, j: this.j}, {i: this.i - 1, j: this.j}, {i: this.i - 2, j: this.j}, {i: this.i - 3, j: this.j}]; break; case 2: // 上字 this.outline = [{i: this.i, j: this.j - 1}, {i: this.i - 1, j: this.j}, {i: this.i, j: this.j}, {i: this.i, j: this.j + 1}]; break; case 3: // L字 this.outline = [{i: this.i - 2, j: this.j - 1}, {i: this.i - 1, j: this.j - 1}, {i: this.i, j: this.j - 1}, {i: this.i, j: this.j}]; break; case 4: // 田字 this.outline = [{i: this.i - 1, j: this.j - 1}, {i: this.i, j: this.j - 1}, {i: this.i, j: this.j}, {i: this.i - 1, j: this.j}]; break; case 5: // 转字 this.outline = [{i: this.i - 1, j: this.j - 1}, {i: this.i, j: this.j - 1}, {i: this.i, j: this.j}, {i: this.i + 1, j: this.j}]; break; } this.dropBlock = function () { // 下落方块 var that = this; if(this.defer == this.speed) { this.outline.map(function (o) { o.i = o.i + 1; }); this.defer = 0; } else this.defer ++; }; this.setSpeed = function () { this.speed = 2; this.defer = 0; }; this.isReady = function () { return this.speed == this.defer; } }
下面是Blocks对象的实现方式:
var Blocks = { nullimg: imga['null.png'], cellimg: imga['cell.png'], pause: false, // 游戏是否处于暂停中 matrix: new Array(21), // 矩阵,-1表示空,0表示正在移动,1表示已存在 block: new Block(1), // 默认第一个出现的方块类型为1 score: 0, // 分数累计 init: function () { var that = this, code = null; for(var i = 0; i < 21; i ++) { // 初始化矩阵数组 this.matrix[i] = new Array(12); for (var j = 0; j < 12; j ++) { this.matrix[i][j] = -1; ctx.drawImage(this.nullimg, j * cell, i * cell, this.nullimg.width, this.nullimg.height); } } document.onkeydown = function (e) { // 按键事件 code = e.keyCode || e.which; switch (code){ case 37: // ← that.setSite(-1); break; case 38: // ↑ that.rotateBlock(); break; case 39: // → that.setSite(1); break; case 40: // ↓ 长按加速下滑 if(that.block.speed == config.SPEED) that.block.speedUp(); // 加速 break; case 32: // 暂停 !that.pause ? that.suspend() : that.start(); break; default : return false; } }; document.onkeyup = function (e) { if(e.keyCode == 40){ // 松开↓恢复速度 that.block.speed = config.SPEED; } } }, start: function () { // 开始游戏 var that = this; time = setInterval(function () { console.time('all'); that.block.dropBlock(); // 下落方块 that.refreshMat(); // 刷新矩阵 that.reachBottom(); // 检测是否到达底部或者碰到已有方块 console.timeEnd('all'); }, config.TIME); this.pause = false; }, suspend: function () { // 暂停 this.pause = true; clearInterval(time); }, refreshMat: function () { // 执行一次矩阵刷新 var img = null, that = this; that.block.outline.forEach(function (o) { // 将移动前的位置都置为-1 if(o.i > 0 && that.matrix[o.i - 1][o.j] != 1 ) that.matrix[o.i - 1][o.j] = -1; }); that.block.outline.forEach(function (o) { // 刷新移动后的位置 if(o.i >= 0) that.matrix[o.i][o.j] = 0; }); this.matrix.forEach(function (l, i) { // 重绘矩阵 l.forEach(function (m, j) { img = (m == -1 ? that.nullimg : that.cellimg); ctx.drawImage(img, j * cell, i * cell, img.width, img.height); }); }); }, rotatePoint: function (c, p) { // c点为旋转中心,p为旋转点,一次顺时针旋转90度。返回旋转后的坐标 return {j: p.i - c.i + c.j, i: -p.j + c.i + c.j}; }, rotateBlock: function () { var that = this, i, o = null, ctr = that.block.outline[1], l = that.block.outline.length; if (that.block.type != 4) { // 田字形无法旋转 for (i = 0; i < l; i++) { o = that.rotatePoint(ctr, that.block.outline[i]); if (o.j < 0 || o.j > 11 || o.i > 20) { // 旋转时不可以碰到边界 break; } else if (o.i > 0 && o.j >= 0 && o.j <= 20 && Blocks.matrix[o.i][o.j] == 1) { // 旋转时不可以已有方块的点 break; } } if (i == 4) { that.block.outline.forEach(function (o, i) { if (o.i >= 0) that.matrix[o.i][o.j] = -1; // 清空变化前的位置 that.block.outline[i] = that.rotatePoint(ctr, o); }); } } }, setSite: function (dir) { // 设置左右移动后的位置 var i, o, l = this.block.outline.length; for(i = 0; i < l; i ++){ o = this.block.outline[i]; // 是否碰到已存在的方块,是否碰到左右边界 if(o.i >= 0 && ((Blocks.matrix[o.i][o.j + dir] == 1) || (o.j + dir == -1 || o.j + dir == 12))){ break; // 一旦发生碰撞,就退出循环,并不执行移动操作 } } if(i == l) { // 当count=l时,表明移动操作没有发生碰撞 this.block.outline.forEach(function (o) { if (o.i >= 0) { Blocks.matrix[o.i][o.j] = -1; // 将当前位置置为-1 o.j = (o.j + dir == -1 || o.j + dir == 12) ? o.j : o.j + dir; // 是否允许移动,允许则将o.j+dir的值赋予o.j Blocks.matrix[o.i][o.j] = 0; // 刷新最新值 } else { // 小于0时(在矩阵之外),也需进行左右移动 o.j = (o.j + dir == -1 || o.j + dir == 12) ? o.j : o.j + dir; } }); } }, reachBottom: function () { var that = this, i, j, o, l = that.block.outline.length; if(that.block.isReady()) { // 当前方块下落帧结束时,然后进行检测是否到达了底部 for (j = 0; j < l; j ++) { o = that.block.outline[j]; if (o.i >= 0 && (o.i == 20 || that.matrix[o.i + 1][o.j] == 1)) { // 向下移动时发生碰撞 break; // 方块到达底部或落在其他方块上,方块停止下落,产生新的方块 } } if (j < l) { // 当方块落在底部或其他方块时,进行检测 for(i = 0; i < l; i ++) { o = that.block.outline[i]; if(o.i >= 0){ that.matrix[o.i][o.j] = 1; // 方块停止后,修改矩阵数据 } else { that.gameOver(); // 游戏结束 return; } } that.ruinMat(); // 检测是否需要爆破行,如果有则执行爆破操作 that.block = new Block(parseInt(Math.random() * 5) + 1); } } }, detectMat: function () { // 检测矩阵,判断是否有连续一行,返回一个数组 var count = 0, s, detecta = []; // 需要爆破的行号 this.matrix.forEach(function (l, i) { for(s = 0; s < l.length; s ++){ if(l[s] == 1) count ++; else break; } count == 12 && detecta.push(i); count = 0; }); return detecta.length == 0 ? false : detecta; }, ruinMat: function () { // 爆破连续的一行 var dmat = this.detectMat(); // 返回整行都有方块的行号集合 if(dmat){ this.score = this.score + (dmat.length == 1 ? 100 : dmat.length == 2 ? 250 : dmat.length == 3 ? 450 : 700); score.innerHTML = this.score.toString(); dmat.forEach(function (d) { Blocks.matrix.splice(d, 1); // 删掉整行都有方块的行 Blocks.matrix.unshift([-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1]); // 弥补被删的行 }); } dmat = null; }, gameOver: function () { clearInterval(time); alert('你挂了'); } };
Block这个对象可以按照下图的坐标来理解:
可以看到,棋盘左上角是坐标(0,0),每个小方格代表一个坐标点,坐标点的值对应状态值,即-1表示位置空闲,0表示位置正在刷新,1表示位置被占用。
当然这里还有一个模块没有实现,这个模块就是提示下一个方块的形状,这个有兴趣的同学可以加下哈。
源码在github地址:https://github.com/zquancai/tetrisc
欢迎下载互相学习
HTML5 Canvas实现简单的俄罗斯方块
最近学习了下HTML5,抽时间利用Canvas写了个俄罗斯方块的小游戏,写的比较简单也比较粗糙,还有点bug,算法方面也没深入考虑,只是为了学习使用Canvas。
主要实现思路:
一、绘制画布
画布宽200px,高400px,小方块宽和高为10px。
二、绘制7种形状的方块
在画布上,每个形状的4个小方块,用相对于画布左上角的坐标来绘制,HTML5的Canvas的左上角坐标为x=0,y=0.
以第一个形状举例,四个方块的坐标分别为:
4个坐标位置存储在数组中[0,0,1,0,1,1,1,2]
每个形状的变形后的相对坐标也存储在数组中,这样做是为了简单实现形状变形的算法。
在画布上绘制的时候,需要以画布上的实际坐标绘制四个正方形,正方形的宽度和高度为10px;
fillRect(0,0,10,10)
fillRect(10,0,10,10)
fillRect(10,10,10,10)
fillRect(10,20,10,10)
三、向下移动
每向下移动一次,Y坐标+1,例如上图的坐标变为:0,1,1,1,1,2,1,3
四、左右移动
移动一次,X坐标+1或-1,例如如果上图向右移动1个坐标后的坐标为:1,0,2,0,2,1,2,2。
需要判断当前坐标是否到了画布的左边距或右边距,即每个形状或形状的变形中的最右边的方块到了画布的右边缘,则不能再向右移动,相反,最左边的方块到了左边缘,不能再向左移动。
五、变形
读取当前形状的变形后的初始坐标,加上未变形前的偏移坐标。
六、消除满格行
按【绘制画布】中的描述,画布被分为40*20个区域,即40行、20列,每个区域可以放置一个方块(10*10),画布中的40*20个区域对应一个二维数组,存储每个区域是否有方块,有为1,没有为0
当每个形状下落到最下方的时候,判断该形状所在的行的每个区域是否都有方块,即对应的数组中的值是否都为1,都为1,则表示该行满格,可以消除。
七、操作
添加按键事件
上箭头:变形
下箭头:加速下移
左箭头:向左移动
右箭头:向右移动
八、 代码
<!DOCTYPE html> <!-- Author:kinglau(liushouqian) Date:2012-07-17 Description:目前有些内容是固定的,例如画布大小。也存在bug Blog:http://www.cnblogs.com/kinglau/ http://weibo.com/u/1712849017 --> <html> <head> <title></title> <meta charset="UTF-8" /> <style> #canvas{ border: black 1px solid; } </style> <script type="text/javascript"> //7个基本形状,及变形后的坐标 var shap1=[[0,0,1,0,1,1,1,2],[0,1,1,1,2,1,2,0],[0,0,0,1,0,2,1,2],[0,0,1,0,2,0,0,1]]; var shap2=[[0,0,0,1,0,2,1,0],[0,0,1,0,2,0,2,1],[0,2,1,0,1,1,1,2],[0,0,0,1,1,1,2,1]]; var shap3=[[0,0,1,0,1,1,2,1],[1,0,1,1,0,1,0,2],[0,0,1,0,1,1,2,1],[1,0,1,1,0,1,0,2]] var shap4=[[0,1,1,0,1,1,2,0],[0,0,0,1,1,1,1,2],[0,1,1,0,1,1,2,0],[0,0,0,1,1,1,1,2]]; var shap5=[[0,0,1,0,0,1,1,1],[0,0,1,0,0,1,1,1],[0,0,1,0,0,1,1,1],[0,0,1,0,0,1,1,1]]; var shap6=[[0,1,1,1,2,1,1,0],[0,0,0,1,0,2,1,1],[0,0,1,0,2,0,1,1],[1,0,1,1,0,1,1,2]]; var shap7=[[0,0,1,0,2,0,3,0],[0,0,0,1,0,2,0,3],[0,0,1,0,2,0,3,0],[0,0,0,1,0,2,0,3]]; var shaps=[shap1,shap2,shap3,shap4,shap5,shap6,shap7]; //单位长度 var unitLen=10; //画布上下文 var ctx; //定时 var tid; //画布区域对应的数组 var resultArray=new Array(40); function init() { //获取画布上下文 ctx=document.getElementById("canvas").getContext('2d'); //增加按键事件 document.addEventListener('keydown',moveShape,false); //初始化画布区域数组 for(var i=0;i<40;i++) { var row=new Array(); for(var j=0;j<20;j++) { row[j]=0; } resultArray[i]=row; } // startRun=true; DrawLine(); topTrue=true; tid=setInterval("DrawTetris();",200); } //每个形状在画布上相对于初始位置的坐标偏移量 var rectX=0; var rectY=0; //形状变形 var rotate=0; //当前形状或变形的初始坐标 var t; //当前形状 var shape; var startRun=true; var shapeHeight=0;//当前形状的高度,四个方块的Y坐标的最大差 //用于产生随机形状 var randmShape=1; //当前形状每个方块的坐标 var shapeXY=new Array(4); //根据坐标绘制形状 function Draw(){ var i=0; var tempY=0; shapeXY=new Array(4); for(i=0;i<4;i++){ DrawRect((t[i*2]+rectX) *unitLen,(t[i*2+1]+rectY)*unitLen); var row=new Array(2); row[0]=(t[i*2+1]+rectY); row[1]=t[i*2]+rectX; shapeXY[i]=row; //shapeXY[i,0]=(t[i*2+1]+rectY); //shapeXY[i,1]=t[i*2]; if(topTrue==true) { if(tempY<(t[i*2+1]+rectY)) { tempY=t[i*2+1]+rectY; shapeHeight=tempY+1; } } else { //tempY=rectY; } } //rectY=tempY; rectY+=1; } //根据捕获的按键,判断具体的按键 function getDirection(event){ var keyCode = event.which || event.keyCode; switch(keyCode){ case 1: case 38: case 269: //up return 'up'; break; case 2: case 40: case 270: return 'down'; break; case 3: case 37: case 271: return 'left'; break; case 4: case 39: case 272: return 'right'; break; case 339: //exit case 240: //back return 'back'; break; } } //根据按键确定执行的操作 function moveShape(event){ if(getDirection(event)=='right') { for(var i=0;i<4;i++){ if(t[2*i]+rectX+1>=20 || resultArray[2*i+rectX+1]==1 ){ return; } } rectX+=1; } if(getDirection(event)=='left') { for(var i=0;i<4;i++) { if(t[2*i]+rectX-1<0 || resultArray[2*i+rectX-1]==1){ return; } } rectX-=1; } if(getDirection(event)=='up'){ var mleft=0; for(var i=0;i<4;i++){ if(t[i*2]+rectX>mleft){ mleft=rectX; } } if(rotate==3){ rotate=0; } else{ rotate+=1; } t=shape[rotate]; for(var i=0;i<4;i++){ //t[2*i]=t[2*i]+mleft; rectX=mleft; } } if(getDirection(event)=='down'){ clearInterval(tid); tid=setInterval("DrawTetris();",50); } } //定时执行的方法 function DrawTetris(){ if(CheckBottom()==true) return; //t=shape[rotate]; if(startRun==false){ ctx.clearRect(shapeXY[0][1]*unitLen-1,shapeXY[0][0]*unitLen-1,unitLen+2,unitLen+2); ctx.clearRect(shapeXY[1][1]*unitLen-1,shapeXY[1][0]*unitLen-1,unitLen+2,unitLen+2); ctx.clearRect(shapeXY[2][1]*unitLen-1,shapeXY[2][0]*unitLen-1,unitLen+2,unitLen+2); ctx.clearRect(shapeXY[3][1]*unitLen-1,shapeXY[3][0]*unitLen-1,unitLen+2,unitLen+2); } startRun=false; //ctx.clearRect(0,0,200,400-(shapeHeight)*10); DrawLine(); var sp=randmShape; shape=shaps[sp-1]; t=shape[rotate]; Draw(); } var topTrue=false; //检查当前形状是否到了画布底部 function CheckBottom() { if(topTrue==true){ startRun=true; topTrue=false; rectX=9; rectY=0; randmShape=Math.floor(Math.random()*7+1); return true; } if(rectY+shapeHeight-1>=40 || rectY==0) { //clearInterval(tid); //ctx.clearRect(0,0,200,300); if(rectY==0) { return false } CurrentShapeOnBottom(); return true; } else { //形状中的四个方块有一个到了底部,就不能再向下移动 if(shapeXY[0][0]==39 || shapeXY[1][0]==39 || shapeXY[2][0]==39 || shapeXY[3][0]==39) { CurrentShapeOnBottom(); return true; } //形状中的每个方块所在行的下一行,如果已经存在方块,不能再向下移动 if((resultArray[shapeXY[0][0]+1][shapeXY[0][1]]+resultArray[shapeXY[1][0]+1][shapeXY[1][1]] +resultArray[shapeXY[2][0]+1][shapeXY[2][1]]+resultArray[shapeXY[3][0]+1][shapeXY[3][1]] >=1) ) { CurrentShapeOnBottom(); return true; } topTrue=false; return false; } } //当前形状到达画布底部后进行的操作 function CurrentShapeOnBottom(){ resultArray[shapeXY[0][0]][shapeXY[0][1]]=1; resultArray[shapeXY[1][0]][shapeXY[1][1]]=1; resultArray[shapeXY[2][0]][shapeXY[2][1]]=1; resultArray[shapeXY[3][0]][shapeXY[3][1]]=1; if(ClearRow()==false){ return; } rectY=0; rectX=9; randmShape=Math.floor(Math.random()*7+1) startRun=true; topTrue=true; clearInterval(tid); tid=setInterval("DrawTetris();",200); } //计分 var vpoint=0; //清除满格行,并计分 function ClearRow(){ var row=new Array(); var spaceRow=new Array(); var spaceRows=new Array(); for(var i=0;i<20;i++){ spaceRow[i]=0; } row[0]=shapeXY[0][0]; for(var i=1;i<4;i++){ for(var j=0;j<row.length;j++){ if(row[j]!=shapeXY[i][0]){ if(row[j]<shapeXY[i][0]){ row.push(shapeXY[i][0]); } else { row.unshift(shapeXY[i][0]); } break; } } } var isNeedRedraw=false; for(var i=0;i<row.length;i++){ var rowState=0; for(var j=0;j<20;j++){ rowState+=resultArray[row[i]][j]; } if(rowState==20){ resultArray.splice(row[i],1); resultArray.unshift(spaceRow); vpoint+=10; document.getElementById("txtPoint").value=vpoint; isNeedRedraw=true; } } if(isNeedRedraw==true){ ctx.clearRect(0,0,200,400); RedrawCanvas(); } else { if(shapeXY[0][0]==0 || shapeXY[1][0]==0 || shapeXY[2][0]==0 || shapeXY[3][0]==0){ document.getElementById("idState").innerText="Game Over"; clearInterval(tid); return false; } } return true; } //清除满格行后,重新绘制画布 function RedrawCanvas() { for(var i=0;i<40;i++){ for(var j=0;j<20;j++){ if(resultArray[i][j]==1){ DrawRect(j*unitLen,i*unitLen); } } } } //绘制形状中的小方格 function DrawRect(x,y){ ctx.fillStyle="red"; ctx.strokeStyle="black"; ctx.lineWidth=1; ctx.fillRect(x,y,10,10); ctx.strokeRect(x,y,10,10); } function DrawLine() { /* var i=1; for(i=1;i<40;i++) { ctx.beginPath(); ctx.strokeStyle="black"; ctx.lineWidth=1; ctx.moveTo(0,i*10); ctx.lineTo(200,i*10); ctx.stroke(); } */ } </script> </head> <body onload="init()"> <canvas id="canvas" width="200" height="400"> 浏览器不支持HTML5 Canvas </canvas> <br> <form id="f" name="f" onsubmit="init()"> <br> <input id="btnStart" type="submit" value="开始" /> <label id="idState"></label> </div> <br> <label>分数:</label> <input id="txtPoint" readonly="true" value="0" /> </form> </body> </html>