使用vue3写一个超大号俄罗斯方块

前几天写了一个贪吃蛇的demo,最近两天很闲还在继续摸鱼。所以又做了一个俄罗斯方块

可以自由设置大小,带下落的辅助线

方块下落位置在横坐标是随机的,方块可以左右互相跨越瞬移,可以存储到localStorage下次继续游戏

用二维数组控制场地的渲染

可以把地图看成是一个二维的数组,然后用不同的数字表示不同的方块,如果方块已经落地,就变为负数,判断方块触底和判断方块移动的时候左右两边有没有方块就是通过方块的正数,和已经触底的负数做对比,所以数组的最下面多加了一个全是-1的数组,用来表示底部

css 复制代码
[	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[-6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
	[-6, -6, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -2, -2, -2],
	[-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1]
]

7种俄罗斯方块的形状

地图用二维数组表示,那么方块同样也用二维数组表示 俄罗斯方块一共有7种

分别是O,I,S,Z,L,J,T型

比如这两个,代表了J型和T型

csharp 复制代码
6: {
  name: 'J型',
  backgroundColor: '#FFC0CB',
  data: [
    [0, 6],
    [0, 6],
    [6, 6]
  ]
},
7: {
  name: 'T型',
  backgroundColor: '#BA55D3',
  data: [
    [7, 7, 7],
    [0, 7, 0]
  ]
}

方块的变形

方块可以旋转变形,本来想把7种变形的方式全部穷举出来,但是考虑到有些形状变形方式有4种,比如T型,但是有些只有一种,比如I型,所以这里用了一种矩阵旋转的算法

ini 复制代码
// 设置方块转动
  const getBlockTurn = (block: any) => {
    return rotateMatrix(block)
  }

  const rotateMatrix = (matrix: any) => {
    const rows = matrix.length
    const cols = matrix[0].length
    const transposed: any = []
    for (let i = 0; i < cols; i++) {
      transposed.push([])
      for (let j = 0; j < rows; j++) {
        transposed[i][j] = matrix[j][i]
      }
    }
    const rotated = transposed.map((row: any) => row.reverse())
    return rotated
  }

  return { getBlock, getRandomNumber, getBlockTurn }

这里把和获取方块变形方块相关的逻辑放在组合式函数里,使用的时候直接传入方块获取选择之后的方块

方块的移动

做的时候方块向下移动是最让我头疼的,思索了一天,最终做了出来但是感觉方法不是很理想,思路是循环方块的二维数组,然后通过递增向下的fallIndex,循环插入到地图的二维数组中

ini 复制代码
// 每次下落clone一遍
    const cloneSourceCodes = JSON.parse(JSON.stringify(cloneArr))
    const len = 4 - block.value.length // 4代表方块的底部从第几层开始下落
    // 反着遍历落块的每一层,从第4层对齐
    for (let i = block.value.length - 1; i >= 0; i--) {
      const nowRow = cloneSourceCodes[fallIndex.value + len + i]
      // 方块的每一层遍历一遍,不等于0的插入
      for (let j = 0; j < block.value[i].length; j++) {
        if (block.value[i][j] !== 0) {
          nowRow.splice(start.value + j, 1, block.value[i][j])
        }
      }
    }
    // 检测是否碰撞
    for (let i = 0; i < cloneSourceCodes.length; i++) {
      if (isCollisions(cloneSourceCodes[i], cloneSourceCodes[i + 1])) {
        sourceCodes.value = SetAlreadyFallen(cloneSourceCodes)
        clearInterval(timer)
        newBegin()
        return
      }
    }

碰撞的逻辑

判断方块和底部接触,方块和方块直接碰撞,或者左右移动的时候左右两边的方块阻挡 通过方块的正数,和负数来对比

typescript 复制代码
// 检测碰撞
  const isCollisions = (nowRow: any, nextRow: any) => {
    //  如果这一行不为0的位置的下一行,是负数,说明有东西会碰撞
    for (let i = 0; i < nowRow.length; i++) {
      if (nowRow[i] > 0 && nextRow[i] < 0) {
        return true
      }
    }
    return false
  }

方块的左右移动

方块下落的时候,我没有设置从中间下落,而是从顶部的随机位置下落

向下移动的时候,是通过一个fallIndex来控制,左右移动的时候,通过startIndex来控制

这里还加了一个isTeleporting的配置,代表向左移动到边缘的时候是否能瞬移到右边

ini 复制代码
const setLeftRight = (direction: 'left' | 'right') => {
    // 判断左右两边是否有方块 或者左下角或者右下角有方块
    for (let i = 0; i < sourceCodes.value.length; i++) {
      for (let j = 0; j < sourceCodes.value[i].length; j++) {
        if (
          (sourceCodes.value[i][j] > 0 &&
            (sourceCodes.value[i][j - 1] < 0 ||
              sourceCodes.value[i + 1][j - 1] < 0) &&
            direction === 'left') ||
          (sourceCodes.value[i][j] > 0 &&
            (sourceCodes.value[i][j + 1] < 0 ||
              sourceCodes.value[i + 1][j + 1] < 0) &&
            direction === 'right')
        ) {
          return
        }
      }
    }
    // 是否左右瞬移
    if (isTeleporting) {
      if (start.value === maxStart.value && direction === 'right') {
        start.value = 0
      } else if (start.value === 0 && direction === 'left') {
        start.value = maxStart.value
      } else {
        start.value += direction === 'left' ? -1 : 1
      }
    } else {
      if (direction === 'left' && start.value > 0) {
        start.value--
      }
      if (direction === 'right' && start.value < maxStart.value) {
        start.value++
      }
    }
    moveBlockDown()
  }

方块向下加速

方块下落加速的时候,先清除定时器,然后给定时器函数传入一个小点的数值

scss 复制代码
const fallBlock = () => {
    clearInterval(timer)
    startBlockFalling(10)
  }
  
  const startBlockFalling = (changeDelay?: number) => {
    timer = setInterval(() => {
      moveBlockDown()
      fallIndex.value++
    }, changeDelay || delay.value)
  }

消除整行

typescript 复制代码
const clearFilledRows = () => {
    const rows = cloneArr[0].length
    const upArr = cloneArr.slice(0, -1)
    const filledRows = upArr.filter((row: any) =>
      row.every((cell: any) => cell !== 0)
    )
    const filterArr = upArr.filter((row: any) => row.includes(0))
    if (filledRows.length > 0) {
      scoreRows.value += filledRows.length
      const emptyRows = filledRows.map(() => Array(rows).fill(0))
      filterArr.unshift(...emptyRows)
    }
    const bottom = Array(rows).fill(-1)
    cloneArr = [...filterArr, bottom]
    const gameover = cloneArr[4].find((row: any) => row < 0)
    if (gameover) {
      isGameOver.value = true
    }
    sourceCodes.value = cloneArr
    return Promise.resolve(cloneArr)
  }

完整代码

目录结构

首页

index.tsx 复制代码
import {
  defineComponent
  onMounted,
  computed
} from 'vue'
import './index.scss'
import config from './config'
import { setGameInterface } from './helpers/setGameInterface'

import { useGetBlock } from './hooks/useGetBlock'
import { useHandleCode } from './hooks/useHandleCode'

export default defineComponent({
  components: {},
  props: {},
  emits: [''],
  setup() {
    // 设置视图宽高css
    const { viewStyle } = setGameInterface()

    // 获取配置
    const { isShowTop, sourceBlockTypes, isAuxiliaryShow } = config

    // 获取随机方块
    const { getBlock, getBlockTurn } = useGetBlock()

    // 操作二维地图数组源码
    const {
      scoreRows,
      isGameOver,
      newStartGame,
      sourceCodes,
      nextBlock,
      start
    } = useHandleCode(getBlock, getBlockTurn)

    onMounted(() => {})

    // 设置下一个显示
    const nextBlockWrap = computed(() => {
      if (nextBlock.value && nextBlock.value) {
        const result = JSON.parse(JSON.stringify(nextBlock.value))
        if (result && result[0]) {
          const rows = result[0].length
          console.log(rows)
          result.forEach((item: any) => {
            if (rows === 1) {
              item.push(...Array(2).fill(0))
              item.unshift(...Array(1).fill(0))
            } else if (rows === 2) {
              item.push(...Array(1).fill(0))
              item.unshift(...Array(1).fill(0))
            } else {
              item.push(...Array(4 - rows).fill(0))
            }
          })
        }
        if (result.length === 2) {
          result.push(...Array(1).fill(Array(4).fill(0)))
          result.unshift(...Array(1).fill(Array(4).fill(0)))
        } else {
          result.push(...Array(4 - result.length).fill(Array(4).fill(0)))
        }

        return result
      } else {
        return []
      }
    })

    return () => {
      return (
        <div class="tetris-wrap">
          <div class="tetris-content" style={[viewStyle]}>
            <div v-show={isGameOver.value}>
              <h3>游戏结束</h3>
              <p>已消除:{scoreRows.value}行</p>
              <el-button
                onClick={() => {
                  newStartGame()
                }}
              >
                重新开始
              </el-button>
            </div>
            {sourceCodes.value.map((column: any, index: number) => {
              return (
                <div
                  class={`tetris-column column-${index}`}
                  style={[
                    `${index > 0 ? 'border-top:1px solid #ccc' : ''}`,
                    `${index < 4 ? 'background-color:red' : ''}`
                  ]}
                  v-show={
                    (isShowTop || (!isShowTop && index > 3)) &&
                    index !== sourceCodes.value.length - 1
                  }
                >
                  {/* 行 */}
                  {column.map((cell: any, cellIndex: number) => {
                    return (
                      <div
                        class={`tetris-cell cell-${Math.abs(cell)}`}
                        style={[
                          `background-color:${
                            sourceBlockTypes[Math.abs(cell)].backgroundColor
                          }`,
                          `${
                            cellIndex > 0 ? 'border-left:1px solid #DCDCDC' : ''
                          }`,
                          `${
                            cellIndex === start.value && isAuxiliaryShow
                              ? 'border-left:1px dashed #696969'
                              : ''
                          }`
                        ]}
                      >
                        {/* {cell} */}
                      </div>
                    )
                  })}
                </div>
              )
            })}
          </div>
          <div class="msg" style="margin-left:10px">
            <p>已消除:{scoreRows.value}行</p>
            <p>下一个</p>
            <div class="tetris-msg">
              {nextBlockWrap.value.map((column: any, index: number) => {
                return (
                  <div class={`tetris-column column-${index}`}>
                    {/* 行 */}
                    {column.map((cell: any) => {
                      return (
                        <div
                          class={`tetris-cell cell-${Math.abs(cell)}`}
                          style={[
                            `background-color:${
                              sourceBlockTypes[Math.abs(cell)].backgroundColor
                            }`
                          ]}
                        >
                          {/* {cell} */}
                        </div>
                      )
                    })}
                  </div>
                )
              })}
            </div>
          </div>
        </div>
      )
    }
  }
})

配置

config.ts 复制代码
export default {
  gridWidth: 20,
  gridHeight: 40,
  isRandomFall: true, // 是否是从顶部随机位置下落
  isShowTop: false, // 是否显示上面四格,显示初始位置的地方,游戏的时候去掉
  isTeleporting: true, // 是否允许瞬移,即移动到最左边从右边出现
  isAuxiliaryShow: true, // 是否显示辅助线
  isStore: true, // 是否存储
  sourceBlockTypes: {
    0: {
      name: '',
      backgroundColor: '',
      data: []
    },
    1: {
      name: 'O型',
      backgroundColor: '#FFD700',
      data: [
        [1, 1],
        [1, 1]
      ]
    },
    2: {
      name: 'I型',
      backgroundColor: '#1E90FF',
      data: [[2], [2], [2], [2]]
    },
    3: {
      name: 'S型',
      backgroundColor: '#DC143C',
      data: [
        [0, 3, 3],
        [3, 3, 0]
      ]
    },
    4: {
      name: 'Z型',
      backgroundColor: '#3CB371',
      data: [
        [4, 4, 0],
        [0, 4, 4]
      ]
    },
    5: {
      name: 'L型',
      backgroundColor: '#DAA520',
      data: [
        [5, 0],
        [5, 0],
        [5, 5]
      ]
    },

    6: {
      name: 'J型',
      backgroundColor: '#FFC0CB',
      data: [
        [0, 6],
        [0, 6],
        [6, 6]
      ]
    },
    7: {
      name: 'T型',
      backgroundColor: '#BA55D3',
      data: [
        [7, 7, 7],
        [0, 7, 0]
      ]
    }
  }
}

获取随机方块、方块转动

useGetBlock.ts 复制代码
import { ref } from 'vue'
import config from '../config'
export const useGetBlock = () => {
  const block: any = ref([])
  // 获取方块类型
  const { sourceBlockTypes } = config
  // 获取随机数范围
  const getRandomNumber = (min: number, max: number): number => {
    return Math.floor(Math.random() * (max - min + 1)) + min
  }
  // 获取随机方块
  const getRandomBlocks = () => {
    const blocks = Object.keys(sourceBlockTypes)
    const key = getRandomNumber(1, blocks.length - 1)
    const block = sourceBlockTypes[key]
    return block.data
  }

  const getBlock = () => {
    block.value = getRandomBlocks()
    return block.value
  }

  // 设置方块转动
  const getBlockTurn = (block: any) => {
    return rotateMatrix(block)
  }

  const rotateMatrix = (matrix: any) => {
    const rows = matrix.length
    const cols = matrix[0].length
    const transposed: any = []
    for (let i = 0; i < cols; i++) {
      transposed.push([])
      for (let j = 0; j < rows; j++) {
        transposed[i][j] = matrix[j][i]
      }
    }
    const rotated = transposed.map((row: any) => row.reverse())
    return rotated
  }

  return { getBlock, getRandomNumber, getBlockTurn }
}

方块的移动操作

useHandleCode.ts 复制代码
import { ref, onMounted, watch } from 'vue'
import config from '../config'
import { useGetBlock } from './useGetBlock'
import { useKeyDown } from './useKeyDown'
import { useSetSourceCode } from './useSetSourceCode'

export const useHandleCode = (
  getBlock: any,
  getBlockTurn: any
  // sourceCodes: Ref<any>
) => {
  // 获取键盘事件
  const { onKeyDown } = useKeyDown()

  // 键盘操作
  onKeyDown((direction: 'left' | 'right' | 'up' | 'down' | 'stop' | '') => {
    if (direction === 'up') {
      // 变形
      changeBlock()
    }
    if (direction === 'left' || direction === 'right') {
      // 左右移动
      setLeftRight(direction)
    }
    if (direction === 'down') {
      // 快速下落
      fallBlock()
    }
    // 暂停
    if (direction === 'stop') {
      if (timer) {
        clearInterval(timer)
        timer = undefined
      } else {
        startBlockFalling()
      }
    }
  })

  // 获取源码
  const { sourceCodes, getNewSourceCodes } = useSetSourceCode()

  // 获取配置
  const { isTeleporting, isStore } = config

  // 获取范围随机数
  const { getRandomNumber } = useGetBlock()

  const fallIndex = ref(0) // 下落的步进索引
  const delay = ref(400) // 下落速度
  const scoreRows = ref(0) // 消除的行数
  const isGameOver = ref(false)
  let timer: any = null

  // 设置游戏难度
  watch(
    () => scoreRows.value,
    () => {
      if (delay.value > 50) {
        delay.value = 400 - scoreRows.value
      }
    }
  )

  const block = ref(getBlock())
  const nextBlock = ref([])

  const maxStart = ref(sourceCodes.value[0].length - block.value[0].length) // 插入的最大位置
  const start = ref(getRandomNumber(0, maxStart.value)) // 设置插入的随机范围

  // 获取存储的数据,继续上次游戏
  let cloneArr: any
  const storedSourceCodes = localStorage.getItem('sourceCodes')
  if (storedSourceCodes && isStore) {
    sourceCodes.value = JSON.parse(storedSourceCodes)
  }
  cloneArr = JSON.parse(JSON.stringify(sourceCodes.value))

  /**
   * @description: 游戏失败后重新开始游戏
   * @Date: 2023-11-24 10:08:25
   * @Author: zengzhaoyan
   */
  const newStartGame = () => {
    getNewSourceCodes()
    cloneArr = JSON.parse(JSON.stringify(sourceCodes.value))
    isGameOver.value = false
    newBegin()
  }

  /**
   * @description: 重新开始下落方块
   * @Date: 2023-11-22 16:35:28
   * @Author: zengzhaoyan
   */
  const newBegin = async () => {
    clearInterval(timer)
    cloneArr = JSON.parse(JSON.stringify(sourceCodes.value))
    await clearFilledRows()
    if (isGameOver.value) {
      return
    }
    const isempty = cloneArr[cloneArr.length - 2].every((row: any) => row === 0)
    if (!isempty) {
      localStorage.setItem('sourceCodes', JSON.stringify(cloneArr))
    }
    fallIndex.value = 0
    if (nextBlock.value.length) {
      block.value = nextBlock.value
    } else {
      block.value = getBlock()
    }
    nextBlock.value = getBlock()
    // console.log(nextBlock.value)
    maxStart.value = sourceCodes.value[0].length - block.value[0].length
    start.value = getRandomNumber(0, maxStart.value)
    startBlockFalling()
  }

  /**
   * @description: 设置方块自动下落定时函数
   * @Date: 2023-11-22 15:14:34
   * @Author: zengzhaoyan
   */
  const startBlockFalling = (changeDelay?: number) => {
    timer = setInterval(() => {
      moveBlockDown()
      fallIndex.value++
    }, changeDelay || delay.value)
  }

  onMounted(newBegin)

  /**
   * @description: 清除行
   * @Date: 2023-11-23 17:28:56
   * @Author: zengzhaoyan
   */
  const clearFilledRows = () => {
    const rows = cloneArr[0].length
    const upArr = cloneArr.slice(0, -1)
    const filledRows = upArr.filter((row: any) =>
      row.every((cell: any) => cell !== 0)
    )
    const filterArr = upArr.filter((row: any) => row.includes(0))
    if (filledRows.length > 0) {
      scoreRows.value += filledRows.length
      const emptyRows = filledRows.map(() => Array(rows).fill(0))
      filterArr.unshift(...emptyRows)
    }
    const bottom = Array(rows).fill(-1)
    cloneArr = [...filterArr, bottom]
    const gameover = cloneArr[4].find((row: any) => row < 0)
    if (gameover) {
      isGameOver.value = true
    }
    sourceCodes.value = cloneArr
    return Promise.resolve(cloneArr)
  }

  /**
   * @description: 设置方块位置
   * @Date: 2023-11-23 14:42:23
   * @Author: zengzhaoyan
   */
  const moveBlockDown = () => {
    // 每次下落clone一遍
    const cloneSourceCodes = JSON.parse(JSON.stringify(cloneArr))
    const len = 4 - block.value.length // 4代表方块的底部从第几层开始下落
    // 反着遍历落块的每一层,从第4层对齐
    for (let i = block.value.length - 1; i >= 0; i--) {
      const nowRow = cloneSourceCodes[fallIndex.value + len + i]
      // 方块的每一层遍历一遍,不等于0的插入
      for (let j = 0; j < block.value[i].length; j++) {
        if (block.value[i][j] !== 0) {
          nowRow.splice(start.value + j, 1, block.value[i][j])
        }
      }
    }
    // 检测是否碰撞
    for (let i = 0; i < cloneSourceCodes.length; i++) {
      if (isCollisions(cloneSourceCodes[i], cloneSourceCodes[i + 1])) {
        sourceCodes.value = SetAlreadyFallen(cloneSourceCodes)
        clearInterval(timer)
        newBegin()
        return
      }
    }
    sourceCodes.value = cloneSourceCodes
  }

  // 检测碰撞
  const isCollisions = (nowRow: any, nextRow: any) => {
    //  如果这一行不为0的位置的下一行,是负数,说明有东西会碰撞
    for (let i = 0; i < nowRow.length; i++) {
      if (nowRow[i] > 0 && nextRow[i] < 0) {
        return true
      }
    }
    return false
  }

  // 已经落下的方块设置为负数和正在下落的区分开
  const SetAlreadyFallen = (arr: any) => {
    return arr.map((row: any) =>
      row.map((value: any) => (value !== 0 ? -Math.abs(value) : 0))
    )
  }

  /**
   * @description: 设置方块左右移动
   * @Date: 2023-11-22 14:51:01
   * @Author: zengzhaoyan
   * @param {*} direction
   */
  const setLeftRight = (direction: 'left' | 'right') => {
    // 判断左右两边是否有方块 或者左下角或者右下角有方块
    for (let i = 0; i < sourceCodes.value.length; i++) {
      for (let j = 0; j < sourceCodes.value[i].length; j++) {
        if (
          (sourceCodes.value[i][j] > 0 &&
            (sourceCodes.value[i][j - 1] < 0 ||
              sourceCodes.value[i + 1][j - 1] < 0) &&
            direction === 'left') ||
          (sourceCodes.value[i][j] > 0 &&
            (sourceCodes.value[i][j + 1] < 0 ||
              sourceCodes.value[i + 1][j + 1] < 0) &&
            direction === 'right')
        ) {
          return
        }
      }
    }
    // 是否左右瞬移
    if (isTeleporting) {
      if (start.value === maxStart.value && direction === 'right') {
        start.value = 0
      } else if (start.value === 0 && direction === 'left') {
        start.value = maxStart.value
      } else {
        start.value += direction === 'left' ? -1 : 1
      }
    } else {
      if (direction === 'left' && start.value > 0) {
        start.value--
      }
      if (direction === 'right' && start.value < maxStart.value) {
        start.value++
      }
    }
    moveBlockDown()
  }

  /**
   * @description: 切换方块形状
   * @Date: 2023-11-22 14:50:18
   * @Author: zengzhaoyan
   */
  const changeBlock = () => {
    // clearInterval(timer)
    block.value = getBlockTurn(block.value)
    // 重新计算右边边界
    maxStart.value = sourceCodes.value[0].length - block.value[0].length
    moveBlockDown()
  }

  /**
   * @description: 快速下落方块
   * @Date: 2023-11-22 14:50:51
   * @Author: zengzhaoyan
   */
  const fallBlock = () => {
    clearInterval(timer)
    startBlockFalling(10)
  }

  return {
    start,
    block,
    sourceCodes,
    isGameOver,
    scoreRows,
    newStartGame,
    nextBlock
  }
}

设置游戏源码

useSetSourceCode.ts 复制代码
import config from '../config'
import { ref } from 'vue'

export const useSetSourceCode = () => {
  const { gridWidth, gridHeight } = config
  const sourceCodes: any = ref([])

  const getNewSourceCodes = () => {
    sourceCodes.value = []
    for (let i = 0; i < gridHeight; i++) {
      sourceCodes.value.push([])
      for (let j = 0; j < gridWidth; j++) {
        sourceCodes.value[i].push(0)
      }
    }
    // 添加一层负数垫底
    sourceCodes.value.push(Array(gridWidth).fill(-1))
  }

  getNewSourceCodes()

  return {
    getNewSourceCodes,
    sourceCodes
  }
}

设置游戏地图画面,布局

setGameInterface.ts 复制代码
import config from '../config'
import { useMain } from '@/hooks/useMain'

export const setGameInterface = () => {
  const { gridWidth, gridHeight, isShowTop } = config

  const height = isShowTop ? gridHeight : gridHeight - 4 // 减去4格不显示

  const { mainHeight } = useMain()

  return {
    viewHeight: mainHeight.value,
    viewWidth: (mainHeight.value / height) * gridWidth,
    viewStyle: `width:${(mainHeight.value / height) * gridWidth}px;height:${
      mainHeight.value
    }px;`
  }
}

键盘事件

useKeyDown.ts 复制代码
import { onMounted, onUnmounted } from 'vue'
type Direction = 'left' | 'right' | 'up' | 'down' | 'stop' | ''

export const useKeyDown = () => {
  const onKeyDown = (callback: (direction: Direction) => void) => {
    // 键盘操作
    const handleKeyPress = (event: KeyboardEvent) => {
      if (['w', 'a', 's', 'd', 'r'].includes(event.key)) {
        const keyMap: Record<string, Direction> = {
          w: 'up',
          a: 'left',
          s: 'down',
          d: 'right',
          r: 'stop'
        }
        const direction: Direction = keyMap[event.key] || ''
        callback(direction)
      }
    }
    
    onMounted(() => {
      window.addEventListener('keydown', handleKeyPress)
    })
    
    onUnmounted(() => {
      window.removeEventListener('keydown', handleKeyPress)
    })
  }
  return {
    onKeyDown
  }
}

样式

index.scss 复制代码
.tetris-wrap {
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 1;
  
  .tetris-content,
  .tetris-msg {
    border: 1px solid #000000;
    display: flex;
    flex-direction: column;
    .tetris-column {
      flex: 1;
      display: flex;
      flex-direction: row;
      .tetris-cell {
        flex: 1;
      }
    }
  }
  .tetris-msg {
    width: 100px;
    height: 100px;
    border: none;
  }
}
相关推荐
Komorebi⁼1 分钟前
Vue核心特性解析(内含实践项目:设置购物车)
前端·javascript·vue.js·html·html5
明月清风徐徐1 分钟前
Vue实训---0-完成Vue开发环境的搭建
前端·javascript·vue.js
daopuyun5 分钟前
LoadRunner小贴士|开发Web-HTTP/HTML协议HTML5相关视频应用测试脚本的方法
前端·http·html
李先静8 分钟前
AWTK-WEB 快速入门(1) - C 语言应用程序
c语言·开发语言·前端
MR·Feng17 分钟前
使用Electron将vue2项目打包为桌面exe安装包
前端·javascript·electron
萧大侠jdeps29 分钟前
图片生成视频-右进
前端·javascript·音视频
CSDN专家-赖老师(软件之家)32 分钟前
养老院管理系统+小程序项目需求分析文档
vue.js·人工智能·小程序·mybatis·springboot
Domain-zhuo1 小时前
JS对于数组去重都有哪些方法?
开发语言·前端·javascript
明月清风徐徐1 小时前
Vue实训---2-路由搭建
前端·javascript·vue.js
王解1 小时前
速度革命:esbuild如何改变前端构建游戏 (1)
前端·vite·esbuild