游戏名称:清凉一夏消消乐
技术栈:Vue3 + TypeScript + Vite + Element-Plus
游戏体验地址(pc/手机皆可):https://wmuhua.com/games/xxl
开源地址:https://github.com/wmuhua/vue...

点赞留香,与有荣焉,感激感激

游戏介绍

先看一下

好吧,我晓得界面有点丑 →_→

外围思路

游戏步骤次要就是:打消、着落、补充、挪动,采纳三种状态来辨别须要删除的(remove)、新增加的(add)、和失常的方块(normal)

  • 次要就是生成小方块列表后,马上保留每一个方块上下左右方块的信息
  • 而后判断每一个方块和高低或和左右类型雷同即为须要打消,并把该方块状态改为 remove
  • 而后通过扭转 topleft 来管制着落,同时要把打消的地位上移,这样补充的时候能力在对应空位上显示,这里专门用了一个矩阵来保留所有对应格子信息,辨别出哪些格子是须要打消/补充的
  • 挪动就比较简单了,因为每个方块上都保留了本人的上下左右信息,所以只须要替换就行了

有一个坑,就是 key,因为 diff 算法的起因,不须要从新渲染就要保障key是惟一的,比方着落的也从新渲染视觉效果会很奇怪

外围代码

html

以下是矩阵区域所有html,就是用一个div来做的,依据类型给不同类名,而后雪糕全是背景图片

<div class="stage">  <div    v-for="item in data"    :style="{      left: `${item.positionLeft}px`,      top: `${item.positionTop}px`,    }"    :key="item.key"    :class="[      'square',      `type${item.type}`,      `scale${item.scale}`,      { active: item.active },    ]"    @click="handleClick(item)"  ></div></div>

js

js 局部次要是封装了一个类,不便对立治理操作

export default class Stage implements IXXL {  x: number // x和y 是游戏舞台行列方块个数  y: number  size: number // 方块大小  typeCount = 7 // 方块类型个数  matrix: Array<any> = [] // 方块矩阵,用于每次打消之后依据矩阵规定生成新的游戏棋盘  data: Array<any> = [] // 用于渲染页面  isHandle = false // 游戏是否正在打消/着落/增加解决中  isSelect = false // 是否有抉择  score = 0 // 分数  target1: any = { active: false } // 选中的方块  target2: any = {}  constructor(x: number, y: number, size: number) {    this.x = x    this.y = y    this.size = size    this.getMatrix() // 生成矩阵    this.init(true) // 生成 data 渲染用  }  getMatrix(){}  init(){}  // 循环执行  gameLoop(){}  // 点击  click(){}  // 换位  swap(){}  // 删除  remove(){}  // 着落  down(){}  // 补充  add(){}}

游戏开始/循环

// 要等动画执行完,所以用 awaitasync gameLoop(bool: boolean = false) {    // 完结游戏后从新开始时分数清0    if (bool) this.score = 0    // 游戏状态改为正在执行中,管制在动画执行过程中不能点击替换    this.isHandle = true    // 找出须要删除的    await this.remove()    // 用于检测点击替换后判断有没有须要删除的,没有就再换回来    let status = this.data.some((item) => item.status === "remove")    // 只有有删除了的,执行下面的着落、补充,补充后再循环找有没有能够删除的    while (this.data.some((item) => item.status === "remove")) {      await this.down()      await this.add()      await this.remove()    }    // 所有能删除的删除后,更改状态,而后就能够点击了    this.isHandle = false    return status}

删除

留神 状态为 remove 的理论没有删除,只是页面上看不到了,到补充的时候才会删除掉状态为 remove

// 革除remove() {    return new Promise((resolve, reject) => {      const { data } = this      data.forEach((item) => {        const { left, right, top, bottom, type } = item        // 如果本人 + 本人的左和右 类型都一样,状态变更为删除        if (left?.type == type && right?.type == type) {          left.status = "remove"          item.status = "remove"          right.status = "remove"        }        // 如果本人 + 本人的上和下 类型都一样,状态变更为删除        if (top?.type == type && bottom?.type == type) {          top.status = "remove"          item.status = "remove"          bottom.status = "remove"        }      })      setTimeout(() => {        // 执行删除动画,页面上看不到了,并统计分数,理论这时还没删除        data.forEach((item, index) => {          if (item.status === "remove") {            item.scale = 0            this.score += 1          }        })        // 这里提早100毫秒是首次进页面的时候,先看到格子有货色,不然会是空的      }, 100)      // 动画时长500毫秒 css 那边定义了,所以提早500毫秒      setTimeout(() => {        resolve(true)      }, 500)    })}

着落

这里有个坑。除了要把删除格子下面的着落下来之外,还须要把曾经删除(状态为删除,页面上看不到了的)的格子上位到,下面的空位上,否则,新增的格子会从上面冒出来

// 着落down() {    return new Promise((resolve, reject) => {      const { data, size, x, y } = this      data.forEach((item, index) => {        let distance = 0 // 挪动格数        if (item.status === "remove") {          // 删除的地位上移,调整新增格子的地位          let top = item.top          // 统计须要上移多少步          while (top) {            if (top.status !== "remove") {              distance += 1            }            top = top.top          }          // 上移          if (distance) {            item.y -= distance            item.positionTop = item.positionTop - size * distance          }        } else {          let bottom = item.bottom          // 统计须要着落多少步          while (bottom) {            if (bottom.status === "remove") {              distance += 1            }            bottom = bottom.bottom          }          // 着落          if (distance) {            item.y += distance            item.positionTop = item.positionTop + size * distance          }        }      })      setTimeout(() => {        resolve(true)      }, 500)    })}

增加

能够设想到,在着落执行完之后,页面中的矩阵,是所有格子都有的,只是看起来空的格子,实际上是删除格子在那占位,而后只有依据程序从新生成矩阵,并保留每个非remove格子的状态,是remove的就从新生成,达到替换补充的成果

// 增加add() {    return new Promise((resolve, reject) => {      const { size, matrix } = this      // 重置矩阵为空      this.getMatrix()      // 把以后所有格子信息保留为矩阵      this.matrix = matrix.map((row, rowIndex) =>        row.map((col: any, colIndex: number) => {          return this.data.find((item) => {            return colIndex == item.x && rowIndex == item.y          })        })      )      // 依据矩阵须要革除的地位替换新方块      this.init()      setTimeout(() => {        // 新增的格子执行动画        this.data.forEach((item) => {          if (item.status === "add") {            item.scale = 1            item.status = "normal"          }        })      }, 100)      // 动画完结      setTimeout(() => {        resolve(true)      }, 500)    })}

接下来前面的逻辑都比较简单了,没啥说的,都写在正文里了

生成矩阵/数据

// 生成全副为空的矩阵getMatrix() {    const { x, y } = this    const row = new Array(x).fill(undefined)    const matrix = new Array(y).fill(undefined).map((item) => row)    this.matrix = matrix}// 生成小方块init(bool: boolean = false) {    const { x, y, typeCount, matrix, size } = this    const data: Array<any> = []    // 这里用两个指针,没有用嵌套循环,缩小复杂度    let _x = 0    let _y = 0    for (let i = 0, len = Math.pow(x, 2); i < len; i++) {      let item      try {        item = matrix[_y][_x]      } catch (e) {}      // 依据矩阵信息来生成方块      let flag: boolean = item && item.status !== "remove"      // 每一个方块的信息      let obj = {        type: flag ? item.type : Math.floor(Math.random() * typeCount),        x: _x,        y: _y,        status: bool ? "normal" : flag ? "normal" : "add",        positionLeft: flag ? item.positionLeft : size * _x,        positionTop: flag ? item.positionTop : size * _y,        left: undefined,        top: undefined,        bottom: undefined,        right: undefined,        scale: bool ? 1 : flag ? 1 : 0,        key: item ? item.key + i : `${_x}${_y}`,        active: false,      }      data.push(obj)      _x++      if (_x == x) {        _x = 0        _y++      }    }    // 保留每个格子上下左右的格子信息    data.forEach((square) => {      square.left = data.find(        (item) => item.x == square.x - 1 && item.y == square.y      )      square.right = data.find(        (item) => item.x == square.x + 1 && item.y == square.y      )      square.top = data.find(        (item) => item.x == square.x && item.y == square.y - 1      )      square.bottom = data.find(        (item) => item.x == square.x && item.y == square.y + 1      )    })    this.data = data}

点击

// 点击小方块click(target: any) {    // 游戏动画正在解决中的时候,不给点击    if (this.isHandle) return    // console.log(target)    const { isSelect } = this    // 如果没有抉择过的    if (!isSelect) {      // 抉择第一个      target.active = true      this.target1 = target      this.isSelect = true    } else {      // 抉择第二个      if (this.target1 === target) return      this.target1.active = false      // 如果是相邻的      if (        ["left", "top", "bottom", "right"].some(          (item) => this.target1[item] == target        )      ) {        this.target2 = target        ;(async () => {          // 调换地位          await this.swap()          // 会返回一个有没有能够删除的,的状态          let res = await this.gameLoop()          // 没有就再次调换地位,还原          if (!res) {            await this.swap()          }        })()        this.isSelect = false      } else {        // 如果不是相邻的        target.active = true        this.target1 = target        this.isSelect = true      }    }}

换地位

这里的逻辑次要就是替换两个方块的地位信息,而后从新生成上下左右,就ok 了

// 换地位swap() {    return new Promise((resolve, reject) => {      const { target1, target2, data } = this      const { positionLeft: pl1, positionTop: pt1, x: x1, y: y1 } = target1      const { positionLeft: pl2, positionTop: pt2, x: x2, y: y2 } = target2      setTimeout(() => {        target1.positionLeft = pl2        target1.positionTop = pt2        target1.x = x2        target1.y = y2        target2.positionLeft = pl1        target2.positionTop = pt1        target2.x = x1        target2.y = y1        data.forEach((square) => {          square.left = data.find(            (item) => item.x == square.x - 1 && item.y == square.y          )          square.right = data.find(            (item) => item.x == square.x + 1 && item.y == square.y          )          square.top = data.find(            (item) => item.x == square.x && item.y == square.y - 1          )          square.bottom = data.find(            (item) => item.x == square.x && item.y == square.y + 1          )        })      }, 0)      setTimeout(() => {        resolve(true)      }, 500)    })}

结语

游戏名称:清凉一夏消消乐
技术栈:Vue3 + TypeScript + Vite + Element-Plus
游戏体验地址(pc/手机皆可):https://wmuhua.com/games/xxl
开源地址:https://github.com/wmuhua/vue...

点赞留香,与有荣焉,感激感激