My MoveTo module keeps stuttering / teleporting

For context before people start getting mad at me for not using pathfinding, I used moveto instead for these 2 reasons:

  1. I’m still rather new to scripting, I want to start simple
  2. I’d love to learn pathfinding but this is a quick project and it doesn’t really require npcs to pathfind (I’ll implement to learn it in the future since I’m a bit busy learning guis, backend scripting, datastores and everything at the same time)

Now on to the issue,
Streamable link of the issue, skip a bit forward

As you can see, while the NPC moves towards me it starts teleporting / stuttering whatever you want to call it. This is my first ever NPC so I have no idea why it’s doing this
Modulescript for managing movement

local EnemyNPCModule = {}

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local npcConnections = {}
local npcStunStates = {}  -- Table to keep track of stun states for each NPC

-- Function to get the nearest player within aggro range
function EnemyNPCModule.getNearestPlayerWithinAggroRange(npc, aggroRange)
	local myRoot = npc:FindFirstChild("HumanoidRootPart")
	if not myRoot then return nil end

	local nearestPlayer = nil
	local shortestDistance = aggroRange

	for _, player in pairs(Players:GetPlayers()) do
		if player.Character and player.Character:FindFirstChild("HumanoidRootPart") then
			local playerRoot = player.Character.HumanoidRootPart
			local distance = (playerRoot.Position - myRoot.Position).magnitude
			if distance < shortestDistance then
				shortestDistance = distance
				nearestPlayer = player
			end
		end
	end

	return nearestPlayer
end

-- Function to start chasing the player
function EnemyNPCModule.StartChase(npc, aggroRange, minDistance)
	local myHuman = npc:FindFirstChild("Humanoid")
	local myRoot = npc:FindFirstChild("HumanoidRootPart")
	if not myHuman or not myRoot then return end

	local isChasing = false

	local connection
	connection = RunService.Heartbeat:Connect(function()
		-- Check if NPC is stunned
		if npcStunStates[npc] and npcStunStates[npc].isStunned then
			-- Stop movement if stunned
			if isChasing then
				myHuman:MoveTo(myRoot.Position)  -- Stop moving
				isChasing = false
			end
			return
		end

		local targetPlayer = EnemyNPCModule.getNearestPlayerWithinAggroRange(npc, aggroRange)
		if targetPlayer and targetPlayer.Character and targetPlayer.Character:FindFirstChild("HumanoidRootPart") then
			local targetRoot = targetPlayer.Character.HumanoidRootPart
			local distance = (targetRoot.Position - myRoot.Position).magnitude
			if distance > minDistance then
				myHuman:MoveTo(targetRoot.Position)
				isChasing = true
			else
				if isChasing then
					myHuman:MoveTo(myRoot.Position)  -- Stop moving if within the minimum distance
					isChasing = false
				end
			end
		else
			if isChasing then
				myHuman:MoveTo(myRoot.Position)  -- Stop moving if no player is in aggro range
				isChasing = false
			end
		end
	end)

	npcConnections[npc] = connection
end

-- Function to stop chasing
function EnemyNPCModule.StopChase(npc)
	local connection = npcConnections[npc]
	if connection then
		connection:Disconnect()
		npcConnections[npc] = nil
	end
end

-- Function to set stun state for an NPC
function EnemyNPCModule.setStunState(npc, isStunned)
	npcStunStates[npc] = { isStunned = isStunned }
end

return EnemyNPCModule

Serverscript inside the NPC (Only the parts I think might be causing the error)

local function performMove()
	local now = tick()
	local targetPlayer = EnemyNPCModule.getNearestPlayerWithinAggroRange(npc, aggroRange)

	if targetPlayer and targetPlayer.Character and targetPlayer.Character:FindFirstChild("HumanoidRootPart") then
		local myRoot = npc:FindFirstChild("HumanoidRootPart")
		local targetRoot = targetPlayer.Character.HumanoidRootPart

		if not myRoot or not targetRoot then return end

		local distanceToPlayer = (targetRoot.Position - myRoot.Position).magnitude

		-- Face the target player
		myRoot.CFrame = CFrame.lookAt(myRoot.Position, targetRoot.Position)

		local moveToPerform = nil

		-- Choose a move based on cooldowns and distance
		for moveName, moveData in pairs(moves) do
			if moveData and now - moveData.lastUsed >= moveData.cooldown and distanceToPlayer <= moveData.distance then
				moveToPerform = moveName
				break
			end
		end

		if moveToPerform then
			if not moves[moveToPerform].canMoveWhileUsing then
				isStunned = true
				stunEndTime = now + moves[moveToPerform].stunTime
				EnemyNPCModule.setStunState(npc, true)  -- Set stun state in module
				print(npc.Name .. " is stopping movement to use " .. moveToPerform)
			end

			-- Call the corresponding move function
			if moveToPerform == "Move1" then
				Move1()
			elseif moveToPerform == "Move2" then
				Move2()
			end

			-- Update the lastUsed time for the move that was performed
			moves[moveToPerform].lastUsed = now
		end
	end
end
-- Function to handle NPC movement
local function handleMovement()
	if isStunned then
		if tick() > stunEndTime then
			isStunned = false
			EnemyNPCModule.setStunState(npc, false)  -- Reset stun state in module
			print(npc.Name .. " is resuming movement")
		else
			return  -- Exit early if stunned
		end
	end

	-- Movement logic here
	if not isStunned then
		EnemyNPCModule.StartChase(npc, aggroRange, minDistance)
	end
end
-- Main loop to perform moves and handle movement
while true do
	handleMovement()
	performMove()
	wait(1)  -- Wait for 1 second between each check, adjust as needed
end

Yes I know I used some horrible methods to implement this but it’s just a learning process for me :sob:
Any advice is appreciated, if I need to provide the full script I can.

while true do
	handleMovement()
	performMove()
	wait(1)  -- Wait for 1 second between each check, adjust as needed
end

I know for a fact it’s something to do with this line because when I take the handlemovement() out of the loop and put it in a seperate while true it works fine. I have no idea how to fix the main issue tho just found the root cause

Maybe because :MoveTo() can’t be runned in a “HeartBeat”. Try this:

local connection
	connection = function()
while task.wait(.1) do
-- Check if NPC is stunned
		if npcStunStates[npc] and npcStunStates[npc].isStunned then
			-- Stop movement if stunned
			if isChasing then
				myHuman:MoveTo(myRoot.Position)  -- Stop moving
				isChasing = false
			end
			return
		end

		local targetPlayer = EnemyNPCModule.getNearestPlayerWithinAggroRange(npc, aggroRange)
		if targetPlayer and targetPlayer.Character and targetPlayer.Character:FindFirstChild("HumanoidRootPart") then
			local targetRoot = targetPlayer.Character.HumanoidRootPart
			local distance = (targetRoot.Position - myRoot.Position).magnitude
			if distance > minDistance then
				myHuman:MoveTo(targetRoot.Position)
				isChasing = true
			else
				if isChasing then
					myHuman:MoveTo(myRoot.Position)  -- Stop moving if within the minimum distance
					isChasing = false
				end
			end
		else
			if isChasing then
				myHuman:MoveTo(myRoot.Position)  -- Stop moving if no player is in aggro range
				isChasing = false
			end
		end
	end)
end
		end
1 Like

nope still stuttering forwards :C

Try this:

while task.wait(1) do
	handleMovement()
	performMove()
end

If that doesn’t work, try this:


local debounceConnectionWait1 = false

RunService.RenderStepped:Connect(function()
if not debounceConnectionWait1 then
debounceConnectionWait1 = true
handleMovement()
task.wait(.1)
	performMove()
task.wait(1)
debounceConnectionWait1 = false
end
end)

All 3 of these didn’t work. the

while task.wait(1) do
	handleMovement()
	performMove()
end

made the delay slower. I think I know why it is happening. The performMove() function is a function I have made to check if the npc is in aggro distance of something and can attack it. Maybe running it in the same loop is the same issue?

-- Function to choose and perform a move
local function performMove()
	local now = tick()
	local targetPlayer = EnemyNPCModule.getNearestPlayerWithinAggroRange(npc, aggroRange)

	if targetPlayer and targetPlayer.Character and targetPlayer.Character:FindFirstChild("HumanoidRootPart") then
		local myRoot = npc:FindFirstChild("HumanoidRootPart")
		local targetRoot = targetPlayer.Character.HumanoidRootPart

		if not myRoot or not targetRoot then return end

		local distanceToPlayer = (targetRoot.Position - myRoot.Position).magnitude

		-- Face the target player
		myRoot.CFrame = CFrame.lookAt(myRoot.Position, targetRoot.Position)

		local moveToPerform = nil

		-- Choose a move based on cooldowns and distance
		for moveName, moveData in pairs(moves) do
			if moveData and now - moveData.lastUsed >= moveData.cooldown and distanceToPlayer <= moveData.distance then
				moveToPerform = moveName
				break
			end
		end

		if moveToPerform then
			if not moves[moveToPerform].canMoveWhileUsing then
				isStunned = true
				stunEndTime = now + moves[moveToPerform].stunTime
				EnemyNPCModule.setStunState(npc, true)  -- Set stun state in module
				print(npc.Name .. " is stopping movement to use " .. moveToPerform)
			end

			-- Call the corresponding move function
			if moveToPerform == "Move1" then
				Move1()
			elseif moveToPerform == "Move2" then
				Move2()
			end

			-- Update the lastUsed time for the move that was performed
			moves[moveToPerform].lastUsed = now
		end
	end
end

issue is with the current way i structured this I have no idea how I’m going to get it out of this loop and still work

Try seperating handleMovement with performMove functions:

task.spawn(function()
    while true do
        task.wait(1)
            handleMovement()
    end
end)

task.spawn(function()
    while true do
        task.wait(1)
            performMove()
    end
end)

That looks like network ownership lag. Basically when an unanchored basepart goes near a player, its ownership will be assigned to the player. This applies to NPCs as well and it’s what you’re experiencing. Try setting the ownership of the primarypart (the HumanoidRootPart) of the npc to the server (nil).

npc.PrimaryPart:SetNetworkOwner(nil);
OR
npc.HumanoidRootPart:SetNetworkOwner(nil);

You can also set the ownership of every basepart of the NPC, in case only the HRP doesn’t work:

for _, descendant in ipairs(npc:GetDescendants()) do
   if descendant:IsA("BasePart") == false then continue end;
   descendant:SetNetworkOwner(nil);
end;

Doesn’t seem to fix it. I should clarify again that the

while task.wait(1) do
	handleMovement() --ThisPart handles the movement of the npc
	performMove() - This part handles the npc using attacks, and aggroing to me
end

When I remove performMove() from that loop it works fine (but breaks the rest of the script because the npc no longer uses attacks of course)
It think the issue might be related to it running the entire performMove() function then redoing the loop.

doesn’t fix it, now the npc has nostun haha and the stuterring still occurs

Guys I fixxed it, asked someone on discord to help me and it turns out this was the issue

yapping? you literally said your self you though the problem was in a certain part. Alright then. We tried helping and now is helping the issue? Sorry, like I said, you told us the part you though was wrong was the waiting or the loop

I’m not pressed but it’s a hate people saying these things like we are useless or like we didn’t try to help at all

myRoot.CFrame = CFrame.lookAt(myRoot.Position, targetRoot.Position)
And what’s the difference in the image with this?

it was inside the wrong loop so it would keep running that line every X amount of time, putting the movement on hold

bro that’s not me i’m Vikramidy :sob: i cant control what others say
I’m genuinely thankful for everyone that attempted helping me

I’m still correct tho even tho it wasn’t you.

Ok, but you mentioned you though of the loop. Not the whole script. But yes we should’ve been attentive about it

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