Pathfinding Multiple Moving Players

So I’m attempting to script an NPC that will run to and attack the nearest player to it. The NPC will need pathfinding, as it has to go through obstacles, etc. As well, the nearest player to it could change, and it needs to respond to that change. So it can’t just pick the nearest player at one moment and then chase them down forever.

What I’ve got so far is essentially calculating the nearest player and a path to them, moving through a few points, and then recalculating the closest player and path. Although not the smoothest, it does work at getting the NPC close to the player. However, once the NPC is within about two studs of the player, the time it takes to calculate the path is long enough that by the time the NPC moves there, the player is no longer there.

I tried modifying the script so that when the NPC gets within about 5 studs, it changes to just a while true do loop with a humanoid:MoveTo(player.Torso.position) and whereas it’s a bit better, there’s the same problem. It’s moving the NPC to where the player was but by the time they get there, the player isn’t there, even if the NPC is ridiculously fast.

Side Note: When testing this, it’s important to use the “Network Simulation” feature in Studio Testing mode. Without it, it may appear as if it works, but it does not in-game.

So essentially, I need some way to have the NPC (once close) move a bit ahead in the direction the player is moving, and not to where they currently are. Humanoid movement is not really my specialty, so I guess it’s totally possible I’m doing this all wrong. Any help is much appreciated!

1 Like

You could offset the target position by a few studs based on the direction a player is moving. To do this I would probably compare a players current position to their previous position x amount of time ago.

You would calculate it something like this (not tested):

local POLL_POSITION_WAIT = 0.5
local POSITION_OFFSET_STUDS = 3

local PlayerService = game:GetService("Players")
local PositionOffset = {}
local PreviousPosition = {}

spawn(function()
	while wait(POLL_POSITION_WAIT) do
		local players = PlayerService:GetPlayers()
		for _, player in pairs(players) do
			if player.Character then
				local hrp = player.Character:FindFirstChild("HumanoidRootPart")
				if hrp then
					local pos = hrp.Position
					if PreviousPosition[player] then
						local offset = Vector3.new(0, 0, 0)
						if (pos -  PreviousPosition[player]).magnitude > 0.1 then
							offset = (pos -  PreviousPosition[player]).unit * POSITION_OFFSET_STUDS
						end
						PositionOffset[player] = offset
						print(offset)
					end
					PreviousPosition[player] = pos
				end
			end
		end
	end
end)

And you could use PositionOffset[player] elsewhere in your code.

7 Likes

What about clearing players from the table? This code is going to have player references hanging in the memory forever.

2 Likes

Yeah, I should have mentioned that. If you want to use a solution like this you should clear both the PreviousPosition and PositionOffset entries for a given player in the PlayerRemoving event.

2 Likes

I wrote a quick script to place a blue sphere where your function is offsetting to, and it resulted in a circle. Not quite sure why.

Screenshot_1

Note: I did make a couple edits to your script to fix minor typos, etc. I’ve posted it here just in case I messed something up

spawn(function()
while wait(POLL_POSITION_WAIT) do
	local players = PlayerService:GetPlayers()
	for _, player in pairs(players) do
		if player.Character then
			local hrp = player.Character:FindFirstChild("HumanoidRootPart")
			if hrp then
				local pos = hrp.Position
				if PreviousPosition[player.UserId] then
					local offset = Vector3.new(0, 0, 0)
					if (pos - PreviousPosition[player.UserId]).magnitude > 0.1 then
						offset = (pos -  PreviousPosition[player.UserId]).unit * POSITION_OFFSET_STUDS
					end
					PositionOffset[player.UserId] = offset
					local part = Instance.new('Part')
						part.Size = Vector3.new(1,1,1)
						part.CanCollide = false
						part.Anchored = true
						part.Material = 'Neon'
						part.BrickColor = BrickColor.new('Really blue')
						part.Shape = Enum.PartType.Ball
						part.Position = PositionOffset[player.UserId]
						part.Parent = workspace
				else
					table.insert(PositionOffset, player.UserId, pos)
					table.insert(PreviousPosition, player.UserId, pos)
				end
				PreviousPosition[player.UserId] = pos
			end
		end
	end
end
end)

game.Players.PlayerRemoving:Connect(function(p)

    table.remove(PositionOffset, p.UserId)

    table.remove(PreviousPosition, p.UserId)

end)

edit: accidentally posted slightly out-dated code

edit 2: just noticed the following in output:
Screenshot_2
(current is the position of the player, offset is the offset position)

You should add the offset to the players current position. The reason it results in a circle is because it offsets a set amount of studs in the direction that the player is moving.

Try it with

part.Position = pos + PositionOffset[player.UserId]
1 Like

Yup - that was it. I guess I shouldn’t try to develop while I’m still tired, too many dumb mistakes on my part like this.

Thank you for your help.