前端游戲巨制! CSS居然可以做3D游戲了
點擊上方 前端瓶子君,關(guān)注公眾號
回復(fù)算法,加入前端編程面試算法每日一題群

前言
偶然接觸到CSS的3D屬性, 就萌生了一種做3D游戲的想法.
了解過css3D屬性的同學(xué)應(yīng)該都了解過perspective、perspective-origin、transform-style: preserve-3d這個三個屬性值, 它們構(gòu)成了CSS的3d世界.
同時, 還有transform屬性來對3D的節(jié)點進(jìn)行平移、縮放、旋轉(zhuǎn)以及拉伸.
屬性值很簡單, 在我們平時的web開發(fā)中也很少用到.
那用這些CSS3D屬性可以做3D游戲嗎?
當(dāng)然是可以的.
即使只有沙盒, 也有我的世界這種神作.
今天我就來帶大家玩一個從未有過的全新3D體驗.
廢話不多說, 我們先來看下效果:

這里是試玩地址pc端暢玩[1]
我們要完成這個迷宮大作戰(zhàn),需要完成以下步驟:
創(chuàng)建一個3D世界 寫一個3D相機的功能 創(chuàng)建一座3D迷宮 創(chuàng)建一個可以自由運動的玩家 在迷宮中找出一條最短路徑提示
我們先來看下一些前置知識.
做一款CSS3D游戲需要的知識和概念
CSS3D坐標(biāo)系
在css3D中, 首先要明確一個概念, 3D坐標(biāo)系.
使用左手坐標(biāo)系, 伸出我們的左手, 大拇指和食指成L狀, 其他手指與食指垂直, 如圖:

大拇指為X軸, 食指為Y軸, 其他手指為Z軸.
這個就是CSS3D中的坐標(biāo)系.
透視屬性
perspective為css中的透視屬性.
這個屬性是什么意思呢, 可以把我們的眼睛看作觀察點, 眼睛到目標(biāo)物體的距離就是視距, 也就是這里說的透視屬性.
大家都知道, 「透視」+「2D」= 「3D」.
perspective: 1200px;
-webkit-perspective: 1200px;
復(fù)制代碼
3D相機
在3D游戲開發(fā)中, 會有相機的概念, 即是人眼所見皆是相機所見.
在游戲中場景的移動, 大部分都是移動相機.
例如賽車游戲中, 相機就是跟隨車子移動, 所以我們才能看到一路的風(fēng)景.
在這里, 我們會使用CSS去實現(xiàn)一個偽3d相機.
變換屬性
在CSS3D中我們對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
矩陣變換
我們完成游戲的過程中會用到矩陣變換.
在js中, 獲取某個節(jié)點的transform屬性, 會得到一個矩陣, 這里我打印一下, 他就是長這個樣子:
var _ground = document.getElementsByClassName("ground")[0];
var bg_style = document.defaultView.getComputedStyle(_ground, null).transform;
console.log("矩陣變換---->>>",bg_style)
復(fù)制代碼

那么我們?nèi)绾问褂镁仃嚾ゲ僮鱰ransform呢?
在線性變換中, 我們都會去使用矩陣的相乘.
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ù)制代碼
平移即使使用原來狀態(tài)的矩陣和以下矩陣相乘, dx, dy, dz分別是移動的方向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ù)制代碼
具體的矩陣的其他知識這里講了, 大家有興趣可以自行下去學(xué)習(xí).
我們這里只需要很簡單的旋轉(zhuǎn)應(yīng)用.
開始創(chuàng)建一個3D世界
我們先來創(chuàng)建UI界面.
相機div 地平線div 棋盤div 玩家div(這里是一個正方體)
注意
正方體先旋轉(zhuǎn)在平移, 這種方法應(yīng)該是最簡單的.
一個平面繞X軸、Y軸旋轉(zhuǎn)180度、±90度, 都只需要平移Z軸.
這里大家試過就明白了.
我們先來看下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>
<!-- 棋盤 -->
<div class="pan"></div>
</div>
</div>
</div>
復(fù)制代碼
很簡單的布局, 其中linex、liney、linez是我畫的坐標(biāo)軸輔助線.
紅線為X軸, 綠線為Y軸, 藍(lán)線為Z軸. 接著我們來看下正方體的主要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é)可以直接下載源碼查看. 界面搭建完成如圖所示:

接下來就是重頭戲了, 我們?nèi)慾s代碼來繼續(xù)完成我們的游戲.
完成一個3D相機功能
相機在3D開發(fā)中必不可少, 使用相機功能不僅能查看3D世界模型, 同時也能實現(xiàn)很多實時的炫酷功能.
一個3d相機需要哪些功能?
最簡單的, 上下左右能夠360度無死角觀察地圖.同時需要拉近拉遠(yuǎn)視距.
通過鼠標(biāo)交互
鼠標(biāo)左右移動可以旋轉(zhuǎn)查看地圖; 鼠標(biāo)上下移動可以觀察上下地圖; 鼠標(biāo)滾輪可以拉近拉遠(yuǎn)視距.
? 1. 監(jiān)聽鼠標(biāo)事件
首先, 我們需要通過監(jiān)聽鼠標(biāo)事件來記錄鼠標(biāo)位置, 從而判斷相機上下左右查看.
/** 鼠標(biāo)上次位置 */
var lastX = 0, lastY = 0;
/** 控制一次滑動 */
var isDown = false;
/** 監(jiān)聽鼠標(biāo)按下 */
document.addEventListener("mousedown", (e) => {
lastX = e.clientX;
lastY = e.clientY;
isDown = true;
});
/** 監(jiān)聽鼠標(biāo)移動 */
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. 判斷相機上下左右
使用perspective-origin來設(shè)置相機的上下視線.
使用transform來旋轉(zhuǎn)Z軸查看左右方向上的360度.
/** 監(jiān)聽鼠標(biāo)移動 */
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;
}
//每次移動旋轉(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ù)制代碼
這里使用了矩陣的方法來旋轉(zhuǎn)Z軸, 矩陣類Matrix4是我臨時寫的一個方法類, 就倆方法, 一個設(shè)置二維數(shù)組matrix4.set, 一個矩陣相乘matrix4.multiplyMatrices.
文末的源碼地址中有, 這里就不再贅述了.
? 3. 監(jiān)聽滾輪拉近拉遠(yuǎn)距離
這里就是根據(jù)perspective來設(shè)置視距.
//監(jiān)聽滾輪
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兩個值, 做不到和u3D一樣的相機.
我這里取巧使用了對地平線的旋轉(zhuǎn), 從而達(dá)到一樣的效果.
滾輪拉近拉遠(yuǎn)視距有點別扭, 和3D引擎區(qū)別還是很大.
完成之后可以看到如下的場景, 已經(jīng)可以隨時觀察我們的地圖了.

這樣子, 一個3D相機就完成, 大家有興趣的可以自己下去寫一下, 還是很有意思的.
繪制迷宮棋盤
繪制格子地圖最簡單了, 我這里使用一個15*15的數(shù)組.
「0」代表可以通過的路, 「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這個數(shù)組, 得到地圖.
寫一個方法去創(chuàng)建地圖格子, 同時返回格子數(shù)組和節(jié)點數(shù)組.
這里的block是在html中創(chuàng)建的一個預(yù)制體, 他是一個正方體.
然后通過克隆節(jié)點的方式添加進(jì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)變成了這樣.

接下來, 我們需要去控制玩家移動了.
控制玩家移動
通過上下左右w s a d鍵來控制玩家移動.
使用transform來移動和旋轉(zhuǎn)玩家盒子.
? 監(jiān)聽鍵盤事件
通過監(jiān)聽鍵盤事件onkeydown來判斷key值的上下左右.
document.onkeydown = function (e) {
/** 移動物體 */
move(e.key);
}
復(fù)制代碼
? 進(jìn)行位移
在位移中, 使用translate來平移, Z軸始終正對我們的相機, 所以我們只需要移動X軸和Y軸.
聲明一個變量記錄當(dāng)前位置.
同時需要記錄上次變換的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ù)制代碼
每一個格子都可以看成是二維數(shù)組的下標(biāo)構(gòu)成, 每次我們移動一個格子的距離.
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)可以移動了.
注意
在css3D中的平移可以看成是世界坐標(biāo).
所以我們只需要關(guān)心X、Y軸. 而不需要去移動Z軸. 即使我們進(jìn)行了旋轉(zhuǎn).
? 在移動的過程中進(jìn)行旋轉(zhuǎn)
在CSS3D中, 3D旋轉(zhuǎn)和其他3D引擎中不一樣, 一般的諸如u3D、threejs中, 在每次旋轉(zhuǎn)完成之后都會重新校對成世界坐標(biāo), 相對來說 就很好計算繞什么軸旋轉(zhuǎn)多少度.
然而, 筆者也低估了CSS3D的旋轉(zhuǎn).
我以為上下左右滾動一個正方體很簡單. 事實并非如此.
CSS3D的旋轉(zhuǎn)涉及到四元數(shù)和萬向鎖.
比如我們旋轉(zhuǎn)我們的玩家盒子. 如圖所示:
首先, 第一個格子(0,0)向上繞X軸旋轉(zhuǎn)90度, 就可以到達(dá)(1.0); 向左繞Y軸旋轉(zhuǎn)90度, 可以到達(dá)(0,1); 那我們是不是就可以得到規(guī)律如下:

如圖中所示, 單純的向上下, 向左右繞軸旋轉(zhuǎn)沒有問題, 但是要旋轉(zhuǎn)到紅色的格子, 兩種不同走法, 到紅色的格子之后旋轉(zhuǎn)就會出現(xiàn)兩種可能. 從而導(dǎo)致旋轉(zhuǎn)出錯.
同時這個規(guī)律雖然難尋, 但是可以寫出來, 最重要的是, 按照這個規(guī)律來旋轉(zhuǎn)CSS3D中的盒子, 是不對的
那有人就說了, 這不說的屁話嗎?
經(jīng)過筆者實驗, 倒是發(fā)現(xiàn)了一些規(guī)律. 我們繼續(xù)按照這個規(guī)律往下走.
旋轉(zhuǎn)X軸的時候, 同時看當(dāng)前Z軸的度數(shù), Z軸為90度的奇數(shù)倍, 旋轉(zhuǎn)Y軸, 否則旋轉(zhuǎn)X軸. 旋轉(zhuǎn)Y軸的時候, 同時看當(dāng)前Z軸的度數(shù), Z軸為90度的奇數(shù)倍, 旋轉(zhuǎn)X軸, 否則旋轉(zhuǎn)Z軸. 旋轉(zhuǎn)Z軸的時候, 繼續(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ù)制代碼
然而, 這還沒有完, 這種方式的旋轉(zhuǎn)還有個坑, 就是我不知道該旋轉(zhuǎn)90度還是-90度了.
這里并不是簡單的上下左右去加減.
旋轉(zhuǎn)方向?qū)α? 旋轉(zhuǎn)角度不知該如何計算了.
具體代碼可以查看源碼[3].
彩蛋時間
?????? 同時這里會伴隨著「萬向鎖」的出現(xiàn), 即是Z軸與X軸重合了. 哈哈哈哈~
?????? 這里筆者還沒有解決, 也希望萬能的網(wǎng)友能夠出言幫忙~
?????? 筆者后續(xù)解決了會更新的. 哈哈哈哈, 大坑.
好了, 這里問題不影響我們的項目. 我們繼續(xù)講如何找到最短路徑并給出提示.
最短路徑的計算
在迷宮中, 從一個點到另一個點的最短路徑怎么計算呢? 這里筆者使用的是廣度優(yōu)先遍歷(BFS)算法來計算最短路徑.
我們來思考:
二維數(shù)組中找最短路徑 每一格的最短路徑只有上下左右相鄰的四格 那么只要遞歸尋找每一格的最短距離直至找到終點
這里我們需要使用「隊列」先進(jìn)先出的特點.
我們先來看一張圖:

很清晰的可以得到最短路徑.
注意
使用兩個長度為4的數(shù)組表示上下左右相鄰的格子需要相加的下標(biāo)偏移量.
每次入隊之前需要判斷是否已經(jīng)入隊了.
每次出隊時需要判斷是否是終點.
需要記錄當(dāng)前入隊的目標(biāo)的父節(jié)點, 方便獲取到最短路徑.
我們來看下代碼:
//春初路徑
var stack = [];
/**
* BFS 實現(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];
//加入第一個元素
queue.enqueue(start);
//存儲一個一樣的用來排查是否遍歷過
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;
}
//遍歷四個相鄰格子
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ù)組(即棋盤原數(shù)組).
點擊提示點亮路徑.
var step = getShortPath(panArr, { x: 0, y: 0 }, { x: 14, y: 14 }, 0);
console.log("最短距離----", step);
_perstep.innerHTML = `請在<span>${step}</span>步內(nèi)走到終點`;
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ù)制代碼
這樣子, 我們可以得到如圖的提示:

大功告成. 嘿嘿, 是不是很驚艷的感覺~
尾聲
當(dāng)然, 我這里的這個小游戲還有可以完善的地方 比如:
可以增加道具, 拾取可以減少已走步數(shù) 可以增加配置關(guān)卡 還可以增加跳躍功能 ...
原來如此, CSS3D能做的事還有很多, 怎么用全看自己的想象力有多豐富了.
哈哈哈, 真想用CSS3D寫一個「我的世界」玩玩, 性能問題恐怕會有點大.
本文例子均在PC端體驗較好.
試玩地址[4]
源碼地址[5]
歡迎大家拍磚指正, 筆者功力尚淺, 如有不當(dāng)之處請斧正!
關(guān)于本文
作者:起小就些熊
https://juejin.cn/post/7000963575573381134
