Enemy Pathfinding Help

Works but it kinda spins out in a way when patrolling

and following the player also errors guess i didnt code it good

I forgot one of the things to change from Ai to self, it should work fine here

--@Ai
local PathfindingService = game:GetService("PathfindingService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")

local Ai = {}
Ai.__index = Ai

function Ai.new(Model : Model)
	local self = setmetatable({}, Ai)
	self.Model = Model
	self.Connections = {
		Waypoints = nil,
		NextWaypointIndex = nil,
		ReachedConnection = nil,
		BlockedConnection = nil
	}
	self.Path = PathfindingService:CreatePath({
		["AgentHeight"] = 7,
		["AgentRadius"] = 4,
		["AgentCanJump"] = false,
		["AgentCanClimb"] = false
	})
	
	return self
end

function Ai:GetTarget(MaxSearchDistance : number)
	local NearestTarget = nil
	local SearchDistance =MaxSearchDistance
	
	for Index, Player in pairs(Players:GetPlayers()) do
		local Character = Player.Character
		if not Character then return end
		
		local Humanoid = Character:FindFirstChild("Humanoid")
		if not Humanoid then return end
		if Humanoid.Health < 0 then return end
		
		local Distance = (self.Model.PrimaryPart.Position - Character.PrimaryPart.Position).Magnitude
		
		if SearchDistance > Distance then
			NearestTarget = Character
			SearchDistance = Distance
		end
	end
	
	return NearestTarget
end

function Ai:Capture(Target)
	local Distance = (self.Model.PrimaryPart.Position - Target.PrimaryPart.Position).Magnitude
	
	if Distance > 3 then
		self:FollowPath(Target.PrimaryPart.Position)
	else
		Target.Humanoid:TakeDamage(100)
	end
end

function Ai:FollowPath(Destination : Vector3)
	local Success, ErrorMessage = pcall(function()
		self.Path:ComputeAsync(self.Model.PrimaryPart.Position, Destination)
	end)
	
	if Success and self.Path.Status == Enum.PathStatus.Success then
		self.Connections.Waypoints = self.Path:GetWaypoints()
		
		for Index, Waypoint in pairs(self.Connections.Waypoints) do
			local Target = self:GetTarget(200)
			
			if Target then
				self:Capture(Target)
				self.Model.Humanoid.WalkSpeed = 25
				break
			else
				self.Model.Humanoid.WalkSpeed = 10
				self.Model.Humanoid:MoveTo(Waypoint.Position)
				self.Model.Humanoid.MoveToFinished:Wait()
			end
		end
	else
		warn(ErrorMessage)
	end
end

function Ai:Patrol()
	local WayPoints = Workspace.Waypoints:GetChildren()
	local RandomWaypoint = math.random(1, #WayPoints)
	self:FollowPath(Workspace.Waypoints[RandomWaypoint].Position)
end
return Ai

What do you mean? It shouldn’t do that because it’s using MoveTo properly.

Okay so it trys to move to Part1, then it gets confused then goes to part2, then part 1 and I think I may just recode it bettter

If this will help here’s the original script that isn’t a module:

--// Game Loaded
repeat
	task.wait(2.5)
until game.Loaded

--// Services
local PathfindingService = game:GetService("PathfindingService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")

--// Pathfinding Setup
local Path = PathfindingService:CreatePath({
	["AgentHeight"] = 7,
	["AgentRadius"] = 4,
	["AgentCanJump"] = false,
	["AgentCanClimb"] = false
})

--// Enemy Variables
local EnemyCharacter = Workspace.Figure2
local EnemyHumanoid = EnemyCharacter:WaitForChild("Humanoid")
local EnemyPrimaryPart = EnemyCharacter.PrimaryPart
local EnemyAnimator = EnemyHumanoid:WaitForChild("Animator")

--// Initialize Enemy
EnemyPrimaryPart:SetNetworkOwner(nil)

--// Pathfinding Variables
local Waypoints
local NextWaypointIndex
local ReachedConnection
local BlockedConnection

--// Enemy Properties
local WalkSpeed = 10
local SprintSpeed = 25
local MaxSearchDistance = 200

--// Animations
local IdleAnimation = script.IdleAnimation
local WalkAnimation = script.WalkAnimation
local RunAnimation = script.RunAnimation

local CurrentAnimationTrack = nil
local FigureStatus = script.Status

--// Stats
local TimeSurvived = 0

--// Functions
local function PlayAnimation(Animation)
	if CurrentAnimationTrack then
		CurrentAnimationTrack:Stop()
		CurrentAnimationTrack = nil
	end
	
	CurrentAnimationTrack = EnemyAnimator:LoadAnimation(Animation)
	CurrentAnimationTrack:Play()
end

local function CanSeeTarget(Target)
	local EnemyToCharacter = (Target.Head.Position - EnemyCharacter.Head.Position).Unit
	local EnemyLook = EnemyCharacter.Head.CFrame.LookVector
	
	local DotProduct = EnemyToCharacter:Dot(EnemyLook)
	
	if DotProduct > 0.5 then
		return true
	else
		return false
	end
end

local function Attack(Target)
	local Humanoid = Target:FindFirstChild("Humanoid")
	
	if Humanoid then
		Humanoid:TakeDamage(100)
	end
end

local function FindTarget()
	local NearestTarget = nil
	local SearchDistance = MaxSearchDistance
	
	for Index, Player in pairs(Players:GetPlayers()) do
		local Character = Player.Character
		local Humanoid = Character:FindFirstChild("Humanoid")
		if Character and Humanoid and Humanoid.Health > 0 then
			local Distance = (EnemyPrimaryPart.Position - Character.PrimaryPart.Position).Magnitude
			if Distance < SearchDistance and CanSeeTarget(Character) then
				NearestTarget = Character
				SearchDistance = Distance
			end
			if Distance < 8 then
				Attack(Character)
			end
		end
	end
	return NearestTarget
end

local function FollowPath(Destination)
	local Success, ErrorMessage = pcall(function()
		Path:ComputeAsync(EnemyPrimaryPart.Position, Destination)
	end)
	
	if Success and Path.Status == Enum.PathStatus.Success then
		Waypoints = Path:GetWaypoints()
		
		if #Waypoints < 2 then
			EnemyHumanoid:MoveTo(Destination)
			if ReachedConnection then ReachedConnection:Disconnect() end
			if BlockedConnection then BlockedConnection:Disconnect() end
			return
		end
		
		BlockedConnection = Path.Blocked:Connect(function(BlockedWaypointIndex)
			if BlockedWaypointIndex >= NextWaypointIndex then
				BlockedConnection:Disconnect()
				FollowPath(Destination)
			end
		end)
		
		if not ReachedConnection then
			ReachedConnection = EnemyHumanoid.MoveToFinished:Connect(function(Reached)
				if Reached and NextWaypointIndex < #Waypoints then
					NextWaypointIndex += 1
					EnemyHumanoid:MoveTo(Waypoints[NextWaypointIndex].Position)
				else
					ReachedConnection:Disconnect()
					BlockedConnection:Disconnect()
				end
			end)
		end
		
		NextWaypointIndex = 2
		EnemyHumanoid:MoveTo(Waypoints[NextWaypointIndex].Position)
		FigureStatus.Value = "Run" -- walk or run here
	else
		--warn(ErrorMessage)
	end
end

RunService.Heartbeat:Connect(function()
	local Target = FindTarget()
	
	if Target then
		FollowPath(Target.PrimaryPart.Position)
	else
		FigureStatus.Value = "Idle"
	end
end)

local LastStatus = ""

FigureStatus.Changed:Connect(function()
	if FigureStatus.Value == "Idle" and LastStatus ~= "Idle" then
		PlayAnimation(IdleAnimation)
		EnemyHumanoid.WalkSpeed = 0
	elseif FigureStatus.Value == "Walk" and LastStatus ~= "Walk" then
		PlayAnimation(WalkAnimation)
		EnemyHumanoid.WalkSpeed = WalkSpeed
	elseif FigureStatus.Value == "Run" and LastStatus ~= "Run" then
		PlayAnimation(RunAnimation)
		EnemyHumanoid.WalkSpeed = SprintSpeed
	end
	
	LastStatus = FigureStatus.Value
end)

@SubtotalAnt8185 I fixed the issue of walking randomly:

--// Game Loaded
repeat
	task.wait(2.5)
until game.Loaded

--// Services
local PathfindingService = game:GetService("PathfindingService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")

--// Pathfinding Setup
local Path = PathfindingService:CreatePath({
	["AgentHeight"] = 7,
	["AgentRadius"] = 4,
	["AgentCanJump"] = false,
	["AgentCanClimb"] = false
})

--// Enemy Variables
local EnemyCharacter = Workspace.Figure
local EnemyHumanoid = EnemyCharacter:WaitForChild("Humanoid")
local EnemyPrimaryPart = EnemyCharacter.PrimaryPart
local EnemyAnimator = EnemyHumanoid:WaitForChild("Animator")

--// Initialize Enemy
EnemyPrimaryPart:SetNetworkOwner(nil)

--// Pathfinding Variables
local Waypoints
local NextWaypointIndex
local ReachedConnection
local BlockedConnection
local CurrentTarget = nil
local PathfindingActive = false 

--// Enemy Properties
local WalkSpeed = 10
local SprintSpeed = 25
local MaxSearchDistance = 200

--// Animations
local IdleAnimation = script.IdleAnimation
local WalkAnimation = script.WalkAnimation
local RunAnimation = script.RunAnimation

local CurrentAnimationTrack = nil
local FigureStatus = script.Status

--// Patrol Variables (New/Adjusted)
local PatrolPoints = {
	workspace.PatrolPoint1.Position,
	workspace.PatrolPoint2.Position,
	workspace.PatrolPoint3.Position
}
local CurrentPatrolIndex = 1
local IsPatrolling = false
local PatrolWaitTime = 2

--// Functions
local function PlayAnimation(Animation)
	if CurrentAnimationTrack then
		CurrentAnimationTrack:Stop()
		CurrentAnimationTrack = nil
	end

	CurrentAnimationTrack = EnemyAnimator:LoadAnimation(Animation)
	CurrentAnimationTrack:Play()
end

local function CanSeeTarget(Target)
	local EnemyToCharacter = (Target.Head.Position - EnemyCharacter.Head.Position).Unit
	local EnemyLook = EnemyCharacter.Head.CFrame.LookVector

	local DotProduct = EnemyToCharacter:Dot(EnemyLook)

	if DotProduct > 0.5 then
		return true
	else
		return false
	end
end

local function Attack(Target)
	local Humanoid = Target:FindFirstChild("Humanoid")

	if Humanoid then
		Humanoid:TakeDamage(100)
	end
end

local function FindTarget()
	local NearestTarget = nil
	local SearchDistance = MaxSearchDistance

	for Index, Player in pairs(Players:GetPlayers()) do
		local Character = Player.Character
		local Humanoid = Character:FindFirstChild("Humanoid")
		if Character and Humanoid and Humanoid.Health > 0 then
			local Distance = (EnemyPrimaryPart.Position - Character.PrimaryPart.Position).Magnitude
			if Distance < SearchDistance and CanSeeTarget(Character) then
				NearestTarget = Character
				SearchDistance = Distance
			end
		end
	end
	return NearestTarget
end

local function StopPathfinding()
	if ReachedConnection then
		ReachedConnection:Disconnect()
		ReachedConnection = nil
	end
	if BlockedConnection then
		BlockedConnection:Disconnect()
		BlockedConnection = nil
	end
	PathfindingActive = false
	EnemyHumanoid:MoveTo(EnemyPrimaryPart.Position) 
end

local function FollowPath(Destination)
	if PathfindingActive and CurrentTarget and (CurrentTarget.PrimaryPart.Position - Destination).Magnitude < 5 then
		-- Already pathfinding to essentially the same location, no need to recompute
		return
	end

	StopPathfinding()
	PathfindingActive = true
	CurrentTarget = {PrimaryPart = {Position = Destination}}

	local Success, ErrorMessage = pcall(function()
		Path:ComputeAsync(EnemyPrimaryPart.Position, Destination)
	end)

	if Success and Path.Status == Enum.PathStatus.Success then
		Waypoints = Path:GetWaypoints()
		
		workspace.Folder:ClearAllChildren()
		
		if #Waypoints < 2 then
			EnemyHumanoid:MoveTo(Destination)
			StopPathfinding()
			return
		end
		
		for Index, Point in pairs(Waypoints) do
			local Part = Instance.new("Part")
			Part.Parent = workspace.Folder
			Part.Position = Point.Position
			Part.Name = "Point"
			Part.BrickColor = BrickColor.new("Persimmon")
			Part.Anchored = true
			Part.Size = Vector3.new(1, 1, 1)
			Part.Material = Enum.Material.Neon
			Part.CanCollide = false
		end
		
		BlockedConnection = Path.Blocked:Connect(function(BlockedWaypointIndex)
			if BlockedWaypointIndex >= NextWaypointIndex then
				StopPathfinding()
				FollowPath(Destination)
			end
		end)

		ReachedConnection = EnemyHumanoid.MoveToFinished:Connect(function(Reached)
			if Reached and NextWaypointIndex < #Waypoints then
				NextWaypointIndex += 1
				EnemyHumanoid:MoveTo(Waypoints[NextWaypointIndex].Position)
			else
				StopPathfinding()
			end
		end)
		
		NextWaypointIndex = 2
		EnemyHumanoid:MoveTo(Waypoints[NextWaypointIndex].Position)
	else
		warn("Path computation failed:", ErrorMessage)
		StopPathfinding()
	end
end

local function StartPatrol()
	if IsPatrolling then return end
	IsPatrolling = true
	FigureStatus.Value = "Walk"

	task.spawn(function()
		while IsPatrolling do
			local Destination = PatrolPoints[CurrentPatrolIndex]
			
			if FigureStatus.Value == "Idle" then
				FigureStatus.Value = "Walk"
				task.wait()
			end
			
			FollowPath(Destination)
			
			repeat
				task.wait(0.5)
			until (EnemyPrimaryPart.Position - Destination).Magnitude < 3 or FindTarget() ~= nil or not PathfindingActive
			
			if FindTarget() ~= nil then
				IsPatrolling = false
				break
			elseif not PathfindingActive then
				FigureStatus.Value = "Idle"
				task.wait(1)
			else
				FigureStatus.Value = "Idle"
				task.wait(PatrolWaitTime)
			end
			
			if IsPatrolling and FindTarget() == nil then
				CurrentPatrolIndex = CurrentPatrolIndex % #PatrolPoints + 1
				if CurrentPatrolIndex == 0 then CurrentPatrolIndex = 1 end
			else
				break
			end
		end
		IsPatrolling = false
		FigureStatus.Value = "Idle"
	end)
end

RunService.Heartbeat:Connect(function()
	local Target = FindTarget()
	
	if Target then
		if IsPatrolling then
			IsPatrolling = false 
			StopPathfinding()
		end
		
		local DistanceToTarget = (EnemyPrimaryPart.Position - Target.PrimaryPart.Position).Magnitude
		if DistanceToTarget < 8 then
			Attack(Target)
			FigureStatus.Value = "Idle"
			return
		end
		
		if not CurrentTarget or (Target.PrimaryPart.Position - CurrentTarget.PrimaryPart.Position).Magnitude > 5 then
			FollowPath(Target.PrimaryPart.Position)
		end
		FigureStatus.Value = "Run"
	else
		if PathfindingActive and not IsPatrolling then
			StopPathfinding()
			FigureStatus.Value = "Idle"
		end
		
		if not IsPatrolling then
			print("No target. Starting patrol.")
			StartPatrol()
		end
	end
end)

local LastStatus = ""

FigureStatus.Changed:Connect(function()
	if FigureStatus.Value == "Idle" and LastStatus ~= "Idle" then
		PlayAnimation(IdleAnimation)
		EnemyHumanoid.WalkSpeed = 0
	elseif FigureStatus.Value == "Walk" and LastStatus ~= "Walk" then
		PlayAnimation(WalkAnimation)
		EnemyHumanoid.WalkSpeed = WalkSpeed
	elseif FigureStatus.Value == "Run" and LastStatus ~= "Run" then
		PlayAnimation(RunAnimation)
		EnemyHumanoid.WalkSpeed = SprintSpeed
	end
	LastStatus = FigureStatus.Value
end)

I know it’s kind of buggy, but it’s a first draft can I have your opinion on the code?

I think that this is pretty good.

This was a good inclusion.

Yea it took me a while but I think I did good on it.

Hey @SubtotalAnt8185 I have a question how can I make a dodge system that the NPC reacts to? Like if the NPCs running at you if you move to the right or left or doge the NPC. The NPC will slide and play an animation?

It’s pretty complicated, but you can get the velocity of the player’s character to the left or right relative to the NPC.

local CharacterPrimaryPart = Character.PrimaryPart
local CharacterVelocity = CharacterPrimaryPart.AssemblyLinearVelocity

local EnemyVelocity = EnemyPrimaryPart.AssemblyLinearVelocity

local Difference = CharacterVelocity - EnemyVelocity
local RelativeVelocity = EnemyPrimaryPart.CFrame.RightVector:Dot(Difference)

if RelativeVelocity > 3 then
--dodge left/right
elseif RelativeVelocity < -3 then
--dodge the other direction
end

Where should I implement this?

I’m not exactly sure, but you would put it in some sort of loop when the enemy is relatively close to the target (the character is the target).

Uhhh okay I’ll try to implement that.

Okay I have the code but how would I make the slide part?

	if FigureStatus.Value == "Run" then
			local PlayerHumanoid = Target:FindFirstChild("Humanoid")
			local PlayerRootPart = Target:FindFirstChild("HumanoidRootPart")
			if PlayerHumanoid and PlayerRootPart and PlayerHumanoid.MoveDirection.Magnitude > 0.1 then
				local AI_to_Player_Vector = (PlayerRootPart.Position - EnemyPrimaryPart.Position).Unit
				local Player_Move_Direction = PlayerHumanoid.MoveDirection.Unit
				
				local Enemy_Forward_Vector = EnemyPrimaryPart.CFrame.LookVector
				local PlayerLateralDot = AI_to_Player_Vector:Dot(Player_Move_Direction)
				
				local AIStraightDot = Enemy_Forward_Vector:Dot(AI_to_Player_Vector)
				local DodgeDistanceThreshold = 15
				
				if math.abs(PlayerLateralDot) < 0.3 and PlayerRootPart.Velocity.Magnitude > 5 and
					AIStraightDot > 0.8 and DistanceToTarget < DodgeDistanceThreshold then
					print("SLIDE")
				end
			end
		end

You can do a few things:

  • Increase the WalkSpeed temporarily and make the NPC move really quickly to the left or right (easiest and most efficient)
  • Use something like a LinearVelocity mover to move the NPC left or right. This will require an attachment, but you could put one in every NPC and just enable/disable the left/right ones as needed.
  • Make the NPC teleport to the left/right and make the animation do the inverse, so the work is put onto the animation to animate the left/right movement, not the physics.

I recommend doing the first or last ones.

For teleporting the NPC:

EnemyPrimaryPart.CFrame *= CFrame.new(5, 0, 0)
...
EnemyPrimaryPart.CFrame *= CFrame.new(-5, 0, 0)

You could alternatively tween it, but it won’t move forward.

For increasing the WalkSpeed:

EnemyHumanoid.WalkSpeed = 50 --change this
EnemyHumanoid:Move(EnemyPrimaryPart.CFrame.RightVector)
task.wait(.5)
EnemyHumanoid.WalkSpeed = originalWalkSpeed --change this

...
--other direction is the same except it's negative:
EnemyHumanoid:Move(-EnemyPrimaryPart.CFrame.RightVector)

Thank you I’ll work on this later…