How can I improve my "NPC AI?

I wanted to create an NPC that follows the player with some configurations. Right now, it’s very basic and has some issues that I don’t know how to fix (it was working before, but I added some things that made it stop working):

Video (At the end, I tested changing the NPC’s speed to see if it could catch up to the player, but it still couldn’t)

  1. The character moves in a jittery way, as shown in the video

  2. It doesn’t follow the player directly but instead follows the exact path the player took. So, for example, if the player goes somewhere and then comes back, the AI will complete the entire path before returning

  3. Another issue I don’t know how to handle is freezing (Weeping Angel effect). I tried making the NPC freeze only when the player looks directly at it, but it shouldn’t freeze if the player is looking at it while behind a wall

  4. And, for some reason, the bot isn’t able to detect the player (the initial detection to start following) when the player is behind a wall, even though BlockPart is set to false. However, it does detect the player while already following them.

----- I already tried using Heartbeat (RunService), but it didn’t fix the issue of the NPC/monster moving in a jittery way and not being able to reach the player, even though its speed is higher than the player’s

local npc = script.Parent
local humanoid = npc:FindFirstChild("Humanoid")
local initialPosition = npc.PrimaryPart.Position
local randomWalkRadius = 100
local detectionRadius = 500
local followRadius = 1500
local WeepingAngel = false
local BlockPart = false
local pathfindingService = game:GetService("PathfindingService")
local randomMoveWaitTimeMin = 1
local randomMoveWaitTimeMax = 10

local isMovingRandomly = false
local isFollowingPlayer = false
local lastMoveTime = tick()

local agentParameters = {
	AgentRadius = 5, 
	AgentHeight = 5, 
	AgentCanJump = true, 
	AgentCanClimb = true, 
	AgentMaxSlopeAngle = 80, 
	MaterialCosts = {
		[Enum.Material.Concrete] = math.huge,
		[Enum.Material.Metal] = math.huge
	}
}

local function isPlayerLooking(player)
	if not WeepingAngel then return false end
	local playerRootPart = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
	if playerRootPart then
		local npcDirection = (npc.PrimaryPart.Position - playerRootPart.Position).Unit
		local playerLookDirection = playerRootPart.CFrame.LookVector
		local dotProduct = npcDirection:Dot(playerLookDirection)
		return dotProduct > 0.5
	end
	return false
end

local function setNPCAnchored(state)
	for _, part in ipairs(npc:GetDescendants()) do
		if part:IsA("BasePart") and part ~= npc.PrimaryPart then
			part.Anchored = state
		end
	end
end

local function isPlayerInDetectionRadius()
	for _, player in pairs(game.Players:GetPlayers()) do
		local playerRootPart = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
		if playerRootPart then
			local distance = (playerRootPart.Position - npc.PrimaryPart.Position).Magnitude
			if distance <= detectionRadius then
				return player
			end
		end
	end
	return nil
end

local function isPlayerBehindWall(player)
	if not BlockPart then return false end
	local playerRootPart = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
	if playerRootPart then
		local ray = Ray.new(npc.PrimaryPart.Position, (playerRootPart.Position - npc.PrimaryPart.Position).Unit * detectionRadius)
		local hit, position = game.Workspace:FindPartOnRay(ray, npc)
		if hit and hit.Transparency < 0.4 and hit ~= playerRootPart then
			return true
		end
	end
	return false
end

function moveRandomly()
	if isMovingRandomly then return end
	isMovingRandomly = true
	lastMoveTime = tick()

	local retries = 0
	local moved = false
	while not moved and retries < 5 and not isFollowingPlayer do
		local targetPosition = initialPosition + Vector3.new(
			math.random(-randomWalkRadius, randomWalkRadius),
			0,
			math.random(-randomWalkRadius, randomWalkRadius)
		)

		local path = pathfindingService:CreatePath(agentParameters)
		path:ComputeAsync(npc.PrimaryPart.Position, targetPosition)

		if path.Status == Enum.PathStatus.Success then
			local waypoints = path:GetWaypoints()
			if #waypoints > 0 then
				for _, waypoint in ipairs(waypoints) do
					if isFollowingPlayer then break end
					humanoid:MoveTo(waypoint.Position)
					if waypoint.Action == Enum.PathWaypointAction.Jump then
						humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
					end
					local precision = 5
					local dist = (npc.PrimaryPart.Position - waypoint.Position).Magnitude
					repeat
						task.wait()
						dist = (npc.PrimaryPart.Position - waypoint.Position).Magnitude
						lastMoveTime = tick()
					until dist <= precision or isFollowingPlayer
				end
				moved = true
			end
		else
			retries = retries + 1
		end
	end

	if not isFollowingPlayer and moved then
		local waitTime = math.random(randomMoveWaitTimeMin, randomMoveWaitTimeMax)
		local startTime = tick()
		while tick() - startTime < waitTime and not isFollowingPlayer do
			if isPlayerInDetectionRadius() then
				break
			end
			wait(0.1)
		end
	end

	isMovingRandomly = false
end

function followPlayer(player)
	if isFollowingPlayer then return end 
	isFollowingPlayer = true

	if not npc then
		warn("Erro: NPC não está definido!")
		isFollowingPlayer = false
		return
	end

	local humanoid = npc:FindFirstChildOfClass("Humanoid")
	if not humanoid then
		warn("Erro: Humanoid não encontrado!")
		isFollowingPlayer = false
		return
	end

	local playerRootPart = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
	if not playerRootPart then
		isFollowingPlayer = false
		return
	end

	local distanceToInitialPosition = (playerRootPart.Position - initialPosition).Magnitude
	if distanceToInitialPosition > followRadius then
		humanoid:MoveTo(initialPosition)
		isFollowingPlayer = false
		return
	end

	if BlockPart and isPlayerBehindWall(player) then
		moveRandomly()
		isFollowingPlayer = false
		return
	end

	local path = pathfindingService:CreatePath(agentParameters)
	path:ComputeAsync(npc.PrimaryPart.Position, playerRootPart.Position)

	if path.Status == Enum.PathStatus.Success then
		local waypoints = path:GetWaypoints()
		if #waypoints > 0 then
			local timeStopped = 0
			local stoppedThreshold = 1
			local precision = 5

			for _, waypoint in ipairs(waypoints) do
				if not isFollowingPlayer then break end
				if isPlayerLooking(player) then
					humanoid:MoveTo(npc.PrimaryPart.Position)
					setNPCAnchored(true)
					isFollowingPlayer = false
					return
				else
					setNPCAnchored(false)
				end

				humanoid:MoveTo(waypoint.Position)
				if waypoint.Action == Enum.PathWaypointAction.Jump then
					humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
				end

				local dist = (npc.PrimaryPart.Position - waypoint.Position).Magnitude
				while dist > precision do
					task.wait(0.05)
					dist = (npc.PrimaryPart.Position - waypoint.Position).Magnitude

					if npc.PrimaryPart.Velocity.Magnitude < 0.1 then
						timeStopped = timeStopped + 0.5
						if timeStopped >= stoppedThreshold then
							print("tentando dnv")
							moveRandomly()
							isFollowingPlayer = false
							return
						end
					else
						timeStopped = 0
					end
				end
			end
		end
	else
		moveRandomly()
	end

	isFollowingPlayer = false
end

function npcBehavior()
	while npc and npc.Parent and humanoid and humanoid.Health > 0 do
		local closestPlayer = isPlayerInDetectionRadius()

		if closestPlayer then
			if isPlayerLooking(closestPlayer) then
				setNPCAnchored(true)
				humanoid:MoveTo(npc.PrimaryPart.Position)
			else
				setNPCAnchored(false)
				followPlayer(closestPlayer)
			end
		else
			setNPCAnchored(false)
			moveRandomly()
		end

		task.wait(0.1)
	end
end

npcBehavior()

AgentMaxSlopeAngle does not exist.

for _, waypoint in ipairs(waypoints) do

you dont loop points in pathfinding.

Your code is a mess and seems like you used chatgpt for that

1 Like

You’re right, but I’m only using this because I don’t have the programming skills for it, and I don’t have the money to pay a programmer who probably wouldn’t give me any return anyway, since I have no intention of making a profit from this game. I’m just making something for fun. Sorry for anything :pensive:… And ty

I understand, there is no role against using chatgpt, you can use it freely. But we cant help if you dont bring up a code that is created by AI that hallucinates and creates made up parameters and code. check the roblox api for pathfinding, there is already a free code there

1 Like