寻路

*此内容使用人工智能(Beta)翻译,可能包含错误。若要查看英文页面,请点按 此处。

寻路是沿着逻辑路径移动角色或物体(代理)以绕过障碍物到达目的地的过程,选项上可以避免危险材料或定义区域。

导航可视化

为了帮助寻路布局和调试,Studio可以渲染导航网格和修饰符标签。要启用它们,请在3D视口的右上角的可视化选项小部件中切换打开导航 网格和寻路 修饰符。

3D视口的特写视图,右上角指示可视化选项按钮。

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

Studio中显示的导航网格

实现

虽然可以通过PathfindingService及其相关方法如CreatePath()以多种方式实现寻路,但本节使用以下寻路脚本来处理玩家的角色。

在阅读时进行测试:

  1. 重要
    在资源管理器中,选择StarterPlayer容器。然后,在属性窗口中,将DevComputerMovementMode和DevTouchMovementMode都设置为Scriptable。

  2. 将以下代码复制到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.01
    local VELOCITY_MULTIPLIER = 0.0625
    local path = PathfindingService:CreatePath({
    AgentCanClimb = true,
    Costs = {
    Water = 20
    }
    })
    local character = script.Parent
    local humanoid = character:WaitForChild("Humanoid")
    local waypoints
    local nextWaypointIndex
    local blockedConnection
    local currentWaypointReachedConnection
    local currentWaypointPlaneNormal = Vector3.zero
    local currentWaypointPlaneDistance = 0
    local pathfinderWorking = false
    local function disconnectCurrentWaypointReachedConnection()
    if not currentWaypointReachedConnection then return end
    currentWaypointReachedConnection:Disconnect()
    currentWaypointReachedConnection = nil
    end
    local function isCurrentWaypointReached()
    if humanoid.FloorMaterial == Enum.Material.Air then
    return false
    end
    local reached = false
    if 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 < threshold
    else
    reached = true
    end
    if reached then
    currentWaypointPlaneNormal = Vector3.zero
    currentWaypointPlaneDistance = 0
    moveToNextWaypoint()
    end
    end
    local function calculateNextWaypointApproach()
    nextWaypointIndex += 1
    if nextWaypointIndex > #waypoints then
    return false
    end
    local currentWaypoint = waypoints[nextWaypointIndex - 1]
    local nextWaypoint = waypoints[nextWaypointIndex]
    -- 从下一个路标到当前路标构建目标平面
    currentWaypointPlaneNormal = currentWaypoint.Position - nextWaypoint.Position
    -- 当不向上爬时,设置法线垂直于Y平面
    if nextWaypoint.Label ~= "Climb" then
    currentWaypointPlaneNormal = Vector3.new(currentWaypointPlaneNormal.X, 0, currentWaypointPlaneNormal.Z)
    end
    if currentWaypointPlaneNormal.Magnitude > 0.000001 then
    currentWaypointPlaneNormal = currentWaypointPlaneNormal.Unit
    currentWaypointPlaneDistance = currentWaypointPlaneNormal:Dot(nextWaypoint.Position)
    end
    return true
    end
    local function resetWaypointData()
    humanoid:Move(Vector3.zero)
    currentWaypointPlaneNormal = Vector3.zero
    currentWaypointPlaneDistance = 0
    disconnectCurrentWaypointReachedConnection()
    pathfinderWorking = false
    end
    local function waitForGround()
    while humanoid.FloorMaterial == Enum.Material.Air do
    task.wait(GROUND_WAIT)
    end
    end
    function moveToNextWaypoint()
    if calculateNextWaypointApproach() then
    disconnectCurrentWaypointReachedConnection()
    currentWaypointReachedConnection = RunService.Heartbeat:Connect(isCurrentWaypointReached)
    local nextWaypointPosition = waypoints[nextWaypointIndex].Position
    local nextWaypointAction = waypoints[nextWaypointIndex].Action
    humanoid:Move(nextWaypointPosition - humanoid.RootPart.Position)
    if waypoints[nextWaypointIndex + 1] and waypoints[nextWaypointIndex + 1].Label == "UseBoat" then
    nextWaypointIndex += 1
    -- 调用您自己的自定义函数使代理使用船只
    elseif nextWaypointAction == Enum.PathWaypointAction.Jump then
    humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
    while humanoid.FloorMaterial ~= Enum.Material.Air do
    task.wait(GROUND_WAIT)
    end
    humanoid:Move(nextWaypointPosition - humanoid.RootPart.Position)
    end
    else
    resetWaypointData()
    end
    end
    local function findStartingPoint(waypoints)
    nextWaypointIndex = 1
    while nextWaypointIndex + 1 <= #waypoints do
    local dist = waypoints[nextWaypointIndex + 1].Position - humanoid.RootPart.Position
    dist = Vector3.new(dist.X, 0, dist.Z)
    if dist.magnitude >= 2 then
    return
    end
    nextWaypointIndex += 1
    end
    end
    local function followPath()
    -- 计算路径
    pathfinderWorking = true
    waitForGround()
    local success, errorMessage = pcall(function()
    path:ComputeAsync(character.PrimaryPart.Position, DESTINATION)
    end)
    if not success or path.Status ~= Enum.PathStatus.Success then
    warn("路径未计算!", errorMessage)
    return
    end
    -- 获取路径路标
    waypoints = path:GetWaypoints()
    -- 检测路径是否被阻塞
    blockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)
    -- 检查障碍物是否在路径的更远处
    if blockedWaypointIndex >= nextWaypointIndex then
    -- 停止检测路径阻塞,直到路径重新计算
    blockedConnection:Disconnect()
    resetWaypointData()
    -- 调用函数重新计算新路径
    followPath()
    end
    end)
    findStartingPoint(waypoints)
    moveToNextWaypoint()
    end
    followPath()
  3. 编辑DESTINATION变量(

    第5行
    )为玩家角色可以到达的3D世界中的Vector3目标。

  4. 继续阅读以下部分以了解路径计算和角色移动。

路径创建

寻路通过PathfindingService及其CreatePath()方法启动(

第9–14行
)。此方法接受一个可选的参数表,以微调角色(代理)沿路径移动的方式。

键描述类型默认
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来计算它(

第133–139行
)。

路径起点/终点标记在两座桥上

一旦Path被计算,它将包含一系列从起点到终点的路标。这些点可以通过Path:GetWaypoints()方法收集(

第142行
)。返回的数组按路径起点到路径终点的路标顺序排列。

计算路径上指示的路标
计算路径上指示的路标

路径移动

每个PathWaypoint由Position(Vector3)和Action(PathWaypointAction)组成。要移动包含Humanoid的角色,如典型的Roblox角色,最佳方法是从路标到路标调用Humanoid:Move()并使用脚本的isCurrentWaypointReached()回调(

第32–56行
)来检测角色何时到达每个路标。

被阻塞的路径

许多Roblox世界是动态的;部件可能移动或掉落,地板可能坍塌。这可能会阻塞计算路径并阻止角色到达其目的地。为了解决这个问题,您可以连接Path.Blocked事件并重新计算绕过阻塞物的路径(

第145–154行
)。

寻路修饰符

默认情况下,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.01
local VELOCITY_MULTIPLIER = 0.0625
local path = PathfindingService:CreatePath({
AgentCanClimb = true,
Costs = {
Water = 20, CrackedLava = 100, Slate = 20
}
})

配置区域

在某些情况下,材料偏好不足以满足需求。例如,您可能希望角色避免定义的区域,无论脚下的材料如何。这可以通过将PathfindingModifier对象添加到一个部件来实现。

  1. 创建一个围绕该区域的Anchored部件,并将其CanCollide属性设置为false。

    定义应用寻路修饰符的区域的锚定部件。
  2. 在该部件上插入一个PathfindingModifier实例,找到其Label属性,并分配一个有意义的名称,如DangerZone。

    PathfindingModifier实例的Label属性设置为DangerZone。
  3. 在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.01
    local VELOCITY_MULTIPLIER = 0.0625
    local path = PathfindingService:CreatePath({
    AgentCanClimb = true,
    Costs = {
    DangerZone = math.huge, Water = 20, CrackedLava = 20, Slate = 20
    }
    })

忽略障碍物

在某些情况下,穿过实心障碍物进行寻路是有用的,就好像它们不存在一样。这使您能够计算通过特定物理阻挡物的路径,而不是计算直接失败。

  1. 创建一个围绕对象的Anchored部件,并将其CanCollide属性设置为false。

    定义应用寻路修饰符的区域的锚定部件。
  2. 在该部件上插入一个PathfindingModifier实例,并启用其PassThrough属性。

    PathfindingModifier实例的PassThrough属性已启用。

    现在,当从僵尸NPC到玩家角色计算路径时,路径会延伸到门之外,您可以提示僵尸穿过它。即使僵尸无法打开门,它也会表现得好像“听到”门后面的角色。

    僵尸NPC路径穿过之前阻挡的门。

寻路链接

有时需要在无法正常穿越的空间中找到路径,例如跨越深渊,并执行自定义操作以到达下一个路标。这可以通过PathfindingLink对象实现。

使用上面的示例,您可以让代理使用船只。

PathfindingLink显示代理如何使用船只。

要使用此示例创建PathfindingLink:

  1. 可选
    在3D视口的右上角的可视化选项小部件中切换打开寻路 链接。这有助于在实现寻路链接时进行可视化和调试。

  2. 创建两个Attachments,一个在船的座位上,另一个在船的着陆点附近。

    为寻路链接的起点和终点创建的附件。
  3. 在工作区中创建一个PathfindingLink对象,然后将其Attachment0和Attachment1属性分别分配给起始和结束附件。

    PathfindingLink的Attachment0/Attachment1属性。 在3D世界中可视化的PathfindingLink。
  4. 将其Label属性分配一个有意义的名称,如UseBoat。此名称在寻路脚本中用作标志,以在代理到达起始链接点时触发自定义操作。

    PathfindingLink的Label属性。
  5. 在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.01
    local VELOCITY_MULTIPLIER = 0.0625
    local path = PathfindingService:CreatePath({
    AgentCanClimb = true,
    Costs = {
    UseBoat = 2, Water = 20
    }
    })
  6. 在moveToNextWaypoint()函数中(

    第93–114行
    ),可以使用对Label修饰符名称的自定义检查来执行与Humanoid:Move()不同的操作;在这种情况下,您可能会调用一个函数来让代理坐在船上,推动船穿过水,在船的着陆点让代理下船,然后继续代理的路径到达最终目的地。

流媒体兼容性

游戏中的实例流媒体是一个强大的功能,可以在玩家角色在世界中移动时动态加载和卸载3D内容。当他们探索3D空间时,新的子集会流式传输到他们的设备,而一些现有的子集可能会流出。

考虑以下在启用流媒体的游戏中使用PathfindingService的最佳实践:

  • 流媒体可以在角色沿路径移动时阻塞或解除阻塞给定路径。例如,当角色穿过森林时,树木可能会在他们前方的某处流入并阻碍路径。为了使寻路与流媒体无缝工作,强烈建议您使用处理被阻塞路径技术,并在必要时重新计算路径。

  • 寻路中的常见方法是使用现有对象的坐标进行计算,例如将路径目标设置为世界中现有TreasureChest模型的位置。这种方法与服务器端的Scripts完全兼容,因为服务器始终对世界有全面的视图,但在客户端运行的LocalScripts和ModuleScripts如果尝试计算到未流入的对象的路径可能会失败。

    为了解决此问题,请考虑将目标设置为持久模型中BasePart的位置。持久模型在玩家加入后不久加载,并且永远不会流出,因此客户端脚本可以连接到PersistentLoaded事件,并在事件触发后安全地访问模型以创建路标。

限制和失败因素

寻路引擎包括特定的限制,以确保高效处理和最佳性能。此外,寻路计算可能因各种原因而失败,如下所述。

©2026 Roblox Corporation、Roblox、Roblox 标志及 Powering Imagination 是我们在美国及其他国家或地区的注册与未注册商标。