前端游戏巨制! CSS居然可以做3D游戏了

开发 前端
了解过css3D属性的同学应该都了解过perspective、perspective-origin、transform-style: preserve-3d这个三个属性值, 它们构成了CSS的3d世界.

 [[425774]]

前言

偶然接触到CSS的3D属性, 就萌生了一种做3D游戏的想法.

了解过css3D属性的同学应该都了解过perspective、perspective-origin、transform-style: preserve-3d这个三个属性值, 它们构成了CSS的3d世界.

同时, 还有transform属性来对3D的节点进行平移、缩放、旋转以及拉伸.

属性值很简单, 在我们平时的web开发中也很少用到.

那用这些CSS3D属性可以做3D游戏吗?

当然是可以的.

即使只有沙盒, 也有我的世界这种神作.

今天我就来带大家玩一个从未有过的全新3D体验.

废话不多说, 我们先来看下效果:

这里是试玩地址pc端畅玩[1]

我们要完成这个迷宫大作战,需要完成以下步骤:

  1.  创建一个3D世界
  2.  写一个3D相机的功能
  3.  创建一座3D迷宫
  4.  创建一个可以自由运动的玩家
  5.  在迷宫中找出一条最短路径提示

我们先来看下一些前置知识.

做一款CSS3D游戏需要的知识和概念

CSS3D坐标系

在css3D中, 首先要明确一个概念, 3D坐标系.

使用左手坐标系, 伸出我们的左手, 大拇指和食指成L状, 其他手指与食指垂直, 如图:

WechatIMG315.png 

大拇指为X轴, 食指为Y轴, 其他手指为Z轴.

这个就是CSS3D中的坐标系.

透视属性

perspective为css中的透视属性.

这个属性是什么意思呢, 可以把我们的眼睛看作观察点, 眼睛到目标物体的距离就是视距, 也就是这里说的透视属性.

大家都知道, 「透视」+「2D」= 「3D」. 

  1. perspective: 1200px;  
  2. -webkit-perspective:  1200px;  
  3. 复制代码 

3D相机

在3D游戏开发中, 会有相机的概念, 即是人眼所见皆是相机所见.

在游戏中场景的移动, 大部分都是移动相机.

例如赛车游戏中, 相机就是跟随车子移动, 所以我们才能看到一路的风景.

在这里, 我们会使用CSS去实现一个伪3d相机.

变换属性

在CSS3D中我们对3D盒子做平移、旋转、拉伸、缩放使用transform属性.

  •  translateX 平移X轴
  •  translateY 平移Y轴
  •  translateZ 平移Z轴
  •  rotateX 旋转X轴
  •  rotateY 旋转Y轴
  •  rotateZ 旋转Z轴
  •  rotate3d(x,y,z,deg) 旋转X、Y、Z轴多少度

注意:

这里「先平移再旋转」和「先旋转再平移」是不一样的

 旋转的角度都是角度值.

这里还有不清楚的同学可以参阅羽飞的这篇[juejin.cn/post/699769…[2]] 附带有demo

矩阵变换

我们完成游戏的过程中会用到矩阵变换.

在js中, 获取某个节点的transform属性, 会得到一个矩阵, 这里我打印一下, 他就是长这个样子: 

  1. var _ground = document.getElementsByClassName("ground")[0];  
  2. var bg_style = document.defaultView.getComputedStyle(_ground, null).transform;  
  3. console.log("矩阵变换---->>>",bg_style) 
  4. 复制代码 

WechatIMG307.png

那么我们如何使用矩阵去操作transform呢?

在线性变换中, 我们都会去使用矩阵的相乘.

CSS3D中使用4*4的矩阵进行3D变换.

下面的矩阵我均用二维数组表示.

例如matrix3d(1,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,1)可以用二维数组表示: 

  1.  
  2.     [1, 0, 0, 0],  
  3.     [0, 1, 0, 0],  
  4.     [0, 0, 1, 0],  
  5.     [0, 0, 0, 1]  
  6.  
  7. 复制代码 

平移即使使用原来状态的矩阵和以下矩阵相乘, dx, dy, dz分别是移动的方向x, y, z. 

  1.  
  2.     [1, 0, 0, dx],  
  3.     [0, 1, 0, dy],  
  4.     [0, 0, 1, dz],  
  5.     [0, 0, 0, 1]  
  6.  
  7. 复制代码 

绕X轴旋转𝞱, 即是与以下矩阵相乘. 

  1.  
  2.     [1, 0, 0, 0],  
  3.     [0, cos𝞱, sin𝞱, 0],  
  4.     [0, -sin𝞱, cos𝞱, 0],  
  5.     [0, 0, 0, 1]  
  6.  
  7. 复制代码 

绕Y轴旋转𝞱, 即是与以下矩阵相乘. 

  1.  
  2.     [cos𝞱, 0, -sin𝞱, 0],  
  3.     [0, 1, 0, 0],  
  4.     [sin𝞱, 0, cos𝞱, 0],  
  5.     [0, 0, 0, 1]  
  6.  
  7. 复制代码 

绕Z轴旋转𝞱, 即是与以下矩阵相乘. 

  1. [ 
  2.     [cos𝞱, sin𝞱, 0, 0],  
  3.     [-sin𝞱, cos𝞱, 0, 0],  
  4.     [0, 0, 1, 0],  
  5.     [0, 0, 0, 1]  
  6.  
  7. 复制代码 

具体的矩阵的其他知识这里讲了, 大家有兴趣可以自行下去学习.

我们这里只需要很简单的旋转应用.

开始创建一个3D世界

我们先来创建UI界面.

  •  相机div
  •  地平线div
  •  棋盘div
  •  玩家div(这里是一个正方体)

注意

正方体先旋转在平移, 这种方法应该是最简单的.

一个平面绕X轴、Y轴旋转180度、±90度, 都只需要平移Z轴.

这里大家试过就明白了.

我们先来看下html部分:   

  1. <div class="camera">  
  2.         <!-- 地面 -->  
  3.         <div class="ground">  
  4.             <div class="box">  
  5.                 <div class="box-con">  
  6.                     <div class="wall">z</div>  
  7.                     <div class="wall">z</div>  
  8.                     <div class="wall">y</div>  
  9.                     <div class="wall">y</div>  
  10.                     <div class="wall">x</div>  
  11.                     <div class="wall">x</div>  
  12.                     <div class="linex"></div>  
  13.                     <div class="liney"></div>  
  14.                     <div class="linez"></div>  
  15.                 </div>  
  16.                 <!-- 棋盘 -->  
  17.                 <div class="pan"></div>  
  18.             </div> 
  19.          </div>  
  20.     </div> 
  21.  复制代码 

很简单的布局, 其中linex、liney、linez是我画的坐标轴辅助线.

红线为X轴, 绿线为Y轴, 蓝线为Z轴. 接着我们来看下正方体的主要CSS代码. 

  1. ...  
  2.   .box-con{  
  3.       width: 50px; 
  4.       height: 50px;  
  5.       transform-style: preserve-3d;  
  6.       transform-origin: 50% 50%;  
  7.       transform: translateZ(25px) ;  
  8.       transition: all 2s cubic-bezier(0.075, 0.82, 0.165, 1);  
  9.   }  
  10.   .wall{  
  11.       width: 100%;  
  12.       height: 100%;  
  13.       border: 1px solid #fdd894;  
  14.       background-color: #fb7922;   
  15.    }  
  16.   .wall:nth-child(1) {  
  17.       transform: translateZ(25px);  
  18.   }  
  19.   .wall:nth-child(2) {  
  20.       transform: rotateX(180deg) translateZ(25px);  
  21.   }  
  22.   .wall:nth-child(3) {  
  23.       transform: rotateX(90deg) translateZ(25px);  
  24.   }  
  25.   .wall:nth-child(4) {  
  26.       transform: rotateX(-90deg) translateZ(25px); 
  27.    } 
  28.    .wall:nth-child(5) {  
  29.       transform: rotateY(90deg) translateZ(25px);  
  30.   }  
  31.   .wall:nth-child(6) {  
  32.       transform: rotateY(-90deg) translateZ(25px);  
  33.   }  
  34. 复制代码 

粘贴一大堆CSS代码显得很蠢.

其他CSS这里就不粘贴了, 有兴趣的同学可以直接下载源码查看. 界面搭建完成如图所示:

WechatIMG308.png

接下来就是重头戏了, 我们去写js代码来继续完成我们的游戏.

完成一个3D相机功能

相机在3D开发中必不可少, 使用相机功能不仅能查看3D世界模型, 同时也能实现很多实时的炫酷功能.

一个3d相机需要哪些功能?

最简单的, 上下左右能够360度无死角观察地图.同时需要拉近拉远视距.

通过鼠标交互

鼠标左右移动可以旋转查看地图; 鼠标上下移动可以观察上下地图; 鼠标滚轮可以拉近拉远视距.

✅ 1. 监听鼠标事件

首先, 我们需要通过监听鼠标事件来记录鼠标位置, 从而判断相机上下左右查看. 

  1. /** 鼠标上次位置 */  
  2.  var lastX = 0lastY = 0 
  3.    /** 控制一次滑动 */  
  4.  var isDown = false 
  5.    /** 监听鼠标按下 */  
  6.  document.addEventListener("mousedown", (e) => {  
  7.      lastX = e.clientX;  
  8.      lastY = e.clientY;  
  9.      isDown = true 
  10.  });  
  11.      /** 监听鼠标移动 */  
  12.  document.addEventListener("mousemove", (e) => {  
  13.      if (!isDown) return;  
  14.      let _offsetX = e.clientX - lastX; 
  15.      let _offsetY = e.clientY - lastY;  
  16.      lastX = e.clientX;  
  17.      lastY = e.clientY;  
  18.      //判断方向  
  19.      var dirH = 1dirV = 1 
  20.      if (_offsetX < 0) {  
  21.          dirH = -1;  
  22.      }  
  23.      if (_offsetY > 0) {  
  24.          dirV = -1;  
  25.      }  
  26.  });  
  27.  document.addEventListener("mouseup", (e) => {  
  28.      isDown = false 
  29.  });  
  30. 码 

✅ 2. 判断相机上下左右

使用perspective-origin来设置相机的上下视线.

使用transform来旋转Z轴查看左右方向上的360度. 

  1. /** 监听鼠标移动 */  
  2.     document.addEventListener("mousemove", (e) => {  
  3.         if (!isDown) return;  
  4.         let _offsetX = e.clientX - lastX;  
  5.         let _offsetY = e.clientY - lastY;  
  6.         lastX = e.clientX;  
  7.         lastY = e.clientY;  
  8.         var bg_style = document.defaultView.getComputedStyle(_ground, null).transform;  
  9.         var camera_style = document.defaultView.getComputedStyle(_camera, null).perspectiveOrigin;  
  10.         var matrix4 = new Matrix4(); 
  11.         var _cy = +camera_style.split(' ')[1].split('px')[0];  
  12.         var str = bg_style.split("matrix3d(")[1].split(")")[0].split(",");  
  13.         var oldMartrix4 = str.map((item) => +item);  
  14.         var dirH = 1dirV = 1 
  15.         if (_offsetX < 0) {  
  16.             dirH = -1;  
  17.         }  
  18.         if (_offsetY > 0) {  
  19.             dirV = -1;  
  20.         }  
  21.         //每次移动旋转角度  
  22.         var angleZ = 2 * dirH;  
  23.         var newMartri4 = matrix4.set(Math.cos(angleZ * Math.PI / 180), -Math.sin(angleZ * Math.PI / 180), 0, 0, Math.sin(angleZ * Math.PI / 180), Math.cos(angleZ * Math.PI / 180), 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); 
  24.          var new_mar = null 
  25.         if (Math.abs(_offsetX) > Math.abs(_offsetY)) {  
  26.             new_mar = matrix4.multiplyMatrices(oldMartrix4, newMartri4);  
  27.         } else {  
  28.             _camera.style.perspectiveOrigin = `500px ${_cy + 10 * dirV}px`;  
  29.         }  
  30.         new_mar && (_ground.style.transform = `matrix3d(${new_mar.join(',')})`);  
  31.     });  
  32. 复制代码 

这里使用了矩阵的方法来旋转Z轴, 矩阵类Matrix4是我临时写的一个方法类, 就俩方法, 一个设置二维数组matrix4.set, 一个矩阵相乘matrix4.multiplyMatrices.

文末的源码地址中有, 这里就不再赘述了.

✅ 3. 监听滚轮拉近拉远距离

这里就是根据perspective来设置视距. 

  1. //监听滚轮  
  2. document.addEventListener('mousewheel', (e) => {  
  3.     var per = document.defaultView.getComputedStyle(_camera, null).perspective;  
  4.     let newper = (+per.split("px")[0] + Math.floor(e.deltaY / 10)) + "px";  
  5.     _camera.style.perspective = newper  
  6. }, false);  
  7. 复制代码 

注意:

perspective-origin属性只有X、Y两个值, 做不到和u3D一样的相机.

我这里取巧使用了对地平线的旋转, 从而达到一样的效果.

 滚轮拉近拉远视距有点别扭, 和3D引擎区别还是很大.

完成之后可以看到如下的场景, 已经可以随时观察我们的地图了.

index1.gif

这样子, 一个3D相机就完成, 大家有兴趣的可以自己下去写一下, 还是很有意思的.

绘制迷宫棋盘

绘制格子地图最简单了, 我这里使用一个15*15的数组.

「0」代表可以通过的路, 「1」代表障碍物. 

  1. var grid = [  
  2.     0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0,  
  3.     0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0,  
  4.     1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1,  
  5.     0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1,  
  6.     0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0,  
  7.     0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0,  
  8.     0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0,  
  9.     1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0,  
  10.     1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 
  11.     0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,  
  12.     1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1,  
  13.     0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,  
  14.     1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0,  
  15.     1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0,  
  16.     0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0  
  17. ];  
  18. 复制代码 

然后我们去遍历这个数组, 得到地图.

写一个方法去创建地图格子, 同时返回格子数组和节点数组.

这里的block是在html中创建的一个预制体, 他是一个正方体.

然后通过克隆节点的方式添加进棋盘中. 

  1. /** 棋盘 */  
  2. function pan() {  
  3.     const con = document.getElementsByClassName("pan")[0];  
  4.     const block = document.getElementsByClassName("block")[0];  
  5.     let elArr = [];  
  6.     grid.forEach((item, index) => {  
  7.         let r = Math.floor(index / 15);  
  8.         let c = index % 15;  
  9.         const gezi = document.createElement("div");  
  10.         gezi.classList = "pan-item"  
  11.         // gezi.innerHTML = `${r},${c}`  
  12.         con.appendChild(gezi);  
  13.         var newBlock = block.cloneNode(true);  
  14.         //障碍物  
  15.         if (item == 1) {  
  16.             gezi.appendChild(newBlock);  
  17.             blockArr.push(c + "-" + r);  
  18.         }  
  19.         elArr.push(gezi);  
  20.     });  
  21.     const panArr = arrTrans(15, grid);  
  22.     return { elArr, panArr };  
  23.  
  24. const panpanData = pan();  
  25. 复制代码 

可以看到, 我们的界面已经变成了这样.

WechatIMG310.png

 

接下来, 我们需要去控制玩家移动了.

控制玩家移动

通过上下左右w s a d键来控制玩家移动.

使用transform来移动和旋转玩家盒子.

✅ 监听键盘事件

通过监听键盘事件onkeydown来判断key值的上下左右. 

  1. document.onkeydown = function (e) {  
  2.     /** 移动物体 */  
  3.     move(e.key);  
  4.  
  5. 复制代码 

✅ 进行位移

在位移中, 使用translate来平移, Z轴始终正对我们的相机, 所以我们只需要移动X轴和Y轴.

声明一个变量记录当前位置.

同时需要记录上次变换的transform的值, 这里我们就不继续矩阵变换了. 

  1. /** 当前位置 */  
  2. var position = { x: 0, y: 0 };  
  3. /** 记录上次变换值 */  
  4. var lastTransform = {  
  5.     translateX: '0px',  
  6.     translateY: '0px',  
  7.     translateZ: '25px',  
  8.     rotateX: '0deg',  
  9.     rotateY: '0deg',  
  10.     rotateZ: '0deg'  
  11. };  
  12. 复制代码 

每一个格子都可以看成是二维数组的下标构成, 每次我们移动一个格子的距离. 

  1.  switch (key) {  
  2.     case 'w':  
  3.         position.y++; 
  4.         lastTransform.translateY = position.y * 50 + 'px';  
  5.         break;  
  6.     case 's':  
  7.         position.y--;  
  8.         lastTransform.translateY = position.y * 50 + 'px';  
  9.         break;  
  10.     case 'a':  
  11.         position.x++;  
  12.         lastTransform.translateX = position.x * 50 + 'px';  
  13.         break;  
  14.     case 'd':  
  15.         position.x--;  
  16.         lastTransform.translateX = position.x * 50 + 'px';  
  17.         break;  
  18.  
  19. //赋值样式  
  20. for (let item in lastTransform) {  
  21.     strTransfrom += item + '(' + lastTransform[item] + ') ';  
  22.  
  23. target.style.transform = strTransfrom 
  24. 复制代码 

到这里, 我们的玩家盒子已经可以移动了.

注意

在css3D中的平移可以看成是世界坐标.

 所以我们只需要关心X、Y轴. 而不需要去移动Z轴. 即使我们进行了旋转.

✅ 在移动的过程中进行旋转

在CSS3D中, 3D旋转和其他3D引擎中不一样, 一般的诸如u3D、threejs中, 在每次旋转完成之后都会重新校对成世界坐标, 相对来说 就很好计算绕什么轴旋转多少度.

然而, 笔者也低估了CSS3D的旋转.

我以为上下左右滚动一个正方体很简单. 事实并非如此.

CSS3D的旋转涉及到四元数和万向锁.

比如我们旋转我们的玩家盒子. 如图所示:

首先, 第一个格子(0,0)向上绕X轴旋转90度, 就可以到达(1.0); 向左绕Y轴旋转90度, 可以到达

(0,1); 那我们是不是就可以得到规律如下:

WechatIMG312.png

如图中所示, 单纯的向上下, 向左右绕轴旋转没有问题, 但是要旋转到红色的格子, 两种不同走法, 到红色的格子之后旋转就会出现两种可能. 从而导致旋转出错.

同时这个规律虽然难寻, 但是可以写出来, 最重要的是, 按照这个规律来旋转CSS3D中的盒子, 是不对的

那有人就说了, 这不说的屁话吗?

经过笔者实验, 倒是发现了一些规律. 我们继续按照这个规律往下走.

  •  旋转X轴的时候, 同时看当前Z轴的度数, Z轴为90度的奇数倍, 旋转Y轴, 否则旋转X轴.
  •  旋转Y轴的时候, 同时看当前Z轴的度数, Z轴为90度的奇数倍, 旋转X轴, 否则旋转Z轴.
  •  旋转Z轴的时候, 继续旋转Z轴

这样子我们的旋转方向就搞定了. 

  1. if (nextRotateDir[0] == "X") {  
  2.     if (Math.floor(Math.abs(lastRotate.lastRotateZ) / 90) % 2 == 1) {  
  3.         lastTransform[`rotateY`] = (lastRotate[`lastRotateY`] + 90 * dir) + 'deg';  
  4.     } else {  
  5.         lastTransform[`rotateX`] = (lastRotate[`lastRotateX`] - 90 * dir) + 'deg';  
  6.     }  
  7.  
  8. if (nextRotateDir[0] == "Y") {  
  9.     if (Math.floor(Math.abs(Math.abs(lastRotate.lastRotateZ)) / 90) % 2 == 1) {  
  10.         lastTransform[`rotateX`] = (lastRotate[`lastRotateX`] + 90 * dir) + 'deg';  
  11.     } else {  
  12.         lastTransform[`rotateZ`] = (lastRotate[`lastRotateZ`] + 90 * dir) + 'deg';  
  13.     }  
  14.  
  15. if (nextRotateDir[0] == "Z") {  
  16.     lastTransform[`rotate${nextRotateDir[0]}`] = (lastRotate[`lastRotate${nextRotateDir[0]}`] - 90 * dir) + 'deg';  
  17.  
  18. 复制代码 

然而, 这还没有完, 这种方式的旋转还有个坑, 就是我不知道该旋转90度还是-90度了.

这里并不是简单的上下左右去加减.

旋转方向对了, 旋转角度不知该如何计算了.

具体代码可以查看源码[3].

彩蛋时间

⚠️⚠️⚠️ 同时这里会伴随着「万向锁」的出现, 即是Z轴与X轴重合了. 哈哈哈哈~

⚠️⚠️⚠️ 这里笔者还没有解决, 也希望万能的网友能够出言帮忙~

⚠️⚠️⚠️ 笔者后续解决了会更新的. 哈哈哈哈, 大坑.

好了, 这里问题不影响我们的项目. 我们继续讲如何找到最短路径并给出提示.

最短路径的计算

在迷宫中, 从一个点到另一个点的最短路径怎么计算呢? 这里笔者使用的是广度优先遍历(BFS)算法来计算最短路径.

我们来思考:

  1.  二维数组中找最短路径
  2.  每一格的最短路径只有上下左右相邻的四格
  3.  那么只要递归寻找每一格的最短距离直至找到终点

这里我们需要使用「队列」先进先出的特点.

我们先来看一张图:

WechatIMG313.png

很清晰的可以得到最短路径.

注意

使用两个长度为4的数组表示上下左右相邻的格子需要相加的下标偏移量.

每次入队之前需要判断是否已经入队了.

每次出队时需要判断是否是终点.

需要记录当前入队的目标的父节点, 方便获取到最短路径.

我们来看下代码: 

  1. //春初路径  
  2. var stack = [];  
  3. /**  
  4.  * BFS 实现寻路  
  5.  * @param {*} grid   
  6.  * @param {*} start {x: 0,y: 0}  
  7.  * @param {*} end {x: 3,y: 3}  
  8.  */  
  9. function getShortPath(grid, start, end, a) {  
  10.     let maxL_x = grid.length;  
  11.     let maxL_y = grid[0].length;  
  12.     let queue = new Queue();  
  13.     //最短步数  
  14.     let step = 0 
  15.     //上左下右  
  16.     let dx = [1, 0, -1, 0];  
  17.     let dy = [0, 1, 0, -1];  
  18.     //加入第一个元素  
  19.     queue.enqueue(start);  
  20.     //存储一个一样的用来排查是否遍历过  
  21.     let mem = new Array(maxL_x);  
  22.     for (let n = 0; n < maxL_x; n++) {  
  23.         mem[n] = new Array(maxL_y);  
  24.         mem[n].fill(100);  
  25.     }  
  26.     while (!queue.isEmpty()) {  
  27.         let p = [];  
  28.         for (let i = queue.size(); i > 0; i--) {  
  29.             let preTraget = queue.dequeue();  
  30.             p.push(preTraget);  
  31.             //找到目标  
  32.             if (preTraget.x == end.x && preTraget.y == end.y) { 
  33.                  stack.push(p);  
  34.                 return step;  
  35.             }  
  36.             //遍历四个相邻格子  
  37.             for (let j = 0; j < 4; j++) {  
  38.                 let nextX = preTraget.x + dx[j];  
  39.                 let nextY = preTraget.y + dy[j];  
  40.                 if (nextX < maxL_x && nextX >= 0 && nextY < maxL_y && nextY >= 0) {  
  41.                     let nextTraget = { x: nextX, y: nextY };  
  42.                     if (grid[nextX][nextY] == a && a < mem[nextX][nextY]) {  
  43.                         queue.enqueue({ ...nextTraget, f: { x: preTraget.x, y: preTraget.y } });  
  44.                         mem[nextX][nextY] = a;  
  45.                     }  
  46.                 }  
  47.             }  
  48.         }  
  49.         stack.push(p);  
  50.         step++; 
  51.      }  
  52.  
  53. /* 找出一条最短路径**/  
  54. function recall(end) { 
  55.     let path = [];  
  56.     let front = { x: end.x, y: end.y };  
  57.     while (stack.length) {  
  58.         let item = stack.pop();  
  59.         for (let i = 0; i < item.length; i++) {  
  60.             if (!item[i].f) break;  
  61.             if (item[i].x == front.x && item[i].y == front.y) {  
  62.                 path.push({ x: item[i].x, y: item[i].y });  
  63.                 front.x = item[i].f.x;  
  64.                 front.y = item[i].f.y;  
  65.                 break;  
  66.             }  
  67.         }  
  68.     }  
  69.     return path;  
  70.  
  71. 复制代码 

这样子我们就可以找到一条最短路径并得到最短的步数.

然后我们继续去遍历我们的原数组(即棋盘原数组).

点击提示点亮路径. 

  1. var step = getShortPath(panArr, { x: 0, y: 0 }, { x: 14, y: 14 }, 0);  
  2. console.log("最短距离----", step);  
  3. _perstep.innerHTML = `请在<span>${step}</span>步内走到终点`;  
  4. var path = recall({ x: 14, y: 14 });  
  5. console.log("路径---", path);  
  6. /** 提示 */  
  7. var tipCount = 0 
  8. _tip.addEventListener("click", () => { 
  9.     console.log("9999", tipCount)  
  10.     elArr.forEach((item, index) => {  
  11.         let r = Math.floor(index / 15);  
  12.         let c = index % 15; 
  13.         path.forEach((_item, i) => {  
  14.             if (_item.x == r && _item.y == c) {  
  15.                 // console.log("ooo",_item)  
  16.                 if (tipCount % 2 == 0)  
  17.                     item.classList = "pan-item pan-path" 
  18.                 else  
  19.                     item.classList = "pan-item" 
  20.             }  
  21.         })  
  22.     });  
  23.     tipCount++;  
  24. });  
  25. 复制代码 

这样子, 我们可以得到如图的提示:

WechatIMG314.png

大功告成. 嘿嘿, 是不是很惊艳的感觉~

尾声

当然, 我这里的这个小游戏还有可以完善的地方 比如:

  •  可以增加道具, 拾取可以减少已走步数
  •  可以增加配置关卡
  •  还可以增加跳跃功能
  •  ...

原来如此, CSS3D能做的事还有很多, 怎么用全看自己的想象力有多丰富了.

哈哈哈, 真想用CSS3D写一个「我的世界」玩玩, 性能问题恐怕会有点大.

本文例子均在PC端体验较好.

试玩地址[4]

源码地址[5]

欢迎大家拍砖指正, 笔者功力尚浅, 如有不当之处请斧正! 

 

责任编辑:庞桂玉 来源: 前端教程
相关推荐

2021-12-28 10:52:10

鸿蒙HarmonyOS应用

2022-09-07 12:00:26

Python3D游戏

2012-12-24 08:48:25

iOSUnity3D

2023-08-18 08:00:00

游戏开发3D模型

2013-11-21 19:36:56

畅游游戏引擎Genesis-3D

2024-03-15 08:10:00

2010-09-08 11:26:26

Windows PhoXNA 4.0 3D游戏开发

2013-11-25 11:29:41

搜狐游戏引擎

2014-07-28 17:09:54

Cocos

2016-06-01 09:19:08

开发3D游戏

2017-07-12 23:08:03

白鹭引擎

2011-09-13 14:06:23

2011-05-07 14:15:30

工作站丽台Quadro FX

2020-07-27 10:30:41

人工智能机器学习技术

2022-06-14 07:51:10

Godot游戏引擎

2021-01-05 08:10:00

Css前端3D旋转透视

2015-06-26 11:51:26

HTML5JavaScript

2021-08-26 15:16:58

鸿蒙游戏3D

2012-12-24 08:46:50

iOSUnity3D

2020-03-06 10:34:40

AI 数据人工智能
点赞
收藏

51CTO技术栈公众号