运营大大们来捉迷藏了,你能找到他们吗?

简介: 运营大大们来捉迷藏了,你能找到他们吗?

前言


最近的日子好忙,好在游戏活动结束之前,小包还是完成了最初的设想,这个游戏大家都不陌生(原谅小包先卖个关子),是一个非常经典的游戏,但小包将原来的二维游戏转化为三维游戏,下面我们来一起瞅瞅吧。


游戏介绍


游戏名称: 捉迷藏的运营大大们


游戏内容:


  1. 游戏版图为 10*10 的立体网格,网格左键点击后可以翻转,右键点击可以标注 🚩
  2. 运营都藏在网格的底面,运营周围的格子会有数字提示,数字表示当前格子周围运营的数量(是不是有些熟悉),根据数字提示可以推理出运营位置。
  3. 运营的数量,嘿嘿,小包不告诉你。
  4. 当找到或标注所有的运营位置后,游戏胜利。
  5. 如果你点击到运营,游戏失败。


设计思想:


  1. 很早之前小包就想实现 3D 扫雷效果,但没有找到很好的创意,前几天小包看朋友玩游戏,有个翻牌环节,然后本游戏就诞生了 —— 翻牌式扫雷游戏。
  2. 来到掘金社区也快半年之久了,参加活动,写技术文章,接触到好多可爱负责的运营,因此小包转变了这个游戏的设定,使用运营来替换雷的概念,你能找到这些可爱负责的运营们吗?(给可爱负责的运营大佬们打 call)


素材:


素材选自掘金运营大大的头像,加个小问题,看头像识运营,你能认出多少?


游戏中使用了运营大大们的头像,如涉及侵权请联系小包


游戏预览:


image.png

游戏体验


欢迎大家体验立体寻运营游戏。你可以有两种体验方式:

image.png



游戏设计


我们首先来分析一下这个游戏:


  1. 游戏地图由 10 * 10 个网格构成,每个网格是一个长方体,长方体的底面存放运营图片或者周围运营数量的提示,顶面可以标记 🚩
  2. 点击网格后,网格会发生翻转


布局方面实现,小包认为有以下亮点:


  • 长方体网格实现
  • 网格布局
  • 底面与顶面显示


长方体格子


CSS 可以实现简单的长方体,其原理基于 transform-rotate 配合 transform-translate 实现。


<!-- face 为立方体六个面 -->
<div class="cube">
  <div class="face"></div>
  <div class="face"></div>
  <div class="face"></div>
  <div class="face"></div>
  <div class="face"></div>
  <div class="face"></div>
</div>
复制代码


Step1: 给 cube 添加 preserve-3d 效果


.cube {
  /*设置3d场景*/
  transform-style: preserve-3d;
  /*调整角度,使六个角度都可以被看到*/
  transform: rotateX(-35deg) rotateY(30deg);
  position: relative;
}
复制代码


Step2: 设置 face 的通用样式


.face {
  height: 50px;
  width: 50px;
  position: absolute;
  /* 设置 transform 的中心 */
  transform-origin: 50% 50%;
}
复制代码


Step3: 设置顶面


.face:nth-child(1) {
  background-color: wheat;
  transform: rotateY(0deg) translateZ(5px);
}
复制代码


image.png


Step4: 设置其他面


.face:nth-child(2) {
  background-color: #efca86;
  width: 10px;
  transform: rotateY(90deg) translateZ(45px);
}
.face:nth-child(3) {
  background-color: #fdcb6e;
  transform: rotateY(180deg) translateZ(5px);
}
.face:nth-child(4) {
  background-color: #efca86;
  width: 10px;
  transform: rotateY(270deg) translateZ(5px);
}
.face:nth-child(5) {
  background-color: #efca86;
  height: 10px;
  transform: rotateX(90deg) translateZ(5px);
}
.face:nth-child(6) {
  background-color: #efca86;
  height: 10px;
  transform: rotateX(-90deg) translateZ(45px);
}
复制代码


然后我们就可以成功的实现一个立体格子。


image.png

网格布局


游戏共有 10 * 10 个网格构成,可以选择的布局方式有很多,但小包认为 grid 布局最为适合,因此小包设置了如下样式:


.board {
  display: grid;
  grid-gap: 10px;
  /* 行列大小及网格数量 */
  grid-template-columns: repeat(10, 50px);
  grid-template-rows: repeat(10, 50px);
  position: relative;
  top: 20px;
  /* 设置 3d 效果 */
  transform-style: preserve-3d;
  transform: rotateX(50deg) rotateZ(22deg);
}
复制代码


image.png

顶面与底面的显示


底面显示效果算是游戏的难点之一,底面元素有三种情况:


  • 运营
  • 数字提示
  • 空白


我们如何来标识这个区别呐?


  • data-id 存放当前网格的横纵坐标
  • data-tile 存放当前是否为运营,如果是运营,则属性值不为空
  • data-num 存放周围运营数量的提示


<div class="cube" data-id="2,8" data-tile="zoe" data-num="1">
  <div class="face"></div>
  <div class="face"></div>
  <div class="face"></div>
  <div class="face"></div>
  <div class="face"></div>
  <div class="face"></div>
</div>
复制代码


data-tile 是用来标识当前网格是否为运营,因此我们可以通过属性选择器来配置网格显示对应运营的头像。


/* 这样我们就可以任意的设置运营位置 */
[data-tile="captain"] div:nth-child(3) {
  background: #fff8e7 url(./images/captain.png) center center no-repeat;
  background-size: 60px;
}
[data-tile="zoe"] div:nth-child(3) {
  background: #fff8e7 url(./images/zoe.png) center center no-repeat;
  background-size: 60px;
}
复制代码


其他部分


  • 翻转效果是通过增删类名配合 transition 所实现,小包先预定义了 flipped 类,类中定义了 transition 语法,当点击网格时,给网格添加 flipped 类名,实现翻转效果。


.flipped {
  pointer-events: none;
  transform: rotateY(180deg) translateZ(0);
  // transition 时间不易设置太长,因为当点击网格时会触发多个网格翻转,设置太长会有明显卡顿感
  transition: all 200ms linear;
}
复制代码


  • 标注 🚩 效果: 通过增删 tile-flagged 类名来区分网格是否被标注,同时配合 innerHTML 修改网格顶部文字实现 🚩 与 空值的切换。


游戏逻辑


乱序算法


常规乱序方法


提到乱序,我们很容易想起这样一种实现方式,基于 Math.random0.5 的比较进行实现,众所周知,Math.random 是伪随机数,这种乱序实现效果其实是非常差的。


var values = [1, 2, 3, 4, 5];
values.sort(function(){
    return Math.random() - 0.5;
});
console.log(values)
复制代码


具体的测试过程可以参考羽哥文章: JavaScript专题之乱序


测试原理是:将 [1, 2, 3, 4, 5] 乱序 10 万次,计算乱序后的数组的最后一个元素是 1、2、3、4、5 的次数分别是多少。 一次随机的结果为:


[30636, 30906, 20456, 11743, 6259]
复制代码


Fisher–Yates算法


Fisher–Yates 也被成为洗牌算法,实现思路非常简单: 遍历数组元素,然后将当前元素与以后随机位置的元素进行交换。


// 根据上面的思想,我们就可以写出下列随机算法
function shuffleArray(array) {
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]];
  }
  return array;
}
复制代码


羽哥同样对 Fisher–Yates 进行了测试,最终结果如下:


image.png


我们可以看到,所有乱序的结果是非常接近的,乱序算法不选他选谁。


为什么需要乱序


上面讲解了乱序的实现,大家有可能会奇怪,我们这个项目中为什么会使用乱序算法呐?小包设计游戏之初选择了 many 运营,但小包不想每个运营都以固定的顺序,因此小包为运营的顺序添加乱序效果。


const opetarors = shuffleArray([...tileOptions]);
复制代码


绘制网格


运营捉迷藏由 10 * 10 个格子构成,每个格子为一个长方体网格,我们提前预定义一个网格,渲染时将网格动态插入,同时为每个网格绑定事件处理函数(但这里并不是最佳的解决方案,我们可以通过事件委托的方式来实现)


创建网格的函数


function buildTile(i, j) {
  const tile = clone.cloneNode(true);
  tile.classList.remove("clone");
  tile.classList.add("cube");
  tile.setAttribute("data-id", `${i},${j}`);
  // 左键点击翻转效果
  tile.addEventListener("click", function (e) {
    clickTile(tile);
  });
  tile.oncontextmenu = function (e) {
    e.preventDefault();
    // 标注🚩
    flag(tile);
  };
  return tile;
}
复制代码


构建整个棋盘


function createBoard() {
  let opeIndex = 0;
  const opetarors = shuffleArray([...tileOptions]);
  for (let i = 0; i < boardSize; i++) {
    for (let j = 0; j < boardSize; j++) {
      board.appendChild(buildTile(i, j));
    }
  }
  tiles = document.getElementsByClassName("cube");
}
复制代码


数字计算逻辑


数字计算逻辑(网格周围运营数量提示)我认为是当前项目中的核心部分,小包自认为还是实现的比较巧妙。


当前游戏中,小包把运营数量求解集成在运营排布之后,具体做法是这样的:


  • 设置存储运营的数组 bombs 及存放运营提示的数组 numbers
  • 通过随机数生成运营位置,检查当前位置是否已经布置运营
  • 如果未布置,布置运营,同时将该运营周围 8 个格子坐标添加到 numbers 数组中
  • 当所有的运营布置完毕后,开始遍历 numbers 数组,统计每个坐标出现的次数,即为当前坐标周围运营的数目


// 设置运营位置
while (minesNum) {
  var mineIndex = Math.floor(Math.random() * 100);
  let x = Math.floor(mineIndex / 10),
    y = mineIndex % 10;
  let tile = tiles[x * 10 + y];
  // 若当前位置未布置运营
  if (!bombs.includes(`${x},${y}`)) {
    bombs.push(`${x},${y}`);
    tile.setAttribute("data-tile", opetarors[opeIndex++]);
    // 将运营周围的8个格子存储到 numbers 数组中
    if (x > 0) numbers.push(`${x - 1},${y}`);
    if (x < boardSize - 1) numbers.push(`${x + 1},${y}`);
    if (y > 0) numbers.push(`${x},${y - 1}`);
    if (y < boardSize - 1) numbers.push(`${x},${y + 1}`);
    if (x > 0 && y > 0) numbers.push(`${x - 1},${y - 1}`);
    if (x < boardSize - 1 && y < boardSize - 1)
      numbers.push(`${x + 1},${y + 1}`);
    if (y > 0 && x < boardSize - 1) numbers.push(`${x + 1},${y - 1}`);
    if (x > 0 && y < boardSize - 1) numbers.push(`${x - 1},${y + 1}`);
    minesNum--;
  }
}
// 遍历 numbers ,给对应坐标添加运营数量提示
numbers.forEach((num) => {
  let coords = num.split(",");
  let tile = tiles[parseInt(coords[0]) * 10 + parseInt(coords[1])];
  let dataNum = parseInt(tile.getAttribute("data-num"));
  if (!dataNum) dataNum = 0;
  tile.setAttribute("data-num", dataNum + 1);
});
复制代码


胜利逻辑


扫雷类游戏会有两种成功方式,一种是 🚩 出所有的"运营",另一种是只剩下"运营"位置未翻开。


🚩 出所有的"运营"


当选择 🚩 后,网格上会添加 tile--flagged 类名,下面两个条件达成后,则游戏胜利:


  • 具有 tile--flagged 类名的数量是否等于"运营"数量
  • 🚩 的网格是否全是"运营"


function checkFlagVictory() {
  const flagNum = document.querySelectorAll(".tile--flagged").length;
  // 如果这里都不相同,后面无需判断
  if (flagNum != tileOptions.length) {
    return;
  }
  let cnt = 0;
  for (let tile of tiles) {
    let coordinate = tile.getAttribute("data-id");
    if (
      tile.classList.contains("tile--flagged") &&
      bombs.includes(coordinate)
    ) {
      cnt++;
    }
  }
  let win = cnt === tileOptions.length ? true : false;
  if (win) {
    gameOver = true;
    overlay.classList.remove("hidden");
    infoP.innerHTML = "恭喜你,成功找到所有运营!!!";
  }
}
复制代码


只剩下"运营"位置未翻开


网格翻转后,对应网格会添加 tile--checked 类名,因此我们可以通过 tile--checked 与是否为"运营"进行判断。


const checkVictory = () => {
  // 两种成功的情况
  let win = true;
  for (let tile of tiles) {
    let coordinate = tile.getAttribute("data-id");
    if (
      !tile.classList.contains("tile--checked") &&
      !bombs.includes(coordinate)
    ) {
      win = false;
    }
  }
  if (win) {
    gameOver = true;
    overlay.classList.remove("hidden");
    infoP.innerHTML = "恭喜你,成功找到所有运营!!!";
  }
};
复制代码


源码仓库


源码地址: 3d寻找运营游戏在线体验


项目地址: 3d寻找运营游戏源码


如果感觉有帮助的话,别忘了给小包点个 ⭐ 。




相关文章
|
4月前
|
API
深入理解技术内容运营
营销是一种商业策略,涉及识别客户需求并确定如何最好地满足这些需求。换句话说,它旨在确保企业或产品以吸引目标受众的方式定位,鼓励他们购买。该策略包含多个方面,包括市场研究、品牌建设、产品开发、销售、促销和公共关系。营销人员使用这些策略来创建公司形象,建立客户关系,吸引潜在客户并留住现有客户。营销的最终目的是促进销售并帮助企业发展。
75 0
|
数据采集 安全 搜索推荐
外贸独立站的全方位运营指南:从定位到转化的完整解决方案
通过数据分析,可以深入了解用户行为,挖掘用户需求,不断改进网站设计和内容制作,从而提高转化率。
199 0
外贸独立站的全方位运营指南:从定位到转化的完整解决方案
《开放搜索在智能化行业搜索和业务增长领域的应用实践》电子版地址
《开放搜索在智能化行业搜索和业务增长领域的应用实践》PDF
124 0
《开放搜索在智能化行业搜索和业务增长领域的应用实践》电子版地址
|
人工智能 Oracle 大数据
31%和187%,神州数码找到云转型路径
31%和187%,神州数码找到云转型路径
160 0
31%和187%,神州数码找到云转型路径
|
存储 安全 专有云
如何找到适合你的那片云
如何找到适合你的那片云
140 0
如何找到适合你的那片云
|
自然语言处理 监控 搜索推荐
搜索运营有哪些玩法,你知道吗?
大部分产品的搜索都是技术同学在不断的优化迭代,很容易忽视可以直接触达业务侧的运营同学的作用和价值。那今天就和大家分享下运营同学在搜索上有哪些玩法?阿里云开放搜索作为一站式可视化搜索开发平台,产品/运营同学可以在控制台参与哪些优化动作呢?
949 1
搜索运营有哪些玩法,你知道吗?
|
机器学习/深度学习 存储 人工智能
如何让用户找到想要的内容?阿里文娱搜索算法实践
视频搜索是涉及信息检索,自然语言处理(NLP),机器学习、计算机视觉(CV)等多领域的综合应用场景,随着深度学习在这些领域的长足进展以及用户对视频生产和消费的广泛需求,视频搜索技术的发展在学术和工业界都取得了飞速的发展。
如何让用户找到想要的内容?阿里文娱搜索算法实践
|
人工智能 机器人 Java
揭秘RPA卓越中心:到底是什么?为什么要建立?有什么价值?
关于RPA卓越中心的一切:RPA卓越中心到底是什么?为什么要建立RPA卓越中心?它有什么价值?
1997 0
ImageDT王芹:从场景出发,在市场验证下找到技术与零售的结合点
因为了解场景的需求点,ImageDT的产品一出现就备受市场认可,也因此受到了红杉资本、真格基金等多家投资机构的认可。
330 0