How would I improve this entity system I'm currently working on?

Heya Everyone!!

I’m working on an entity script and all I’m looking for is what I can do to improve the entity system such as readability, better practices, and so on. I’d like to also mention that self.EntityBehaviors part will be separated to a data ModuleScript for better maintenance.

--[[SERVICES]]--
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local PathfindingService = game:GetService("PathfindingService")
local RunService = game:GetService("RunService")

--[[FOLDERS]]--
local EntityFolder = ServerStorage:WaitForChild("Entities")
local RegularFolder = EntityFolder.Regular
local SpecialFolder = EntityFolder.Special

--[[MODULE]]--
local BaseEntity = {}
BaseEntity.__index = BaseEntity

--//Creating the entity
function BaseEntity:CreateEntity(EntityName)
	--//Finding entity
	local RequestedEntity = RegularFolder:FindFirstChild(EntityName) if not EntityName then
		RequestedEntity = SpecialFolder:FindFirstChild(EntityName) if not EntityName then
			warn("CANNOT FIND ENTITY: "..EntityName)
			return
		end
	end
	
	--//Creating entity
	local self = setmetatable({}, BaseEntity)
	
	self.Entity = RequestedEntity:Clone()
	self.Humanoid = self.Entity:FindFirstChildWhichIsA("Humanoid")
	self.EntitySettings = require(self.Entity.EntitySettings)
	self.Entity.Parent = game.Workspace
	
	--//List of entity behaviors & assinging them to their respective entity.
	--//EntityBehaviors will be in a seperate module for better maintenance.
	self.EntityBehaviors = {
		["Starvlud"] = {
			["Behavior"] = function()
				self:SearchAndChaseTarget()
				self:Wander()
			end,
		}
	}
	
	task.wait(2)
	self.EntityBehaviors.Starvlud.Behavior()

	return self
end

--//Default functions
function BaseEntity:Wander()
	self.Humanoid.WalkSpeed = self.EntitySettings.WanderingSpeed
	local NewPath = PathfindingService:CreatePath()
	
	while task.wait() do
		if self.EntitySettings.IsChasing then return end
		
		--//Getting random walkpoints. This might be bad practice.
		local Walkpoints = workspace:WaitForChild("Walkpoints"):GetChildren()
		local RandomWalkPoint = Walkpoints[math.random(1,#Walkpoints)]

		NewPath:ComputeAsync(self.Entity.PrimaryPart.Position, RandomWalkPoint.Position)

		if NewPath.Status == Enum.PathStatus.Success then
			local Waypoints = NewPath:GetWaypoints()

			for _, Waypoint in pairs(Waypoints) do
				
				if self.EntitySettings.IsChasing then
					break
				end
				self.Humanoid:MoveTo(Waypoint.Position)
				local Timeout = self.Humanoid.MoveToFinished:Wait(2)
				if not Timeout then
					warn("Entity got stuck on something. Repathfinding.")
					self:Wander()
				end
				
			end
		end
		
	end
end

function BaseEntity:InvestigateNoise()
	--//TBA
end

function BaseEntity:SearchAndChaseTarget()
	local Target = nil
	local NewPath = PathfindingService:CreatePath()
	
	RunService.Heartbeat:Connect(function()
		
		if not self.EntitySettings.IsChasing then
			
			local SightDirection = self.Entity.PrimaryPart.CFrame.LookVector
			local Raycast = workspace:Raycast(self.Entity.PrimaryPart.Position, SightDirection * self.EntitySettings.SightLength)
			if not Raycast then return end --//Little check to see if the raycast hit anything.

			local RaycastInstance = Raycast.Instance
			if RaycastInstance then
				self.EntitySettings.IsChasing = true
				self.Humanoid.WalkSpeed = self.EntitySettings.ChasingSpeed
				Target = Raycast.Instance.Parent
			end
			
		else
			
			--//Creating a thread for the ChaseDuration
			local Coro = coroutine.create(function()
				local Timer = self.EntitySettings.ChaseDuration
				
				repeat
					Timer -= 1
					task.wait(0.5)
				until Timer <= 1
				self.EntitySettings.IsChasing = false
				self.Humanoid.WalkSpeed = self.EntitySettings.WanderingSpeed
			end)
			coroutine.resume(Coro)
			
			--//Pathfinding
			NewPath:ComputeAsync(self.Entity.PrimaryPart.Position, Target.PrimaryPart.Position)
			if NewPath.Status == Enum.PathStatus.Success then
				local Waypoints = NewPath:GetWaypoints()

				for i, Waypoint in pairs(Waypoints) do
					if i <= 2 then continue end
					if Waypoints[4] then
						
						if not self.EntitySettings.IsChasing then
							break
						end
						self.Humanoid:MoveTo(Waypoints[4].Position)
						local Timeout = self.Humanoid.MoveToFinished:Wait(2)
						if not Timeout then
							warn("Entity got stuck on something. Repathfinding.")
							self:SearchAndChaseTarget()
						end
						
					end
				end
			end
			
		end
		
	end)
end

function BaseEntity:TryJumpscare()
	--//TBA
end

return BaseEntity

You are checking EntityName twice, instead check it only once and on the second if not EntityName replace it with a check for RequestedEntity

function BaseEntity:CreateEntity(EntityName)
–//Finding entity
local RequestedEntity = RegularFolder:FindFirstChild(EntityName) if not EntityName then
RequestedEntity = SpecialFolder:FindFirstChild(EntityName) if not EntityName then
warn("CANNOT FIND ENTITY: "…EntityName)
return
end
end

In the Wander() fuinction you can wrap it up in a coroutine instead of constantly looping it, its a good practice to avoid infinite loops in the future unless you absolutely need to use one. Also ive noticed in if Waypoints[4] then you are setting the index to 4? It would be better to set the index concretely to the waypoints Waypoints[#Waypoints]

Put the entire pathfinding code inside of a coroutine. I’d assume you where thinking it like this?

--//Default functions
function BaseEntity:Wander()
	self.Humanoid.WalkSpeed = self.EntitySettings.WanderingSpeed
	local NewPath = PathfindingService:CreatePath()
	
	local Coro = coroutine.create(function()
		if self.EntitySettings.IsChasing then return end

		--//Getting random walkpoints. This might be bad practice.
		local Walkpoints = workspace:WaitForChild("Walkpoints"):GetChildren()
		local RandomWalkPoint = Walkpoints[math.random(1,#Walkpoints)]

		NewPath:ComputeAsync(self.Entity.PrimaryPart.Position, RandomWalkPoint.Position)

		if NewPath.Status == Enum.PathStatus.Success then
			local Waypoints = NewPath:GetWaypoints()

			for _, Waypoint in pairs(Waypoints) do

				if self.EntitySettings.IsChasing then
					break
				end
				self.Humanoid:MoveTo(Waypoint.Position)
				local Timeout = self.Humanoid.MoveToFinished:Wait(2)
				if not Timeout then
					warn("Entity got stuck on something. Repathfinding.")
					self:Wander()
				end

			end
		end
	end)
	coroutine.resume(Coro)
end

As for the Waypoint[4] part, the reason why I’m setting it to specifically 4 is that, iirc, it would prevent the NPC from stuttering, especially when near the player.

Yea, exactly. You could also swap out coroutine.create with task.spawn() if you ever want to simplify your code even more. Overall tho your code is pretty fine and readable, you could change some minor things here and there such as instead of IsChasing to isChasingPlayer