JS+Canvas实现的俄罗斯方块游戏完整实例


Posted in Javascript onDecember 12, 2016

本文实例讲述了JS+Canvas实现的俄罗斯方块游戏。分享给大家供大家参考,具体如下:

试玩(没有考虑兼容低版本浏览器):

JS+Canvas实现的俄罗斯方块游戏完整实例

**********************************************************************
9月3日更新:
修复了隐藏的比较深的BUG
加上暂停、再来一次功能
速度随分数增高而递减
添加log日志
*********************************************************************

通过写这个游戏收获几点:

1、canvas的isPointInPath方法不支持fillRect、strokeRect的上下文。

2、canvas有内边距(padding)时,传入isPointInPath方法的坐标需要减去padding值。

3、通过json的方法JSON.parse(JSON.stringify(Object))可以快速克隆Object对象,但是要注意:当Object对象里有方法时,克隆过来依然是引用关系。

源码:

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>canvas版俄罗斯方块</title>
  <style>
  #canvas{
    background: #272822;
    display: block;
    margin: 0 auto;
  }
  </style>
</head>
<body>
<p style="text-align: center;">操作:↑变形;↓下移;←左移;→右移</p>
<canvas id="canvas" width="640" height="600">
  您的浏览器不支持canvas!
</canvas>
<script>
/****************************
作者:王美建 2015年9月3日20:30:35
*后续可添加怪异变形,类似于L可变成Z
*积分随关卡递增
*初始化部分historyBlock
****************************/
var tetris = {
  canvas : document.getElementById("canvas"),
  ctx : this.canvas.getContext("2d"),
  width : 481,
  height : 600,
  logLen: 8,
  mapColor: "#464741",
  logColor: "green",
  status: 'ready',
  unit : 30,
  curText : "开始",
  blockData : function(index, row, col){
    var r = row || 1,
      c = col || Math.floor((this.col - 3)/2) + 2,
      block = [
        [
          {color: 'red', status: 1, data: [{x: r, y:c-1}, {x: r+1, y:c-1}, {x: r+1, y:c}, {x: r+1, y:c+1}], center: {x: r, y: c}},
          {color: 'red', status: 2, data: [{x: r-1, y:c-1}, {x: r-1, y:c}, {x: r, y:c-1}, {x: r+1, y:c-1}], center: {x: r, y: c}},
          {color: 'red', status: 3, data: [{x: r-1, y:c-1}, {x: r-1, y:c}, {x: r-1, y:c+1}, {x: r, y:c+1}], center: {x: r, y: c}},
          {color: 'red', status: 4, data: [{x: r-1, y:c+1}, {x: r, y:c+1}, {x: r+1, y:c+1}, {x: r+1, y:c}], center: {x: r, y: c}}
        ],
        [
          {color: 'green', status: 1, center: {x: r, y:c}, data: [{x: r, y:c+1}, {x: r+1, y:c-1}, {x: r+1, y:c}, {x: r+1, y:c+1}]},
          {color: 'green', status: 2, center: {x: r, y:c}, data: [{x: r-1, y:c-1}, {x: r, y:c-1}, {x: r+1, y:c-1}, {x: r+1, y:c}]},
          {color: 'green', status: 3, center: {x: r, y:c}, data: [{x: r, y:c-1}, {x: r-1, y:c-1}, {x: r-1, y:c}, {x: r-1, y:c+1}]},
          {color: 'green', status: 4, center: {x: r, y:c}, data: [{x: r-1, y:c}, {x: r-1, y:c+1}, {x: r, y:c+1}, {x: r+1, y:c+1}]}
        ],
        [
          {color: 'blue', status: 1, center: {x: r, y:c}, data: [{x: r, y:c-1}, {x: r, y:c}, {x: r+1, y:c}, {x: r+1, y:c+1}]},
          {color: 'blue', status: 2, center: {x: r, y:c}, data: [{x: r+1, y:c-1}, {x: r, y:c-1}, {x: r, y:c}, {x: r-1, y:c}]}
        ],
        [
          {color: 'orange', status: 1, center: {x: r, y:c}, data: [{x: r+1, y:c-1}, {x: r+1, y:c}, {x: r, y:c}, {x: r, y:c+1}]},
          {color: 'orange', status: 2, center: {x: r, y:c}, data: [{x: r-1, y:c}, {x: r, y:c}, {x: r, y:c+1}, {x: r+1, y:c+1}]}
        ],
        [
          {color: 'yellow', status: 1, center: {x: r, y:c}, data: [{x: r, y:c-1}, {x: r, y:c}, {x: r+1, y:c-1}, {x: r+1, y:c}]}
        ],
        [
          {color: 'aqua', status: 1, center: {x: r, y:c}, data: [{x: r, y:c-1}, {x: r, y:c}, {x: r, y:c+1}, {x: r-1, y:c}]},
          {color: 'aqua', status: 2, center: {x: r, y:c}, data: [{x: r+1, y:c}, {x: r, y:c}, {x: r, y:c+1}, {x: r-1, y:c}]},
          {color: 'aqua', status: 3, center: {x: r, y:c}, data: [{x: r, y:c-1}, {x: r, y:c}, {x: r, y:c+1}, {x: r+1, y:c}]},
          {color: 'aqua', status: 4, center: {x: r, y:c}, data: [{x: r, y:c-1}, {x: r, y:c}, {x: r+1, y:c}, {x: r-1, y:c}]}
        ],
        [
          {color: 'indigo', status: 1, center: {x: r, y:c}, data: [{x: r, y:c-1}, {x: r, y:c}, {x: r, y:c+1}, {x: r, y:c+2}]},
          {color: 'indigo', status: 2, center: {x: r, y:c}, data: [{x: r-2, y:c}, {x: r-1, y:c}, {x: r, y:c}, {x: r+1, y:c}]}
        ]
      ]
    return block[index];
  },
  init : function(){
    var self = this;
    self.reset();
    self.addEvent("keydown", window, function(ev){
      var ev = ev || window.event,
        code = ev.keycode || ev.which;
      if(self.handle[code] && self.status === "play"){
        self.handle[code].call(self);
        self.createMap();
        ev.preventDefault();
      }
    })
    self.addEvent("click", document, function(ev){
      self.createMap(ev);
    })
    return this;
  },
  reset: function(){
    var self = this;
    self.score = 0;
    self.speed = 1000;
    self.log = [];
    self.historyBlock = [];
    self.row = Math.floor(self.height/self.unit);
    self.col = Math.floor(self.width/self.unit);
    self.curBlockIndex = Math.round(Math.random()*6);
    self.curBlocks = self.blockData(self.curBlockIndex);
    self.curBlock = self.curBlocks[0];
    self.createNext().createMap();
    return this;
  },
  createNext: function(){
    var self = this;
    self.nextBlockIndex = self.status === "ready" ? self.curBlockIndex : Math.round(Math.random()*6);
    self.nextBlocks = self.blockData(self.nextBlockIndex, 4, self.col+3);
    self.nextBlock = self.nextBlocks[0];
    return this;
  },
  addEvent : function(ev, ele, callback){
    if( ele.addEventListener ){
      ele.addEventListener(ev,callback,false);
    }else{
      ele.attachEvent("on"+ev, callback);
    }
  },
  createMap : function(ev){
    var self = this,
      ctx = self.ctx;
    ctx.clearRect(0, 0, self.canvas.width, self.canvas.height);
    for (var i = 0; i < self.col; i++) {
      for (var j = 0; j < self.row; j++) {
        ctx.save();
        ctx.strokeStyle = self.mapColor;
        ctx.strokeRect(i*self.unit, j*self.unit, self.unit, self.unit);
        ctx.stroke();
        ctx.restore();
      };
    };
    self.showText(ev).createBlock().createLog();
    if(self.status !== "ready"){
      self.drawRect(self.curBlock.data);
    }
    return this;
  },
  createBlock : function(){
    var self = this,
      block = self.curBlock.data;
    self.drawRect(self.historyBlock);
    if(self.collide(40, true)){
      block.map(function(val){
        val.x--;
      })
      setTimeout(function(){
        clearInterval(self.timer);
        if(localStorage.getItem("score") === null){
          localStorage.setItem("score", self.score);
        }else if(localStorage.getItem("score") - self.score < 0 ){
          localStorage.setItem("score", self.score);
          alert("新纪录!"+self.score+"分!");
          self.printLog({log:"新纪录!"+self.score+"分!", color: 'red'});
          return;
        }
        self.status = "over";
        self.curText = "重来";
        self.showText();
        self.printLog({log:"GAME OVER", color: 'red'});
      },10)
    }
    return this;
  },
  drawRect : function(block){
    var self = this;
    for (var i = 0; i < block.length; i++) {
      self.ctx.save();
      self.ctx.fillStyle = block[i].color || self.curBlock.color;
      self.ctx.strokeStyle = self.mapColor;
      self.ctx.fillRect((block[i].y - 1)*self.unit, (block[i].x - 1)*self.unit, self.unit, self.unit );
      self.ctx.strokeRect((block[i].y - 1)*self.unit, (block[i].x - 1)*self.unit, self.unit, self.unit );
      self.ctx.restore();
    };
  },
  move : function(){
    var self = this;
    clearInterval(self.timer);
    self.timer = setInterval(function(){
      // 实时刷新数据 大坑!
      var curBlock = self.curBlock,
        data = self.curBlock.data;
      if( self.collide() || data.some(function(val){
        return val.x + 1 > self.row;
      }) ){
        clearInterval(self.timer);
        self.historyBlock.push.apply(self.historyBlock, data.map(function(val){
            val.color = curBlock.color;
            return val;
        }));
        self.remove();
        self.curBlockIndex = self.nextBlockIndex;
        self.curBlocks = self.blockData(self.curBlockIndex);
        self.curBlock = self.curBlocks[0];
        self.createNext().createMap().move();
        return false;
      }
      for (var i = 0; i < data.length; i++) {
        data[i].x++;
      };
      self.curBlock.center.x++;
      self.createMap();
    }, self.speed)
    return this;
  },
  remove : function(){
    var self = this,
      count = {},
      n = 0,
      maxRow = 0,
      delArr = [],
      block = self.historyBlock;
    for (var i = 0; i < block.length; i++) {
      if(count[block[i].x]){
        count[block[i].x].length += 1;
      }else{
        count[block[i].x] = [1];
      }
    };
    console.log( count )
    for (var attr in count) {
      if(count[attr].length === self.col){
        n++;
        //maxRow = attr > maxRow ? attr : maxRow;
        for (var i = 0; i < block.length; i++) {
          if(block[i].x == attr){
            delArr = block.splice(i, 1);
            i--;
          }
        };
        count[attr].length = 0;
        block.forEach(function(val){
          val.x < attr && (val.x += 1);
        })
      }
    };
    // 边消除边下降会死循环
    if(n > 0){
      self.score += n*100;
      self.printLog("得分+"+n*100);
      // 一次消除3行奖励100分
      if(n === 3){
        self.score += 100;
        self.printLog("奖励"+100+"分~");
      }
      // 一次消除4行奖励200分
      if(n === 4){
        self.score += 200;
        self.printLog("奖励"+200+"分~");
      }
      /*block.forEach(function(val){
        val.x < maxRow && (val.x += n);
      })*/
      self.changeSpeed();
    }
  },
  changeSpeed: function(){
    var self = this;
    if( self.score >= 3000 && self.score < 5000 ){
      self.speed = 800;
    }else if( self.score >= 5000 && self.score < 10000 ){
      self.speed = 600;
      self.score += 100;
    }else if( self.score >= 10000 && self.score < 20000 ){
      self.speed = 400;
      self.score += 150;
    }else if( self.score >= 20000 && self.score < 40000 ){
      self.speed = 200;
      self.score += 200;
    }else if( self.score >= 40000 ){
      self.speed = 100;
      self.score += 300;
    }
    return this;
  },
  collide : function(direction, isCreate){
    var block = JSON.parse(JSON.stringify(this.curBlock)),
      result = false,
      self = this;
    direction = direction || 40;
    // direction:碰撞方向,默认下方
    if(direction === 37){
      self.mLeft(block);
    }else if(direction === 38){
      block = self.distortion(block);
    }else if(direction === 39){
      self.mRight(block);
    }else if(direction === 40 && !isCreate){
      // 非新增方块则往下移动一格
      block.data.forEach(function(val){
        val.x++;
      })
    }
    result = block.data.some(function(val){
      return (val.x > self.row || val.y < 1 || val.y > self.col);
    })
    if(result){
      return result;
    }else{
      return block.data.some(function(val){
        return self.historyBlock.some(function(value){
          return (value.x === val.x && value.y === val.y);
        })
      })
    }
  },
  mLeft : function(block){
    if(block.data.every(function(val){
      return val.y - 1 >= 1;
    })){
      block.data.forEach(function(val){
        val.y--;
      })
      block.center.y--;
    }
  },
  mRight : function(block){
    var self = this;
    if(block.data.every(function(val){
      return val.y + 1 <= self.col;
    })){
      block.data.forEach(function(val){
        val.y++;
      })
      block.center.y++;
    }
  },
  distortion : function(block){
    var self = this,
      curRow = block.center.x,
      curCol = block.center.y,
      status = block.status + 1 > self.curBlocks.length ? 1 : block.status + 1;
    self.curBlocks = self.blockData(self.curBlockIndex, block.center.x, block.center.y);
    return self.curBlocks[status-1];
  },
  // 控制:上(变形)、右(右移)、下(下移)、左(左移)
  handle : {
    // 左键 code 37
    37: function(){
      var self = this;
      if(!self.collide(37)){
        self.mLeft(self.curBlock);
      }
    },
    // 上键 code 38
    38: function(){
      var self = this;
      if(!self.collide(38)){
        self.curBlock = self.distortion(self.curBlock);
      }
    },
    // 右键 code 39
    39: function(){
      var self = this;
      if(!self.collide(39)){
        self.mRight(self.curBlock);
      }
    },
    // 下键 code 40
    40: function(){
      var self = this;
      if(!self.collide()){
        self.curBlock.data.forEach(function(val){
          val.x++;
        })
        self.curBlock.center.x++;
      }
    }
  },
  showText: function(ev){
    var self = this,
      ctx = self.ctx;
    ctx.clearRect(self.width, 0, self.canvas.width - self.width, self.height);
    ctx.save();
    ctx.beginPath();
    ctx.font = "20px Verdana";
    ctx.fillStyle = "green";
    ctx.fillText("下一个:", self.width+10, 30);
    ctx.fillText("分数:", self.width+10, 200);
    ctx.fillText("速度:", self.width+10, 260);
    ctx.fillText("作者:王美建", self.width+10, 580);
    ctx.fillStyle = "red";
    ctx.fillText(self.score, self.width+10, 230);
    ctx.fillText(self.speed + "毫秒", self.width+10, 290);
    ctx.fillStyle = "green";
    ctx.fillRect(self.width + 30, 320, 100, 40);
    // isPointInPath对fillRect()兼容不好 又一个大坑!
    ctx.rect(self.width + 30, 320, 100, 40);
    if( ev && ctx.isPointInPath(ev.layerX, ev.layerY) ){
      switch(self.status){
        case "ready":
        self.status = "play";
        self.curText = "暂停";
        self.log = ["开始游戏."];
        self.createNext().move();
        break;
        case "play":
        self.status = "paush";
        self.curText = "继续";
        clearInterval(self.timer);
        self.printLog("暂停.");
        break;
        case "paush":
        self.status = "play";
        self.curText = "暂停";
        self.printLog("继续游戏~");
        self.move();
        break;
        case "over":
        self.status = "play";
        self.curText = "暂停";
        self.reset().move();
        self.printLog("开始游戏~");
        break;
      }
    }
    ctx.fillStyle = "black";
    ctx.fillText(self.curText, self.width+60, 350);
    ctx.restore();
    ctx.closePath();
    self.nextBlock.data.forEach(function(val){
      val.color = self.nextBlock.color;
    })
    self.drawRect(self.nextBlock.data);
    return this;
  },
  printLog: function(log){
    var self = this;
    if(log){
      self.log.unshift(log);
      self.log.length > self.logLen && (self.log.length = self.logLen);
    }
    self.createLog();
    return this;
  },
  createLog: function(){
    var self = this,
      ctx = self.ctx;
    // 清除log
    ctx.clearRect(self.width+10, 380, 136, 170);
    self.log.forEach(function(val, index){
      if(val){
        ctx.save();
        ctx.font = "16px Verdana";
        ctx.fillStyle = val.color || self.logColor,
        ctx.fillText(val.log || val, self.width+10, 400+index*22);
        ctx.restore();
      }
    })
    return this;
  }
}
tetris.init();
</script>
</body>
</html>

持续优化中……

希望本文所述对大家JavaScript程序设计有所帮助。

Javascript 相关文章推荐
jquery uaMatch源代码
Feb 14 Javascript
解析jquery获取父窗口的元素
Jun 26 Javascript
jQuery中获取checkbox选中项等操作及注意事项
Nov 24 Javascript
基于iframe实现类似于ajax的页面无刷新
May 31 Javascript
jquery实现点击页面计算点击次数
Jan 23 Javascript
如何减少浏览器的reflow和repaint
Feb 26 Javascript
js实现类似jquery里animate动画效果的方法
Apr 10 Javascript
Angularjs的ng-repeat中去除重复数据的方法
Aug 05 Javascript
值得收藏的vuejs安装教程
Nov 21 Javascript
开发Vue树形组件的示例代码
Dec 21 Javascript
create-react-app构建项目慢的解决方法
Mar 14 Javascript
JavaScript中七种流行的开源机器学习框架
Oct 11 Javascript
最好用的Bootstrap fileinput.js文件上传组件
Dec 12 #Javascript
jQuery Ajax File Upload实例源码
Dec 12 #Javascript
VueJs与ReactJS和AngularJS的异同点
Dec 12 #Javascript
layer实现弹窗提交信息
Dec 12 #Javascript
详解Javascript数据类型的转换规则
Dec 12 #Javascript
设置jquery UI 控件的大小方法
Dec 12 #Javascript
JS中检测数据类型的几种方式及优缺点小结
Dec 12 #Javascript
You might like
开启PHP Static 关键字之旅模式
2015/11/13 PHP
php实现给二维数组中所有一维数组添加值的方法
2017/02/04 PHP
Yii2框架中使用PHPExcel导出Excel文件的示例
2017/08/09 PHP
PHP+Redis链表解决高并发下商品超卖问题(实现原理及步骤)
2020/08/03 PHP
js用图作提交按钮或超连接
2008/03/26 Javascript
js判断是否为数组的函数: isArray()
2011/10/30 Javascript
浅谈重写window对象的方法
2014/12/29 Javascript
解析微信JS-SDK配置授权,实现分享接口
2016/12/09 Javascript
使用jsonp实现跨域获取数据实例讲解
2016/12/25 Javascript
javascript常用的设计模式
2017/02/09 Javascript
jQuery树控件zTree使用方法详解(一)
2017/02/28 Javascript
Vue如何引入远程JS文件
2017/04/20 Javascript
Vue input控件通过value绑定动态属性及修饰符的方法
2017/05/03 Javascript
Three.js利用dat.GUI如何简化试验流程详解
2017/09/26 Javascript
JS实现中文汉字按拼音排序的方法
2017/10/09 Javascript
Vue在页面数据渲染完成之后的调用方法
2018/09/11 Javascript
nodejs微信开发之授权登录+获取用户信息
2019/03/17 NodeJs
通过JS深度判断两个对象字段相同
2019/06/14 Javascript
layui实现左侧菜单点击右侧内容区显示
2019/07/26 Javascript
JS回调函数 callback的理解与使用案例分析
2019/09/09 Javascript
vue实现点击按钮切换背景颜色的示例代码
2020/06/23 Javascript
[10:34]DOTA2上海特级锦标赛全纪录
2016/03/25 DOTA
python pyinstaller 加载ui路径方法
2019/06/10 Python
Django框架基础模板标签与filter使用方法详解
2019/07/23 Python
关于Numpy中的行向量和列向量详解
2019/11/30 Python
Python操作MySQL数据库的示例代码
2020/07/13 Python
监理员的岗位职责
2013/11/13 职场文书
升职自荐书范文
2013/11/28 职场文书
科学发展观演讲稿
2014/09/11 职场文书
党的群众路线专项整治方案
2014/11/03 职场文书
2014年护士个人工作总结
2014/11/11 职场文书
毕业设计指导教师评语
2014/12/30 职场文书
承诺书范本
2015/01/21 职场文书
2016秋季校长开学典礼致辞
2015/11/26 职场文书
Android自定义scrollview实现回弹效果
2022/04/01 Java/Android
python游戏开发之pygame实现接球小游戏
2022/04/22 Python