Looking for possible improvements on my AI control program

--|| SERVICES ||--
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Debris = game:GetService("Debris")
local ServerScriptService = game:GetService("ServerScriptService")
local ServerStorage = game:GetService("ServerStorage")
local TweenService = game:GetService("TweenService")
local PathfindingService = game:GetService("PathfindingService")

--|| LOCALISE ||--
local Pairs = pairs
local Require = require
local Script = script

--|| MODULES ||--
local NPCData = Require(ReplicatedStorage.Database.NPCS)
local GlobalFunctions = Require(ReplicatedStorage.GlobalFunctions)
local Damage = Require(ServerScriptService.Server.Damage)

--|| REMOTE EVENTS ||--
local clientSFX = ReplicatedStorage['Remote Events'].clientSFX

--|| VARIABLES ||--
local RandomSeed = Random.new()

local DeathConnections = {}
local NPCS = {}
local CivillianNPCS = {
	[game.Workspace.World.Map.NPCS.Zeppeli] = {
		["Last Update"] = tick(),
	}
}

local module = {
	logNPC = function(NPC)
		print("[NPC MANAGER]: "..NPC.Name.." has been logged (Animations also preloaded).")
		
		NPCS[NPC] = {
			["CFrame"] = NPC.PrimaryPart.CFrame,
			["Last Update"] = tick(),
			["Last Attack"] = tick(),
			['Animations'] = {},
			["Attack Animation"] = 1,
			["Whitelist"] = {},
			["Owner"] = {},
		}
		
		-- Loading in NPC stats
		local Humanoid = NPC.Humanoid
		Humanoid.MaxHealth = GlobalFunctions.calculateHealth(NPCData[NPC.Name].Defence)
		Humanoid.Health = Humanoid.MaxHealth
		Humanoid.WalkSpeed = NPCData[NPC.Name].WalkSpd
	
		-- Preloading them animations
		for _, Animation in Pairs(ReplicatedStorage.Animations.NPCS[NPC.Name]:GetChildren()) do
			NPCS[NPC].Animations[Animation.Name] = Humanoid:LoadAnimation(Animation)
		end
		
		NPCS[NPC].Animations["Idle"]:Play()
	end,
	bindDeath = function(NPC)
		print("[NPC MANAGER]: "..NPC.Name.." has been connected to it's death event.")
		DeathConnections[NPC] = NPC.Humanoid.Died:Connect(function()
			DeathConnections[NPC]:Disconnect()
			DeathConnections[NPC] = nil
			local respawnPoint = NPCS[NPC].CFrame
			NPCS[NPC] = nil

			delay(NPCData[NPC.Name].RespawnTime, function()
				local respawnModel = ServerStorage.NPCS[NPC.Name]:Clone()
				respawnModel:SetPrimaryPartCFrame(respawnPoint)
				respawnModel.Parent = game.Workspace.World.Enemies
			end)
			
			NPC:Destroy()
		end)
	end,
	getClosestEnemy = function(NPC, Whitelist)
		local closest = nil
		
		for _, target in Pairs(game.Workspace:GetDescendants()) do
			if target:IsA("Humanoid") 
			and target.Health > 0 
			and target.Parent:IsA("Model")
			and not Whitelist[target.Parent]
			and target.Parent ~= NPC
			and target.Parent.Parent ~= game.Workspace.World.Map.NPCS 
			and target.Parent.Parent ~= game.Workspace.World.Enemies then
				local Distance = (NPC.PrimaryPart.Position - target.Parent.PrimaryPart.Position).Magnitude
				
				if Distance <= NPCData[NPC.Name].Range then
					if closest == nil then
						closest = target.Parent
					else
						local distanceToClosest = (NPC.PrimaryPart.Position - closest.PrimaryPart.Position).Magnitude
						local distanceToTarget = (NPC.PrimaryPart.Position - target.Parent.PrimaryPart.Position).Magnitude
						
						if distanceToTarget < distanceToClosest then
							closest = target.Parent
						end
					end
				end
			end
		end
		
		return closest
	end,
	moveTo = function(NPC, Pos)
		NPC.Humanoid:MoveTo(Pos)
	end,
	setState = function(NPC, State)
		NPC.Data.State.Value = State
	end,
	attack = function(NPC, Target)
		local Inflicted = Damage.inflictDamage(NPC, Target.Humanoid, NPCData[NPC.Name].Strength)
		
		if Inflicted then
			local kb = GlobalFunctions.createInstance({
				instance = "BodyVelocity",
				properties = {
					Name = "Knockback",
					Velocity = (Target.PrimaryPart.Position - NPC.PrimaryPart.Position).Unit*20,
					MaxForce = Vector3.new(1,1,1)*500000,
					Parent = Target.PrimaryPart
				}
			})
			Debris:AddItem(kb, 0.1)
			
			local Sound = GlobalFunctions.createInstance({
				instance = "Sound",
				properties = {
					Name = NPC.Name.." Swoosh",
					SoundId = "rbxassetid://12222200",
					Parent = NPC.PrimaryPart
				},
			})
			Sound:Play()
			Debris:AddItem(Sound, 3)
			
			clientSFX:FireAllClients({
				Module = "Chicken", 
				Function = "Hit", 
				Data = {["Target"] = Target.Humanoid, ["Character"] = NPC}
			})
		end
	end,
}

spawn(function()
	while true do
		wait()		
		for NPC, Data in Pairs(NPCS) do
			if not DeathConnections[NPC] then module.bindDeath(NPC) end
			if NPC and NPC.PrimaryPart and NPC.Humanoid and NPC.Humanoid:IsDescendantOf(game.Workspace) then
				local RandomUpdateTime = 0.1 + 0.1 * math.random() -- Generate a random update time
				if tick() - Data["Last Update"] >= RandomUpdateTime then -- If the last update was larger then the random time
					Data["Last Update"] = tick() -- Set 
					
					if NPC.Humanoid.Health ~= NPC.Humanoid.MaxHealth then
						local closestEnemy = module.getClosestEnemy(NPC, Data.Whitelist)
						
						if closestEnemy ~= nil then
							local distance = (closestEnemy.PrimaryPart.Position - NPC.PrimaryPart.Position).Magnitude
							
							if tick() - Data["Last Attack"] >= NPCData[NPC.Name].AtkSpd and distance <= NPCData[NPC.Name].AtkDistance then
								Data["Last Attack"] = tick()
								
								NPCS[NPC]["Attack Animation"] = NPCS[NPC]["Attack Animation"] + 1
								if Data["Attack Animation"] > 2 then
									NPCS[NPC]["Attack Animation"] = 1
								end
								
								local FaceTarget = TweenService:Create(NPC.PrimaryPart, TweenInfo.new(0.1, Enum.EasingStyle.Sine), {["CFrame"] = CFrame.new(NPC.PrimaryPart.Position, closestEnemy.PrimaryPart.Position)})
								FaceTarget:Play()
								Debris:AddItem(FaceTarget, 0.1)
								
								NPCS[NPC].Animations["Attack"..Data["Attack Animation"]]:Play()
								module.attack(NPC, closestEnemy)
							else
								if not NPCS[NPC].Animations.Moving.IsPlaying then
									NPCS[NPC].Animations["Moving"]:Play()
								end

								local startPos = NPC.PrimaryPart.Position
								local endPos = closestEnemy.PrimaryPart.Position
								local distanceAway = (NPCData[NPC.Name].AtkDistance - 1)
								local FinalisedPosition = endPos + ((startPos - endPos).Unit * distanceAway)

								module.moveTo(NPC, FinalisedPosition)		
							end				
						else
							NPCS[NPC].Animations["Moving"]:Stop()
						end
					end
				end
			end
		end
	end
	
--	for NPC, Data in Pairs(CivillianNPCS) do
--		local updateTime = 20 + 20 * math.random()
--		if tick() - Data["Last Update"] >= updateTime then
--			
--		end
--	end
end)

for _, Enemy in Pairs(game.Workspace.World.Enemies:GetChildren()) do
	module.logNPC(Enemy)
end

game.Workspace.World.Enemies.ChildAdded:Connect(function(Enemy)
	module.logNPC(Enemy)
end)

return module

This is my NPC manager code, what I wish to do is implement pathfinding service but what happens is the NPC’s start stopping at each node and it looks odd. I’d like to make a good performing NPC manager code. I 've applied everything I know but it seems like it’s not enough to handle up to 200 NPCs.

I was wondering what I could do to improve, I’ve read several topics regarding NPC control and how some remove the humanoid and etc but in my case, the NPC’s just look really off and can start floating.

4 Likes