How do you make an NPC wander/attack in a specific area?

So I have an enemy AI script that works pretty good, it can wander when I’m not near and it attacks when I get closer. Although, it can still wander or follow a player all around the map when it needs to stay in its designated area, so how would I go about letting an NPC only wander and attack in a certain area and so that they don’t follow me out of the area?

Here is the script I am using(I got it from a YT tutorial because I’m bad at AI :c)

local myHuman = script.Parent:WaitForChild("Humanoid")
local myRoot = script.Parent:WaitForChild("HumanoidRootPart")
local head = script.Parent:WaitForChild("Head")

local grab = script.Parent:WaitForChild("Grab")
local grabAnim = myHuman:LoadAnimation(grab)
local walkAnim = script.Parent.Humanoid.walkani
local loadWalk = myHuman:LoadAnimation(walkAnim)
grabAnim.Priority = Enum.AnimationPriority.Action

--local grabSound = head:WaitForChild("Attack")

local clone = script.Parent:Clone()

function walkRandomly()
	local xRand = math.random(-50,50)
	local zRand = math.random(-50,50)
	local goal = myRoot.Position + Vector3.new(xRand,0,zRand)
	
	local path = game:GetService("PathfindingService"):CreatePath()
	path:ComputeAsync(myRoot.Position, goal)
	local waypoints = path:GetWaypoints()
	
	if path.Status == Enum.PathStatus.Success then
		for _, waypoint in ipairs(waypoints) do
			if waypoint.Action == Enum.PathWaypointAction.Jump then
				myHuman.Jump = true
			end
			myHuman:MoveTo(waypoint.Position)
			local timeOut = myHuman.MoveToFinished:Wait(1)
			if not timeOut then
				print("Stuck")
				myHuman.Jump = true
				wait(0.1)
				print("trying to get unstuck")
				walkRandomly()
			end
		end
	else
		print("Path failed")
		wait(1)
		walkRandomly()
	end
end

function findPath(target)
	local path = game:GetService("PathfindingService"):CreatePath()
	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
				myHuman.Jump = true
			end
			myHuman:MoveTo(waypoint.Position)
			local timeOut = myHuman.MoveToFinished:Wait(1)
			if not timeOut then
				myHuman.Jump = true
				print("path too long")
				findPath(target)
				break
			end
			if checkSight(target) then
				repeat
					print("moving directly to target")
					myHuman:MoveTo(target.Position)
					attack(target)
					wait(0.1)
					if target == nil then
						break
					elseif target.Parent == nil then
						break
					end
				until checkSight(target) == false or myHuman.Health < 1 or target.Parent.Humanoid.Health < 1
				break
			end
			if (myRoot.Position - waypoints[1].Position).magnitude > 20 then
				print("target moved, new path")
				findPath(target)
				break
			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
			print("i can see you bruh")
			return true
		end
	end
	return false
end

function findTarget()
	local dist = 50
	local target = nil
	local potentialTargets = {}
	local seeTargets = {}
	for i,v in ipairs(workspace:GetChildren()) do
		local human = v:FindFirstChild("Humanoid")
		local torso = v:FindFirstChild("Torso") or v:FindFirstChild("HumanoidRootPart")
		if human and torso and v.Name ~= script.Parent.Name then
			if (myRoot.Position - torso.Position).magnitude < dist and human.Health > 0 then
				table.insert(potentialTargets,torso)
			end
		end
	end
	if #potentialTargets > 0 then
		for i,v in ipairs(potentialTargets) do
			if checkSight(v) then	
				table.insert(seeTargets, v)
			elseif #seeTargets == 0 and (myRoot.Position - v.Position).magnitude < dist then
				target = v 
				dist = (myRoot.Position - v.Position).magnitude
			end
		end
	end
	if #seeTargets > 0 then
		dist = 50
		for i,v in ipairs(seeTargets) do
			if (myRoot.Position - v.Position).magnitude < dist then
				target = v 
				dist = (myRoot.Position - v.Position).magnitude
			end
		end
	end
	return target
end

function died()
	wait(5)
	clone.Parent = workspace
	game:GetService("Debris"):AddItem(script.Parent,0.1)
end

myHuman.Died:Connect(died)

function attack(target)
	if (myRoot.Position - target.Position).magnitude < 7 then
		grabAnim:Play()
		--grabSound:Play()
		if target.Parent ~= nil then
			target.Parent.Humanoid:TakeDamage(10)
		end
		wait(1.25)
	end
end

function main()
	local target = findTarget() 
	if target then
		myHuman.WalkSpeed = 15
		findPath(target)
	else
		myHuman.WalkSpeed = 8
		walkRandomly()
	end
end

while wait(0.1) do
	if myHuman.Health < 1 then
		break
	end
	main() 
end

Thank you for your help!

2 Likes

You could save a central position for the mob’s follow area radius and check for the magnitude of the distance between the central position and the position the mob is trying to walk to. If the magnitude is greater than your desired area length tell the mob to stop moving.

I want the NPCs to be able to spawn in random spots in the area once they die though.

Is the area they are supposed to randomly spawn in dynamic or more like an arena?

If it’s a fixed area, a brute force approach could be adding many invisible parts grouped together under a parent, then getting the children of spawn points from said parent and picking one at random.

Otherwise, one approach could be to define the positions of the area’s boundaries, then randomly spawn an enemy within the space between those positions.

They would be more like what you said how they could spawn from invisible parts. I would have the spawn points on the edge of the boundary I want so they appear to be coming into the “arena”, as long as they don’t spawn too many, I might need to have a 3 or 4 enemy limit.

But I still don’t know how to do the boundary thing

Anyone have advice on how I could do this?
Bump