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

前言
偶然接觸到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),需要完成以下步驟:
創(chuàng)建一個(gè)3D世界 寫(xiě)一個(gè)3D相機(jī)的功能 創(chuàng)建一座3D迷宮 創(chuàng)建一個(gè)可以自由運(yùn)動(dòng)的玩家 在迷宮中找出一條最短路徑提示
我們先來(lái)看下一些前置知識(shí).
做一款CSS3D游戲需要的知識(shí)和概念
CSS3D坐標(biāo)系
在css3D中, 首先要明確一個(gè)概念, 3D坐標(biāo)系.
使用左手坐標(biāo)系, 伸出我們的左手, 大拇指和食指成L狀, 其他手指與食指垂直, 如圖:

大拇指為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ù)制代碼

那么我們?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ù)組表示:
[
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
]
復(fù)制代碼
平移即使使用原來(lái)狀態(tài)的矩陣和以下矩陣相乘, dx, dy, dz分別是移動(dòng)的方向x, y, z.
[
[1, 0, 0, dx],
[0, 1, 0, dy],
[0, 0, 1, dz],
[0, 0, 0, 1]
]
復(fù)制代碼
繞X軸旋轉(zhuǎn)??, 即是與以下矩陣相乘.
[
[1, 0, 0, 0],
[0, cos??, sin??, 0],
[0, -sin??, cos??, 0],
[0, 0, 0, 1]
]
復(fù)制代碼
繞Y軸旋轉(zhuǎn)??, 即是與以下矩陣相乘.
[
[cos??, 0, -sin??, 0],
[0, 1, 0, 0],
[sin??, 0, cos??, 0],
[0, 0, 0, 1]
]
復(fù)制代碼
繞Z軸旋轉(zhuǎn)??, 即是與以下矩陣相乘.
[
[cos??, sin??, 0, 0],
[-sin??, cos??, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
]
復(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)單的布局, 其中linex、liney、linez是我畫(huà)的坐標(biāo)軸輔助線.
紅線為X軸, 綠線為Y軸, 藍(lán)線為Z軸. 接著我們來(lái)看下正方體的主要CSS代碼.
...
.box-con{
width: 50px;
height: 50px;
transform-style: preserve-3d;
transform-origin: 50% 50%;
transform: translateZ(25px) ;
transition: all 2s cubic-bezier(0.075, 0.82, 0.165, 1);
}
.wall{
width: 100%;
height: 100%;
border: 1px solid #fdd894;
background-color: #fb7922;
}
.wall:nth-child(1) {
transform: translateZ(25px);
}
.wall:nth-child(2) {
transform: rotateX(180deg) translateZ(25px);
}
.wall:nth-child(3) {
transform: rotateX(90deg) translateZ(25px);
}
.wall:nth-child(4) {
transform: rotateX(-90deg) translateZ(25px);
}
.wall:nth-child(5) {
transform: rotateY(90deg) translateZ(25px);
}
.wall:nth-child(6) {
transform: rotateY(-90deg) translateZ(25px);
}
復(fù)制代碼
粘貼一大堆CSS代碼顯得很蠢.
其他CSS這里就不粘貼了, 有興趣的同學(xué)可以直接下載源碼查看. 界面搭建完成如圖所示:

接下來(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), 0, 0, Math.sin(angleZ * Math.PI / 180), Math.cos(angleZ * Math.PI / 180), 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
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í)觀察我們的地圖了.

這樣子, 一個(gè)3D相機(jī)就完成, 大家有興趣的可以自己下去寫(xiě)一下, 還是很有意思的.
繪制迷宮棋盤(pán)
繪制格子地圖最簡(jiǎn)單了, 我這里使用一個(gè)15*15的數(shù)組.
「0」代表可以通過(guò)的路, 「1」代表障礙物.
var grid = [
0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0,
0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0,
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1,
0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1,
0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0,
0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0,
0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0,
1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0,
1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0,
0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1,
0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,
1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0,
1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0,
0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0
];
復(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)變成了這樣.

接下來(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 = { x: 0, y: 0 };
/** 記錄上次變換值 */
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ī)律如下:

如圖中所示, 單純的向上下, 向左右繞軸旋轉(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)思考:
二維數(shù)組中找最短路徑 每一格的最短路徑只有上下左右相鄰的四格 那么只要遞歸尋找每一格的最短距離直至找到終點(diǎn)
這里我們需要使用「隊(duì)列」先進(jìn)先出的特點(diǎn).
我們先來(lái)看一張圖:

很清晰的可以得到最短路徑.
注意
使用兩個(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 = [1, 0, -1, 0];
let dy = [0, 1, 0, -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, { x: 0, y: 0 }, { x: 14, y: 14 }, 0);
console.log("最短距離----", step);
_perstep.innerHTML = `請(qǐng)?jiān)?lt;span>${step}</span>步內(nèi)走到終點(diǎn)`;
var path = recall({ x: 14, y: 14 });
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ù)制代碼
這樣子, 我們可以得到如圖的提示:

大功告成. 嘿嘿, 是不是很驚艷的感覺(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
