寻路是沿着逻辑路径移动角色或对象(代理)以绕过障碍物到达目的地的过程,可选地避免有害材料或定义区域。
导航可视化
为了帮助路径布局和调试,Studio可以渲染导航网格和 修饰符 标签。要启用它们,请在3D视口的右上角从 可视化选项 小部件中切换 导航 网格 和 寻路 修饰符。

启用 导航网格 后,彩色区域显示角色可能行走或游泳的位置。小箭头指示角色将通过跳跃尝试到达的区域。

实现
虽然寻路可以通过 PathfindingService 和其关联的方法(如 CreatePath())以各种方式实现,但本节使用以下脚本为角色进行寻路。
在阅读时进行测试:
- 重要在 资源管理器 中选择 StarterPlayer 容器。然后在 属性 窗口中,将 DevComputerMovementMode 和 DevTouchMovementMode 两者都设置为 Scriptable。


将以下代码复制到 LocalScript 中,放置在 StarterCharacterScripts 内,或 获取此包 并将其放入 StarterCharacterScripts。
PlayerPathFollow (LocalScript in StarterCharacterScripts)local PathfindingService = game:GetService("PathfindingService")local Players = game:GetService("Players")local RunService = game:GetService("RunService")local DESTINATION = Vector3.new(20, 0.5, 20)local GROUND_WAIT = 0.01local VELOCITY_MULTIPLIER = 0.0625local path = PathfindingService:CreatePath({AgentCanClimb = true,Costs = {Water = 20}})local character = script.Parentlocal humanoid = character:WaitForChild("Humanoid")local waypointslocal nextWaypointIndexlocal blockedConnectionlocal currentWaypointReachedConnectionlocal currentWaypointPlaneNormal = Vector3.zerolocal currentWaypointPlaneDistance = 0local pathfinderWorking = falselocal function disconnectCurrentWaypointReachedConnection()if not currentWaypointReachedConnection then return endcurrentWaypointReachedConnection:Disconnect()currentWaypointReachedConnection = nilendlocal function isCurrentWaypointReached()if humanoid.FloorMaterial == Enum.Material.Air thenreturn falseendlocal reached = falseif currentWaypointPlaneNormal ~= Vector3.zero then-- 计算人形角色到目标平面的距离local dist = currentWaypointPlaneNormal:Dot(humanoid.RootPart.Position) - currentWaypointPlaneDistance-- 计算人形角色速度的组成部分, 该部分指向平面local velocity = -currentWaypointPlaneNormal:Dot(humanoid.RootPart.Velocity)-- 根据人形角色速度计算目标平面上的阈值local threshold = math.max(1.0, VELOCITY_MULTIPLIER * velocity)-- 如果到目标平面的前方距离小于阈值,则认为到达了reached = dist < thresholdelsereached = trueendif reached thencurrentWaypointPlaneNormal = Vector3.zerocurrentWaypointPlaneDistance = 0moveToNextWaypoint()endendlocal function calculateNextWaypointApproach()nextWaypointIndex += 1if nextWaypointIndex > #waypoints thenreturn falseendlocal currentWaypoint = waypoints[nextWaypointIndex - 1]local nextWaypoint = waypoints[nextWaypointIndex]-- 从下一个路点到当前路点构建目标平面currentWaypointPlaneNormal = currentWaypoint.Position - nextWaypoint.Position-- 如果没有向上攀爬,则将法线设置为垂直于Y平面if nextWaypoint.Label ~= "Climb" thencurrentWaypointPlaneNormal = Vector3.new(currentWaypointPlaneNormal.X, 0, currentWaypointPlaneNormal.Z)endif currentWaypointPlaneNormal.Magnitude > 0.000001 thencurrentWaypointPlaneNormal = currentWaypointPlaneNormal.UnitcurrentWaypointPlaneDistance = currentWaypointPlaneNormal:Dot(nextWaypoint.Position)endreturn trueendlocal function resetWaypointData()humanoid:Move(Vector3.zero)currentWaypointPlaneNormal = Vector3.zerocurrentWaypointPlaneDistance = 0disconnectCurrentWaypointReachedConnection()pathfinderWorking = falseendlocal function waitForGround()while humanoid.FloorMaterial == Enum.Material.Air dotask.wait(GROUND_WAIT)endendfunction moveToNextWaypoint()if calculateNextWaypointApproach() thendisconnectCurrentWaypointReachedConnection()currentWaypointReachedConnection = RunService.Heartbeat:Connect(isCurrentWaypointReached)local nextWaypointPosition = waypoints[nextWaypointIndex].Positionlocal nextWaypointAction = waypoints[nextWaypointIndex].Actionhumanoid:Move(nextWaypointPosition - humanoid.RootPart.Position)if waypoints[nextWaypointIndex + 1] and waypoints[nextWaypointIndex + 1].Label == "UseBoat" thennextWaypointIndex += 1-- 调用您自己的自定义函数让代理使用船只elseif nextWaypointAction == Enum.PathWaypointAction.Jump thenhumanoid:ChangeState(Enum.HumanoidStateType.Jumping)while humanoid.FloorMaterial ~= Enum.Material.Air dotask.wait(GROUND_WAIT)endhumanoid:Move(nextWaypointPosition - humanoid.RootPart.Position)endelseresetWaypointData()endendlocal function findStartingPoint(waypoints)nextWaypointIndex = 1while nextWaypointIndex + 1 <= #waypoints dolocal dist = waypoints[nextWaypointIndex + 1].Position - humanoid.RootPart.Positiondist = Vector3.new(dist.X, 0, dist.Z)if dist.magnitude >= 2 thenreturnendnextWaypointIndex += 1endendlocal function followPath()-- 计算路径pathfinderWorking = truewaitForGround()local success, errorMessage = pcall(function()path:ComputeAsync(character.PrimaryPart.Position, DESTINATION)end)if not success or path.Status ~= Enum.PathStatus.Success thenwarn("路径未计算!", errorMessage)returnend-- 获取路径路点waypoints = path:GetWaypoints()-- 检测路径是否被阻塞blockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)-- 检查障碍物是否在路径更远处if blockedWaypointIndex >= nextWaypointIndex then-- 停止检测路径阻塞,直到重新计算路径blockedConnection:Disconnect()resetWaypointData()-- 调用函数重新计算新路径followPath()endend)findStartingPoint(waypoints)moveToNextWaypoint()endfollowPath()继续浏览以下部分以了解路径计算和角色移动。
路径创建
寻路是通过 PathfindingService 和其 CreatePath() 方法发起的 (
| 键 | 描述 | 类型 | 默认 |
|---|---|---|---|
| AgentRadius | 代理半径,单位以Stud为单位。用于确定与障碍物之间的最小间隔。 | 整数 | 2 |
| AgentHeight | 代理高度,单位以Stud为单位。小于此值的空隙(例如楼梯下方的空间)将被标记为不可通行。 | 整数 | 5 |
| AgentCanJump | 确定在寻路过程中是否允许跳跃。 | 布尔值 | true |
| AgentCanClimb | 确定在寻路过程中是否允许攀爬 TrussParts。可攀爬的路径有一个名为 Climb 的 Label 标签,和 费用 默认值为 1。 | 布尔值 | false |
| WaypointSpacing | 路径中间路点之间的间距。如果设置为 math.huge,则将没有中间路点。 | 数字 | 4 |
| Costs | 材料或定义的 PathfindingModifiers 的表格,它们的通行费用。用于使代理更喜欢某些材料/区域而非其他材料。有关详细信息,请参阅 修饰符。 | 表 | nil |
路径计算
在您使用 CreatePath() 创建了有效路径后,必须通过调用 Path:ComputeAsync() 进行 计算,同时为起点和目的地提供 Vector3 (

一旦 Path 被计算,其将包含一系列从开始到结束的 路点。这些点可以通过 Path:GetWaypoints() 方法获取 (

路径移动
每个 PathWaypoint 由一个 Position (Vector3) 和 Action (PathWaypointAction) 组成。要移动包含 Humanoid 的角色(如典型的Roblox角色),最佳方式是调用 Humanoid:Move() 从路点到路点,并使用脚本的 isCurrentWaypointReached() 回调 (
被阻塞的路径
许多Roblox世界是动态的;零件可能会移动或掉落,地面可能会坍塌。这可能会阻塞已计算的路径,禁止角色到达目的地。为此,您可以连接 Path.Blocked 事件并重新计算路径以绕过阻塞的物体 (
寻路修饰符
默认情况下,Path:ComputeAsync() 返回起点和目的地之间的 最短 路径,但它会尝试避免跳跃。这在某些情况下看起来很不自然;例如,路径可能通过沼泽水而不是绕过它,仅仅因为通过水的路径在几何上更短。

为了进一步优化寻路,您可以实现 寻路修饰符 来计算跨越各种 材料、围绕定义 区域 或 忽略障碍 的更智能路径。
材料费用
在处理 Terrain 和 BasePart 材料时,您可以在 CreatePath() 中包含一个 Costs 表,以使某些材料比其他材料更易通行。所有材料的默认费用为 1,任何材料都可以通过将其值设置为 math.huge 来定义为不可通行。
Costs 表中的键应该是 字符串 名称,表示 Enum.Material 名称,例如 Water 表示 Enum.Material.Water 或 CrackedLava 表示 Enum.Material.CrackedLava。
PlayerPathFollow (LocalScript)local PathfindingService = game:GetService("PathfindingService")local Players = game:GetService("Players")local RunService = game:GetService("RunService")local DESTINATION = Vector3.new(20, 0.5, 20)local GROUND_WAIT = 0.01local VELOCITY_MULTIPLIER = 0.0625local path = PathfindingService:CreatePath({AgentCanClimb = true,Costs = {Water = 20, CrackedLava = 100, Slate = 20}})
配置区域
在某些情况下,材料偏好 可能不够。例如,您可能希望角色避免经过定义的 区域,而不考虑脚下的材料。可以通过向零件添加 PathfindingModifier 对象来实现。
创建一个 Anchored 部件,围绕该区域并将其 CanCollide 属性设置为 false。

在该部分上插入 PathfindingModifier 实例,找到其 Label 属性,并分配一个有意义的名称,如 DangerZone。

在 CreatePath() 的 Costs 表中包含匹配的 DangerZone 键和相关的数值。通过将其值设置为 math.huge,可以定义修饰符为不可通行。
PlayerPathFollow (LocalScript)local PathfindingService = game:GetService("PathfindingService")local Players = game:GetService("Players")local RunService = game:GetService("RunService")local DESTINATION = Vector3.new(20, 0.5, 20)local GROUND_WAIT = 0.01local VELOCITY_MULTIPLIER = 0.0625local path = PathfindingService:CreatePath({AgentCanClimb = true,Costs = {DangerZone = math.huge, Water = 20, CrackedLava = 20, Slate = 20}})
忽略障碍
在某些情况下,通过坚固的障碍物寻路是有用的,就像它们不存在一样。这允许您计算通过特定物理阻挡的路径,而不是计算直接失败。
创建一个 Anchored 部件,围绕物体并将其 CanCollide 属性设置为 false。

在该部分上插入 PathfindingModifier 实例,并启用其 PassThrough 属性。

现在,当从僵尸 NPC 到玩家角色计算路径时,路径延伸到了门之上,您可以提示僵尸穿越它。即使僵尸无法打开门,它也会对门后面的角色产生“听见”的反应。

寻路链接
有时需要跨越不能正常通行的空间找到路径,例如跨越深渊并执行自定义操作以到达下一个路点。这可以通过 PathfindingLink 对象实现。
使用上述示例,您可以让代理使用船只。

要使用此示例创建 PathfindingLink:
在船只的座位上和靠近船只着陆点创建两个 Attachments。

将有意义的名称(如 UseBoat)分配给其 Label 属性。此名称在路径寻找脚本中用作标志,以在代理到达起始链接点时触发自定义操作。

在 CreatePath() 中包含一个 Costs 表,包含 Water 键和与 Label 属性名称相匹配的自定义键。为自定义键分配一个 低于 Water 的值。
PlayerPathFollow (LocalScript)local PathfindingService = game:GetService("PathfindingService")local Players = game:GetService("Players")local RunService = game:GetService("RunService")local DESTINATION = Vector3.new(20, 0.5, 20)local GROUND_WAIT = 0.01local VELOCITY_MULTIPLIER = 0.0625local path = PathfindingService:CreatePath({AgentCanClimb = true,Costs = {UseBoat = 2, Water = 20}})在 moveToNextWaypoint() 函数中 (
第93–114行),可以自定义检查 Label 修饰符名称,以进行与 Humanoid:Move() 不同的操作;在这种情况下,您可以调用一个函数将代理坐在船上,使船只横穿水面,然后在船只着陆点解除代理,并继续代理的路径到达其最终目的地。
流式兼容性
游戏中的 实例流式 是一种强大的功能,可随玩家角色移动而动态加载和卸载3D内容。当玩家在3D空间中探索时,新的子集流到他们的设备,某些现有的子集可能会被流出。
在启用流式的游戏中使用 PathfindingService 时,请考虑以下最佳实践:
当角色沿路径移动时,流式可以封锁或解封锁给定的路径。例如,当角色穿越森林时,树木可能在他们前面流入并阻塞路径。为了使寻路与流式无缝配合,强烈建议您使用 处理被阻塞路径 技术并在必要时重新计算路径。
在寻路中常见的一种方法是使用现有对象的坐标进行 计算,例如将路径目的地设置为世界中现有 TreasureChest 模型的位置。这种方法与服务器端的 Scripts 完全兼容,因为服务器始终完全了解世界的情况,但在客户端运行的 LocalScripts 和 ModuleScripts 如果尝试计算到未流入的对象的路径,则可能会失败。
为解决此问题,请考虑将目的地设置为 持久性 模型中 BasePart 的位置。持久性模型在玩家加入后将很快加载,并且永远不会流出,因此客户端脚本可以连接到 PersistentLoaded 事件,在事件触发后安全访问模型以创建路点。
限制和失败因素
寻路引擎包含特定限制以确保高效处理和最佳性能。此外,寻路的 计算 可能因各种原因失败,如下所示。

