canvas+js从0开始撸一个俄罗斯方块

8,903 阅读8分钟

最近工作之余看了一个俄罗斯方块小游戏实现的视频教程,觉得这个小游戏的算法挺有意思,打算整理出来学习参考一下。

虽然标题标着canvas,其实这个小游戏用到的canvas知识点甚少,其核心还是js算法问题。教程会比较枯燥,可能对实际工作中也没太大用处(不过可以用俄罗斯方块做一个营销小活动),写出来目的主要是为了学习一下这款小游戏的算法思想,以及锻炼解决问题的能力。

前言

本篇文章将分为几个模块逐步讲解:

  1. 游戏玩法介绍
  2. 算法思想
  3. 创建游戏地图
  4. 方块的创建
  5. 生成不同形状方块
  6. 方块下落
  7. 方块碰撞
  8. 方块左右移动
  9. 方块变形
  10. 方块向下加速
  11. 消除整行
  12. 游戏结束

游戏虽小,功能还挺复杂,需要的就是思维和耐心。下面直接进入正题。

游戏玩法介绍

俄罗斯方块想必大家都玩过,大体玩法简单说一下,在一个格子地图上随机下落不同形状的方块,通过←→方向键去控制方块的移动,以及↑键改变方块的形状,↓键加速其下落,当方块落下后满一行时消除该行。

来看一下我们最终实现的效果图:

tetris

算法思想

我们可以把俄罗斯方块一分为二的看,地图+方块。我们把地图看成一个二维的数组:

[
    [0,0,0,0,0,0...],
    [0,0,0,0,0,0...],
    [0,0,0,0,0,0...],
    [0,0,0,0,0,0...],
    ...
]

方块也看作一个二维数组

如Z形状的方块可看作为:

[
    [1,1,0],
    [0,1,1]
]

倒T形状的方块可看作为:

[
    [0,1,0],
    [1,1,1]
]

等等 然后两个数组结合起来:

[
    [0,0,1,1,0,0,0,0],
    [0,0,0,1,1,0,0,0],
    [0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0],
    [0,0,0,0,0,0,0,0],
]

上面是一个Z字形方块在第一行。

从上面数组可以一目了然的看到,我们渲染canvas的时候将数组值为0的元素渲染成地图的颜色,而元素值为1的渲染成方块的颜色即可。 那么方块的移动又是怎么做呢,很简单就是去改变方块左右的元素从0改为1,再将本来的位置重置为0即可,在视觉上造成方块的移动。

如下图一个Z字方块的移动:

arr

知道其核心思想之后,可以码代码开发了。

创建游戏地图

首先我们先准备一个canvas画布:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>canvas</title>
    <style>
        #myCanvas {
            position: absolute;
            left: 0;
            top: 0;
            right: 0;
            bottom: 0;
            margin: auto;
            background: #000;
        }
    </style>
</head>
<body>
<canvas id="myCanvas" height="500" width="500"></canvas>
<button id="stop">停止</button>
<button id="score">得分: 0</button>
<script src="/js/tetris/index.js"></script>
</body>
</html>

首先我们需要了解一下canvas的简单操作,在画布上画一个小方块:

index.js

let Tetris = {
    init(){
        //初始化
        const canvas = document.getElementById('myCanvas');
        this.gc = canvas.getContext('2d');
        this.render();
    },
    //渲染画布
    render(){
        //在坐标为100,100的位置画一个40*40的红色方块
        this.gc.fillStyle = 'red';
        this.gc.fillRect(100,100,40,40);
    }
};

Tetris.init();

如图:

canvas

做俄罗斯方块所需的canvas知识点你只需要知道fillStyle,fillRect即可,没有那么复杂。

接下来,我们要画出N*N的格子地图了。

先生成一个N*N的二维数组,值都为0:

function map(r,c){
    let data = [];
    for(let i = 0; i < c; i++){
        data.push([]);
        //每行长度为 r
        data[i].length = r;
        //所有元素默认值 0
        data[i].fill(0);
    }
    return data;
}
console.table(map(20,10));

20*10

根据上面的方块的渲染方式和地图数组的生成结合,来渲染出一个20*20的地图:

完整代码如下:

index.js

let Tetris = {
    init(){
        //初始化
        const canvas = document.getElementById('myCanvas');
        this.gc = canvas.getContext('2d');
        //20*20的格子
        let data = this.map(20,20);
        this.render(data);
    },
    map(r,c){
        let data = [];
        for(let i = 0; i < c; i++){
            data.push([]);
            //每行长度为 r
            data[i].length = r;
            //所有元素默认值 0
            data[i].fill(0);
        }
        return data;
    },
    render(data){
        //计算每个方块的宽高 方块之间间隔为4
        let w = 500/20 - 4;
        let h = 500/20 - 4;

        //计算方块的行列
        let r = data.length;
        let c = data[0].length;

        for(let i = 0; i < r; i ++){
            for (let j = 0; j < c; j++){
                //判断数组里的值 若为1 则渲染为红色 0 则渲染为白色
                this.gc.fillStyle = data[i][j] === 0 ? 'white' : 'red';
                this.gc.fillRect(
                    (w+4)*j+2,
                    (h+4)*i+2,
                    w,
                    h
                );
            }
        }
    }
};

Tetris.init();

分析一下上面稍微难以理解的是每个方块x,y轴值的计算。

详细分析如下:

画布宽高500 * 500 要平均分配20*20个方块,
那么方块的最大宽度为 500/20 = 25 
但是这样的话方块会挤在一块 因此我们再给每个方块之间增加4个单位的间距
于是 单个方块的 w/h = 500/20 - 4 = 21;
坐标的算法:
20个方块之间有19个间隙 剩下一个4单位的距离 分配到小方块距离左右两侧的间隙
于是可以列出:

n列   x坐标
0     w*0 + 2
1     w*1 + 2 + 4*1
2     w*2 + 2 + 4*2
3     w*3 + 2 + 4*3
...
n     w*n + 2 + 4*n 
所以第j列的x坐标可以归纳为 (w+4)*j + 2
y坐标亦然

执行完代码看效果如下:

map

长征已经成功走出第一步,接下来就是方块的创建了。

方块的创建

方块有多种类型,如一字型,L字型,Z字型,倒T字型,田字型等等,根据上面算法中提到的,每种类型可以用一个二维数组描述出来:

blockType: [
    [[1,1,1,1]],
    [[1,1],[1,1]],
    [[1,1,0],[0,1,1]],
    [[0,1,1],[1,1,0]],
    [[0,1,0],[1,1,1]],
    [[1,0,0],[1,1,1]],
    [[0,0,1],[1,1,1]]
]

那怎么把一个方块再地图上显示出来呢?

也很简单,只需要把方块的二维数组插入到地图的数组中就可以了。

简单实现如下:

//更新data数组
draw(block){
    /*
    * 假如block为Z字型的方块 [[1,1,0],[0,1,1]]
    */
    for (let i = 0; i < block.length; i++){
        for (let j = 0; j < block[0].length; j++){
            this.data[i][j + this.y] = block[i][j];
        }
    }
    console.table(this.data);
    //再次调用render方法更新画布
    this.render(this.data);
}

生成不同形状方块

要随机生成一个不同形状的方块,调用Math.random()即可。

贴完整的代码:

index.js

let Tetris = {
    //初始化
    init(){
        const canvas = document.getElementById('myCanvas');
        this.gc = canvas.getContext('2d');
        //20*20的格子
        this.data = this.map(20,20);
        //X轴的偏移量 之所以保存为变量 是以后我们做左右移动的是需要通过改变这个值来实现 
        this.x = 7;
        //随机生成一个方块
        this._block = this.block();
        this.draw(this._block);
    },
    //地图数据
    map(r,c){
        let data = [];
        for(let i = 0; i < c; i++){
            data.push([]);
            //每行长度为 r
            data[i].length = r;
            //所有元素默认值 0
            data[i].fill(0);
        }
        return data;
    },
    //随机生成一个类型的方块
    block(){
        let index = Math.floor(Math.random()*7);
        return this.blockType[index];
    },
    //方块的类型
    blockType: [
        [[1,1,1,1]],
        [[1,1],[1,1]],
        [[1,1,0],[0,1,1]],
        [[0,1,1],[1,1,0]],
        [[0,1,0],[1,1,1]],
        [[1,0,0],[1,1,1]],
        [[0,0,1],[1,1,1]]
    ],
    //重绘画布
    draw(block){
        for (let i = 0; i < block.length; i++){
            for (let j = 0; j < block[0].length; j++){
                //要向x轴偏移 需要为j加一个偏移量即可
                this.data[i][j + this.x] = block[i][j];
            }
        }
        console.table(this.data);
        this.render(this.data);
    },
    //渲染
    render(data){
        //计算每个方块的宽高 方块之间间隔为4
        let w = 500/20 - 4;
        let h = 500/20 - 4;

        //计算方块的行列
        let r = data.length;
        let c = data[0].length;

        for(let i = 0; i < r; i ++){
            for (let j = 0; j < c; j++){
                //判断数组里的值 若为1 则渲染为红色 0 则渲染为白色
                this.gc.fillStyle = data[i][j] === 0 ? 'white' : 'red';
                /*
                 * 坐标算法
                 * 画布宽度500 小方格宽度21 个数20 则 留下的空隙宽度为 500 - 21*20 = 80 其中 20个小方块可分4单位的间隙
                 * 20个方块之间有19个间隙 剩下一个4单位的距离 分配到小方块距离左右两侧的间隙
                 * 总结一下规律
                 * n行     x坐标
                 * 0       w*0 + 2
                 * 1       w*1 + 2 + 4
                 * 2       w*2 + 2 + 4*2
                 * 3       w*3 + 2 + 4*3
                 * ...
                 * n       w*n + 2 + 4*n
                 * 所以第j列的x坐标可以归纳为 (w+4)*j + 2
                 * y坐标亦然
                 */
                this.gc.fillRect(
                    (w+4)*j+2,
                    (h+4)*i+2,
                    w,
                    h
                );
            }
        }
    }
};

Tetris.init();

刷新页面会出现随机的方块如下图:

生成方块

方块下落

方块下落,其实就是要改变方块在画布上的Y轴位置,怎么持续下落呢,很显然需要开一个定时器。

[未完待续。。。]

方块碰撞

[未完待续。。。]

方块左右移动

[未完待续。。。]

方块变形

[未完待续。。。]

方块向下加速

[未完待续。。。]

消除整行

[未完待续。。。]

游戏结束

[未完待续。。。]

后言

最近时间比较紧,剩下模块慢慢更新。大体思想了解之后,其他问题都可以迎刃而解,无非就是需要耐心和不断的调试。

喜欢就赞一波,给点更新的动力😆

参考文献

俄罗斯方块游戏实战

人人贷大前端技术博客中心

最后广而告之。 欢迎访问人人贷大前端技术博客中心

里面有关nodejs react reactNative 小程序 前端工程化等相关的技术文章陆续更新中,欢迎访问和吐槽~

上一篇: 微信小程序打怪之定时发送模板消息(node版)