前言
学了electron想搞个东西,想起了之前刷到的俄罗斯方块,就实验了一下,虽然不难,但是很多东西还是很吃思路的,现在写博客来总结一下。
相信大家都是会玩俄罗斯方块的,所以什么碰到左边就不能左移啊,满一行就消掉啊这种我就不说了。
前期准备
创建一个vue项目,不需要router,不需要store。可以用element-plus,但是不是很重要,而且是在最后一步,所以也不着急。(router不需要也可以,直接在app.vue里面写也行的,如果建了router就直接在提供的homePage写就行了,不用太麻烦)。
大家自己找个喜欢的图片或者视频当背景就行,方块的话我选择的是自己用css写。
如何用electron创建和打包为exe看我上一篇文章就行。我们先用vue写在H5查看和修改,然后再照着这个去创建和打包就行。
vite+vue+electron的创建并使用electron-build打包-CSDN博客
画地图和方块
画地图
我选择的是用表格。
首先确定好行和列,我是的用20*12。然后写对应的样式之类的。
<script setup>
import { onMounted,ref } from 'vue';
const row=20
const col=12
const Map = ref([])//确定地图
const makeMap=()=>{
let Map_temp=[]
for(let i=0;i<row;i++){
let line=[]
for(let j=0;j<col;j++){
line.push(0)
}
Map_temp.push(line)
}
Map.value=Map_temp
}
const Previews = ref([
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
])//确定预览的方块
onMounted(()=>{
makeMap()
})
</script>
<template>
<!-- 地图 -->
<table border="1">
<tbody>
<tr v-for="(row, rowIndex) in Map" :key="rowIndex">
<td v-for="(cell, colIndex) in row" :key="colIndex" :class="`c${cell}`">
</td>
</tr>
</tbody>
</table>
<!-- 预览 -->
<table border="1" class="Preview">
<tbody>
<tr v-for="(row, rowIndex) in Previews" :key="rowIndex">
<td v-for="(cell, colIndex) in row" :key="colIndex" :class="`c${cell}`">
</td>
</tr>
</tbody>
</table>
</template>
<style scoped>
td {
width: 30px;
height: 30px;
user-select: none;
border: 1px solid #ddd;
/* 边框颜色设置为浅灰色 */
/* 禁用文本选择 */
outline: none;
/* 禁用聚焦时的轮廓 */
}
table {
position: relative;
margin: 30px auto;
z-index: 100;
}
table,
tbody,
tr {
z-index: 100;
user-select: none;
border: none;
border: 1px solid #ddd;
/* 边框颜色设置为浅灰色 */
/* 禁用文本选择 */
outline: none;
/* 禁用聚焦时的轮廓 */
}
.Preview {
position: absolute;
z-index: 100;
right: 80px;
top: 0px;
}
</style>
写完上面的运行就能看到表格了。这个就是我们的地图了,右边是预览(下一步初始化之后就能看到)。
然后加上一个地图渲染的逻辑
//渲染地图颜色
const changeMapColor = () => {
for (let i = 0; i < 20; i++) {
for (let j = 0; j < 12; j++) {
// console.log(block.value[i][j])
if (Map.value[i][j] != 0) {
// 这里修正了单元格索引计算
const index = i * 12 + j
document.querySelectorAll('td')[index].className = `c${Map.value[i][j]}`;
} else {
const index = i * 12 + j
document.querySelectorAll('td')[index].className = ``;
}
}
}
}
画方块
然后接下来导入方块。每一种用一个4*4的表格画出来,如果没有颜色就是0,否则就非0。
就像这样。
0 | 1 | 0 | 0 |
0 | 1 | 1 | 0 |
0 | 0 | 1 | 0 |
0 | 0 | 0 | 0 |
然后大家写的时候一定要按照顺序,就是做好方块旋转完就是下面,方便后面的旋转功能。比如Block['S'][0]旋转完成变成了Block['S'][1],Block['J'][0]旋转完成变成了Block['J'][1]。
const Block = {
"S": [
[
[0, 1, 1, 0],
[1, 1, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
],
[
[0, 1, 0, 0],
[0, 1, 1, 0],
[0, 0, 1, 0],
[0, 0, 0, 0]
]
],
"Z": [
[
[2, 2, 0, 0],
[0, 2, 2, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
],
[
[0, 0, 2, 0],
[0, 2, 2, 0],
[0, 2, 0, 0],
[0, 0, 0, 0]
]
],
"J": [
[
[0, 3, 0, 0],
[0, 3, 0, 0],
[3, 3, 0, 0],
[0, 0, 0, 0]
],
[
[3, 0, 0, 0],
[3, 3, 3, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
],
[
[0, 3, 3, 0],
[0, 3, 0, 0],
[0, 3, 0, 0],
[0, 0, 0, 0]
],
[
[3, 3, 3, 0],
[0, 0, 3, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
]
],
"L": [
[
[0, 4, 0, 0],
[0, 4, 0, 0],
[0, 4, 4, 0],
[0, 0, 0, 0]
],
[
[0, 0, 0, 0],
[4, 4, 4, 0],
[4, 0, 0, 0],
[0, 0, 0, 0]
],
[
[4, 4, 0, 0],
[0, 4, 0, 0],
[0, 4, 0, 0],
[0, 0, 0, 0]
],
[
[0, 0, 4, 0],
[4, 4, 4, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
]
],
"I": [
[
[0, 5, 0, 0],
[0, 5, 0, 0],
[0, 5, 0, 0],
[0, 5, 0, 0]
],
[
[5, 5, 5, 5],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
]
],
"O": [
[
[0, 6, 6, 0],
[0, 6, 6, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
]
],
"T": [
[
[0, 7, 0, 0],
[7, 7, 0, 0],
[0, 7, 0, 0],
[0, 0, 0, 0]
],
[
[0, 7, 0, 0],
[7, 7, 7, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
],
[
[0, 7, 0, 0],
[0, 7, 7, 0],
[0, 7, 0, 0],
[0, 0, 0, 0]
],
[
[7, 7, 7, 0],
[0, 7, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
]
]
}
然后就是方块的颜色,用css的背景色就行。
.c1 {
background-color: red;
}
.c2 {
background-color: orange;
}
.c3 {
background-color: yellow;
}
.c4 {
background-color: green;
}
.c5 {
background-color: blue;
}
.c6 {
background-color: indigo;
}
.c7 {
background-color: violet;
}
定义一下给方块上色的逻辑,还有清除颜色的逻辑
//生成颜色的盒子
const makeBlockColor =async () => {
await nextTick();
// console.log(block.value)
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
// console.log(block.value[i][j])
if (block.value[i][j] != 0) {
const index = (i + nowRow.value) * 12 + j + nowcol.value
document.querySelectorAll('td')[index].className = `c${block.value[i][j]}`;
}
}
}
};
//清除颜色
const clearColor = () => {
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
// console.log(block.value[i][j])
if (block.value[i][j] != 0) {
// 这里修正了单元格索引计算+
const index = (i + nowRow.value) * 12 + j + nowcol.value
document.querySelectorAll('td')[index].className = ``;
}
}
}
}
定义生成一个新block
//生成一个新的格子
const beginBlock = () => {
nowRow.value = 0
nowcol.value = 4
type.value = nextType.value
type_type.value = nexttype_type.value
block.value = Block[type.value][type_type.value]
nextType.value = typeLine[Math.floor(Math.random() * typeLine.length)]
nexttype_type.value = Math.floor(Math.random() * Block[nextType.value].length)
Previews.value = Block[nextType.value][nexttype_type.value]
makeBlockColor();
}
初始化地图和方块
都定义好了之后现在来初始化一下地图和开始一个方块
import { onMounted, ref ,nextTick} from 'vue';
const typeLine = ["S", "Z", "J", "L", "I", "O", 'T']
const nowRow = ref(0)//用来计算下移的量
const nowcol = ref(4)//用来计算左右的移动,为了一开始在中间出现,我们设初始值为4
const nexttype_type = ref(0)//下一个具体角度
const nextType = ref('')//确定下一个的形状
const type = ref('')//用来确定本次方块的形状
const type_type = ref(0)//用来确定本次方块的具体角度,比如Block[type.value][type_type.value]
const block = ref([]);//确定现在的方块
const Previews = ref([])//确定预览的方块,也就是下一个方块
//定义一个初始化
const init = async() => {
setTimeout(() => {
}, 2000)
count.value = 0
speed.value = 50
nextType.value = typeLine[Math.floor(Math.random() * typeLine.length)]
nexttype_type.value = Math.floor(Math.random() * Block[nextType.value].length)
Previews.value = Block[nextType.value][nexttype_type.value]
makeMap()
await nextTick();
changeMapColor()
beginBlock()
}
onMounted(() => {
init()
})
到现在为止,我们就完成了初始化,包括地图和方块盒子、预览。
向下移动和判断停止
初始化完成之后我们就需要来判断移动了。
首先是定一下固定地图,就是我们停止了之后就变成了其他数字。(每种数字和颜色是对应的)。
//将格子固定到地图上
const changeMap = () => {
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (block.value[i][j] != 0) {
const newRow = i + nowRow.value;
const newCol = j + nowcol.value
Map.value[newRow][newCol] = block.value[i][j]
}
}
}
}
然后就是判断是否可以继续向下移动
const check = () => {
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (block.value[i][j] != 0) {
const newRow = i + nowRow.value + 1;
const newCol = j + nowcol.value;
if (newRow >= row || Map.value[newRow][newCol] != 0) {
return false;
}
}
}
}
return true;
}
确实之后我们就可以让方块向下移动了,修改一下上面的beginBlock就可以了。
//生成一个新的格子
const beginBlock = () => {
nowRow.value = 0
nowcol.value = 4
type.value = nextType.value
type_type.value = nexttype_type.value
block.value = Block[type.value][type_type.value]
nextType.value = typeLine[Math.floor(Math.random() * typeLine.length)]
nexttype_type.value = Math.floor(Math.random() * Block[nextType.value].length)
Previews.value = Block[nextType.value][nexttype_type.value]
makeBlockColor();
T.value = setInterval(() => {
if (check()) {
clearColor()
nowRow.value++
makeBlockColor()
} else {
clearInterval(T.value)
changeMap()
beginBlock()
}
}, speed.value * 10)
}
完成之后我们就能看到自动向下移动并且判断是否到底并生成新的。
左右移动、变形和一键到底
接下来就是左右移动和旋转了,我这里用的是上下左右键,如果想换成WASD的换一下key值就好了。
确定左移的条件并且左移。
//左移
const checkLeft = () => {
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (block.value[i][j] != 0) {
const newCol = j + nowcol.value;
if (newCol == 0 || Map.value[i + nowRow.value][newCol - 1] != 0) {
return
}
}
}
}
clearColor()
nowcol.value--
makeBlockColor()
return
}
确定右移的条件并且右移。
//右移
const checkRight = () => {
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (block.value[i][j] != 0) {
const newCol = j + nowcol.value;
if (newCol == 11 || Map.value[i + nowRow.value][newCol + 1] != 0) {
return
}
}
}
}
clearColor()
nowcol.value++
makeBlockColor()
return
}
向下移动我设置的是直接到底。
//一键下移
const moveDown = () => {
clearInterval(T.value)
while (check()) {
clearColor()
nowRow.value++
makeBlockColor()
}
changeMap()
setTimeout(beginBlock, 0);
}
上键我设置的就是旋转,这就是我上面强调方块位置的原因,旋转其实也就是让变成同个type里面的不同type_type而已。
//旋转
const roate = () => {
const len = Block[type.value].length
const temp_type = type_type.value
clearColor()
if (type_type.value + 1 == len) {
type_type.value = 0
} else {
type_type.value++
}
//先看看新的是否符合要求
const temp = Block[type.value][type_type.value]
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (temp[i][j] != 0 && Map.value[i + nowRow.value][nowcol.value + j] != 0) {
type_type.value = temp_type
makeBlockColor()
return
}
}
}
block.value = Block[type.value][type_type.value]
makeBlockColor()
}
都定义完成之后,我们就可以监听鼠标事件了。
// 处理键盘事件
const handleKeydown = (event) => {
switch (event.key) {
case 'ArrowUp':
roate()
break;
case 'ArrowLeft':
checkLeft()
break;
case 'ArrowRight':
checkRight()
break;
case 'ArrowDown':
moveDown()
break;
}
};
onMounted(() => {
// beginBlock()
init()
window.addEventListener('keydown', handleKeydown);
});
onUnmounted(() => {
window.removeEventListener('keydown', handleKeydown);
});
所以现在已经是具有初步样子了,剩下的只有判断胜负了。
判断消除条件、判负
消除条件,只需要判断某一行0的个数,如果没有0就消掉,同时在最地图上面补上一行全新的空行。可以自行决定加分之类的,我这里定义是消除一行加5分,有什么规则就各自发挥咯!
//返回数组0的个数
const countZeros = (arr) => {
return arr.filter(item => item === 0).length;
};
//判断是否可以消除
const ifEliminate = async() => {
for (let i = 0; i < 20; i++) {
if (countZeros(Map.value[i]) == 0) {
Map.value.splice(i, 1);
Map.value.unshift([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
await nextTick()
changeMapColor()
count.value += 5
}
}
};
判负,只要第一行如果不是12个全部为0就判负游戏停止。
//判断失败
const ifLost = () => {
if (countZeros(Map.value[0]) != 12) {
alert('失败!')
clearInterval(T.value)
return false
}
return true
}
然后把这些运用起来,加到所有沉底的地方。
//生成一个新的格子
const beginBlock = () => {
nowRow.value = 0
nowcol.value = 4
type.value = nextType.value
type_type.value = nexttype_type.value
block.value = Block[type.value][type_type.value]
nextType.value = typeLine[Math.floor(Math.random() * typeLine.length)]
nexttype_type.value = Math.floor(Math.random() * Block[nextType.value].length)
Previews.value = Block[nextType.value][nexttype_type.value]
makeBlockColor();
T.value = setInterval(() => {
if (check()) {
clearColor()
nowRow.value++
makeBlockColor()
} else {
clearInterval(T.value)
changeMap()
ifEliminate()
if (ifLost())
setTimeout(beginBlock, 0);
}
}, speed.value * 10)
}
//一键下移
const moveDown = () => {
clearInterval(T.value)
while (check()) {
clearColor()
nowRow.value++
makeBlockColor()
}
changeMap()
ifEliminate()
if (ifLost())
setTimeout(beginBlock, 0);
}
其实到这个地方游戏就已经是完成了,可以正常的玩了,剩下的就是自定义规则了。
相关完善(设置加分、开始、速度)
这部分算是锦上添花了,不影响游戏,但是既然不多,那就把这个一起弄好看点吧!
背景我引入的是一个视频,这个大家各自发挥。
<video id="video-background" class="video-js vjs-fill vjs-fluid" muted autoplay loop>
<source src="../../assets/bg.mp4" type="video/mp4" />
</video>
#video-background {
z-index: 0;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
首先是引入element-plus,如何引入请参考该文章最后部分
vite+vue+electron的创建并使用electron-build打包-CSDN博客
然后我们加几个按钮来控制
<div class='actionArea'>
<el-card style="width: 150px;height: 40px;margin-bottom: 10px" shadow="always">分数:<span style='color:red'>{{
count }}</span></el-card>
<el-button type="primary" :disabled="begin" @click="BeginGame">开始</el-button>
<el-button type="primary" :disabled="!begin" @click="ReatartGame">重新开始</el-button>
<div class="slider-demo-block">
<span class="demonstration" style="width: 100px;"><el-text class="mx-1" type="danger">速度间隔</el-text></span>
<el-slider v-model="speed" />
</div>
</div>
.actionArea {
position: absolute;
z-index: 100
}
.slider-demo-block {
max-width: 600px;
display: flex;
align-items: center;
}
.slider-demo-block .el-slider {
margin-top: 0;
margin-left: 12px;
}
还有加上一个模态框
新建一个Dialog.vue
<template>
<el-dialog
:model-value="Visible"
title="结束"
width="500"
@close="handleClose"
:before-close="handleClose"
>
<span>游戏结束!总共获得{{ count }}分</span>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="closeDialog">确认</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch } from 'vue';
import { ElDialog, ElButton } from 'element-plus';
import 'element-plus/dist/index.css';
const emit = defineEmits(['close']);
const props = defineProps({
Visible: {
type: Boolean,
required: true
},
count: {
type: Number,
required: true
}
});
const handleClose = () => {
emit('close');
};
const closeDialog = () => {
emit('close');
};
</script>
<style scoped>
.dialog-footer {
text-align: right;
}
</style>
然后回到原来
import Dialog from '@/components/Dialog.vue'
<Dialog :Visible="Visible" :count="count" @close="closeDialog"></Dialog>
然后汇总一下需要修改的js
const Visible = ref(false)//显示模态框
const begin = ref(false)//开始
//开始游戏
const BeginGame = () => {
init()
begin.value = !begin.value
}
//关闭模态框
const closeDialog = () => {
Visible.value = false
}
//重新开始
const ReatartGame = () => {
init()
}
//判断失败
const ifLost = () => {
if (countZeros(Map.value[0]) != 12) {
Visible.value = true
clearInterval(T.value)
return false
}
return true
}
onMounted(() => {
makeMap()
window.addEventListener('keydown', handleKeydown);
});
这样我们就能愉快地玩耍了!
配置打包为exe
这里看参考我发的博客,完全就是我写完这个项目后打包的。
vite+vue+electron的创建并使用electron-build打包-CSDN博客
但是还是走遍吧!
npm i cnpm
cnpm install electron --save-dev
cnpm install electron-builder -D
目录下新建main.js
import { app, BrowserWindow } from 'electron'
import path from 'path'
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
function createWindow() {
console.log('Creating window...')
const mainWindow = new BrowserWindow({
width: 950,
height: 850,
title: "Russia cubes",
icon: '@/assets/logo.ico',
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
devTools: false,// 禁用开发者工具
webSecurity: false
},
// 加载 index.html
})
// 加载开发服务器提供的 URL(如果使用)
const devServerUrl = 'http://localhost:5173' // 根据你的开发服务器配置进行调整
mainWindow
// .loadURL(devServerUrl)//还没打包之前用url,打包之后用index
.loadFile('./dist/index.html')
.then(() => {
console.log('Loaded URL:', devServerUrl)
})
.catch((err) => {
console.error('Failed to load URL:', err)
})
// 删除或注释掉打开开发者工具的代码
mainWindow.webContents.openDevTools();
}
app.whenReady().then(() => {
console.log('App is ready')
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
新建preload.js
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}
for (const dependency of ['chrome', 'node', 'electron']) {
replaceText(`${dependency}-version`, process.versions[dependency])
}
})
在vite.config.js里面加上
base:'./',
在package.json里面加上
"main": "main.js",
"description": "Russia cubes",
"author": "Ye",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"start": "electron .",//加上
"exe-build": "electron-builder"//加上
},
"build": {
"appId": "Ye.russiacubes",
"files": [
"dist/**/*",
"main.js",
"preload.js",
"index.html",
"src/**/*",
"node_modules/**/*",
"package.json"
],
"asarUnpack": [
"main.js",
"preload.js"
],
"win": {
"target": [
"nsis"
]
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
}
},
cnpm run build
cnpm run exe-build
然后就可以啦!在dist里面有setup.exe,也有直接运行的exe。
当然第一次打包有很多坑,直接看我的博客就行了。
仓库和exe
https://github.com/yanmengssss/Russia-Block-Reset-.git
这个是我的仓库,和现在的代码完全对应,除了没有dist,要自己打包
Russia cubes: 用vue+vite+electron构建的纯前端俄罗斯方块
这个是原来的(gitee),和现在的区别就是我这次没有用router和加了很多注释,原来的没有多注释。
setup.exe在这