Skip to content
待添加

拼图游戏

最近玩了玩孩子的拼图游戏,感觉还挺好玩的,心血来潮要不动手做一个吧,偷懒摸鱼的时候可以来一把。 以下就是拼图游戏的界面截图。

体验地址代码开源地址

心得体会

虽说是一个小游戏,但是需要注意的地方还是挺多的

方块大小

譬如说这个方块的显示就比较麻烦,也是经历过几个版本的迭代,比较直观的想法可能是这个方块有九种,分为好几个尺寸,开始也是这么弄的。不过各种鼠标拖动之类的计算就比较麻烦了。后来的拼接也比较费劲,各种计算位置是否合适。

后来又更新了一个版本把所有的方块都弄成同样大小了,这样后续的计算就会简单很多,代码看起来也简洁了。

这样也方便后续如果说想要支持不同尺寸的方块,只需要改改相应的大小及偏移就能完全搞定了。

方块显示

每个方块都是在原图的一小部分,很容易就想到了使用背景偏移显示的方式,不过方块还有个问题就是他有地方突出来点,有的地方凹进去点,这个就用到了css的蒙版图片功能,也就做了上边的那种蒙版图片。

拖动拼接

由于方块目前设计的是5x8的方块进行游戏,如果太多了话其实是有一个缩放的问题,目前没有考虑,毕竟这样操作起来感觉还是挺麻烦的。实现来说就是改改相应的大小,倒是没有那么难弄。后续考虑给加上。

方块的拼接能够使用选中的一个图片进行拼接,也可能是已经拼好的部分进行拼接,也都是设计的取舍,当前游戏来说感觉还是以选中拖动移动的为主,只拼接这个方块周围的方块,更加专注一下,避免大力出奇迹的随便就完成游戏了。

代码

整体代码量也不多,也涉及到一些素材,可以从开源项目位置查看。 以下是部分代码,仅供参考。

js
<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>