How to stop a NPC from moving once in attacking range?

Heya Everyone!!

I’m trying to find a way to stop a NPC from moving once in their attacking range and move once away from their attacking range. I tried finding posts but couldn’t find one aside from this one and tried checking the magnitude if its less or greater than the attack range to stop moving but didn’t work.

Honestly, I should’ve known how to do this already but this is kinda embarrassing?

--[[DEVELOPER NOTES]]--
--//Rough module script, I know.
--//This is fine but it can definitely be improved on some parts.

--[[SERVICES]]--
local ServerStorage = game:GetService("ServerStorage")
local PathfindingService = game:GetService("PathfindingService")
local Debris = game:GetService("Debris")

--[[FOLDERS]]--
local Entities_Folder = ServerStorage:WaitForChild("ENTITIES")
local Allies_Foldder = Entities_Folder:FindFirstChild("ALLIES")
local Enemies_Folder = Entities_Folder:FindFirstChild("ENEMIES")

--[[MODULES]]--
local BASE_ENTITY_MODULE = {}
BASE_ENTITY_MODULE.__index = BASE_ENTITY_MODULE

function BASE_ENTITY_MODULE.CreateEntity(EntityName, EntityTeam)
	--//Sorting out what team the entity is so we can find their folders.
	local SearchFolder = if EntityTeam == "Allies" then Allies_Foldder
		elseif EntityTeam == "Enemies" then Enemies_Folder
		else nil

	--//Checking to see if the entity DOES exist.
	local UnitModel = nil

	for _, FindUnit in pairs(SearchFolder:GetChildren()) do
		if FindUnit:IsA("Model") and FindUnit.Name == EntityName then
			UnitModel = FindUnit:Clone()
		elseif FindUnit:IsA("Model") and FindUnit.Name ~= EntityName then
			warn(EntityName.." doesn't exist!")
			return
		end
	end

	--//Creating the entity.
	local self = setmetatable({}, BASE_ENTITY_MODULE)

	self.Unit = UnitModel
	self.UnitHumanoid = self.Unit:FindFirstChildWhichIsA("Humanoid")
	self.UnitTeam = self.Unit:AddTag(EntityTeam)
	self.Unit.Parent = workspace

	--//Entity Settings
	self.EntitySettings = require(self.Unit.ENTITY_SETTINGS)
	self.EntityDamage = self.EntitySettings.ENTITY_DAMAGE
	self.EntityAttackRate = self.EntitySettings.ENTITY_ATTACK_RATE
	self.EntityAttackRange = self.EntitySettings.ENTITY_ATTACK_RANGE --Using this in order to detect if the NPC/entity is in attacking range.
	self.EntityResistance = self.EntitySettings.ENTITY_RESISTANCE

	task.spawn(function()
		game:GetService("RunService").Heartbeat:Connect(function()
			local Target = self:FindNearestTarget()

			if Target then
				self:ChaseTarget(Target)
			else
				self.UnitHumanoid:MoveTo(self.Unit.PrimaryPart.Position)
			end
		end)
	end)

	return self
end

function BASE_ENTITY_MODULE:FindNearestTarget()
	local Targets = {}
	local Nearest = math.huge
	local Target = nil

	for _, PotentialTargets in pairs(workspace:GetChildren()) do
		if PotentialTargets:IsA("Model") then
			local TargetHumanoid = PotentialTargets:FindFirstChildWhichIsA("Humanoid")
			if TargetHumanoid.Health <= 0 then --//Dead, obviously.
				return
			end

			local Distance = (PotentialTargets.PrimaryPart.Position - self.Unit.PrimaryPart.Position).Magnitude
			if Distance <= math.huge then				
				if (PotentialTargets:HasTag("Enemies") and self.Unit:HasTag("Allies")) or 
					(PotentialTargets:HasTag("Allies") and self.Unit:HasTag("Enemies")) then
					table.insert(Targets,{
						Magnitude = Distance,
						FoundTarget = PotentialTargets
					})
				end
			end

		end
	end

	for _, Entry in pairs(Targets) do
		if Entry.Magnitude <= Nearest then
			Nearest = Entry.Magntidue
			Target = Entry.FoundTarget
		end
	end

	return Target
end

function BASE_ENTITY_MODULE:ChaseTarget(Target)
	local NewPath = PathfindingService:CreatePath()
	NewPath:ComputeAsync(Target.PrimaryPart.Position, self.Unit.PrimaryPart.Position)
	if NewPath.Status == Enum.PathStatus.Success then
		local Waypoints = NewPath:GetWaypoints()

		for _, Waypoint in pairs(Waypoints) do
			self.UnitHumanoid:MoveTo(Waypoint.Position)
			local Timeout = self.UnitHumanoid.MoveToFinished:Wait(2)
			if not Timeout then
				self:ChaseTarget(Target)
			end
		end
	end
end

return BASE_ENTITY_MODULE

Conceptually that’s the correct way of doing it. I’m assuming your main update function is the callback inside the heartbeat? If there’s a valid target, I would check whether the NPC is within attack range, and if so try to attack. Otherwise, continue with chasing them. Something like the following:

if Target then
    local Distance = (self.Unit.PrimaryPart.Position - Target.PrimaryPart.Position).Magnitude
    if Distance < self.EntityAttackRange then
        -- Try to perform an attack
    else
        self:ChaseTarget(Target)
    end
else
    self.UnitHumanoid:MoveTo(self.Unit.PrimaryPart.Position)
end

My best guess as to why the NPC isn’t stopping is because inside ChaseTarget, you’re looping through all of the waypoints and waiting for the NPC to reach each one.

local Timeout = self.UnitHumanoid.MoveToFinished:Wait(2)

For example, if the NPC is chasing the target, it’s going to loop through each of the waypoints and only consider attacking once it reaches the final waypoint, which isn’t the behavior you want.

My suggestion for how to fix this behavior would be to change ChaseTarget to use a callback instead of yielding on the MoveTo event.

-- Something like this. Set it up however you like. 
-- The important thing here is we're using an event callback instead of yielding in the for loop
local Waypoints = NewPath:GetWaypoints()
if #Waypoints > 0 then
    local CurrentWaypointIndex = 1
    local NextWaypoint = Waypoints[CurrentWaypointIndex]

    if self.MoveToConnection == nil then
        local function HandleMoveToFinished()
            CurrentWaypointIndex++
            local NextWaypoint = Waypoints[CurrentWaypointIndex]
            if NextWaypoint ~= nil then
                self.UnitHumanoid:MoveTo(NextWaypoint.Position)
            end
        end
        
        -- This could probably be set during the initialization in CreateEntity in order
        -- to avoid the nil check above. Of course, you'd need to set up the callback function
        -- elsewhere as well
        self.MoveToConnection = self.UnitHumanoid.MoveToFinished:Connect(HandleMoveToFinished)
    end
    
    self.UnitHumanoid:MoveTo(NextWayPoint.Position)
end

Also, it’s important to note that if you switch ChaseTarget to be non-yielding like above, you’re likely going to want to prevent it from calculating a new path every single heartbeat. Before, it would only calculate a path when it completed it’s current. With the changes above, it would calculate a path every single heartbeat which could be expensive and unnecessary. But with this setup, the distance check would now also be performed every heartbeat, meaning right when the NPC is within attack range, it will do whatever you want it do (stop moving and perform attack).

1 Like

I probably should’ve worded it better since I was originally just focusing on detecting if the NPC is in range but I’m still glad you still mentioned it. I’ll take a look on the code and use it later. Anyway, I used your example and managed to make the NPC attack. Setting the NPC’s movement speed to 0 when attacking should work.