js 实现俄罗斯方块

js 实现俄罗斯方块,第1张

实现思路

MVC思路。


三个端分工合作。


可以仔细看。


整个俄罗斯方块为一个二维数组。


C控制M, M控制二维数组,V只要把二维数组渲染出来就行 。


我下面的实现思路可以应用到任何一个这种二维数组感觉的小游戏。


扩展性还算不错。


视图层

        html部分

        js部分

         这里就是画背景和画全部方框的方法。


// 画背景
function drawBG(){
    CTX.lineWidth = 1;
    CTX.fillStyle = "#000";
    CTX.fillRect(0,0, CANVAS.width, CANVAS.height);
    CTX.strokeStyle = "#eee";

    // 画横线
    for(let i = BLOCK_SIZE; i < CANVAS.height; i +=BLOCK_SIZE){
        CTX.beginPath(); //新建一条path
        CTX.moveTo(0, i); 
        CTX.lineTo(CANVAS.width, i); 
        CTX.closePath();
        CTX.stroke(); //绘制路径。


} // 画竖线 for(let i = BLOCK_SIZE; i < CANVAS.width; i += BLOCK_SIZE){ CTX.beginPath(); //新建一条path CTX.moveTo(i, 0); CTX.lineTo(i, CANVAS.height); CTX.closePath(); CTX.stroke(); //绘制路径。


} } // 绘制全部格子 function drawAllBlock(){ for(let i = 0; i < Y_MAX; i++){ for(let j = 0; j < X_MAX; j ++){ CTX.lineWidth = 8; let cell = BLOCK_LIST[i][j]; if(!cell){ continue } CTX.strokeStyle = cell.color; CTX.strokeRect( cell.x * BLOCK_SIZE + 4, cell.y*BLOCK_SIZE + 4, BLOCK_SIZE - 8, BLOCK_SIZE-8); } } }

模型层

        Cell。


我以一个单元格为一个对象。


这个格子有自己的颜色,坐标。


当然还可以扩展,例如图片之类的。


在这个游戏里面,格子还会移动。


所以还包含移动的方法。


// 单元类(就是一个一个的格子)
class Cell{
    // 位置
    x = 0;
    y = 0;

    //颜色
    color = "#000";

    // 单元格的id,区别不同的单元格(谁叫 js 没罚判断对象是否同一个)
    id = "";

    /**
     * 构造器
     * @param 
     * {Number} x: x轴位置
     * {Number} y: y轴位置
     */
    constructor({x, y, color}) {
        this.x = x || 0;
        this.y = y || 0;
        this.color = color || "#000";
        this.id = `${new Date().getTime()}${x}${y}`;//生成唯一 id 瞎写逻辑,不重复就行
        if(this.y >= 0){
            BLOCK_LIST[this.y][this.x] = this;
        }
        
    }
    
    /**
     * 移动方法
     * 
     * @params {String} type: 类型,LEFT、LEFT、DOWN 对应的移动的方向
     * 
     * @returns 返回移动结果坐标
     */
    getMovePositionByType(type){
        let position = {
            x: this.x,
            y: this.y
        }
        switch(type){
            case "LEFT":
                position.x -= 1;
                break;
            case "RIGHT":
                position.x += 1;
                break;
            case "DOWN":
                position.y += 1
                break;
        }

        return position;
    } 

    /**
     * 设定位置
     * @params { int } x: x坐标
     * @params { int } y: Y坐标
     */
    setCellPoisition({x, y}){
        // 记住上一次位置
        let beforX = this.x;
        let beforY = this.y;
    
        // 如果上一个格子没有被征用就回收
        if(beforY >= 0 && BLOCK_LIST[beforY][beforX].id == this.id){
            BLOCK_LIST[beforY][beforX] = null;
        }

        if( y >= 0 ){
            BLOCK_LIST[y][x] = this;
        }
        
        // 赋值给 x y
        this.x = x;
        this.y = y;
    }
}

Block类

        这个就是这个游戏的主角,所有形状的格子都可以理解为多个 Cell 的集合。


所以这个类强调的是集合,并不是块本身。


所以这个类没有坐标,用一个数组表示自己。


这个块主要拥有下落,和旋转方法。


        碰撞检测

        整体设计清楚后,这个反而很简单,只要判断当前坐标在二维数组里面是否有对象就行了。


        

/**
 * 移动碰撞检测方法
 * 
 * @arg type: 移动类型
 * @returns true 撞了
 */
isCollision(type){
    // 碰撞检测
    let result = false;
    for(let i = 0, len = this.body.length; i < len; i ++ ){
        let position = {};
        // 移动类型
        if( type != "ROTATION"){
            // 拿到移动的下一个坐标
            position = this.body[i].getMovePositionByType(type);
        }else{
            // 旋转类型
            position = this.getOffsetPosition(i);
        }
        
        // 边界判断
        // 看看x有没有撞
        if(position.x < 0 || position.x >= X_MAX){
            result = true
            break;
        }
        // 看看y有么有撞
        if( position.y >= Y_MAX){
            result = true
            break;
        }
        // 如果y为负数,不用验证,直接过
        if(position.y < 0){
            break;
        }
        // 看看有没有碰到格子
        // 排除自身碰撞 !this.body.some( e => e.id == targetBlock.id) 只要有任意一个是自己body里面的cell就不算碰撞
        let targetBlock = BLOCK_LIST[position.y][position.x];
        if(targetBlock && !this.body.some( e => e.id == targetBlock.id)){
            result = true
            break;
        }
    }

    // 如果是下落的移动方式且也碰到了东西,我们认为这个块已经结束使命了。


if( type == "DOWN" && result){ this.stopActive = true; } return result }

        旋转    这个方法比较有意思。


旋转的实现我自己是这么理解的。


他其实也是一个新的块。


只是他们之间的位置一样。


所以这个类在创建之初,就需要输入对应旋转的各个状态。


以及旋转中心。


//旋转方法
rotationBlock(){
    // 没有旋转中心的不转
    if(this.centerPoint == -1){
        return
    }

    // 撞了就打断,不转了
    if(this.isCollision("ROTATION")){
        return
    }
    
    // 偏移值
    this.body.forEach( (e, index) => {
        e.setCellPoisition(this.getOffsetPosition(index))
    })
    this.rotate += 1;
    if( this.rotate == this.rotationList.length){
        this.rotate = 0;
    }
}

// 偏移值计算方法
getOffsetPosition(cellIndex){
    let index = this.rotate + 1;
    if(this.rotate ==  this.rotationList.length -1){
        index = 0;
    }
    let targetPoisition = this.rotationList[index][cellIndex];
    let centerPoisition = this.rotationList[index][this.centerPoint];
    return {
        x: this.body[this.centerPoint].x + (targetPoisition.x - centerPoisition.x),
        y: this.body[this.centerPoint].y + (targetPoisition.y  - centerPoisition.y)
    };
}

        下落    方法就是定时器,每隔个几秒执行一次移动,碰撞就停止

// 下落方法
down(){
    this.downTimer = setInterval(()=>{
        // 是否已经停止活动
        if(this.stopActive){
            clearInterval(this.downTimer);
            return
        }
        // 游戏运行状态才可以移动 
        if(GAME_STATE == 0){
            this.move("DOWN");
        }
        
    }, this.downSpeed)
}



 基础结束后,我们可以开始扩展了。


比如我们添加一个L字格子

L字格子

我这里是用继承类来设置,其实可以不用。


因为到 Block 类哪里,已经代表全部了,可以不用在扩展类了

// L形格子
class LBlock extends Block {
    
    /**
     * 构造器
     */
    constructor({x, y, color}) {
        super()
        this.color = color || "#00FFFF";
        this.centerPoint = 2; // 第几个块是旋转中心
        this.rotate = 0; // 当前旋转角度
        this.rotationList = [
            [{x: 0, y: 0}, {x: 0, y: 1}, {x: 1, y: 1}, {x: 2, y: 1}], // 原来形状
            [{x: 2, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}, {x: 1, y: 2}], // 旋转 90
            [{x: 2, y: 2}, {x: 2, y: 1}, {x: 1, y: 1}, {x: 0, y: 1}], // 旋转 180
            [{x: 0, y: 2}, {x: 1, y: 2}, {x: 1, y: 1}, {x: 1, y: 0}]  // 旋转 270
        ]
        this.initBody(x || 4, y || -1); // 初始化位置(就是你希望他一开始在哪里出现)
    }
}

运行

 终于到最后一步了采用 requestAnimationFrame 来刷新故方法如下

// 运行
function run(){
    
    // 使用 AnimationFrame 刷新
    window.requestAnimationFrame(run);
    // 游戏非暂停运行阶段都停止渲染
    if(GAME_STATE != 0){
        return
    }
    // 绘制背景
    drawBG();

    // 绘制二维表格
    drawAllBlock();

    // 如果活动格子停止活动了,就开始算分和创建新的格子
    if(ACTIVE_BLOCK.stopActive){
        // 算分
        computeScore();
        // 更新最顶的格子
        setTopCellPoisition()
        // 是否游戏结束
        gameOver();
        // 创建格子
        ACTIVE_BLOCK = getRandomBlock();
    }
    
}

// 启动方法
function start(){
    // 设置为运行状态
    GAME_STATE = 0;
    OVER_DOM.style.display = "none";
    // 初始化二维数组
    initBodyList();
    // 创建正在活动的格子
    ACTIVE_BLOCK = getRandomBlock();
    // 启动渲染
    window.requestAnimationFrame(run);
}

// 启动
start()

这里游戏就跑起来了。


你可能会疑问,菜鸟教程有一篇文章这么提到

原文链接

其实仔细观察代码就可以发现,我是把画背景的方法在画全部二维数组格子前执行。


背景直接覆盖掉前面的所有东西,就实现了动画的效果。


下面是全部源代码




    
    
    
    俄罗斯方块
    


    按 W 旋转, ASD移动, 空格暂停
    分数:
    

游戏结束 游戏暂停

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/563638.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-02
下一篇 2022-04-02

发表评论

登录后才能评论

评论列表(0条)

保存