I’ve been working on a pathfinding-based monster NPC. The core AI logic is contained within a ModuleScript, and for the most part it functions as expected. However, I’ve noticed that the monster will sometimes glitch—for example, it won’t move at all, or it will get stuck in place.
I’m not sure if this behavior is caused by limitations of Roblox’s PathfindingService itself, or if the problem lies in my implementation. I’d appreciate any feedback on how I could improve the script’s reliability and overall performance.
Module Script
local AImodule = {}
AImodule.__index = AImodule
function AImodule.new(object, pathParams, configuration)
-- Create a new instance of AImodule
local self = setmetatable({}, AImodule)
-- Initialize variables for this instance
self.object = object
self.pathParams = pathParams
self.configuration = configuration
self.walkanim = nil
self.path2 = nil
self.target = nil
self.npc = nil
self.playerNormalWalkingSpeed = nil
self.atackingValue = nil
-- Assign configuration values
self.walkSpeed = configuration.walkSpeed
self.runSpeed = configuration.runSpeed
self.playerSpeed = configuration.playerSpeed
self.range = configuration.range
self.damage = configuration.damage
-- Debounce Position
self.previousPosition = Vector3.new(0,0,0)
self.currentPosition = Vector3.new(0,0,0)
self.positionCount = 0
-- Return the instance
return self
end
function AImodule:AImodulescript()
local success, errorMessage = pcall(function()
local pathfindingservice = game:GetService("PathfindingService")
local dbStudRadius = 3
self.npc = self.object
local humanoid = self.npc:WaitForChild("Humanoid")
local hrp = self.npc:WaitForChild("HumanoidRootPart")
local body = self.npc:FindFirstChild("HumanoidRootPart")
local db = true
self.path2 = pathfindingservice:CreatePath()
hrp:SetNetworkOwner(nil)
self.walkanim = humanoid.Animator:LoadAnimation(humanoid.Animator.Walk)
local attackanim = humanoid.Animator:LoadAnimation(humanoid.Animator.Attack)
local rayprams = RaycastParams.new()
rayprams.FilterType = Enum.RaycastFilterType.Exclude
rayprams.FilterDescendantsInstances = {self.npc}
local lastPos
local animPlaying = false
self.walkanim:Play()
local function canseetarget(target)
local origin = hrp.Position
local direction = (target.HumanoidRootPart.Position - hrp.Position).Unit * self.range
local ray = workspace:Raycast(origin, direction, rayprams)
if ray and ray.Instance and ray.Instance:IsDescendantOf(target) then
return true
else
return false
end
end
local function findtarget()
local players = game.Players:GetPlayers()
local maxDistance = self.range
local nearestTarget
for _, player in pairs(players) do
if player.Character and #player:WaitForChild("SafeValues"):GetChildren() <= 0 and (not player:FindFirstChild("BeingAtacked")) then
local target = player.Character
local distance = (hrp.Position - target.HumanoidRootPart.Position).Magnitude
if distance < maxDistance and canseetarget(target) then
nearestTarget = target
maxDistance = distance
self:CallPlayer(player, "Found")
else
--self:CallPlayer(player, "Lost")
end
end
end
return nearestTarget
end
local function getpath(destination)
local path = pathfindingservice:CreatePath(self.pathParams)
path:ComputeAsync(hrp.Position, destination.Position)
if path.Status == Enum.PathStatus.Success then
return path
else
self.npc:SetPrimaryPartCFrame(CFrame.new(self.object.Parent.FailSafePart.Position))
self:CallPlayer(game.Players:GetPlayerFromCharacter(destination), "Lost")
path:Destroy()
return nil
end
end
local function DebugPos()
self.currentPosition = self.npc.HumanoidRootPart.Position
if (self.currentPosition - self.previousPosition).Magnitude <= dbStudRadius then
self.positionCount = self.positionCount + 1
else
self.positionCount = 0
end
if self.positionCount > 20 then
error("positionCount: ".. self.positionCount)
end
self.previousPosition = self.currentPosition
end
local function attack(target)
self.walkanim:Stop()
attackanim:Play()
while true do
local distance = (hrp.Position - target.HumanoidRootPart.Position).Magnitude
if distance >= self.range or #game.Players:GetPlayerFromCharacter(target):WaitForChild("SafeValues"):GetChildren() > 0 or target.Humanoid.Health <= 0 or target.Humanoid:GetState() == Enum.HumanoidStateType.Dead then
break
end
DebugPos()
task.wait()
self.path2:ComputeAsync(body.Position, target.HumanoidRootPart.Position)
local waypoints = self.path2:GetWaypoints()
for _, waypoint in pairs(waypoints) do
if distance >= self.range or #game.Players:GetPlayerFromCharacter(target):WaitForChild("SafeValues"):GetChildren() > 0 or target.Humanoid.Health <= 0 then
break
end
humanoid:MoveTo(waypoint.Position)
humanoid.MoveToFinished:Wait()
end
end
if self.path2 then
self.path2:Destroy()
end
self.walkanim:Play()
self:CallPlayer(game.Players:GetPlayerFromCharacter(target), "Lost")
target = nil
self:patrol()
end
local function walkto(destination)
local path = getpath(destination)
if not path then
self:patrol()
return
end
for _, waypoint in pairs(path:GetWaypoints()) do
path.Blocked:Connect(function()
path:Destroy()
self:patrol()
end)
if not animPlaying then
self.walkanim:Play()
animPlaying = true
end
attackanim:Stop()
self.target = findtarget()
if self.target and self.target.Humanoid.Health > 0 then
lastPos = self.target.HumanoidRootPart.Position
attack(self.target)
break
else
if waypoint.Action == Enum.PathWaypointAction.Jump then
humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
end
if lastPos then
humanoid:MoveTo(lastPos)
humanoid.MoveToFinished:Wait()
lastPos = nil
break
else
humanoid:MoveTo(waypoint.Position)
humanoid.MoveToFinished:Wait()
end
end
end
end
function self:patrol()
local waypoints = self.object.Parent.WayPoints:GetChildren()
if #waypoints == 0 then
return
end
local randomNum = math.random(1, #waypoints)
local selectedWaypoint = waypoints[randomNum]
if selectedWaypoint then
DebugPos()
walkto(selectedWaypoint)
end
end
task.spawn(DebugPos)
while task.wait() do
self:patrol()
end
end)
if not success then
task.wait()
warn("Script encountered an error: " .. errorMessage)
self.positionCount = 0
if self.path2 then
self.path2:Destroy()
end
self.walkanim:Play()
local s, e = pcall(function()
task.wait()
self:CallPlayer(game.Players:GetPlayerFromCharacter(self.target), "Lost")
end)
if self.target and self.target:FindFirstChild("humanoid") then
self:CallPlayer(game.Players:GetPlayerFromCharacter(self.target), "Lost")
self.npc.Humanoid.WalkSpeed = self.walkSpeed
self.target:FindFirstChild("humanoid").WalkSpeed = self.playerNormalWalkingSpeed
end
wait(1)
self.npc:SetPrimaryPartCFrame(CFrame.new(self.object.Parent.FailSafePart.Position))
task.wait(1)
self:AImodulescript()
end
end
function AImodule:CallPlayer(player, Value)
if Value then
if Value == "Found" then
self.atackingValue = Instance.new("Configuration")
self.atackingValue.Name = "BeingAtacked"
self.atackingValue.Parent = player
game.ReplicatedStorage.AIEvents.TargetUpdate:FireClient(player, "Found", self.npc:FindFirstChild("Chase"))
self.playerNormalWalkingSpeed = player.Character:FindFirstChild("Humanoid").WalkSpeed
task.wait()
player.Character:FindFirstChild("Humanoid").WalkSpeed = self.playerSpeed-- + self.playerNormalWalkingSpeed
self.npc.Humanoid.WalkSpeed = self.runSpeed
elseif Value == "Lost" then
if self.atackingValue then
self.atackingValue:Destroy()
end
task.wait()
if player and (not player:FindFirstChild("BeingAtacked")) then
if self.playerNormalWalkingSpeed and player then
player.Character:FindFirstChild("Humanoid").WalkSpeed = self.playerNormalWalkingSpeed
end
game.ReplicatedStorage.AIEvents.TargetUpdate:FireClient(player, "Lost")
end
self.npc.Humanoid.WalkSpeed = self.walkSpeed
self.playerNormalWalkingSpeed = nil
end
end
end
return AImodule
Server Script
local run = require(game:GetService("ServerScriptService").MainModules.AIModule)
local pathParams = {
AgentHeight = 8,
AgentRadius = 4,
AgentCanJump = false,
}
local configuration = {
walkSpeed = 20,
runSpeed = 45,
playerSpeed = 35,
range = 100,
damage = 75,
}
task.wait(1)
local aiInstance = run.new(script.Parent, pathParams, configuration)
aiInstance:AImodulescript()