경로 탐색

*이 콘텐츠는 AI(베타)를 사용해 번역되었으며, 오류가 있을 수 있습니다. 이 페이지를 영어로 보려면 여기를 클릭하세요.

경로 탐색은 캐릭터나 객체(요원)가 장애물을 피하며 목적지에 도달하기 위해 논리적 경로를 따라 이동하는 과정입니다. 이 과정에서 위험한 물질이나 정의된 구역을 피할 수도 있습니다.

탐색 시각화

경로 탐색 레이아웃 및 디버깅을 도와주기 위해 스튜디오는 탐색 메쉬 및 수정기 레이블을 렌더링할 수 있습니다. 이를 활성화하려면 3D 뷰포트 오른쪽 상단의 시각화 옵션 위젯에서 탐색 메쉬경로 탐색 수정기를 켜세요.

시각화 옵션 버튼이 오른쪽 상단에 표시된 3D 뷰포트의 클로즈업 이미지.

탐색 메쉬가 활성화되면 색이 있는 영역은 캐릭터가 걷거나 수영할 수 있는 곳을 보여줍니다. 작은 화살표는 캐릭터가 점프하여 도달하려고 시도하는 영역을 나타냅니다.

Studio에서 표시된 탐색 메쉬

구현

경로 탐색은 PathfindingService와 그에 관련된 메소드들, 예를 들어 CreatePath()를 통해 여러 가지 방법으로 구현할 수 있지만, 이 섹션에서는 플레이어 캐릭터를 위한 다음 경로 탐색 스크립트를 사용합니다.

읽으면서 테스트하기 위해:

  1. 중요
    탐색기에서 StarterPlayer 컨테이너를 선택합니다. 그런 다음 속성 창에서 DevComputerMovementModeDevTouchMovementMode를 모두 Scriptable로 설정합니다.

  2. 다음 코드를 LocalScript에 복사하여 StarterCharacterScripts 안에 붙여넣거나 이 패키지를 가져와 StarterCharacterScripts에 드롭하세요.

    PlayerPathFollow (StarterCharacterScripts의 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
    }
    })
    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
    -- Compute the distance from humanoid to destination plane
    local dist = currentWaypointPlaneNormal:Dot(humanoid.RootPart.Position) - currentWaypointPlaneDistance
    -- Compute the component of the humanoid velocity that is towards the plane
    local velocity = -currentWaypointPlaneNormal:Dot(humanoid.RootPart.Velocity)
    -- Compute the threshold from the destination plane based on humanoid velocity
    local threshold = math.max(1.0, VELOCITY_MULTIPLIER * velocity)
    -- Consider waypoint reached if less then threshold in front of the plane
    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]
    -- Build destination plane from next waypoint towards current one
    currentWaypointPlaneNormal = currentWaypoint.Position - nextWaypoint.Position
    -- Set normal perpendicular to Y plane when not climbing up
    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
    -- Call your own customized function to make agent use the boat
    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()
    -- Compute the path
    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
    -- Get the path waypoints
    waypoints = path:GetWaypoints()
    -- Detect if path becomes blocked
    blockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)
    -- Check if the obstacle is further down the path
    if blockedWaypointIndex >= nextWaypointIndex then
    -- Stop detecting path blockage until path is re-computed
    blockedConnection:Disconnect()
    resetWaypointData()
    -- Call function to re-compute new path
    followPath()
    end
    end)
    findStartingPoint(waypoints)
    moveToNextWaypoint()
    end
    followPath()
  3. DESTINATION 변수를 (

    5행
    ) 플레이어 캐릭터가 도달할 수 있는 3D 세계 내의 Vector3 목적지로 수정하세요.

  4. 경로 계산 및 캐릭터 이동에 대한 다음 섹션을 진행하세요.

경로 생성

경로 탐색은 PathfindingService와 그 메소드인 CreatePath()를 통해 시작됩니다 (

9–14행
). 이 메소드는 캐릭터(요원)가 경로를 따라 이동하는 방식을 세밀하게 조정하는 선택적 매개변수 테이블을 받습니다.

설명유형기본값
AgentRadius요원 반지름, 스터드 단위. 장애물에서 최소 분리를 결정하는 데 유용합니다.integer2
AgentHeight요원 높이, 스터드 단위. 이 값보다 작은 빈 공간은 비가로잡을 수 없는 것으로 표시됩니다.integer5
AgentCanJump경로 탐색 중 점프가 허용되는지를 결정합니다.booleantrue
AgentCanClimb경로 탐색 중 TrussParts를 오르는 것이 허용되는지를 결정합니다. 오를 수 있는 경로는 LabelClimb인 경로이며, 오를 수 있는 경로의 비용은 기본적으로 1입니다.booleanfalse
WaypointSpacing경로의 중간 웨이포인트 간의 간격입니다. math.huge로 설정하면 중간 웨이포인트가 없습니다.number4
Costs이동 시 비용을 위한 재료 또는 정의된 PathfindingModifiers의 테이블입니다. 에이전트가 특정 재료/지역을 다른 것보다 선호하도록 만드는 데 유용합니다. 자세한 내용은 수정기를 참조하세요.tablenil

경로 계산

유효한 경로를 CreatePath()로 생성한 후, Path:ComputeAsync()를 호출하여 시작점과 목적지에 대해 Vector3를 사용하여 계산해야 합니다 (

133–139행
).

두 다리 전체를 가로질러 표시된 경로 시작/종료

Path가 계산되면 시작부터 끝까지 경로를 추적하는 일련의 웨이포인트를 포함하게 됩니다. 이 점들은 Path:GetWaypoints() 메소드를 통해 수집할 수 있습니다 (

142행
). 반환되는 배열은 경로 시작부터 끝까지 웨이포인트의 순서로 정렬되어 있습니다.

계산된 경로에 걸쳐 나타난 웨이포인트
계산된 경로에 걸쳐 나타난 웨이포인트

경로 이동

PathWaypointPosition (Vector3)과 Action (PathWaypointAction)을 모두 포함합니다. 일반적인 Roblox 캐릭터와 같은 Humanoid를 포함하는 캐릭터를 이동하기 위한 가장 좋은 방법은 웨이포인트에서 웨이포인트로 Humanoid:Move()를 호출하고, 스크립트의 isCurrentWaypointReached() 콜백 (

32–56행
)을 사용하여 캐릭터가 각 웨이포인트에 도달했는지를 감지하는 것입니다.

차단된 경로

많은 Roblox 세계는 동적입니다. 일부 부품은 이동하거나 떨어질 수 있고 바닥은 무너질 수 있습니다. 이로 인해 계산된 경로가 차단되어 캐릭터가 목적지에 도달하지 못할 수 있습니다. 이를 처리하기 위해서는 Path.Blocked 이벤트에 연결하고 장애물 주변으로 경로를 재계산할 수 있습니다 (

145–154행
).

경로 탐색 수정기

기본적으로 Path:ComputeAsync()는 시작점과 목적지 사이의 최단 경로를 반환합니다. 단, 점프를 피하도록 시도합니다. 이는 일부 상황에서는 부자연스럽게 보일 수 있습니다. 예를 들어, 경로가 수로를 우회하는 대신 수로를 통과하도록 설정되어 있는 이유는 수로를 통과하는 경로가 기하학적으로 더 짧기 때문입니다.

짧은 경로가 반드시 더 논리적이지 않은 두 경로

경로 탐색을 더욱 최적화하기 위해 다양한 재료, 정의된 구역을 가로지르거나 장애물을 무시하는 경로 탐색 수정기를 구현할 수 있습니다.

재료 비용

TerrainBasePart 재료 작업 시, CreatePath() 내에 Costs 테이블을 포함하여 특정 재료가 다른 재료보다 더 쉽게 통과할 수 있도록 만들 수 있습니다. 모든 재료의 기본 비용은 1이며, 어떤 재료든지 값을 math.huge로 설정하여 비가로잡을 수 없는 것으로 정의할 수 있습니다.

Costs 테이블의 키는 Enum.Material 이름을 나타내는 문자열이어야 하며, 예를 들어, WaterEnum.Material.Water을, CrackedLavaEnum.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과 같은 의미 있는 이름을 할당합니다.

    Label 속성이 DangerZone으로 설정된 PathfindingModifier 인스턴스.
  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 속성을 활성화합니다.

    PassThrough 속성이 활성화된 PathfindingModifier 인스턴스.

    이제 좀비 NPC에서 플레이어 캐릭터로의 경로가 계산될 때 경로가 문을 넘어 확장되며, 좀비에게 이 경로를 통과하도록 지시할 수 있습니다. 좀비가 문을 열 수 없더라도, 문 뒤에 캐릭터가 있는 것처럼 반응합니다.

    이전에 차단된 문을 통과하는 좀비 NPC 경로.

경로 탐색 링크

때때로 정상적으로 통과할 수 없는 공간을 가로지르는 경로를 찾아야 할 수도 있습니다. 예를 들어, 깊은 협곡을 가로지르며 다음 웨이포인트에 도달하기 위해 사용자 정의 작업을 수행해야 할 수도 있습니다. 이는 PathfindingLink 객체를 통해 수행할 수 있습니다.

위에서 언급한 예시를 사용하여 요원이 보트를 사용할 수 있도록 만들 수 있습니다.

요원이 보트를 사용할 수 있는 방법을 보여주는 PathfindingLink.

이 예제를 사용하여 PathfindingLink를 생성하려면:

  1. 선택 사항
    3D 뷰포트 오른쪽 상단의 시각화 옵션 위젯에서 경로 탐색 링크를 켭니다. 이는 경로 탐색 링크를 구현할 때 시각화 및 디버깅에 도움이 됩니다.

  2. 보트의 좌석 및 보트 착륙 지점 근처에 Attachments 두 개를 생성합니다.

    경로 탐색 링크의 시작과 끝을 위한 첨부물.
  3. 작업 공간에 PathfindingLink 객체를 생성한 다음, 해당 Attachment0Attachment1 속성을 각각 시작 첨부물과 끝 첨부물에 할당합니다.

    PathfindingLink의 Attachment0/Attachment1 속성. 3D 세계에서 시각화된 PathfindingLink.
  4. UseBoat와 같은 의미 있는 이름을 Label 속성에 할당합니다. 이 이름은 에이전트가 시작 링크 지점에 도달할 때 사용자 정의 작업을 트리거하기 위한 플래그로 사용됩니다.

    PathfindingLink에 대해 지정된 Label 속성.
  5. CreatePath() 내에 Water 키와 Label 속성 이름과 일치하는 사용자 정의 키를 포함하는 Costs 테이블을 포함합니다. 사용자 정의 키의 값은 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를 사용할 때의 최선의 관행은 다음과 같습니다:

  • 스트리밍은 캐릭터가 이동할 때 주어진 경로를 차단하거나 차단 해제할 수 있습니다. 예를 들어, 캐릭터가 숲을 달리는 동안, 나무가 앞쪽 어딘가에 스트리밍되어 경로를 obstruct할 수 있습니다. 스트리밍과 원활하게 작동하는 경로 탐색을 위해, 차단된 경로 처리 기술을 사용하는 것이 권장되며 필요할 때 경로를 재계산해야 합니다.

  • 경로 탐색에서 일반적인 접근 방법은 기존 객체의 좌표를 사용하여 계산하는 것입니다. 예를 들어, 경로 목적지를 세계에 존재하는 TreasureChest 모델의 위치로 설정하는 것입니다. 이 접근 방식은 서버에서 항상 세계를 완전히 볼 수 있으므로 서버 측 Scripts와 완벽하게 호환되지만, 클라이언트에서 실행되는 LocalScriptsModuleScripts가 스트리밍되지 않은 객체에 대해 경로를 계산하려고 하면 실패할 수 있습니다.

    이 문제를 해결하기 위해, 지속적인 모델 내의 BasePart 위치로 목적지를 설정하는 것을 고려해 보세요. 지속적인 모델은 플레이어가 참여한 후 곧바로 로드되며 절대 스트리밍 아웃되지 않으므로, 클라이언트 측 스크립트가 PersistentLoaded 이벤트에 연결하고 이벤트가 발생한 후 웨이포인트 생성을 위한 모델에 안전하게 접근할 수 있습니다.

제한 사항 및 실패 요인

경로 탐색 엔진에는 효율적인 프로세싱과 최적의 성능을 보장하기 위한 특정 제한 사항이 있습니다. 또한 경로 탐색 계산은 다음과 같은 여러 이유로 실패할 수 있습니다.

©2026 Roblox Corporation. Roblox 및 Roblox 로고, 'Powering Imagination'은 미국 및 기타 국가 내 당사의 등록 및 미등록 상표입니다.