Turning an AI Script to Module Script

Hello, I’m LuaGirlDeveloper and I’m trying to turn my NPC AI system into a module so I can just call the module and setup the NPC, so I don’t have to repeat my scripts. I’ve tried making it into a Module but I’m not the best of doing self. (The AI isn’t optimized.)

I don’t want the script I just need ideas and how should I complete this.

--@Ai
repeat
	task.wait(0.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)

Um just return dictionary with functions you want to use outside?
Etc:

return {
StartPatrol = StartPatrol;
}

btw:

if not game:IsLoaded() then game.Loaded:Wait() end

Thank you @Yarik_superpro and @iATtaCk_Noobsi for the responses. I will look into the OOP Tutorial.

I have one last question how I would make it so that the code would be run in the module so I can have 5 NPCS and call the main function to make it cleaner and easier?

NO USE HYBRID ECS

OOP is not scalable and hard to update.

ECS is whole arsenal while OOP is just a mere button.

Like this?

local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Module = require(ReplicatedStorage.Ai)

local Ais = {}

local function CreateNewAi(Model)
	local NewAi = Module.new(Model)
	table.insert(Ais, NewAi)
end

CreateNewAi(workspace.NpcNameHere)

RunService.Heartbeat:Connect(function()
	for Index, Npc in pairs(Ais) do
		Npc:Update()
	end
end)

Bro its not source :skull:
Its not :Think() ahh

Whatever you are doing its a very terrible approach.
Plus method overhead instead of dirrect call.

Then how should I do it Yarik?

Just dont slap OOP where it should not be please.

Like would this work I don’t know about OOP:

local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Module = require(ReplicatedStorage.Ai)

local Ais = {}

local function CreateNewAi(Model)
	local NewAi = Module.new(Model)
	table.insert(Ais, NewAi)
end

local function Update(Ai)
	-- Main Logic
	-- Pathfinding & FindTarget
end

CreateNewAi(workspace.NpcNameHere)

RunService.Heartbeat:Connect(function()
	for Index, Npc in pairs(Ais) do
		Update(Npc)
	end
end)
1 Like

Ok it looks better now.

Instead of having .new you can dirrectly return constructor like: return function() end
Also you dont need to use pairs,ipairs becouse they have overhead (especially ipairs)

for Index, Npc in Ais do
		Update(Npc)
	end

Like can you give an example or should I leave it how it is?

instead of doing:

function module.new() end

--You can just move it to the end of modulescript and do:

return function()

end

so you can do:

local module = require("./Module")

local new = module()

Okay then were should I put my movement functions?

Inside update i suppose.

You dont need ticking entities each heartbeat tbh

Use task.wait or tottal delta to determine when you should update AI

So would while true or while CanMove == true do

yeah you can do that aswell.
Just dont call update wayy too often and have like 0.5 delay so it will have less overhead.

Okay thank you so much. (Max Characters)

1 Like

Hey Yarik, one last question how can I make it so that you can doge when the NPC is chasing you. So if you doge the NPC it will play a slide animation like in Foxy from FNAF Doom?