前几天写了一个贪吃蛇的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;
}
}