How to slow player speed when close to an npc or enemy

You can write your topic however you want, but you need to answer these questions:

  1. What do you want to achieve? Keep it simple and clear!
    I want to make an NPC that can slow down player movement when close
  2. What is the issue? Include screenshots / videos if possible!
    I don’t know how to implement it since my NPC walk randomly and has myHuman.MoveToFinished:Wait() this will break the loop or check if close to the player

here is my code

local function findtarget()
	local maxCharacterRadius = 15  
	local smallestDistance, target = math.huge, nil 
	local getplayers = game:GetService("Players")
	local Players = {

	}
	for k,player in ipairs(getplayers:GetChildren()) do

		local character = player.Character
		table.insert(Players, character)
	end
	for i, v in ipairs(Players) do
		local humanoidRootPart = v:FindFirstChild('HumanoidRootPart')
		if not humanoidRootPart then continue end  
		local currentMagnitude = (myRoot.Position - humanoidRootPart.Position).Magnitude
		if (currentMagnitude < smallestDistance) and (currentMagnitude < maxCharacterRadius) then
			smallestDistance, target = currentMagnitude, v
		end
	end
	return target

end

function walkRandomly()
	local xoff = math.random(5, 10)
	if math.random() > .5 then
		xoff = xoff * -1
	end
	local zoff = math.random(5, 10)
	if math.random() > .5 then
		zoff = zoff * -1
	end
	local goal = Vector3.new(bot.Torso.Position.X + xoff,bot.Torso.Position.Y,bot.Torso.Position.Z + zoff)
	findPath:ComputeAsync(myRoot.Position, goal)
	local waypoints = findPath:GetWaypoints()
	if findPath.Status == Enum.PathStatus.Success then
		for _, waypoint in ipairs(waypoints) do
			
			-- this is not the best method so that why I need help
			--because we need to wait for the bot to reach the next waypoint (myHuman.MoveToFinished:Wait()) before scan the nearest target
			--its will take more than 2-4 seconds because my bot walkspeed is 3
			local target = findtarget()
			if target  then
				target.Humanoid.WalkSpeed = 3
			else
				--will be error because target is nil
				--Idk how to reset player speed after player is outside the range
				target.Humanoid.WalkSpeed = 13
			end

			myHuman:MoveTo(waypoint.Position)
			myHuman.MoveToFinished:Wait()

		end
	else
		wait(1)
		walkRandomly()
	end
end

while true do
	walkRandomly()
	wait(3)
end

2 Likes

Try changing the Speed value in the function up there so if the magnitude is greater it returns to normal speed value, or use return at the end of the function if the player is close or far from the npc and if the value is positive it gives slow , negative return to normal speed.

3 Likes

Here’s a solution that uses your current approach:

local Players = game:GetService("Players")

local FIND_TARGET_RANGE = 15  
local TARGET_SLOWED_WALKSPEED = 3

local currentTargetDefaultWalkspeed
local currentTarget

function findNearestCandidate()
	local candidates = {}
	for _, player in ipairs(Players:GetPlayers()) do
		table.insert(candidates, player.Character)
	end

	local smallestDistance, target = math.huge, nil 
	for _, candidate in ipairs(candidates) do
		local humanoidRootPart = candidate:FindFirstChild('HumanoidRootPart')
		if not humanoidRootPart then continue end

		local distance = (myRoot.Position - humanoidRootPart.Position).Magnitude
		if (distance < smallestDistance) and (distance < FIND_TARGET_RANGE) then
			smallestDistance, target = distance, candidate
		end
	end

	return target
end

function randomSign()
	return math.random() < 0.5 and -1 or 1
end

function unsetTarget()
	assert(currentTarget ~= nil)

	local humanoid = currentTarget:FindFirstChild("Humanoid")
	if humanoid then
		humanoid.WalkSpeed = currentTargetDefaultWalkspeed
		currentTargetDefaultWalkspeed = nil
	else
		warn("Figure out how to deal with this!")
	end

	currentTarget = nil
end

function setTarget(target)
	if target == currentTarget then
		--Otherwise currentTargetDefaultWalkspeed would be overwritten D:
		return 
	end
	
	if currentTarget then
		unsetTarget()
	end

	currentTarget = target

	local humanoid = currentTarget:FindFirstChild("Humanoid")
	if humanoid then
		currentTargetDefaultWalkspeed = humanoid.WalkSpeed
		humanoid.WalkSpeed = TARGET_SLOWED_WALKSPEED
	else
		warn("Figure out how to deal with this!")
	end
end

function walkRandomly()
	local offset = 
		Vector3.xAxis * math.random(5, 10) * randomSign() +
		Vector3.zAxis * math.random(5, 10) * randomSign() 
	local goal = bot.Torso.Position + offset

	--Not sure about this???
	findPath:ComputeAsync(myRoot.Position, goal)
	local waypoints = findPath:GetWaypoints()

	if findPath.Status == Enum.PathStatus.Success then
		for _, waypoint in ipairs(waypoints) do
			--This works even if the target-finding loop below isn't done when MoveToFinished fires
			local moveToFinished, c = false
			c = myHuman.MoveToFinished:Connect(function()
				moveToFinished = true
				c:Disconnect()
			end)

			myHuman:MoveTo(waypoint.Position)
			
			--Do target-finding lots of times between waypoints
			local checkPeriod = 1/2
			local timeToWaypoint = (waypoint.Position - myRoot.Position).Magnitude / myHuman.WalkSpeed
			timeToWaypoint *= 0.9 --Make it a biiit smaller because I often find that MoveToFinished fires a bit before actually reaching the waypoint
			for t = 0, timeToWaypoint, checkPeriod do
				local newTarget = findNearestCandidate()
				setTarget(newTarget) --Calls unsetTarget if currentTarget is not nil, and does nothing if newTarget is same as currentTarget
				
				task.wait(checkPeriod)
			end
			
			repeat task.wait() until moveToFinished
		end
	else
		task.wait(1)
		walkRandomly()
	end
end

while true do
	walkRandomly()
	task.wait(3)
end

I modified a few other things just while trying to understand your code, but you can ignore everything except the stuff in the waypoint-following loop and the setTarget and unsetTarget functions.

This approach works okay if you really want it so that the enemy only slows down players while the enemy itself is walking to a waypoint. If you want it to always slow down players, it’s better to use a less “loop based” approach than this.

There’s also the issue of applying/removing effects by just setting a variable. The setTarget/unsetTarget functions make sure that the “default” walkspeed is remembered so the target’s walkspeed can be reset when the enemy no longer applies it’s effect. But this doesn’t work if there’s anything else in the entire game that can modify the target’s walkspeed, including other identical enemies. This problem can be solved in a few ways, like with FSMs or PDAs. That’s a whole topic though, let me know if you’d like an example of how that can be applied here :wink:

3 Likes

thank you its works very well, and that’s very smart
yea I think so, I should not only scan the player when the NPC walking,
may I know what is that means by less “loop based”? should I use an event instead of a loop? for example a touch event maybe?

2 Likes

Hmm not necessarily, I’m not 100% sure what I meant actually xD

But right now there’s just a single loop running in the entire script. Since you want several things to happen at once, IMO it makes more sense to have several loops running, one for each thing. So I’d make one for walking and one for scanning for nearby players. E.g.

--Both loops run at the same time
task.spawn(function()
    while true do
        walkRandomly()
        task.wait(3)
    end
end)

task.spawn(function()
    while true do
        searchForTargets()
        task.wait(0.5)
    end
end)
1 Like

ouh true, I also thought that way, but doesn’t that gonna hurt the server? because I want to spawn multiple NPC, not just 1, bigger round = more NPC

1 Like

Hmm, I don’t know how having more threads doing less work each affects performance vs having one thread do all the work (assuming the total work is the same, ofc).

So far there’s very little going so I don’t think it would be an issue.

1 Like

all right, thank you so much for your help @ThanksRoBama , I learn something new

2 Likes