Methods of optimizing fast/smart AI

I’ve been working on NPCs lately in my game. So far it has worked as planned, but there is one major problem, with just one AI active the game runs noticeably slower. This is not caused by the humanoid itself, it’s entirely from the AI script.

The AI needs to have very fast loops to be smart, so I would rather not lower the check rate.
It runs at around 30 checks per second.

Do certain things like Raycasting, Humanoid move function, or changing properties affect performance enough to be considered?

Can you share code? It’s hard to make a guess on what is hindering performance, I assume it’s Raycasting.

30 checks per second is a LOT for an NPC that presumably just walks around. Depending on what they are doing, it can have a serious performance toll. Raycasting has the most significant impact IIRC, but it shouldn’t make that much of an impact unless there are thousands running a second.

I know there are debugging tools for this kind of thing in studio, but I don’t use them so I personally don’t know which is the best for this case.

First thing I would do is reducing the checks to maybe 3-5 per second. Depends on what you’re doing, though.

It’s a very large script. The AI is designed for fast paced combat, so that is why I cannot lower the check rate.

local Remote_Events = game.ReplicatedStorage.Remote_Events
local Server_Events = game.ServerStorage.Server_Events
local Humanoids = game.ReplicatedStorage.Humanoids
local Effect_Storage = game.ServerStorage.Game_Effects
local Module_Scripts = game.ReplicatedStorage.Module_Scripts

local Function_Module = require(Module_Scripts.Function_Module)
local Item_Data = require(Module_Scripts.Item_Data)
local Stun_Module = require(Module_Scripts.Stun_Module)
local Effects = require(Module_Scripts.Effect_Module)
local Npc_Data_Module = require(Module_Scripts.Npc_Data)
local Item_Module = require(Module_Scripts.Item_Module)
local Climb_Module = require(Module_Scripts.Climb_Module)
local Stamina_Module = require(Module_Scripts.Stamina_Module)

local Move_Directions = {
	"0,0,-1", "0,0,1", "-1,0,0", "1,0,0",
	"1,0,-1", "1,0,1", "-1,0,-1", "-1,0,1",
}


local Action_Input = function(Npc, Input_Name, Action_Module) --Always put returns at the end unless it's a global return like stats.loaded
	local C_Stats = Npc.Npc_Stats
	local Stats = Npc.Character_Data
	
	if Input_Name == "Block_End" then
		C_Stats.Mouse2_Down.Value = false
		Action_Module.Block_Input(Npc, false)
	elseif Input_Name == "Light_End" then
		C_Stats.Mouse1_Down.Value = false
	elseif Input_Name == "Sprint_End" then
		Action_Module.Sprint(Npc, false)
	end
	
	if Stats.Knocked_Out.Value or not Stats.Loaded.Value then return end
	
	if not Stats.Climbing.Value and not C_Stats.Eating.Value and not C_Stats.Heavy_Attacking.Value and not C_Stats.Using_Skill.Value then
		if Input_Name == "Dodge" then
			Action_Module.Dodge(Npc)
		elseif Input_Name == "Eat" then
			Action_Module.Eat_Item(Npc)
		elseif Input_Name == "Interact" then
			Action_Module.Interact(Npc)
		elseif Input_Name == "Visor" then
			Action_Module.Change_Visor(Npc)
		end
	end
	if Input_Name == "Sprint_Start" then
		Action_Module.Sprint(Npc, true)
	elseif Input_Name == "Climb" then
		Climb_Module.Climb(Npc)
	end 
	
	if not Stats.Climbing.Value and not Stats.Knocked_Out.Carry.Value and not C_Stats.Equipping.Value then
		if Stats.Stunned.Value or C_Stats.Light_Attacking.Value or C_Stats.Heavy_Attacking.Value or C_Stats.Kicking.Value or C_Stats.Using_Skill.Value or C_Stats.Eating.Value then return end

		if Input_Name == "Block_Start" then
			C_Stats.Mouse2_Down.Value = true
			C_Stats.Mouse2_Down.Time.Value = time()
			Action_Module.Block_Input(Npc, true)
		end

		if C_Stats.Blocking.Value then return end

		if Input_Name == "Light_Start" then
			Action_Module.Light_Start(Npc)
		elseif Input_Name == "Switch_Primary" then
			Action_Module.Weapon_Switch(Npc, Stats.Equip_Slots.Weapon.Primary)
		elseif Input_Name == "Switch_Secondary" then
			Action_Module.Weapon_Switch(Npc, Stats.Equip_Slots.Weapon.Secondary)
		elseif Input_Name == "Switch_Shield" then
			Action_Module.Weapon_Switch(Npc, Stats.Equip_Slots.Shield.Shield)
		elseif Input_Name == "Heavy_Attack" then
			Action_Module.Heavy_Start(Npc)
		elseif Input_Name == "Kick" then
			Action_Module.Kick_Attack(Npc)
		end
	end
end

local Set_Combat = function(Stats,Enemy_Stats)
	Stats.In_Combat.Timer.Value = 30
	Stats.In_Combat.Value = true
	Stats.In_Combat.Enemy.Value = Enemy_Stats.Parent

	Enemy_Stats.In_Combat.Timer.Value = 30
	Enemy_Stats.In_Combat.Value = true
	Enemy_Stats.In_Combat.Enemy.Value = Stats.Parent
end

local Get_Enemy_Armor = function(Enemy)
	if not Enemy then return "Soft" end
	
	local Enemy_Stats = Enemy.Character_Data
	local Soft_Rating = 0
	local Hard_Rating = 0
	local Type = "Soft"
	
	for i,Armor_Value in pairs(Enemy_Stats.Armored_Parts:GetChildren()) do
		if Armor_Value.Value then
			local Armor_Data = Item_Data[Armor_Value.Value.Name]
			
			Soft_Rating += Armor_Data.Soft_Armor
			Hard_Rating += Armor_Data.Hard_Armor
		end
	end
	
	if Hard_Rating > Soft_Rating then
		Type = "Hard"
	end
	
	return Type
end

local Find_Best_Weapon = function(Npc)
	local Stats = Npc.Character_Data
	local Weapon_Slots = Stats.Equip_Slots.Weapon
	local Enemy_Armor = Get_Enemy_Armor(Stats.In_Combat.Enemy.Value)
	local Usable_Slot = nil
	local Target_Slot = nil
	
	for i,Slot in pairs(Weapon_Slots:GetChildren()) do
		if not Slot.Value then
			local Found_Weapon = nil

			for i,Item in pairs(Stats.Inventory:GetChildren()) do
				if Item_Data[Item.Name].Type == "Weapon" then
					Found_Weapon = Item
				end
			end

			if Found_Weapon then
				Item_Module.Equip(Stats,Found_Weapon,Slot.Name)
				wait(0.5)
			end
		end
		
		if Slot.Value then
			local Data = Item_Data[Slot.Value.Name]
			local Has_Arrows = Stats.Inventory:FindFirstChild("Item_15")
			local Usable = true
			
			if Data.Class == "Ranged" then
				if not Has_Arrows then
					Usable = false
				end
				Data = Item_Data["Item_15"] --Arrow Data
			end
			
			if Usable then
				Usable_Slot = Slot
			end

			if Usable and (Enemy_Armor == "Hard" and Data.Hard_Penetration > Data.Soft_Penetration) or (Enemy_Armor == "Soft" and Data.Soft_Penetration >= Data.Hard_Penetration) then
				Target_Slot = Slot
				break
			end
		end
	end
	
	if not Target_Slot and Usable_Slot then
		Target_Slot = Usable_Slot
	end
	
	return Target_Slot
end

local Equip_Any_Weapon = function(Npc, Action_Module)
	local C_Stats = Npc.Npc_Stats
	local Stats = Npc.Character_Data
	local Weapon_Slots = Stats.Equip_Slots.Weapon
	local Target_Slot = C_Stats.Desired_Weapon.Value
	
	if Target_Slot then
		Action_Input(Npc, "Switch_"..Target_Slot.Name, Action_Module)
	end
end

local Unequip_Any_Weapon = function(Npc, Action_Module)
	local Stats = Npc.Character_Data
	local Weapon_Slots = Stats.Equip_Slots.Weapon

	if Stats.Held_Weapon.Value then
		Action_Input(Npc, "Switch_"..Stats.Held_Weapon.Value.Name, Action_Module)
	end
end

local Get_Food_Item = function(Stats)
	return Stats.Inventory:FindFirstChild(Stats.Food_Item.Value)
end

local Eat_Food = function(Npc, Action_Module)
	local C_Stats = Npc.Npc_Stats
	local Stats = Npc.Character_Data
	
	if Get_Food_Item(Stats) and Stats.Hunger.Value < 50 then
		Action_Input(Npc, "Eat", Action_Module)
	else
		local Nearest = nil
		local Distance = 100
		
		for i,Item in pairs(workspace.Drop_Folder:GetChildren()) do
			if Item_Data[Item.Name].Type == "Food" then
				local New_Distance = (Item.PrimaryPart.Position - Npc.PrimaryPart.Position).Magnitude
				if New_Distance < Distance then
					Distance = New_Distance
					Nearest = Item
				end
			end
		end
		
		C_Stats.Target_Item.Value = Nearest
	end
end

local Get_Weapon_Distance = function(Stats)
	local Weapon_Slot = Stats.Held_Weapon.Value
	local Distance = 0
	
	if Weapon_Slot then
		local Item = Weapon_Slot.Value
		local Data = Item_Data[Item.Name]
		
		if Data.Weapon_Type == "Sword" then
			Distance = 2
		elseif Data.Weapon_Type == "Spear" then
			Distance = 7
		elseif Data.Weapon_Type == "Axe" or Data.Weapon_Type == "Hammer" then
			Distance = 3
		elseif Data.Weapon_Type == "Mace" then
			Distance = 1
		elseif Data.Weapon_Type == "Bow" then
			Distance = 70
		end
	end
	
	return Distance
end

local Get_Target_Point = function(Npc, Look_Point)
	local C_Stats = Npc.Npc_Stats
	local Stats = Npc.Character_Data
	local Enemy = C_Stats.Target_Enemy.Value
	local Desired_Point = nil

	if C_Stats.Current_Behaviour.Value == "Attack" and Enemy then
		Desired_Point = Enemy.PrimaryPart.Position + (Enemy.Humanoid.MoveDirection / 2)
		if not Look_Point then
			local Target_Distance = Get_Weapon_Distance(Stats)
			if Target_Distance then
				Desired_Point -= (Desired_Point - Npc.PrimaryPart.Position).Unit * Target_Distance
			end
		end
	elseif C_Stats.Current_Behaviour.Value == "Get_Item" and C_Stats.Target_Item.Value then
		Desired_Point = C_Stats.Target_Item.Value.PrimaryPart.Position
	elseif C_Stats.Current_Behaviour.Value == "Get_Body" and C_Stats.Target_Body.Value then
		Desired_Point = C_Stats.Target_Body.Value.PrimaryPart.Position
	elseif C_Stats.Current_Behaviour.Value == "Run_Away" and C_Stats.Target_Enemy.Value then
		Desired_Point = Npc.PrimaryPart.Position + ((Npc.PrimaryPart.Position - Enemy.PrimaryPart.Position).Unit * 50) + Vector3.new(0,15,0)
	elseif C_Stats.Current_Behaviour.Value == "Dialog" and C_Stats.Target_Speaker.Value then
		Desired_Point = C_Stats.Target_Speaker.Value.PrimaryPart.Position
		if not Look_Point then
			Desired_Point = Npc.PrimaryPart.Position
		end
	end
	
	return Desired_Point
end

local Relative_Vector = function(Center, Vector)
	return (Center * CFrame.new(Vector)).Position - Center.Position
end

local Check_Move_Direction = function(Npc, Desired_Point, Locked, Vector)
	local C_Stats = Npc.Npc_Stats
	local Stats = Npc.Character_Data
	local Weight = 0
	local Ray_Vector = Vector3.new()
	local Center = Npc.PrimaryPart.CFrame
	local Blocks = {workspace.Block_Terrain}
	local Base_Distance = (Npc.PrimaryPart.Position - Desired_Point).Magnitude
	local Unit_Target = Npc.PrimaryPart.Position - ((Npc.PrimaryPart.Position - Desired_Point).Unit)
	
	if (C_Stats.Current_Behaviour.Value == "Attack" and Base_Distance < 0.3) or (Stats.Climbing.Value and Base_Distance < 0.6) or (C_Stats.Current_Behaviour.Value ~= "Attack" and Base_Distance < 6) then
		return 0
	end
	
	if not Stats.Climbing.Value then
		Weight = 10 / ((Center * CFrame.new(Vector)).Position - Desired_Point).Magnitude
	else
		Weight = 1 / ((Center * CFrame.new(Vector)).Position - Unit_Target).Magnitude
	end
	Ray_Vector = Relative_Vector(Center, Vector) * 3

	local Obstructed, Point = workspace:FindPartOnRayWithWhitelist(Ray.new(Center.Position + Vector3.new(0,-1,0),Ray_Vector),Blocks)
	local Wall, Point_2 = workspace:FindPartOnRayWithWhitelist(Ray.new(Center.Position + Vector3.new(0,3,0),Ray_Vector),Blocks)
		
	C_Stats.Path_Obstructed.Value = Obstructed
	C_Stats.Path_Obstructed.Is_Wall.Value = Wall
	
	if Wall then
		Weight *= ((Center.Position + Vector3.new(0,3,0)) - Point_2).Magnitude / 4
	end
	
	if Stats.Climbing.Value then
		if Vector.Y ~= 0 then
			if string.find(math.floor(Vector.Y), "1") then
				Weight *= 6
			else
				Weight *= 4
			end
		end
	end

	return Weight
end

local Convert_Vector = function(String, Climbing)
	local Nums = string.split(String,",")
	
	if not Climbing then
		return Vector3.new(tonumber(Nums[1]),tonumber(Nums[2]),tonumber(Nums[3])).Unit
	else
		return Vector3.new(tonumber(Nums[1]),tonumber(Nums[3]),tonumber(Nums[2])).Unit
	end
end


local Check_Target_Visible = function(Main_P, Target_P)
	local Dir = CFrame.new(Main_P, Target_P).LookVector
	local Dist = (Main_P - Target_P).Magnitude
	local Checker = Ray.new(Main_P, Dir * Dist)
	local Wall = workspace:FindPartOnRayWithWhitelist(Checker,{workspace.Block_Terrain, workspace.Decorations.Blocks})

	return Wall
end


local Behaviour_Considerations = {
	Attack = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Stats = Npc.Character_Data
		local Enemy = C_Stats.Target_Enemy.Value
		local Has_Weapon = C_Stats.Desired_Weapon.Value
		local Weight = 0
		
		if Has_Weapon and Enemy and Enemy.Humanoid.Health > 0 and (Stats.Equip_Slots.Weapon.Primary.Value or Stats.Equip_Slots.Weapon.Secondary.Value) and not C_Stats.Target_Body.Value and (Npc.PrimaryPart.Position - Enemy.PrimaryPart.Position).Magnitude < 100 then
			Weight = 0.5 + ((Npc.Humanoid.Health / Npc.Humanoid.MaxHealth) / 2)
		end
		
		return Weight
	end,
	Get_Item = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Stats = Npc.Character_Data
		local Weight = 0

		if C_Stats.Target_Item.Value then
			if C_Stats.Target_Item.Value.Parent == workspace.Drop_Folder then
				Weight = 0.6
				if C_Stats.Target_Enemy.Value or C_Stats.Target_Speaker.Value then
					Weight /= 2
				end
			else
				C_Stats.Target_Item.Value = nil
			end
		end
		
		return Weight
	end,
	Get_Body = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Stats = Npc.Character_Data
		local Target_Body = C_Stats.Target_Body.Value
		local Weight = 0

		if Target_Body then
			Weight = 2
		end
		
		return Weight
	end,
	Run_Away = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Stats = Npc.Character_Data
		local Enemy = C_Stats.Target_Enemy.Value
		local Mood = Function_Module.Get_Npc_Mood(C_Stats)
		local Weight = 0
		
		if Enemy and Enemy.Humanoid.Health > 0 and ((Npc.PrimaryPart.Position - Enemy.PrimaryPart.Position).Magnitude < 100 or Stats.Climbing.Value) then
			Weight = ((-(Npc.Humanoid.Health / 100)) + (Enemy.Humanoid.Health / 100)) * 1.5
			if Mood == "Afraid" then
				Weight *= 1.2
			end
		end
		
		return Weight
	end,
	Dialog = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Stats = Npc.Character_Data
		local Speaker = C_Stats.Target_Speaker.Value
		local Weight = 0
		
		if Speaker then
			Weight = 0.8
		end
		
		return Weight
	end,
	Do_Nothing = function(Npc)
		return 0.2
	end,
}

local Action_Considerations = {
	Equip_Weapon = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Stats = Npc.Character_Data
		local Weight = 0
		
		if Stats.Knocked_Out.Value or not Stats.Loaded.Value or Stats.Climbing.Value or Stats.Knocked_Out.Carry.Value or C_Stats.Equipping.Value then return 0 end
		if Stats.Stunned.Value or C_Stats.Light_Attacking.Value or C_Stats.Heavy_Attacking.Value or C_Stats.Kicking.Value or C_Stats.Using_Skill.Value or C_Stats.Eating.Value or C_Stats.Blocking.Value then return 0 end
		
		if C_Stats.Desired_Weapon.Value and Stats.Held_Weapon.Value ~= C_Stats.Desired_Weapon.Value and C_Stats.Current_Behaviour.Value == "Attack" then
			Weight = 1
		end
		
		return Weight
	end,
	Unequip_Weapon = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Stats = Npc.Character_Data
		local Weight = 0
		
		if Stats.Knocked_Out.Value or not Stats.Loaded.Value or Stats.Climbing.Value or Stats.Knocked_Out.Carry.Value or C_Stats.Equipping.Value then return 0 end
		if Stats.Stunned.Value or C_Stats.Light_Attacking.Value or C_Stats.Heavy_Attacking.Value or C_Stats.Kicking.Value or C_Stats.Using_Skill.Value or C_Stats.Eating.Value or C_Stats.Blocking.Value then return 0 end

		if Stats.Held_Weapon.Value and (C_Stats.Current_Behaviour.Value == "Do_Nothing" or C_Stats.Current_Behaviour.Value == "Get_Item" or not C_Stats.Desired_Weapon.Value) then
			Weight = 1
		end

		return Weight
	end,
	Switch_Shield = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Stats = Npc.Character_Data
		local Weapon_Slot = Stats.Held_Weapon.Value
		local Shield = Stats.Equip_Slots.Shield.Shield.Value
		local Weight = 0
		
		if Stats.Knocked_Out.Value or not Stats.Loaded.Value or Stats.Climbing.Value or Stats.Knocked_Out.Carry.Value or C_Stats.Equipping.Value then return 0 end
		if Stats.Stunned.Value or C_Stats.Light_Attacking.Value or C_Stats.Heavy_Attacking.Value or C_Stats.Kicking.Value or C_Stats.Using_Skill.Value or C_Stats.Eating.Value or C_Stats.Blocking.Value then return 0 end
		
		if not Stats.Held_Shield.Value and Shield and C_Stats.Current_Behaviour.Value == "Attack" then
			if (Weapon_Slot and Item_Data[Weapon_Slot.Value.Name].Handle_Type == "Two") and Item_Data[Shield.Name].Handle_Type == "Grip" then return 0 end
			
			Weight = 0.6
			if Npc.Humanoid.Health < 40 then
				Weight *= 1.5
			end
		elseif Stats.Held_Shield.Value and (C_Stats.Current_Behaviour.Value == "Do_Nothing" or C_Stats.Current_Behaviour.Value == "Get_Item") then
			Weight = 0.9
		end

		return Weight
	end,
	Light_Start = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Stats = Npc.Character_Data
		local Enemy = C_Stats.Target_Enemy.Value
		local Weapon_Slot = Stats.Held_Weapon.Value
		local Weight = 0
		
		if Stats.Knocked_Out.Value or not Stats.Loaded.Value or Stats.Climbing.Value or Stats.Knocked_Out.Carry.Value or C_Stats.Equipping.Value then return 0 end
		if Stats.Stunned.Value or C_Stats.Light_Attacking.Value or C_Stats.Heavy_Attacking.Value or C_Stats.Kicking.Value or C_Stats.Using_Skill.Value or C_Stats.Eating.Value or C_Stats.Blocking.Value then return 0 end
		if not Stats.Held_Weapon.Value or C_Stats.Cooldowns.Light.Value then return 0 end

		if C_Stats.Current_Behaviour.Value == "Attack" and Enemy and Weapon_Slot and C_Stats.Point_Visible.Value then
			local Enemy_Stats = Enemy.Character_Data
			if Stats.Held_Weapon.Class.Value == "Melee" and (Npc.PrimaryPart.Position - Enemy.PrimaryPart.Position).Magnitude < 5 + Get_Weapon_Distance(Stats) then
				Weight = 0.6
				if Enemy_Stats.Stunned.Value or Enemy_Stats.Attacking.Heavy.Value or (Stats.Blocking.Block_Strength.Value > 0 and Stats.Blocking.Hit_Count.Value >= Stats.Blocking.Block_Strength.Value - 2) then
					Weight = 1.1
				end
			elseif Stats.Held_Weapon.Class.Value == "Ranged" then
				Weight = 0.7
			end
		end

		return Weight
	end,
	Light_End = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Stats = Npc.Character_Data
		local Enemy = C_Stats.Target_Enemy.Value
		local Weight = 0

		if C_Stats.Mouse1_Down.Value and time() - C_Stats.Mouse1_Down.Time.Value > 1 then
			if Enemy and Stats.Held_Weapon.Value then
				local Enemy_Stats = Enemy.Character_Data
				local Weapon_Data = Item_Data[Stats.Held_Weapon.Value.Value.Name]
				
				if time() - C_Stats.Mouse1_Down.Time.Value > 2.5-(Weapon_Data.Draw_Speed * 2) then
					Weight = 1
				end
				if Enemy_Stats.Stunned.Value or Enemy_Stats.Attacking.Value then
					Weight = 3
				end
			elseif not Enemy then
				Weight = 1
			end
		end

		return Weight
	end,
	Heavy_Attack = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Stats = Npc.Character_Data
		local Enemy = C_Stats.Target_Enemy.Value
		local Weight = 0
		
		if Stats.Knocked_Out.Value or not Stats.Loaded.Value or Stats.Climbing.Value or Stats.Knocked_Out.Carry.Value or C_Stats.Equipping.Value then return 0 end
		if Stats.Stunned.Value or C_Stats.Light_Attacking.Value or C_Stats.Heavy_Attacking.Value or C_Stats.Kicking.Value or C_Stats.Using_Skill.Value or C_Stats.Eating.Value or C_Stats.Blocking.Value then return 0 end
		if not Stats.Held_Weapon.Value or C_Stats.Cooldowns.Heavy.Value then return 0 end

		if C_Stats.Current_Behaviour.Value == "Attack" and Stats.Held_Weapon.Class.Value == "Melee" and Enemy and C_Stats.Point_Visible.Value then
			local Enemy_Stats = Enemy.Character_Data
			if (Npc.PrimaryPart.Position - Enemy.PrimaryPart.Position).Magnitude < 4 + Get_Weapon_Distance(Stats) then
				Weight = 0.7
				if Enemy_Stats.Stunned.Value or Enemy_Stats.Blocking.Value then
					Weight = 1.2
				end
			end
		end
		
		return Weight
	end,
	Kick = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Stats = Npc.Character_Data
		local Enemy = C_Stats.Target_Enemy.Value
		local Weight = 0
		
		if Stats.Knocked_Out.Value or not Stats.Loaded.Value or Stats.Climbing.Value or Stats.Knocked_Out.Carry.Value or C_Stats.Equipping.Value then return 0 end
		if Stats.Stunned.Value or C_Stats.Light_Attacking.Value or C_Stats.Heavy_Attacking.Value or C_Stats.Kicking.Value or C_Stats.Using_Skill.Value or C_Stats.Eating.Value or C_Stats.Blocking.Value then return 0 end
		if C_Stats.Cooldowns.Kick.Value then return 0 end

		if C_Stats.Current_Behaviour.Value == "Attack" and Enemy and C_Stats.Point_Visible.Value then
			local Enemy_Stats = Enemy.Character_Data
			if (Npc.PrimaryPart.Position - Enemy.PrimaryPart.Position).Magnitude < 4 then
				Weight = 0.5
				if Enemy_Stats.Attacking.Value then
					Weight = 0.8
				end
				if not C_Stats.Light_Attacking.Value and C_Stats.Cooldowns.Light.Value then
					Weight = 1
				end
			end
		end

		return Weight
	end,
	Block_Start = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Stats = Npc.Character_Data
		local Enemy = C_Stats.Target_Enemy.Value
		local Weight = 0
		
		if Stats.Knocked_Out.Value or not Stats.Loaded.Value or Stats.Climbing.Value or Stats.Knocked_Out.Carry.Value or C_Stats.Equipping.Value then return 0 end
		if Stats.Stunned.Value or C_Stats.Light_Attacking.Value or C_Stats.Heavy_Attacking.Value or C_Stats.Kicking.Value or C_Stats.Using_Skill.Value or C_Stats.Eating.Value then return 0 end
		if not Function_Module.Get_Blocking(Stats) or Stats.Blocking.Block_Strength.Value < 0 then return 0 end

		if C_Stats.Current_Behaviour.Value == "Attack" and Enemy and not C_Stats.Blocking.Value and not C_Stats.Mouse2_Down.Value and time() - C_Stats.Mouse2_Down.Time.Value > 1.05 and C_Stats.Point_Visible.Value then
			local Enemy_Stats = Enemy.Character_Data
			if Enemy_Stats.Held_Weapon.Class.Value == "Melee" or Stats.Held_Shield.Value then
				if Enemy_Stats.Attacking.Value and not Enemy_Stats.Attacking.Heavy.Value then
					Weight = 1
				end
				if Stats.Blocking.Block_Strength.Value > 0 and Stats.Blocking.Hit_Count.Value >= Stats.Blocking.Block_Strength.Value - 1 then
					Weight = 0
				end
			end
		end

		return Weight
	end,
	Block_End = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Stats = Npc.Character_Data
		local Enemy = C_Stats.Target_Enemy.Value
		local Weight = 0

		if C_Stats.Mouse2_Down.Value and time() - C_Stats.Mouse2_Down.Time.Value > 1 then
			if Enemy and Stats.Held_Weapon.Value then
				local Enemy_Stats = Enemy.Character_Data
				Weight = (time() - C_Stats.Mouse2_Down.Time.Value) / 4
				if Enemy_Stats.Attacking.Value and not Enemy_Stats.Attacking.Heavy.Value then
					Weight = 0
				end
				if Enemy_Stats.Stunned.Value or Enemy_Stats.Attacking.Heavy.Value or (Stats.Blocking.Block_Strength.Value > 0 and Stats.Blocking.Hit_Count.Value >= Stats.Blocking.Block_Strength.Value - 1) then
					Weight *= 3
				end
				if Enemy_Stats.Held_Weapon.Class.Value == "Melee" and (Npc.PrimaryPart.Position - Enemy.PrimaryPart.Position).Magnitude > 16 then
					Weight = 10
				end
			elseif not Enemy then
				Weight = 1
			end
		end

		return Weight
	end,
	Dodge = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Stats = Npc.Character_Data
		local Enemy = C_Stats.Target_Enemy.Value
		local Desired_Point = Get_Target_Point(Npc, false)
		local Weight = 0
		
		if Stats.Knocked_Out.Value or not Stats.Loaded.Value or Stats.Climbing.Value or C_Stats.Eating.Value or C_Stats.Heavy_Attacking.Value or C_Stats.Using_Skill.Value then return 0 end
		if Stats.Held_Weapon.Value and Stats.Held_Weapon.Class.Value == "Ranged" and C_Stats.Light_Attacking.Value then return 0 end
		if C_Stats.Cooldowns.Dodge.Value or Stats.Stunned.Dodge_Stun.Value or Npc.Humanoid.FloorMaterial == Enum.Material.Air then return 0 end

		if C_Stats.Current_Behaviour.Value == "Attack" and Enemy and C_Stats.Point_Visible.Value then
			local Enemy_Stats = Enemy.Character_Data
			local Distance = (Npc.PrimaryPart.Position - Enemy.PrimaryPart.Position).Magnitude
			local Attack_Distance = 8+Get_Weapon_Distance(Stats)

			if Enemy_Stats.Attacking.Value or (Stats.Blocking.Block_Strength.Value > 0 and Stats.Blocking.Hit_Count.Value >= Stats.Blocking.Block_Strength.Value - 2) then
				Weight = 1.5
			end
			if Stats.Held_Weapon.Class.Value == "Melee" and (Distance > Attack_Distance or (C_Stats.Light_Attacking.Value and Distance < Attack_Distance)) then
				Weight = 2
			end
			if Stats.Stunned.Value and not Stats.Stunned.Dodge_Stun.Value then
				Weight = 3
			end
		else
			if Desired_Point and (Npc.PrimaryPart.Position - Desired_Point).Magnitude > 50 then
				Weight = 1
			end
		end
		
		return Weight
	end,
	Sprint_Start = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Stats = Npc.Character_Data
		local Weight = 0
		
		if Stats.Knocked_Out.Value or not Stats.Loaded.Value then return 0 end
		
		if Npc.Humanoid.MoveDirection.Magnitude > 0.5 and not Stats.Sprinting.Value then
			local Desired_Point = Get_Target_Point(Npc, false)
			if Desired_Point then
				Weight = ((Npc.PrimaryPart.Position - Desired_Point).Magnitude / 100) - 0.2
			end
			if Stats.In_Combat.Value then
				Weight += 2
			end
			if C_Stats.Current_Behaviour.Value == "Run_Away" then
				Weight += 1
			end
		end
		
		return Weight
	end,
	Sprint_End = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Stats = Npc.Character_Data
		local Weight = 0
		
		if Npc.Humanoid.MoveDirection.Magnitude < 0.5 and Stats.Sprinting.Value then
			Weight = 0.2
			Weight += (1-(Stats.Hunger.Value / 100)) / 2
		end
		
		return Weight
	end,
	Eat = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Stats = Npc.Character_Data
		local Weight = 0
		
		if Stats.Knocked_Out.Value or not Stats.Loaded.Value or Stats.Climbing.Value or C_Stats.Eating.Value or C_Stats.Heavy_Attacking.Value or C_Stats.Using_Skill.Value then return 0 end
		if C_Stats.Cooldowns.Eat.Value or C_Stats.Light_Attacking.Value or C_Stats.Blocking.Value then return 0 end

		Weight = 1 - (Stats.Hunger.Value / 100)
		
		if not Get_Food_Item(Stats) then
			if C_Stats.Current_Behaviour.Value == "Attack" or C_Stats.Current_Behaviour.Value == "Run_Away" or C_Stats.Current_Behaviour.Value == "Dialog" then
				Weight = 0
			end
		elseif Npc.Humanoid.Health < 40 then
			Weight *= 2
		end

		return Weight
	end,
	Find_Body = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Stats = Npc.Character_Data
		local Enemy = C_Stats.Target_Enemy.Value
		local Target_Body = C_Stats.Target_Body.Value
		local Weight = 0
		
		if Enemy and not Target_Body and (Npc.PrimaryPart.Position - Enemy.PrimaryPart.Position).Magnitude < 100 then
			if (C_Stats.Current_Behaviour.Value == "Attack" and Enemy.Humanoid.Health > 0) or (Enemy.Humanoid.Health <= 0 and not Enemy.Humanoid:FindFirstChild("Life_Absorbed")) then
				local Enemy_Stats = Enemy.Character_Data
				
				if Enemy_Stats.Knocked_Out.Value and not Enemy_Stats.Knocked_Out.Carry.Value then
					Weight = 2
				end
			end
		end
		
		return Weight
	end,
	Interact = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Stats = Npc.Character_Data
		local Target_Item = C_Stats.Target_Item.Value
		local Target_Body = C_Stats.Target_Body.Value
		local Desired_Point = Get_Target_Point(Npc, false)
		local Weight = 0
		
		if Stats.Knocked_Out.Value or not Stats.Loaded.Value or Stats.Climbing.Value or C_Stats.Eating.Value or C_Stats.Heavy_Attacking.Value or C_Stats.Using_Skill.Value then return 0 end
		if C_Stats.Blocking.Value or C_Stats.Light_Attacking.Value then return 0 end

		if Desired_Point then
			if C_Stats.Current_Behaviour.Value == "Get_Item" and Target_Item and (Desired_Point - Npc.PrimaryPart.Position).Magnitude < 8 then
				if not C_Stats.Target_Enemy.Value then
					Weight = 1.2
				else
					Weight = 0.6
				end
			elseif C_Stats.Current_Behaviour.Value == "Get_Body" and Target_Body and (Desired_Point - Npc.PrimaryPart.Position).Magnitude < 6 then
				Weight = 3
			end
		end

		return Weight
	end,
	Climb = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Stats = Npc.Character_Data
		local Target_Point = Get_Target_Point(Npc, false)
		local Root_Point = Npc.PrimaryPart.Position
		local Weight = 0
		
		if Stats.Knocked_Out.Value or not Stats.Loaded.Value then return 0 end
		
		if Target_Point then
			local Height = (Vector3.new(0,Target_Point.Y,0) - Vector3.new(0,Root_Point.Y,0)).Magnitude
			if Height > 3 and C_Stats.Stamina.Value > 15 + Height then
				local Can_Climb, Is_Down = Climb_Module.Detect(Npc)
				if Can_Climb then
					if (C_Stats.Path_Obstructed.Value and C_Stats.Path_Obstructed.Is_Wall.Value) or Is_Down then
						Weight = 1.5
					end
				end
			end
		end

		return Weight
	end,
	Jump = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Stats = Npc.Character_Data
		local Enemy = C_Stats.Target_Enemy.Value
		local Weight = 0
		
		if Stats.Knocked_Out.Value or not Stats.Loaded.Value then return 0 end
		
		if Npc.Humanoid.FloorMaterial ~= Enum.Material.Air then
			if C_Stats.Path_Obstructed.Value and not C_Stats.Path_Obstructed.Is_Wall.Value then
				Weight = 1
			end
		end

		return Weight
	end,
	Do_Nothing = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Weight = 0.1
		
		if C_Stats.Current_Behaviour.Value == "Do_Nothing" then
			Weight *= 2
		end
		
		return Weight
	end,
}

local Movement_Considerations = {
	Stop = function(Npc)
		return 0.1
	end,
}
for i,Vector_String in pairs(Move_Directions) do
	Movement_Considerations[Vector_String] = function(Npc)
		local C_Stats = Npc.Npc_Stats
		local Stats = Npc.Character_Data
		local Weight = 0
		local Desired_Point = Get_Target_Point(Npc, false)
		
		if Desired_Point then
			Weight = Check_Move_Direction(Npc,Desired_Point,Stats.Mouse_Locked.Value,Convert_Vector(Vector_String, Stats.Climbing.Value))
		end

		return Weight
	end
end


local Get_Best = function(Table, ...)
	local Best = {Weight = -1, Value = nil}
	for i,F in pairs(Table) do
		local Current_Weight = F(...)
		if Current_Weight > Best.Weight then
			Best = {Weight = Current_Weight, Value = i}
		end
	end
	return Best.Value, Best.Weight
end


local Actions_Main = function(Npc, Action_Module)
	local C_Stats = Npc.Npc_Stats
	local Stats = Npc.Character_Data
	
	local Behaviour = Get_Best(Behaviour_Considerations, Npc)
	local Action = Get_Best(Action_Considerations, Npc)
	
	if Npc.Name == "Npc_1" then
		--print(Behaviour,Action)
	end
	
	C_Stats.Current_Behaviour.Value = Behaviour
	
	if Action == "Equip_Weapon" then
		Equip_Any_Weapon(Npc, Action_Module)
	elseif Action == "Unequip_Weapon" then
		Unequip_Any_Weapon(Npc, Action_Module)
	elseif Action == "Eat" then
		Eat_Food(Npc, Action_Module)
	elseif Action == "Jump" then
		Npc.Humanoid.Jump = true
	elseif Action == "Find_Body" then
		C_Stats.Target_Body.Value = C_Stats.Target_Enemy.Value
	elseif Action ~= "Do_Nothing" then
		Action_Input(Npc, Action, Action_Module)
	end
end

local Movement_Main = function(Npc)
	local C_Stats = Npc.Npc_Stats
	local Stats = Npc.Character_Data
	
	local Direction = Get_Best(Movement_Considerations, Npc)
	
	if not Stats.Climbing.Value then
		if Direction ~= "Stop" then
			Npc.Humanoid:Move(Relative_Vector(Npc.PrimaryPart.CFrame, Convert_Vector(Direction, false)))
		else
			Npc.Humanoid:Move(Vector3.new(0,0,0))
		end
	else
		if Direction ~= "Stop" then
			C_Stats.Climb_Direction.Value = Convert_Vector(Direction, true)
		else
			C_Stats.Climb_Direction.Value = Vector3.new(0,0,0)
		end
	end
end

local Main = function(Npc)
	local Npc_Data = Npc_Data_Module[Npc.Name]

	if Npc_Data then
		local Module_Clone = Module_Scripts.Action_Module:Clone()
		local Action_Module = require(Module_Clone)
		local C_Stats = Npc:WaitForChild("Npc_Stats")
		local Stats = Npc:WaitForChild("Character_Data")
		local Look_Object = nil
		local Changes = {}

		Action_Module.Enable_Hitting(Npc)
		Action_Module.Enable_Cancelling(Npc)
		Stamina_Module.Stamina_Loop(C_Stats,Npc.Humanoid,Stats)
		
		
		
		spawn(function()
			while Npc.Humanoid:GetState() ~= Enum.HumanoidStateType.Dead and Npc.Parent do
				if C_Stats.Target_Enemy.Value then
					wait(0.1)
				else
					wait(0.5)
				end
				spawn(function()
					Actions_Main(Npc, Action_Module)
				end)
				spawn(function()
					Look_Object = Npc.PrimaryPart:FindFirstChild("Mouse_Lock")
					if Look_Object and not Stats.Climbing.Value then 
						if not Stats.Knocked_Out.Value then
							if Stats.Mouse_Locked.Value or C_Stats.Current_Behaviour.Value == "Dialog" then
								local Target_P = Get_Target_Point(Npc, true)

								if C_Stats.Current_Behaviour.Value == "Dialog" then
									Look_Object.D = 500 / Npc_Data.Dialog_Turn_Speed
								else
									Look_Object.D = 150
								end
								Look_Object.MaxTorque = Vector3.new(500000,500000,500000)

								if Target_P then
									Look_Object.CFrame = CFrame.new(Npc.PrimaryPart.Position, Vector3.new(Target_P.X,Npc.PrimaryPart.Position.Y,Target_P.Z))
								end
							else
								Look_Object.D = 150
								Look_Object.MaxTorque = Vector3.new(500000,0,500000)

								Look_Object.CFrame = CFrame.new(Npc.PrimaryPart.Position, Npc.PrimaryPart.Position + Vector3.new(0,0,-1))
							end
						else
							Look_Object.MaxTorque = Vector3.new(0,0,0)
						end
					end
					Movement_Main(Npc)
				end)
			end
		end)
		C_Stats.Target_Enemy:GetPropertyChangedSignal("Value"):Connect(function()
			local Add_Function = function(Stat)
				if Stat:IsA("ValueBase") and ((not Stat:IsA("NumberValue") and not Stat:IsA("IntValue")) or Stat.Parent.Name == "Blocking") then
					local Change_Event = Stat.Changed:Connect(function()
						Actions_Main(Npc, Action_Module)
					end)
					table.insert(Changes,1,Change_Event)
				end
			end
			for i,Event in pairs(Changes) do
				Event:Disconnect()
			end
			if C_Stats.Target_Enemy.Value then
				for i,Stat in pairs(C_Stats.Target_Enemy.Value.Character_Data:GetDescendants()) do
					Add_Function(Stat)
				end
				Add_Function(Stats.Stunned)
				Add_Function(Stats.Stunned.Dodge_Stun)
				Add_Function(Stats.Blocking)
				Add_Function(Stats.Blocking.Hit_Count)
			end
		end)
		spawn(function()
			while Npc.Humanoid:GetState() ~= Enum.HumanoidStateType.Dead and Npc.Parent do
				wait(1)
				local Target_P = Get_Target_Point(Npc, true)
				
				if Target_P then
					local Is_Blocked = Check_Target_Visible(Npc.PrimaryPart.Position, Target_P)
					
					C_Stats.Point_Visible.Value = not Is_Blocked
				end
				
				C_Stats.Desired_Weapon.Value = Find_Best_Weapon(Npc)
			end
		end)
		
		
		
		Stats.Inventory.ChildAdded:Connect(function(Item)
			local Data = Item_Data[Item.Name]
			if Data.Type == "Food" then
				Stats.Food_Item.Value = Item.Name
			end
		end)
		
		Npc.Humanoid.Died:Connect(function()
			Module_Clone:Destroy()
			if Look_Object then
				Look_Object:Destroy()
			end
			for i,Event in pairs(Changes) do
				Event:Disconnect()
			end
		end)
	end
end

local Select_Enemy = function(Player, Npc, Enemy)
	if (Player and Enemy and Player.Character ~= Enemy) then return end
	
	local C_Stats = Npc.Npc_Stats
	local Stats = Npc.Character_Data
	
	C_Stats.Target_Enemy.Value = Enemy
end

local Dialog_Change = function(Player, Npc, Bool)
	if Npc then
		local Character = Player.Character
		local Stats = Character.Character_Data
		local Npc_Stats = Npc.Npc_Stats
		
		if Bool then
			Npc_Stats.Target_Speaker.Value = Character
		else
			Npc_Stats.Target_Speaker.Value = nil
		end
	end
end

wait(1)

Remote_Events.Npc_Hostile_Event.OnServerEvent:Connect(Select_Enemy)
Server_Events.Npc_Hostile_Event.Event:Connect(Select_Enemy)

Server_Events.Npc_Character_Event.Event:Connect(Main)

Remote_Events.Set_Dialog_Event.OnServerEvent:Connect(Dialog_Change)
Server_Events.Set_Dialog_Event.Event:Connect(Dialog_Change)
1 Like

I have asked a few developers who scripted top anime and fighting-style games which have their npc checks in a while loop running every 0.1 seconds. This is the common rate you’ll find against large npc count games.