Need Help for Enemy NPC "AI"

Hello.

I have a question regarding enemy NPC “AI”. How can I go about creating a system that updates the player’s position every 2 seconds (or so). I know this seems like an amateur-ish question, but I’m still kinda starting out. Any help or guidance would be greatly appreciated.

P.S. The enemies are all melee enemies.

2 Likes

If you only want a system that updates the player’s pos every 2 seconds you can use a task.spawn() to make the script not pause while it’s looping and a while true loop to detect if players are nearby every 2 seconds.

local PlayerService = game:GetService("Players")

task.spawn(function()
	while true do
		for _,aPlayer in PlayerService:GetPlayers() do
			if aPlayer.Character then -- checks if player has a character
				local HumRoot = aPlayer.Character.HumanoidRootPart
				-- update position
			end
		end
		task.wait(2) -- waits 2 seconds
	end
end)

-- your other code

Looking at your profile you seem pretty new. I would suggest you not make an Ai system as your first project as it will get very complicated. You’re free to do whatever though.

And I would put this in the enemy model?

Not sure if you mean like ChatGpt for “Ai”, but Here’s a simple, Roblox-friendly way to “update the player’s position every ~2 seconds” on the server, and something tiny for melee enemies to use it.

1) Server tracker (drop in ServerScriptService)

  • Stores each character’s last position in a Character Attribute (LastPos) every ~2s.
  • Auto-handles respawns.
-- ServerScriptService/PlayerPositionTracker.server.lua
local Players = game:GetService("Players")

local function trackCharacter(char: Model)
	local hrp = char:WaitForChild("HumanoidRootPart")
	char:SetAttribute("LastPos", hrp.Position)

	task.spawn(function()
		while char.Parent and hrp.Parent do
			char:SetAttribute("LastPos", hrp.Position)
			task.wait(2 + math.random() * 0.5) -- small jitter to avoid spikes
		end
	end)
end

local function onPlayer(p: Player)
	if p.Character then trackCharacter(p.Character) end
	p.CharacterAdded:Connect(trackCharacter)
end

Players.PlayerAdded:Connect(onPlayer)
for _, p in ipairs(Players:GetPlayers()) do onPlayer(p) end

2) Using it in melee AI (tiny example)

  • Every ~2s, pick the nearest player by their LastPos (falls back to live HRP position if attribute isn’t set yet).
  • Recompute a path only on that cadence (keeps it cheap).
-- Script inside your Enemy model (server)
local PathfindingService = game:GetService("PathfindingService")
local enemy = script.Parent
local hum = enemy:WaitForChild("Humanoid")
local root = enemy:WaitForChild("HumanoidRootPart")

local SEARCH_RANGE = 120
local MELEE_RANGE = 4

while task.wait(2 + math.random() * 0.5) do
	local closestChar, closestPos, closestDist = nil, nil, math.huge
	for _, plr in ipairs(game:GetService("Players"):GetPlayers()) do
		local char = plr.Character
		local hrp = char and char:FindFirstChild("HumanoidRootPart")
		if hrp then
			local pos = char:GetAttribute("LastPos") or hrp.Position
			local d = (root.Position - pos).Magnitude
			if d < closestDist and d <= SEARCH_RANGE then
				closestChar, closestPos, closestDist = char, pos, d
			end
		end
	end

	if closestPos then
		if closestDist <= MELEE_RANGE then
			hum:MoveTo(root.Position) -- stop & swing
			-- play attack here
		else
			local path = PathfindingService:CreatePath()
			path:ComputeAsync(root.Position, closestPos)
			for _, wp in ipairs(path:GetWaypoints()) do
				hum:MoveTo(wp.Position)
				hum.MoveToFinished:Wait(0.5)
			end
		end
	end
end

That’s it: one server tracker updating every ~2 seconds, and enemies that read it on the same cadence. This avoids heavy per-frame work and is perfectly fine for melee AI.

if this works pls set this as the solution pls i neeeeedd ittt :sob:

I was able to repurpose a script that a friend of mine made for a different reason. Thanks for all the suggestions, though!

it would be amazing if you gave some sort of explanation on how the solution works, for anyone who has the same problem as you

This was the point that I left off on. It’s not perfect, but it’s closer to what I want.

local npc = workspace:WaitForChild("Rig")
local npcHumanoid = npc:WaitForChild("Humanoid")
local npcRoot = npc:WaitForChild("HumanoidRootPart")

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

-- Settings
local DETECTION_RADIUS = 50         -- How close the player must be for the monster to chase
local UPDATE_INTERVAL = 2           -- How often to update the path (seconds)
local CHECK_PLAYERS_INTERVAL = 0.5  -- How often to check for nearby players

local chasingAnimationId = "http://www.roblox.com/asset/?id=10921336997"

-- Load chasing animation once
local chasingAnimation = Instance.new("Animation")
chasingAnimation.AnimationId = chasingAnimationId
local animationTrack = npcHumanoid:LoadAnimation(chasingAnimation)

local chasingPlayer = nil
local lastUpdateTime = 0

-- Function to get closest player in range
local function getNearestPlayerInRange()
	local closestPlayer = nil
	local shortestDistance = DETECTION_RADIUS

	for _, player in pairs(Players:GetPlayers()) do
		local char = player.Character
		if char and char:FindFirstChild("HumanoidRootPart") then
			local distance = (npcRoot.Position - char.HumanoidRootPart.Position).Magnitude
			if distance <= shortestDistance then
				shortestDistance = distance
				closestPlayer = player
			end
		end
	end

	return closestPlayer
end

-- Function to move towards player using pathfinding
local function updatePathToPlayer(player)
	if not player.Character or not player.Character:FindFirstChild("HumanoidRootPart") then return end

	local playerRoot = player.Character.HumanoidRootPart
	local path = PathfindingService:CreatePath()
	path:ComputeAsync(npcRoot.Position, playerRoot.Position)

	if path.Status == Enum.PathStatus.Success then
		local waypoints = path:GetWaypoints()
		if #waypoints > 1 then
			npcHumanoid:MoveTo(waypoints[2].Position)
		elseif #waypoints == 1 then
			npcHumanoid:MoveTo(waypoints[1].Position)
		end
	end
end

-- Main loop
RunService.Heartbeat:Connect(function(dt)
	-- Look for player if not currently chasing
	if not chasingPlayer then
		chasingPlayer = getNearestPlayerInRange()
		if chasingPlayer then
			animationTrack:Play()
			lastUpdateTime = 0  -- Reset timer
		end
	else
		-- Check if player is still valid and in range
		local char = chasingPlayer.Character
		local playerRoot = char and char:FindFirstChild("HumanoidRootPart")
		if not char or not playerRoot or (npcRoot.Position - playerRoot.Position).Magnitude > DETECTION_RADIUS then
			chasingPlayer = nil
			animationTrack:Stop()
			npcHumanoid:MoveTo(npcRoot.Position)
			return
		end

		-- Update path every 2 seconds
		lastUpdateTime += dt
		if lastUpdateTime >= UPDATE_INTERVAL then
			lastUpdateTime = 0
			updatePathToPlayer(chasingPlayer)
		end
	end
end)
1 Like

I obviously still have work to do on it, but that’s what I got so far.

For anyone who’s interested, I got the script to work better

-- Inside of the enemy npc

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

local npc = script.Parent
local humanoid = npc:WaitForChild("Humanoid")
local hrp = npc:WaitForChild("HumanoidRootPart")

-- Function to get the closest player
local function getClosestPlayer()
	local closestPlayer = nil
	local shortestDistance = 100 -- # of studs the npc detects players

	for _, player in ipairs(Players:GetPlayers()) do
		local character = player.Character
		if character and character:FindFirstChild("HumanoidRootPart") then
			local distance = (character.HumanoidRootPart.Position - hrp.Position).Magnitude
			if distance < shortestDistance then
				shortestDistance = distance
				closestPlayer = character
			end
		end
	end

	return closestPlayer
end

-- Main loop
while true do
	task.wait(0.1)

	local target = getClosestPlayer()
	if not target then continue end

	local targetHRP = target:FindFirstChild("HumanoidRootPart")
	if not targetHRP then continue end

	-- Compute path
	local path = PathfindingService:CreatePath()
	path:ComputeAsync(hrp.Position, targetHRP.Position)

	if path.Status == Enum.PathStatus.Success then
		local waypoints = path:GetWaypoints(task.wait(1.5))

		for _, waypoint in ipairs(waypoints) do
			humanoid:MoveTo(waypoint.Position)
			local reached = humanoid.MoveToFinished:Connect(function()
				task.wait(0.1)
			end)
			if not reached then break end
		end
	else
		warn("Path failed to compute")
	end
end

1 Like

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