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

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

前言


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


游戏介绍


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


游戏内容:


  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月前
产品运营方法论问题之在运营过程中如何找到增长的关键点
产品运营方法论问题之在运营过程中如何找到增长的关键点
|
4月前
|
搜索推荐
总是拿不下大客户 不妨从它的企业全历史行为数据里找找思路
在ToB领域,攻克大客户是一项复杂战役,需要综合团队能力、销售技巧及多方面因素。新工具——企业全历史行为数据,类似企业时间轴,但更庞大复杂,包含全面的行为记录。这种数据能帮助销售人员深入理解客户偏好,提供全场景接触点,甚至比客户自己更了解他们,从而提升营销策略的精准度和效果。利用这种数据可以打破传统沟通限制,创造更多机会。尝试运用企业全历史行为数据,或能为大客户营销打开新思路。
|
4月前
|
人工智能
客户在哪儿 AI:如何用最少场次的活动覆盖最多的目标客户
在ToB市场,线下活动是高效获客的关键。面对面交流增进信任,潜在客户集中,直接展示产品能缩短销售路径。然而,高成本和低效活动是挑战。通过分析目标客户历史活动数据,客户在哪儿AI帮助企业精准定位,以最少投入触达最多潜在客户,如仅10场活动即可覆盖44.9%年营收客户。
|
6月前
|
API
深入理解技术内容运营
营销是一种商业策略,涉及识别客户需求并确定如何最好地满足这些需求。换句话说,它旨在确保企业或产品以吸引目标受众的方式定位,鼓励他们购买。该策略包含多个方面,包括市场研究、品牌建设、产品开发、销售、促销和公共关系。营销人员使用这些策略来创建公司形象,建立客户关系,吸引潜在客户并留住现有客户。营销的最终目的是促进销售并帮助企业发展。
108 0
|
数据采集 安全 搜索推荐
外贸独立站的全方位运营指南:从定位到转化的完整解决方案
通过数据分析,可以深入了解用户行为,挖掘用户需求,不断改进网站设计和内容制作,从而提高转化率。
274 0
外贸独立站的全方位运营指南:从定位到转化的完整解决方案
|
存储 监控 安全
数据人必知!认识数据“四种”分类“五大”价值,帮企业找到核心数据
在大数据时代,企业首先要做的是收集大量数据,但收集数据并非仅是把收集过来的数据放到数据存储平台里面那么简单,更重要的是对数据进行分类、加工及管理。
数据人必知!认识数据“四种”分类“五大”价值,帮企业找到核心数据
《开放搜索在智能化行业搜索和业务增长领域的应用实践》电子版地址
《开放搜索在智能化行业搜索和业务增长领域的应用实践》PDF
140 0
《开放搜索在智能化行业搜索和业务增长领域的应用实践》电子版地址
|
人工智能 Oracle 大数据
31%和187%,神州数码找到云转型路径
31%和187%,神州数码找到云转型路径
218 0
31%和187%,神州数码找到云转型路径
|
存储 安全 专有云
如何找到适合你的那片云
如何找到适合你的那片云
185 0
如何找到适合你的那片云