设计游戏

课后整理 2020-12-14

俄罗斯方块是一款单机休闲小游戏。在俄罗斯方块的游戏界面中,有一组正在下落的方块,方块通常有4个,组合成各种不同的形状,游戏玩家需要控制正在下落的方块的移动,将这组方块摆放到合适的位置。只要下面某一行全部充满方块,没有空缺,那么这行就可以消除,上面的所有方块会整体掉下来。

俄罗斯方块的游戏界面比较简单,游戏的实现逻辑也不太复杂,非常适合作为JavaScript初学者作为进阶训练项目。本例采用HTML5的canvas来绘制游戏界面,用Local Storage来记录游戏状态。游戏演示效果如图1所示。

预览效果

 

图8 俄罗斯方块游戏演示效果

设计游戏界面

俄罗斯方块的游戏界面分为两个区域:速度、积分显示区;主游戏界面区。

【操作步骤】

第1步,新建网页文档,保存为index.html。

第2步,使用<div>包含框定义一个提示信息框,作为速度、积分显示区,该区域包含3个<span>标签,用于显示当前速度、当前积分、最高积分。代码如下所示:

<div  style="width:336px;">&nbsp;
    <div style="float:left;">速度:<span  id="curSpeedEle"></span> 当前积分:<span id="curScoreEle"></span></div>
    <div style="float:right;">最高积分:<span  id="maxScoreEle"></span></div>
</div>

第3步,游戏主界面通过HTML5的canvas进行绘制,由JavaScript动态生成。在网页头部区域插入<script type="text/javascript">标签。

第4步,为了让程序可以动态地改变界面大小,canvas的大小是由程序动态计算得到的,下面是动态计算、生成画布的JavaScript脚本。

var createCanvas =  function(rows , cols , cellWidth, cellHeight){
    tetris_canvas =  document.createElement("canvas");
    // 设置canvas组件的高度、宽度
    tetris_canvas.width = cols * cellWidth;
    tetris_canvas.height = rows * cellHeight;
    // 设置canvas组件的边框
    tetris_canvas.style.border = "1px  solid black";
    // 获取canvas上的绘图API
    tetris_ctx =  tetris_canvas.getContext('2d');
    // 开始创建路径  
    tetris_ctx.beginPath();
    // 绘制横向网络对应的路径
    for (var i = 1 ; i < TETRIS_ROWS ; i++)    {
        tetris_ctx.moveTo(0 , i * CELL_SIZE);
        tetris_ctx.lineTo(TETRIS_COLS *  CELL_SIZE , i * CELL_SIZE);
    }
    // 绘制竖向网络对应的路径
    for (var i = 1 ; i < TETRIS_COLS ; i++)    {
        tetris_ctx.moveTo(i * CELL_SIZE , 0);
        tetris_ctx.lineTo(i * CELL_SIZE ,  TETRIS_ROWS * CELL_SIZE);
    }
    tetris_ctx.closePath(); 
    // 设置笔触颜色
    tetris_ctx.strokeStyle = "#aaa";
    // 设置线条粗细
    tetris_ctx.lineWidth = 0.3;
    // 绘制线条
    tetris_ctx.stroke();
}

上面代码动态创建了一个canvas组件,该组件的高度、宽度是由程序动态计算得到,同时使用路径绘制了横向、竖向的线条,用于表示俄罗斯方块游戏界面上的网格。

设计游戏模型

俄罗斯方块的游戏界面是一个N×M的网格,每个网格上显示一张图片,这个网格用一个二维数组来定义,而每个网格上所显示的色块,对于底层的数学模型来说,不同的色块对应于不同的数值即可。本例直接使用一个二维数组来保存游戏的状态数据,不过由于JavaScript是动态语言,因此使用二维数组时依然是一维数组。

【操作步骤】

第1步,俄罗斯方块的游戏界面上还有一组正在下落的方块。这组正在下落的方块通常有4个,这4个方块的位置随时在改变,因此采用一个长度为4的数组来记录这4个方块的位置:

// 定义初始化正在下掉的方块
var initBlock =  function(){
    var rand = Math.floor(Math.random() *  blockArr.length);
    // 随机生成正在下掉的方块
    currentFall  = [
        {x: blockArr[rand][0].x , y:  blockArr[rand][0].y
            , color: blockArr[rand][0].color},
        {x: blockArr[rand][1].x , y:  blockArr[rand][1].y
            , color: blockArr[rand][1].color},
        {x: blockArr[rand][2].x , y:  blockArr[rand][2].y
            , color: blockArr[rand][2].color},
        {x: blockArr[rand][3].x , y:  blockArr[rand][3].y 
            , color: blockArr[rand][3].color}
    ];
};

在上面数组中,每个数组元素都是一个对象,对象包括x、y、color三个属性,分别代表了该方块所在的位置、颜色值。

第2步,为了初始化游戏状态,创建一个二维数组,这个二维数组记录了游戏界面上每个位置的方块值(不同的值代表不同的颜色)。具体代码如下所示:

// 该数组用于记录底下已经固定下来的方块。
var tetris_status = [];
for (var i = 0; i <  TETRIS_ROWS ; i++ ){
    tetris_status[i] = [];
    for (var j = 0; j < TETRIS_COLS ; j++ ) {
        tetris_status[i][j] = NO_BLOCK;
    }
}

上面代码创建了一个二维数组,并将这个二维数组的每个数组元素都赋值为NO BLOCK(也就是0),这代表该游戏界面上还没有方块。

第3步,为了能随机生成各种向下掉的方块组合((4个方块组成一组),预先把各种组合定义出来,然后每次需要开始掉落新的方块组时,随机取出一组即可。下面是随机获取掉落的方块组的代码。

// 定义几种可能出现的方块组合
var blockArr = [
    // 代表第一种可能出现的方块组合:Z
    [
        {x: TETRIS_COLS / 2 - 1 , y:0 ,  color:1},
        {x: TETRIS_COLS / 2 , y:0 ,color:1},
        {x: TETRIS_COLS / 2 , y:1 ,color:1},
        {x: TETRIS_COLS / 2 + 1 , y:1 ,  color:1}
    ],
    // 代表第二种可能出现的方块组合:反Z
    [
        {x: TETRIS_COLS / 2 + 1 , y:0 ,  color:2},
        {x: TETRIS_COLS / 2 , y:0 , color:2},
        {x: TETRIS_COLS / 2 , y:1 , color:2},
        {x: TETRIS_COLS / 2 - 1 , y:1 ,  color:2}
    ],
    // 代表第三种可能出现的方块组合: 田
    [
        {x: TETRIS_COLS / 2 - 1 , y:0 ,  color:3},
        {x: TETRIS_COLS / 2 , y:0 ,  color:3},
        {x: TETRIS_COLS / 2 - 1 , y:1 ,  color:3},
        {x: TETRIS_COLS / 2 , y:1 , color:3}
    ],
    // 代表第四种可能出现的方块组合:L
    [
        {x: TETRIS_COLS / 2 - 1 , y:0 ,  color:4},
        {x: TETRIS_COLS / 2 - 1, y:1 ,  color:4},
        {x: TETRIS_COLS / 2 - 1 , y:2 ,  color:4},
        {x: TETRIS_COLS / 2 , y:2 , color:4}
    ],
    // 代表第五种可能出现的方块组合:J
    [
        {x: TETRIS_COLS / 2  , y:0 , color:5},
        {x: TETRIS_COLS / 2 , y:1, color:5},
        {x: TETRIS_COLS / 2  , y:2, color:5},
        {x: TETRIS_COLS / 2 - 1, y:2, color:5}
    ],
    // 代表第六种可能出现的方块组合 : 条
    [
        {x: TETRIS_COLS / 2 , y:0 , color:6},
        {x: TETRIS_COLS / 2 , y:1 , color:6},
        {x: TETRIS_COLS / 2 , y:2 , color:6},
        {x: TETRIS_COLS / 2 , y:3 , color:6}
    ],
    // 代表第七种可能出现的方块组合 : ┵
    [
        {x: TETRIS_COLS / 2 , y:0 , color:7},
        {x: TETRIS_COLS / 2 - 1 , y:1 ,  color:7},
        {x: TETRIS_COLS / 2 , y:1 , color:7},
        {x: TETRIS_COLS / 2 + 1, y:1 , color:7}
    ]
];
// 定义初始化正在下掉的方块
var initBlock =  function()
{
    var rand = Math.floor(Math.random() *  blockArr.length);
    // 随机生成正在下掉的方块
    currentFall = [
        {x: blockArr[rand][0].x , y:  blockArr[rand][0].y
            , color: blockArr[rand][0].color},
        {x: blockArr[rand][1].x , y:  blockArr[rand][1].y
            , color: blockArr[rand][1].color},
        {x: blockArr[rand][2].x , y:  blockArr[rand][2].y
            , color: blockArr[rand][2].color},
        {x: blockArr[rand][3].x , y:  blockArr[rand][3].y 
            , color: blockArr[rand][3].color}
    ];
};

上面代码先定义了一个数组,这个数组定义了所有可能出现的方块组合,这里一共定义了7种组合。由于程序随机从上面数组中取出可能出现的方块组合,因此在游戏中完全可能出现上面7种掉落的方块组合。

实现游戏功能

定义了游戏状态模型之后,接下来处理方块组合的掉落,在掉落过程中还需要处理方块组合左移、右移、旋转等动画。

【操作步骤】

第1步,处理方块掉落。

让方块组合掉落,只需把每个方块的y属性增加1即可,但在处理方块组合掉落之前,需要判断方块组合是否可以掉落。如果出现如下两种情况,方块组合则不能掉落。

如果方块组合可以掉落,需要把方块组合原来所在位置的颜色清除,再把方块组合中每个方块的y属性增加1,最后把当前方块所在位置涂上相应的颜色。

当每次方块掉落之后,还需要逐行扫描每一行,判断是否某一行的方块已满,当方块已满时将该行方块消除,并增加积分。

下面moveDown()函数定义了方块掉落的实现方法:

// 控制方块向下掉。
var moveDown =  function(){
    // 定义能否下掉的旗标
    var canDown = true;    //①
    // 遍历每个方块,判断是否能向下掉
    for (var i = 0 ; i < currentFall.length  ; i++) {
        // 判断是否已经到“最底下”
        if(currentFall[i].y >= TETRIS_ROWS -  1) {
            canDown = false;
            break;
        }
        // 判断下一格是否“有方块”, 如果下一格有方块,不能向下掉
        if(tetris_status[currentFall[i].y +  1][currentFall[i].x] != NO_BLOCK) {
            canDown = false;
            break;
        }
    }
    // 如果能向下“掉”
    if(canDown) {
        // 将下移前的每个方块的背景色涂成白色
        for (var i = 0 ; i <  currentFall.length ; i++) {
            var cur = currentFall[i];
            // 设置填充颜色
            tetris_ctx.fillStyle = 'white';
            // 绘制矩形
            tetris_ctx.fillRect(cur.x *  CELL_SIZE + 1 
                , cur.y * CELL_SIZE + 1 ,  CELL_SIZE - 2 , CELL_SIZE - 2);
        }
        // 遍历每个方块, 控制每个方块的y坐标加1。
        // 也就是控制方块都下掉一格
        for (var i = 0 ; i <  currentFall.length ; i++) {
            var cur = currentFall[i];
            cur.y ++;
        }
        // 将下移后的每个方块的背景色涂成该方块的颜色值
        for (var i = 0 ; i <  currentFall.length ; i++){
            var cur = currentFall[i];
            // 设置填充颜色
            tetris_ctx.fillStyle =  colors[cur.color];
            // 绘制矩形
            tetris_ctx.fillRect(cur.x *  CELL_SIZE + 1 
                , cur.y * CELL_SIZE + 1 ,  CELL_SIZE - 2 , CELL_SIZE - 2);
        }
    } else{ // 不能向下掉
        // 遍历每个方块, 把每个方块的值记录到tetris_status数组中
        for (var i = 0 ; i <  currentFall.length ; i++){
            var cur = currentFall[i];
            // 如果有方块已经到最上面了,表明输了
            if(cur.y < 2) {
                // 清空Local Storage中的当前积分值、游戏状态、当前速度
                localStorage.removeItem("curScore");
                localStorage.removeItem("tetris_status");
                localStorage.removeItem("curSpeed");
                if(confirm("您已经输了!是否参数排名?")){
                    // 读取Local Storage里的maxScore记录
                    maxScore =  localStorage.getItem("maxScore");
                    maxScore = maxScore == null  ? 0 : maxScore ;
                    // 如果当前积分大于localStorage中记录的最高积分
                    if(curScore >= maxScore)  {
                        // 记录最高积分
                        localStorage.setItem("maxScore"  , curScore);
                    }
                }
                // 游戏结束
                isPlaying = false;
                // 清除计时器
                clearInterval(curTimer);
                return;
            }
            // 把每个方块当前所在位置赋为当前方块的颜色值
            tetris_status[cur.y][cur.x] =  cur.color;
        }
        // 判断是否有“可消除”的行
        lineFull();
        // 使用Local Storage记录俄罗斯方块的游戏状态
        localStorage.setItem("tetris_status"  , JSON.stringify(tetris_status));
        // 开始一组新的方块。
        initBlock();
    }
}

上面代码先定义了一个canDown变量,该变量用于标识方块组合是否能掉落。如果方块组合的任一方块己经到了最底下,或者方块组合的任一方块下方已有方块,将canDown变量值设为false。

当方块组合能掉落时,则先清除该方块组合所在位置的背景色,然后控制方块组合向下掉落,最后把掉落一格后的方块组合所在位置的背景涂上相应的颜色。

当方块组合不能向下掉落时,则执行如下几个任务:

上面代码采用了Local Storage记录当前俄罗斯方块的游戏状态,就是把记录游戏状态的二维数组转换为字符串后写入Local Storage,这样就可保证游戏状态不会丢失,下次打开浏览器时还可以接着上次的游戏状态继续。

initBlock()函数用来判断是否有可消除的行,如果发现某一行的方块已满,则程序处理积分增加,并且该行上面的所有行整体下移一行。下面是lineFul()函数的详细代码:

// 判断是否有一行已满
var lineFull =  function(){
    // 依次遍历每一行
    for (var i = 0; i < TETRIS_ROWS ; i++ ) {
        var flag = true;
        // 遍历当前行的每个单元格
        for (var j = 0 ; j < TETRIS_COLS ; j++  ) {
            if(tetris_status[i][j] == NO_BLOCK)  {
                flag = false;
                break;
            }
        }
        // 如果当前行已全部有方块了
        if(flag) {
            // 将当前积分增加100
            curScoreEle.innerHTML = curScore+=  100;
             // 记录当前积分
            localStorage.setItem("curScore"  , curScore);
            // 如果当前积分达到升级极限。
            if( curScore >= curSpeed *  curSpeed * 500) {
                curSpeedEle.innerHTML =  curSpeed += 1;
                // 使用Local Storage记录curSpeed。
                localStorage.setItem("curSpeed"  , curSpeed);
                clearInterval(curTimer);
                curTimer =  setInterval("moveDown();" ,   500 / curSpeed);
            }
            // 把当前行的所有方块下移一行。
            for (var k = i ; k > 0 ; k--){
                for (var l = 0; l <  TETRIS_COLS ; l++ ) {
                    tetris_status[k][l]  =tetris_status[k-1][l];
                }
            }
            // 消除方块后,重新绘制一遍方块
            drawBlock();      //②
        }
    }
}

当某行的所有位置都有方块之后,需要增加curScore游戏积分。除此之外,如果游戏积分已经达到了升级界限,将增加游戏速度,并清空原有的计时器,根据现有的游戏速度启用新的游戏计时器。

当游戏消除了指定某一行的所有方块之后,需要调用drawBlock()函数绘制tetris_status数组中的所有方块。drawBlock()函数代码如下。

// 绘制俄罗斯方块的状态
var drawBlock =  function(){
    for (var i = 0; i < TETRIS_ROWS ; i++ ){
        for  (var j = 0; j < TETRIS_COLS ; j++ ) {
            // 有方块的地方绘制颜色
            if(tetris_status[i][j] != NO_BLOCK)  {
                // 设置填充颜色
                tetris_ctx.fillStyle =  colors[tetris_status[i][j]];
                // 绘制矩形
                tetris_ctx.fillRect(j *  CELL_SIZE + 1 
                    , i * CELL_SIZE + 1,  CELL_SIZE - 2 , CELL_SIZE - 2);
            } else{                                // 没有方块的地方绘制白色
                // 设置填充颜色
                tetris_ctx.fillStyle = 'white';
                // 绘制矩形
                tetris_ctx.fillRect(j * CELL_SIZE + 1 
                    , i * CELL_SIZE + 1 ,  CELL_SIZE - 2 , CELL_SIZE - 2);
            }
        }
    }
}

上面的drawBlock()函数负责把俄罗斯方块的数据模型转换成可视化的方块图。

第2步,处理方块左移。

先给键盘事件绑定事件监听器,当用户按下不同按键时,调用不同的方法进行处理。下面是为按键事件绑定监听器的代码。

// 为窗口的按键事件绑定事件监听器
window.onkeydown =  function(evt) {
    switch(evt.keyCode){
        // 按下了“向下”箭头
        case 40:
            if(!isPlaying)
                return;
            moveDown();
            break;
        // 按下了“向左”箭头
        case 37:
            if(!isPlaying)
                return;
            moveLeft();
            break;
        // 按下了“向右”箭头
        case 39:
            if(!isPlaying)
                return;
            moveRight();
            break;
        // 按下了“向上”箭头
        case 38:
            if(!isPlaying)
                return;
            rotate();
            break;
    }
}

在上面代码中,当按下向左箭头时,如果还处于游戏中,则调用moveLeft()函数处理方块组合左移;相反按下向右箭头时,如果还处于游戏中,则调用moveRight()函数处理方块组合右移;当按下向上箭头时,如果还处于游戏中,则调用rotate()函数处理方块组合旋转。

方块组合左移实现比较简单,只要将方块组合里所有方块的x属性减1即可。但在左移之前先要判断是否可以左移。如果方块组合已经到了最左边,或者方块组合的左边已有方块,那么方块组合就环能左移。详细代码如下:

// 定义左移方块的函数
var moveLeft =  function(){
    // 定义能否左移的旗标
    var canLeft = true;
    for (var i = 0 ; i < currentFall.length  ; i++){
        // 如果已经到了最左边,不能左移
        if(currentFall[i].x <= 0){
            canLeft = false;
            break;
        }
        // 或左边的位置已有方块,不能左移
        if  (tetris_status[currentFall[i].y][currentFall[i].x - 1] != NO_BLOCK) {
            canLeft = false;
            break;
        }
    }
    // 如果能左移
    if(canLeft) {
        // 将左移前的每个方块的背景色涂成白色
        for (var i = 0 ; i <  currentFall.length ; i++){
            var cur = currentFall[i];
            // 设置填充颜色
            tetris_ctx.fillStyle = 'white';
            // 绘制矩形
            tetris_ctx.fillRect(cur.x *  CELL_SIZE +1 
                , cur.y * CELL_SIZE + 1 ,  CELL_SIZE - 2, CELL_SIZE - 2);
        }
        // 左移所有正在下掉的方块
        for (var i = 0 ; i <  currentFall.length ; i++){
            var cur = currentFall[i];
            cur.x --;
        }
        // 将左移后的每个方块的背景色涂成方块对应的颜色
        for (var i = 0 ; i <  currentFall.length ; i++) {
            var cur = currentFall[i];
            // 设置填充颜色
            tetris_ctx.fillStyle =  colors[cur.color];
            // 绘制矩形
            tetris_ctx.fillRect(cur.x *  CELL_SIZE + 1  
                , cur.y * CELL_SIZE + 1,  CELL_SIZE - 2 , CELL_SIZE - 2);
        }
    }
}

第3步,处理方块右移。

与方块组合左移的思路基本相似,都是先判断方块组合能否右移,如果可以右移,则将方块移动之前位置的背景色清空,将每个方块的x属性加1,再将移动后的方块所在位置的背景涂上相应的颇色即可。详细代码如下:

// 定义右移方块的函数
var moveRight =  function()
{
    // 定义能否右移的旗标
    var canRight = true;
    for (var i = 0 ; i < currentFall.length  ; i++) {
        // 如果已到了最右边,不能右移
        if(currentFall[i].x >= TETRIS_COLS -  1) {
            canRight = false;
            break;
        }
        // 如果右边的位置已有方块,不能右移
        if  (tetris_status[currentFall[i].y][currentFall[i].x + 1] != NO_BLOCK) {
            canRight = false;
            break;
        }
    }
    // 如果能右移
    if(canRight) {        
        // 将右移前的每个方块的背景色涂成白色
        for (var i = 0 ; i <  currentFall.length ; i++) {
            var cur = currentFall[i];
            // 设置填充颜色
            tetris_ctx.fillStyle = 'white';
            // 绘制矩形
            tetris_ctx.fillRect(cur.x *  CELL_SIZE + 1  
                , cur.y * CELL_SIZE + 1 ,  CELL_SIZE - 2 , CELL_SIZE - 2);
        }
        // 右移所有正在下掉的方块
        for (var i = 0 ; i <  currentFall.length ; i++){
            var cur = currentFall[i];
            cur.x ++;
        }
        // 将右移后的每个方块的背景色涂成各方块对应的颜色
        for (var i = 0 ; i <  currentFall.length ; i++) {
            var cur = currentFall[i];
            // 设置填充颜色
            tetris_ctx.fillStyle =  colors[cur.color];
            // 绘制矩形
            tetris_ctx.fillRect(cur.x *  CELL_SIZE + 1 
                , cur.y * CELL_SIZE + 1 ,  CELL_SIZE - 2, CELL_SIZE -2);
        }
    }
}

第4步,处理方块旋转。

处理方块旋转是最复杂的,因为需要动态地计算方块旋转后的坐标。当然,在对方块组进行旋转之前,同样需要先判断是否可以旋转方块,只有当方块可以旋转时才去进行旋转。下面是控制方块组合逆时针旋转的代码:

// 定义旋转方块的函数
var rotate = function()
{
    // 定义记录能否旋转的旗标
    var canRotate = true;
    for (var i = 0 ; i < currentFall.length  ; i++) {
        var preX = currentFall[i].x;
        var preY = currentFall[i].y;
        // 始终以第三个方块作为旋转的中心,
        // i == 2时,说明是旋转的中心
        if(i != 2){
            // 计算方块旋转后的x、y坐标
            var afterRotateX = currentFall[2].x  + preY - currentFall[2].y;
            var afterRotateY = currentFall[2].y  + currentFall[2].x - preX;
            // 如果旋转后所在位置已有方块,表明不能旋转
            if(tetris_status[afterRotateY][afterRotateX  + 1] != NO_BLOCK) {
                canRotate = false;
                break;
            }
            // 如果旋转后的坐标已经超出了最左边边界
            if(afterRotateX < 0 ||  tetris_status[afterRotateY - 1][afterRotateX] != NO_BLOCK) {
                moveRight();
                afterRotateX = currentFall[2].x  + preY - currentFall[2].y;
                afterRotateY = currentFall[2].y  + currentFall[2].x - preX;
                break;
            }
            if(afterRotateX < 0 ||  tetris_status[afterRotateY-1][afterRotateX] != NO_BLOCK) {
                moveRight();
                break;
            }
            // 如果旋转后的坐标已经超出了最右边边界
            if(afterRotateX >= TETRIS_COLS -  1 || 
                tetris_status[afterRotateY][afterRotateX+1]  != NO_BLOCK) {
                moveLeft();
                afterRotateX = currentFall[2].x  + preY - currentFall[2].y;
                afterRotateY = currentFall[2].y  + currentFall[2].x - preX;
                break;
            }
            if(afterRotateX >= TETRIS_COLS -  1 || 
                tetris_status[afterRotateY][afterRotateX+1]  != NO_BLOCK) {
                moveLeft();
                break;
            }
        }
    }
    // 如果能旋转
    if(canRotate) {
        // 将旋转移前的每个方块的背景色涂成白色
        for (var i = 0 ; i <  currentFall.length ; i++) {
            var cur = currentFall[i];
            // 设置填充颜色
            tetris_ctx.fillStyle = 'white';
            // 绘制矩形
            tetris_ctx.fillRect(cur.x *  CELL_SIZE + 1  
                , cur.y * CELL_SIZE + 1 ,  CELL_SIZE - 2, CELL_SIZE - 2);
        }
        for (var i = 0 ; i <  currentFall.length ; i++) {
            var preX = currentFall[i].x;
            var preY = currentFall[i].y;
            // 始终以第三个方块作为旋转的中心,
            // i == 2时,说明是旋转的中心
            if(i != 2) {
                currentFall[i].x =  currentFall[2].x + 
                    preY - currentFall[2].y;
                currentFall[i].y =  currentFall[2].y + 
                    currentFall[2].x - preX;
            }
        }
        // 将旋转后的每个方块的背景色涂成各方块对应的颜色
        for (var i = 0 ; i <  currentFall.length ; i++){
            var cur = currentFall[i];
            // 设置填充颜色
            tetris_ctx.fillStyle =  colors[cur.color];
            // 绘制矩形
            tetris_ctx.fillRect(cur.x *  CELL_SIZE + 1 
                , cur.y * CELL_SIZE + 1 ,  CELL_SIZE - 2, CELL_SIZE - 2);
        }
    }
}

第5步,初始化游戏状态。

在游戏过程中,使用了Local Storage保存游戏状态,包括游戏的当前积分、游戏速度、已有方块的状态等。为了正常使用Local Storage所记录的游戏状态,可以在页面初始化后通过Local Storage读取这些数据,并把这些数据显示出来,具体代码如下:

// 当页面加载完成时,执行该函数里的代码。
window.onload =  function(){
    // 创建canvas组件
    createCanvas(TETRIS_ROWS , TETRIS_COLS ,  CELL_SIZE , CELL_SIZE);
    document.body.appendChild(tetris_canvas);
    curScoreEle =  document.getElementById("curScoreEle");
    curSpeedEle =  document.getElementById("curSpeedEle");
    maxScoreEle = document.getElementById("maxScoreEle");
    // 读取Local Storage里的tetris_status记录
    var tmpStatus =  localStorage.getItem("tetris_status");
    tetris_status = tmpStatus == null ?  tetris_status : JSON.parse(tmpStatus);
    // 把方块状态绘制出来
    drawBlock();
    // 读取Local Storage里的curScore记录
    curScore =  localStorage.getItem("curScore");
    curScore = curScore == null ? 0 :  parseInt(curScore);
    curScoreEle.innerHTML = curScore;
    // 读取Local Storage里的maxScore记录
    maxScore =  localStorage.getItem("maxScore");
    maxScore = maxScore == null ? 0 :  parseInt(maxScore);
    maxScoreEle.innerHTML = maxScore;
    // 读取Local Storage里的curSpeed记录
    curSpeed =  localStorage.getItem("curSpeed");
    curSpeed = curSpeed == null ? 1 :  parseInt(curSpeed);
    curSpeedEle.innerHTML = curSpeed;
    // 初始化正在下掉的方块
    initBlock();
    // 控制每隔固定时间执行一次向下”掉“
    curTimer =  setInterval("moveDown();" ,   500 / curSpeed);
}

上面代码在游戏开始时需要完成如下事情。

小结

在学习本例时,读者需要熟悉本游戏的玩法,然后能够理解界面设计的思路和实现途径。同时应该熟练掌握使用JavaScript操控canvas和Local Storage技术的能力。