Prevent Breaks/Pauses in Movement for Ai

The Ai doesn’t track the player when the player spawns until at a random time.
When the Ai chases the player, the Ai sometimes stops moving.

local Players = game:GetService("Players")
local Pathfinding = game:GetService("PathfindingService")
local RunService = game:GetService("RunService")
local Debris = game:GetService("Debris")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Ai = script.Parent
local primPart = Ai.PrimaryPart
local humanoid = Ai:WaitForChild("Ai")
local Settings = require(Ai:WaitForChild("Settings"))

local UpdateSpeed_Event = ReplicatedStorage:WaitForChild("UpdateSpeed")

-- Disable client network ownership
for _, part in ipairs(Ai:GetDescendants()) do
	if part:IsA("BasePart") then
		part:SetNetworkOwner(nil)
	end
end

-- Initialization
humanoid.WalkSpeed = Settings.minSpeed
local lastPosition = primPart.Position
local stuck = false
local obstructed = true
local chasing = false

local path = Pathfinding:CreatePath({
	AgentCanClimb = false,
	Costs = { Climb = 0 }
})

local function GetClosestTarget(maxDistance)
	local closest = nil
	local shortestDistance = maxDistance

	local forwardVector = primPart.CFrame.LookVector
	local fovAngle = math.rad(Settings.range)
	local blindSpotRange = Settings.nearRange

	for _, player in ipairs(Players:GetPlayers()) do
		local character = player.Character
		if character and character.PrimaryPart then
			local root = character.PrimaryPart
			local humanoid = character:FindFirstChildOfClass("Humanoid")

			if humanoid and humanoid.Health > 0 then
				local directionToPlayer = (root.Position - primPart.Position)
				local dist = directionToPlayer.Magnitude

				if dist <= maxDistance then
					local inBlindSpotRange = dist <= blindSpotRange
					local angle = math.acos(forwardVector:Dot(directionToPlayer.Unit))

					if inBlindSpotRange or angle <= fovAngle then
						if dist < shortestDistance then
							shortestDistance = dist
							closest = character
						end
					end
				end
			end
		end
	end

	return closest, shortestDistance
end

-- Check if there's something between AI and target
local function CheckObstruction(target)
	if not target or not target.PrimaryPart then return true end

	local rayParams = RaycastParams.new()
	rayParams.FilterDescendantsInstances = { Ai, target }
	rayParams.FilterType = Enum.RaycastFilterType.Exclude

	local direction = target.PrimaryPart.Position - primPart.Position
	local result = workspace:Raycast(primPart.Position, direction, rayParams)

	return result ~= nil
end

local function Follow(target)
	if not target.PrimaryPart then return end

	local destination = target.PrimaryPart.Position

	local success, err = pcall(function()
		path:ComputeAsync(primPart.Position, destination)
	end)

	if success and path.Status == Enum.PathStatus.Success then
		local waypoints = path:GetWaypoints()
		for i, waypoint in ipairs(waypoints) do
			if waypoint.Action == Enum.PathWaypointAction.Jump then
				humanoid.Jump = true
			end

			humanoid:MoveTo(waypoint.Position)
			local reached = humanoid.MoveToFinished:Wait(2) -- 2 second timeout per waypoint

			if not reached then
				-- Possibly stuck, break to recompute path next loop
				break
			end
		end
	else
		warn(`Pathfinding failed for {Ai.Name} to {target.Name}: {err or path.Status}`)
	end

	-- Update stuck detection
	stuck = (primPart.Position - lastPosition).Magnitude < 1
	lastPosition = primPart.Position
end

-- Damage and temporary speed boost
local attackCount = 0

local function AttemptAttack(target: Model)
	local targetHumanoid = target:FindFirstChildWhichIsA("Humanoid")
	if not targetHumanoid or target:FindFirstChildWhichIsA("ForceField") then return end

	targetHumanoid:TakeDamage(Settings.damage)

	-- Increase attack count and reduce WalkSpeed accordingly (max 3 hits / 30 reduction)
	attackCount = math.clamp(attackCount + 1, 0, 3)
	local reduction = attackCount * 1
	humanoid.WalkSpeed = math.max(Settings.minSpeed, Settings.maxSpeed - reduction)

	-- Tell client to pause its WalkSpeed control
	local player = Players:GetPlayerFromCharacter(target)
	if player then
		UpdateSpeed_Event:FireClient(player, false, true) -- pause speed + trigger phase increase
	end

	if targetHumanoid.Health > 0 then
		local boost = math.floor((targetHumanoid.MaxHealth - targetHumanoid.Health) / 8 + 8)
		targetHumanoid.WalkSpeed += boost

		local ff = Instance.new("ForceField")
		ff.Visible = false
		ff.Parent = target

		ff.Destroying:Once(function()
			targetHumanoid.WalkSpeed -= boost

			-- Tell client to resume WalkSpeed control after forcefield ends
			if player then
				UpdateSpeed_Event:FireClient(player, true, false) -- resume speed, no phase increase
			end
		end)

		Debris:AddItem(ff, 10)
	end
end

-- Update obstruction constantly
RunService.Stepped:Connect(function()
	local target = GetClosestTarget(Settings.range)
	obstructed = CheckObstruction(target)
end)

-- Reroute periodically in case of stuck
task.spawn(function()
	while true do
		task.wait(0.05)
		if stuck then
			-- Trigger a re-follow by breaking current path and restarting
			local target, _ = GetClosestTarget(Settings.range)
			if target then
				Follow(target)
			end
		end
	end
end)

-- Speed control based on chase state
task.spawn(function()
	while task.wait() do
		if chasing then
			if humanoid.WalkSpeed < Settings.maxSpeed then
				humanoid.WalkSpeed = math.min(humanoid.WalkSpeed + Settings.speedIncreaseRate, Settings.maxSpeed)
			end
		else
			if humanoid.WalkSpeed > Settings.minSpeed then
				humanoid.WalkSpeed = math.max(humanoid.WalkSpeed - Settings.speedIncreaseRate, Settings.minSpeed)
			end
		end
	end
end)

-- Main AI Loop
task.spawn(function()
	while task.wait(0.2) do -- check 5 times per second for responsiveness
		local target, distance = GetClosestTarget(Settings.range)
		if target then
			chasing = distance <= Settings.chaseRange
			Follow(target)
			if distance <= Settings.attackRange then
				AttemptAttack(target)
			end
		else
			chasing = false
			humanoid:MoveTo(primPart.Position) -- stop moving when no target
		end
	end
end)

I do apologies if the code is unorganised.

Update your GetClosestTarget() function to ensure the character has fully loaded before it’s considered a valid target.

Add a check like this:

if character and character.PrimaryPart and character:FindFirstChildOfClass("Humanoid") and character:FindFirstChild("HumanoidRootPart") then
And optionally, delay the AI's targeting logic at startup for a second or two to let new players load:
task.delay(2, function()
    -- Begin AI loop or enable targeting here
end)

Should I create a video of the issue?

You can also

A. Increase the Wait Timeout Dynamically
Try making the timeout proportional to distance:

local distanceToWaypoint = (waypoint.Position - primPart.Position).Magnitude
local moveTimeout = math.clamp(distanceToWaypoint / humanoid.WalkSpeed, 1, 4)

local reached = humanoid.MoveToFinished:Wait(moveTimeout)

B. Recompute Path More Aggressively If Stuck
You already have a loop for when the AI is stuck, but it’s not reliable if the path is invalid or blocked mid-chase. You can adjust the main AI loop to check this too:

Update the Follow() function like this:

if not reached or stuck then
    break -- Recompute next loop
end

And log when it’s stuck for debugging:

if stuck then
    warn(Ai.Name .. " might be stuck at " .. tostring(primPart.Position))
end


@STOOK_X

local Players = game:GetService("Players")
local Pathfinding = game:GetService("PathfindingService")
local RunService = game:GetService("RunService")
local Debris = game:GetService("Debris")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Ai = script.Parent
local primPart = Ai.PrimaryPart
local humanoid = Ai:WaitForChild("Ai")
local Settings = require(Ai:WaitForChild("Settings"))

local UpdateSpeed_Event = ReplicatedStorage:WaitForChild("UpdateSpeed")

-- Disable client network ownership
for _, part in ipairs(Ai:GetDescendants()) do
	if part:IsA("BasePart") then
		part:SetNetworkOwner(nil)
	end
end

-- Initialization
humanoid.WalkSpeed = Settings.minSpeed
local lastPosition = primPart.Position
local stuck = false
local obstructed = true
local chasing = false

local path = Pathfinding:CreatePath({
	AgentCanClimb = false,
	Costs = { Climb = 0 }
})

local function GetClosestTarget(maxDistance)
	local closest = nil
	local shortestDistance = maxDistance

	local forwardVector = primPart.CFrame.LookVector
	local fovAngle = math.rad(Settings.range)
	local blindSpotRange = Settings.nearRange

	for _, player in ipairs(Players:GetPlayers()) do
		local character = player.Character
		if character and character.PrimaryPart then
			local root = character.PrimaryPart
			local humanoid = character:FindFirstChildOfClass("Humanoid")
			if character and character.PrimaryPart and character:FindFirstChildOfClass("Humanoid") and character:FindFirstChild("HumanoidRootPart") then

			if humanoid and humanoid.Health > 0 then
				local directionToPlayer = (root.Position - primPart.Position)
				local dist = directionToPlayer.Magnitude

				if dist <= maxDistance then
					local inBlindSpotRange = dist <= blindSpotRange
					local angle = math.acos(forwardVector:Dot(directionToPlayer.Unit))

					if inBlindSpotRange or angle <= fovAngle then
						if dist < shortestDistance then
							shortestDistance = dist
							closest = character
						end
					end
				end
			end
		end
	end

	return closest, shortestDistance
	end
end

-- Check if there's something between AI and target
local function CheckObstruction(target)
	if not target or not target.PrimaryPart then return true end

	local rayParams = RaycastParams.new()
	rayParams.FilterDescendantsInstances = { Ai, target }
	rayParams.FilterType = Enum.RaycastFilterType.Exclude

	local direction = target.PrimaryPart.Position - primPart.Position
	local result = workspace:Raycast(primPart.Position, direction, rayParams)

	return result ~= nil
end

local function Follow(target)
	if not target.PrimaryPart then return end

	local destination = target.PrimaryPart.Position

	local success, err = pcall(function()
		path:ComputeAsync(primPart.Position, destination)
	end)

	if success and path.Status == Enum.PathStatus.Success then
		local waypoints = path:GetWaypoints()
		for i, waypoint in ipairs(waypoints) do
			if waypoint.Action == Enum.PathWaypointAction.Jump then
				humanoid.Jump = true
			end
			
			local distanceToWaypoint = (waypoint.Position - primPart.Position).Magnitude
			local moveTimeout = math.clamp(distanceToWaypoint / humanoid.WalkSpeed, 1, 4)

			humanoid:MoveTo(waypoint.Position)
			local reached = humanoid.MoveToFinished:Wait(moveTimeout)

			if not reached or stuck then
				break -- Recompute next loop
			end
		end
	else
		warn(`Pathfinding failed for {Ai.Name} to {target.Name}: {err or path.Status}`)
	end

	-- Update stuck detection
	stuck = (primPart.Position - lastPosition).Magnitude < 1
	lastPosition = primPart.Position
end

-- Damage and temporary speed boost
local attackCount = 0

local function AttemptAttack(target: Model)
	local targetHumanoid = target:FindFirstChildWhichIsA("Humanoid")
	if not targetHumanoid or target:FindFirstChildWhichIsA("ForceField") then return end

	targetHumanoid:TakeDamage(Settings.damage)

	-- Increase attack count and reduce WalkSpeed accordingly (max 3 hits / 30 reduction)
	attackCount = math.clamp(attackCount + 1, 0, 3)
	local reduction = attackCount * 1
	humanoid.WalkSpeed = math.max(Settings.minSpeed, Settings.maxSpeed - reduction)

	-- Tell client to pause its WalkSpeed control
	local player = Players:GetPlayerFromCharacter(target)
	if player then
		UpdateSpeed_Event:FireClient(player, false, true) -- pause speed + trigger phase increase
	end

	if targetHumanoid.Health > 0 then
		local boost = math.floor((targetHumanoid.MaxHealth - targetHumanoid.Health) / 8 + 8)
		targetHumanoid.WalkSpeed += boost

		local ff = Instance.new("ForceField")
		ff.Visible = false
		ff.Parent = target

		ff.Destroying:Once(function()
			targetHumanoid.WalkSpeed -= boost

			-- Tell client to resume WalkSpeed control after forcefield ends
			if player then
				UpdateSpeed_Event:FireClient(player, true, false) -- resume speed, no phase increase
			end
		end)

		Debris:AddItem(ff, 10)
	end
end

-- Update obstruction constantly
RunService.Stepped:Connect(function()
	local target = GetClosestTarget(Settings.range)
	obstructed = CheckObstruction(target)
end)

-- Reroute periodically in case of stuck
task.spawn(function()
	while true do
		task.wait(0.05)
		if stuck then
			warn(Ai.Name .. " might be stuck at " .. tostring(primPart.Position))
			
			local target, _ = GetClosestTarget(Settings.range)
			if target then
				Follow(target)
			end
		end
	end
end)

-- Speed control based on chase state
task.spawn(function()
	while task.wait() do
		if chasing then
			if humanoid.WalkSpeed < Settings.maxSpeed then
				humanoid.WalkSpeed = math.min(humanoid.WalkSpeed + Settings.speedIncreaseRate, Settings.maxSpeed)
			end
		else
			if humanoid.WalkSpeed > Settings.minSpeed then
				humanoid.WalkSpeed = math.max(humanoid.WalkSpeed - Settings.speedIncreaseRate, Settings.minSpeed)
			end
		end
	end
end)

-- Main AI Loop
task.delay(2, function()
	while task.wait(0.2) do -- check 5 times per second for responsiveness
		local target, distance = GetClosestTarget(Settings.range)
		if target then
			chasing = distance <= Settings.chaseRange
			Follow(target)
			if distance <= Settings.attackRange then
				AttemptAttack(target)
			end
		else
			chasing = false
			humanoid:MoveTo(primPart.Position) -- stop moving when no target
		end
	end
end)

I fixed the issue!
The issue occurred in this line:

	while task.wait(0.2) do -- check 5 times per second for responsiveness

I changed the line to

	while task.wait() do -- check 5 times per second for responsiveness

This means the Ai has no breaks while chasing the player

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.