How can I navigate an NPC to a moving object using PathfindingService?

I want to create an NPC that will follow a player around. While I could use Humanoid:MoveTo() to make it go straight to the player, the problem with this is that the NPC won’t round corners or go up stairs, but instead just go in the direction of the player. Due to the way my map is designed, navigation would be preferred. So instead, I opted to use game:GetService("PathfindingService")

The problem is that as far as I can tell Roblox’s AI navigation system, game:GetService("PathfindingService), is not designed to handle following moving objects. This means while it is great at moving an NPC to a specific location, it is not good at following a moving object. However, this is exactly what I want for my game.

I’ve tried looking for solutions on the Developer Hub, DevForum, and other places, but I can’t seem to find one. It seems most people only use Humanoid:MoveTo() for their systems. While I could use this, I think it would be much more interesting if the NPCs used intelligent navigation. The system also needs to support switching between players on the fly and the map is dynamic and the path may be blocked. The NPC shouldn’t move when its path is blocked.

Currently, I’m using some code from this thread, but the while the code “should” determine which player to follow, it doesn’t contain code on how to navigate to them.

--Variables
local PlayerService = game:GetService("Players")
local pathFindingService = game:GetService("PathfindingService")
local humanoid = script.Parent.Humanoid
local CollectionService = game:GetService("CollectionService")
local RunService = game:GetService("RunService")
local PlayerTag = "PLAYER"
local targetCharacter = nil
local TargetPos = script.Parent:FindFirstChild("HumanoidRootPart").Position or script.Parent:FindFirstChild("Torso").Position
local path = pathFindingService:CreatePath()

--Looping Code
RunService.Heartbeat:Connect(function()
	local min = math.huge
	local char = nil

	for i, character in pairs(CollectionService:GetTagged(PlayerTag)) do
		local d = (character.PrimaryPart.Position - TargetPos).Magnitude

		if d < min then
			min = d
			char = character
		end
	end

	if targetCharacter ~= char then
		targetCharacter = char
		local player = PlayerService:GetPlayerFromCharacter(char)
		local position = char:WaitForChild("HumanoidRootPart").Position or char:WaitForChild("Torso").Position
		path:ComputeAsync(TargetPos, position)
		
		local waypoints = path:GetWaypoints()
		--[[
		There should be more code here to execute the commands, but that
		is precisely what I am asking for help about. ]]
	end
end)

I’m open to any suggestions on how to make the NPC follow a player while still avoiding obstacles.

3 Likes

The MoveTo function in this case is used to make the humanoid to move to the waypoints one by one. To do this:

for _,waypoint in pairs(waypoints) do
    humanoid:MoveTo(waypoint.Position)
    if waypoint.Action == Enum.PathWaypointAction.Jump then
        humanoid.Jump = true
    humanoid.MoveToFinished:Wait()
end
1 Like

Yes, actually I don’t think I specified enough. I understand you need to use MoveTo in order to move between waypoints. The problem is that the position I want to move to is constantly changing, meaning that the path needs to be recalculated before the NPC has a chance to finish moving. This means the path needs to be updated while the NPC is still traveling. This code doesn’t work because it is designed to move to a fixed position, and is only capable of recalcuating once it has reached its destination.

1 Like

Could you just calculate all of the waypoints, but only move to the first couple waypoints? Then when they are done moving recalculate the waypoints and repeat.

1 Like

I was able to get it to work in two different ways, but both cause a significant problem. In the better method, in which you loop through all waypoints like the method @ItzMeZeus_IGotHacked proposed, but update after every cycle, it seems that as the NPC moves closer, it updates more frequently, eventually getting to the point where it updates so fast that the NPC’s walk speed is decreased, preventing it from following you. I’ve included a video before so you can understand exactly what the problem is.

In the other method, which is more similar to your proposal, only cycling through the first few waypoints before recalculating, it is effectively the same issue with the other system. Since you are updating the navigation so frequently, the NPC never reaches anything near normal walk speed. It also seems to be generally less reliable in following the player than the other method. I’ve included another video before so you can understand exactly what the problem is.

I’m sorry about taking so long to respond, I didn’t have a fully functional model ready for testing purposes, and there were a lot of bugs to work out.

1 Like

How about trying to use raycast beforehand and if the NPC is unable to find the player due to certain distance, you would call out the Pathfinding function.
Something like this:

> function raycastchase(zombie)
> 	local ignorelist = {zombie.root.Parent}
> 	local currentTarget = zombie.target
> 	if zombie.target == nil then
> 		return
> 	end
> 	local v2root = currentTarget.Position - zombie.root.Position
> 	local ray = Ray.new(zombie.root.Position, v2root.Unit * 800)
> 	local hit, position = workspace:FindPartOnRayWithIgnoreList(ray, ignorelist)
> 	if hit then
> 		print("isee")
> 		if hit:IsDescendantOf(currentTarget.Parent) then
> 			zombie.human.WalkSpeed = 16
> 				print("a person", currentTarget.Parent.Name)
> 			zombie.human:MoveTo(currentTarget.Position)
> 		else 
> 			pathToTarget(zombie)
> 		end
> 	end
> end
2 Likes

This absolutely works. It appears to be a bit buggy (at least in my implementation), but switching between the two was definitely the right option. It can now pursue you anywhere in the map, but can still get very close when you are within range. If I can work the rest of the bugs out, this should work great. Thanks for the help!

to also stop the bugs for every part in the npc set it’s network owner to nil:

for _, part in npc:GetChildren() do
	if part:IsA("BasePart") then
		part:SetNetworkOwner(nil)
	end
end
1 Like