使用typescript实现游戏中的JPS跳点寻路算法

JPS是一种优化A*算法的路径规划算法,主要用于网格地图,通过跳过不必要的节点来提高搜索效率。它利用路径的对称性,只扩展特定的“跳点”,从而减少计算量。

deepseek生成的总是无法完整运行,因此决定手写一下。

需要注意的几点:

  1. 跳点检测jump() 方法和 hasForcedNeighbor() 方法是算法核心,需要完整实现强制邻居检查逻辑

  2. 邻居剪枝findNeighbors() 需要根据父节点方向进行方向剪枝,避免不必要的检查

  3. 代价计算:对角线移动使用√2成本,直线移动使用1单位成本

  4. 启发函数:使用适合网格移动的切比雪夫距离

以下是完整的跳点寻路算法,经测试可以运行:

type Grid = number[][];
type Point = { x: number; y: number };
type Direction = { dx: number; dy: number };class Cell {public x: number;public y: number;public parent: Cell | null;public g: number = 0;public h: number = 0;constructor(x: number,y: number,parent: Cell | null = null,g: number = 0,h: number = 0) {this.x = x;this.y = y;this.parent = parent;this.g = g;this.h = h;}get f(): number {return this.g + this.h;}
}class JPS {private openList: Cell[] = [];private closedSet = new Set<string>();private readonly directions: Direction[] = [{ dx: 0, dy: -1 },  // 上{ dx: 1, dy: 0 },   // 右{ dx: 0, dy: 1 },   // 下{ dx: -1, dy: 0 },  // 左{ dx: 1, dy: -1 },  // 右上{ dx: 1, dy: 1 },   // 右下{ dx: -1, dy: 1 }, // 左下{ dx: -1, dy: -1 } // 左上];constructor(private grid: Grid,private start: Point,private end: Point) {}public findPath(): Point[] {this.openList.push(new Cell(this.start.x, this.start.y));while (this.openList.length > 0) {// 按F值排序获取最小节点this.openList.sort((a, b) => a.f - b.f);const currentCell = this.openList.shift()!;if (currentCell.x === this.end.x && currentCell.y === this.end.y) {return this.buildPath(currentCell);}const key = `${currentCell.x},${currentCell.y}`;this.closedSet.add(key);const neighbors = this.findNeighbors(currentCell);for (const neighbor of neighbors) {const jumpPoint = this.jump(neighbor, currentCell);if (jumpPoint) {const existing = this.openList.find(n => n.x === jumpPoint.x && n.y === jumpPoint.y);const g = currentCell.g + this.calculateCost(currentCell, jumpPoint);if (!existing || g < existing.g) {const newCell = new Cell(jumpPoint.x,jumpPoint.y,currentCell,g,this.heuristic(jumpPoint));if (!existing) {this.openList.push(newCell);} else {existing.parent = newCell.parent;existing.g = newCell.g;}}}}}return []; // 无路径}private findNeighbors(node: Cell): Point[] {// 实现邻居查找(考虑父节点方向)// 此处需要根据父节点方向剪枝不必要的方向// 完整实现需要处理不同移动方向的情况return this.directions.map(d => ({ x: node.x + d.dx, y: node.y + d.dy })).filter(p => this.isWalkable(p.x, p.y));}private jump(current: Point, from: Cell): Point | null {// 实现跳点检测的核心逻辑if (!this.isWalkable(current.x, current.y)) return null;if (current.x === this.end.x && current.y === this.end.y) return current;// 检查强制邻居(核心跳点逻辑)if (this.hasForcedNeighbor(current, from)) {return current;}// 直线跳跃和对角线跳跃处理const dx = current.x - from.x;const dy = current.y - from.y;// 对角线移动if (dx !== 0 && dy !== 0) {// 尝试水平/垂直方向跳跃if (this.jump({ x: current.x + dx, y: current.y }, from) ||this.jump({ x: current.x, y: current.y + dy }, from)) {return current;}}// 继续沿方向跳跃return this.jump({x: current.x + dx,y: current.y + dy}, from);}private hasForcedNeighbor(point: Point, parent: Cell): boolean {const dx = point.x - parent.x;const dy = point.y - parent.y;// 直线移动方向检测if (dx === 0 || dy === 0) {return this.checkStraightForcedNeighbors(point, dx, dy);}// 对角线移动方向检测return this.checkDiagonalForcedNeighbors(point, dx, dy);}private checkStraightForcedNeighbors(point: Point, dx: number, dy: number): boolean {// 水平/垂直移动时的强制邻居检查const forcedDirs: Direction[] = [];if (dx !== 0) { // 水平移动const checkDir = dy === 0 ? [1, -1] : [dy]; // 处理可能的误差forcedDirs.push({ dx: 0, dy: 1 },  // 下方强制邻居方向{ dx: 0, dy: -1 }  // 上方强制邻居方向);} else { // 垂直移动forcedDirs.push({ dx: 1, dy: 0 },  // 右侧强制邻居方向{ dx: -1, dy: 0 }  // 左侧强制邻居方向);}// 主移动方向const mainDir = { dx, dy };return forcedDirs.some(dir => {// 检查障碍物+可行走区域模式const obstaclePos = {x: point.x + (mainDir.dx !== 0 ? mainDir.dx : dir.dx),y: point.y + (mainDir.dy !== 0 ? mainDir.dy : dir.dy)};const neighborPos = {x: point.x + dir.dx,y: point.y + dir.dy};// 必须满足:障碍物位置不可行走 + 邻居位置可行走return !this.isWalkable(obstaclePos.x, obstaclePos.y) && this.isWalkable(neighborPos.x, neighborPos.y);});}private checkDiagonalForcedNeighbors(point: Point, dx: number, dy: number): boolean {// 对角线移动时的强制邻居检查const horizontalCheck = { dx, dy: 0 };const verticalCheck = { dx: 0, dy };// 检查水平方向是否有障碍物导致强制邻居const hasHorizontal = !this.isWalkable(point.x, point.y - dy) && this.isWalkable(point.x + dx, point.y - dy);// 检查垂直方向是否有障碍物导致强制邻居const hasVertical = !this.isWalkable(point.x - dx, point.y) && this.isWalkable(point.x - dx, point.y + dy);// 检查自然邻居const naturalNeighbor = this.isWalkable(point.x + dx, point.y) || this.isWalkable(point.x, point.y + dy);return hasHorizontal || hasVertical || naturalNeighbor;}private isWalkable(x: number, y: number): boolean {return x >= 0 && y >= 0 && x < this.grid[0].length && y < this.grid.length &&this.grid[y][x] === 0;}private buildPath(node: Cell): Point[] {const path: Point[] = [];let current: Cell | null = node;while (current) {path.unshift({ x: current.x, y: current.y });current = current.parent;}return path;}private heuristic(point: Point): number {// 使用对角线距离(切比雪夫距离)const dx = Math.abs(point.x - this.end.x);const dy = Math.abs(point.y - this.end.y);return Math.max(dx, dy);}private calculateCost(a: Cell, b: Point): number {// 对角线移动成本为√2,直线为1const dx = Math.abs(a.x - b.x);const dy = Math.abs(a.y - b.y);return dx === dy ? Math.SQRT2 : 1;}
}// 使用示例
const grid: Grid = [[0, 0, 0, 0, 0],[0, 1, 1, 1, 0],[0, 0, 0, 0, 0],[0, 1, 1, 1, 0],[0, 0, 0, 0, 0]
];const jps = new JPS(grid, { x: 0, y: 0 }, { x: 4, y: 4 });
const path = jps.findPath();
console.log("Found path:", path);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/75429.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Jetpack Compose 状态管理指南:从基础到高级实践

在Jetpack Compose中&#xff0c;界面状态管理是构建响应式UI的核心。以下是Compose状态管理的主要概念和实现方式&#xff1a; 基本状态管理 1. 使用 mutableStateOf Composable fun Counter() {var count by remember { mutableStateOf(0) }Button(onClick { count }) {T…

vant4+vue3上传一个pdf文件并实现pdf的预览。使用插件pdf.js

注意下载的插件的版本"pdfjs-dist": "^2.2.228", npm i pdfjs-dist2.2.228 然后封装一个pdf的遮罩。因为pdf文件有多页&#xff0c;所以我用了swiper轮播的形式展示。因为用到移动端&#xff0c;手动滑动页面这样比点下一页下一页的方便多了。 直接贴代码…

Leetcode hot 100(day 4)

翻转二叉树 做法&#xff1a;递归即可&#xff0c;注意判断为空 class Solution { public:TreeNode* invertTree(TreeNode* root) {if(rootnullptr)return nullptr;TreeNode* noderoot->left;root->leftinvertTree(root->right);root->rightinvertTree(node);retu…

C,C++语言缓冲区溢出的产生和预防

缓冲区溢出的定义 缓冲区是内存中用于存储数据的一块连续区域&#xff0c;在 C 和 C 里&#xff0c;常使用数组、指针等方式来操作缓冲区。而缓冲区溢出指的是当程序向缓冲区写入的数据量超出了该缓冲区本身能够容纳的最大数据量时&#xff0c;额外的数据就会覆盖相邻的内存区…

大数据(4)Hive数仓三大核心特性解剖:面向主题性、集成性、非易失性如何重塑企业数据价值?

目录 背景&#xff1a;企业数据治理的困境与破局一、Hive数据仓库核心特性深度解析1. ‌面向主题性&#xff08;Subject-Oriented&#xff09;&#xff1a;从业务视角重构数据‌2. ‌集成性&#xff08;Integrated&#xff09;&#xff1a;打破数据孤岛的统一视图‌3. ‌非易失…

A股复权计算_前复权数据计算_终结章

目录 前置&#xff1a; 计算方法推导 数据&#xff1a; 代码&#xff1a; 视频&#xff1a; 前置&#xff1a; 1 本系列将以 “A股复权计算_” 开头放置在“随想”专栏 2 权息数据结合 “PostgreSQL_” 系列博文中的股票未复权数据&#xff0c;可以自行计算复权日数据 …

Nature:新发现!首次阐明大脑推理神经过程

人类具有快速适应不断变化的环境的认知能力。这种能力的核心是形成高级、抽象表示的能力&#xff0c;这些表示利用世界上的规律来支持泛化。然而&#xff0c;关于这些表征如何在神经元群中编码&#xff0c;它们如何通过学习出现以及它们与行为的关系&#xff0c;人们知之甚少。…

Kotlin 集合函数:map 和 first 的使用场景

Kotlin 提供了丰富的集合操作函数&#xff0c;使开发者可以更加简洁、高效地处理数据。其中&#xff0c;map 和 first 是两个常用的函数&#xff0c;分别用于转换集合和获取集合中的第一个元素。 1. map 的使用场景 场景 1&#xff1a;对象列表转换 在开发中&#xff0c;我们…

EIR管理中IMEI和IMSI信息的作用

在EIR&#xff08;设备身份注册&#xff09;管理中&#xff0c;IMEI&#xff08;国际移动设备身份码&#xff09;和IMSI&#xff08;国际移动用户识别码&#xff09;各自具有重要作用&#xff0c;以下是详细介绍&#xff1a; IMEI的作用 设备身份识别&#xff1a;IMEI是移动设…

MAUI开发第一个app的需求解析:登录+版本更新,用于喂给AI

vscode中MAUI框架已经搭好,用MAUI+c#webapi+orcl数据库开发一个app, 功能是两个界面一个登录界面,登录注册常用功能,另一个主窗体,功能先空着,显示“主要功能窗体”。 这是一个全新的功能,需要重零开始涉及所有数据表 登录后检查是否有新版本程序,自动更新功能。 1.用户…

KUKA机器人查看运行日志的方法

对于KUKA机器人的运行日志都是可以查看和导出的&#xff0c;方便查找问题。KUKA机器人的运行日志查看方法如下&#xff1a; 1、在主菜单下&#xff0c;选择【诊断】-【运行日志】-【显示】下打开&#xff1b; 2、显示出之前的机器人运行日志&#xff1b; 3、也可以通过【过滤器…

Kali Linux 2025.1a:主题焕新与树莓派支持的深度解析

一、年度主题更新与桌面环境升级 Kali Linux 2025.1a作为2025年的首个版本&#xff0c;延续了每年刷新主题的传统。本次更新包含全新的启动菜单、登录界面及桌面壁纸&#xff0c;涵盖Kali标准版和Kali Purple版本。用户可通过安装kali-community-wallpapers包获取社区贡献的额…

【UVM学习笔记】更加灵活的UVM—通信

系列文章目录 【UVM学习笔记】UVM基础—一文告诉你UVM的组成部分 【UVM学习笔记】UVM中的“类” 文章目录 系列文章目录前言一、TLM是什么&#xff1f;二、put操作2.1、建立PORT和EXPORT的连接2.2 IMP组件 三、get操作四、transport端口五、nonblocking端口六、analysis端口七…

uni-app项目上传至gitee方法详细教程

1. 准备工作 1.1 安装 Git 下载并安装 Git&#xff1a;前往 Git 官网&#xff0c;根据操作系统下载安装包。 配置用户名和邮箱&#xff08;需与 Gitee 账号一致&#xff09;&#xff1a; git config --global user.name "你的Gitee用户名" git config --global use…

走向多模态AI之路(三):多模态 AI 的挑战与未来

目录 前言一、多模态 AI 真的成熟了吗&#xff1f;二、多模态 AI 的主要挑战2.1 计算资源消耗&#xff1a;模型复杂度带来的成本问题2.2 数据标注困难&#xff1a;跨模态数据集的挑战2.3 对齐和融合的难点2.4 泛化能力与鲁棒性2.5 伦理与隐私问题 三、研究方向与未来发展3.1 轻…

STM32单片机入门学习——第12节: [5-2]对射式红外传感器计次旋转编码器计次

写这个文章是用来学习的,记录一下我的学习过程。希望我能一直坚持下去,我只是一个小白,只是想好好学习,我知道这会很难&#xff0c;但我还是想去做&#xff01; 本文写于&#xff1a;2025.04.03 STM32开发板学习——第12节: [5-2]对射式红外传感器计次&旋转编码器计次 前言…

汇编学习之《jcc指令》

JCC&#xff08;Jump on Condition Code&#xff09;指的是条件跳转指令&#xff0c;c中的就是if-else, while, for 等分支循环条件判断的逻辑。它包括很多指令集&#xff0c;各自都不太一样&#xff0c;接下来我尽量将每一个指令的c 源码和汇编代码结合起来看&#xff0c;加深…

深度解析算法之滑动窗口

12滑动窗口—将 x 减到 0 的最小操作数 题目传送门 题目描述&#xff1a; 给你一个整数数组 nums 和一个整数 x 。每一次操作时&#xff0c;你应当移除数组 nums 最左边或最右边的元素&#xff0c;然后从 x 中减去该元素的值。请注意&#xff0c;需要 修改 数组以供接下来的操…

[MySQL初阶]MySQL表的操作

MySQL表的操作 1. 创建表2. 查看表结构3. 修改表&#xff08;修改表的属性而非表的数据&#xff09;4. 删除表 1. 创建表 语法&#xff1a; CREATE TABLE table_name (field1 datatype,field2 datatype,field3 datatype ) character set 字符集 collate 校验规则 engine 存储…

sqlalchemy详细介绍以及使用方法

SQLAlchemy是一个Python的ORM&#xff08;对象关系映射&#xff09;工具&#xff0c;它允许开发者使用Python代码来操作数据库而不必直接编写SQL语句。SQLAlchemy提供了一种抽象层&#xff0c;使开发者可以通过简单的Python对象来表示数据库表和记录&#xff0c;从而实现对数据…