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 相关文章推荐
js屏蔽鼠标键盘(右键/Ctrl+N/Shift+F10/F11/F5刷新/退格键)
Jan 24 Javascript
JavaScript fontcolor方法入门实例(按照指定的颜色来显示字符串)
Oct 17 Javascript
JavaScript极简入门教程(三):数组
Oct 25 Javascript
js操作css属性实现div层展开关闭效果的方法
May 11 Javascript
关于获取DIV内部内容报错的原因分析及解决办法
Jan 29 Javascript
JQuery解析XML数据的几个简单实例
May 18 Javascript
JavaScript中的操作符类型转换示例总结
May 30 Javascript
JS实现放大、缩小及拖拽图片的方法【可兼容IE、火狐】
Aug 23 Javascript
JS与jQuery实现隔行变色的方法
Sep 09 Javascript
Bootstrap响应式导航由768px变成992px的实现代码
Jun 15 Javascript
Vue中使用ElementUI使用第三方图标库iconfont的示例
Oct 11 Javascript
express异步函数异常捕获示例详解
Nov 30 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
某大型网络公司应聘时的笔试题目附答案
2008/03/27 PHP
PHP Cookie的使用教程详解
2013/06/03 PHP
php比较两个绝对时间的大小
2014/01/31 PHP
一个简洁实用的PHP缓存类完整实例
2014/07/26 PHP
thinkPHP自定义类实现方法详解
2016/11/30 PHP
PHPstorm启用自动换行的方法详解(IDE)
2020/09/17 PHP
关于javascript中的typeof和instanceof介绍
2012/12/04 Javascript
javascript常用对话框小集
2013/09/13 Javascript
js读取配置文件自写
2014/02/11 Javascript
raphael.js绘制中国地图 地图绘制方法
2014/02/12 Javascript
js弹出确认是否删除对话框
2014/03/27 Javascript
最短的IE判断var ie=!-[1,]分析
2014/05/28 Javascript
实例分析js和C#中使用正则表达式匹配a标签
2014/11/26 Javascript
使用js实现数据格式化
2014/12/03 Javascript
Jquery实现动态切换图片的方法
2015/05/18 Javascript
jQuery选择器用法实例详解
2015/12/17 Javascript
详解Node.js中的事件机制
2016/09/22 Javascript
vue-router的钩子函数用法实例分析
2019/10/26 Javascript
jQuery实现手风琴效果(蒙版)
2020/01/11 jQuery
详解JavaScript类型判断的四种方法
2020/10/21 Javascript
JS实现手风琴特效
2020/11/08 Javascript
Python的Flask框架应用程序实现使用QQ账号登录的方法
2016/06/07 Python
Python元组拆包和具名元组解析实例详解
2018/03/26 Python
Python将list中的string批量转化成int/float的方法
2018/06/26 Python
对python 通过ssh访问数据库的实例详解
2019/02/19 Python
python实现弹跳小球
2019/05/13 Python
Python 中判断列表是否为空的方法
2019/11/24 Python
python使用配置文件过程详解
2019/12/28 Python
Tkinter中复选菜单是否被选中的判断与设置方式
2020/03/04 Python
关于Python 解决Python3.9 pandas.read_excel(‘xxx.xlsx‘)报错的问题
2020/11/28 Python
matplotlib之pyplot模块坐标轴标签设置使用(xlabel()、ylabel())
2021/02/22 Python
英国厨房与餐具用品为主的设计品牌:Joseph Joseph
2018/04/26 全球购物
美国在线购买空气净化器、除湿器、加湿器网站:AllergyBuyersClub
2021/03/16 全球购物
开工仪式主持词
2014/03/20 职场文书
周年庆典主持词
2014/04/02 职场文书
施工安全标语
2014/06/07 职场文书