pc端访问自己调整样式,没有适配,运行请再开启浏览器调试模拟移动端运行
先看一下运行效果
- 一开始准备做标准4x4的2048小游戏,后来突发奇想设计了自定义横纵数量的2048,让2048变得更有趣,首先定义了一个配置对象,建议空白单元格设置透明色,不然滑动动画感觉有点奇怪,其它格格块颜色可以自己加,不加就是随机颜色
const info = { rows: 4, // 横轴数量 cols: 4, // 纵轴数量 background: "pink", // 游戏盒子背景色 width: 80, // 矩阵宽度 space: 16, // 格子之间的间距 // 各个块背景颜色-没写将随机生成 0: "transparent", // 空白格颜色 // 生成数值概率(%) generate: [ [20, 4], // 20 - 0 = 20%概率 [100, 2], // 100 - 20 = 80%概率 ], initValueNum: 2, // 初始值个数 score: 0, // 分数 slide: 0, // 累计滑动次数 backNum: 3, // 可撤回次数 slideGenerateNum: 1, // 滑动一次生成数字个数 knockNum: 3, // 可敲击次数 };
- 接下来准备游戏盒子dom、一些变量存储数据
// 当前游戏矩阵 let matrix = []; // 生成dom列表 const contentList = []; // 生成随机颜色 const getColor = () => `#${Math.random().toString(16).slice(2, 8)}`; // 需要+动画的单元格 const animationList = []; // 历史矩阵记录列表 const historyList = [] // 历史分数记录列表 const scoreList = [] // 敲击标识 let knockFlag = false // 游戏盒子 const gameBox = document.querySelector("#game"); gameBox.style.padding = info.space + "px"; gameBox.style.width = info.width + "vw"; gameBox.style.backgroundColor = info.background; // 分数盒子 const scoreBox = document.querySelector(".score"); scoreBox.innerHTML = `分数:${info.score}`; // 滑动次数盒子 const slideBox = document.querySelector(".slide"); slideBox.innerHTML = `滑动:${info.slide}次`; // 撤回次数盒子 const backNumBox = document.querySelector(".backNum") backNumBox.innerHTML = `可撤回次数:${info.backNum}次` // 敲击次数盒子 const knockNumBox = document.querySelector(".knockNum") knockNumBox.innerHTML = `可敲击次数:${info.knockNum}次` // 操作盒子 const operationBox = document.querySelector(".operation") operationBox.style.width = info.width + "vw" // 提示盒子 const tipBox = document.querySelector(".tip") // 撤回按钮 const back = document.querySelector(".back") // 敲击按钮 const knock = document.querySelector(".knock")
- 准备几个辅助方法
// 动态计算字体大小 const calFontSize = (parentDom, dom) => { const pw = parentDom.clientWidth - 16; const ph = parentDom.clientHeight - 16; const w = dom.scrollWidth; const h = dom.scrollHeight; if (w > pw || h > ph) { const dw = w - pw; const dh = h - ph; if (dw > dh) { dom.firstChild.style.transform = `scale(${(pw - 50) / w})`; } else { dom.firstChild.style.transform = `scale(${(ph - 50) / h})`; } } }; // 空白格填入随机值 const generateNum = () => { // 空白位置列表 const zeroList = []; for (let i = 0; i < info.cols; i++) { for (let j = 0; j < info.rows; j++) { !matrix[i][j] && zeroList.push(i * info.rows + j); } } if(!zeroList.length) return // 随机点 const index = Math.floor(Math.random() * zeroList.length); // 计算坐标 const col = Math.floor(zeroList[index] / info.rows); const row = zeroList[index] % info.rows; // 随机数值并填入坐标 const random = Math.floor(Math.random() * 100); for (const [probability, value] of info.generate) { if (random < probability) { if (contentList[col * info.cols + row]) { contentList[col * info.cols + row].style.animation = "cell 0.3s" setTimeout(() => { contentList[col * info.cols + row].style.animation = "none" }, 300); } matrix[col][row] = value; break; } } }; // 判断游戏结束 const checkedGameOver = () => { // 获取单元格相邻单元格 const getSurround = (i, j) => [ [i + 1, j], [i - 1, j], [i, j + 1], [i, j - 1], ]; for (let i = 0; i < info.cols; i++) { for (let j = 0; j < info.rows; j++) { const num = matrix[i][j]; if (num === 0) return; const surround = getSurround(i, j); for (const [col, row] of surround) { if (matrix[col] && matrix[col][row] != undefined) { if (matrix[col][row] == num) return; } } } } alert("游戏结束"); };
- 初始化游戏,设置页面渲染函数
// 初始化 const init = () => { for (let i = 0; i < info.cols; i++) { const row = new Array(info.rows).fill(0); matrix.push(row); } // 初始值填入 for (let i = 0; i < info.initValueNum; i++) { generateNum(); } historyList.push([...matrix.map(m => [...m])]) scoreList.push(info.score) // 初始化渲染dom for (let i = 0; i < info.cols; i++) { const arr = matrix[i] for (let j = 0; j < info.rows; j++) { const num = arr[j] const div = document.createElement("div"); div.style.width = `calc(100% / ${info.rows})`; div.style.paddingBottom = `calc(100% / ${info.rows})`; div.style.height = "0"; div.style.position = "relative"; div.dataIndex = i * info.rows + j gameBox.appendChild(div); const content = document.createElement("div"); content.style.padding = info.space + "px"; content.classList.add("cell"); div.appendChild(content); const cell = document.createElement("div"); cell.classList.add("matrix"); if (!info[num]) info[num] = getColor(); cell.style.backgroundColor = info[num]; content.appendChild(cell); const valueDom = document.createElement("div"); valueDom.innerHTML = num === 0 ? "" : num; cell.appendChild(valueDom); contentList.push(content); // 计算字体大小 calFontSize(content, cell); } } }; // 渲染矩阵 const render = (direction) => { // 获取dom绝对纵轴位置 const getOffsetTop = (dom) => { return dom.offsetParent ? dom.offsetTop + getOffsetTop(dom.offsetParent) : dom.offsetTop; }; // 获取dom绝对横轴位置 const getOffsetLeft = (dom) => { return dom.offsetParent ? dom.offsetLeft + getOffsetLeft(dom.offsetParent) : dom.offsetLeft; }; // 动画效果 for (const [col, row, i, j] of animationList) { const start = contentList[col * info.rows + row]?.firstChild; const end = contentList[i * info.rows + j]?.firstChild; start.style.transition = "all 0.1s ease-out"; if (["up", "down"].includes(direction)) { start.style.transform = `translateY(${ getOffsetTop(end) - getOffsetTop(start) }px)`; } else { start.style.transform = `translateX(${ getOffsetLeft(end) - getOffsetLeft(start) }px)`; } } setTimeout(() => { animationList.splice(0); for (let i = 0; i < info.cols; i++) { const arr = matrix[i]; for (let j = 0; j < info.rows; j++) { const num = arr[j]; const index = i * info.rows + j; const dom = contentList[index].firstChild; dom.style.transition = "none"; dom.style.transform = "none"; if (!info[num]) info[num] = getColor(); dom.style.backgroundColor = info[num]; dom.firstChild.innerHTML = num === 0 ? "" : num; calFontSize(contentList[index], dom); } } }, 100); scoreBox.innerHTML = `分数:${info.score}`; slideBox.innerHTML = `滑动:${info.slide}次`; backNumBox.innerHTML = `可撤回次数:${info.backNum}次` knockNumBox.innerHTML = `可敲击次数:${info.knockNum}次` };
- 判断手势滑动方向
// 判断方向 const checkedDirection = (startX, startY, endX, endY) => { const absX = Math.abs(startX - endX); const absY = Math.abs(startY - endY); if (absX > absY) { // 左右 if (startX > endX) { return "left"; } else { return "right"; } } else { // 上下 if (startY > endY) { return "up"; } else { return "down"; } } };
- 重点来了,游戏算法核心逻辑,对二位数组进行上下左右移动。
// 移动 const move = (direction) => { // 是否在范围内 const isRange = (col, row) => matrix[col] && matrix[col][row] != undefined; // 各个方向map const next = { up: (col, row) => [col + 1, row], down: (col, row) => [col - 1, row], left: (col, row) => [col, row + 1], right: (col, row) => [col, row - 1], }; // 获取下一个非0的值 const getNextNonZeroValue = (col, row) => { let [nextX, nextY] = next[direction](col, row); while (isRange(nextX, nextY)) { const nextValue = matrix[nextX][nextY]; if (nextValue) { return [nextX, nextY, nextValue]; } else { [nextX, nextY] = next[direction](nextX, nextY); } } }; let changeFlag; // 计算坐标值 const cal = (i, j) => { if (!isRange(i, j)) return; // 计算当前坐标的值 const result = getNextNonZeroValue(i, j); if (!result) return; const [nextX, nextY, nextValue] = result; if (matrix[i][j] === 0) { changeFlag = true; matrix[i][j] = nextValue; matrix[nextX][nextY] = 0; animationList.push([nextX, nextY, i, j]); cal(i, j); } else if (matrix[i][j] === nextValue) { animationList.push([nextX, nextY, i, j]); changeFlag = true; matrix[i][j] *= 2; info.score += matrix[i][j]; matrix[nextX][nextY] = 0; } // 计算下一个坐标的值 cal(...next[direction](i, j)); }; if (direction === "up") { for (let i = 0; i < info.rows; i++) { cal(0, i); } } else if (direction === "down") { for (let i = 0; i < info.rows; i++) { cal(info.cols - 1, i); } } else if (direction === "left") { for (let i = 0; i < info.cols; i++) { cal(i, 0); } } else if (direction === "right") { for (let i = 0; i < info.cols; i++) { cal(i, info.rows - 1); } } if (changeFlag) { info.slide++; for (let i = 0; i < info.slideGenerateNum; i++) { generateNum() } render(direction); scoreList.push(info.score) historyList.push([...matrix.map(m => [...m])]) setTimeout(checkedGameOver, 300); } };
- 做手势事件监听,以及撤回,敲击按钮事件绑定,最后调用init(),开始游戏!!
let startX, startY; // 滑动事件绑定 gameBox.addEventListener("touchstart", (e) => { e.preventDefault() const { changedTouches: [{ clientX, clientY }] } = e startX = clientX; startY = clientY; }); gameBox.addEventListener("touchend", (e) => { e.preventDefault() const { changedTouches: [{ clientX, clientY }] } = e if (knockFlag) { const x = Math.abs(startX - clientX) const y = Math.abs(startY - clientY) if (x < 10 && y < 10) { // 获取自定义属性索引 const getIndex = (dom) => { return dom.dataIndex ?? getIndex(dom.parentNode) } const index = getIndex(e.target) const col = Math.floor(Number(index) / info.rows) const row = Number(index) % info.rows // 消除块儿 matrix[col][row] = 0 // 可敲击次数-1 info.knockNum-- knockFlag = false tipBox.style.display = "none" render() } } else { const direction = checkedDirection(startX, startY, clientX, clientY); move(direction); } }); // 撤回 back.onclick = () => { if (historyList.length === 1 || !info.backNum) return // 删除当前矩阵 historyList.pop() // 上一次矩阵 const back = historyList[historyList.length - 1] // 回退到上一次矩阵 matrix = [...back.map(m => [...m])] // 删除当前分数 scoreList.pop() // 回退到上一次分数 info.score = scoreList[scoreList.length - 1] // 滑动次数-1 info.slide-- // 可撤回次数-1 info.backNum-- render() } // 敲击 knock.onclick = () => { knockFlag = true tipBox.innerHTML = "请选择需要敲碎的块儿" tipBox.style.display = "block" } init();
html骨架如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="view" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="./index.css" />
<title>2048</title>
</head>
<body>
<noscript>
很抱歉!当前浏览器没有启用Javascript脚本,请先开启Javascript脚本
</noscript>
<div class="score"></div>
<div class="slide"></div>
<div class="backNum"></div>
<div class="knockNum"></div>
<div class="tip"></div>
<div id="game"></div>
<div class="operation">
<button class="operation-item back">撤回</button>
<button class="operation-item knock">敲击</button>
</div>
<script src="./index.js"></script>
</body>
</html>
样式如下
* {
padding: 0;
margin: 0;
touch-action: pan-y;
}
body {
font-size: 3em;
}
.score {
text-align: center;
}
.slide {
text-align: center;
}
.backNum {
text-align: center;
}
.knockNum {
text-align: center;
}
.tip {
margin-top: 32px;
text-align: center;
}
.operation {
height: 10vw;
position: fixed;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
}
.operation-item {
flex: 1;
margin: 0 8px;
height: 10vw;
font-size: 1em;
}
#game {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 16px;
display: flex;
flex-wrap: wrap;
box-sizing: border-box;
}
.cell {
position: absolute;
width: 100%;
height: 100%;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
}
.matrix {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
color: #ffffff;
border-radius: 8px;
font-size: 3.2em;
box-sizing: border-box;
}
.animation {
animation: cell 1s;
}
@keyframes cell {
0% {
transform: scale(0.8);
}
/*50% {
transform: scale(1.1);
}*/
100% {
transform: scale(1);
}
}
有不足的地方或者优化建议欢迎指出来纠正,谢谢观赏