【教程】用 HTML JavaScript 制作 2.5D 迷宫游戏地图

文章介绍了如何通过HTML5Canvas和JavaScript来创建一个2.5D迷宫游戏地图,首先讨论了使用CSS实现2.5D效果的局限性,然后详细阐述了如何利用等角变换和递归回溯算法生成迷宫地图,包括坐标转换和迷宫的绘制方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  我写了一个能够随机生成迷宫的算法,得到了用户很好的反响,对大家有所帮助。我现在想将这个迷宫以2.5D游戏地图的方式呈现出来。最初我考虑使用CSS来实现这个目标,但效果并不太理想,因为我只能将它渲染成背景,不能做更多的js操作。因此,我不打算继续使用CSS来达到这个目标。后来,我决定使用HTML5画布功能和地图块的坐标变换的方式来生成2.5D游戏地图。在文章的最后将把实现的地图的代码分享出来。
在这里插入图片描述

1 CSS 生成渲染2.5D

  使用css脚本样式将html中的div元素转换成2.5D效果,主要是通过css中的属性
在这里插入图片描述

<html>
<style>
.wrapper {
  position: fixed;
  width: 100%; height: 100%;
  left: 0; top: 0;
  text-align: center;
}
.wrapper:before {
  content: "";
  display: inline-block; vertical-align: middle;
  width: 0; height: 100%;
}
.wrapper > .grid {
  display: inline-block;
  vertical-align: middle;
}
.grid > .row {
  font-size: 0;
  width: 100px;
  white-space: nowrap;
}
.grid > .row > .cell {
  position: relative;
  display: inline-block;
  width: 10px; height: 10px;
  outline: 1px solid rgba(0, 0, 0, 0.3);
}
.grid > .row > .cell:hover {
  background-color: red;
}
.grid {
  transform: rotateX(40deg) rotateZ(45deg);
}
</style>
<body>
<div class="wrapper">
  <div class="grid">
    <div class="row">
      <div class="cell"></div>
      <div class="cell"></div>
      <div class="cell"></div>
      <div class="cell"></div>
      <div class="cell"></div>
    </div>
    <div class="row">
      <div class="cell"></div>
      <div class="cell"></div>
      <div class="cell"></div>
      <div class="cell"></div>
      <div class="cell"></div>
    </div>
    <div class="row">
      <div class="cell"></div>
      <div class="cell"></div>
      <div class="cell"></div>
      <div class="cell"></div>
      <div class="cell"></div>
    </div>
    <div class="row">
      <div class="cell"></div>
      <div class="cell"></div>
      <div class="cell"></div>
      <div class="cell"></div>
      <div class="cell"></div>
    </div>
    <div class="row">
      <div class="cell"></div>
      <div class="cell"></div>
      <div class="cell"></div>
      <div class="cell"></div>
      <div class="cell"></div>
    </div>
  </div>
</div>
<script>
</script>
</body>
</html>

这段代码中使用了 CSS transform 属性,它在一些较旧的浏览器(如 IE9 及更早版本)中可能不被支持。这意味着在这些浏览器中,等角视图可能无法正常显示。但是CSS又无法处理复杂交互代码中如果需要为你的网格添加更复杂的交互(如拖拽、缩放等),需要使用 JavaScript 或其他技术实现这些功能。而你的地图块的坐标的样式是写在 CSS 中的,如果你需要在运行时动态修改这些样式,可能需要使用 JavaScript 来操作 DOM,这会导致代码的复杂程度太高,最后导致无法增加其他的元素和功能到地图中来。

2 使用脚本生成迷宫地图

  本文将介绍如何使用HTML5 Canvas和JavaScript来制作一个2.5D迷宫游戏地图。HTML和CSS准备工作 我们需要在HTML文件中添加一个canvas元素来作为画布,然后使用CSS样式对其进行一些简单的样式设置。因为我们要在画布上绘制2.5D效果的地图,所以需要使用坐标变换函数将等角坐标系转换为屏幕坐标系和迷宫地图的生成算法。

<html>
<head>
</head>
<body>
   <style>
	canvas {
	  display: block;
	  margin: 0 auto;
	}
   </style>
  <div style="text-align: center;" id="maindiv">
     行数<input type="text" id="rowv" value="10"><button onclick="oncreate()" >生成</button>

	 <br>
    <!-- 创建一个画布,宽度为 640 像素,高度为 360 像素 -->
    <canvas width="800" height="500" id="canvas" ></canvas>
    <br>


  </div>
  <script type="text/javascript">

	// 获取画布元素
    var canvas = document.getElementById("canvas");

    // 获取画布宽度和高度
    var width = canvas.width;
    var height = canvas.height;
    // 获取画布上下文
    var context = canvas.getContext("2d");
    // 初始化格子
    var tile = [];
    var cols = 9;
    var rows = cols;

    // 等角变换的变量和辅助函数
    var IsoW = 40; // 格子宽度
    var IsoH = 20; // 格子高度
    var IsoX = width / 2; // 等角网格的中心 x 坐标
    var IsoY = 20; // 等角网格的顶部 y 坐标

    function IsoToScreenX(localX, localY) {
      // 将等角坐标转换为屏幕坐标的 x 坐标
      return IsoX + (localX - localY) * IsoW;
    }
    function IsoToScreenY(localX, localY) {
      // 将等角坐标转换为屏幕坐标的 y 坐标
      return IsoY + (localX + localY) * IsoH;
    }
    function ScreenToIsoX(globalX, globalY) {
      // 将屏幕坐标转换为等角坐标的 x 坐标
      return ((globalX - IsoX) / IsoW + (globalY - IsoY) / IsoH) / 2;
    }
    function ScreenToIsoY(globalX, globalY) {
      // 将屏幕坐标转换为等角坐标的 y 坐标
      return ((globalY - IsoY) / IsoH - (globalX - IsoX) / IsoW) / 2;
    }
	// 在给定的坐标处绘制变形倾斜45度
	function DrawIsoTile(x, y, color) {
	  // 设置填充颜色
	  context.fillStyle = color;
	  // 开始路径
	  context.beginPath();
	  // 绘制倾斜45度矩形
	  context.moveTo(x, y);
	  context.lineTo(x - IsoW, y + IsoH);
	  context.lineTo(x, y + IsoH * 2);
	  context.lineTo(x + IsoW, y + IsoH);
	  context.closePath();
	  // 填充矩形
	  context.fill();
	}

// 绘制事件
function onshow(ary) {
     canvas = document.getElementById("canvas");

    context.clearRect(0,0, canvas.width, canvas.height);
  // 循环遍历每个格子
	  for (var y = 0; y < rows; y++){
		for (var x = 0; x < cols; x++) {
		  // 获取该格子的图块和对象类型
		  var t = ary[y][x];
		  // 将等角坐标转换为屏幕坐标
		  var rx = IsoToScreenX(x, y);
		  var ry = IsoToScreenY(x, y);
		  // 绘制图块(如果有)
		  switch (t) {
		    //方格子上色,与变形倾斜45度
			case 0: DrawIsoTile(rx, ry, "#C59E77"); break;
			case 1: DrawIsoTile(rx, ry, "#94BA57"); break;
			case 2: DrawIsoTile(rx, ry, "#9DD5E2"); break;
		  }
		}
	}
}

function recursiveBacktrackingMaze(rows, cols) {
  let grid = new Array(rows);
  for (let i = 0; i < rows; i++) {
    grid[i] = new Array(cols).fill(1);
  }
  let stack = [{ row: 1, col: 1 }];
  while (stack.length > 0) {
    let current = stack[stack.length - 1];
    let neighbors = [];
    if (current.row > 2 && grid[current.row - 2][current.col] === 1) {
      neighbors.push({ row: current.row - 2, col: current.col });
    }
    if (current.col > 2 && grid[current.row][current.col - 2] === 1) {
      neighbors.push({ row: current.row, col: current.col - 2 });
    }
    if (current.row < rows - 3 && grid[current.row + 2][current.col] === 1) {
      neighbors.push({ row: current.row + 2, col: current.col });
    }
    if (current.col < cols - 3 && grid[current.row][current.col + 2] === 1) {
      neighbors.push({ row: current.row, col: current.col + 2 });
    }
    if (neighbors.length > 0) {
      let next = neighbors[Math.floor(Math.random() * neighbors.length)];
      let wallRow = (current.row + next.row) / 2;
      let wallCol = (current.col + next.col) / 2;
      grid[next.row][next.col] = 0;
      grid[wallRow][wallCol] = 0;
      stack.push(next);
    } else {
      stack.pop();
    }
  }
  // 设置入口和出口
  grid[1][0] = 0; // 入口
  grid[rows - 2][cols - 1] = 0; // 出口
  return grid;
}
function oncreate(){
    canvas = document.getElementById("canvas");
    cols = parseInt(document.getElementById("rowv").value);
	if(cols<=5){
		cols=5;
	}
	if(cols%2==0){
		cols=cols-1;
	}
    rows = cols;
    // 获取画布宽度和高度
	console.log(context);
	if(cols>12){
		canvas.width=740+(cols*45);
		canvas.height=500+(cols*30);
	}else{
		canvas.width=740;
		canvas.height=600
	}
     width = canvas.width;
     height = canvas.height;
	 IsoX = width / 2;
	 var ar=recursiveBacktrackingMaze(rows,cols);
	onshow(ar);
}
</script>
</body>
</html>

2.1 地图坐标45°变化

  为了将迷宫地图调整为2.5D效果,我们需要定义四个函数,它们用于将等角坐标系(isometric)与笛卡尔坐标系(screen)之间进行转换。等角坐标系常用于制作类似于 2.5D 的视觉效果。以下是各个函数的解释及示例:

// 初始化地图参数
var IsoW = 40; // 格子宽度
var IsoH = 20; // 格子高度
var IsoX = width / 2; // 等角网格的中心 x 坐标
var IsoY = 20; // 等角网格的顶部 y 坐标

function IsoToScreenX(localX, localY) {
    // 将等角坐标转换为屏幕坐标的 x 坐标
    return IsoX + (localX - localY) * IsoW;
}
function IsoToScreenY(localX, localY) {
    // 将等角坐标转换为屏幕坐标的 y 坐标
    return IsoY + (localX + localY) * IsoH;
}
function ScreenToIsoX(globalX, globalY) {
    // 将屏幕坐标转换为等角坐标的 x 坐标
    return ((globalX - IsoX) / IsoW + (globalY - IsoY) / IsoH) / 2;
}
function ScreenToIsoY(globalX, globalY) {
    // 将屏幕坐标转换为等角坐标的 y 坐标
    return ((globalY - IsoY) / IsoH - (globalX - IsoX) / IsoW) / 2;
}
// 在给定的坐标处绘制变形倾斜45度
function DrawIsoTile(x, y, color) {
    // 设置填充颜色
    context.fillStyle = color;
    // 开始路径
    context.beginPath();
	// 绘制倾斜45度矩形
    context.moveTo(x, y);
    context.lineTo(x - IsoW, y + IsoH);
    context.lineTo(x, y + IsoH * 2);
    context.lineTo(x + IsoW, y + IsoH);
    context.closePath();
    // 填充矩形
    context.fill();
}
  1. IsoToScreenX(localX, localY):将等角坐标转换为屏幕坐标的 x 坐标。
    • 输入:等角坐标的 localX 和 localY。
    • 输出:屏幕坐标的 x 坐标。
    • 示例:IsoToScreenX(2, 1) 可能返回 70(具体值取决于 IsoX 和 IsoW)。
  2. IsoToScreenY(localX, localY):将等角坐标转换为屏幕坐标的 y 坐标。
    • 输入:等角坐标的 localX 和 localY。
    • 输出:屏幕坐标的 y 坐标。
    • 示例:IsoToScreenY(2, 1) 可能返回 35(具体值取决于 IsoY 和 IsoH)。
  3. ScreenToIsoX(globalX, globalY):将屏幕坐标转换为等角坐标的 x 坐标。
    • 输入:屏幕坐标的 globalX 和 globalY。
    • 输出:等角坐标的 x 坐标。
    • 示例:ScreenToIsoX(70, 35) 可能返回 2(具体值取决于 IsoX 和 IsoW)。
  4. ScreenToIsoY(globalX, globalY):将屏幕坐标转换为等角坐标的 y 坐标。
    • 输入:屏幕坐标的 globalX 和 globalY。
    • 输出:等角坐标的 y 坐标。
    • 示例:ScreenToIsoY(70, 35) 可能返回 1(具体值取决于 IsoY 和 IsoH)。

最后,DrawIsoTile(x, y, color) 函数用于在给定坐标处绘制倾斜 45 度的矩形。这个函数接受三个参数:x 坐标、y 坐标和填充颜色。示例:DrawIsoTile(70, 35, '#FF0000') 会在屏幕坐标 (70, 35) 处绘制一个红色的等角矩形。

这里有一些关于等角投影和笛卡尔坐标系之间的转换的例子: 假设 IsoX = 0, IsoY = 0, IsoW = 30, IsoH = 15。

  • 将等角坐标 (2, 1) 转换为屏幕坐标:

    • X 坐标:IsoToScreenX(2, 1) = 0 + (2 - 1) * 30 = 30
    • Y 坐标:IsoToScreenY(2, 1) = 0 + (2 + 1) * 15 = 45

    所以,等角坐标 (2, 1) 对应的屏幕坐标是 (30, 45)。

    另外,你也可以使用 ScreenToIsoXScreenToIsoY 函数将屏幕坐标转换回等角坐标。例如,假设屏幕坐标是 (30, 45):

    • X 坐标:ScreenToIsoX(30, 45) = ((30 - 0) / 30 + (45 - 0) / 15) / 2 = (1 + 3) / 2 = 2
    • Y 坐标:ScreenToIsoY(30, 45) = ((45 - 0) / 15 - (30 - 0) / 30) / 2 = (3 - 1) / 2 = 1

所以,屏幕坐标 (30, 45) 对应的等角坐标是 (2, 1)。

2.2 迷宫算法DFS生成

  我优化了一下迷宫地图的生成方法,在原来的基础上使用了递归回溯算法的迷宫生成方式。在javascript脚本中创建一个recursiveBacktrackingMaze函数。这个算法的名称是“递归回溯法生成迷宫”(Recursive Backtracking Maze Generation)。它是一种基于深度优先搜索(Depth-First Search,DFS)的迷宫生成算法。递归回溯法通过从起点开始,随机选择一个方向移动,并创建迷宫的路径。当无法继续前进时,它会回溯到先前的路径点,直到找到新的未访问的邻居或回到起点。这个过程持续进行,直到所有可访问的单元格都被访问过。

function recursiveBacktrackingMaze(rows, cols) {
  // 创建一个空白网格,所有单元格都填充为1(墙壁)
  let grid = new Array(rows);
  for (let i = 0; i < rows; i++) {
    grid[i] = new Array(cols).fill(1);
  }
  // 初始化栈,并将起始单元格放入栈中
  let stack = [{ row: 1, col: 1 }];
  // 当栈不为空时,继续循环
  while (stack.length > 0) {
    // 获取当前单元格(栈顶元素)
    let current = stack[stack.length - 1];
    // 查找当前单元格的邻居
    let neighbors = [];
    // 检查上方邻居
    if (current.row > 2 && grid[current.row - 2][current.col] === 1) {
      neighbors.push({ row: current.row - 2, col: current.col });
    }
    // 检查左侧邻居
    if (current.col > 2 && grid[current.row][current.col - 2] === 1) {
      neighbors.push({ row: current.row, col: current.col - 2 });
    }
    // 检查下方邻居
    if (current.row < rows - 3 && grid[current.row + 2][current.col] === 1) {
      neighbors.push({ row: current.row + 2, col: current.col });
    }
    // 检查右侧邻居
    if (current.col < cols - 3 && grid[current.row][current.col + 2] === 1) {
      neighbors.push({ row: current.row, col: current.col + 2 });
    }

    // 如果有未访问的邻居
    if (neighbors.length > 0) {
      // 随机选择一个邻居
      let next = neighbors[Math.floor(Math.random() * neighbors.length)];
      // 移除墙壁(将邻居和中间单元格设为0)
      let wallRow = (current.row + next.row) / 2;
      let wallCol = (current.col + next.col) / 2;
      grid[next.row][next.col] = 0;
      grid[wallRow][wallCol] = 0;
      // 将选择的邻居添加到栈中
      stack.push(next);
    } else {
      // 如果没有未访问的邻居,从栈中弹出当前单元格
      stack.pop();
    }
  }

  // 返回生成的迷宫网格
  return grid;
}

  递归回溯算法是一种深度优先搜索(DFS)算法,它从一个起始单元格开始,沿着未访问过的邻居单元格随机移动,并创建一个路径。当没有未访问的邻居时,它会回溯到先前的单元格,寻找其他未访问的邻居。这个过程将持续进行,直到回溯到起始单元格并且所有可访问的单元格都被访问过。我们需要先了解一下网格的结构和单元格的坐标。在这个算法中,网格是由奇数行和奇数列组成的,每个奇数行和奇数列上的单元格都是墙壁。这样,相邻的两个房间(即0值单元格)之间总是有一个墙壁(即1值单元格)。
  让我们使用一个5x5网格来演示该算法的运行过程,并提供一些代码示例。

初始状态,5x5网格:

1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1

1 设置起始单元格(1,1)为0,并将其添加到栈中。

javascriptCopy codelet stack = [{ row: 1, col: 1 }];
grid[1][1] = 0;
-----------------------------------
1 1 1 1 1
1 0 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1

2 可访问的邻居生成
  在递归回溯迷宫生成算法中,可访问的邻居是从当前单元格开始,沿上、下、左、右四个方向,跳过一个单元格的位置。这意味着邻居之间始终有一个单元格的距离,这个单元格是它们之间的墙壁。这就是为什么算法中的行和列索引增量是2,而不是1。
  在算法中,我们首先创建一个空的邻居列表。然后,我们检查当前单元格的上、下、左、右四个方向的邻居,以确保它们在网格范围内并且尚未访问(即其值为1)。如果满足条件,我们将邻居添加到列表中。

let neighbors = [];
// 检查上方邻居
if (current.row > 2 && grid[current.row - 2][current.col] === 1) {
  neighbors.push({ row: current.row - 2, col: current.col });
}
// 检查左侧邻居
if (current.col > 2 && grid[current.row][current.col - 2] === 1) {
  neighbors.push({ row: current.row, col: current.col - 2 });
}
// 检查下方邻居
if (current.row < rows - 3 && grid[current.row + 2][current.col] === 1) {
  neighbors.push({ row: current.row + 2, col: current.col });
}
// 检查右侧邻居
if (current.col < cols - 3 && grid[current.row][current.col + 2] === 1) {
  neighbors.push({ row: current.row, col: current.col + 2 });
}
-----------------------------------
假设我们有一个5x5网格,并且当前单元格位于(1,11 1 1 1 1
1 0 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1

那么可访问的邻居计算如下:

  • 上方邻居:没有,因为它超出了网格范围。
  • 左侧邻居:没有,因为它超出了网格范围。
  • 下方邻居:没有,因为它是墙壁(值为1)。
  • 右侧邻居:(1,3),因为它在网格范围内并且尚未访问(值为1)。

所以在这个例子中,可访问的邻居列表只包含一个元素:[{ row: 1, col: 3 }]

3 当前单元格(1,1)有一个可访问的邻居:(1,3)。选择(1,3)并移除(1,1)和(1,3)之间的墙壁。

​ 这段代码的目的是移除两个相邻单元格之间的墙壁。为了理解这段代码,我们需要先了解一下网格的结构和单元格的坐标。在这个算法中,网格是由奇数行和奇数列组成的,每个奇数行和奇数列上的单元格都是墙壁。这样,相邻的两个房间(即0值单元格)之间总是有一个墙壁(即1值单元格)。
  这里,current 是当前单元格,next 是选择的邻居单元格。由于我们在处理奇数行和奇数列,所以这两个单元格之间的距离始终是2。
  因此,我们可以通过对它们的行和列分别求平均值来找到它们之间的墙壁单元格。这就是 wallRowwallCol 的计算方法。接下来,我们将 next 单元格(邻居单元格)的值设为0,表示这个单元格已被访问并且成为通路。同时,我们也将 wallRowwallCol 对应的单元格设为0,表示移除了墙壁,连接了 currentnext 两个房间。

javascriptCopy codelet current = stack[stack.length - 1]; // { row: 1, col: 1 }
let neighbors = [{ row: 1, col: 3 }];
let next = neighbors[0];
let wallRow = (current.row + next.row) / 2;
let wallCol = (current.col + next.col) / 2;
grid[next.row][next.col] = 0;
grid[wallRow][wallCol] = 0;
stack.push(next);
-----------------------------------
1 1 1 1 1
1 0 0 0 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1

4 当前位置:(1,3),向下移动,移动到(3,3) 并移除墙壁((1,3) 和 (3,3) 之间)。
  当前位置在(1,3),接下来我们要向下移动,即沿列方向移动。首先,我们要找到下一个可移动的邻居位置。在算法中,这部分由以下代码实现:

// 查找当前单元格的邻居
let neighbors = [];
...
// 检查下方邻居
if (current.row < rows - 3 && grid[current.row + 2][current.col] === 1) {
  neighbors.push({ row: current.row + 2, col: current.col });
}

  这段代码检查了下方邻居是否可达。current.row < rows - 3 确保我们不会超出网格的边界。grid[current.row + 2][current.col] === 1 确保下方邻居(距离2个单位的位置)还没有被访问过(值为1表示墙壁,也表示未访问)。如果满足条件,我们将这个邻居添加到邻居列表中。
  接下来,我们从邻居列表中随机选择一个邻居作为下一个要访问的位置。这个过程由以下代码实现:

// 随机选择一个邻居
let next = neighbors[Math.floor(Math.random() * neighbors.length)];

  这个例子中,我们选择了下方邻居,即(3,3)。接下来,我们需要移除当前位置(1,3)和目标位置(3,3)之间的墙壁。墙壁位于这两个位置的中间,即(2,3)。以下代码实现了墙壁的移除:

javascriptCopy code// 移除墙壁(将邻居和中间单元格设为0)
let wallRow = (current.row + next.row) / 2;
let wallCol = (current.col + next.col) / 2;
grid[next.row][next.col] = 0;
grid[wallRow][wallCol] = 0;

  wallRowwallCol 计算了墙壁的位置。grid[next.row][next.col] = 0; 将目标位置(3,3)设置为0,表示这是一个通路。grid[wallRow][wallCol] = 0; 将墙壁位置(2,3)设置为0,表示墙壁已经移除。
  最后,将目标位置(3,3)添加到栈中,表示我们已经访问过这个位置。这由以下代码实现:

// 将选择的邻居添加到栈中
stack.push(next);

  现在,我们已经从(1,3)移动到了(3,3),并移除了它们之间的墙壁。这就是向下移动的生成过程。在迭代过程中,这个过程会不断重复,直到所有可访问的单元格都被访问。

1 1 1 1 1
1 0 0 0 1
1 1 1 0 1
1 1 1 0 1
1 1 1 1 1

5 当前位置:(3,3),向左移动,移动到(3,1) 并移除墙壁((3,3) 和 (3,1) 之间)

1 1 1 1 1
1 0 0 0 1
1 1 1 0 1
1 0 0 0 1
1 1 1 1 1

  这些数据表示了迷宫生成过程中每个步骤的网格状态。请注意,由于算法的随机性,实际生成的迷宫可能有所不同。
如果大家有兴趣,可以改成我前面的文章中的迷宫生成算法,来生产自己的2.5D地图迷宫。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zht_bs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值