恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

three.js A*寻路实战:从网格降维到角色自动移动

  • 首页
  • 资讯中心
  • /
  • three.js A*寻路实战:从网格降维到角色自动移动

相关资讯

three.js算法寻路示例:从网格化到A*路径回填全流程 2026/10/22 9:10:33
Option-Critic:让时间抽象从回报中自动涌现的层次强化学习架构 2026/10/22 9:10:33
AI一键生成图表:从原理到实操,避开数据准备与描述陷阱 2026/10/22 9:05:33

最新资讯

微信小程序反编译原理与实战:从wxapkg解密到代码还原
空间与通道双注意力机制复现:从设计到调参的工程实践
K8S Service与Deployment深度解析:从线上故障到生产实践
3D高斯泼溅从照片到实时可交互场景重建,一文讲清原理与实操
WSUS同步失败排查指南:从网络代理到TLS配置的深度解析
结构体内存对齐详解:从sizeof到#pragma pack实战

今日推荐

电商智能运营体系从0到1:架构设计、落地步骤与避坑指南
压缩空气储能与热网热惯性联合优化调度:热电联产系统建模与求解
若依框架AI代码生成不稳定?用Agent Skill三层结构+五步法搞定

本周热门

基于GMM的语音识别系统原理与MATLAB实现详解
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

three.js A*寻路实战:从网格降维到角色自动移动

发布时间:2026/10/22 9:10:33
three.js A*寻路实战:从网格降维到角色自动移动 简介一套面向WebGL与前端开发者的three.js算法寻路综合示例结合Vue框架搭建可交互的3D场景演示A算法与DFS/BFS遍历在路径规划中的表现适用于游戏开发、VR/AR项目中的寻路技术验证与学习。压缩包共71个文件其中22个PNG、16个SVG、11个JPG主要承担纹理素材与界面图标8个JS文件与5个Vue组件负责三维逻辑和交互界面另有说明文档、样式文件等整体大小约3.56MB结构清晰方便直接运行或二次修改。目前已有778人学习下载。通过该示例可以掌握three.js构建含障碍物的三维场景、Vue管理界面状态并让用户自由切换算法理解A算法如何结合启发函数在3D空间中寻找最优路径亦可对照DFS/BFS输出区别不同搜索策略的耗时与路径质量适合希望快速上手WebGL寻路实战并兼顾前端工程化细节的开发者。1. three.js 算法寻路示例先让角色找到路再谈走得好不好刚写完角色移动控制的那个晚上我把速度拉满想试手感结果 NPC 一头撞在墙上推都推不开。前后找了一小时最后发现是转向逻辑里少判断了一步。这是做 three.js 算法寻路示例时的常态模型会跑不会找路。真正能落地的方案不是去调大量转向逻辑而是把寻路从 3D 空间降维成 2D 问题用规则网格表示地图可走性用 A* 求出路径点再让角色沿着路径点移动。接下来按这条路线我给出一份可直接照做的 three.js 实现覆盖选型、代码和避坑适合刚把 three.js 基础跑通、想让 NPC 或玩家角色自动找路的开发者。2. 为什么先用 2D 网格寻路three.js 里的算法选型与降维思路2.1 三种地图表示对比网格搜索、导航网格与路点图在开始敲代码前先明确一件事three.js 本身不提供寻路能力它只负责渲染。寻路要在它外面算好路径再把路径数据喂给场景里的对象。最常见的三种做法是规则网格搜索、导航网格和路点图。网格搜索是把地面切成等大的格子每个格子标记可走或不可走然后跑图搜索算法导航网格是用凸多边形把可走区域包裹起来在这些多边形上做图搜索路点图则是手工在场景里放若干个路标点连成图再搜索。三种方式的选择直接决定后面开发量参考对比如下对比维度网格搜索导航网格路点图three.js 落地难度最低网格可直接对应数组中等需要生成和烘焙最低路径质量折线需要平滑贴合地形平滑度高取决于路点密度动态障碍处理需要更新网格数据需要更新导航网格难以动态处理适用场景中小型固定地图大型复杂地形简单原型演示如果你的项目是中小场景、障碍物数量可控我建议先用网格搜索把主流程跑通。这是多数 three.js 寻路项目的起步方案也是本文后面代码采用的方式。导航网格质量更高但它引入的生成工具链、离线烘焙和动态维护复杂度对初版来说负担不小。把网格搜索做好以后换导航网格也容易因为 A* 搜索逻辑可以复用。网上讨论时很多人把「导航网格」和「寻路算法」混为一谈其实它们是两件事导航网格是地图的数据结构A* 是跑在这个图上的搜索算法。理解这一点再看各种资料会顺很多。2.2 降维到 XZ 平面对 three.js 意味着什么three.js 世界坐标里 Y 轴朝上地面上的普通寻路只关心水平方向的移动。把三维坐标直接放进 A* 是很吃亏的搜索空间从二维多出一层 Y 方向维度路径会绕到空中而且计算量成倍增加。所以常规做法是寻路计算限制在 XZ 平面算完后把网格坐标恢复成 Y0或通过高度采样得到真实 Y 值的世界坐标。这里有个容易被忽视的细节场景地形如果是起伏的不能直接把 Y 设成 0否则角色会走进地面。我一般会在路径生成后做一次高度采样// 从地形几何中采样 Y 值 function getHeightAt(x, z) { const raycaster new THREE.Raycaster(); raycaster.set(new THREE.Vector3(x, 100, z), new THREE.Vector3(0, -1, 0)); const hit raycaster.intersectObject(terrainMesh); return hit.length ? hit[0].point.y : 0; }这段代码把射线从高空垂直向下打取第一个交点作为地面高度。这样网格寻路只管 XZ最后再把 Y 填上既保留算法效率又适配了起伏地形。多层建筑的做法是拆成多个水平层每层一张独立的 XZ 网格层间用楼梯或电梯点衔接。新手最常见的翻车是把楼板和楼梯全部丢进一个三维网格里搜出来的路径又慢又怪我见过某个项目把三维网格边长调到 0.1寻路一次要半秒完全无法实时使用。2.3 定参数再写代码网格边长、邻域、启发式与移动代价这四个参数决定寻路效率和路径质量建议在写代码前就定好。网格边长单位是 three.js 场景坐标。常见初始值是 0.5 或 1。边长越小路径越精细但格子数量按平方增长A* 的开销也随之变大。0.25 以下的边长适合几十米范围的小场景超过 100 米的地图就建议缓存或换导航网格。做示例时 0.5 是平衡点障碍物边界误差在可接受范围内10×10 的场景只有 400 个格子A* 跑一次几乎无感。邻域4 邻域只允许上下左右走路径简单无斜穿但看起来生硬8 邻域允许斜向路径更自然代价是允许对角穿墙后面避坑章节会补判断。真实项目里超过 90% 会选 8 邻域因为角色斜着走更流畅。启发函数4 邻域选曼哈顿距离8 邻域选切比雪夫或欧几里得距离。启发必须小于等于真实代价否则寻路会退化成贪心路径明显变差。移动代价正交移动为 1斜向为Math.SQRT2约 1.414。不区分会让路径走向发飘搜索顺序也不稳定。完成这些参数设置后就可以进入代码了。常见做法是先写一个纯 JavaScript 的 A* 模块它完全不知道 three.js 存在再写坐标转换层把网格路径换算成世界坐标。两者分开后算法单元测试好写网格逻辑出错能立刻定位。我一般把 A* 放在astar.js把 three.js 初始化和动画放在主文件这样数据与渲染解耦后面换导航网格也不用重写核心算法。3. 搭建 three.js 寻路场景从空场景到可走性网格数据3.1 初始化场景和轨道控制器调试视角怎么搭先写一个最基础的三维场景固定好一个便于观察的初始视角。import * as THREE from three; import { OrbitControls } from three/addons/controls/OrbitControls.js; let scene, camera, renderer, controls; function initScene() { scene new THREE.Scene(); scene.background new THREE.Color(0x222222); camera new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 200); camera.position.set(14, 10, 14); camera.lookAt(0, 0, 0); renderer new THREE.WebGLRenderer({ antialias: true }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); controls new OrbitControls(camera, renderer.domElement); controls.target.set(0, 0, 0); const ambient new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambient); const dir new THREE.DirectionalLight(0xffffff, 0.8); dir.position.set(5, 10, 7); scene.add(dir); // 网格辅助线方便核对格子的实际边界 const gridHelper new THREE.GridHelper(20, 40, 0xcccccc, 0x555555); scene.add(gridHelper); renderer.setAnimationLoop(animate); } function animate() { controls.update(); renderer.render(scene, camera); } initScene();逻辑说明固定初始视角加轨道控制器的目的是为了在调试时随时旋转观察路径点与障碍物的贴合程度。GridHelper把地面按每格 0.5 单位画出网格线正好对应我们后面设定的网格边长 0.5能直观看出「格子」与「障碍物」的边界关系。灯光用环境光加方向光组合避免场景过暗影响判断。setAnimationLoop比requestAnimationFrame更适合 WebXR 和需要帧率控制的场景。参数说明camera.position.set(14, 10, 14)让相机位于场景对角线上方透视效果能同时看到 X 和 Z 两个方向的延伸。GridHelper(20, 40)表示总长 20每边 40 格。如果后面把网格边长改成 1辅助线的分割数也要改成 20否则看不出格子边界调试会非常吃力。3.2 生成障碍物与包围盒把场景对象变成寻路数据寻路逻辑其实不关心渲染它只关心「哪里能走」。障碍物的 Box3 包围盒就是最直接的拦路数据。// 记录所有动态障碍物 const obstacles []; function createBoxObstacle(x, z, w, h, d) { const geo new THREE.BoxGeometry(w, h, d); const mat new THREE.MeshStandardMaterial({ color: 0xcc5522, roughness: 0.7 }); const mesh new THREE.Mesh(geo, mat); mesh.position.set(x, h / 2, z); // 底部贴地 scene.add(mesh); // 统一保存包围盒后续网格标记直接用 mesh.userData.bounds new THREE.Box3().setFromObject(mesh); obstacles.push(mesh); // 调试画出包围盒确认阻挡范围 const boxHelper new THREE.BoxHelper(mesh, 0xffaa00); scene.add(boxHelper); return mesh; } // 示例场景三堵墙和一个方块 createBoxObstacle(-2, -1, 4, 1.5, 0.6); createBoxObstacle(1, 2, 0.6, 1.5, 3); createBoxObstacle(4, 0, 2, 1, 2);逻辑说明BoxHelper会绘制障碍物的包围盒线框。A* 寻路不需要场景对象只需要每个障碍物在 XZ 平面的投影范围。我把包围盒存进userData.bounds生成网格时就不用遍历场景里所有 Object3D只遍历这个obstacles数组即可。参数说明h / 2是因为BoxGeometry的中心在几何体中心要让底部贴地必须把 Y 上移半高。障碍物数量少时逐个创建完全够用如果场景里有几百个障碍建议直接用一个数组保存包围盒数据跳过 Mesh 创建因为寻路不关心渲染层这样能省掉大量 GPU 内存占用。3.3 生成二维网格把障碍物投影到可走性数组现在把场景地图拆成一个二维数组。数组的行对应 Z 方向列对应 X 方向值 1 表示障碍。const GRID_SIZE 0.5; // 每格边长 const MAP_WIDTH 10; // X 方向范围 const MAP_DEPTH 10; // Z 方向范围 const GRID_COLS Math.ceil(MAP_WIDTH / GRID_SIZE); const GRID_ROWS Math.ceil(MAP_DEPTH / GRID_SIZE); // walkableGrid[row][col]1 表示障碍物0 表示可走 const walkableGrid Array.from({ length: GRID_ROWS }, () Array.from({ length: GRID_COLS }, () 0) ); function buildWalkableGrid() { for (let r 0; r GRID_ROWS; r) { for (let c 0; c GRID_COLS; c) { walkableGrid[r][c] 0; // 重置 const x c * GRID_SIZE GRID_SIZE / 2 - MAP_WIDTH / 2; const z r * GRID_SIZE GRID_SIZE / 2 - MAP_DEPTH / 2; const testPoint new THREE.Vector3(x, 0, z); for (const obstacle of obstacles) { const b obstacle.userData.bounds; // 包围盒投影到 XZ 平面只看水平范围 if (x b.min.x x b.max.x z b.min.z z b.max.z) { walkableGrid[r][c] 1; break; } } } } } buildWalkableGrid();逻辑说明这里取格子中心点判断它是否落在障碍物包围盒的水平投影范围内。行号对应 Z、列号对应 X是因为setFromObject返回的 Box3 是世界坐标。后续我们把网格坐标转回世界坐标时必须按同一套约定否则路径会横向反转或前后来回错乱。参数说明GRID_SIZE是寻路的空间分辨率它必须和障碍物最小厚度匹配。如果墙厚 0.6 而网格边长是 0.5格子中心可能落在墙外 0.3 的位置导致角色走到墙和障碍之间的夹缝里。安全做法是让GRID_SIZE小于等于障碍物最小厚度的一半或者判断时把包围盒向外扩半格。我在避坑章节会详细展开这里先知道有这个隐患就好。4. 在 three.js 里实现 A* 并驱动角色移动4.1 纯 JavaScript A* 实现开集合、闭集合与启发式A* 模块不依赖 three.js这样纯算法逻辑可以单独测试。代码里我用开集合存待考察节点用cameFrom记录前驱用gScore和fScore做代价评估。// astar.js —— 纯算法模块不依赖 three.js function heuristic(a, b) { // 八邻域使用切比雪夫距离避免高估真实代价 return Math.max(Math.abs(a.r - b.r), Math.abs(a.c - b.c)); } function aStar(grid, start, end) { const rows grid.length; const cols grid[0].length; const key (r, c) r * cols c; const openSet []; const cameFrom new Map(); const gScore new Map(); const fScore new Map(); gScore.set(key(start.r, start.c), 0); fScore.set(key(start.r, start.c), heuristic(start, end)); openSet.push(start); while (openSet.length 0) { // 每次取 fScore 最小的节点线性扫描即可 let bestIdx 0; for (let i 1; i openSet.length; i) { if (fScore.get(key(openSet[i].r, openSet[i].c)) fScore.get(key(openSet[bestIdx].r, openSet[bestIdx].c))) { bestIdx i; } } const current openSet[bestIdx]; openSet.splice(bestIdx, 1); if (current.r end.r current.c end.c) { return reconstructPath(cameFrom, current, cols); } // 八方向邻居 const dirs [ [1, 0], [-1, 0], [0, 1], [0, -1], [1, 1], [1, -1], [-1, 1], [-1, -1], ]; for (const [dr, dc] of dirs) { const nr current.r dr; const nc current.c dc; if (nr 0 || nr rows || nc 0 || nc cols) continue; if (grid[nr][nc] 1) continue; // 障碍不可走 // 八邻域穿墙检查斜向移动时拐角两侧都必须可走 if (dr ! 0 dc ! 0) { if (grid[current.r][nc] 1 || grid[nr][current.c] 1) continue; } const moveCost (dr ! 0 dc ! 0) ? Math.SQRT2 : 1; const tentativeG (gScore.get(key(current.r, current.c)) ?? Infinity) moveCost; const nk key(nr, nc); if (!gScore.has(nk) || tentativeG gScore.get(nk)) { cameFrom.set(nk, current); gScore.set(nk, tentativeG); fScore.set(nk, tentativeG heuristic({ r: nr, c: nc }, end)); if (!openSet.some(n n.r nr n.c nc)) { openSet.push({ r: nr, c: nc }); } } } } return null; // 没有可达路径 } function reconstructPath(cameFrom, current, cols) { const path [current]; let cur current; while (cameFrom.has(cur.r * cols cur.c)) { cur cameFrom.get(cur.r * cols cur.c); path.push(cur); } return path.reverse(); }逻辑说明openSet每次取 fScore 最小的节点节点少时线性扫描完全够用。cameFrom用 Map 存前驱键是行 * 列数 列避免把整个对象当键造成内存浪费。八邻域穿墙检查是这段代码里最关键的细节从(r, c)斜向(r1, c1)时必须确认(r, c1)和(r1, c)都不是墙否则路径会贴着墙角斜切运行时视觉上非常怪。参数说明Math.SQRT2是内置常量保证斜向移动代价为 1.414启发函数用切比雪夫距离在八邻域下不会高估真实代价搜索方向也会更偏向目标。?? Infinity用于gScore尚未赋值的节点未探索节点代价视为无穷大。如果节点数量超过 5000建议把openSet改成二叉堆避免每次线性扫全集合否则单次搜索可能突破几十毫秒。4.2 网格坐标与世界坐标互转偏置必须成对出现A* 算出来的结果是网格行列号喂给 three.js 角色前必须转成世界坐标。转换函数要保证偏移逻辑一致。function worldToGrid(x, z) { const col Math.floor((x MAP_WIDTH / 2) / GRID_SIZE); const row Math.floor((z MAP_DEPTH / 2) / GRID_SIZE); // 防止越界索引直接夹回有效范围 return { r: Math.min(GRID_ROWS - 1, Math.max(0, row)), c: Math.min(GRID_COLS - 1, Math.max(0, col)), }; } function gridToWorld(row, col) { const x col * GRID_SIZE GRID_SIZE / 2 - MAP_WIDTH / 2; const z row * GRID_SIZE GRID_SIZE / 2 - MAP_DEPTH / 2; // 高度默认填 0起伏地形请用 getHeightAt 采样真实 Y return new THREE.Vector3(x, 0, z); } const start worldToGrid(-2, -3); const end worldToGrid(3, 2); const gridPath aStar(walkableGrid, start, end); const worldPath (gridPath || []).map(p gridToWorld(p.r, p.c));逻辑说明worldToGrid把世界坐标平移到地图中心后除以格子边长并向下取整。格子中心是col * GRID_SIZE GRID_SIZE / 2起点在-MAP_WIDTH / 2这个偏置必须同时出现在正向和反向两个函数里否则路径会整体偏移半格看起来就像角色总是贴着障碍物边缘走。参数说明worldToGrid里的Math.max(0, row)是防止角色点到地图外时出现负索引。实际项目中如果不做夹取A* 会直接越界然后崩溃这个问题非常隐蔽。gridToWorld的 Y 值默认设为 0如果你的地图有起伏这里要调用 2.2 节的getHeightAt(x, z)去采样真实高度。4.3 创建角色并沿路径移动速度与到达判定有了世界坐标路径还需要一个占位角色和逐帧移动逻辑。// 创建一个占位角色用圆柱体表示 const player new THREE.Mesh( new THREE.CylinderGeometry(0.3, 0.3, 0.8, 8), new THREE.MeshStandardMaterial({ color: 0x33ccff }) ); player.position.set(-2, 0.4, -3); scene.add(player); let pathIndex 0; const moveSpeed 3; // 场景单位 / 秒 function updatePlayer(playerMesh, pathWorld, delta) { if (pathIndex pathWorld.length) return; const target pathWorld[pathIndex]; const toTarget new THREE.Vector3().subVectors(target, playerMesh.position); const distance toTarget.length(); if (distance 0.05) { // 达到当前目标点切换到下一个 pathIndex; } else { const step Math.min(moveSpeed * delta, distance); toTarget.normalize().multiplyScalar(step); playerMesh.position.add(toTarget); playerMesh.lookAt(target); } } const clock new THREE.Clock(); function animate() { const delta clock.getDelta(); updatePlayer(player, worldPath, delta); controls.update(); renderer.render(scene, camera); }逻辑说明updatePlayer向量化的方式是new THREE.Vector3().subVectors(target, position)得到从角色指向目标点的向量。用Math.min(moveSpeed * delta, distance)限制单帧移动量可以避免角色在高速移动时直接越过路径点来回摆动。lookAt让角色的正前方朝向目标。参数说明0.05是到达判定半径。路径点密集时这个值要调小否则角色会在拐弯前提前转向速度越快判定值要适当调大否则角色会错过目标点。clock.getDelta()在setAnimationLoop回调里获取是 WebXR 和普通动画循环都适用的一帧间隔。速度3意味着每秒移动 3 个场景单位在实际项目里一般会换算成玩家配置属性方便调参。5. 避坑指南three.js 寻路的 5 个常见问题与排查5.1 网格边长大于障碍物宽度导致穿模现象寻路结果明明绕开了障碍但角色移动时还是卡在墙边或穿到墙里。原因GRID_SIZE比障碍物厚度还大格子的中心点可能落在障碍物之外但格子的面积同时覆盖了障碍物边缘导致路径贴着墙走。解决把障碍物包围盒向外扩展半个格再判断。最简单的方式是在buildWalkableGrid里让b.min.x - GRID_SIZE / 2b.max.x GRID_SIZE / 2Z 方向同理。更严谨的做法是把角色半径也加进扩展量确保角色模型不会和障碍物产生视觉交叉。5.2 八邻域未做穿墙检查路径斜切墙角现象路径会从一个墙角斜着切过去运行时角色明显被碰撞挡住但寻路结果却一直选这条路。原因八邻域允许对角移动却没有检查拐角两侧的三个格子尤其是对角相邻的两个格子。斜向移动时路径从墙角的顶点穿过去了。解决在扩展邻居时增加穿墙检测也就是 4.1 节代码里的if (grid[current.r][nc] 1 || grid[nr][current.c] 1) continue。缺少这一步任何八邻域寻路都会出现「抄近路」问题。这类问题很多人第一天发现不了等角色一多就开始穿模属于最容易翻车的隐藏坑。5.3 路径点过密角色高频转向像「抽风」现象角色移动时每个格点都转一次方向视觉上像小步乱戳转弯不自然。原因A* 在网格上输出的路径包含大量相邻格点每个格点都做了一次转向。格点间距小于角色步长时转向频率就非常高。解决对路径做一次「视线剔除」。从当前点出发尝试直接连线到更远的路径点如果直线经过的格子全部可走就跳过中间的格点。简化后路径点数通常能减少 60% 以上再配合下一章的曲线平滑移动观感会干净很多。判断直线是否可走可以采样直线上多个点逐一检查所在格子的可走性。5.4 动态障碍刷新走全量帧率突然暴跌现象某个障碍物被移动后整个寻路系统卡顿 1 到 2 秒。原因buildWalkableGrid每次全量重建O(行 × 列) 加上每个格子遍历障碍物几千个格子的场景重跑一次就掉帧了。解决不要每次移动都全量重建改为局部更新。静态部分提前烘焙进网格动态障碍只在自身包围盒覆盖的格子范围内改写。如果场景实在很小、格子不超过几百个全量重建也能接受但GRID_SIZE不要低于 0.25否则格子数量会突增到上万个任何手机浏览器都会卡。这个坑在 PC 上可能不明显移动端一测就露馅。5.5 把 Y 轴混进搜索维度的「飞天」绕路现象两点在同一水平面路径却先高后低甚至从半空中绕过去角色移动时像在飞。原因寻路网格被做成了三维数组把 Y 也放进搜索空间。three.js 的 Y 轴是高度方向直接搜三维网格会把现实中不可达的区域也纳入候选。解决约定所有寻路都发生在 XZ 平面。分楼层场景就拆多层每层做一次二维 A*层与层之间用楼梯或电梯点衔接。如果必须支持坡道建议改用导航网格不要硬磕三维网格 A*。这个问题我在 2.2 小节已经提过但实际项目里还是会反复出现归根结底是因为很多人拿到动态角色高度就顺手塞进了寻路入参。6. 进阶技巧路径平滑与角色朝向让寻路结果不呆板6.1 用 CatmullRomCurve3 把折线路径变成平滑曲线路径简化后还是折线段直接让角色沿折线移动会出现硬转弯。常见做法是用 three.js 内置的CatmullRomCurve3做曲线插值const curve new THREE.CatmullRomCurve3(waypoints); const pointsOnCurve curve.getPoints(50); // 采样 50 个点 const lineGeo new THREE.BufferGeometry().setFromPoints(pointsOnCurve); const lineMat new THREE.LineBasicMaterial({ color: 0x00ff88 }); scene.add(new THREE.Line(lineGeo, lineMat));waypoints是 4.2 节得到的worldPath数组。CatmullRomCurve3默认穿过每个传入点同时把拐角拉圆角色沿着pointsOnCurve走就不会出现直角急转。注意曲线可能会把路径拉向障碍物一侧所以简化后的 waypoints 周围要保留至少一个格子的余量或者把转弯点稍微向可走区域中心偏移。6.2 用四元数插值替代 lookAt转身更自然当角色移动速度较快时直接lookAt(target)每帧转向很生硬。用四元数插值可以让转身带有惯性感const targetQuat new THREE.Quaternion().setFromUnitVectors( new THREE.Vector3(0, 0, 1), dir.clone().normalize() ); player.quaternion.slerp(targetQuat, 0.1);参数说明slerp系数 0.1 表示每帧朝目标方向转 10%系数越小越慢0.15 到 0.2 的手感更跟手。注意dir必须是在 XZ 平面归一化后的方向向量否则角色会低头或仰头。模型面部朝向如果默认是 X 轴正方向需要先给模型套一个父级 Object3D 做轴向修正这是 three.js 角色动画里最常见的玄学问题我也踩过。6.3 验证寻路结果的三个检查项调试寻路时我会常看三个值路径总长度、转向次数、最小障碍物距离。路径总长度能反映邻域设置是否合理转向次数多说明平滑不足最小障碍物距离用于检查是否有贴边现象。把这三个值打印出来对比调整GRID_SIZE和邻域比肉眼盯着屏幕猜问题可靠得多。经历过一次「邻域改成八邻域后路径长度降了 30%但出现十几处贴边斜切」的事故后我把穿墙检测、路径简化和 slerp 朝向插值固定成了寻路工具的标配。回看这个 three.js 算法寻路示例初版不要急着追求花哨的动态避让先把「网格 → A* → 坐标互转 → 路径移动」这条链路跑通再逐步加复杂度。这个方向值不值得投入取决于你的项目里有多少个需要自动寻路的角色角色一多网格那套很快会遇到性能瓶颈到时候自然会把视线转向导航网格。你在网格方案里踩过的那些坑换导航网格时还会再遇到一遍。我后来养成了每个寻路改动先打印路径统计的习惯省掉了大量深夜调试时间。希望帮到你。本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号