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

 更新时间:2017年1月9日 10:00  点击:2137

本文实例讲述了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数据结构与算法技巧总结》、《JavaScript数学运算用法总结》、《JavaScript切换特效与技巧总结》、《JavaScript查找算法技巧总结》、《JavaScript动画特效与技巧汇总》、《JavaScript错误与调试技巧总结》及《JavaScript遍历算法与技巧总结》

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

[!--infotagslink--]

相关文章

  • JavaScript判断浏览器及其版本信息

    本篇文章主要分享了通过window.navigator来判断浏览器及其版本信息的实例代码。具有一定的参考价值,下面跟着小编一起来看下吧...2017-01-23
  • js实现浏览器打印功能的示例代码

    这篇文章主要介绍了js如何实现浏览器打印功能,文中示例代码非常详细,帮助大家更好的理解和学习,感兴趣的朋友可以了解下...2020-07-15
  • Nest.js参数校验和自定义返回数据格式详解

    这篇文章主要给大家介绍了关于Nest.js参数校验和自定义返回数据格式的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-03-28
  • 详解前端安全之JavaScript防http劫持与XSS

    作为前端,一直以来都知道HTTP劫持与XSS跨站脚本、CSRF跨站请求伪造。防御这些劫持最好的方法是从后端入手,前端能做的太少。而且由于源码的暴露,攻击者很容易绕过防御手段。但这不代表我们去了解这块的相关知识是没意义的,本文的许多方法,用在其他方面也是大有作用。...2021-05-24
  • 利用JS实现点击按钮后图片自动切换的简单方法

    下面小编就为大家带来一篇利用JS实现点击按钮后图片自动切换的简单方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2016-10-25
  • js+css实现回到顶部按钮(back to top)

    这篇文章主要为大家详细介绍了js+css实现回到顶部按钮back to top回到顶部按钮,感兴趣的小伙伴们可以参考一下...2016-03-03
  • js实现上传图片及时预览

    这篇文章主要为大家详细介绍了js实现上传图片及时预览的相关资料,具有一定的参考价值,感兴趣的朋友可以参考一下...2016-05-09
  • js实现调用网络摄像头及常见错误处理

    这篇文章主要介绍了js实现调用网络摄像头及常见错误处理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-03-07
  • JS实现随机生成验证码

    这篇文章主要为大家详细介绍了JS实现随机生成验证码,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-09-06
  • JavaScript仿支付宝密码输入框

    那么今天我就用JavaScript代码来实现这个效果吧,那么首先介绍一下整个的思路,首先我们先将确定输入密码的位数,我的需求是5位,那么就用一个div标签包住5个input标签...2016-01-02
  • js组件SlotMachine实现图片切换效果制作抽奖系统

    这篇文章主要介绍了js组件SlotMachine实现图片切换效果制作抽奖系统的相关资料,需要的朋友可以参考下...2016-04-19
  • vue.js 表格分页ajax 异步加载数据

    Vue.js通过简洁的API提供高效的数据绑定和灵活的组件系统.这篇文章主要介绍了vue.js 表格分页ajax 异步加载数据的相关资料,需要的朋友可以参考下...2016-10-20
  • 基于JavaScript实现表单密码的隐藏和显示出来

    为了网站的安全性,很多朋友都把密码设的比较复杂,但是如何密码不能明显示,不知道输的是对是错,为了安全起见可以把密码显示的,那么基于js代码如何实现的呢?下面通过本文给大家介绍JavaScript实现表单密码的隐藏和显示,需要的朋友参考下...2016-03-03
  • js实现列表按字母排序

    这篇文章主要为大家详细介绍了js实现列表按字母排序,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-08-11
  • 基于JavaScript实现文字超出部分隐藏

    这篇文章主要介绍了基于JavaScript实现文字超出部分隐藏 的相关资料,需要的朋友可以参考下...2016-03-01
  • 浅析AngularJS Filter用法

    系统的学习了一下angularjs,发现angularjs的有些思想根php的模块smarty很像,例如数据绑定,filter。如果对smarty比较熟悉的话,学习angularjs会比较容易一点,这篇文章给大家介绍angularjs filter用法详解,感兴趣的朋友一起学习吧...2015-12-29
  • 一个关于JS正则匹配的踩坑记录

    这篇文章主要给大家介绍了一个关于JS正则匹配的踩坑记录,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-04-13
  • NodeJS实现阿里大鱼短信通知发送

    本文给大家介绍的是nodejs实现使用阿里大鱼短信API发送消息的方法和代码,有需要的小伙伴可以参考下。...2016-01-20
  • JS实现响应鼠标点击动画渐变弹出层效果代码

    这篇文章主要介绍了JS实现响应鼠标点击动画渐变弹出层效果代码,具有非常自然流畅的动画过度效果,涉及JavaScript针对鼠标事件的响应及页面元素样式的动态操作相关技巧,需要的朋友可以参考下...2016-03-28
  • node.JS md5加密中文与php结果不一致怎么办

    这次文章要给大家介绍的是node.JS md5加密中文与php结果不一致怎么办,不知道具体解决办法的下面跟小编一起来看看。 因项目需要,需要Node.js与PHP做接口调用,发现nod...2017-07-06