游戏的展示
项目概述
1. 界面初始化
整个棋盘的大小通过读取可视窗口来计算,通过vue的onMountd钩子来调用棋盘初始化,根据玩家所选的难度生成不同的障碍物地形数量和npc数量以及npc的移动速度。
2. 玩家的移动
通过键盘事件监听正确的玩家按键来决定玩家下一步的方位,通过下一步的按键找到对应的格子通过格子定位到玩家的下一步正确位置,对边界进行判断无法移动到边界和障碍物地形。同时调用碰撞检测函数是否与npc发生碰撞。
3.npc的移动
开始以后,给予npc移动状态、随机时间、移动速度,使得npc们错位时间下移动随机位置,并调用判断周围是否有玩家函数,如果有玩家那么调用A*寻路算法追逐玩家。
4.游戏结束
当玩家抵达右下角后清除所有定时器,调用胜利弹窗
当玩家爱心数量为0时,调用失败弹窗
代码逻辑
初始化游戏
根据容器大小设置初始化格子
function init() {
//获取屏幕宽高
h = document.querySelector(".game-container").offsetHeight
w = document.querySelector(".game-container").offsetWidth
containerSytle.value.width = w + 'px'
containerSytle.value.height = h + 'px'
//盒子宽度设置好
box = document.querySelector(".game-container")
boxItem.value.width = w / 30 - 0.04 + 'px'
boxItem.value.height = h / 15 - 0.04 + 'px'
//起点终点
startRow = 0,
startCol = 0,
endRow = 14,
endCol = 29
//初始化容器
ground = [],
annimalList = []
canWInFlag = false;
moveTarget = new Array(300).fill(0)
//初始化玩家及其血条
const spanPlaer = document.createElement("span")
spanPlaer.id = "player"
spanPlaer.style.width = boxItem.value.width
spanPlaer.style.height = boxItem.value.height
const xt = document.createElement("span")
xt.className = "groth"
for (let i = 1; i <= 3; i++) {
const img = document.createElement("img")
img.src = './GameIcon/爱心.png'
xt.appendChild(img)
}
xt.style.bottom = offsetH - 10 + 'px'
spanPlaer.appendChild(xt)
box.appendChild(spanPlaer)
player = document.getElementById("player")
animalSpan = []
animalHas = []
moveIndex = {
left: 0,
top: 0,
};
offsetW = w / 30
offsetH = h / 15
board = Array.from({ length: 15 }).map(() =>
Array.from({ length: 30 }).fill(0)
)
vis = Array.from({ length: 15 }).map(() =>
Array.from({ length: 30 }).fill(0)
)
dangerAnimal = Array.from({ length: 15 }).map(() =>
Array.from({ length: 30 }).fill(0)
)
//初始化棋盘
for (let i = 1; i <= 450; i++) {
let div = document.createElement("div")
div.className = "game-item"
div.style.width = boxItem.value.width
div.style.height = boxItem.value.height
box.appendChild(div)
}
变量声明,dx和dy为四个方向偏移量
var dx = [-1, 0, 1, 0],
dy = [0, 1, 0, -1]
//游戏初始化
let w, h, ground, annimalList, moveTarget, moveIndex, player, animalSpan, board, vis, dangerAnimal, divList, offsetW, offsetH, startRow, startCol, endRow, endCol, canWInFlag, box, stopTimer, animalHas
坐标生成函数用来生成位置随机行和列。
//坐标生成函数
function indexsAppear() {
return [Math.floor(Math.random() * 15), Math.floor(Math.random() * 30)]
}
toIndexs函数用来把坐标转成索引渲染指定格子。
function toIndexs(row, col) {
return row * 30 + col
}
groundAppear函数固定遍历100次,如果格子是起点或者终点就跳过,在dom树上给格子标记为ground(地形),board数组同时也标记。
//地形生成
function groundAppear() {
for (let i = 1; i <= 100; i++) {
let divList = document.querySelectorAll(".game-item")
let arr = indexsAppear();
if (arr[0] == startRow && arr[1] == startCol || arr[0] == endRow && arr[1] == endCol) {
i--;
continue
}
let indexs = toIndexs(arr[0], arr[1])
ground.push({
row: arr[0],
col: arr[1]
})
board[arr[0]][arr[1]] = 999;
divList[indexs].id = "ground"
}
}
danger函数生成npc,为了游戏体验,在起点和终点的9个格子周围不允许生成。
//随机动物阻挡格子生成
function danger(diff) {
let cnt = diff;
for (let i = 1; i <= cnt; i++) {
let indexs = indexsAppear()
//起点周围9个格子不允许有动物
if (board[indexs[0]][indexs[1]] === 999 || dangerAnimal[indexs[0]][indexs[1]] === 1 ||
indexs[0] == startRow && indexs[1] == startCol || indexs[0] == endRow && indexs[1] == endCol || indexs[0] <
3 && indexs[1] < 3) {
i--;
continue;
}
dangerAnimal[indexs[0]][indexs[1]] = 1;
annimalList.push({
row: indexs[0],
col: indexs[1]
})
//处理动物 这些动物经过定位在棋盘某个位置上
let row = indexs[0], col = indexs[1]
if (animalHas.indexOf(toIndexs(row, col)) !== -1) {
i--;
continue;
}
animalHas.push(toIndexs(row, col))
let span = document.createElement("span")
span.className = "animalDanger"
span.style.width = boxItem.value.width
span.style.height = boxItem.value.height
span.style.top = row * offsetH + 'px'
span.style.left = col * offsetW + 'px'
span.style.backgroundImage = `url(./GameIcon/${i - 1}.png)`
box.appendChild(span)
animalSpan.push(span)
}
}
canwin函数使用了dfs来判断此次的地形是否会阻挡起点和终点的路径 如何不符合接下来会重复调用地形生成函数
//地形是否支持到达终点
function canWin(row, col) {
if (row === endRow && col === endCol) {
canWInFlag = true;
return;
}
for (let i = 0; i < 4; i++) {
let tx = row + dx[i]
let ty = col + dy[i]
if (tx < 0 || ty < 0 || tx >= 15 || ty >= 30 || vis[tx][ty] === 1 || board[tx][ty] === 999 || dangerAnimal[tx][
ty
] === 1) continue;
vis[tx][ty] = 1;
canWin(tx, ty)
}
}
开始游戏
玩家移动,清除鼠标的默认事件防止影响游戏体验,对键位进行case, 监听其下一步是否越界,并根据下一步的位置计算格子的所在位置进行偏移。
//玩家的创建和移动
function playerMove(e) {
e.preventDefault()
let divList = document.querySelectorAll(".game-item")
switch (e.key) {
case 'ArrowLeft':
if (moveIndex.left - 1 < 0 || divList[toIndexs(moveIndex.top, moveIndex.left - 1)].id === "ground") return;
moveSy.currentTime = 0;
moveSy.play()
gameStart.step++;
moveIndex.left--
player.style.left = divList[moveIndex.left].offsetLeft + 'px'
break;
case 'ArrowRight':
if (moveIndex.left + 1 > 29 || divList[toIndexs(moveIndex.top, moveIndex.left + 1)].id === "ground") return;
moveSy.currentTime = 0;
moveSy.play()
moveIndex.left++
gameStart.step++;
player.style.left = divList[moveIndex.left].offsetLeft + 'px'
break;
case 'ArrowDown':
if (moveIndex.top + 1 > 14 || divList[toIndexs(moveIndex.top + 1, moveIndex.left)].id === "ground") return;
moveSy.currentTime = 0;
moveSy.play()
moveIndex.top++
gameStart.step++;
player.style.top = divList[moveIndex.top * 30].offsetTop + 'px'
break;
case 'ArrowUp':
if (moveIndex.top - 1 < 0 || divList[toIndexs(moveIndex.top - 1, moveIndex.left)].id === "ground") return;
moveSy.currentTime = 0;
moveSy.play()
gameStart.step++;
moveIndex.top--
player.style.top = divList[moveIndex.top * 30].offsetTop + 'px'
break;
}
}
startMove函数通过给与动物随机的移动时间并开始移动,只能在周围5*5的方格内移动,同时不能越界和超过障碍物,如果检测到周围有玩家则进入A*函数
//周围是否有玩家
function hasPlayer(sRow, sCol, eRow, eCol) {
for (let i = sRow; i <= eRow; i++) {
for (let j = sCol; j <= eCol; j++) {
if (i === moveIndex.top && j === moveIndex.left) {
gameStart.chase++
return false;
}
}
}
return true;
}
function startMove() {
for (let i = 0; i < annimalList.length; i++) {
let randomTime = Math.floor(Math.random() * difficultyInfo.value[difficulty.value].animalMoveSpeed +
Math.random() * difficultyInfo.value[difficulty.value].animalMoveSpeed + 100)
timerList.value.push(
setInterval(() => {
animalMove(i, difficultyInfo.value[difficulty.value].animalChase);
}, randomTime)
);
}
}
function animalMove(target, t) {
//感应玩家到四周追逐玩家 或者自由移动
let sRow = Math.max(0, annimalList[target].row - difficultyInfo.value[difficulty.value].around)
let sCol = Math.max(0, annimalList[target].col - difficultyInfo.value[difficulty.value].around)
let eRow = Math.min(14, annimalList[target].row + difficultyInfo.value[difficulty.value].around)
let eCol = Math.min(29, annimalList[target].col + difficultyInfo.value[difficulty.value].around)
if (!hasPlayer(sRow, sCol, eRow, eCol)) {
AStar(annimalList[target].row, annimalList[target].col, moveIndex.top, moveIndex.left, target, t);
} else {
let ts = Math.floor(Math.random() * 4)
let row = annimalList[target].row + dx[ts];
let col = annimalList[target].col + dy[ts];
if (row < 0 || col < 0 || row > 14 || col > 29 || board[row][col] === 999) {
return
}
animalSpan[target].style.left = col * offsetW + 'px'
animalSpan[target].style.top = row * offsetH + 'px'
annimalList[target].row = row
annimalList[target].col = col
}
}
动物的寻路逻辑的A*函数,通过计算曼哈顿距离作为成本量来确定下一次的路径,保存路径后调用定时器追击玩家,直到路径走完回归随机路线。
//A*函数
function AStar(startx, starty, endx, endy, targetDanger, t) {
//曼哈顿距离
function mhd(startx, starty, targetx, targety) {
return Math.abs(startx - targetx) + Math.abs(targety - starty)
}
let dis = Array.from({ length: 15 }).map(() => Array.from({ length: 30 }).fill(Infinity))
let AVis = Array.from({ length: 15 }).map(() => Array.from({ length: 30 }).fill(0))
let prev = Array.from({ length: 15 }, () => Array(30).fill(null));
let heap = []
dis[startx][starty] = 0;
heap.push({ row: startx, col: starty, step: +mhd(startx, starty, endx, endy) });
while (heap.length > 0) {
heap.sort((a, b) => a.step - b.step)
let { row, col } = heap.shift()
if (row === endx && col === endy) break;
if (AVis[row][col] === 1) continue;
AVis[row][col] = 1;
for (let i = 0; i < 4; i++) {
let tx = row + dx[i];
let ty = col + dy[i];
if (tx < 0 || ty < 0 || tx > 14 || ty > 29 || board[tx][ty] === 999) continue;
if (dis[row][col] + 1 < dis[tx][ty]) {
dis[tx][ty] = dis[row][col] + 1
prev[tx][ty] = { row, col };
heap.push({ row: tx, col: ty, step: dis[row][col] + 1 + mhd(tx, ty, endx, endy) });
}
}
}
let path = [];
let currentNode = { row: endx, col: endy };
while (currentNode !== null) {
path.unshift(currentNode);
currentNode = prev[currentNode.row][currentNode.col];
}
if (path.length > 1) { // 确保找到了有效路径
let indexs = 0
let timer = setInterval(() => {
indexs++
animalSpan[targetDanger].style.left = path[indexs].col * offsetW + 'px'
animalSpan[targetDanger].style.top = path[indexs].row * offsetH + 'px'
annimalList[targetDanger].row = path[indexs].row
annimalList[targetDanger].col = path[indexs].col
if (indexs + 1 >= path.length) {
// moveTarget[targetDanger] = 0
clearInterval(timer)
return
}
}, t)
} else {
return;
}
}
碰撞检测函数,如果检测到碰撞那么玩家头顶就会掉一颗星,且短暂进入无敌状态。
碰撞检测的原理可以看一下这张图,描述的是安全距离
function hit() {
if (!ifStart.value) return
for (let i = 0; i < animalSpan.length; i++) {
// 定义碰撞缓冲区的大小
if (
(animalSpan[i].offsetLeft + animalSpan[i].offsetWidth>= player.offsetLeft) &&
(animalSpan[i].offsetLeft<= player.offsetLeft + player.offsetWidth) &&
(animalSpan[i].offsetTop + animalSpan[i].offsetHeight >= player.offsetTop) &&
(animalSpan[i].offsetTop <= player.offsetTop + player.offsetHeight)
) {
gameStart.groth--;
let groth = document.querySelector(".groth")
pengzhuang.currentTime = 0;
pengzhuang.play()
if (groth.lastChild) groth.removeChild(groth.lastChild)
if (gameStart.groth < 1) {
document.getElementById("bai").play()
gameStart.gameState = false;
gameVictory.value = false;
document.documentElement.removeEventListener("keydown", playerMove)
clearInterval(stopTimer)
} else {
//进入短暂无敌状态
invincible.value = true
clearInterval(stopTimer)
setTimeout(() => {
invincible.value = false
}, 400)
}
}
}
游戏结束
通过控制pinia来共享组件间的游戏状态,当游戏结束时对应的游戏状态随之改变。
<div class="game-victory" :class="{ appear: gameState.gameVictory === true || gameState.gameVictory === false }">
<h3>{{ gameState.gameVictory === true ? "恭喜你,赢了!" : "很遗憾,输了!" }}</h3>
<p>{{info[gameState.difficulty] }}</p>
<p>走了 {{ gameState.step }} 步<span v-if="gameState.gameVictory">到达终点</span>,躲过 {{ gameState.chase }} 次动物的追击</p>
<p>打败了全世界的人!</p>
<button @click="gameState.reset">再来一局</button>
</div>
至此整个项目的部分代码和逻辑就到这里结束啦,这样看是不是也不能很难呢,由于完整代码太长了,大家可以试着思路自己写以下,如果实在写不出来的话可以关注我在底下评论我把代码打包发给你哦。