写在前面得话:

这篇文章主要记录了我是怎么一步一步写出俄罗斯方块,整个代码用的函数编程,主要是为了让一些不熟悉es6, 面向对象写法得 新手能更容易看明白,全部得代码中都是一些js的基础知识,很容易理解。要说有点麻烦的,那就是游戏过程中的各种检测。但是只要你多思考,你就能理解代码为什么要那样写,你也可以实现这个游戏。(当然也许你有更好的实现方法)。

预览地址:http://blog.cwlserver.top/demo/Tetris.html

1,先理清游戏逻辑

  • 游戏场景:场景大小为 10*18,
  • 下落时间:初始方块每隔1秒,会下落一格。随着游戏进行时间得增加,方块下落时间间隔会缩短。
  • 操作方法:方向键得 上下左右 分别控制方块得, 变形,加速下落,左移,右移。
  • 方块类型:一共7种类型得方块。每次随机出现一种, 每种方块由数个 1*1大小得小方块组成
  • 方块下落:当方块落到底, 或者下一格已经被占,方块停止下落,然后会有一个新的方块出现
  • 方块左右移动:方块左右移动时,如果左,右是墙或者是已经被占,方块将不能移动。
  • 方块变形:方块逆时针旋转90°,变形时需要判断方块是否可以变形。
  • 游戏会有下一个方块得提示
  • 消行:当一行被填满时,这一行将被消除
  • 计分规则: 消1行得2分,2行4分,3行8分,4行16分
  • 游戏结束: 当方块下落到底,并且方块超出游戏场景时,判定游戏结束

2,分步实现游戏中得功能

html结构

    <div id="box">
<canvas id="canvas" width="300" height="540"></canvas>
<div class="scorebox">
<p>游戏已进行: <span id="game-time">00:00:00</span></p><br>
<p>当前得分: <span id="score">0</span></p><br>
<p>下一个方块:</p><br>
<canvas id="next" width="120" height="120"></canvas><br>
<p class="btns"><button id="pause">暂停</button><button id="restart">重新开始</button></p>
</div>
</div>

构建场景

因为场景大小是10x18,所以我决定用一个 10x18得二维数组来模拟场景,这样方便和方块做碰撞检测。

//定义列数
var ROW = 10;
//定义行数
var COL = 18;
//游戏得分
var SCORE = 0;
//游戏场景
var area = new Array(COL);
for(var i=0; i<area.length; i++){
area[i] = new Array(ROW).fill(0);
}
/*
最终得到得area是这样得
area = [
[0,0,0,0....]
[0,0,0,0....]
[0,0,0,0....]
...
]
*/

构建小方块

小方块我同样使用二维数组来构建

//定义各种方块得数组, 一共7种不同得方块,数组中的1,2,3,4..这些数字主要是为了每个方块设置不同的颜色
var data = {
'o':[
[1, 1],
[1, 1]
],
's':[
[2, 0, 0],
[2, 2, 0],
[0, 2, 0]
],
'5':[
[0, 0, 3],
[0, 3, 3],
[0, 3, 0]
],
'l':[
[4, 0, 0],
[4, 0, 0],
[4, 4, 0]
],
't':[
[5, 5, 5],
[0, 5, 0],
[0, 0, 0]
],
'j':[
[0, 0, 6],
[0, 0, 6],
[0, 6, 6]
],
'|':[
[0, 7, 0, 0],
[0, 7, 0, 0],
[0, 7, 0, 0],
[0, 7, 0, 0]
]
};
//定义方块得颜色,每个数字对应一种颜色
var aColor = ['', '#fff', '#0000FF', '#00FF00', '#CC00FF', '#CCFFFF','#FFFF33','#99FFFF'];
//将data中得key放到一个字符串中 方便随机调用
var sKey = 'os5ltj|';
//定义当前方块, 当前方块默认null;
var cur = null;
//因为游戏中会有下一个方块得提示, 所以这里要提前声明一下
var next = null;
//定义一个生成方块得函数
function createBox(){
//首先创建提示方块
if(!next){
//从skey中随机取出一个键名
var rnd = Math.floor(Math.random()*sKey.length);
//根据key取得方块数组
var box = data[sKey[rnd]];
//每一个方块都有,x, y, box 这三个属性
next = {
//方块初始在场景中间位置,方块左移 x--, 右移 x++;
x: Math.floor((ROW-box[0].length)/2),
//方块在垂直方向得位置,刚好在场景外, y++ 方块下落
y: -box[0].length,
//方块得数组
box: box
};
}
//当前方块不存在时, 创建当前方块
if(!cur){
//直接下一个方块变成当前这个
cur = next;
//然后再重新生成下一个
next = {
x: Math.floor((ROW-box[0].length)/2),
y: -box[0].length,
box: data[sKey[Math.floor(Math.random()*sKey.length)]]
}
}
}

现在想一个问题,有了场景和方块的数据之后,如何把他们联系起来?

我的处理方式是这样的,在方块下落的过程中,方块和场景是分开的,方块的位置和场景是分开刷新的。在下落的过程中我会 检测方块和场景是否发生碰撞,如果发生了碰撞,将当前方块的数组合并到场景的数组中,使方块变成场景的一部分,同时生成一个新的方块。看下代码如何实现

//将当前方块合并到场景
function mergeBoxArea(){
//循环当前方块
for(var i=0; i<cur.box.length; i++){
//这里的判断是为了当方块的一部分在场景外的时候,将那一部分跳过,只计算在场景中的部分
if(i+cur.y>=0){
for(var j=0; j<cur.box[i].length){
//将当前方块数组中不为0的项,和 场景中当前位置为0的项合并
if(cur.box[i][j] !== 0 && area[i+cur.y][j+cur.x] == 0){
//合并的结果, 将场景中当前位置的值设置为方块对应位置的值
area[i+cur.y][j+cur.x] = cur.box[i][j];
}
}
}
}
//将方块合并入场景的同时要尝试 消行
var arr = isRemove(area);
if(arr.length !== 0){
for(var i=0; i<arr.length; i++){
area.splice(arr[i], 1)
area.unshift(new Array(ROW).fill(0))
}
//更新得分
SCORE+=Math.pow(2, arr.length)
scoreEle.innerHTML = SCORE;
};
}
//碰撞检测
//垂直方向的碰撞检测, 需要接受当前方块做为参数,
//作用:检测方块下落一格之后和场景的碰撞情况,如果会碰撞返回true,否则返回false;
function collide(cur){
var box = cur.box;
var len = box.length;
var x = box.x;
//因为是检测下一个位置,所以要+1;
var y = box.y + 1;
for(var i=0; i<len; i++){
//做碰撞检测同样需要将场景外的方块部分排除掉
if(i+y>=0){
//方块的数组都是n*n的所以都用len
for(var j=0; j<len; j++){
//将方块为0的项不检测
if(box[i][j] !== 0){
//第一种碰撞情况:当i+y大于等于场景的高度时,说明方块出界
//第二种碰撞情况:方块没有出界,但是场景中的这个位置,被占用了
if(i+y>=area.length || (i+y<area.length && area[i+y][j+x] !== 0)){
//碰撞了返回 true
return true;
}
}
}
}
}
//代码执行到这里时说明没有碰撞,返回false;
return false;
}
//水平方向的移动限制
//当用键盘控制方块左右移动的时候,需要检测左右是否是墙,或者方块,这里检测的也是下一个位置的碰撞情况
//如果没有墙或者方块(不碰撞),返回true
//如果碰撞, 返回 false;
//接受参数: 当前方块:cur, 移动方向: dir -1|0|1
function bMove(cur, dir){
//当前位置加上方向 就是 下一个位置
var x = cur.x+dir;
for(var i=0; i<cur.box.length; i++){
for(var j=0; j<cur.box[i].length; j++){
if(cur[i][j] !== 0){
//这里发生碰撞的情况有3中
//1.方块在左边出界了, 这时 j+x<0
//2.方块在右边出界了, j+x>= ROW
//3.方块没有出界,但是场景中的这个位置被占用 area[i+cur.y][j+x]!==0
// 加上 i+cur.y>=0 && j+x>=0 && area[i+cur.y] 是为了防止报错
if(j+x<0 || j+x==ROW || ( i+cur.y>=0 && j+x>=0 && area[i+cur.y] && area[i+cur.y][j+x]!==0)){
return false;
}
}
}
}
return true;
}

如何处理方块旋转?

方块的旋转比较容易处理,就把二维数组旋转一下就可以了。但是要注意方块旋转的时候也是需要检测 旋转的合理性, 可以想象一下,一个长条下落的过程中,如果他的左右两边都是方块,这种情况肯定是不能旋转的(其它方块同理)。还有一种情况就是,方块靠墙下落的时候,旋转一下之后,有一部分转到墙里面去了,这种也是不合理的,但是玩游戏的时候,这种情况也能旋转,所以出现这种情况的时候,我们需要修正一下方块的位置。 下面看代码怎么写

//此函数用于检测方块是否能够旋转
/*
参数: 当前方块 cur
返回值: true //方块可以直接旋转
false //方块不能旋转,即使是在尝试修正位置之后,就是上面说到的左右都是方块的情况
cur.x //当返回 一个数值的时候,说明 将方块水平移动到这个位置后,可以旋转, 即上面说的修正位置
*/
function bRotate(cur){
//在这里复制一个旋转后的方块出来,用于检测
var _cur = {x: cur.x, y:cur.y, box: rotateBox(cur.box)};
//检测方块旋转之后,水平和垂直方向的碰撞情况, 如果在任意方向会发生碰撞
if( collide(_cur) === true || bMove(_cur, 0) === false ){
//尝试水平移动方块,移动方向是分别向左,向右移动2格
for(var i=0; i<2; i++){
//方块靠近左边的时候,尝试向右移动,并且检测移动的合理性
if(_cur.x<4 && bMove(_cur, 1)){
_cur.x++;
}
//靠近右侧的时候,向左移动,并且检测移动的合理性
if(_cur.x>6 && bMove(_cur, -1)){
_cur.x--;
}
//移动之后再检测是否碰撞, 如果不会发生碰撞, 返回移动后的位置
if(collide(_cur) === false && bMove(_cur, 0)){
return _cur.x;
}
}
//代码执行到这里的时候说明,移动了之后仍会碰撞
return false;
//如果旋转之后不会发生碰撞,直接返回true;
}else{
return true;
}
}
//旋转数组的函数
function rotateBox(arr){
var res = [];
for(var i=0; i<arr.length; i++){
res.push([]);
}
//旋转
for(var i=0; i<arr.length; i++){
for(var j=0; j<arr[i].length; j++){
res[arr.length-1-y][x] = arr[x][y];
}
}
return res;
}

现在开始处理游戏的刷新, 计算游戏的时间

游戏用 requestAnimationFrame 更新

var timer = null;
//记录一个旧的时间,这里用于辅助计算, 每次刷新的间隔时间
var oldTime = Date.now();
//n 用于累加 raf 的间隔时间
var n = 0;
//游戏运行时间 单位 毫秒
var gameTime = 0;
//方块下落的间隔时间
var step = 1000;
//游戏是否暂停
var bPause = false;
//获取dom元素
//主场景canvas
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
//提示下一个方块 canvas
var nextCanvas = document.getElementById('next');
var nextctx = nextCanvas.getContext('2d');
//游戏得分
var scoreEle = document.getElementById('score');
//暂停按钮
var pauseEle = document.getElementById('pause');
//重新开始按钮
var restartEle = document.getElementById('restart')
//显示游戏时间
var gameTimeEle = document.getElementById('game-time');
//开启主循环
timer = requestAnimationFrame(animate);
//主循环函数
function animate(){
//累加 raf 的间隔时间
n+=Date.now()-oldTime;
//累加游戏运行时间
gameTime+=Date.now()-oldTime;
oldTime = Date.now();
//方块要开始下落了
if(n>=step){
n = 0;
//每秒钟更新一次游戏时间
updateGameTime()
//根据游戏进行时间提高游戏难度
changeDifficulty();
//方块下落之前要先检测是否会发生碰撞
//会发生碰撞
if(collide(cur)){
//会碰撞,并且此时,如果方块有一部分在外面,说明游戏结束
if(cur.y<0){
gameover()
//正常的碰撞
}else{
//将方块合并入游戏场景
mergeBoxArea()
//并将cur 设置为null
cur = null;
//产生一个新的方块
createBox()
}
//不会碰撞
}else{
cur.y++;
}
}
//更新游戏场景
drawArea();
//画提示方块
drawNextBox();
timer = requestAnimationFrame(animate);
}
//更新游戏场景
function drawArea(){
ctx.clearRect(0, 0, 300, 540)
ctx.save()
ctx.scale(30, 30)
//ctx.fillStyle = '#fff';
//画游戏场景
drawcube(ctx, area)
//画当前方块
drawcube(ctx, cur.box, cur.x, cur.y)
ctx.restore();
}
//更新提示
function drawNextBox(){
nextctx.clearRect(0, 0, 120, 120)
nextctx.save()
nextctx.scale(30, 30)
//画下一个方块
next&&drawcube(nextctx, next.box)
nextctx.restore();
}
//画方块,接受一个ctx对象,一个数组, 数组的偏移值
function drawcube(ctx, arr, x, y){
x = x || 0;
y = y || 0;
for(var i=0; i<arr.length; i++){
for(var j=0; j<arr[i].length; j++){
if(arr[i][j] !== 0){
//设置方块的颜色
ctx.fillStyle = aColor[arr[i][j]];
ctx.fillRect(j+x, i+y, 1, 1)
}
}
}
}

监听键盘事件,移动方块

//监听键盘事件
document.addEventListener('keydown', function(ev){
if(bPause || !cur){
return false;
}
var keycode = ev.keyCode;
switch(keycode){
//左
case 37:
//是否能向左移动
if(bMove(cur, -1)){
cur.x--;
}
break;
//右
case 39:
//是否能向右移动
if(bMove(cur, 1)){
cur.x++;
}
break;
//下
case 40:
//如果触底或者落到其它方块上面
if(collide(cur)){
if(cur.y<0){
gameover()
}else{
mergeBoxArea()
cur = null;
createBox()
}
}else{
cur.y++;
}
break;
//上
case 38:
//是否能旋转 当n为true时可以直接旋转,当n为数值时需要将方块x位置移动到此处才能旋转
var rotateRes = bRotate(cur);
//可以直接旋转
if(rotateRes === true){
cur.box = rotateBox(cur.box);
//不能旋转
}else if(rotateRes === false){
console.log('不能旋转')
//需要移动之后才能旋转
}else{
cur.x = rotateRes;
cur.box = rotateBox(cur.box);
}
break;
}
})

处理游戏结束, 游戏暂停, 游戏重新开始, 消行, 更新游戏得分, 更新游戏运行时间等等

//点击暂停按钮
pauseEle.addEventListener('click', function(){
var html = this.innerHTML;
if(html === '暂停'){
pause();
this.innerHTML = '继续';
}else{
start();
this.innerHTML = '暂停';
}
})
//点击重新开始
restartEle.addEventListener('click', function(){
restart();
})
//暂停游戏
function pause(){
cancelAnimationFrame(timer);
bPause = true;
}
//继续
function start(){
timer = requestAnimationFrame(animate);
bPause = false;
}
//重新开始
function restart(){
//重置场景
for(var i=0; i<area.length; i++){
for(var j=0; j<area[i].length; j++){
area[i][j] = 0;
}
}
cancelAnimationFrame(timer);
timer = requestAnimationFrame(animate);
bPause = false;
pauseEle.innerHTML = '暂停';
//重置游戏时间
gameTime = 0;
//更新游戏时间
updateGameTime();
cur = null;
//创建第一个方块
createBox();
}
//游戏结束
function gameover(){
cancelAnimationFrame(timer);
alert('游戏结束, 您一共获得:'+SCORE+"分")
restart()
}
//检测是否可以消行,并将可以消除得行 加入结果数组返回出去
function isRemove(area){
var arr = [];
for(var i=0; i<area.length; i++){
var remove = true;
//如果数组的一行的每一项都不为0说明可以消除
for(var j=0; j<area[i].length; j++){
if(area[i][j] == 0){
remove = false;
}
}
//储存消除行的索引
if(remove){
arr.push(i)
}
}
return arr;
}
//更新游戏运行时间
function updateGameTime(){
var n = gameTime/1000;
var h = Math.floor(n/(60*60));
n%=60*60;
var m = Math.floor(n/60);
n%=60;
var s = Math.floor(n);
h = h<9?'0'+h:''+h;
m = m<9?'0'+m:''+m;
s = s<9?'0'+s:''+s;
gameTimeEle.innerHTML = h+':'+m+':'+s;
}
//根据游戏时间修改难度(方块下落间隔时间)
function changeDifficulty(){
//游戏进行5分钟 方块下落间隔为300ms
if(gameTime>=1000*60*5){
step = 300;
//游戏运行3分钟 方块下落间隔500ms
}else if(gameTime>=1000*60*3){
step = 500;
//游戏运行2分钟 方块下落间隔700ms
}else if(gameTime>=1000*60*2){
step = 700;
}
}

到这里整个游戏差不多就算完了,最终的预览demo和现在的代码并某些细节不是完全一样。

作者简介:陈蔚磊(高级前端工程师),公众号“铅笔学园”前端内容合作作者之一,目前在一培训机构任职前端工程师,掌握最前沿的前端技术。

铅笔学园:IT资源分享|知识分享,做初级程序员的指明灯

【javascript小案例】从0开始实现一个俄罗斯方块的更多相关文章

  1. JavaScript小案例-阶乘!

    JavaScript小案例-阶乘! 阶乘:就是像台阶一样一阶一阶的,从高阶到低阶,依次乘下来!代码超少!容易理解! // factorial 阶乘 // 如果 function factorial(n ...

  2. 【前端学习笔记】JavaScript 小案例合集

    获取一个0-9的随机数: Math.round(Math.random()*9); 去除数组中重复的元素: var arr=[1,3,5,4,3,3,1,4] function editArr(arr ...

  3. 使用定时器限制点击按钮发送短信(附源码)--JavaScript小案例

    不说多哈,有注释哦,直接贴代码了哈,有疑问请追评呢…… 1.禁用按钮: this.disabled = "disabled"(this指按钮)或: this.disabled = ...

  4. javascript常用小案例

    常用javascript小案例 样式调节 //注: 这个可以控制td中的字段成行显示 #modelInfos td,th { white-space: nowrap; } //文本输入框随着内容尺寸往 ...

  5. javascript之JSON小案例,实现添加数据与清楚数据

    对json应用给出一个小案例,加深一些理解: <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" & ...

  6. javascript综合小案例,校验用户注册信息提交

    完成这个综合小案例,对于html.css.javascript的大部分内容复习快结束了. 这里做一个小案例--要实现的功能,以一张图片的形式给出: 首先,写出提交数据之后进入的页面代码: <!D ...

  7. 【JavaScript定时器小案例】常见的几种定时器实现的案例

    [JavaScript定时器小案例]常见的几种定时器实现的案例 博客说明 文章所涉及的资料来自互联网整理和个人总结,意在于个人学习和经验汇总,如有什么地方侵权,请联系本人删除,谢谢! 说明 在日常开发 ...

  8. JavaScript apply函数小案例

    //回调函数1 function callback(a,b,c) { alert(a+b+c); } //回调函数2 function callback2(a,b) { alert(a+b); } / ...

  9. (24/24) webpack小案例--自己动手用webpack构建一个React的开发环境

    通过前面的学习,对webpack有了更深的认识,故此节我们就利用前面相关知识自己动手用webpack构建一个React的开发环境,就算是一个小案例吧. 注:此处使用的开发工具是Webstorm. 1. ...

随机推荐

  1. 环境搭建之allure的安装配置,及简单使用

    环境准备 首先是要安装好jdk的电脑上,运行java.javac这些命令都没有问题,要不安装allure时会报错 下载allure 如果直接用Jenkins上的插件,并不需要下载安装 allure官网 ...

  2. k个一组翻转链表(java实现)

    题目: 给出一个链表,每 k 个节点一组进行翻转,并返回翻转后的链表. k 是一个正整数,它的值小于或等于链表的长度.如果节点总数不是 k 的整数倍,那么将最后剩余节点保持原有顺序. 示例 : 给定这 ...

  3. 方程的解_NOI导刊2010提高

    方程的解 给定x,求\(a_1+a_2+...+a_k=x^x\ mod\ 1000\)的正整数解解的组数,对于100%的数据,k≤100,x≤2^31-1. 解 显然x是可以快速幂得到答案的,而该问 ...

  4. js判断输入的input内容是否为数字

    有时候我们输入的input的内容需要判断一下是否是数字,所以为了更好的客户体验,在前端先处理一下: <input type="text" name="val&quo ...

  5. Facebook主页照片和封面照片的尺寸要求

    为什么好好的照片上传到Facebook后效果总不理想?为了避免你的照片在上传时被压缩,建议你尽量调整一下图片大小和格式,下面一起来看看Facebook主页照片和封面照片的尺寸要求. 1. Facebo ...

  6. erlang下lists模块sort(排序)方法源码解析(一)

    排序算法一直是各种语言最简单也是最复杂的算法,例如十大经典排序算法(动图演示)里面讲的那样 第一次看lists的sort方法的时候,蒙了,几百行的代码,我心想要这么复杂么(因为C语言的冒泡排序我记得不 ...

  7. c#+Unity3D

    Windows系统unity3d中的快捷键 自己整理常用; f2 改名 shift + 三维轴中间:45度 点击左键时 F:聚焦 V顶点贴合 ws拉近拉远 ad左右 qe 上下 长按右键 自由调换视角 ...

  8. mysql Access denied for user root @localhost (using password:YES)错误

    C:\AppServ\MySQL> mysql -u root -p Enter password:  ERROR 1045 (28000): Access denied for user 'r ...

  9. thymeleaf下拉框从后台动态获取集合数据并回显选中

    今天遇到从后台集合中取出对象在前台页面下拉列表展示: <select name="signature" lay-search="" class=" ...

  10. 【C/C++】C++11 Variadic Templates

    Variadic Templates 1.function template:利用“参数个数逐一递减”的特性,实现递归函数调用 template <typename T, typename... ...