虽然只是个很简答的小游戏,时间安排上:一天逻辑功能代码五天美化样式完善功能。。实现上比较简单,只用到了HTML+CSS+JavaScript。代码部分一会会做一个详细的解释说明,风格上主要是新拟态风格,看上去比较简约大气。
先来列举一下这个游戏的主要功能和一些扩展的功能。
主要功能
1、能够实现AD 控制不同形状方块的移动
2、空格可以直接下落、S可以加速方块下落速度
3、W可以控制方块逆时针旋转
4、落到游戏界面底部,或者任何一块落在已经被固定方块的上部(后面都叫落地了)方块被固定,同时产生新的模型块继续下落
5、如果有一行完整的方块,就可以消除那一整行,并且上面的方块向下移动一格(两行三行同理)
大概和平时玩过的俄罗斯方块在规则上没有太大变动,还有一些扩展的功能
6、得分机制
6.1 一次性消除多行会有额外得分
6.2 空格迅速下落也可以获得额外分数
7、分数增加到一定程度等级提升,方块下落速度会增加
8、有多种模式可以选择
8.1 经典 传统俄罗斯方块
8.2 挑战模式 会出现传统方块意外的畸形方块=、=(老容易死)
8.3 竞速模式
8.3.1 计时竞速
8.3.2 得分竞速
8.3.3 消行竞速
9、可以选择不同的难度等级,普通和困难模式下每隔一段时间底部会额外增加一行相间的方块
10、增加一个提前预览下一个方块的功能。
11、美化一下样式。后面会介绍一些实现新拟态风格的主要要用到的css样式属性和制作这个css中一些我比较常用的属性(定位属性、样式属性这些)
实现的逻辑和相关代码
经典俄罗斯方块里有七种不同类型的方块,分别是正L型、反L型、正Z型、反Z型、凸字型、田字型、一字型。加之要实现方块各种的旋转,可以想到用一个对象数组来保存不同形状的方块在一个4×4的方块网格中的坐标,之后生成的div类根据这个对象数组保存的下标来生成不同形状的方块。比如“L形状”的方块在下面这个4×4网格中就可以用这样一个对象表示:
{
0:{ row:2 , col:0 },
1:{ row:2 , col:1 },
2:{ row:2 , col:2 },
3:{ row:1 , col:2 }
}
L形状的一个模型示例
同理可以这样保存所有类型的方块,然后把他们全部放到一个数组里,之后生成元素块再根据这个数据源保存的坐标把方块移动到对应的位置。如下面这段代码,这样就保存了所有方块的坐标。
var models = [
{//正L型
0:{row:2,col:0},
1:{row:2,col:1},
2:{row:2,col:2},
3:{row:1,col:2}
},
{//反L型
0:{row:2,col:1},
1:{row:2,col:2},
2:{row:2,col:3},
3:{row:1,col:1}
}
..... //各种形状的方块坐标
]
然后用div标签构造一个游戏框,我们规定一个方块的大小是20px(px指的是像素单位),并且规定俄罗斯方块游戏界面的容器是12行20列,由此可以设置定义一个游戏进行的容器类,这里取名叫container。
.container{
width: 240px;
height: 400px;
position: relative;
}
这里要把container属性中的position属性设置为relative,元素块的position属性设置为absolute。这样可以实现由他生成的子对象的top和left属性会根据这个这个父对象的左边界和上边界设置,就是子对象相对于这个父对象容器的位置。
然后定义一个activity_model类,代表的是每一个最小单位的元素块,边长为20px。
.activity_model{
width: 20px;
height: 20px;
position: absolute;
background-color:gray;//方便区分设置为灰色
}
然后引入CurrentX和CurrentY,代表的是网格相对整个游戏容器内中的坐标。可以按照下图理解:
0 | ||||||||||||
1 | ||||||||||||
2 | ||||||||||||
3 | ||||||||||||
4 | ||||||||||||
5 | ||||||||||||
6 | ||||||||||||
0 1 2 3 4 5 6 7 8 9 10 11 |
例如此时的CurrentX和CurrentY分别为1和3,这样每个元素对于游戏容器中相对的坐标就可以令其为
X坐标:CurrentX+每个元素块的col属性
Y坐标:CurrentY+每个元素块的row属性
由此我们可以写一个生成模型的函数
这段代码仅仅生成了四个activity_model放在游戏容器container中,并没有更改元素块的实际坐标,后续通过locationBlocks()函数将生成的四个模型块定位。这里重新设置一个函数的目的是为了增加代码的复用性,因为后面的旋转和移动函数需要更新CurrentX和CurrentY以及模型中每个元素块的row和col属性,需要重新渲染的时候可以重复使用locationBlocks这段代码。
function createModel()
{
rand = 0; //rand为生成的模型的种类
currentModel = models[rand];//currentModel是一个全局的变量
for(var key in currentModel)//遍历四次,models[0]中共保存了四个元素块的坐标
{
var divEle = document.createElement("div");
divEle.className = "activity_model";
document.getElementById("container").appendChild(divEle);//container增加divEle为子对象
}
locationBlocks();//代码在后面分析
autoDown();//自动降落的函数,这里暂时先不管
}
下面是locationBlocks()代码:
function locationBlocks(){
var eles = document.getElementsByClassName("activity_model");//得到页面中的所有activity_model,串成一个数组
for( var i =0 ;i<eles.length;i++)
{
eles[i].style.top = ( currentY + currentModel[i].row ) * 20 + "px";
eles[i].style.left= ( currentX + currentModel[i].col ) * 20 + "px";
}
}
locationBlocks()的功能是获取之前createModel()中创建的四个元素块的位置,根据之间保存的对象数组中每个对象中每个元素块的row和col属性设置元素块相对游戏容器container的坐标。( 这里不要忘了把坐标转换为px属性哦!!因为定义的元素块大小为20px,所以实际的left和top属性应该是坐标乘以20,最后还要加上"px"字符串,不清楚的话可以去手册上查一查left和top属性的用法)
到这里应该可以实现在container中出现一个灰色的L型方块了!
然后我们来实现方块在容器中的移动。
function move(x,y) {//控制块元素移动
currentX += x;
currentY += y;
locationBlocks();
}
思路很简单,左移就是向整个网格的横坐标减一,右移下移同理,更新完CurrentX和CurrentY后调用locationBlocks()函数重新设置容器内四个activity_model的坐标即可。
接下来是方块的旋转:
旋转前 |
旋转后 |
可以发现每个元素块坐标的变化:
( 1 , 0 ) ====> ( 0 , 2 )
( 2 , 0 ) ====> ( 0 , 1 )
( 2 , 1 ) ====> ( 1 , 1 )
( 2 , 2 ) ====> ( 2 , 1 )
观察能够发现( x , y ) 坐标经过逆时针旋转变换后会变成( y , 3-x );(学过线性代数的盆友们也可以用里面的一个旋转变换公式去得到。其实质可以理解为对一个4×4矩阵进行一次旋转变换,这里比较简单直接观察就能归纳出结论。
所以我们就可以来写我们的旋转函数:
function rotate(){
for(var key in currentModel){ //依次取出所有模型中保存每个元素块的坐标,四个元素块的坐标都得进行旋转变换
var blockModel = currentModel[key];
var temp = blockModel.row;//因为后面的row会被重新设置,所以需要用一个temp保存原先这个row的值
blockModel.row = blockModel.col;
blockModel.col = 3 - temp;
locationBlocks();//更新完后重新根据坐标属性画出元素块的位置
}
}
到这里我们最最最最基本的变换模型函数都已经写完啦!下面还有一个问题,如何调用这个函数控制屏幕中方块的移动呢?
这里已经封装好了一个键盘按下事件发出的信号,可以写成
document.onkeydown = function ( event ) { }
event指的就是一个事件对象,因为我们写了当键盘被按下时执行这个函数,那么这个event指的就是event被按下这个事件。键盘上不同按钮被按下会对event这个对象里的属性赋值。这里有一个keyCode属性,不同按钮被按下的时候这个keyCode的值会变成对应的数字。这里可以通过alert(event.keyCode)的方式得到对应按钮被按下时的keyCode值,就可以执行对应的操作。
下面是键盘监听事件的代码:
document.onkeydown = function(event) {
//alert(event.keyCode);//下面case后面具体的数字可以通过调试这个alert获得
if(Over == 0)return;
switch(event.keyCode)
{
case 65: //a
move(-1,0);
break;
case 68: //d
move(1,0)
break;
case 87: //w
rotate();
break;
case 83: //s
move(0,1);
break;
case 32: //瞬间降落 空格
downImmediately();//迅速下落的函数
break;
case 80:
pause(); //p键暂停
break;
default:
break;
}
}
这个时候已经可以控制方块的移动和旋转了,后面还有一个问题,此时的方块是允许重叠和旋转的,所有我们还要在编写两个判定函数,分别是边界判定和重叠判定。
边界判定函数:
function checkBound()
{
var leftBound = 0;
/*col_count和row_count是定义的常量,分别为12和20,代表容器的宽和高*/
var RightBound = col_count;
var bottomBound = row_count;
for(var key in currentModel)//遍历当前模型中每个元素的坐标
{
var blockModel = currentModel[key];
if( (blockModel.col+currentX) < 0 ) //其中一个元素左侧越界
{
currentX++; //就将整个模型的横坐标加一
}
if( (blockModel.col+currentX) >= RightBound )//右侧越界
{
currentX--;
}
if ( (blockModel.row+currentY) >= bottomBound)
{
currentY--;
fixBottomModel();//如果落地了就执行固定函数,将所有元素块固定,下述
}
}
//结束时不用调用locationBlocks(),我们在控制变换里调用这个函数,那里面有locationBlocks()画出元素块
//这里只更改他的坐标
}
在写重叠判定函数之前,我们得先实现落地的固定,重叠判定指的就是可活动的元素块和已经落地的元素块之间不能重叠,那么这个已经落地的元素块的类肯定与activity_model不同。我们只要让其落地后将四个元素块的类名更改,同时调用createModel()函数制造一个新的activity_model即可。
下面这个是固定方块的类的名称和样式以及固定函数
.fixed_model{
width: 20px;
height: 20px;
background: rgb(150,150,150);偏灰篇白色 和activitym_model区分
position: absolute;
}
var fixedBlocks = {};//fixedBlocks是一个对象数组,用于记录所有落地方块的坐标,为后续的重叠判定和边界判定保存落地元素块及其坐标
function fixBottomModel () {
var activityModel = document.getElementsByClassName("activity_model");//获取场上所有activity_model
for(var i = activityModel.length-1;i>=0;i--)
{
activityModelEle = activityModel[i];
activityModelEle.className = "fixed_model"; //这里必须声明新的变量,不然下面更改这个变量类名时就无法识别这个row和col属性,因为
//把该块元素放入变量中
//
fixedBlocks[ ( currentY + currentModel[i].row ) + "_" +
( currentX + currentModel[i].col ) ] = activityModel[i];
}
/* 形如fixedBlocks[2_1] = activity_model */
/* 此时这个fixedBlocks[2_1] 就指向了场上的坐标为(2,1)的元素块 */
/* 这个语句是合法的,虽然看上去很违背常识,可以当成一个名为"2_1"的下标
isRemoveLine();//判定是否需要消除一行,后面会讲
createModel();//制造新的avtivity_model
}
这样我们就实现了方块落地固定,并且同时获取了被固定的所有方块的坐标。
然后我们就可以通过fixedBlocks记录的被固定方块的坐标来写后面的重叠判定、消行判定。
重叠判定:
function isMeet(x,y,model){ // (x,y)代表当前网格在容器中的坐标,model代表当前的方块模型
for(var k in model)
{
if(fixedBlocks[(y+model[k].row)+"_"+(x+model[k].col)]) //如果这个坐标在之前固定方块的记录里存在,就说明这个model中至少存在一个元素块与先前的固定方块中重复
{
return true;
}
}
return false;
}
在这之后我们就可以在我们的移动方块move和旋转方块rotate中加入这个重叠判定函数。这里有一点需要注意,在进行判定前要先对当前模型的数据进行一次深复制,然后将这个对象去执行旋转或者移动,如果这个对象没有发生重叠,则无需改变,如果重叠了则将原先复制的对象赋给发生重叠的对象。
可以理解为:
用深复制构造了一个当前对象的存档点,如果下面一步旋转或者移动操作会导致重叠,就退回到上一个存档点,也就是没有发生重叠的状态。
深复制:
1、大家可以去网上找一找lodash.min.js ( 一个具有一致接口、模块化、高性能等特性的JavaScript工具库 )
2、用下述语句就可以实现对象的深复制 ( 要将lodash.min.js放在引用这个函数的js文件的相同目录中 )
var cloneCurrentModel = _.cloneDeep(currentModel);//深复制
这里简述一下深复制和浅复制的区别:
比如执行下面的代码段:
<script>
var b = { b1 : 2 , b2 : 3}
var a = b;
a.b1 = 1;
alert(b.b1);//这里b中b1属性为1
</script>
我们并没有自己更改b中属性b1的值,而将b这个对象直接赋给a,更改a中属性b1的值,会发现原本b中b1的值也会跟随a中属性b1的改变而改变,这就是浅复制。因为实际上a中的b1和b中的b1指向的是同一块地址。深复制要实现的就是开辟另一块地址存放和b中相同的属性值。还有一点,深复制仅仅是对对象而言的,如果是单独的整数则不用担心浅复制共用一块地址。
<script>
var b = 1;
var a = b;
a = 2;
alert(b);//这里的b仍然会输出1
</script>
这里是一个旋转的代码,对之前的旋转函数进行了更改,加入了重叠判定:
function rotate(){
if(isPause )return;
checkBound();//边界判定
var cloneCurrentModel = _.cloneDeep(currentModel);//深复制
for ( var key in currentModel) //将当前对象执行一次旋转操作
{
var temp = currentModel[key].row;
currentModel[key].row = currentModel[key].col;
currentModel[key].col = 3 - temp;
}
locationBlocks(); //定位一次
if(isMeet(currentX,currentY,currentModel) )//如果发生了重叠
{
currentModel = cloneCurrentModel; //退回至之前的存档点
locationBlocks();
}
}
下面是加入了重叠判定和边界判定的move函数,这里要注意:
如果这个重叠判定是由下落引起的,就是说明这个方块落在了别的方块之上,则要将这个方块固定,所以如果move(x,y)中y不为0,则需要将当前的四个元素块固定住,即调用fixedBottomModel()函数。
function move(x,y) {//控制块元素移动
if(isPause)return;
if( isMeet(currentX+x,currentY+y,currentModel) )
{
if( y!= 0)//由于下落造成的重叠则固定元素块
{
fixBottomModel();
}
return 0;
}
currentX += x;
currentY += y;
k = checkBound();
locationBlocks();
}
接下来分别是消行判定和消行函数:
function isRemoveLine()//判断是否被清理
{
var Removerows = 0;//移除的行数,这里不管
for(var i = 0;i<row_count;i++) //逐行检验,第i行
{
var flag = true; //假设行需要被清楚
for(var j = 0;j<col_count;j++) //第i行第j列
{
if( !fixedBlocks[i+"_"+j]){ //如果不存在第i行第j列的元素
flag = false; //假设错误
break; //退出j循环,检验下一行
}
}
if(flag) //假设成立
{
removeLine(i); //删除第i行,下面讲解
Removerows++; //一次性删除的行数加1
}
}
addScores(Removerows); //增加分数的函数,一次性消除的行数不同增加的分数也不同
isLevelUp(); //到达一定的分数升级,提升难度,暂时先不管
}
function removeLine(line) //移除第line行
{
for(var i=0;i<12;i++) //共12列,删除第line行的12个被固定的元素块
{
document.getElementById("container").removeChild(fixedBlocks[line+"_"+i]);//容器内移除这个被固定的元素,显示上没有了
fixedBlocks[line+"_"+i]=null;//同时要将fixedBlocks中的第line行第i列的元素去除
}
downLine(line); //下降函数,将第line行之上的元素全部清空
addRemoveRows(); //增加被清除的行数,用于显示,这里不管
}
删除完了一行,接下来就是要将这一行之上的所有元素下降一格,实现通过downLine(line)这个函数。
function downLine(line) //被删除行之上的元素下降
{
for(var i= line- 1;i>=0;i--) //从第line-1行至第i行
{
for(var j = 0;j<col_count;j++) //这一行的第j个元素
{
if( !fixedBlocks[i+"_"+j])
continue;
fixedBlocks[(i+1)+"_"+j]=fixedBlocks[i+"_"+j];
fixedBlocks[(i+1)+"_"+j].style.top = (i+1)*step +"px";
fixedBlocks[i+"_"+j] = null;
}
}
}
这段代码的解释可以看我下面这个图:假设第三行的元素要被清除,0、1、2行的元素要全部下落一格
0 | |||||
1 | |||||
2 | |||||
3 |
那我们要执行的操作就是把第二行先把第二行挪到第三行,再把第一行挪至第二行,而不能从第零行开始,这里也比较好理解。
那么清楚第三行的操作用文字来描述:
1、清空第三行
2、将第二行中每一个个元素块复制到第三行
3、将第二行的元素清空
4、将第一行的元素块复制到第二行
5、将第一行清空
6、.......
理解完这些再回过头看这些代码逻辑会更清楚。
到这里我们已经基本实现了最基础的功能了,然后我们只需要最后增加一个定时器,每经过一段时间执行一次move(0,1)这个函数,让方块自动落下一格。
直接上代码:
function autoDown(){
if(downInterval) //这样写比较规范,如果已经存在了就重新设置
clearInterval(downInterval);
downInterval = setInterval(function(){
move(0,1);
}, 1000-40*level - 200*(hard_level-1))
/* 后面的level和hard_level可以不用管,这里设置的是难度等级上上速度会逐渐加快 */
}
讲一下定时器的用法:
Interval = setInterval( 执行的函数 , 时间间隔)
其中时间间隔的单位是毫秒。上述实现的就是每隔一段时间执行一次第一个参数中的函数。不要忘了把Interval设置为全局变量。
然后这个代码在哪里调用呢,很简单,每次创造出新的方块以后,都要让这个新的方块紫铜下降,所以可以把他直接放在createModel()函数中。这里不细讲,直接放在createModel函数的结尾即可。
到这里我么已经可以实现俄罗斯方块的基础操作了,还剩下最后一个游戏结束的判定,这个也很简单。
function isGameOver()
{
for( var i=0;i<col_count;i++)
{
if(fixedBlocks["1_"+i]){//如果被固定的方块到达了一定高度,则说明游戏结束
return true;
}
}
return false;
}
最后是gameOver()函数:关闭所有的定时器。
function gameOver()
{
clearInterval(downInterval);
}
到这里已经实现了比较简单的俄罗斯方块的实现 。篇幅有限我只讲到这里,这些代码在b站上的黑马程序员的一个俄罗斯方块编写教程里都有,写的也比较详细,大家有兴趣可以去看一看:
https://www.bilibili.com/video/BV11E411x7jw?from=search&seid=7199414031866678079
然后我也在此基础上对他做了些改进:
1、玩过俄罗斯方块的小伙伴都知道,有的时候方块贴着墙或者是在夹缝中旋转,他会自适应的平移一格,如下述情况:
直接旋转如果不对代码进行改进,他是拒绝执行旋转这个操作的,我在这个旋转代码的基础上进行了一部分的改进,能够比较自适应的调整方块的位置。
2、增加了计分、时间、等级和消除行数的显示,如下图
这里给大家分享一下相关的CSS样式表属性的设置:
第一个是制造出方块的立体感的CSS代码:
.activity_model{
width: 20px;
height: 20px;
border: 4px solid #9e8ec2;
box-sizing: border-box;
border-style: outset;
position: absolute;
display:block;
border-radius: 2px;
border-top-color: whitesmoke;
}
不同的颜色需要增加自己的颜色设置函数,这里是我写的不同的随机数下返回的rgb色彩字符串,在生成方块的同时根据随机数设置方块的backgroun-color属性即可,如果想要更美观的话还需要单独对他的边框样式进行设置。
想了解的话可以私信我。
function setContentColor(rand)
{
var a =rand;
if ( a == 0)
return "rgb(0, 78, 228)" ;
if ( a == 1)
return "rgb(217, 13, 47)";
if ( a == 2)
return "rgb(237,251,11)" ;
if ( a == 3)
return "rgb(200,200,130)";
if ( a == 4)
return "rgb(0,228,39)";
if ( a == 5)
return "rgb(0,228,227)";
if ( a == 6)
return "rgb(227,221,1)";
if ( a == 7)
return "rgb(164,87,145)";
if ( a == 8)
return "rgb(80,52,220)";
if( a == 9)
return "rgb(69,189,203)";
if( a == 10 )
return "rgb(81,191,84)"
if ( a == 11 )
return "rgb(226,42,174)";
if( a == 12 )
return "rgb(114,233,69)";
if( a == 13)
return "rgb(245,0,110)";
if( a == 14 )
return "rgb(31,167,245)" ;
}
最后是介绍一下新拟态风格:
按照我自己的理解,就是把元素的box-shadow属性进行合适的设置,调配出一种看上去非常高级很有立体感的样式。
先给大家看看我的封面样式:
下面附上相关代码和一些说明:
.container .box .img{
width: 100px;
height: 100px;
border-radius: 20px;
background: #ffffff;
box-shadow: 17px 17px 34px #c7c7c7,
-17px -17px 34px #ffffff;
display: flex;
align-items: center;
justify-content: center;
transition: box-shadow .2s ease-out;/*规定以慢速结束的过渡效果*/
position: relative;
}
关键是这个box-shadow属性,设置的是这个图片周围的阴影,可以是不同的颜色,上面这段代码就是设置右下角偏灰白,左上角白色。
这里贴一个新拟态风格自动生成css代码的网站:
https://neumorphism.io/#d7c6c6
直接说具体的数字可能不太明显,大家可以上去自己尝试一下,然后直接把代码复制到自己的css样式表中。
后面还有一个transition属性,.2s ease-out 代表的是这个box-shadow属性变化会在0.2s内渐变,配合上鼠标悬停时的样式,可以实现鼠标放上去模拟出这个按钮缓慢被按下的动态样式。
.container .box .img:hover{
box-shadow: 0px 0px 0px rgba(0,0,0,1),
0px 0px 0px rgba(255,255,255,1),
inset 4px 4px 5px rgb(52, 63, 73),
inset -5px -5px 10px rgba(255,255,255,1);
}
就可以不用动画实现一个按钮被按下的动态画面。
这里再分享一个小链接:
此外游戏结束时我还增加了一个上传分数的功能。
大家可以来玩哦嘿嘿嘿嘿嘿QAQ
想要具体相关代码的也可以私信我
最后放个链接,欢迎大家来踩一踩