纯JS实现填字游戏实战总结

1 游戏整体框架和流程

最终效果图如图:
在这里插入图片描述
功能点:

  1. 顶部4个格子存放拖放的字,底部16个格子存放待选字
  2. 字依靠拖动放入空白中
  3. 正确、错误给出提示,并重置

开发计划:
UI设计:顶部4个空白、底部16个字
功能设计:提供拖拽跟随鼠标、提供松开鼠标回弹、拖动到空白格吸附、4字判断、重置

2 UI设计

2.1 顶部空白格

框架:顶部最外层用一个div,每个格子为一个div
要实现每个格子间隔开,简单的办法就是flex布局,或者设置固定宽度并设置margin(margin不在宽度中所以还要计算,比较麻烦)
这里采用flex并设置固定宽度25%,为了留白需要设置padding并在内部添加一个div,在该div上设置背景色
此处采用border-box方式,使border和padding不影响宽度

  <style>
    body {
      margin: 0;
    }
    .blank-cell{
      display: flex;
    }
    .blank-cell .blank-item{
      width: 25%;
      height: 25vw;
      padding: .5rem;
      box-sizing: border-box;
    }

    .blank-cell .blank-item .wrapper{
      width: 100%;
      height: 100%;
      /* margin: 15px; padding和border在border-box下不会影响宽度,但是margin在任何情况下都会影响宽度 */
      border: .1rem solid black;
      box-sizing: border-box;
    }
  </style>
  
  <div class="container">
    <div class="blank-cell">
      <div class="blank-item">
        <div class="wrapper"></div>
      </div>
      <div class="blank-item">
        <div class="wrapper"></div>
      </div>
      <div class="blank-item">
        <div class="wrapper"></div>
      </div>
      <div class="blank-item">
        <div class="wrapper"></div>
      </div>
    </div>
  </div>

2.2 底部16字

很明显,底部的16字肯定采用循环方式进行生成的,使用innerHtml赋值拼接html即可,网页UI整体结构如下:
此处做了一个分辨率适配:document.documentElement.style.fontSize = document.documentElement.clientWidth / 39 + 'px';
适配原理:默认情况下:1rem=document.documentElement.style.fontSize=14px只需要改变后者即可改变1rem大小

此处分辨率为390*844,适配后1rem=10px,使用rem可以比较好的做移动端适配

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script>
    document.documentElement.style.fontSize = document.documentElement.clientWidth / 39 + 'px';
  </script>
  <style>
    body {
      margin: 0;
    }
    .blank-cell,
    .cell {
      display: flex;
    }
    .cell {
      flex-wrap: wrap;
      margin-top: 5rem;
    }
    .blank-cell .blank-item,
    .cell .item {
      width: 25%;
      height: 25vw;
      padding: .5rem;
      box-sizing: border-box;
    }
    .blank-cell .blank-item .wrapper,
    .cell .item .wrapper {
      width: 100%;
      height: 100%;
      /* margin: 15px; padding和border在border-box下不会影响宽度,但是margin在任何情况下都会影响宽度 */
      border: .1rem solid black;
      box-sizing: border-box;
    }
    .cell .item .wrapper {
      border: none;
      background-color: orange;
      font-size: 3rem;
      justify-content: center;
      align-items: center;
      display: flex;
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="blank-cell">
      <div class="blank-item">
        <div class="wrapper"></div>
      </div>
      <div class="blank-item">
        <div class="wrapper"></div>
      </div>
      <div class="blank-item">
        <div class="wrapper"></div>
      </div>
      <div class="blank-item">
        <div class="wrapper"></div>
      </div>
    </div>
    <div class="cell">
    </div>
  </div>
</body>
</html>

生成底部16字:

    function renderCellItem(text, index) {
      return `<div class="item">
        <div class="wrapper" data-index="${index}">${text}</div>
      </div>`
    }

3 功能设计

3.1 四字洗牌并展示

原始数据:const idioms = ['一发入魂', '三羊开泰', '二狗上树', '五福临门']
将原始数据打乱成一个一个字并拼接html,放在dom中

    cellDom = document.querySelector('.cell');
    function splitIdioms() {
      return idioms.reduce((pre, cur) => pre + cur).split("");
    }
    function shuffle() {
      return splitIdioms().sort(randomSorter)
    }
    function randomSorter() {
      return Math.random() > 0.5 ? 1 : -1;
    }
    function renderCell(words) {
      let index = 0;
      const cellInnerHtml = words.reduce((pre, cur) => {
        return pre + renderCellItem(cur, index++);
      }, "")
      cellDom.innerHTML = cellInnerHtml;
    }

    renderCell(shuffle());

3.2 拖拽

需要用到的事件:touchstarttouchmovetouchend
触碰开始,需要获取元素坐标信息
移动中,需要获取鼠标坐标信息并实时更新元素位置,使用fixedlefttop控制位置,还需要设置width等来控制宽度
松开手,需要对当前位置进行判定,如处于可吸附区则吸附,如果不是则回到原来位置,并改为static
添加事件:

function bindEvent() {
  const wrapperList = document.querySelectorAll('.cell .wrapper');
  wrapperList.forEach(item => {
    item.addEventListener('touchstart', touchStart, false);
    item.addEventListener('touchmove', touchMove, false);
    item.addEventListener('touchend', touchEnd, false);
  })
}

3.2.1 元素移动的坐标要素

元素移动控制的是left和top,鼠标移动获取到的是鼠标坐标,所以在start时需要获取到鼠标坐标到元素左上角的距离,再移动时减去即可

    function touchStart(e) {
      const rect = this.getBoundingClientRect();
      mouseOffsetX = e.touches[0].clientX - rect.left;
      mouseOffsetY = e.touches[0].clientY - rect.top;
      this.style.position = 'fixed'; // 让其可以自由移动,以视窗为坐标
      this.style.left = rect.left + 'px';
      this.style.top = rect.top + 'px';
      this.style.width = rect.width + 'px';
      this.style.height = rect.height + 'px';
    }
    function touchMove(e) {
      e.preventDefault();
      this.style.left = (e.touches[0].clientX - mouseOffsetX) + 'px';
      this.style.top = (e.touches[0].clientY - mouseOffsetY) + 'px';
    }

移动时一定要取消默认事件,否则屏幕会有一段距离的滑动

3.2.2 元素结束移动的判断条件

移动结束时需要回归或吸附,此时需要保存好原始坐标的信息空白坐标信息和吸附区域的信息:

cellRect = getRect(document.querySelectorAll('.cell .wrapper')); // 原始坐标信息
blankRect = getRect(document.querySelectorAll('.blank-item .wrapper')) // 空白坐标信息
checkRange = getCheckRange(blankRect) // 吸附区域信息

function getRect(eList) {
  return Array.prototype.slice.call(eList).map(item => {
    let itemRect = item.getBoundingClientRect()
    return { left: itemRect.left, top: itemRect.top, width: itemRect.width, height: itemRect.height }
  })
}
function getCheckRange(range) {
 return range.map(item => ({ x: [item.left, item.left + item.width], y: [item.top, item.top + item.height] }))
}

在拖拽结束时进行判断:当被拖拽元素的中心位于吸附区域内时进行吸附

function touchEnd(e) {
	// 获取拖动元素中心点坐标值
	const el = e.target;
	const index = this.dataset.index;
	const elRect = el.getBoundingClientRect();
	const [x, y] = [elRect.left + elRect.width / 2, elRect.top + elRect.height / 2] // 获取被拖拽元素中心
	for (let i = 0; i < checkRange.length; i++) {
	  const item = checkRange[i];
	  if (x >= item.x[0] && x <= item.x[1] && y >= item.y[0] && y <= item.y[1]) {
	    this.style.left = blankRect[i].left + 'px';
	    this.style.top = blankRect[i].top + 'px';
	    return;
	  }
	}
	const item = cellRect[parseInt(index)];
    this.style.left = item.left + 'px';
    this.style.top = item.top + 'px';
    this.style.position = 'static'; // 这个很重要!不改的话滑动页面字会跟着动
}

3.3 成功判定

当空白格全满且顺序与原始数据某一项相同,则成功,否则失败!

function checkIdiomFunc() {
  console.log("检查中")
  const idiom = checkIdiom.map(c => c.text).join("");
  const flag = idioms.find(idm => idm === idiom)
  if (flag != null) {
    alert("成功!找到:" + idiom)
  } else {
    alert("失败!!!")
  }
  checkIdiom.map(c => {
    let el = c.el;
    let index = c.index;
    const _el = cellRect[parseInt(index)];
    el.style.left = _el.left + 'px';
    el.style.top = _el.top + 'px';
    el.style.position = 'static';
  })
  checkIdiom = [];
}

在touchend事件中调用检查函数:

function touchEnd(e) {
// 获取拖动元素中心点坐标值
 const el = e.target;
 const index = this.dataset.index;
 const elRect = el.getBoundingClientRect();
 const [x, y] = [elRect.left + elRect.width / 2, elRect.top + elRect.height / 2]
 for (let i = 0; i < checkRange.length; i++) {
   const item = checkRange[i];
   if (x >= item.x[0] && x <= item.x[1] && y >= item.y[0] && y <= item.y[1]) {
     if (checkIdiom[i] != null) {
       break;
     }
     this.style.left = blankRect[i].left + 'px';
     this.style.top = blankRect[i].top + 'px';
     checkIdiom[i] = { text: el.innerText, el, index };
     if (checkIdiom.length === 4 && !checkIdiom.includes(undefined)) {
       setTimeout(checkIdiomFunc, 500); // 此处如果不延迟,则会导致DOM还未渲染就弹出alert,因为alert是阻塞的
     }
     return;
   }
 }

 for (let i = 0; i < checkIdiom.length; i++) {
   if (checkIdiom && checkIdiom[i].index == index) {
     checkIdiom[i] = undefined;
     break;
   }
 }
 const item = cellRect[parseInt(index)];
 this.style.left = item.left + 'px';
 this.style.top = item.top + 'px';
 this.style.position = 'static'; // 这个很重要!不改的话滑动页面字会跟着动
}

4 项目整体代码

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script>
    document.documentElement.style.fontSize = document.documentElement.clientWidth / 39 + 'px';
  </script>
  <style>
    body {
      margin: 0;
    }

    .blank-cell,
    .cell {
      display: flex;
    }

    .cell {
      flex-wrap: wrap;
      margin-top: 5rem;
    }

    .blank-cell .blank-item,
    .cell .item {
      width: 25%;
      height: 25vw;
      padding: .5rem;
      box-sizing: border-box;
    }

    .blank-cell .blank-item .wrapper,
    .cell .item .wrapper {
      width: 100%;
      height: 100%;
      /* margin: 15px; padding和border在border-box下不会影响宽度,但是margin在任何情况下都会影响宽度 */
      border: .1rem solid black;
      box-sizing: border-box;
    }

    .cell .item .wrapper {
      border: none;
      background-color: orange;
      font-size: 3rem;
      justify-content: center;
      align-items: center;
      display: flex;
    }

  </style>
</head>

<body>
  <div class="container">
    <div class="blank-cell">
      <div class="blank-item">
        <div class="wrapper"></div>
      </div>
      <div class="blank-item">
        <div class="wrapper"></div>
      </div>
      <div class="blank-item">
        <div class="wrapper"></div>
      </div>
      <div class="blank-item">
        <div class="wrapper"></div>
      </div>
    </div>

    <div class="cell">
    </div>

  </div>
  <script>
    const idioms = ['一发入魂', '三羊开泰', '二狗上树', '五福临门'],
      cellDom = document.querySelector('.cell');
    let mouseOffsetX = 0,
      mouseOffsetY = 0,
      cellRect = [],
      blankRect = [],
      checkRange = [],
      checkIdiom = [];
    function init() {
      renderCell(shuffle());
      cellRect = getRect(document.querySelectorAll('.cell .wrapper'));
      blankRect = getRect(document.querySelectorAll('.blank-item .wrapper'))
      checkRange = getCheckRange(blankRect)
      bindEvent();
    }
    function getCheckRange(range) {
      return range.map(item => ({ x: [item.left, item.left + item.width], y: [item.top, item.top + item.height] }))
    }
    function getRect(eList) {
      return Array.prototype.slice.call(eList).map(item => {
        let itemRect = item.getBoundingClientRect()
        return { left: itemRect.left, top: itemRect.top, width: itemRect.width, height: itemRect.height }
      })
    }
    function splitIdioms() {
      return idioms.reduce((pre, cur) => pre + cur).split("");
    }
    function shuffle() {
      return splitIdioms().sort(randomSorter)
    }
    function randomSorter() {
      return Math.random() > 0.5 ? 1 : -1;
    }
    function renderCell(words) {
      let index = 0;
      const cellInnerHtml = words.reduce((pre, cur) => {
        return pre + renderCellItem(cur, index++);
      }, "")
      cellDom.innerHTML = cellInnerHtml;
    }
    function renderCellItem(text, index) {
      return `<div class="item">
        <div class="wrapper" data-index="${index}">${text}</div>
      </div>`
    }
    function bindEvent() {
      const wrapperList = document.querySelectorAll('.cell .wrapper');
      wrapperList.forEach(item => {
        item.addEventListener('touchstart', touchStart, false);
        item.addEventListener('touchmove', touchMove, false);
        item.addEventListener('touchend', touchEnd, false);
      })
    }
    function touchStart(e) {
      const rect = this.getBoundingClientRect();
      mouseOffsetX = e.touches[0].clientX - rect.left;
      mouseOffsetY = e.touches[0].clientY - rect.top;
      this.style.position = 'fixed'; // 让其可以自由移动,以视窗为坐标
      this.style.left = rect.left + 'px';
      this.style.top = rect.top + 'px';
      this.style.width = rect.width + 'px';
      this.style.height = rect.height + 'px';
    }
    function touchMove(e) {
      e.preventDefault();
      this.style.left = (e.touches[0].clientX - mouseOffsetX) + 'px';
      this.style.top = (e.touches[0].clientY - mouseOffsetY) + 'px';
    }
    function touchEnd(e) {
      // 获取拖动元素中心点坐标值
      const el = e.target;
      const index = this.dataset.index;
      const elRect = el.getBoundingClientRect();
      const [x, y] = [elRect.left + elRect.width / 2, elRect.top + elRect.height / 2]
      for (let i = 0; i < checkRange.length; i++) {
        const item = checkRange[i];
        if (x >= item.x[0] && x <= item.x[1] && y >= item.y[0] && y <= item.y[1]) {
          if (checkIdiom[i] != null) {
            break;
          }
          this.style.left = blankRect[i].left + 'px';
          this.style.top = blankRect[i].top + 'px';
          checkIdiom[i] = { text: el.innerText, el, index };
          if (checkIdiom.length === 4 && !checkIdiom.includes(undefined)) {
            setTimeout(checkIdiomFunc, 500);
          }
          return;
        }
      }

      for (let i = 0; i < checkIdiom.length; i++) {
        if (checkIdiom && checkIdiom[i].index == index) {
          checkIdiom[i] = undefined;
          break;
        }
      }
      const item = cellRect[parseInt(index)];
      this.style.left = item.left + 'px';
      this.style.top = item.top + 'px';
      this.style.position = 'static'; // 这个很重要!不改的话滑动页面字会跟着动
    }

    function checkIdiomFunc() {
      console.log("检查中")
      const idiom = checkIdiom.map(c => c.text).join("");
      const flag = idioms.find(idm => idm === idiom)
      if (flag != null) {
        alert("成功!找到:" + idiom)
      } else {
        alert("失败!!!")
      }
      checkIdiom.map(c => {
        let el = c.el;
        let index = c.index;
        const _el = cellRect[parseInt(index)];
        el.style.left = _el.left + 'px';
        el.style.top = _el.top + 'px';
        el.style.position = 'static';
      })
      checkIdiom = [];
    }

    init();
  </script>
</body>

</html>

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值