Is this code messy?

Hello, i have made a npc monster that patrols each 20 seconds and chase near targets (players) when on sight. However, i decided to create a module script to deal with the npc states, since i thought it was hard to get with it

I have utilized a very simple module too: “Simple Path”, that just pathfinds with 1 line of code and works beauty.

Enemy AI script:

-- Services
local ServerStorage = game:GetService("ServerStorage")
local RunService = game:GetService("RunService")

local me = script.Parent
local patrolwaypoints = workspace.Waypoints

local StatesMachine = require(script.StatesMachine)
local SimplePath = require(ServerStorage.SimplePath)

local myHuman = me:WaitForChild("Humanoid")
local myTorso = me:WaitForChild("HumanoidRootPart")
local myHead  = me:WaitForChild("Head")

-- Sounds
local attackSound = myHead:WaitForChild("Attack")

-- Animations
local attack = script.Attack
local attackAnim = myHuman.Animator:LoadAnimation(attack)

-- Configurations
local config = me:WaitForChild("Configuration")
local fieldOfView = config.FieldOfView.Value
local distanceOfView = config.DistanceOfView.Value
local patrolSpeed = config.PatrolSpeed.Value
local chaseSpeed = config.ChaseSpeed.Value
local chaseRest = config.ChaseRest.Value
local timeoutSight = config.TimeoutSight.Value

-- Variables
local sm = StatesMachine.new()

local Path = SimplePath.new(script.Parent)
Path.Visualize = true

local target = nil
local targetDistance = nil

local startedChasing = nil
local lostSight = nil

-- Makes the monster to be only physics handled by the server
myTorso:SetNetworkOwner(nil)

-- Main functions
function Patrol()
	local randomSpot = patrolwaypoints:GetChildren()[math.random(#patrolwaypoints:GetChildren())]
	myTorso.CFrame = randomSpot.CFrame
end

function Chase()
	if not target then
		sm:ChangeState("Patrol")
		return 
	end

	-- Tired of chasing target
	if os.clock() - startedChasing > chaseRest then
		sm:ChangeState("Patrol")
		return
	end

	-- Not on sight anymore, stay trying to chase for a few seconds
	if onSight(target) then
		lostSight = os.clock()
	elseif os.clock() - lostSight > timeoutSight then
		sm:ChangeState("Patrol")
		return
	end
	
	
	if not behindObstacle(target) then
		myHuman:MoveTo(target.Position)
		
		if targetDistance <= 4 then
			-- Kill target
			target.Parent:FindFirstChild("Humanoid"):TakeDamage(100)
		end
	else	
		Path:Run(target) -- Just follow the target
	end
end

function Main()
	sm:BindListenner(function()	
		target, targetDistance = findTarget()

		if target and onSight(target) and sm:GetCurrentState() == "Patrol" then
			sm:ChangeState("Chase")
		end
	end)
	
	-- Patrol
	sm:AddState({
		name = "Patrol",
		
		released = function()
			myHuman.WalkSpeed = patrolSpeed
		end,
		
		run = Patrol,
		cooldown = 20,
	})
	
	-- Chase
	sm:AddState({
		name = "Chase",
	
		released = function()
			startedChasing = os.clock()
			myHuman.WalkSpeed = chaseSpeed
		end,

		run = Chase,
		cooldown = .1,
		
		finished = function()
			myHuman:MoveTo(myTorso.Position)
		end,
	})
	
	-- Start with patrol as default
	sm:ChangeState("Patrol")
	sm:Resume()
end
Main()


-- Sub functions
function liesInFOV(target)
	local directionToTarget = (target.Position - myTorso.Position).Unit
	return myTorso.CFrame.LookVector:Dot(directionToTarget) > math.cos(math.rad(fieldOfView/2))
end

function behindObstacle(target)
	local params = RaycastParams.new()
	params.FilterType = Enum.RaycastFilterType.Blacklist
	params.FilterDescendantsInstances = { script.Parent }

	local origin = myTorso.Position
	local direction = (target.Position - myTorso.Position).Unit
	local result = workspace:Raycast(origin, direction * distanceOfView, params)

	if result then
		if result.Instance:IsDescendantOf(target.Parent) then
			return false
		end
	end
	return true
end

function onSight(target)
	local distToTarget = (myTorso.Position - target.Position).Magnitude
	return (liesInFOV(target) and not behindObstacle(target)) or distToTarget < 10
end

function findTarget()
	local dist = distanceOfView
	local target = nil

	for i, v in ipairs(workspace:GetChildren()) do
		local human = v:FindFirstChild("Humanoid")
		local torso = v:FindFirstChild("Torso") or v:FindFirstChild("HumanoidRootPart")

		if human and torso and v.Name ~= script.Parent.Name then
			local newDist = (myTorso.Position - torso.Position).Magnitude
			if newDist < dist and human.Health > 0 then
				target = torso
				dist = newDist
			end
		end
	end
	return target, dist
end

StatesMachine (the modulescript i have made):

Obs:

  • Released function its a function that is called with the first iteration with the state
  • Run function its a function that is called every frame in the state
  • Finished its a function that is called with the last iteration with the state
local StatesMachine = {}
StatesMachine.__index = StatesMachine

StatesMachine.states = {}

StatesMachine.previousState = ""
StatesMachine.currentState = ""
StatesMachine.nextState = ""

function StatesMachine.new()
	local self = {}
	setmetatable(self, StatesMachine)
	return self
end

function StatesMachine:AddState(newstate)
	newstate.cooldown = newstate.cooldown or 0
	newstate._clock = os.clock()
	
	table.insert(self.states, newstate)
	self.nextState = newstate.name
	self.currentState = newstate.name
end

function StatesMachine:BindListenner(listenner)
	self.listenner = listenner
end

function StatesMachine:Resume()
	local run = coroutine.create(_run)
	coroutine.resume(run, self)
end

function StatesMachine:ChangeState(stateName)
	self.nextState = stateName
	self.scheduled = nil
	
	for i, state in ipairs(self.states) do
		if state.name == stateName then
			if state.released then
				state.released()
			end
			
			if state.run then
				--coroutine.wrap(state.run)()
				state.run()
			end
		end
	end
end

function StatesMachine:ScheduleState(stateName, scheduleTime, overrideSchedule)
	if overrideSchedule == false and self.scheduled then return end
	
	self.scheduled = {
		name = stateName,
		_clock = os.clock(), 
		_scheduleTime = scheduleTime
	}
end

function StatesMachine:GetCurrentState()
	return self.currentState
end

function _run(self)
	while task.wait() do	
		-- Call listenner interface
		if self.listenner then self.listenner() end
		
		-- Scheduled
		if self.scheduled then		
			if os.clock() - self.scheduled._clock > self.scheduled._scheduleTime then
				self:ChangeState(self.scheduled.name)
				self.scheduled = nil
			end
		end
		
		-- Update states
		for i, state in ipairs(self.states) do
			if state.name ~= self.currentState then continue end

			if self.currentState ~= self.nextState then
				self.currentState = self.nextState
				if state.finished then state.finished() end
				continue
			end

			if self.previousState ~= self.currentState then
				if state.released then state.released() end
			end
			self.previousState = self.currentState

			if state.run then	
				if os.clock() - state._clock > state.cooldown then
					--coroutine.wrap(state.run)()
					state.run()
					state._clock = os.clock()
				end
			end
		end
	end
end

return StatesMachine

--[[
local sm = StatesMachine.new()

sm:AddState({
	name = "Patrol",
	
	released = function()
	
	end,
	
	run = function()
		
	end,
	
	finished = function()
	
	end
})
]]

I just want to know if this is a messy code or potentially dangerous with bugs. Thanks.

This code is very clean and fresh, keep it up!!

1 Like

It’s not terrible, but I’d highly recommend not abbreviating sm (and also not using "me" for character). Also not sure why the path variable is capitalized when most of the other variables aren’t.

I’d really highly recommend this youtuber https://www.youtube.com/@CodeAesthetic if you’re interested in the writing better code side of things! While not all of his coding videos are 100% relevant to lua (since we don’t have classes), you can take things out of certain videos like this one:

3 Likes

That code is obviously very neat and clean (at least compared to my style of coding), but this entire post just feels like a deliberate humblebrag to flex the cleanness.

That was not that obvious to me since idk how others may have implemented this AI.