[JavaScript游戏开发] 跟随人物二维动态地图绘制、自动寻径、小地图显示(人物红点显示)

系列文章目录

第一章 2D二维地图绘制、人物移动、障碍检测
第二章 跟随人物二维动态地图绘制、自动寻径、小地图显示(人物红点显示)


文章目录


前言

带大家回顾下第一章的内容。

  • 使用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、地图绘制(地图数据、英雄初始数据、物品数据)

javascript 复制代码
         /**
         * 加载地图数据
         * 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、设置地图最大高度、英雄与上下边框的距离

javascript 复制代码
		 // 设定地图最大的高度
        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)]

javascript 复制代码
	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算法,计算出从起点坐标到终点坐标的所有路径数据

javascript 复制代码
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毫秒去获取路径数组的头部数据(访问即删除),并实时更新地图信息(包括实时英雄坐标点、自动寻径数据)

javascript 复制代码
	// 定时任务,每隔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、多种障碍物检测等多种玩法。

相关推荐
_志哥_1 小时前
深度解析:解决 backdrop-filter 与 border-radius 的圆角漏光问题
前端·javascript·html
qiao若huan喜1 小时前
10、webgl 基本概念 + 坐标系统 + 立方体
前端·javascript·信息可视化·webgl
摸着石头过河的石头2 小时前
Service Worker 深度解析:让你的 Web 应用离线也能飞
前端·javascript·性能优化
不爱吃糖的程序媛3 小时前
Electron 如何判断运行平台是鸿蒙系统(OpenHarmony)
javascript·electron·harmonyos
Hilaku3 小时前
我用AI重构了一段500行的屎山代码,这是我的Prompt和思考过程
前端·javascript·架构
Cxiaomu3 小时前
React Native App 自动检测版本更新完整实现指南
javascript·react native·react.js
掘金安东尼4 小时前
前端周刊第439期(2025年11月3日–11月9日)
前端·javascript·vue.js
起这个名字4 小时前
微前端应用通信使用和原理
前端·javascript·vue.js
鹏多多5 小时前
Web使用natapp进行内网穿透和预览本地页面
前端·javascript
钱端工程师5 小时前
uniapp封装uni.request请求,实现重复接口请求中断上次请求(防抖)
前端·javascript·uni-app