系列文章目录
第一章 2D二维地图绘制、人物移动、障碍检测
第二章 跟随人物二维动态地图绘制、自动寻径、小地图显示(人物红点显示)
文章目录
- 系列文章目录
- 前言
- 一、本章节效果图
- 二、介绍
- 2.1、左边区域
- 2.2、右边区域
- 三、列计划
- 3.1、目标
- 3.1.1、完成跟随人物二维动态地图绘制(本期只完成高度动态)
- 3.12、自动寻径
- 3.13、小地图显示(人物红点显示)
- 3.2、步骤
- 四、实际作业流程
- 4.1、固定画布高度
- 4.1.1、地图绘制(地图数据、英雄初始数据、物品数据)
- 4.1.2、设置地图最大高度、英雄与上下边框的距离
- 4.1.3、根据人物英雄中心点位置,确定二维地图渲染的内容
- 4.2、自动寻径
- 4.2.1、采用ChatGpt生成的JavaScript版本aStart算法,计算出从起点坐标到终点坐标的所有路径数据
- 4.2.2、通过JavaScript定时器每100毫秒去获取路径数组的头部数据(访问即删除),并实时更新地图信息(包括实时英雄坐标点、自动寻径数据)
- 总结
前言
带大家回顾下第一章的内容。
- 使用JavaScript绘制简单的二维地图
采用二维数组存储地图信息,使用表格绘制地图,每个td单元格存储数据 - 键盘上下左右控制
使用JavaScript keyPress键盘事件监听WASD键,按键触发时人物做出相应操作 - 障碍物碰撞检测(采用格子碰撞检测)
人物下一步碰撞到石头时,提示遇到障碍,终止人物运动
一、本章节效果图
二、介绍
游戏界面分2个区域,左边有小地图、英雄坐标、自动寻径路径,右边是大地图区域
2.1、左边区域
- 小地图
最左边顶部区域,会等比缩放大地图数据,英雄格使用红点替代;
点击小地图也会触发自动寻径 - 英雄坐标
显示英雄的实时坐标地址,包括 - 自动寻径的坐标
采用ChatGpt生成JavaScript版本aStart算法,里面存放的是从起点坐标到终点坐标的所有路径数据,是[[x,y],[x,y],[x,y]]这样的数据,坐标格式正好跟第一章里的坐标相反(因为要先渲染y轴,也就是tr数据)
x | x1 | x2 | |
---|---|---|---|
y | 0, 0 | 0,1 | 0,2 |
y1 | 1,0 | 1,1 | 1,2 |
y2 | 2,0 | 2,1 | 2,2 |
2.2、右边区域
- 大地图
右边的大区域,渲染地图、英雄信息、障碍信息、英雄自动寻径路径、英雄移动地图动态跟随绘制等
三、列计划
3.1、目标
3.1.1、完成跟随人物二维动态地图绘制(本期只完成高度动态)
3.12、自动寻径
3.13、小地图显示(人物红点显示)
3.2、步骤
- 固定画布高度(本期只完成高度动态)
根据人物英雄中心点位置,确定二维地图渲染的内容
需要判断上边距、下边距 - 自动寻径
采用ChatGpt生成的JavaScript版本aStart算法,计算出从起点坐标到终点坐标的所有路径数据,通过JavaScript定时器每100毫秒去获取路径数组的头部数据(访问即删除),并实时更新地图信息(包括实时英雄坐标点、自动寻径数据) - 小地图显示(人物红点显示)
等比缩放大地图,英雄采用红点替代,小地图也可触发自动寻径
四、实际作业流程
4.1、固定画布高度
4.1.1、地图绘制(地图数据、英雄初始数据、物品数据)
/**
* 加载地图数据
* 0 空地/草坪
* 1 石头
* 9 英雄
* @type {number[]}
*/
var mapData = [
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1],
[1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1],
[1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 0, 1, 3, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1],
[1, 1, 8, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1],
[1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1],
[1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 7, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2, 0, 1, 1, 0, 1],
[1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1],
[1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 4, 0, 1, 1, 0, 1],
[1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 3, 0, 1, 1, 0, 1],
[1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 8, 0, 1, 1, 0, 1],
[1, 0, 1, 0, 0, 0, 0, 1, 0, 6, 7, 0, 1, 1, 0, 1],
[1, 0, 1, 0, 0, 0, 0, 5, 0, 0, 2, 0, 1, 1, 0, 1],
[1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1],
[1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1],
[1, 0, 1, 0, 0, 0, 0, 0, 6, 0, 1, 0, 1, 1, 0, 1],
[1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1],
[1, 0, 7, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1],
[1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1],
[1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1],
[1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
]
var item = {};
item.empty = 0; //空地或草坪
item.stone = 1; //石头的标记是1
item.factory = 2; //工厂
item.girl = 3; //女子
item.girl_01 = 4; //女孩
item.kt = 5; //空投大礼包
item.lz = 6; //路障
item.pz = 7; //喷子
item.zz = 8; //沼泽
item.hero = 9; //英雄的标记是9
item.heroHasPath = 10; //自动寻径的英雄标记是10
var items = [];
var itemPrefixPath = "../img/item/";
items[0] = "";
items[1] = itemPrefixPath + "stone.png";
items[2] = itemPrefixPath + "gc.png";
items[3] = itemPrefixPath + "girl.png";
items[4] = itemPrefixPath + "girl.bmp";
items[5] = itemPrefixPath + "kt.png";
items[6] = itemPrefixPath + "lz.png";
items[7] = itemPrefixPath + "pz.png";
items[8] = itemPrefixPath + "zz.png";
items[9] = itemPrefixPath + "/spine/hero002.gif";
items[10] = itemPrefixPath + "/spine/tank.gif";
var heroPoint = [1, 4]; //初始化英雄的位置是 1,4
// 自动寻径的路径
var path = [];
4.1.2、设置地图最大高度、英雄与上下边框的距离
// 设定地图最大的高度
var maxRow = 7;
// 地图的行
var row = mapData.length > maxRow ? maxRow : mapData.length;
// 英雄与上下边框的距离
var heroMargin = Math.floor(row / 2);
//地图的列
var column = mapData[0].length;
4.1.3、根据人物英雄中心点位置,确定二维地图渲染的内容
1、判断上边距
英雄的y坐标点,如果在头几行,地图从头开始加载
如果不在头几行,需要从 [英雄的y坐标点-向下取整(固定长度/2)] 开始加载数据
2、判断下边距
英雄的y坐标点,如果在尾部几行,地图从 [地图数据长度 - 固定距离] 到地图数据长度开始渲染数据
如果不在尾部几行,需要从 [英雄的y坐标点-向下取整(固定长度/2)] 开始加载数据
3、中间部分直接走[英雄的y坐标点-向下取整(固定长度/2)]
function loadData() {
// 获取地图对象
var map = document.getElementById("map1001");
// 获取小地图
var smallMap = document.getElementById("smallMap1001");
// 英雄的坐标位置
var heroPointElement = document.getElementById("heroPoint")
// i的初始值
// 判断上边距
var nowI = heroPoint[0] - heroMargin > 0 ? heroPoint[0] - heroMargin : 0;
if (heroPoint[0] + heroMargin > mapData.length - 1) {
// 判断下边距
nowI = heroPoint[0] + maxRow > mapData.length ? mapData.length - maxRow : nowI;
}
//渲染 row 行 column 列的数据
var mapHTML = "";
for (var i = nowI; i < nowI + row; i++) {
mapHTML += "<tr>";
for (var j = 0; j < column; j++) {
if (mapData[i][j] == item.empty) { //只有点击路,才能自动寻径
mapHTML += "<td οnclick='tdClick(" + i + "," + j + ")'></td>";
} else {
mapHTML += '<td><img src="'+ items[mapData[i][j]] +'" style="width: 100%; height: 100%; border-radius: 0%;" ></td>';
}
}
mapHTML += "</tr>";
}
// 渲染大地图
map.innerHTML = mapHTML;
//渲染小地图数据
var smallMapHTML = "";
for (var i = 0; i < mapData.length; i++) {
smallMapHTML += "<tr>";
for (var j = 0; j < column; j++) {
if (mapData[i][j] == item.empty) { //只有点击路,才能自动寻径
smallMapHTML += "<td οnclick='tdClick(" + i + "," + j + ")'></td>";
} else if (mapData[i][j] == item.stone) {
smallMapHTML += '<td><img src="'+ items[mapData[i][j]] +'" style="width: 100%; height: 100%; border-radius: 0%;" ></td>';
} else if (mapData[i][j] == item.hero || mapData[i][j] == item.heroHasPath) {
smallMapHTML += '<td><div style="background-color: red; border-radius: 50%;height: 50%; width: 50%;"></div></td>';
}
}
smallMapHTML += "</tr>";
}
// 渲染小地图
smallMap.innerHTML = smallMapHTML;
// 渲染英雄坐标信息
heroPointElement.innerText = heroPoint[1] + " , " + heroPoint[0]
}
4.2、自动寻径
采用ChatGpt生成的JavaScript版本aStart算法,计算出从起点坐标到终点坐标的所有路径数据。其中需要用到曼哈顿距离、从起点到终点其中每步距离终点的代价计算
-
曼哈顿距离(有兴趣可以研究下,为:两点在南北方向上的距离加上在东西方向上的距离)
Math.abs(this.x - target.x) + Math.abs(this.y - target.y) -
总代价(g + h)
g:累计移动代价(从起点到当前节点的实际代价)
h:启发式评估代价(当前节点到目标节点的估算代价)
f:总代价(g + h)
4.2.1、采用ChatGpt生成的JavaScript版本aStart算法,计算出从起点坐标到终点坐标的所有路径数据
class Node {
constructor(x, y) {
this.x = x;
this.y = y;
this.g = 0; // 累计移动代价(从起点到当前节点的实际代价)
this.h = 0; // 启发式评估代价(当前节点到目标节点的估算代价)
this.f = 0; // 总代价(g + h)
this.parent = null; // 用于记录当前节点的父节点,构成路径
}
// 计算当前节点到目标节点的曼哈顿距离(启发式评估函数)
calculateManhattanDistance(target) {
return Math.abs(this.x - target.x) + Math.abs(this.y - target.y);
}
}
function isValidNode(x, y, maze) {
const numRows = maze.length;
const numCols = maze[0].length;
return x >= 0 && x < numRows && y >= 0 && y < numCols && maze[x][y] === 0;
}
function findMinCostNode(openSet) {
let minCostNode = openSet[0];
for (const node of openSet) {
if (node.f < minCostNode.f) {
minCostNode = node;
}
}
return minCostNode;
}
function reconstructPath(currentNode) {
const path = [];
while (currentNode !== null) {
path.unshift([currentNode.x, currentNode.y]);
currentNode = currentNode.parent;
}
return path;
}
function aStar(maze, start, end) {
const numRows = maze.length;
const numCols = maze[0].length;
// 创建起始节点和目标节点
const startNode = new Node(start[0], start[1]);
const endNode = new Node(end[0], end[1]);
const openSet = [startNode]; // 待探索节点集合
const closedSet = []; // 已探索节点集合
while (openSet.length > 0) {
// 从待探索节点集合中选择F值最小的节点
const currentNode = findMinCostNode(openSet);
if (currentNode.x === endNode.x && currentNode.y === endNode.y) {
return reconstructPath(currentNode);
}
// 将当前节点从待探索节点集合移除,并添加到已探索节点集合
openSet.splice(openSet.indexOf(currentNode), 1);
closedSet.push(currentNode);
// 探索当前节点的邻居节点
const neighbors = [
[currentNode.x - 1, currentNode.y],
[currentNode.x + 1, currentNode.y],
[currentNode.x, currentNode.y - 1],
[currentNode.x, currentNode.y + 1],
];
for (const [nx, ny] of neighbors) {
if (isValidNode(nx, ny, maze)) {
const neighborNode = new Node(nx, ny);
if (closedSet.some((node) => node.x === neighborNode.x && node.y === neighborNode.y)) {
continue;
}
const tentativeG = currentNode.g + 1; // 假设移动代价为1(每个格子的代价都为1)
if (!openSet.includes(neighborNode) || tentativeG < neighborNode.g) {
neighborNode.g = tentativeG;
neighborNode.h = neighborNode.calculateManhattanDistance(endNode);
neighborNode.f = neighborNode.g + neighborNode.h;
neighborNode.parent = currentNode;
if (!openSet.includes(neighborNode)) {
openSet.push(neighborNode);
}
}
}
}
}
return null; // 如果无法找到路径,则返回null
}
4.2.2、通过JavaScript定时器每100毫秒去获取路径数组的头部数据(访问即删除),并实时更新地图信息(包括实时英雄坐标点、自动寻径数据)
// 定时任务,每隔100毫秒绘制地图
var timer = setInterval(function () {
if (path.length > 0) {
// 如果只有一个点了,证明已经到目标点了,清除自动寻径的路径
if (path.length == 1) {
mapData[heroPoint[0]][heroPoint[1]] = item.hero;
path = [];
console.log("已抵达目的")
} else {
//清除当前点的英雄
mapData[path[0][0]][path[0][1]] = item.empty;
//把英雄放置到下一个点
mapData[path[1][0]][path[1][1]] = item.heroHasPath;
//队头出栈
path.splice(0, 1);
//设置当前英雄坐标
heroPoint = path[0];
console.log("绘制路径");
}
document.getElementById("autoPath").innerText = JSON.stringify(path)
// 重新绘制地图数据
loadData();
}
}, 100);
总结
以上就是今天要讲的内容,本文仅仅简单介绍了地图y轴的动态绘制、自动寻径、小地图显示,后续还有血量、陷阱、大礼包、随机空投大礼包、武器店+武器系统、护盾+防具系统、自动行走AI、多种障碍物检测等多种玩法。