CocosCreator Typescript制作俄羅斯方塊遊戲

1.引言

最近開始學cocos,學完Typescript語法之後,跑去看cocos的官方文檔,搗鼓瞭幾天,寫瞭一個非常簡單的貪吃蛇,甚至連像樣的碰撞檢測也沒有,自覺無趣,就荒廢瞭一段時間。這幾個星期我又重拾瞭cocos,就有瞭實現俄羅斯方塊的想法。一開始我想著上網找找資料,發現關於cocos開發俄羅斯方塊的文章幾乎寥寥無幾(也有可能是我找的方法不對),更頭痛的是,我找到的僅有幾個分享文章的代碼註釋比較少,也可能是我的理解能力不行,後來花瞭幾天也沒能完全看懂。所以我打算自己嘗試寫寫看,過瞭兩個星期,總算是完成瞭。

在文章的後面,我會附上整個cocos的項目文件供大傢參考,代碼寫得不好,請大傢多多指教。

2.需要解決的幾個關鍵問題

1.遊戲區的方塊我們怎麼存儲起來

 因為俄羅斯方塊是像素遊戲,我們可以把每一個方塊看成一個像素,那麼整個遊戲區就是一塊像素集合,結合到cocos內,我們把每一個方塊定義成cc.Node型,那麼我們的遊戲區就可以使用一個cc.Node型的二維數組將方塊保存起來,方便進行旋轉,位移,堆疊,刪除等關鍵操作。在這裡我使用的是一個20*10的二維數組。

//整個遊戲區的格子用二維數組保存
    box: cc.Node[][] = [];
//初始化box二維數組,這個數組的[0][0]在遊戲區的最左下角
    InitBox() {
        for (let i = 0; i < 20; i++) {
            this.box[i] = [];
            for (let j = 0; j < 10; j++) {
                this.box[i][j] = null;
            }
        }
        //生成不同的方塊集合
        this.buildBlock();
    }

2.每種類型方塊集合的構建

總所周知(),俄羅斯方塊中的方塊有七種,分別是:反Z型、L型、反L型、Z型、條型、T型、方形。

我們可以發現,每種方塊集合都是由四個小方塊組成的,我們可以利用這個特點構建統一的構建方法。

為瞭後續使用起來方便,我首先定義瞭每種小方塊的預制體(Prefab)和一個空節點的預制體,這個預制體所生成的節點是用來裝後續構建的方塊節點的。所以結構上是父與子的關系。

    //正方形的子塊
    @property(cc.Prefab)
    block_0: cc.Prefab = null;
    //Z字型的子塊
    @property(cc.Prefab)
    block_1: cc.Prefab = null;
    //左L型的子塊
    @property(cc.Prefab)
    block_2: cc.Prefab = null;
    //右L型的子塊
    @property(cc.Prefab)
    block_3: cc.Prefab = null;
    //反Z型的子塊
    @property(cc.Prefab)
    block_4: cc.Prefab = null;
    //長條型的子塊
    @property(cc.Prefab)
    block_5: cc.Prefab = null;
    //T字型的子塊
    @property(cc.Prefab)
    block_6: cc.Prefab = null;
    //方塊集合的中心
    @property(cc.Prefab)
    currentBlockCentre = null;
    //當前的塊
    currentBlock: cc.Node = null;        //currentBlockCentre的具體實現
    currentBlockPart01: cc.Node = null;  //四個子塊的具體實現
    currentBlockPart02: cc.Node = null;
    currentBlockPart03: cc.Node = null;
    currentBlockPart04: cc.Node = null;

關於隨機生成哪種顏色、哪種類型的方塊,我隻是簡單的選擇瞭自帶的Math.random()。

    buildBlock() {
        this.rand = Math.floor(7 * Math.random());    //從七種中隨機選擇一種構建
        this.chooseColor(this.rand);
        this.chooseType(this.rand);
    }

後面就是根據輸入的rand參數來選擇構建方塊集合的顏色、種類。關於如何構建,具體就是選擇這個方塊集合的中心點——最好選擇在某個子塊的中心,並將position設為(0, 0)。這樣,在後續的旋轉方面的實現會非常方便。然後選擇好中心點之後,其他的子塊就根據這個中心點來設置position,而cocos中子節點的position是相對於父節點的,子節點如果將position設置為(0, 0),那麼子節點的位置就在父節點中心點上。

另外,每個子塊的預制體尺寸都是60*60,也就是說遊戲區每個格子之間的間隔是60。

這一段的代碼比較長,我就不詳細給出瞭。

    //選擇方塊集合的顏色
    chooseColor(rand) {
        ……
        //Z字形方塊的顏色
        if (rand == 1) {
            this.currentBlockPart01 = cc.instantiate(this.block_1);
            this.currentBlockPart02 = cc.instantiate(this.block_1);
            this.currentBlockPart03 = cc.instantiate(this.block_1);
            this.currentBlockPart04 = cc.instantiate(this.block_1);
            this.currentBlock = cc.instantiate(this.currentBlockCentre);
            this.node.addChild(this.currentBlock);
            this.currentBlock.setPosition(30, 510);     //將當前生成的方塊集合位置設定在遊戲區的上面,準備後續的下落
        }
        //左L型方塊的顏色
        if (rand == 2)
        ……
    }
    //選擇形狀
    chooseType(rand) {
        ……
        //創建Z字形
        if (rand == 1) {
            //Z字形左
            this.currentBlockPart01.setPosition(-60, 0);
            this.currentBlockPart01Pos = cc.v2(18, 4);  //初始化當前塊的位置,相對於currentBlock
            //Z字形中
            this.currentBlockPart02.setPosition(0, 0);
            this.currentBlockPart02Pos = cc.v2(18, 5);
            //Z字形下
            this.currentBlockPart03.setPosition(0, -60);
            this.currentBlockPart03Pos = cc.v2(17, 5);
            //Z字形右
            this.currentBlockPart04.setPosition(60, -60);
            this.currentBlockPart04Pos = cc.v2(17, 6);
        }
        //創建左L型
        if (rand == 2)
        ……
    }

3.如何將創建的方塊集合和節點二維數組結合起來

上面的代碼裡有這樣的變量:currentBlockPart0XPos,定義瞭當前可操作方塊集合currentBlock每個子塊currentBlockPart0X在box節點二維數組中的具體位置。這四個變量非常有用,之後就可以實現當前可操作方塊移動之後,將位置信息保存在box節點二維數組中。

//當前子塊的位置
    currentBlockPart01Pos: cc.Vec2 = null;
    currentBlockPart02Pos: cc.Vec2 = null;
    currentBlockPart03Pos: cc.Vec2 = null;
    currentBlockPart04Pos: cc.Vec2 = null;

之後在每次可操作方塊集合變化後,我們都可以調用下面這兩個方法更新可操作方塊集合在box數組中的位置。

    //讀取當前操作方塊集合的位置信息
    checkCurrentBlockPos() {
        this.box[this.currentBlockPart01Pos.x][this.currentBlockPart01Pos.y] = this.currentBlockPart01;
        this.box[this.currentBlockPart02Pos.x][this.currentBlockPart02Pos.y] = this.currentBlockPart02;
        this.box[this.currentBlockPart03Pos.x][this.currentBlockPart03Pos.y] = this.currentBlockPart03;
        this.box[this.currentBlockPart04Pos.x][this.currentBlockPart04Pos.y] = this.currentBlockPart04;
    }
    //清除上個位置的當前操作方塊集合位置信息
    deleteCurrentBlockPos() {
        this.box[this.currentBlockPart01Pos.x][this.currentBlockPart01Pos.y] = null;
        this.box[this.currentBlockPart02Pos.x][this.currentBlockPart02Pos.y] = null;
        this.box[this.currentBlockPart03Pos.x][this.currentBlockPart03Pos.y] = null;
        this.box[this.currentBlockPart04Pos.x][this.currentBlockPart04Pos.y] = null;
    }

4.方塊集合的移動和旋轉

關於移動,遵循大部分俄羅斯方塊遊戲的操作方式,左鍵左移,右鍵右移,上鍵旋轉,下鍵下移,還有自動下落。

    //自動下落
    autoDown() {
        this.schedule(() => {
            //一直下落直到碰到下邊界
            if (this.isClashBottom()) {
                this.deleteRow();   //行消除檢測
                this.buildBlock();  //創建新的方塊集合
            } else if (this.isClashBlockDown()) {   //一直下落直到碰到其他方塊
                this.isGameOver();  //判斷遊戲是否結束
                this.deleteRow();
                this.buildBlock();
            } else {
                //向下一格
                this.currentBlock.y -= 60;
                this.deleteCurrentBlockPos();
                this.currentBlockPart01Pos.x -= 1;
                this.currentBlockPart02Pos.x -= 1;
                this.currentBlockPart03Pos.x -= 1;
                this.currentBlockPart04Pos.x -= 1;
                this.checkCurrentBlockPos();
            }
        }, 1);
    }
    
    //鍵盤監聽
    onKeyDown(e) {
        switch (e.keyCode) {
            case cc.macro.KEY.left:
                if (this.isClashLeft()) {    //判斷是否撞到左邊界
                    break;
                } else if (this.isClashBlockLeft()) {    //判斷當前操作塊是否左邊撞到瞭其他子塊
                    break;
                } else {
                    this.currentBlock.x -= 60;
                    this.deleteCurrentBlockPos();
                    this.currentBlockPart01Pos.y -= 1;
                    this.currentBlockPart02Pos.y -= 1;
                    this.currentBlockPart03Pos.y -= 1;
                    this.currentBlockPart04Pos.y -= 1;
                    this.checkCurrentBlockPos();
                    break;
                }
            case cc.macro.KEY.right:
                ……
            case cc.macro.KEY.up:
                //改變形態
                if (this.isClashLeft()) {    //判斷是否撞到左邊界
                    break;
                } else if (this.isClashRight()) {    //判斷是否撞到右邊界
                    break;
                } else if (this.isClashBottom()) {    //判斷是否撞到下邊界
                    break;
                } else if (this.isClashBlockLeft()) {    //判斷當前操作塊是否左邊撞到瞭其他子塊
                    break;
                } else if (this.isClashBlockRight()) {    //判斷當前操作塊是否右邊邊撞到瞭其他子塊
                    break;
                } else if (this.isClashBlockDown()) {    //判斷當前操作塊是否下邊撞到瞭其他子塊
                    break;
                } else {
                    this.deleteCurrentBlockPos();
                    this.changeShape();    //旋轉變形態
                    this.checkCurrentBlockPos();
                    break;
                }
            case cc.macro.KEY.down:
                ……
        }
    }

關於旋轉這部分,我其實是取巧瞭,我特意設置瞭某些子塊的位置為中心點,正好可以使我這種旋轉操作成立。

圖中灰色圓形指出的子塊則是我設定的中心點。而如果將中心點作為二維坐標原點,可以劃分為八個區域:y軸上半、y軸下半、x軸左半、x軸右半、第一象限、第二象限、第三象限、第四象限。

以Z型旋轉為例,可以發現,在四個坐標軸上的子塊x和y都改變瞭,而在象限上的子塊隻是改變瞭x和y的其中一個,而且是取原來值的相反數。我們這樣實現旋轉,實際上隻是子塊的位置改變瞭,子塊所朝方向並沒有改變。

//旋轉變形態
    changeShape() {
        this.whichPartChange(this.currentBlockPart01, this.currentBlockPart01Pos);
        this.whichPartChange(this.currentBlockPart02, this.currentBlockPart02Pos);
        this.whichPartChange(this.currentBlockPart03, this.currentBlockPart03Pos);
        this.whichPartChange(this.currentBlockPart04, this.currentBlockPart04Pos);
    }
    
    //傳入被判斷的部分
    whichPartChange(currentBlockPart: cc.Node, currentBlockPartPos: cc.Vec2) {
        //修正參數,用於旋轉currentBlockPartPos的位置,從左邊到上邊,上邊到右邊,右邊到下邊,下邊到左邊,在象限中的不需要用到
        let modParameterX = Math.abs(currentBlockPart.position.x / 60);
        let modParameterY = Math.abs(currentBlockPart.position.y / 60);
        let modParameterMax = Math.max(modParameterX, modParameterY);
        //y軸上半
        if (currentBlockPart.position.x == 0 && currentBlockPart.position.y > 0) {
            //行- 列+
            currentBlockPartPos.x -= modParameterMax;
            currentBlockPartPos.y += modParameterMax;
            //旋轉當前塊的位置
            currentBlockPart.setPosition(currentBlockPart.position.y, currentBlockPart.position.x);
        }
        //x軸左半 
        else if (currentBlockPart.position.x < 0 && currentBlockPart.position.y == 0) {
            ……
        }
        //y軸下半
        else if (currentBlockPart.position.x == 0 && currentBlockPart.position.y < 0) {
            ……
        }
        //x軸右半
        else if (currentBlockPart.position.x > 0 && currentBlockPart.position.y == 0) {
            ……
        }
        //第一象限
        if (currentBlockPart.position.x > 0 && currentBlockPart.position.y > 0) {
            //行-
            if (currentBlockPart.position.x >= 60 && currentBlockPart.position.y >= 60) {
                currentBlockPartPos.x -= 2;
            } else {
                currentBlockPartPos.x -= 1;
            }
            //旋轉當前塊的位置
            currentBlockPart.setPosition(currentBlockPart.position.x, -currentBlockPart.position.y);
        }
        //第二象限
        else if (currentBlockPart.position.x < 0 && currentBlockPart.position.y > 0) {
            ……
        }
        //第三象限
        else if (currentBlockPart.position.x < 0 && currentBlockPart.position.y < 0) {
            ……
        }
        //第四象限
        else if (currentBlockPart.position.x > 0 && currentBlockPart.position.y < 0) {
            ……
        }
    }

5.邊界和方塊檢測

邊界檢測有三種,分別是左邊界檢測、右邊界檢測和下邊界檢測。方塊檢測同樣為三種,分別是當前可操作方塊集合下方檢測、左方檢測和右方檢測。

//判斷是否即將碰撞到左邊界
    isClashLeft(): boolean {
        if (this.currentBlockPart01Pos.y - 1 < 0 || this.currentBlockPart02Pos.y - 1 < 0 ||
            this.currentBlockPart03Pos.y - 1 < 0 || this.currentBlockPart04Pos.y - 1 < 0) {
            return true;
        }
        return false;
    }
 
    //判斷是否即將碰撞到右邊界
    isClashRight(): boolean {
        ……
    }
 
    //判斷是否即將碰撞到下邊界
    isClashBottom(): boolean {
        ……
    }
//判斷是否即將碰撞到其他方塊(下)
    isClashBlockDown(): boolean {
        //向下檢測方塊碰撞
        if (this.box[this.currentBlockPart01Pos.x - 1][this.currentBlockPart01Pos.y] != null && !this.isCurrentBlockChild(this.box[this.currentBlockPart01Pos.x - 1][this.currentBlockPart01Pos.y]) ||
            this.box[this.currentBlockPart02Pos.x - 1][this.currentBlockPart02Pos.y] != null && !this.isCurrentBlockChild(this.box[this.currentBlockPart02Pos.x - 1][this.currentBlockPart02Pos.y]) ||
            this.box[this.currentBlockPart03Pos.x - 1][this.currentBlockPart03Pos.y] != null && !this.isCurrentBlockChild(this.box[this.currentBlockPart03Pos.x - 1][this.currentBlockPart03Pos.y]) ||
            this.box[this.currentBlockPart04Pos.x - 1][this.currentBlockPart04Pos.y] != null && !this.isCurrentBlockChild(this.box[this.currentBlockPart04Pos.x - 1][this.currentBlockPart04Pos.y])) {
            return true;
        }
    }
 
    //判斷是否即將碰撞到其他方塊(左)
    isClashBlockLeft() {
        ……
    }
 
    //判斷是否即將碰撞到其他方塊(右)
    isClashBlockRight() {
        ……
    }
 
    //判斷是否是當前操作方塊集合的子塊
    isCurrentBlockChild(judgeObj: cc.Node): boolean {
        for (let i = 0; i < 4; i++) {
            if (judgeObj === this.currentBlock.children[i]) {
                return true;
            }
        }
        return false;
    }

因為每個子塊在對方塊檢測時,都要向左、右或下一格判斷是否存在其他方塊,而有可能判斷的方塊是和自己同一個父類的,所以判斷時還要判斷是否為當前操作方塊集合的子塊。

6.方塊的整行消除

需要註意的是,遊戲內方塊如果一列一列看的話,有時會存在鏤空的情況,這時就要考慮鏤空的時候要怎麼向下移動一格。所以在rowDown()方法中,在整體下降的時候,如果判斷到同一列上一格是空的,則賦為null,把剛移動到下一格的方塊信息刪除。

//行消除檢測
    deleteRow() {
        for (let i = 0; i < 18; i++) {
            let count = 0;
            for (let j = 0; j < 10; j++) {
                if (this.box[i][j] != null) {
                    count++;
                }
            }
            //如果某一行內都存在方塊
            if (count == 10) {
                for (let j = 0; j < 10; j++) {
                    //方塊刪除
                    this.box[i][j].removeFromParent();
                    this.box[i][j] = null;
                }
                this.rowDown(i);
                i--;    //因為rowDown(i)後,整體向下瞭一格,所以i--,否則無法實現多行消除,導致遊戲無法正常運行
            }
        }
    }
    
    //全體方塊向下移動一格
    rowDown(i: number) {
        //記錄i值,即被當前被消除行
        let k = i;
        //列遍歷
        for (let j = 0; j < 10; j++) {
            //temp:用於計算當前被消除行上面有多少行的方塊元素(包括中間層存在鏤空)
            let temp = -1;
            for (i = k; i < 18; i++) {
                temp++;
                if (this.box[i][j] != null) {
                    this.box[i - 1][j] = this.box[i][j];
                    this.box[i][j].y -= 60;
                    if (this.box[i + 1][j] == null) {
                        this.box[temp + k][j] = null;
                    }
                }
            }
        }
    }

3.寫在最後

大體上最核心的問題我應該都好好說明瞭,如果有某些地方不清楚的話,歡迎下載原項目文件:

鏈接: 百度網盤 請輸入提取碼 提取碼: c4ss

到此這篇關於CocosCreator 制作俄羅斯方塊遊戲的文章就介紹到這瞭,更多相關CocosCreator內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: