App下載

簡單的俄羅斯方塊小游戲案例分享!

微光傾城 2021-08-10 15:37:18 瀏覽數(shù) (2063)
反饋

簡在之前完成有關于canvas這方面的相關內容整理和項目的時候,我就發(fā)現(xiàn)原來canvas可以實現(xiàn)的內容還有很多,那么今天我們就來說說有關于:“簡單的俄羅斯方塊小游戲案例分享!”這方面的內容的分享!

導言

在一個風和日麗的一天,看完了瘋狂HTML 5+CSS 3+JavaScript講義,跟著做了書里最后一章的俄羅斯方塊小游戲,并做了一些改進,作為自己前端學習的第一站。

游戲效果:

制作思路

因為書里的俄羅斯方塊比較普通,太常規(guī)了,不是很好看,所以我在網上找了上面那張圖片,打算照著它來做。(請無視成品和原圖的差距)

然后便是游戲界面和常規(guī)的俄羅斯方塊游戲邏輯。

接著便是游戲結束界面了。

原本想做個彈出層,但覺得找圖片有點麻煩,所以就在網上找了文字特效,套用了一下。

代碼實現(xiàn):

首先是html文件和css文件,主要涉及了布局方面。作為新手,在上面真的是翻來覆去的踩坑。o(╥﹏╥)o

index.html:

<!DOCTYPE html>
<html>
<head>
    <title>俄羅斯方塊</title>
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
    <link rel=stylesheet type="text/css" href="teris.css">
    <style type="text/css">
        /*導入外部的字體文件*/
        @font-face{
            font-family:tmb;/*為字體命名為tmb*/
            src:url("DS-DIGIB.TTF") format("TrueType");/*format為字體文件格式,TrueType為ttf*/
        }
        div>span{
            font-family:tmb;
            font-size:18pt;
            color:green;
        }
    </style>
</head>

<body>
    <div id="container" class="bg">
        <!--ui-->
        <div class="ui_bg">
            <div style="float:left;margin-right:4px;">
                速度:<span id="cur_speed">1</span>
            </div>
            <div style="float:left;">
                當前分數(shù):<span id="cur_points">0</span>
            </div>
            <div style="float:right;">
                最高分數(shù):<span id="max_points">0</span>
            </div>
        </div>
        <canvas id="text" width="500" height="100" style="position:absolute;"></canvas>
        <canvas id="stage" width="500" height="100" style="position:absolute;"></canvas>
    </div>
    <script src='EasePack.min.js'></script>
    <script src='TweenLite.min.js'></script>
    <script src='easeljs-0.7.1.min.js'></script>
    <script src='requestAnimationFrame.js'></script>
    <script type="text/javascript" src="jquery-3.4.1.min.js"></script>
    <script type="text/javascript" src="teris.js"></script>
</body>
</html>

teris.css

*{
    margin:0;
    padding:0;
}
html, body{
    width:100%;
    height:100%;
}

.bg{
    font-size:13pt;
    background-color:rgb(239, 239, 227);
    /*好看的漸變色*/
    background-image:radial-gradient(rgb(239, 239, 227), rgb(230, 220, 212));
    /*陰影*/
    box-shadow:#cdc8c1 -1px -1px 7px 0px;
    padding-bottom:4px;
}

.ui_bg{
    border-bottom:1px #a69e9ea3 solid;
    padding-bottom:2px;
    overflow:hidden;/*沒有這句的話因為子div都設置了float,所以是浮在網頁上的,所以父div就沒有高度,這句清除了浮動,讓父div有了子div的高度*/
}

然后是重頭戲,teris.js

游戲變量:

//游戲設定
var TETRIS_ROWS = 20;
var TETRIS_COLS = 14;
var CELL_SIZE = 24;
var NO_BLOCK=0;
var HAVE_BLOCK=1;
// 定義幾種可能出現(xiàn)的方塊組合
var blockArr = [
    // Z
    [
        {x: TETRIS_COLS / 2 - 1 , y:0},
        {x: TETRIS_COLS / 2 , y:0},
        {x: TETRIS_COLS / 2 , y:1},
        {x: TETRIS_COLS / 2 + 1 , y:1}
    ],
    // 反Z
    [
        {x: TETRIS_COLS / 2 + 1 , y:0},
        {x: TETRIS_COLS / 2 , y:0},
        {x: TETRIS_COLS / 2 , y:1},
        {x: TETRIS_COLS / 2 - 1 , y:1}
    ],
    // 田
    [
        {x: TETRIS_COLS / 2 - 1 , y:0},
        {x: TETRIS_COLS / 2 , y:0},
        {x: TETRIS_COLS / 2 - 1 , y:1},
        {x: TETRIS_COLS / 2 , y:1}
    ],
    // L
    [
        {x: TETRIS_COLS / 2 - 1 , y:0},
        {x: TETRIS_COLS / 2 - 1, y:1},
        {x: TETRIS_COLS / 2 - 1 , y:2},
        {x: TETRIS_COLS / 2 , y:2}
    ],
    // J
    [
        {x: TETRIS_COLS / 2  , y:0},
        {x: TETRIS_COLS / 2 , y:1},
        {x: TETRIS_COLS / 2  , y:2},
        {x: TETRIS_COLS / 2 - 1, y:2}
    ],
    // □□□□
    [
        {x: TETRIS_COLS / 2 , y:0},
        {x: TETRIS_COLS / 2 , y:1},
        {x: TETRIS_COLS / 2 , y:2},
        {x: TETRIS_COLS / 2 , y:3}
    ],
    // ┴
    [
        {x: TETRIS_COLS / 2 , y:0},
        {x: TETRIS_COLS / 2 - 1 , y:1},
        {x: TETRIS_COLS / 2 , y:1},
        {x: TETRIS_COLS / 2 + 1, y:1}
    ]
];

// 記錄當前積分
var curScore=0;
// 記錄曾經的最高積分
var maxScore=1;
var curSpeed=1;
//ui元素
var curSpeedEle=document.getElementById("cur_speed");
var curScoreEle=document.getElementById("cur_points");
var maxScoreEle=document.getElementById("max_points");

var timer;//方塊下落控制

var myCanvas;
var canvasCtx;
var tetris_status;//地圖數(shù)據
var currentFall;//當前下落的block

游戲界面的完善

//create canvas
function createCanvas(){
    myCanvas=document.createElement("canvas");
    myCanvas.width=TETRIS_COLS*CELL_SIZE;
    myCanvas.height=TETRIS_ROWS*CELL_SIZE;
    //繪制背景
    canvasCtx=myCanvas.getContext("2d");
    canvasCtx.beginPath();
    //TETRIS_COS
    for(let i=1; i<TETRIS_COLS; i++){
        canvasCtx.moveTo(i*CELL_SIZE, 0);
        canvasCtx.lineTo(i*CELL_SIZE, myCanvas.height);
    }
    for(let i=1; i<TETRIS_ROWS; i++){
        canvasCtx.moveTo(0, i*CELL_SIZE);
        canvasCtx.lineTo(myCanvas.width, i*CELL_SIZE);
    }
    canvasCtx.closePath();
    canvasCtx.strokeStyle="#b4a79d";
    canvasCtx.lineWidth=0.6;
    canvasCtx.stroke();
    //第一行,最后一行,第一列,最后一列粗一點。
    canvasCtx.beginPath();
    canvasCtx.moveTo(0, 0);
    canvasCtx.lineTo(myCanvas.width, 0);
    canvasCtx.moveTo(0, myCanvas.height);
    canvasCtx.lineTo(myCanvas.width, myCanvas.height);
    canvasCtx.moveTo(0, 0);
    canvasCtx.lineTo(0, myCanvas.height);
    canvasCtx.moveTo(myCanvas.width, 0);
    canvasCtx.lineTo(myCanvas.width, myCanvas.height);
    canvasCtx.closePath();
    canvasCtx.strokeStyle="#b4a79d";
    canvasCtx.lineWidth=4;
    canvasCtx.stroke();
    //設置繪制block時的style
    canvasCtx.fillStyle="#201a14";
}

draw canvas
function changeWidthAndHeight(w, h){
    //通過jquery設置css
    h+=$("ui_bg").css("height")+$("ui_bg").css("margin-rop")+$("ui_bg").css("margin-bottom")+$("ui_bg").css("padding-top")+$("ui_bg").css("padding-bottom");
    $(".bg").css({
        "width":w,
        "height":h,
        "top":0, "bottom":0, "right":0, "left":0,
        "margin":"auto"
    });
}

change width and height
//draw blocks
function drawBlocks(){
    //清空地圖
    for(let i=0; i<TETRIS_ROWS;i++){
        for(let j=0;j<TETRIS_COLS;j++)
            canvasCtx.clearRect(j*CELL_SIZE+1, i*CELL_SIZE+1, CELL_SIZE-2, CELL_SIZE-2);
    }
    //繪制地圖
    for(let i=0; i<TETRIS_ROWS;i++){
        for(let j=0;j<TETRIS_COLS;j++){
            if(tetris_status[i][j]!=NO_BLOCK)
                canvasCtx.fillRect(j*CELL_SIZE+1, i*CELL_SIZE+1, CELL_SIZE-2, CELL_SIZE-2);//中間留點縫隙
        }
    }
    //繪制currentFall
    for(let i=0;i<currentFall.length;i++)
        canvasCtx.fillRect(currentFall[i].x*CELL_SIZE+1, currentFall[i].y*CELL_SIZE+1, CELL_SIZE-2,CELL_SIZE-2);
}

draw block

游戲邏輯

function rotate(){
    // 定義記錄能否旋轉的旗標
    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 preX = currentFall[i].x;
            var preY = currentFall[i].y;
            if(i != 2){
                currentFall[i].x = currentFall[2].x + 
                    preY - currentFall[2].y;
                currentFall[i].y = currentFall[2].y + 
                    currentFall[2].x - preX;
            }
        }
        localStorage.setItem("currentFall", JSON.stringify(currentFall));
    }
}

旋轉
//按下 下 或 interval到了
function next(){
    if(moveDown()){
        //記錄block
        for(let i=0;i<currentFall.length;i++)
            tetris_status[currentFall[i].y][currentFall[i].x]=HAVE_BLOCK;
        //判斷有沒有滿行的
        for(let j=0;j<currentFall.length;j++){
            for(let i=0;i<TETRIS_COLS; i++){
                if(tetris_status[currentFall[j].y][i]==NO_BLOCK)
                    break;
                //最后一行滿了
                if(i==TETRIS_COLS-1){
                    //消除最后一行
                    for(let i=currentFall[j].y; i>0;i--){
                        for(let j=0;j<TETRIS_COLS;j++)
                            tetris_status[i][j]=tetris_status[i-1][j];
                    }
                    //分數(shù)增加
                    curScore+=5;
                    localStorage.setItem("curScore", curScore);
                    if(curScore>maxScore){
                        //超越最高分
                        maxScore=curScore;
                        localStorage.setItem("maxScore", maxScore);
                    }
                    //加速
                    curSpeed+=0.1;
                    localStorage.setItem("curSpeed", curSpeed);
                    //ui輸出
                    curScoreEle.innerHTML=""+curScore;
                    maxScoreEle.innerHTML=""+maxScore;
                    curSpeedEle.innerHTML=curSpeed.toFixed(1);//保留兩位小數(shù)
                    clearInterval(timer);
                    timer=setInterval(function(){
                        next();
                    }, 500/curSpeed);
                }
            }
        }
        //判斷是否觸頂
        for(let i=0;i<currentFall.length;i++){
            if(currentFall[i].y==0){
                gameEnd();
                return;
            }
        }
        localStorage.setItem("tetris_status", JSON.stringify(tetris_status));
        //新的block
        createBlock();
        localStorage.setItem("currentFall", JSON.stringify(currentFall));
    }
    drawBlocks();
}

//右移
function moveRight(){
    for(let i=0;i<currentFall.length;i++){
        if(currentFall[i].x+1>=TETRIS_ROWS || tetris_status[currentFall[i].y][currentFall[i].x+1]!=NO_BLOCK)
            return;
    }
    for(let i=0;i<currentFall.length;i++)
        currentFall[i].x++;
    localStorage.setItem("currentFall", JSON.stringify(currentFall));
    return;
}
//左移
function moveLeft(){
    for(let i=0;i<currentFall.length;i++){
        if(currentFall[i].x-1<0 || tetris_status[currentFall[i].y][currentFall[i].x-1]!=NO_BLOCK)
            return;
    }
    for(let i=0;i<currentFall.length;i++)
        currentFall[i].x--;
    localStorage.setItem("currentFall", JSON.stringify(currentFall));
    return;
}
//judge can move down and if arrive at end return 1, if touch other blocks return 2, else, return 0
function moveDown(){
    for(let i=0;i<currentFall.length;i++){
        if(currentFall[i].y>=TETRIS_ROWS-1 || tetris_status[currentFall[i].y+1][currentFall[i].x]!=NO_BLOCK)
            return true;
    }

    for(let i=0;i<currentFall.length;i++)
        currentFall[i].y+=1;
    return false;
}

上下左右移動
function gameKeyEvent(evt){
    switch(evt.keyCode){
        //向下
        case 40://↓
        case 83://S
            next();
            drawBlocks();
            break;
        //向左
        case 37://←
        case 65://A
            moveLeft();
            drawBlocks();
            break;
        //向右
        case 39://→
        case 68://D
            moveRight();
            drawBlocks();
            break;
        //旋轉
        case 38://↑
        case 87://W
            rotate();
            drawBlocks();
            break;
    }
}

keydown事件監(jiān)聽

keydown事件監(jiān)聽

其他的詳細情況可以看源代碼,我就不整理了。

接下來我們看游戲結束時的特效。因為我也不是很懂,所以在這里整理的會比較詳細。當做學習。

//game end
function gameEnd(){
    clearInterval(timer);
    //鍵盤輸入監(jiān)聽結束
    window.onkeydown=function(){
        //按任意鍵重新開始游戲
        window.onkeydown=gameKeyEvent;
        //初始化游戲數(shù)據
        initData();
        createBlock();
        localStorage.setItem("currentFall", JSON.stringify(currentFall));
        localStorage.setItem("tetris_status", JSON.stringify(tetris_status));
        localStorage.setItem("curScore", curScore);
        localStorage.setItem("curSpeed", curSpeed);
        //繪制
        curScoreEle.innerHTML=""+curScore;
        curSpeedEle.innerHTML=curSpeed.toFixed(1);//保留兩位小數(shù)
        drawBlocks();
        timer=setInterval(function(){
            next();
        }, 500/curSpeed);
        //清除特效
        this.stage.removeAllChildren();
        this.textStage.removeAllChildren();
    };
    //特效,游戲結束
    setTimeout(function(){
        initAnim();
        //擦除黑色方塊
        for(let i=0; i<TETRIS_ROWS;i++){
            for(let j=0;j<TETRIS_COLS;j++)
                canvasCtx.clearRect(j*CELL_SIZE+1, i*CELL_SIZE+1, CELL_SIZE-2, CELL_SIZE-2);
        }
    }, 200);
    //推遲顯示Failed
    setTimeout(function(){
        if(textFormed) {
            explode();
            setTimeout(function() {
                createText("FAILED");
            }, 810);
        } else {
            createText("FAILED");
        }
    }, 800);
}

上面代碼里的localstorage是html5的本地數(shù)據存儲。因為不是運用很難,所以具體看代碼。

整個特效是運用了createjs插件。要引入幾個文件。

easeljs-0.7.1.min.js,EasePacj.min.js,requestAnimationFrame.js和TweenLite.min.js 游戲重新開始就要清除特效。我看api里我第一眼望過去最明顯的就是removeAllChildren(),所以就選了這個。其他的改進日后再說。

 //清除特效
        this.stage.removeAllChildren();
        this.textStage.removeAllChildren();
function initAnim() {
    initStages();
    initText();
    initCircles();
    //在stage下方添加文字——按任意鍵重新開始游戲.
    tmp = new createjs.Text("t", "12px 'Source Sans Pro'", "#54555C");
    tmp.textAlign = 'center';
    tmp.x = 180;
    tmp.y=350;
    tmp.text = "按任意鍵重新開始游戲";
    stage.addChild(tmp);
    animate();
}

initAnim

上面初始化了一個stage,用于存放特效,一個textstage,用于形成“FAILED”的像素圖片。還有一個按任意鍵重新游戲的提示。同時開始每隔一段時間就刷新stage。

根據block的位置來初始化小圓點。

function initCircles() {
    circles = [];
    var p=[];
    var count=0;
    for(let i=0; i<TETRIS_ROWS;i++)
        for(let j=0;j<TETRIS_COLS;j++)
            if(tetris_status[i][j]!=NO_BLOCK)
                p.push({'x':j*CELL_SIZE+2, 'y':i*CELL_SIZE+2, 'w':CELL_SIZE-3, 'h':CELL_SIZE-4});
    for(var i=0; i<250; i++) {
        var circle = new createjs.Shape();
        var r = 7;
        //x和y范圍限定在黑色block內
        var x = p[count]['x']+p[count]['w']*Math.random();
        var y = p[count]['y']+p[count]['h']*Math.random();
        count++;
        if(count>=p.length)
            count=0;
        var color = colors[Math.floor(i%colors.length)];
        var alpha = 0.2 + Math.random()*0.5;
        circle.alpha = alpha;
        circle.radius = r;
        circle.graphics.beginFill(color).drawCircle(0, 0, r);
        circle.x = x;
        circle.y = y;
        circles.push(circle);
        stage.addChild(circle);
        circle.movement = 'float';
        tweenCircle(circle);
    }
}

initCircles

然后再講顯示特效Failed的createText()。先將FAILED的text顯示在textstage里,然后ctx.getImageData.data獲取像素數(shù)據,并以此來為每個小圓點定義位置。

function createText(t) {
    curText=t;
    var fontSize = 500/(t.length);
    if (fontSize > 80) fontSize = 80;
    text.text = t;
    text.font = "900 "+fontSize+"px 'Source Sans Pro'";
    text.textAlign = 'center';
    text.x = TETRIS_COLS*CELL_SIZE/2;
    text.y = 0;
    textStage.addChild(text);
    textStage.update();

    var ctx = document.getElementById('text').getContext('2d');
    var pix = ctx.getImageData(0,0,600,200).data;
    textPixels = [];
    for (var i = pix.length; i >= 0; i -= 4) {
        if (pix[i] != 0) {
            var x = (i / 4) % 600;
            var y = Math.floor(Math.floor(i/600)/4);
            if((x && x%8 == 0) && (y && y%8 == 0)) textPixels.push({x: x, y: y});
        }
    }

    formText();
    textStage.clear();//清楚text的顯示
}

CreateText

跟著代碼的節(jié)奏走,我們現(xiàn)在來到了formtext.

function formText() {
    for(var i= 0, l=textPixels.length; i<l; i++) {
        circles[i].originX = offsetX + textPixels[i].x;
        circles[i].originY = offsetY + textPixels[i].y;
        tweenCircle(circles[i], 'in');
    }
    textFormed = true;
    if(textPixels.length < circles.length) {
        for(var j = textPixels.length; j<circles.length; j++) {
            circles[j].tween = TweenLite.to(circles[j], 0.4, {alpha: 0.1});
        }
    }
}

formtext

explode()就是講已組成字的小圓點給重新遣散。

動畫實現(xiàn)是使用了tweenlite.

function tweenCircle(c, dir) {
    if(c.tween) c.tween.kill();
    if(dir == 'in') {
        /*TweenLite.to 改變c實例的x坐標,y坐標,使用easeInOut彈性函數(shù),透明度提到1,改變大小,radius,總用時0.4s*/
        c.tween = TweenLite.to(c, 0.4, {x: c.originX, y: c.originY, ease:Quad.easeInOut, alpha: 1, radius: 5, scaleX: 0.4, scaleY: 0.4, onComplete: function() {
            c.movement = 'jiggle';/*輕搖*/
            tweenCircle(c);
        }});
    } else if(dir == 'out') {
        c.tween = TweenLite.to(c, 0.8, {x: window.innerWidth*Math.random(), y: window.innerHeight*Math.random(), ease:Quad.easeInOut, alpha: 0.2 + Math.random()*0.5, scaleX: 1, scaleY: 1, onComplete: function() {
            c.movement = 'float';
            tweenCircle(c);
        }});
    } else {
        if(c.movement == 'float') {
            c.tween = TweenLite.to(c, 5 + Math.random()*3.5, {x: c.x + -100+Math.random()*200, y: c.y + -100+Math.random()*200, ease:Quad.easeInOut, alpha: 0.2 + Math.random()*0.5,
                onComplete: function() {
                    tweenCircle(c);
                }});
        } else {
            c.tween = TweenLite.to(c, 0.05, {x: c.originX + Math.random()*3, y: c.originY + Math.random()*3, ease:Quad.easeInOut,
                onComplete: function() {
                    tweenCircle(c);
                }});
        }
    }
}

TweenLite.to函數(shù)第一個參數(shù),要做動畫的實例,第二個參數(shù),事件,第三個參數(shù),動畫改變參數(shù)。

Quad.easeInOut()意思是在動畫開始和結束時緩動。onComplete動畫完成時調用的函數(shù)。易得,在我們的應用中,我們將開始下一次動畫。

個人感言

其實剛開始沒想做這么復雜,所以文件排的比較隨意,然后就導致了后期項目完成時那副雜亂無章的樣子。^_^,以后改。等我等看懂動畫效果時在說,現(xiàn)在用的有點半懵半懂。

這篇博客寫得有點亂。新手之作,就先這樣吧。同上,以后改。因為不知道這個項目會不會拿來直接當我們計算機職業(yè)實踐的作業(yè)。要是的話,我就徹改,連同博客。

以下是源代碼地址。

還在審核。明天再說。相信我,我在小番茄里做了記錄。

總結

以上所述是小編給大家介紹的“簡單的俄羅斯方塊小游戲案例分享!”這方面的相關內容,希望對大家有所幫助,有喜歡的前端的小伙伴們都可以在W3Cschool網站中學習和了解! 

0 人點贊