Nawigacja to proces poruszania się postacią lub obiektem (agentem) wzdłuż logicznej ścieżki wokół przeszkód, aby dotrzeć do celu, opcjonalnie unikając materiałów niebezpiecznych lub zdefiniowanych obszarów.
Wizualizacja nawigacji
Aby pomóc w układzie i debugowaniu nawigacji, Studio może renderować siatkę nawigacyjną oraz etykiety modyfikatorów. Aby je włączyć, przełącz Siatkę nawigacyjną i Modyfikatory nawigacji w widżecie Opcje wizualizacji w prawym górnym rogu widoku 3D.

Z włączoną Siatką nawigacyjną kolorowe obszary pokazują, gdzie postać może chodzić lub pływać. Małe strzałki wskazują obszary, które postać spróbuje osiągnąć, skacząc.

Implementacja
Chociaż nawigacja może być implementowana na różne sposoby za pomocą PathfindingService i jego powiązanych metod, takich jak CreatePath(), ta sekcja używa następującego skryptu nawigacji dla postaci gracza.
Aby przetestować podczas czytania:
- WAŻNEW Eksploratorze wybierz kontener StarterPlayer. Następnie w oknie Właściwości ustaw zarówno DevComputerMovementMode, jak i DevTouchMovementMode na Scriptable.


Skopiuj poniższy kod do LocalScript w StarterCharacterScripts, lub pobierz ten pakiet i umieść go w StarterCharacterScripts.
PlayerPathFollow (LocalScript w 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-- Oblicz odległość od humanoida do płaszczyzny celulocal dist = currentWaypointPlaneNormal:Dot(humanoid.RootPart.Position) - currentWaypointPlaneDistance-- Oblicz komponent prędkości humanoida, który jest skierowany w stronę płaszczyznylocal velocity = -currentWaypointPlaneNormal:Dot(humanoid.RootPart.Velocity)-- Oblicz próg od płaszczyzny celu na podstawie prędkości humanoidalocal threshold = math.max(1.0, VELOCITY_MULTIPLIER * velocity)-- Uznaj waypoint za osiągnięty, jeśli jest bliżej niż próg przed płaszczyzną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]-- Zbuduj płaszczyznę celu z następnego waypointu w kierunku aktualnegocurrentWaypointPlaneNormal = currentWaypoint.Position - nextWaypoint.Position-- Ustaw normalną prostopadłą do płaszczyzny Y, gdy nie wspinamy się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-- Wywołaj swoją własną spersonalizowaną funkcję, aby sprawić, że agent użyje łodzielseif 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()-- Oblicz ścieżkępathfinderWorking = truewaitForGround()local success, errorMessage = pcall(function()path:ComputeAsync(character.PrimaryPart.Position, DESTINATION)end)if not success or path.Status ~= Enum.PathStatus.Success thenwarn("Ścieżka nie została obliczona!", errorMessage)returnend-- Pobierz punkty na ścieżcewaypoints = path:GetWaypoints()-- Wykryj, czy ścieżka została zablokowanablockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)-- Sprawdź, czy przeszkoda jest dalej wzdłuż ścieżkiif blockedWaypointIndex >= nextWaypointIndex then-- Przestań wykrywać blokadę ścieżki, aż ścieżka zostanie ponownie obliczonablockedConnection:Disconnect()resetWaypointData()-- Wywołaj funkcję, aby ponownie obliczyć nową ścieżkęfollowPath()endend)findStartingPoint(waypoints)moveToNextWaypoint()endfollowPath()Przejdź przez następujące sekcje, aby dowiedzieć się o obliczaniu ścieżek i ruchu postaci.
Tworzenie ścieżki
Nawigacja jest inicjowana przez PathfindingService i jego metodę CreatePath() (
| Klucz | Opis | Typ | Domyślny |
|---|---|---|---|
| AgentRadius | Promień agenta, w studach. Przydatne do określenia minimalnego odstępu od przeszkód. | integer | 2 |
| AgentHeight | Wysokość agenta, w studach. Pusta przestrzeń mniejsza niż ta wartość, jak przestrzeń pod schodami, będzie oznaczona jako nieprzechodnia. | integer | 5 |
| AgentCanJump | Określa, czy skakanie podczas nawigacji jest dozwolone. | boolean | true |
| AgentCanClimb | Określa, czy wspinanie się po TrussParts podczas nawigacji jest dozwolone. Wspinaczka ma etykietę Label o nazwie Climb, a koszt wspinaczki wynosi domyślnie 1. | boolean | false |
| WaypointSpacing | Odstęp między pośrednimi punktami na ścieżce. Jeśli ustawione na math.huge, nie będzie pośrednich punktów. | number | 4 |
| Costs | Tabela materiałów lub zdefiniowanych PathfindingModifiers i ich kosztów za przejście. Przydatne do sprawienia, że agent preferuje niektóre materiały/obszary nad innymi. Zobacz modyfikatory po szczegóły. | table | nil |
Obliczanie ścieżki
Po utworzeniu ważnej ścieżki za pomocą CreatePath(), musi być ona obliczona przez wywołanie Path:ComputeAsync() z Vector3 zarówno dla punktu startowego, jak i celu (

Gdy Path jest obliczona, będzie zawierać szereg punktów nawigacyjnych, które śledzą ścieżkę od początku do końca. Punkty te można zebrać za pomocą metody Path:GetWaypoints() (

Ruch po ścieżce
Każdy PathWaypoint składa się zarówno z Position (Vector3), jak i Action (PathWaypointAction). Aby poruszyć postać zawierającą Humanoid, jak typowa postać Roblox, najlepszym sposobem jest wywołanie Humanoid:Move() z punktu nawigacyjnego do punktu nawigacyjnego i użycie zwrotu isCurrentWaypointReached() skryptu (
Zablokowane ścieżki
Wiele światów Roblox jest dynamicznych; części mogą się poruszać lub upadać, a podłogi mogą się zapadać. Może to zablokować obliczoną ścieżkę i uniemożliwić postaci dotarcie do celu. Aby to obsłużyć, możesz połączyć zdarzenie Path.Blocked i ponownie obliczyć ścieżkę wokół tego, co ją zablokowało (
Modyfikatory nawigacji
Domyślnie Path:ComputeAsync() zwraca najkrótszą ścieżkę między punktem startowym a celem, z wyjątkiem sytuacji, gdy stara się unikać skoków. Wygląda to nienaturalnie w niektórych sytuacjach; na przykład ścieżka może prowadzić przez wodę bagienną zamiast wokół niej, po prostu dlatego, że ścieżka przez wodę jest geometrycznie krótsza.

Aby jeszcze bardziej zoptymalizować nawigację, możesz wdrożyć modyfikatory nawigacji, aby obliczyć mądrzejsze ścieżki przez różne materiały, wokół zdefiniowanych obszarów lub aby ignorować przeszkody.
Koszty materiałów
Pracując z materiałami Terrain i BasePart, możesz dołączyć tabelę Costs w CreatePath(), aby uczynić niektóre materiały bardziej przejezdnymi niż inne. Wszystkie materiały mają domyślny koszt równy 1, a każdy materiał można zdefiniować jako nieprzechodni, ustawiając jego wartość na math.huge.
Klucze w tabeli Costs powinny być nazwami string reprezentującymi nazwy Enum.Material, na przykład Water dla Enum.Material.Water lub CrackedLava dla Enum.Material.CrackedLava.
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
}
})Konfiguracja obszarów
W niektórych przypadkach preferencje materiałów nie są wystarczające. Na przykład, możesz chcieć, aby postacie unikały zdefiniowanego obszaru, niezależnie od materiałów pod stopami. Można to osiągnąć, dodając obiekt PathfindingModifier do części.
Utwórz część Anchored wokół obszaru i ustaw jej właściwość CanCollide na false.

Wstaw instancję PathfindingModifier na część, znajdź jej właściwość Label i przypisz jej znaczącą nazwę, taką jak DangerZone.

Dołącz odpowiadający klucz DangerZone i powiązaną wartość numeryczną w tabeli Costs w CreatePath(). Modyfikator można zdefiniować jako nieprzechodni, ustawiając jego wartość na 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}})
Ignorowanie przeszkód
W niektórych przypadkach przydatne jest, aby nawigacja przebiegała przez solidne przeszkody, jakby ich nie było. Pozwala to obliczyć ścieżkę przez konkretne fizyczne blokady, zamiast aby obliczenie nie powiodło się.
Utwórz część Anchored wokół obiektu i ustaw jej właściwość CanCollide na false.

Wstaw instancję PathfindingModifier na część i włącz jej właściwość PassThrough.

Teraz, gdy ścieżka jest obliczana z NPC zombie do postaci gracza, ścieżka rozciąga się poza drzwi i możesz nakazać zombie przejść przez nie. Nawet jeśli zombie nie jest w stanie otworzyć drzwi, reaguje tak, jakby "słyszało" postać za drzwiami.

Linki nawigacyjne
Czasami konieczne jest znalezienie ścieżki przez przestrzeń, która nie może być normalnie przebyta, na przykład przez przepaść, i wykonanie niestandardowej akcji, aby dotrzeć do następnego punktu nawigacyjnego. Można to osiągnąć za pomocą obiektu PathfindingLink.
Używając przykładu z powyżej, możesz sprawić, że agent użyje łodzi.

Aby utworzyć PathfindingLink używając tego przykładu:
- OPCJONALNEWłącz Linki nawigacyjne w widżecie Opcje wizualizacji w prawym górnym rogu widoku 3D. Pomaga to w wizualizacji i debugowaniu podczas implementacji linków nawigacyjnych.
Utwórz dwa Attachments, jeden na siedzeniu łodzi, a drugi w pobliżu punktu lądowania łodzi.

Utwórz obiekt PathfindingLink w przestrzeni roboczej, a następnie przypisz jego właściwości Attachment0 i Attachment1 do odpowiednio początkowych i końcowych załączników.


Przypisz znaczącą nazwę, taką jak UseBoat, do jej właściwości Label. Ta nazwa jest używana jako flaga w skrypcie nawigacyjnym do wywołania niestandardowej akcji, gdy agent osiągnie punkt początkowy linku.

Dołącz tabelę Costs w CreatePath(), zawierającą zarówno klucz Water, jak i niestandardowy klucz odpowiadający nazwie właściwości Label. Przypisz niestandardowemu kluczowi wartość niższą niż wartość 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}})W funkcji moveToNextWaypoint() (
LINIE 93–114) można użyć niestandardowego sprawdzenia dla nazwy modyfikatora Label, aby podjąć inną akcję niż Humanoid:Move(); w tym przypadku możesz wywołać funkcję, aby posadzić agenta w łodzi, przemieścić łódź przez wodę, wysadzić agenta w punkcie lądowania łodzi, a następnie kontynuować ścieżkę agenta do jego ostatecznego celu.
Kompatybilność ze strumieniowaniem
Strumieniowanie instancji w grze streaming to potężna funkcja, która dynamicznie ładuje i odłącza zawartość 3D, gdy postać gracza porusza się po świecie. Gdy eksplorują przestrzeń 3D, nowe podzbiory przestrzeni strumieniują do ich urządzenia, a niektóre z istniejących podzbiorów mogą być odłączane.
Rozważ następujące najlepsze praktyki dotyczące używania PathfindingService w grach z włączonym strumieniowaniem:
Strumieniowanie może blokować lub odblokowywać daną ścieżkę, gdy postać porusza się wzdłuż niej. Na przykład, gdy postać biegnie przez las, drzewo może strumieniować gdzieś przed nimi i zablokować ścieżkę. Aby nawigacja działała płynnie ze strumieniowaniem, zdecydowanie zaleca się użycie techniki obsługi zablokowanych ścieżek i ponowne obliczenie ścieżki, gdy to konieczne.
Powszechnym podejściem w nawigacji jest użycie współrzędnych istniejących obiektów do obliczeń, na przykład ustawienie celu ścieżki na pozycję istniejącego modelu TreasureChest w świecie. To podejście jest w pełni zgodne z serwerowymi Scripts, ponieważ serwer ma pełny widok na świat przez cały czas, ale LocalScripts i ModuleScripts, które działają po stronie klienta, mogą nie powieść się, jeśli próbują obliczyć ścieżkę do obiektu, który nie jest strumieniowany.
Aby rozwiązać ten problem, rozważ ustawienie celu na pozycję BasePart w modelu trwałym. Trwałe modele ładują się wkrótce po dołączeniu gracza i nigdy nie są odłączane, więc skrypt po stronie klienta może połączyć się z zdarzeniem PersistentLoaded i bezpiecznie uzyskać dostęp do modelu w celu utworzenia punktów nawigacyjnych po wywołaniu zdarzenia.
Ograniczenia i czynniki awarii
Silnik nawigacji ma określone ograniczenia, aby zapewnić efektywne przetwarzanie i optymalną wydajność. Dodatkowo, obliczenia nawigacji computations mogą nie powieść się z różnych powodów, jak opisano poniżej.