Monster NPC stopping momentarily every waypoint

Hello developers!

I recently created a game on Roblox but I have reached a problem during development. It seems like the Monster NPC are stopping momentarily before moving to the next waypoint which is frustrating since It makes the NPC too slow to attack players.

Here is a video:

Here is the code:

local Monster = script.Parent
local MHumanoid = Monster:FindFirstChildOfClass("Humanoid")
local myRoot = Monster.HumanoidRootPart

local IgnoreParent = Monster.Parent

local Hitbox = Monster.Hitbox

local AttackDelay = Monster:GetAttribute("AttackDelay")
local Damage = Monster:GetAttribute("Damage")
local NoticeDistance = Monster:GetAttribute("NoticeDistance")

local Animator = MHumanoid:FindFirstChildOfClass("Animator")
local goal = Monster.HumanoidRootPart

local Animations = {
	["AttackAnimation"] = 18616845543
}

local path = game:GetService("PathfindingService"):CreatePath({
	AgentRadius = 2,
	AgentHeight = 5,
	AgentCanJump = true,
	AgentJumpHeight = 7.2,
	AgentCanClimb = true
})

local Chasing = false
local Attacking = false

Monster:GetAttributeChangedSignal("NoticeDistance"):Connect(function()
	NoticeDistance = Monster:GetAttribute("NoticeDistance")
end)

Monster:GetAttributeChangedSignal("Damage"):Connect(function()
	Damage = Monster:GetAttribute("Damage")
end)

Monster:GetAttributeChangedSignal("AttackDelay"):Connect(function()
	AttackDelay = Monster:GetAttribute("AttackDelay")
end)

--// Services
local PathfindingService = game:GetService("PathfindingService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local ServerStorage = game:GetService("ServerStorage")

Monster.PrimaryPart:SetNetworkOwner(nil)

-- Function to find the nearest player
local function findNearestPlayer()
	local nearestPlayer = nil
	local shortestDistance = math.huge

	for _, player in ipairs(Players:GetPlayers()) do
		local character = player.Character
		if character and character:FindFirstChild("HumanoidRootPart") then
			--if isTargetVisible(character.HumanoidRootPart) then
				local distance = (character.HumanoidRootPart.Position - Monster.HumanoidRootPart.Position).Magnitude
				if distance < shortestDistance then
					nearestPlayer = character
					shortestDistance = distance
				end
		--	end
		end
	end

	return nearestPlayer
end

local function PlayAnimation(id, duration)
	if type(id) == "string" then
		id = tonumber(id)
	end
	
	local Animation = Instance.new("Animation", Monster)
	Animation.AnimationId = "rbxassetid://"..tostring(id)
	
	local anim = Animator:LoadAnimation(Animation)
	local speed = 1
	if duration then
		speed = duration / anim.Length
	end
	anim:Play(0, 1, speed)
	anim.Ended:Connect(function()
		Animation:Destroy()
	end)
	
	local a = duration or 10
	
	task.spawn(function()
		task.wait(a)
		Animation:Destroy()
	end)
end



coroutine.wrap(function()
	-- Attack
	while MHumanoid.Health > 0 do
		task.wait(AttackDelay)
		
		local oOverlapParams = OverlapParams.new()
		oOverlapParams.FilterDescendantsInstances = {IgnoreParent}
		oOverlapParams.FilterType = Enum.RaycastFilterType.Exclude
		
		local Parts = workspace:GetPartsInPart(Hitbox, oOverlapParams)
		if #Parts >= 1 then
			for i, part in pairs(Parts) do
				if Players:GetPlayerFromCharacter(part.Parent) then
					local Char:Model = part.Parent
					if part.Name == "HumanoidRootPart" or Char.PrimaryPart == part then
						local PHum = Char:FindFirstChildOfClass("Humanoid")
						if PHum.Health > 0 then
							PHum:TakeDamage(Damage)
							task.spawn(PlayAnimation, Animations.AttackAnimation,  Random.new():NextNumber(0.2,0.3))
							Monster.Head.Attack:Play()
						end
					end
				end
			end
		end
	end
end)()

function checkSight(target)
	local ray = Ray.new(myRoot.Position, (target.Position - myRoot.Position).Unit * 40)
	local hit,position = workspace:FindPartOnRayWithIgnoreList(ray, {script.Parent})
	if hit then
		if hit:IsDescendantOf(target.Parent) and math.abs(hit.Position.Y - myRoot.Position.Y) < 3 then
			return true
		end
	end
	return false
end

function findPath(target)
	path:ComputeAsync(myRoot.Position,target.Position)
	local waypoints = path:GetWaypoints()

	if path.Status == Enum.PathStatus.Success then
		for _, waypoint in ipairs(waypoints) do
			if waypoint.Action == Enum.PathWaypointAction.Jump then
				MHumanoid.Jump = true
			end
			MHumanoid:MoveTo(waypoint.Position)
			local timeOut = MHumanoid.MoveToFinished:Wait(1)
			if not timeOut then
				MHumanoid.Jump = true
				findPath(target)
				break
			end
			if checkSight(target) then
				repeat
					MHumanoid:MoveTo(target.Position)
					task.wait(0.1)
					if target == nil then
						break
					elseif target.Parent == nil then
						break
					end
				until checkSight(target) == false or MHumanoid.Health == 0 or target.Parent.Humanoid.Health == 0
				break
			end
			if (myRoot.Position - waypoints[1].Position).magnitude > 15 then
				findPath(target)
				break
			end
		end
	end
end


coroutine.wrap(function()
	while task.wait(0.1) do
		if MHumanoid.Health <= 0 then
			break
		end
		
		local target = findNearestPlayer()
		if target then
			findPath(target.HumanoidRootPart)
		end
	end
end)()


coroutine.wrap(function()
	while task.wait(1) do
		if Chasing then
			local Chance = math.random(1, 20)
			if Chance 	>= 15 then
				if Chasing == true then
					Monster.Head.Scream:Play()
				end
			end
		end
	end
end)()
2 Likes

pathfinding services usually do this commonly, i don’t know how to fix it but i know it’s a problem with pathfinding service

1 Like

Try changing the refresh time for the pathfinding, if this still doesn’t work let me know!

local RunService = game:GetService("RunService")

coroutine.wrap(function()
	local connection

	connection = RunService.Heartbeat:Connect(function()
		if MHumanoid.Health <= 0 then
			connection:Disconnect()
		end

		local target = findNearestPlayer()
		if target then
			findPath(target.HumanoidRootPart)
		end
	end)
end)()

They seem to be moving back and forth repeatedly

1 Like

Not sure if you’ve done it yet but if you haven’t Set the Ai’s HumanoidRootPart to nil.

-- Somewhere near the top I guess
AiHumanoidRootPart:SetNetworkOwner(nil)

Don’t think this’ll fix your problem but it should help a bit, probably

Well, that’s because you aren’t in the game right, isn’t the code attempting to find you, or do are they meant to have like pathfinding places where they just walk around, because if they have places they are meant to walk around then they should be doing that. Show me in the actual game, maybe it won’t fix anything but I am curious

The Monster NPCs are meant to chase the player and they don’t have any places to walk around or places to patrol

Ok, well you aren’t in the game are you in that second video, so they don’t have anywhere to go

I’m using the free cam feature which means I’m still on the game

waypoint distance from each other vs speed needs to be tested and refined.

Your movement loop in findPath exits early because of this code

if (myRoot.Position - waypoints[1].Position).magnitude > 15 then
	findPath(target)
	break
end

You’re using the first waypoint’s position to calculate the distance, meaning your NPC recomputes a new path every 15 studs.

RBXScriptSignal:Wait() does not have any parameters, passing any value to it does nothing.

You’re right! My bad, I thought it allowed for an additional wait time value.

Yeah I got confused too when OP added that 1 there

So what should I fix, change, or remove? I’m new to this Pathfinding scripting part so can you please enlighten me? Any help will be appreciated!

1 Like

How do you have organized this system? You are doing like a “handler” that updates every AI with a single repeat loop (while or renderstepped) or every model has a individual script?, if so, have you checked if this is a web problem?, like, if there are multiple instances of this AI running at the same time, they are slowing down the server in general, is receiving so much data from the pathfinding instances that it can’t handle it all at the same time, doing that slowness.

Change it to the waypoint the NPC was moving to.

The stopping momentarily every waypoint is fixed now but sometimes it goes to the player’s past position and updates once the player is near

Here’s what the code looks like so far:

local Monster = script.Parent
local MHumanoid = Monster:FindFirstChildOfClass("Humanoid")
local myRoot = Monster.HumanoidRootPart

local IgnoreParent = Monster.Parent

local Hitbox = Monster.Hitbox

local AttackDelay = Monster:GetAttribute("AttackDelay")
local Damage = Monster:GetAttribute("Damage")
local NoticeDistance = Monster:GetAttribute("NoticeDistance")

local Animator = MHumanoid:FindFirstChildOfClass("Animator")
local goal = Monster.HumanoidRootPart

local Animations = {
	["AttackAnimation"] = 18616845543
}

local path = game:GetService("PathfindingService"):CreatePath({
	AgentRadius = 2,
	AgentHeight = 2,
	AgentCanJump = true,
	AgentJumpHeight = 7.2,
	AgentCanClimb = true
})

local Chasing = false
local Attacking = false

Monster:GetAttributeChangedSignal("NoticeDistance"):Connect(function()
	NoticeDistance = Monster:GetAttribute("NoticeDistance")
end)

Monster:GetAttributeChangedSignal("Damage"):Connect(function()
	Damage = Monster:GetAttribute("Damage")
end)

Monster:GetAttributeChangedSignal("AttackDelay"):Connect(function()
	AttackDelay = Monster:GetAttribute("AttackDelay")
end)

--// Services
local PathfindingService = game:GetService("PathfindingService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local ServerStorage = game:GetService("ServerStorage")

Monster.PrimaryPart:SetNetworkOwner(nil)

-- Function to find the nearest player
local function findNearestPlayer()
	local nearestPlayer = nil
	local shortestDistance = math.huge

	for _, player in ipairs(Players:GetPlayers()) do
		local character = player.Character
		if character and character:FindFirstChild("HumanoidRootPart") then
			--if isTargetVisible(character.HumanoidRootPart) then
				local distance = (character.HumanoidRootPart.Position - Monster.HumanoidRootPart.Position).Magnitude
				if distance < shortestDistance then
					nearestPlayer = character
					shortestDistance = distance
				end
		--	end
		end
	end

	return nearestPlayer
end

local function PlayAnimation(id, duration)
	if type(id) == "string" then
		id = tonumber(id)
	end
	
	local Animation = Instance.new("Animation", Monster)
	Animation.AnimationId = "rbxassetid://"..tostring(id)
	
	local anim = Animator:LoadAnimation(Animation)
	local speed = 1
	if duration then
		speed = duration / anim.Length
	end
	anim:Play(0, 1, speed)
	anim.Ended:Connect(function()
		Animation:Destroy()
	end)
	
	local a = duration or 10
	
	task.spawn(function()
		task.wait(a)
		Animation:Destroy()
	end)
end



coroutine.wrap(function()
	-- Attack
	while MHumanoid.Health > 0 do
		task.wait(AttackDelay)
		
		local oOverlapParams = OverlapParams.new()
		oOverlapParams.FilterDescendantsInstances = {IgnoreParent}
		oOverlapParams.FilterType = Enum.RaycastFilterType.Exclude
		
		local Parts = workspace:GetPartsInPart(Hitbox, oOverlapParams)
		if #Parts >= 1 then
			for i, part in pairs(Parts) do
				if Players:GetPlayerFromCharacter(part.Parent) then
					local Char:Model = part.Parent
					if part.Name == "HumanoidRootPart" or Char.PrimaryPart == part then
						local PHum = Char:FindFirstChildOfClass("Humanoid")
						if PHum.Health > 0 then
							PHum:TakeDamage(Damage)
							task.spawn(PlayAnimation, Animations.AttackAnimation,  Random.new():NextNumber(0.2,0.3))
							Monster.Head.Attack:Play()
						end
					end
				end
			end
		end
	end
end)()

function checkSight(target)
	local ray = Ray.new(myRoot.Position, (target.Position - myRoot.Position).Unit * 40)
	local hit,position = workspace:FindPartOnRayWithIgnoreList(ray, {script.Parent})
	if hit then
		if hit:IsDescendantOf(target.Parent) and math.abs(hit.Position.Y - myRoot.Position.Y) < 3 then
			return true
		end
	end
	return false
end

function findPath(target)
	path:ComputeAsync(myRoot.Position, target.Position)
	local waypoints = path:GetWaypoints()

	if path.Status == Enum.PathStatus.Success then
		for _, waypoint in ipairs(waypoints) do
			if waypoint.Action == Enum.PathWaypointAction.Jump then
				MHumanoid.Jump = true
			end

			MHumanoid:MoveTo(waypoint.Position)
			local timeOut = MHumanoid.MoveToFinished:Wait()
			if not timeOut then
				MHumanoid.Jump = true
				findPath(target)
				break
			end
			if checkSight(target) then
				repeat
					MHumanoid:MoveTo(target.Position)
					task.wait(0.1)
					if target == nil then
						break
					elseif target.Parent == nil then
						break
					end
				until checkSight(target) == false or MHumanoid.Health == 0 or target.Parent.Humanoid.Health == 0
				break
			end
			if (myRoot.Position - waypoint.Position).magnitude > 15 then
				findPath(target)
				break
			end
		end
	end
end


coroutine.wrap(function()
while task.wait(0.1) do
		if MHumanoid.Health <= 0 then
			break
		end

		local target = findNearestPlayer()
		if target then
			findPath(target.HumanoidRootPart)
		end
	end
end)()


coroutine.wrap(function()
	while task.wait(1) do
		if Chasing then
			local Chance = math.random(1, 20)
			if Chance 	>= 15 then
				if Chasing == true then
					Monster.Head.Scream:Play()
				end
			end
		end
	end
end)()

You’ll have to refresh your path every so often or set the part parameter in Humanoid:MoveTo() to the target