<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 30710字,需瀏覽 62分鐘

           ·

          2021-09-10 16:02

          點(diǎn)擊上方 全棧前端精選,回復(fù)“【1】
          加入我們一起學(xué)習(xí),天天進(jìn)步


          前言

          偶然接觸到CSS的3D屬性, 就萌生了一種做3D游戲的想法.

          了解過(guò)css3D屬性的同學(xué)應(yīng)該都了解過(guò)perspective、perspective-origin、transform-style: preserve-3d這個(gè)三個(gè)屬性值, 它們構(gòu)成了CSS的3d世界.

          同時(shí), 還有transform屬性來(lái)對(duì)3D的節(jié)點(diǎn)進(jìn)行平移、縮放、旋轉(zhuǎn)以及拉伸.

          屬性值很簡(jiǎn)單, 在我們平時(shí)的web開(kāi)發(fā)中也很少用到.

          那用這些CSS3D屬性可以做3D游戲嗎?

          當(dāng)然是可以的.

          即使只有沙盒, 也有我的世界這種神作.

          今天我就來(lái)帶大家玩一個(gè)從未有過(guò)的全新3D體驗(yàn).

          廢話不多說(shuō), 我們先來(lái)看下效果:

          這里是試玩地址pc端暢玩[1]

          我們要完成這個(gè)迷宮大作戰(zhàn),需要完成以下步驟:

          1. 創(chuàng)建一個(gè)3D世界
          2. 寫(xiě)一個(gè)3D相機(jī)的功能
          3. 創(chuàng)建一座3D迷宮
          4. 創(chuàng)建一個(gè)可以自由運(yùn)動(dòng)的玩家
          5. 在迷宮中找出一條最短路徑提示

          我們先來(lái)看下一些前置知識(shí).

          做一款CSS3D游戲需要的知識(shí)和概念

          CSS3D坐標(biāo)系

          在css3D中, 首先要明確一個(gè)概念, 3D坐標(biāo)系.
          使用左手坐標(biāo)系, 伸出我們的左手, 大拇指和食指成L狀, 其他手指與食指垂直, 如圖:

          WechatIMG315.png

          大拇指為X軸, 食指為Y軸, 其他手指為Z軸.
          這個(gè)就是CSS3D中的坐標(biāo)系.

          透視屬性

          perspective為css中的透視屬性.

          這個(gè)屬性是什么意思呢, 可以把我們的眼睛看作觀察點(diǎn), 眼睛到目標(biāo)物體的距離就是視距, 也就是這里說(shuō)的透視屬性.

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

          perspective: 1200px;
          -webkit-perspective:  1200px;
          復(fù)制代碼

          3D相機(jī)

          在3D游戲開(kāi)發(fā)中, 會(huì)有相機(jī)的概念, 即是人眼所見(jiàn)皆是相機(jī)所見(jiàn).
          在游戲中場(chǎng)景的移動(dòng), 大部分都是移動(dòng)相機(jī).
          例如賽車(chē)游戲中, 相機(jī)就是跟隨車(chē)子移動(dòng), 所以我們才能看到一路的風(fēng)景.
          在這里, 我們會(huì)使用CSS去實(shí)現(xiàn)一個(gè)偽3d相機(jī).

          變換屬性

          在CSS3D中我們對(duì)3D盒子做平移、旋轉(zhuǎn)、拉伸、縮放使用transform屬性.

          • translateX 平移X軸
          • translateY 平移Y軸
          • translateZ 平移Z軸
          • rotateX 旋轉(zhuǎn)X軸
          • rotateY 旋轉(zhuǎn)Y軸
          • rotateZ 旋轉(zhuǎn)Z軸
          • rotate3d(x,y,z,deg) 旋轉(zhuǎn)X、Y、Z軸多少度

          注意:
          這里「先平移再旋轉(zhuǎn)」和「先旋轉(zhuǎn)再平移」是不一樣的
          旋轉(zhuǎn)的角度都是角度值.

          這里還有不清楚的同學(xué)可以參閱羽飛的這篇[juejin.cn/post/699769…[2]] 附帶有demo

          矩陣變換

          我們完成游戲的過(guò)程中會(huì)用到矩陣變換.
          在js中, 獲取某個(gè)節(jié)點(diǎn)的transform屬性, 會(huì)得到一個(gè)矩陣, 這里我打印一下, 他就是長(zhǎng)這個(gè)樣子:

          var _ground = document.getElementsByClassName("ground")[0];
          var bg_style = document.defaultView.getComputedStyle(_ground, null).transform;
          console.log("矩陣變換---->>>",bg_style)
          復(fù)制代碼
          WechatIMG307.png

          那么我們?nèi)绾问褂镁仃嚾ゲ僮鱰ransform呢?

          在線性變換中, 我們都會(huì)去使用矩陣的相乘.
          CSS3D中使用4*4的矩陣進(jìn)行3D變換.
          下面的矩陣我均用二維數(shù)組表示.
          例如matrix3d(1,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,1)可以用二維數(shù)組表示:

          [
              [1000],
              [0100],
              [0010],
              [0001]
          ]
          復(fù)制代碼

          平移即使使用原來(lái)狀態(tài)的矩陣和以下矩陣相乘, dx, dy, dz分別是移動(dòng)的方向x, y, z.

          [
              [100, dx],
              [010, dy],
              [001, dz],
              [0001]
          ]
          復(fù)制代碼

          繞X軸旋轉(zhuǎn)??, 即是與以下矩陣相乘.

          [
              [1000],
              [0, cos??, sin??, 0],
              [0, -sin??, cos??, 0],
              [0001]
          ]
          復(fù)制代碼

          繞Y軸旋轉(zhuǎn)??, 即是與以下矩陣相乘.

          [
              [cos??, 0, -sin??, 0],
              [0100],
              [sin??, 0, cos??, 0],
              [0001]
          ]
          復(fù)制代碼

          繞Z軸旋轉(zhuǎn)??, 即是與以下矩陣相乘.

          [
              [cos??, sin??, 00],
              [-sin??, cos??, 00],
              [0010],
              [0001]
          ]
          復(fù)制代碼

          具體的矩陣的其他知識(shí)這里講了, 大家有興趣可以自行下去學(xué)習(xí).
          我們這里只需要很簡(jiǎn)單的旋轉(zhuǎn)應(yīng)用.

          開(kāi)始創(chuàng)建一個(gè)3D世界

          我們先來(lái)創(chuàng)建UI界面.

          • 相機(jī)div
          • 地平線div
          • 棋盤(pán)div
          • 玩家div(這里是一個(gè)正方體)

          注意
          正方體先旋轉(zhuǎn)在平移, 這種方法應(yīng)該是最簡(jiǎn)單的.
          一個(gè)平面繞X軸、Y軸旋轉(zhuǎn)180度、±90度, 都只需要平移Z軸.
          這里大家試過(guò)就明白了.

          我們先來(lái)看下html部分:

              <div class="camera">
                  <!-- 地面 -->
                  <div class="ground">
                      <div class="box">
                          <div class="box-con">
                              <div class="wall">z</div>
                              <div class="wall">z</div>
                              <div class="wall">y</div>
                              <div class="wall">y</div>
                              <div class="wall">x</div>
                              <div class="wall">x</div>
                              <div class="linex"></div>
                              <div class="liney"></div>
                              <div class="linez"></div>
                          </div>
                          <!-- 棋盤(pán) -->
                          <div class="pan"></div>
                      </div>
                  </div>
              </div>
          復(fù)制代碼

          很簡(jiǎn)單的布局, 其中linexliney、linez是我畫(huà)的坐標(biāo)軸輔助線.
          紅線為X軸, 綠線為Y軸, 藍(lán)線為Z軸. 接著我們來(lái)看下正方體的主要CSS代碼.

          ...
            .box-con{
                width50px;
                height50px;
                transform-style: preserve-3d;
                transform-origin50% 50%;
                transformtranslateZ(25px) ;
                transition: all 2s cubic-bezier(0.0750.820.1651);
            }
            .wall{
                width100%;
                height100%;
                border1px solid #fdd894;
                background-color#fb7922;
              
            }
            .wall:nth-child(1) {
                transformtranslateZ(25px);
            }
            .wall:nth-child(2) {
                transformrotateX(180degtranslateZ(25px);
            }
            .wall:nth-child(3) {
                transformrotateX(90degtranslateZ(25px);
            }
            .wall:nth-child(4) {
                transformrotateX(-90degtranslateZ(25px);
            }
            .wall:nth-child(5) {
                transformrotateY(90degtranslateZ(25px);
            }
            .wall:nth-child(6) {
                transformrotateY(-90degtranslateZ(25px);
            }
          復(fù)制代碼

          粘貼一大堆CSS代碼顯得很蠢.
          其他CSS這里就不粘貼了, 有興趣的同學(xué)可以直接下載源碼查看. 界面搭建完成如圖所示:

          WechatIMG308.png

          接下來(lái)就是重頭戲了, 我們?nèi)?xiě)js代碼來(lái)繼續(xù)完成我們的游戲.

          完成一個(gè)3D相機(jī)功能

          相機(jī)在3D開(kāi)發(fā)中必不可少, 使用相機(jī)功能不僅能查看3D世界模型, 同時(shí)也能實(shí)現(xiàn)很多實(shí)時(shí)的炫酷功能.

          一個(gè)3d相機(jī)需要哪些功能?

          最簡(jiǎn)單的, 上下左右能夠360度無(wú)死角觀察地圖.同時(shí)需要拉近拉遠(yuǎn)視距.

          通過(guò)鼠標(biāo)交互

          鼠標(biāo)左右移動(dòng)可以旋轉(zhuǎn)查看地圖; 鼠標(biāo)上下移動(dòng)可以觀察上下地圖; 鼠標(biāo)滾輪可以拉近拉遠(yuǎn)視距.

          ? 1. 監(jiān)聽(tīng)鼠標(biāo)事件

          首先, 我們需要通過(guò)監(jiān)聽(tīng)鼠標(biāo)事件來(lái)記錄鼠標(biāo)位置, 從而判斷相機(jī)上下左右查看.

              /** 鼠標(biāo)上次位置 */
              var lastX = 0, lastY = 0;
                /** 控制一次滑動(dòng) */
              var isDown = false;
                /** 監(jiān)聽(tīng)鼠標(biāo)按下 */
              document.addEventListener("mousedown", (e) => {
                  lastX = e.clientX;
                  lastY = e.clientY;
                  isDown = true;
              });
                  /** 監(jiān)聽(tīng)鼠標(biāo)移動(dòng) */
              document.addEventListener("mousemove", (e) => {
                  if (!isDown) return;
                  let _offsetX = e.clientX - lastX;
                  let _offsetY = e.clientY - lastY;
                  lastX = e.clientX;
                  lastY = e.clientY;
                  //判斷方向
                  var dirH = 1, dirV = 1;
                  if (_offsetX < 0) {
                      dirH = -1;
                  }
                  if (_offsetY > 0) {
                      dirV = -1;
                  }
              });
              document.addEventListener("mouseup", (e) => {
                  isDown = false;
              });
          復(fù)制代碼

          ? 2. 判斷相機(jī)上下左右

          使用perspective-origin來(lái)設(shè)置相機(jī)的上下視線.
          使用transform來(lái)旋轉(zhuǎn)Z軸查看左右方向上的360度.

          /** 監(jiān)聽(tīng)鼠標(biāo)移動(dòng) */
              document.addEventListener("mousemove", (e) => {
                  if (!isDown) return;
                  let _offsetX = e.clientX - lastX;
                  let _offsetY = e.clientY - lastY;
                  lastX = e.clientX;
                  lastY = e.clientY;
                  var bg_style = document.defaultView.getComputedStyle(_ground, null).transform;
                  var camera_style = document.defaultView.getComputedStyle(_camera, null).perspectiveOrigin;
                  var matrix4 = new Matrix4();
                  var _cy = +camera_style.split(' ')[1].split('px')[0];
                  var str = bg_style.split("matrix3d(")[1].split(")")[0].split(",");
                  var oldMartrix4 = str.map((item) => +item);
                  var dirH = 1, dirV = 1;
                  if (_offsetX < 0) {
                      dirH = -1;
                  }
                  if (_offsetY > 0) {
                      dirV = -1;
                  }
                  //每次移動(dòng)旋轉(zhuǎn)角度
                  var angleZ = 2 * dirH;
                  var newMartri4 = matrix4.set(Math.cos(angleZ * Math.PI / 180), -Math.sin(angleZ * Math.PI / 180), 00Math.sin(angleZ * Math.PI / 180), Math.cos(angleZ * Math.PI / 180), 0000100001);
                  var new_mar = null;
                  if (Math.abs(_offsetX) > Math.abs(_offsetY)) {
                      new_mar = matrix4.multiplyMatrices(oldMartrix4, newMartri4);
                  } else {
                      _camera.style.perspectiveOrigin = `500px ${_cy + 10 * dirV}px`;
                  }
                  new_mar && (_ground.style.transform = `matrix3d(${new_mar.join(',')})`);
              });
          復(fù)制代碼

          這里使用了矩陣的方法來(lái)旋轉(zhuǎn)Z軸, 矩陣類(lèi)Matrix4是我臨時(shí)寫(xiě)的一個(gè)方法類(lèi), 就倆方法, 一個(gè)設(shè)置二維數(shù)組matrix4.set, 一個(gè)矩陣相乘matrix4.multiplyMatrices.
          文末的源碼地址中有, 這里就不再贅述了.

          ? 3. 監(jiān)聽(tīng)滾輪拉近拉遠(yuǎn)距離

          這里就是根據(jù)perspective來(lái)設(shè)置視距.

          //監(jiān)聽(tīng)滾輪
          document.addEventListener('mousewheel', (e) => {
              var per = document.defaultView.getComputedStyle(_camera, null).perspective;
              let newper = (+per.split("px")[0] + Math.floor(e.deltaY / 10)) + "px";
              _camera.style.perspective = newper
          }, false);
          復(fù)制代碼

          注意:
          perspective-origin屬性只有X、Y兩個(gè)值, 做不到和u3D一樣的相機(jī).
          我這里取巧使用了對(duì)地平線的旋轉(zhuǎn), 從而達(dá)到一樣的效果.
          滾輪拉近拉遠(yuǎn)視距有點(diǎn)別扭, 和3D引擎區(qū)別還是很大.

          完成之后可以看到如下的場(chǎng)景, 已經(jīng)可以隨時(shí)觀察我們的地圖了.

          index1.gif

          這樣子, 一個(gè)3D相機(jī)就完成, 大家有興趣的可以自己下去寫(xiě)一下, 還是很有意思的.

          繪制迷宮棋盤(pán)

          繪制格子地圖最簡(jiǎn)單了, 我這里使用一個(gè)15*15的數(shù)組.
          0」代表可以通過(guò)的路, 「1」代表障礙物.

          var grid = [
              000100000100010,
              010100101011010,
              100001000000001,
              000100000100101,
              001010010101000,
              000011000001000,
              001000100100010,
              110010010001000,
              101001010100010,
              000011000000000,
              101001010001001,
              010010000000010,
              110010010101000,
              101001000000100,
              001010100110000
          ];
          復(fù)制代碼

          然后我們?nèi)ケ闅v這個(gè)數(shù)組, 得到地圖.
          寫(xiě)一個(gè)方法去創(chuàng)建地圖格子, 同時(shí)返回格子數(shù)組和節(jié)點(diǎn)數(shù)組.
          這里的block是在html中創(chuàng)建的一個(gè)預(yù)制體, 他是一個(gè)正方體.
          然后通過(guò)克隆節(jié)點(diǎn)的方式添加進(jìn)棋盤(pán)中.

          /** 棋盤(pán) */
          function pan() {
              const con = document.getElementsByClassName("pan")[0];
              const block = document.getElementsByClassName("block")[0];
              let elArr = [];
              grid.forEach((item, index) => {
                  let r = Math.floor(index / 15);
                  let c = index % 15;
                  const gezi = document.createElement("div");
                  gezi.classList = "pan-item"
                  // gezi.innerHTML = `${r},${c}`
                  con.appendChild(gezi);
                  var newBlock = block.cloneNode(true);
                  //障礙物
                  if (item == 1) {
                      gezi.appendChild(newBlock);
                      blockArr.push(c + "-" + r);
                  }
                  elArr.push(gezi);
              });
              const panArr = arrTrans(15, grid);
              return { elArr, panArr };
          }
          const panData = pan();
          復(fù)制代碼

          可以看到, 我們的界面已經(jīng)變成了這樣.

          WechatIMG310.png

          接下來(lái), 我們需要去控制玩家移動(dòng)了.

          控制玩家移動(dòng)

          通過(guò)上下左右w s a d鍵來(lái)控制玩家移動(dòng).
          使用transform來(lái)移動(dòng)和旋轉(zhuǎn)玩家盒子.

          ? 監(jiān)聽(tīng)鍵盤(pán)事件

          通過(guò)監(jiān)聽(tīng)鍵盤(pán)事件onkeydown來(lái)判斷key值的上下左右.

          document.onkeydown = function (e{
              /** 移動(dòng)物體 */
              move(e.key);
          }
          復(fù)制代碼

          ? 進(jìn)行位移

          在位移中, 使用translate來(lái)平移, Z軸始終正對(duì)我們的相機(jī), 所以我們只需要移動(dòng)X軸和Y軸.
          聲明一個(gè)變量記錄當(dāng)前位置.
          同時(shí)需要記錄上次變換的transform的值, 這里我們就不繼續(xù)矩陣變換了.

          /** 當(dāng)前位置 */
          var position = { x0y0 };
          /** 記錄上次變換值 */
          var lastTransform = {
              translateX'0px',
              translateY'0px',
              translateZ'25px',
              rotateX'0deg',
              rotateY'0deg',
              rotateZ'0deg'
          };
          復(fù)制代碼

          每一個(gè)格子都可以看成是二維數(shù)組的下標(biāo)構(gòu)成, 每次我們移動(dòng)一個(gè)格子的距離.

           switch (key) {
              case 'w':
                  position.y++;
                  lastTransform.translateY = position.y * 50 + 'px';
                  break;
              case 's':
                  position.y--;
                  lastTransform.translateY = position.y * 50 + 'px';
                  break;
              case 'a':
                  position.x++;
                  lastTransform.translateX = position.x * 50 + 'px';
                  break;
              case 'd':
                  position.x--;
                  lastTransform.translateX = position.x * 50 + 'px';
                  break;
          }
          //賦值樣式
          for (let item in lastTransform) {
              strTransfrom += item + '(' + lastTransform[item] + ') ';
          }
          target.style.transform = strTransfrom;
          復(fù)制代碼

          到這里, 我們的玩家盒子已經(jīng)可以移動(dòng)了.

          注意
          在css3D中的平移可以看成是世界坐標(biāo).
          所以我們只需要關(guān)心X、Y軸. 而不需要去移動(dòng)Z軸. 即使我們進(jìn)行了旋轉(zhuǎn).

          ? 在移動(dòng)的過(guò)程中進(jìn)行旋轉(zhuǎn)

          在CSS3D中, 3D旋轉(zhuǎn)和其他3D引擎中不一樣, 一般的諸如u3D、threejs中, 在每次旋轉(zhuǎn)完成之后都會(huì)重新校對(duì)成世界坐標(biāo), 相對(duì)來(lái)說(shuō) 就很好計(jì)算繞什么軸旋轉(zhuǎn)多少度.

          然而, 筆者也低估了CSS3D的旋轉(zhuǎn).
          我以為上下左右滾動(dòng)一個(gè)正方體很簡(jiǎn)單. 事實(shí)并非如此.

          CSS3D的旋轉(zhuǎn)涉及到四元數(shù)和萬(wàn)向鎖.

          比如我們旋轉(zhuǎn)我們的玩家盒子. 如圖所示:

          首先, 第一個(gè)格子(0,0)向上繞X軸旋轉(zhuǎn)90度, 就可以到達(dá)(1.0); 向左繞Y軸旋轉(zhuǎn)90度, 可以到達(dá)(0,1); 那我們是不是就可以得到規(guī)律如下:

          WechatIMG312.png

          如圖中所示, 單純的向上下, 向左右繞軸旋轉(zhuǎn)沒(méi)有問(wèn)題, 但是要旋轉(zhuǎn)到紅色的格子, 兩種不同走法, 到紅色的格子之后旋轉(zhuǎn)就會(huì)出現(xiàn)兩種可能. 從而導(dǎo)致旋轉(zhuǎn)出錯(cuò).

          同時(shí)這個(gè)規(guī)律雖然難尋, 但是可以寫(xiě)出來(lái), 最重要的是, 按照這個(gè)規(guī)律來(lái)旋轉(zhuǎn)CSS3D中的盒子, 是不對(duì)的

          那有人就說(shuō)了, 這不說(shuō)的屁話嗎?

          經(jīng)過(guò)筆者實(shí)驗(yàn), 倒是發(fā)現(xiàn)了一些規(guī)律. 我們繼續(xù)按照這個(gè)規(guī)律往下走.

          • 旋轉(zhuǎn)X軸的時(shí)候, 同時(shí)看當(dāng)前Z軸的度數(shù), Z軸為90度的奇數(shù)倍, 旋轉(zhuǎn)Y軸, 否則旋轉(zhuǎn)X軸.
          • 旋轉(zhuǎn)Y軸的時(shí)候, 同時(shí)看當(dāng)前Z軸的度數(shù), Z軸為90度的奇數(shù)倍, 旋轉(zhuǎn)X軸, 否則旋轉(zhuǎn)Z軸.
          • 旋轉(zhuǎn)Z軸的時(shí)候, 繼續(xù)旋轉(zhuǎn)Z軸

          這樣子我們的旋轉(zhuǎn)方向就搞定了.

          if (nextRotateDir[0] == "X") {
              if (Math.floor(Math.abs(lastRotate.lastRotateZ) / 90) % 2 == 1) {
                  lastTransform[`rotateY`] = (lastRotate[`lastRotateY`] + 90 * dir) + 'deg';
              } else {
                  lastTransform[`rotateX`] = (lastRotate[`lastRotateX`] - 90 * dir) + 'deg';
              }
          }
          if (nextRotateDir[0] == "Y") {
              if (Math.floor(Math.abs(Math.abs(lastRotate.lastRotateZ)) / 90) % 2 == 1) {
                  lastTransform[`rotateX`] = (lastRotate[`lastRotateX`] + 90 * dir) + 'deg';
              } else {
                  lastTransform[`rotateZ`] = (lastRotate[`lastRotateZ`] + 90 * dir) + 'deg';
              }
          }
          if (nextRotateDir[0] == "Z") {
              lastTransform[`rotate${nextRotateDir[0]}`] = (lastRotate[`lastRotate${nextRotateDir[0]}`] - 90 * dir) + 'deg';
          }
          復(fù)制代碼

          然而, 這還沒(méi)有完, 這種方式的旋轉(zhuǎn)還有個(gè)坑, 就是我不知道該旋轉(zhuǎn)90度還是-90度了.
          這里并不是簡(jiǎn)單的上下左右去加減.

          旋轉(zhuǎn)方向?qū)α? 旋轉(zhuǎn)角度不知該如何計(jì)算了.

          具體代碼可以查看源碼[3].

          彩蛋時(shí)間

          ?????? 同時(shí)這里會(huì)伴隨著「萬(wàn)向鎖」的出現(xiàn), 即是Z軸與X軸重合了. 哈哈哈哈~
          ?????? 這里筆者還沒(méi)有解決, 也希望萬(wàn)能的網(wǎng)友能夠出言幫忙~
          ?????? 筆者后續(xù)解決了會(huì)更新的. 哈哈哈哈, 大坑.

          好了, 這里問(wèn)題不影響我們的項(xiàng)目. 我們繼續(xù)講如何找到最短路徑并給出提示.

          最短路徑的計(jì)算

          在迷宮中, 從一個(gè)點(diǎn)到另一個(gè)點(diǎn)的最短路徑怎么計(jì)算呢? 這里筆者使用的是廣度優(yōu)先遍歷(BFS)算法來(lái)計(jì)算最短路徑.

          我們來(lái)思考:

          1. 二維數(shù)組中找最短路徑
          2. 每一格的最短路徑只有上下左右相鄰的四格
          3. 那么只要遞歸尋找每一格的最短距離直至找到終點(diǎn)

          這里我們需要使用「隊(duì)列」先進(jìn)先出的特點(diǎn).

          我們先來(lái)看一張圖:

          WechatIMG313.png

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

          注意
          使用兩個(gè)長(zhǎng)度為4的數(shù)組表示上下左右相鄰的格子需要相加的下標(biāo)偏移量.
          每次入隊(duì)之前需要判斷是否已經(jīng)入隊(duì)了.
          每次出隊(duì)時(shí)需要判斷是否是終點(diǎn).
          需要記錄當(dāng)前入隊(duì)的目標(biāo)的父節(jié)點(diǎn), 方便獲取到最短路徑.

          我們來(lái)看下代碼:

          //春初路徑
          var stack = [];
          /**
           * BFS 實(shí)現(xiàn)尋路
           * @param {*} grid 
           * @param {*} start {x: 0,y: 0}
           * @param {*} end {x: 3,y: 3}
           */

          function getShortPath(grid, start, end, a{
              let maxL_x = grid.length;
              let maxL_y = grid[0].length;
              let queue = new Queue();
              //最短步數(shù)
              let step = 0;
              //上左下右
              let dx = [10-10];
              let dy = [010-1];
              //加入第一個(gè)元素
              queue.enqueue(start);
              //存儲(chǔ)一個(gè)一樣的用來(lái)排查是否遍歷過(guò)
              let mem = new Array(maxL_x);
              for (let n = 0; n < maxL_x; n++) {
                  mem[n] = new Array(maxL_y);
                  mem[n].fill(100);
              }
              while (!queue.isEmpty()) {
                  let p = [];
                  for (let i = queue.size(); i > 0; i--) {
                      let preTraget = queue.dequeue();
                      p.push(preTraget);
                      //找到目標(biāo)
                      if (preTraget.x == end.x && preTraget.y == end.y) {
                          stack.push(p);
                          return step;
                      }
                      //遍歷四個(gè)相鄰格子
                      for (let j = 0; j < 4; j++) {
                          let nextX = preTraget.x + dx[j];
                          let nextY = preTraget.y + dy[j];

                          if (nextX < maxL_x && nextX >= 0 && nextY < maxL_y && nextY >= 0) {
                              let nextTraget = { x: nextX, y: nextY };
                              if (grid[nextX][nextY] == a && a < mem[nextX][nextY]) {
                                  queue.enqueue({ ...nextTraget, f: { x: preTraget.x, y: preTraget.y } });
                                  mem[nextX][nextY] = a;
                              }
                          }
                      }
                  }
                  stack.push(p);
                  step++;
              }
          }
          /* 找出一條最短路徑**/
          function recall(end{
              let path = [];
              let front = { x: end.x, y: end.y };
              while (stack.length) {
                  let item = stack.pop();
                  for (let i = 0; i < item.length; i++) {
                      if (!item[i].f) break;
                      if (item[i].x == front.x && item[i].y == front.y) {
                          path.push({ x: item[i].x, y: item[i].y });
                          front.x = item[i].f.x;
                          front.y = item[i].f.y;
                          break;
                      }
                  }
              }
              return path;
          }

          復(fù)制代碼

          這樣子我們就可以找到一條最短路徑并得到最短的步數(shù).
          然后我們繼續(xù)去遍歷我們的原數(shù)組(即棋盤(pán)原數(shù)組).
          點(diǎn)擊提示點(diǎn)亮路徑.

          var step = getShortPath(panArr, { x0y0 }, { x14y14 }, 0);
          console.log("最短距離----", step);
          _perstep.innerHTML = `請(qǐng)?jiān)?lt;span>${step}</span>步內(nèi)走到終點(diǎn)`;
          var path = recall({ x14y14 });
          console.log("路徑---", path);
          /** 提示 */
          var tipCount = 0;
          _tip.addEventListener("click", () => {
              console.log("9999", tipCount)
              elArr.forEach((item, index) => {
                  let r = Math.floor(index / 15);
                  let c = index % 15;
                  path.forEach((_item, i) => {
                      if (_item.x == r && _item.y == c) {
                          // console.log("ooo",_item)
                          if (tipCount % 2 == 0)
                              item.classList = "pan-item pan-path";
                          else
                              item.classList = "pan-item";
                      }
                  })
              });
              tipCount++;
          });
          復(fù)制代碼

          這樣子, 我們可以得到如圖的提示:

          WechatIMG314.png

          大功告成. 嘿嘿, 是不是很驚艷的感覺(jué)~

          尾聲

          當(dāng)然, 我這里的這個(gè)小游戲還有可以完善的地方 比如:

          • 可以增加道具, 拾取可以減少已走步數(shù)
          • 可以增加配置關(guān)卡
          • 還可以增加跳躍功能
          • ...

          原來(lái)如此, CSS3D能做的事還有很多, 怎么用全看自己的想象力有多豐富了.

          哈哈哈, 真想用CSS3D寫(xiě)一個(gè)「我的世界」玩玩, 性能問(wèn)題恐怕會(huì)有點(diǎn)大.

          本文例子均在PC端體驗(yàn)較好.

          試玩地址[4]

          源碼地址[5]

          歡迎大家拍磚指正, 筆者功力尚淺, 如有不當(dāng)之處請(qǐng)斧正!

          文章粗淺, 望諸位不吝您的評(píng)論和點(diǎn)贊~

          注: 本文系作者嘔心瀝血之作, 轉(zhuǎn)載須聲明

          關(guān)于本文

          作者:起小就些熊

          https://juejin.cn/post/7000963575573381134

          瀏覽 44
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  99中文无码 | 噜噜噜久久久噜噜 国产 | 久久综合婷婷国产五区 | 中文无码网站 | 久久久久久免费一级A片 |