拼图游戏
最近玩了玩孩子的拼图游戏,感觉还挺好玩的,心血来潮要不动手做一个吧,偷懒摸鱼的时候可以来一把。
以下就是拼图游戏的界面截图。
体验地址
代码开源地址
心得体会
虽说是一个小游戏,但是需要注意的地方还是挺多的
方块大小
譬如说这个方块的显示就比较麻烦,也是经历过几个版本的迭代,比较直观的想法可能是这个方块有九种,分为好几个尺寸,开始也是这么弄的。不过各种鼠标拖动之类的计算就比较麻烦了。后来的拼接也比较费劲,各种计算位置是否合适。
后来又更新了一个版本把所有的方块都弄成同样大小了,这样后续的计算就会简单很多,代码看起来也简洁了。
这样也方便后续如果说想要支持不同尺寸的方块,只需要改改相应的大小及偏移就能完全搞定了。
方块显示
每个方块都是在原图的一小部分,很容易就想到了使用背景偏移显示的方式,不过方块还有个问题就是他有地方突出来点,有的地方凹进去点,这个就用到了css的蒙版图片功能,也就做了上边的那种蒙版图片。
拖动拼接
由于方块目前设计的是5x8的方块进行游戏,如果太多了话其实是有一个缩放的问题,目前没有考虑,毕竟这样操作起来感觉还是挺麻烦的。实现来说就是改改相应的大小,倒是没有那么难弄。后续考虑给加上。
方块的拼接能够使用选中的一个图片进行拼接,也可能是已经拼好的部分进行拼接,也都是设计的取舍,当前游戏来说感觉还是以选中拖动移动的为主,只拼接这个方块周围的方块,更加专注一下,避免大力出奇迹的随便就完成游戏了。
代码
整体代码量也不多,也涉及到一些素材,可以从开源项目位置查看。
以下是部分代码,仅供参考。
<script setup>
import { onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import HeaderCompoent from '../../components/HeaderComponent.vue'
const itemStyles = ref([])
var currentIndex = -1
var fixed = []
var relatives = []
var currentRelative = new Set()
const row = 5
const column = 8
const route = useRoute()
const picId = ref('0001')
const router = useRouter()
const handleNaviBack = () => {
router.back()
}
onMounted(() => {
picId.value = route.params.id
let styles = []
let index = 0
for (let i = 0; i < row; i++) {
for (let j = 0; j < column; j++) {
let classIndex = 0
if (i == 0) {
if (j == 0) {
classIndex = 1
} else if (j == (column - 1)) {
classIndex = 3
} else {
classIndex = 2
}
} else if (i == (row - 1)) {
if (j == 0) {
classIndex = 7
} else if (j == (column - 1)) {
classIndex = 9
} else {
classIndex = 8
}
} else {
if (j == 0) {
classIndex = 4
} else if (j == (column - 1)) {
classIndex = 6
} else {
classIndex = 5
}
}
styles.push({
offsetx: -j * 100,
offsety: -i * 100,
classIndex: classIndex,
// top: i * 100,
// left: j * 100,
top: (row-1) * 100 * (Math.random() * 1.2 - 0.1),
left: (column-1) * 100 * (Math.random() * 1.2 - 0.1),
x: i,
y: j,
w: 160,
h: 160,
index: index,
})
fixed.push(0)
index += 1
}
}
itemStyles.value = styles
window.onmousemove = handleMouseMove
window.onmouseup = () => {
console.log('onmouseup')
if (currentIndex == -1) {
return
}
let styles = itemStyles.value
let x = styles[currentIndex].x
let y = styles[currentIndex].y
for(let offset of [[0,1],[0,-1],[1,0],[-1,0]]) {
// 判断一下是否越界
let xx = x + offset[0]
let yy = y + offset[1]
if (xx < 0 || xx > (row-1) || yy < 0 || yy > (column-1)) {
continue
}
let otherIndex = xx * column + yy
if (otherIndex >= styles.length) {
continue
}
if (currentRelative.has(otherIndex)) {
continue
}
console.log('currentIndex ' + currentIndex)
console.log('otherIndex ' + otherIndex)
// 判断试一下是否很近
if (Math.abs(styles[currentIndex].top - styles[otherIndex].top + (styles[otherIndex].x - x) * 100) > 20 ||
Math.abs(styles[currentIndex].left - styles[otherIndex].left + (styles[otherIndex].y - y) * 100) > 20) {
continue
}
// 判断一下是否已经设置过
fixed[currentIndex] = 1
fixed[otherIndex] = 1
for(let oIndex of currentRelative) {
styles[oIndex].top = styles[otherIndex].top - (styles[otherIndex].x - styles[oIndex].x) * 100
styles[oIndex].left = styles[otherIndex].left - (styles[otherIndex].y - styles[oIndex].y) * 100
}
if (!currentRelative.has(otherIndex)) {
currentRelative.add(otherIndex)
}
if (relatives.indexOf(currentRelative) < 0) {
relatives.push(currentRelative)
}
for(let sindex in relatives) {
if (relatives[sindex] == currentRelative) {
continue
}
if (relatives[sindex].has(otherIndex)) { // 如果说对上的元素在其他的分组里边
for(let v of relatives[sindex]) {
currentRelative.add(v)
}
relatives.splice(sindex, 1)
}
}
console.log('fixed '+ currentIndex + ' ' + otherIndex)
console.log('handleMouseUp - ' + Array.from(currentRelative), relatives)
}
let count = 0
for(let i = 0; i < row * column; i++) {
count += fixed[i]
}
if (count == row * column && relatives.length == 1) {
setTimeout(() => {
alert('恭喜完成拼图')
}, 200);
}
currentIndex = -1
}
})
const handleMouseMove = (event) => {
if (currentIndex > -1) {
let ele = document.getElementById('grid')
let rect = ele.getBoundingClientRect()
let styles = itemStyles.value
let x = styles[currentIndex].x
let y = styles[currentIndex].y
let classIndex = styles[currentIndex].classIndex
let offsetx = event.clientX - rect.left
let offsety = event.clientY - rect.top
styles[currentIndex].left = offsetx - styles[currentIndex].w / 2 + 30
styles[currentIndex].top = offsety - styles[currentIndex].h / 2 + 30
for(let otherIndex of currentRelative) {
styles[otherIndex].left = (styles[otherIndex].y - y) * 100 + styles[currentIndex].left
styles[otherIndex].top = (styles[otherIndex].x - x) * 100 + styles[currentIndex].top
}
itemStyles.value = styles
}
}
const handleMouseDown = (index) => {
console.log('handleMouseDown ' + index)
currentIndex = index
currentRelative = new Set()
for(let s of relatives) {
if (s.has(currentIndex)) {
currentRelative = s
break
}
}
if (currentRelative.size == 0) {
currentRelative.add(currentIndex)
}
console.log('handleMouseDown - ' + Array.from(currentRelative), relatives)
}
</script>
<template>
<div class="container">
<div id="grid" class="grid">
<img
:src="`/images/pintu/${picId}.jpg`"
alt=""
class="backgroundImage">
<div v-for="ss in itemStyles"
:key="ss"
class="imageContainer"
:style="{
top: `${ss.top}px`,
left: `${ss.left}px`,
}"
@mousedown="handleMouseDown(ss.index)"
>
<div
:class="['imageClass', `imageCover00${ss.classIndex}`]"
:style="{
backgroundPosition: `${ss.offsetx}px ${ss.offsety}px`,
backgroundImage: `url(/images/pintu/${picId}.jpg)`
}">
</div>
</div>
</div>
</div>
<HeaderCompoent></HeaderCompoent>
<div class="backBtn" @click="handleNaviBack">返回目录</div>
</template>
<style scoped>
.container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center
}
.backBtn {
position: fixed;
top: 20px;
right: 20px;
}
.grid {
position: relative;
width: 800px;
height: 500px;
}
.backgroundImage {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0.2;
}
.imageContainer {
position: absolute;
width: 100px;
height: 100px;
}
.imageClass {
width: 160px;
height: 160px;
background-image: url('/images/pintu/0001.jpg');
background-size: 800px 500px;
overflow: visible;
}
.imageCover001 {
mask-image: url('/images/pintu/cover/001.png');
mask-repeat: no-repeat;
mask-position: -30px -30px;
mask-size: 160px;
-webkit-mask-image: url('/images/pintu/cover/001.png');
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: -30px -30px;
-webkit-mask-size: 160px;
}
.imageCover002 {
mask-image: url('/images/pintu/cover/002.png');
mask-repeat: no-repeat;
mask-position: -30px -30px;
mask-size: 100%;
-webkit-mask-image: url('/images/pintu/cover/002.png');
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: -30px -30px;
-webkit-mask-size: 100%;
}
.imageCover003 {
mask-image: url('/images/pintu/cover/003.png');
mask-repeat: no-repeat;
mask-position: -30px -30px;
mask-size: 100%;
-webkit-mask-image: url('/images/pintu/cover/003.png');
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: -30px -30px;
-webkit-mask-size: 100%;
}
.imageCover004 {
mask-image: url('/images/pintu/cover/004.png');
mask-repeat: no-repeat;
mask-position: -30px -30px;
mask-size: 100%;
-webkit-mask-image: url('/images/pintu/cover/004.png');
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: -30px -30px;
-webkit-mask-size: 100%;
}
.imageCover005 {
mask-image: url('/images/pintu/cover/005.png');
mask-repeat: no-repeat;
mask-position: -30px -30px;
mask-size: 100%;
-webkit-mask-image: url('/images/pintu/cover/005.png');
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: -30px -30px;
-webkit-mask-size: 100%;
}
.imageCover006 {
mask-image: url('/images/pintu/cover/006.png');
mask-repeat: no-repeat;
mask-position: -30px -30px;
mask-size: 100%;
-webkit-mask-image: url('/images/pintu/cover/006.png');
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: -30px -30px;
-webkit-mask-size: 100%;
}
.imageCover007 {
mask-image: url('/images/pintu/cover/007.png');
mask-repeat: no-repeat;
mask-position: -30px -30px;
mask-size: 100%;
-webkit-mask-image: url('/images/pintu/cover/007.png');
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: -30px -30px;
-webkit-mask-size: 100%;
}
.imageCover008 {
mask-image: url('/images/pintu/cover/008.png');
mask-repeat: no-repeat;
mask-position: -30px -30px;
mask-size: 100%;
-webkit-mask-image: url('/images/pintu/cover/008.png');
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: -30px -30px;
-webkit-mask-size: 100%;
}
.imageCover009 {
mask-image: url('/images/pintu/cover/009.png');
mask-repeat: no-repeat;
mask-position: -30px -30px;
mask-size: 100%;
-webkit-mask-image: url('/images/pintu/cover/009.png');
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: -30px -30px;
-webkit-mask-size: 100%;
}
</style>