3

#盲盒+码# #跟着小白一起学鸿蒙# [番外]一起学做Tetris(下)

 1 year ago
source link: https://blog.51cto.com/harmonyos/5915150
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

#跟着小白一起学鸿蒙# [番外]一起学做Tetris(下)

作者:王石

接着《#跟着小白一起学鸿蒙# [番外]一起学做Tetris(上)》我们完善了页面,增加了左右按键和之前方块显示,方块消除。

#盲盒+码# #跟着小白一起学鸿蒙# [番外]一起学做Tetris(下)_openharmony

1. 按键增加

之前我们布局一直是只有个Canvas控件,现在我们需要设置高度后增加一个Row的布局,并增加两个Button控件,以下就是基础的Hap的page文件:index.ets

  build() {
    Column() {
      Column() {
        Canvas(this.context)
          .width('100%')
          .height('100%')
          .onClick((ev: ClickEvent) => {
            console.info("click!!")
            this.doClick()
          })
          .onTouch((ev) => {
            console.info("touch:"+ev.type.toString())
            console.info("touch x:"+ev.touches[0].screenX.toString())
            console.info("touch y:"+ev.touches[0].screenY.toString())
          })

          .onReady(() =>{
            this.context.imageSmoothingEnabled = false
            this.randomType()
            this.drawall()
          })
      }
      .height('92%')
      .width('100%')
      Row() {
        Button() {    //按钮控件
          Text('左')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
        }.type(ButtonType.Capsule)
        .width('20%')
        .height('6%')
        .backgroundColor('#0D9FFB')
        .onClick(() => {    //点击事件
          if (this.left > 0) {
            this.moveAction -= 1
          }
        })
        Button() {    //按钮控件
          Text('右')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
        }.type(ButtonType.Capsule)
        .width('20%')
        .height('6%')
        .backgroundColor('#0D9FFB')
        .onClick(() => {    //点击事件
          if (this.rightX < 240) {
            this.moveAction += 1
          }
        })
      }

    }
    .width('100%')
    .height('100%')
    .backgroundColor("#cccccc")
  }

2. 游戏完善的说明

之前我们的游戏只有布局,方块显示和变形,在完善后我们增加了积累方块的显示,消除,计分,游戏结束判断等。

2.1 积累方块显示
drawBlockmap() {
    let bs = this.blockSize
    this.context.fillStyle = 'rgb(250,0,0)'
    for (let i=0;i<23;i++) {
      for (let j=0;j<9;j++) {
        //是否有方块
        if (this.blockmap[i][j] == 1) {
          let y = i * this.blockSize
          let x = j * this.blockSize
          this.context.fillRect(x, y, bs, bs)
          this.context.rect(x, y, bs, bs)
          console.info("drawBlockmap:"+x.toString()+":"+y.toString())
        }
      }
      this.context.stroke()
      console.info("drawBlockmap:"+this.storeBlock.length.toString())
    }
  }

因为都是画布画的,为了重绘已经存在的方块,我们应用二维数组blockmap表示,值为1则有方块,数组索引则表示绘制坐标位置:

2.2 判断是否到底还是到顶
  checkBlockmap() {
    if (this.storeBlock.length == 0) {
      if (this.slotBottomY >= 660) {
        return 1
      }
    } else {
      let coords = this.curBlockShap
      let startx = this.slotStartX + this.moveAction * this.blockSize
      let starty = this.slotStartY + this.step * this.blockSize
      for (let i=0;i<4;i++) {
        let x = startx + coords[i][0]*this.blockSize
        let y = starty + coords[i][1]*this.blockSize + this.blockSize
        for (let k=0;k<22;k++) {
          for (let l=0;l<9;l++) {
            if (this.blockmap[k][l] == 1) {
              let blocky = k * this.blockSize
              let blockx = l * this.blockSize
              //判断是否到底
              if ((x == blockx && y == blocky) || y > 660) {
                //判断是否到顶
                if (y == 210) {
                  this.context.drawImage( this.gameoverImg,this.startX,this.startY,300,90)
                  //到顶回2
                  return 2
                }
                //到底回1
                return 1
              }
            }
          }
        }
      }
    }
    return 0
  }

先判断是否到底,到底的同时判断是否到顶,如果到顶了就是满了,如果只是到底则表明游戏可以继续 ;

2.3 到底积累方块
stackBlock() {
    let block = []
    let coords = this.curBlockShap
    let startx = this.slotStartX + this.moveAction * this.blockSize
    let starty = this.slotStartY + this.step * this.blockSize
    for (let i=0;i<4;i++) {
      let x = startx + coords[i][0]*this.blockSize
      let y = starty + coords[i][1]*this.blockSize
      console.info("stackBlock x:"+x.toString()+"y:"+y.toString())
      let indexX = x/this.blockSize
      let indexY = y/this.blockSize
      this.blockmap[indexY][indexX] = 1
      console.info("stackBlock:"+indexX+":"+indexY)
      block.push([x,y])
    }
    this.storeBlock.push(block)

    console.info("stackBlock:"+this.storeBlock.length.toString())
  }

如果到底了,就用坐标计算出 索引,然后在blockmap里标识为1,说明此处有方块,这样方便后面的绘制的显示;

2.4 清除方块
cleanBlockmap() {
    //检查是否一行满了
    let needMove = 0
    for (let i=22;i>=0;i--) {
      let linecnt = 0
      for (let j = 8;j >= 0; j--) {
        //是否有方块
        if (this.blockmap[i][j] == 1) {
          linecnt += 1
        }
      }
      if (linecnt == 9) {
        //此行都是方块,消除,计分
        for (let j = 8;j >= 0; j--) {
          this.blockmap[i][j] = 0
        }
        needMove += 1
        this.score += 1
      } else if (needMove > 0) {
        for (let j = 8;j >= 0; j--) {
          this.blockmap[i+needMove][j] = this.blockmap[i][j]
          this.blockmap[i][j] = 0
        }
      }
    }
  }

二维数组的好处就是方便每行计数清除,然后从底向上再逐层替换;

2.5 绘制每一步
drawStep() {
    this.context.clearRect(0,0,this.context.width,this.context.height)
    this.step += 1
    this.drawBox()
    this.drawBlockmap()
    this.cleanBlockmap()
    this.drawSideBlock()
    this.drawBoxBlock()
    this.drawScore()
    let stepType = this.checkBlockmap()
    if ( stepType == 1) {
      this.stackBlock()
      this.step = 0
      this.randomType()
    } else if (stepType == 2) {
      this.state= 2
      this.context.drawImage( this.gameoverImg,this.startX,this.startY,300,90)
    }
  }

绘制每一步其实就是重绘界面,包括如果游戏结束显示game over

3. 游戏逻辑

简单的小游戏主体游戏逻辑为:等待开始,开始,结束流程图如下:

graph LR
timer开始 --> 方块下落
timer开始 --> click[点击]
click[点击] --> 方块变形
方块下落 --> |落到底| 能消除 --> 计分 --> 堆积
方块下落 --> |落到底| 不能消除 --> 堆积
堆积 --> |堆积到顶| 满了 --> 游戏结束
堆积 --> |堆积到顶| 未满 --> 方块下落
doClick() {
    if (this.state == 0) {
      this.direction += 1
    } else if (this.state == 2) {
      this.state = 0
      this.score = 0
      this.storeBlock = []
      this.initMap()
    }
  }

游戏结束后需要重新初始化内部数据。

4. 完整逻辑

@Entry
@Component
struct Index {
  @State message: string = 'Hello World'
  private settings: RenderingContextSettings = new RenderingContextSettings(true);
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
  private gameoverImg:ImageBitmap = new ImageBitmap("common/images/gameover.png")
  private blockType: number = 0
  private blockSize: number = 30
  private blockShapBasic = [
    [[0,0],[0,1],[0,2],[0,3]],
    [[0,0],[0,1],[0,2],[1,2]],
    [[0,0],[0,1],[1,1],[0,2]],
    [[0,0],[0,1],[1,1],[1,2]],
    [[0,0],[0,1],[1,0],[1,1]],
  ]
  private blockShap = [
    [[0,0],[0,1],[0,2],[0,3]],
    [[0,0],[0,1],[0,2],[1,2]],
    [[0,0],[0,1],[1,1],[0,2]],
    [[0,0],[0,1],[1,1],[1,2]],
    [[0,0],[0,1],[1,0],[1,1]],
  ]
  private blockmap = [];
  private curBlockShap = []
  private storeBlock = []
  private sideStartX = 300;
  private sideStartY = 150;
  private startX = 20
  private startY = 300

  private slotStartX = 120;
  private slotStartY = 150;
  private slotBottomY = 150;
  private xleft = 0;
  private rightX = 0;

  private score = 0;
  private step = 0;
  private direction = 0;
  private moveAction = 0;
  private state = 0;

  aboutToDisappear() {
  }

  aboutToAppear() {
    this.initMap()
    this.sleep(1000)
  }

  initMap() {
    for (let i=0;i<23;i++) {
      let item = []
      for (let j=0;j<9;j++) {
        item.push(0)
      }
      this.blockmap.push(item)
    }
  }

  async sleep(ms: number) {
    return new Promise((r) => {
      setInterval(() => {
//        console.log(this.message)
        if (this.state == 0) {
          this.drawStep()
        }
      }, ms)
    })
  }

  doClick() {
    if (this.state == 0) {
      this.direction += 1
    } else if (this.state == 2) {
      this.state = 0
      this.score = 0
      this.storeBlock = []
      this.initMap()
    }
  }

  drawBox() {
    this.context.lineWidth = 4
    this.context.beginPath()
    this.context.lineCap = 'butt'
    this.context.moveTo(0, 100)
    this.context.lineTo(270, 100)
    this.context.moveTo(270, 100)
    this.context.lineTo(270, 690)
    this.context.moveTo(0, 690)
    this.context.lineTo(270, 690)
  }

  setDirection() {
    this.curBlockShap = this.blockShap[this.blockType]
    if (this.direction > 0) {
      for (let i=0;i<4;i++) {
        let x = this.curBlockShap[i][0]
        this.curBlockShap[i][0] = this.curBlockShap[i][1]
        this.curBlockShap[i][1] = x
      }
      this.direction = 0
    }
  }

  drawSideBlock() {
    this.context.fillStyle = 'rgb(250,0,0)'
    let bs = this.blockSize
    let coords = this.blockShapBasic[this.blockType]
    for (let i=0;i<4;i++) {
      let x = this.sideStartX + coords[i][0]*this.blockSize
      let y = this.sideStartY + coords[i][1]*this.blockSize
      this.context.fillRect(x, y, bs, bs)
      this.context.rect(x, y, bs, bs)
//      console.info("x,y"+x.toString()+":"+y.toString())
    }
    this.context.stroke()
  }

  drawBoxBlock() {
    let min = 690
    let max = 0
    this.setDirection()
    this.context.fillStyle = 'rgb(250,0,0)'
    let bs = this.blockSize
    let coords = this.curBlockShap
    let startx = this.slotStartX + this.moveAction * this.blockSize
    let starty = this.slotStartY + this.step * this.blockSize
    for (let i=0;i<4;i++) {
      let x = startx + coords[i][0]*this.blockSize
      let y = starty + coords[i][1]*this.blockSize
      min = min > x ? x:min
      max = max < x ? x:max
      this.context.fillRect(x, y, bs, bs)
      this.context.rect(x, y, bs, bs)
//      console.info("x,y"+x.toString()+":"+y.toString())
      this.slotBottomY = y
      this.xleft = min
      this.rightX = max
    }
    this.context.stroke()
//    console.info("min,max"+min.toString()+":"+max.toString())
  }

  stackBlock() {
    let block = []
    let coords = this.curBlockShap
    let startx = this.slotStartX + this.moveAction * this.blockSize
    let starty = this.slotStartY + this.step * this.blockSize
    for (let i=0;i<4;i++) {
      let x = startx + coords[i][0]*this.blockSize
      let y = starty + coords[i][1]*this.blockSize
      console.info("stackBlock x:"+x.toString()+"y:"+y.toString())
      let indexX = x/this.blockSize
      let indexY = y/this.blockSize
      this.blockmap[indexY][indexX] = 1
      console.info("stackBlock:"+indexX+":"+indexY)
      block.push([x,y])
    }
    this.storeBlock.push(block)

    console.info("stackBlock:"+this.storeBlock.length.toString())
  }

  checkBlockmap() {
    if (this.storeBlock.length == 0) {
      if (this.slotBottomY >= 660) {
        return 1
      }
    } else {
      let coords = this.curBlockShap
      let startx = this.slotStartX + this.moveAction * this.blockSize
      let starty = this.slotStartY + this.step * this.blockSize
      for (let i=0;i<4;i++) {
        let x = startx + coords[i][0]*this.blockSize
        let y = starty + coords[i][1]*this.blockSize + this.blockSize
        for (let k=0;k<22;k++) {
          for (let l=0;l<9;l++) {
            if (this.blockmap[k][l] == 1) {
              let blocky = k * this.blockSize
              let blockx = l * this.blockSize
              //判断是否到底
              if ((x == blockx && y == blocky) || y > 660) {
                //判断是否到顶
                if (y == 210) {
                  this.context.drawImage( this.gameoverImg,this.startX,this.startY,300,90)
                  //到顶回2
                  return 2
                }
                //到底回1
                return 1
              }
            }
          }
        }
      }
    }
    return 0
  }

  cleanBlockmap() {
    //检查是否一行满了
    let needMove = 0
    for (let i=22;i>=0;i--) {
      let linecnt = 0
      for (let j = 8;j >= 0; j--) {
        //是否有方块
        if (this.blockmap[i][j] == 1) {
          linecnt += 1
        }
      }
      if (linecnt == 9) {
        //此行都是方块,消除,计分
        for (let j = 8;j >= 0; j--) {
          this.blockmap[i][j] = 0
        }
        needMove += 1
        this.score += 1
      } else if (needMove > 0) {
        for (let j = 8;j >= 0; j--) {
          this.blockmap[i+needMove][j] = this.blockmap[i][j]
          this.blockmap[i][j] = 0
        }
      }
    }
  }

  drawBlockmap() {
    let bs = this.blockSize
    this.context.fillStyle = 'rgb(250,0,0)'
    for (let i=0;i<23;i++) {
      for (let j=0;j<9;j++) {
        //是否有方块
        if (this.blockmap[i][j] == 1) {
          let y = i * this.blockSize
          let x = j * this.blockSize
          this.context.fillRect(x, y, bs, bs)
          this.context.rect(x, y, bs, bs)
          console.info("drawBlockmap:"+x.toString()+":"+y.toString())
        }
      }
      this.context.stroke()
      console.info("drawBlockmap:"+this.storeBlock.length.toString())
    }
  }

  drawScore() {
    this.context.fillStyle = 'rgb(0,0,0)'
    this.context.font = '80px sans-serif'
    this.context.fillText("Score:"+this.score.toString(), 20, 140)
  }

  randomType() {
    this.blockType = Math.floor(Math.random()*5)
    console.info("blocktype:"+this.blockType.toString())
  }

  drawStep() {
    this.context.clearRect(0,0,this.context.width,this.context.height)
    this.step += 1
    this.drawBox()
    this.drawBlockmap()
    this.cleanBlockmap()
    this.drawSideBlock()
    this.drawBoxBlock()
    this.drawScore()
    let stepType = this.checkBlockmap()
    if ( stepType == 1) {
      this.stackBlock()
      this.step = 0
      this.randomType()
    } else if (stepType == 2) {
      this.state= 2
      this.context.drawImage( this.gameoverImg,this.startX,this.startY,300,90)
    }
  }

  drawall() {
    this.drawBox()
    this.drawSideBlock()
    this.drawBoxBlock()
    this.drawScore()
  }

  build() {
    Column() {
      Column() {
        Canvas(this.context)
          .width('100%')
          .height('100%')
          .onClick((ev: ClickEvent) => {
            console.info("click!!")
            this.doClick()
          })
          .onTouch((ev) => {
            console.info("touch:"+ev.type.toString())
            console.info("touch x:"+ev.touches[0].screenX.toString())
            console.info("touch y:"+ev.touches[0].screenY.toString())
          })

          .onReady(() =>{
            this.context.imageSmoothingEnabled = false
            this.randomType()
            this.drawall()
          })
      }
      .height('92%')
      .width('100%')
      Row() {
        Button() {    //按钮控件
          Text('左')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
        }.type(ButtonType.Capsule)
        .width('20%')
        .height('6%')
        .backgroundColor('#0D9FFB')
        .onClick(() => {    //点击事件
          if (this.xleft > 0) {
            this.moveAction -= 1
          }
        })
        Button() {    //按钮控件
          Text('右')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
        }.type(ButtonType.Capsule)
        .width('20%')
        .height('6%')
        .backgroundColor('#0D9FFB')
        .onClick(() => {    //点击事件
          if (this.rightX < 240) {
            this.moveAction += 1
          }
        })
      }

    }
    .width('100%')
    .height('100%')
    .backgroundColor("#cccccc")
  }
}

遗留问题:

  1. 有bug,方块变形不完整;

  2. 可实现网络对战(分布式对战);

5. 获取源码

等游戏完整发布,会有两个版本,单机和联机版本


本文主要介绍了小游戏的开发,画布功能的使用,游戏逻辑,分布式

本文作者: 左翼风发

 想了解更多关于开源的内容,请访问:​

 ​51CTO 开源基础软件社区​

 ​https://ost.51cto.com/#bkwz​


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK