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)
-
The character moves in a jittery way, as shown in the video
-
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
-
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
-
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()