canvas版《俄罗斯方块》

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

See the Pen Canvas俄罗斯方块 by 王美建 (@wangmeijian) on CodePen.

**********************************************************************

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>
View Code

持续优化中…… 

 

作者:古德God
出处:http://www.cnblogs.com/wangmeijian
本文版权归作者和博客园所有,欢迎转载,转载请标明出处。
如果您觉得本篇博文对您有所收获,请点击右下角的 [推荐],谢谢!

原文地址:https://www.cnblogs.com/wangmeijian/p/4772898.html