小游戏《俄罗斯方块》开发

本文详细介绍了如何使用白鹭引擎开发一款简单的《俄罗斯方块》小游戏,包括游戏主界面和重新开始界面的设计,方块图形的坐标设置、添加、移动和分数计算,以及操作方块的实现,提供了游戏开发的基础步骤和关键逻辑。
摘要由CSDN通过智能技术生成

前述

《俄罗斯方块》这款游戏大家应该都不陌生吧,以前的老爷手机上都会内置这款游戏,本篇我们一起使用白鹭引擎开发一款简易版的《俄罗斯方块》小游戏。

演示地址:点击查看

开始

运行效果:image
(说明:帧数做了删减)

界面中,中间是游戏界面,底部是三个操作按钮,右边是游戏信息展示。图形下落过程中,玩家可以操作按钮控制图形。

首先将相应图片资源复制一份到我们的项目中,然后回到编辑器头部的资源,弹出添加提示,点击添加,这样资源配置信息就会自动添加到 default.res.json 中。

设计游戏主界面

设计好的皮肤文件效果:

image

首先我们将界面大小设置为 400 * 500 ,界面上添加相关控件:图形方块容器 scrollBox,下一个图形方块预览容器 nextShapeBox,分数显示控件 scoreLabel,底部三个操作按钮 leftBtnrotateShapeBtnrightBtn(已开启触摸监听)。

新建ts文件 Pannel.ts,创建类 Pannel ,将皮肤引入,绑定自定义事件:

// Pannel.ts
class Pannel extends eui.Component  {
    public scrollBox: eui.Group;
    public nextShapeBox: eui.Group;
    public leftBtn: eui.Button;
    public rotateShapeBtn: eui.Button;
    public rightBtn: eui.Button;
    public scoreLabel: eui.Label;
    
    // 分数 
    private _score: number = 0;
    
    public constructor() {
        super();
        this.skinName = "resource/skins/Pannel.exml";
        
        this.event();
    }
    
    private event() {
        const LeftEvent:MainEvent = new MainEvent(MainEvent.Left);
        const RightEvent:MainEvent = new MainEvent(MainEvent.Right);
        const RotateShapeEvent:MainEvent = new MainEvent(MainEvent.RotateShape);

        /**点击按钮'左边' */
        this.leftBtn.addEventListener(egret.TouchEvent.TOUCH_TAP, () => {
            this.dispatchEvent(LeftEvent);
        }, this);

        /**点击按钮'右边' */
        this.rightBtn.addEventListener(egret.TouchEvent.TOUCH_TAP, () => {
            this.dispatchEvent(RightEvent);
        }, this);

        /**点击按钮'翻转' */
        this.rotateShapeBtn.addEventListener(egret.TouchEvent.TOUCH_TAP, () => {
            this.dispatchEvent(RotateShapeEvent);
        }, this);
    }
    
}

当点击底部三个按钮,触发对应的自定义事件,然后在父层上监听事件:

// Main.ts
this.pannelUI = new Pannel();
this.pannelUI.addEventListener(MainEvent.Left, this.translateAction, this);
this.pannelUI.addEventListener(MainEvent.Right, this.translateAction, this);
this.pannelUI.addEventListener(MainEvent.RotateShape, this.rotateShape, this);

自定义事件类 MainEvent 的实现:

// MainEvent.ts
class MainEvent extends egret.Event {
    /**往左边移动 */
    public static Left:string = '左移';
    /**往右边移动 */
    public static Right:string = '右移';
    /**图形翻转 */
    public static RotateShape:string = '图形翻转';
    /**重新开始 */
    public static Restart:string = '重新开始';


    private _resName: string = ""; 

    public constructor(type:string, resName:string="", bubbles:boolean=false, cancelable:boolean=false) {
        super(type, bubbles, cancelable);
        this._resName = resName;
    }

    public get resName(): string {
        return this._resName;
    }
}

当点击左右按钮时,触发的回调方法 translateAction 中,我们通过属性 type 来确定是左移还是右移 :

// Main.ts
private translateAction(event: MainEvent): void {
    if (event.type === '左移') {
        this.translateXShape(-1);
    } else
    if (event.type === '右移') {
        this.translateXShape(1);
    }
}

最后我们给游戏主容器添加一个黑色矩形边框

// Pannel.ts
// 给主容器添加一个矩形边框 
const shp:egret.Shape = new egret.Shape();
shp.graphics.lineStyle( 2, 0xffffff );
shp.graphics.beginFill( 0x000000, 1);
shp.graphics.drawRect( 0, 0, this.scrollBox.width, this.scrollBox.height);
shp.graphics.endFill();
this.scrollBox.addChild( shp );
设计重新开始界面

设计好的皮肤文件效果:

image

首先我们将界面大小设置为 400 * 500 ,界面上添加相关控件:先添加一个透明度为0.8、背景颜色为黑色的矩形,然后再添加按钮“来一局”(已开启触摸监听)。

新建ts文件 Restart.ts,创建类 Restart ,将皮肤引入,绑定自定义事件:

// Restart.ts
class Restart extends eui.Component  {
    public restart: eui.Button;

    public constructor() {
        super();
        this.skinName = "resource/skins/Restart.exml";

        this.event();
    }

    private event() {
        const RestartEvent:MainEvent = new MainEvent(MainEvent.Restart);
        /**点击按钮 `再来一局` */
        this.restart.addEventListener(egret.TouchEvent.TOUCH_TAP, () => {
            this.dispatchEvent(RestartEvent);
        }, this);
    }
}

这里提下,如果编译后提示如:Duplicate identifier 'xxx' 等,如果没有问题怎么也通过不了,此时我们先运行 egret clean 清除 ,然后重新编译。

当点击按钮“再来一局”,触发对应的自定义事件,然后在父层上监听事件:

// Main.ts
this.restartUI = new Restart();
this.restartUI.addEventListener(MainEvent.Restart, this.start, this);
方块图形

俄罗斯方块一共有七种形状,如图:

image

设置方块坐标

本文我们就讲解第一种图形,其它图形读者可参考下面内容自行研究。

第一种方块图形的在坐标轴上的表示:

image

上图我们在坐标轴上绘制了第一种形状,由四个格子组成,每个格子的起点为:A(1 , 0)、B(1, 1)、C(1, 2)、D(0, 2)。(这里每个格子的长度为单位长度)

这样我们就获取到第一种图形的起点坐标:shapeArr = [[1, 0],[1, 1],[1, 2],[0, 2]]。

格子 A 的起点坐标转为实际坐标:

x = Main.Gridsize * shapeArr[0][0];
y = Main.Gridsize * shapeArr[0][1];

Main.Gridsize 是每个正方形格子的大小。

然后我们将格子 x轴方向居中放置到容器 this.pannelUI.nextShapeBox 中。x轴上居中的位置坐标:shapeX = this.pannelUI.scrollBox.width / 2,y轴: shapeY = Main.Gridsize * 2。

此时格子 A 的起点坐标转为实际坐标:

x = Main.Gridsize * shapeArr[0][0] + shapeX;
y = Main.Gridsize * shapeArr[0][1] + shapeY;

这里 x 值最终要等于值 shapeX。 但由于格子 A 起点坐标为 (1, 0),并不满足需求。这里我们直接将格子 A 起点坐标的 X 轴方向往左移动一个单位距离,转换后坐标为: (0, 1)。

故该形状的坐标表示变为:shapeArr = [[0, 0],[0, 1],[0, 2],[-1, 2]]。

整理后实现转换实际坐标方法:

// Main.ts
private transitionCoordinate(shapeArr, shapeX, shapeY) {
    const arr = [];
    for (let i = 0; i < shapeArr.length; i++) {
        arr.push([ 
            Main.Gridsize * shapeArr[i][0] + shapeX,  
            Main.Gridsize * shapeArr[i][1] + shapeY
        ]);
    }
    return arr;
}
添加方块

获取到图形的实际坐标后,我们在父层添加图形。图形是由 20 * 20 大小的格子组成,格子的图片资源为 rect_png

一个格子:

grid = Util.createBitmapByName('rect_png');

在辅助类 Util 中我们封装了获取资源位图的方法。

因为所有的方块都是由 grid 组成,随着游戏的持续,生成的格子对象越来越多,会影响性能。我们有必要从回收池中获取格子对象。

从回收池中获取格子:

// Main.ts
private getGrid():egret.Bitmap  {
    let grid;
    if (this.poolList.length) {
        // 取出队列的最后一个
        grid = this.poolList.pop();
    } else {
        grid = Util.createBitmapByName('rect_png');
    }
    return grid;
}

属性 this.poolList 是格子回收池列表。 获取一个格子时,先从列表中取,没有的话再实例一个格子对象。

从显示对象列表中移除格子:

// Main.ts
private destroyGrid(grid: egret.Bitmap, layer:eui.Group) {
    layer.removeChild(grid);
    this.poolList.push(grid);
}

当在父容器上的格子移除后,放回到回收池中。

然后根据坐标绘制图形:

// Main.ts
/**绘制图形 */
private drawShape(shape?:any, layer?: eui.Group): void {
    const shapeObject = shape || this.nowShape;
    const container = layer || this.pannelUI.scrollBox;
    const arr = this.transitionCoordinate(shapeObject.data, shapeObject.x, shapeObject.y);
    for (let i = 0; i < arr.length; i++) {
        const grid = this.getGrid();
        grid.x = arr[i][0];
        grid.y = arr[i][1];
        grid.name = 'grid' + '_' + shapeObject.index;
        container.addChild(grid);
    }
}

方法内 this.nowShape 是当前要添加的图形属性。参数 shape 是要添加的图形属性,参数layer 是图形容器。

一个图形的属性对象组成:

// Main.ts
// 默认Y轴超出容器范围,x轴居中
this.nowShape = {
    x: this.pannelUI.scrollBox.width / 2,
    y: -40,
    shapeIndex: this.nextShapeIndex,
    index: this.index,
    data: JSON.parse(JSON.stringify(this.shapeList[this.nextShapeIndex]))
};

属性 this.shapeList 是方块形状的集合,属性 this.nextShapeIndex 是下一个要添加的方块形状索引。

创建一个新的图形方法如下:

// Main.ts
private createNewShape(): void {
    this.nowShape = {...}
    this.index ++;
    // 随机赋值下一个方块形状索引
    this.nextShapeIndex = Math.floor(Math.random() * this.shapeList.length);
    
    // 将下一个图形添加到预览容器中
    const nextShape = {
        x: 40,
        y: 40,
        index: 0,
        data: JSON.parse(JSON.stringify(this.shapeList[this.nextShapeIndex]))
    };
    this.clearNextShape();
    this.drawShape(nextShape, this.pannelUI.nextShapeBox);
    
    // 添加心跳监听,图形不断往下移动
    egret.startTick(this.translateYShape, this);
}

创建一个图形的流程:我们先设置好当前要添加的图形属性,然后随机设置下一个图形的类型索引,再将下一个图形添加到预览容器中。最后添加心跳监听,让当前方块不断往下移动。

清除预览容器内的格子方法 this.clearNextShape 内,我们调用方法 this.clearShape

清除父层上所有的子对象,我们可直接调用 this.removeChildren 方法。但因为我们需要将界面上要移除的格子对象保存到回收池中,所以需要先获取父层上的格子对象。

// Main.ts
/**获取指定容器中格子对象列表 */
private getGridFromLayer(layer: eui.Group, index?: number): egret.Bitmap[] {
    let arr = [];
    for (let i = 0; i < layer.numChildren; i++) {
        const grid = layer.getChildAt(i);
        if (grid) {
            if (typeof index === 'undefined') {
                if (grid.name.indexOf('grid') > -1) {
                    arr.push(grid);
                }
            } else
            if (grid.name === ('grid_'+index) ) {
                arr.push(grid);
            }
        }
    }
    return arr;
}

获取到格子列表后,再移除:

// Main.ts
/**清除图形显示容器的当前图形 */
private clearShape(layer: eui.Group, index?: number):void  {
    let gridArr = this.getGridFromLayer(layer, index);
    let grid;
    while(gridArr.length) {
        grid = gridArr.shift();
        this.destroyGrid(<egret.Bitmap>grid, layer);
    }
}
方块往下移动

方块添加到容器后,就不断往下移动,移动速度越快,游戏难度就越高。

首先定义时间阈值:

// Main.ts
private timeNum: number = 0;
// 移动速度,阈值
private timeMax = 300;
private time = 0;

添加一个新的方块后,就会监听心跳,执行回调方法 this.translateYShape ,实现下降速率控制:

// Main.ts
// timeStamp 是心跳回调时的时间戳
private translateYShape(timeStamp:number): boolean {
    const now = timeStamp;
    const time = this.time;
    const pass = now - time;
    this.timeNum += pass;
    // 超出阈值
    if (this.timeNum > this.timeMax) {
        this.timeNum = 0;
        // 更新当前图形Y轴值,重绘当前图形
        this.clearShape(this.pannelUI.scrollBox, this.nowShape.index);
        this.nowShape.y += Main.Gridsize;
        this.drawShape();
    }
    this.time = now;
    return false;
}

执行后,方块就不断往下移动,一直超出游戏场景范围。我们希望方块碰到容器底部后,就停止运动,然后新增一个方块到容器,假设我们已经实现了检测方块能否继续往下移动方法,修改如下:

// Main.ts
private translateYShape(timeStamp:number): boolean {
    //其它代码省略......
    // 检测方块是否可以继续运行
    const checkedBool = this.checkYBoundary();
    if (checkedBool) {
        // 更新当前图形Y轴值,重绘当前图形
        this.clearShape(this.pannelUI.scrollBox, this.nowShape.index);
        this.nowShape.y += Main.Gridsize;
        this.drawShape();
    } else {
        // 停止心跳监听
        egret.stopTick(this.translateYShape, this);
        // 新增下一个图形方块到容器
        this.createNewShape();
    }
    //其它代码省略......
}

接下我们实现方法 this.checkYBoundary

方块是由多个大小相同的小格子组成的,每次往下移动都是一个格子大小。判断方块能否继续往下移动,需要检查三个情况:

  1. 当前方块中有个小格子的 Y 轴已经在整个容器最底部,那就不能往下移动;
  2. 当前方块中有个小格子,如果下个移动位置已经被占用,那就不能往下移动;
  3. 如果满足条件2后,如果此时有个小格子超出顶部位置,那么游戏结束;

在次之前,我们将容器分隔成多个小格子(容器大小设置成可以被 Main.Gridsize 整除):

// Main.ts
private createMatrix():void {
    // grids[i] Y轴  grids[i][j] X轴,注意我们的坐标轴是左上角开始,往下是Y轴,往右是X轴
    this.grids = <any>[];
    for (let i = 0; i < this.pannelUI.scrollBox.height / Main.Gridsize; i++) {
        this.grids[i] = <any>[];
        for (let j = 0; j < this.pannelUI.scrollBox.width / Main.Gridsize; j++) {
            this.grids[i][j] = false;
        }
    }
}

初始时,我们给每个格子的值都设置为 false ,表示没有被占用。游戏每次开始前,运行上面方法。

有了上面的准备,我们实现方法 checkYBoundary

首先获取当前方块(移动中)的所有小格子实际坐标,然后再获取每个小格子在容器格子集合( this.grids )的位置:

// Main.ts
private checkYBoundary() : boolean {
    let bool = true;
    // 获取当前方块的小格子的实际坐标,
    const arr = this.transitionCoordinate(this.nowShape.data, this.nowShape.x, this.nowShape.y);
    for (let i = 0; i < arr.length; i++) {
        // xNum,yNum 是方块的小格子在容器的位置索引
        const xNum = arr[i][0] / Main.Gridsize;
        const yNum = arr[i][1] /  Main.Gridsize;
    }
    return bool;
}

在循环体中,先判断是否已经在最底部:

// Main.ts
// 注意:坐标轴的原点是左上角
if (yNum === (this.grids.length - 1)) {
    bool = false;
    break;
}

再判断下一步是否被占用:

// Main.ts
if ( (typeof this.grids[yNum + 1] !== 'undefined') && this.grids[yNum + 1][xNum]) {
   bool = false;
   break;
}

最后再判断此时的方块是否还未进入容器(每次新增的方块 Y 轴值都是负值):

// Main.ts
if ( (typeof this.grids[yNum + 1] !== 'undefined') && this.grids[yNum + 1][xNum]) {
   if (yNum === -1) {
      // 游戏结束
      this.restart();
   }
   bool = false;
   break;
}

一旦游戏结束就不能再自动新增图形,所以我们定义了属性 isPause ,标记游戏是否暂停。

方法 restart

// Main.ts
private restart(): void {
    console.log('游戏结束')
    this.isPause = true;
    egret.stopTick(this.translateYShape, this);
    this.addChild(this.restartUI);
}

重写修改方法 translateYShape :

// Main.ts
private translateYShape(timeStamp:number): boolean {
    // 省略其它代码......
    if (!this.isPause) {
        const checkedBool = this.checkYBoundary();
        if (checkedBool) {
            // 省略
        } else {
            // 省略 
        }
    }
    // 省略
}

上面我们实现了方块下降和下降的检测。当停止下降后,我们需要将容器的相应格子标记为占用,如果此时某行都被占用后就得销毁同时增加分数。

当停止下降后,我们执行方法 this.drawWall ,再次改造方法 translateYShape

// Main.ts
private translateYShape(timeStamp:number): boolean {
    // 省略其它代码......
    if (!this.isPause) {
        const checkedBool = this.checkYBoundary();
        if (checkedBool) {
            // 省略
        } else {
            this.drawWall();
            // 省略 
        }
    }
    // 省略
}
分数

方块停止下降后,当检测到某行全被占用,就更新分数,销毁该行的小格子,下面我们讲解如何实现。

方块停止下降后,我们需要把方块所在的容器小格子标记为已占用,跟方法 checkYBoundary 一样,我们先获取当前图形的坐标信息,然后再获取索引,最后标记。

我们实现方法 drawWall

// Main.ts
private drawWall():void {
    const arr = this.transitionCoordinate(this.nowShape.data, this.nowShape.x, this.nowShape.y);
    let i = 0;
    try {
        for (i = 0; i < arr.length; i++) {
            const yNum = arr[i][1] / Main.Gridsize;
            const xNum = arr[i][0] / Main.Gridsize;
            //停止下降后,此时要把所在的格子标志为已占用(true)
            this.grids[yNum][xNum] = true;
        }
    } catch (error) {
        this.restart();
    }
}

然后检测某行都被占用的格子,设置分数,并重新赋予每个格子的值(最终表现为堆叠的格子整体下降了):

// Main.ts  
// 方法 drawWall
for (i = 0; i < this.grids.length; i++) {
    // 当前循环的行是否满格,值默认占满
    let mark = true;
    // 循环某个行,该行上的所有小格子都被占用
    for (let k = 0; k < this.grids[i].length; k++) {
        if (!this.grids[i][k]) {
            mark = false;
            break;
        }
    }
    if (mark) {
        this.changeScore();
    }
}

当检测到满格时,就更新分数:

// Main.ts
private changeScore(score?:number): void {
    if (typeof score === 'undefined') {
        this.score += 1;
    } else {
        this.score = score;
    }
    this.pannelUI.score = this.score;
}

我们在类 Pannel 里新增更新分数方法:

// Pannel.ts
public get score(): number {
    return this._score;
}

/**设置分数 */
public set score(score: number) {
    this._score = score;
    this.scoreLabel.text = this._score + '分';
}

分数更新完毕后,接下来将该行以上占用格子往下移动,我们只需将前一行值赋予当前行,循环赋予即可。

// Main.ts
// 方法 drawWall
if (mark) {
    this.changeScore();
    // i 是此时占满格子的行所在的索引
    for (let j = i; j > 0; j--) {
        for (let h = 0; h < this.grids[i].length; h++) {
            // 将上一行的占用值赋予当前行
            this.grids[j][h] = this.grids[j-1][h];
        }
    }
}

然后重新绘制被占用的格子,先清除再绘制:

// Main.ts
// 方法 drawWall
this.clearShape(this.pannelUI.scrollBox);
// 绘制已被占的格子
for (let g = 0; g < this.grids.length; g++) {
    for (let s = 0; s < this.grids[g].length; s++) {
        if (this.grids[g][s]) {
            const grid = this.getGrid();
            grid.x = s * Main.Gridsize;
            grid.y = g * Main.Gridsize;
            this.pannelUI.scrollBox.addChild(grid);
        }
    }
}
小结

本小结讲解了方块的形状坐标表示,添加方块,往下移动方块,检测Y轴移动、分数的设置。

操作方块

游戏界面上设计了三个按钮,分别为:左移、翻转、右移,这小结我们实现这三个功能。

  • 左右移动

左右移动实现是一样的,我们在父层已经监听了点击左右移动的事件,执行回调方法 translateAction :

// Main.ts
private translateAction(event: MainEvent): void {
    if (event.type === '左移') {
        this.translateXShape(-1);
    } else
    if (event.type === '右移') {
        this.translateXShape(1);
    }
} 

跟Y轴下降一样,每次左右移动的距离都是属性值 Main.Gridsize 的 n 倍。

// Main.ts
// num  负往左边移动;正往右边移动
private translateXShape(num: number): void {
    this.nowShape.x += Main.Gridsize * num;
    this.clearShape(this.pannelUI.scrollBox, this.nowShape.index);
    this.drawShape();
}

当不断点击往左或往右后,方块就超出容器范围,我们希望当处于边界时,不能移动,假设已经实现了x轴左右移动检测方法 this.checkXBoundary, 上面重新改造:

// Main.ts
// num  负往左边移动;正往右边移动
private translateXShape(num: number): void {
    // x轴可左右移动检测
    if (this.checkXBoundary()) {
        this.nowShape.x += Main.Gridsize * num;
        this.clearShape(this.pannelUI.scrollBox, this.nowShape.index);
        this.drawShape();
    }
}
  • 翻转

跟左右按钮一样,我们也实现监听回调,先放代码:

private rotateShape():void {
    // 田字图形无需翻转
    if (this.nowShape.shapeIndex  === 5) {
        return;
    }
    const data = this.nowShape.data;
    const temp = [];
    
    for (let i = 0; i < data.length; i++) {
        // 关键代码
        temp.push([data[i][1], -data[i][0] ]);
    }
    this.nowShape.data = temp;
    this.clearShape(this.pannelUI.scrollBox, this.nowShape.index);
    this.drawShape();
}

上面我们实现了图形绕原点逆时针旋转,读者可能对这部分不是很清楚,接下来我们简单推导下公式。

我们先看在直角坐标系上一个点围绕原点逆时针旋转的效果图:

image

现在问题为:已知旋转前点的坐标为 P,方向角度为 a ,绕原点的旋转角度为 b ,点到原点的距离为 R,求旋转后点的坐标 P’ 。

根据三角函数,我们可以得出坐标 P’ :

x' = Rcos(a+b)
y' = Rsin(a+b)

根据和差公式,得到如下等式:

x' = Rcos(a)cos(b) - Rsin(a)sin(b)      

y' = Rsin(a)cos(b) + Rcos(a)sin(b)

观察上式,Rcos(a) = x, Rsin(a) = y, 带入等式:

x' = xcos(b) - ysin(b)

y' = xsin(b) + ycos(b)

我们每次翻转都是 90° ,因为 cos(90) = 0, sin(90) = 1 带入上面公式,得出:

x' = -y     

y' = x

上面公式是通过直角坐标系得出,我们的画布坐标轴值是相反的,故:

x' = -(-y) = y     

y' = (-x) = -x

这里顺便提下,因为 π 精度和浮点运算问题, js根据角度计算sin和cos值的计算方式可如下:

//角度
var  vAngle=90//正弦值
var vSin= Math.round(Math.sin((vAngle * Math.PI/180)) * 1000000) / 1000000;
//余弦值
var vCos= Math.round(Math.cos((vAngle * Math.PI/180)) * 1000000) / 1000000;

实现了逆时针翻转后,我们需要再次检测边界问题,重新改造方法 rotateShape

// Main.ts
private rotateShape():void {
    // 代码省略......
    if (this.checkXBoundary()) {
        // 通过后才重新绘制,代码省略......
    }
}

x轴左右移动检测有三种情况是无法通过的:

  1. 超出容器左边;
  2. 超出容器右边;
  3. 碰到被占用的格子;

我们在方法 checkXBoundary 中实现三种情况的检测。

先获取组成当前方块的小格子坐标,获取所在容器的位置:

// Main.ts
private checkXBoundary(): Boolean {
    let bool = true;
    const arr = this.transitionCoordinate(this.nowShape.data, this.nowShape.x, this.nowShape.y);
    
    try {
        for (let i = 0; i < arr.length; i++) {
            const xNum = arr[i][0] / Main.Gridsize;
            const yNum = arr[i][1] /  Main.Gridsize;
            // 实现条件判断
        }
    }catch(e) {}
    
    return bool;
}

判断超出左边边界:

// Main.ts
private checkXBoundary(): Boolean {
    // 省略代码......
    if (xNum < 0) {
        bool = false;
        break;
    }
}

判断超出右边边界:

// Main.ts
private checkXBoundary(): Boolean {
    // 省略代码...... this.grids[0].length 取第一行的格子数
    if (xNum > this.grids[0].length - 1) {
        bool = false;
        break;
    }
}

判断碰到被占用的格子:

// Main.ts
private checkXBoundary(): Boolean {
    // 省略代码......
    if (this.grids[yNum][xNum]) {
        bool = false;
        break;
    }
}

我们在翻转图形时,如果已经在边界,或者左右两边已经有被占用的格子,此时不能翻转:

// Main.ts
private rotateShape():void {
    // 省略代码......、
    // 只有检测通过才能重绘实现翻转效果
    if (this.checkXBoundary()) {
        this.clearShape(this.pannelUI.scrollBox, this.nowShape.index);
        this.drawShape();
    }
}

假如翻转超出了左右,我们也允许翻转的话,那么此时应该将图形往左移或右移,具体实现请读者参考示例。

小结

本小结讲解了方块的左右移动控制,x轴移动检测,图形翻转的实现。

最后

本篇我们从头讲解了一个简单俄罗斯方块游戏的实现,希望读者能够学到使用白鹭引擎开发小游戏。本篇中方块图形一共有7种,希望读者能够按照教程自行推出坐标集,另外上面实现图形翻转效果也可以进行改进,这些请读者自行实现,本篇不再细述。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值