Tips / Mistakes with my FSM AI script?

Hello! I tried writing an AI that utilizes FSMs (Fixed State Machines).
Here’s the code:

CODE
-----------------------
-- 	   SERVICES 	 --
-----------------------
local PathfindingService = game:GetService("PathfindingService")

-----------------------
-- CONSTANTS / MODEL --
-----------------------
local model = script.Parent
local humanoid = model.Humanoid
local head = model.Head
local humanoidRootPart = model.HumanoidRootPart

---------------------------
-- CONSTANTS / ANIMATION --
---------------------------
local animator = humanoid:FindFirstChild("Animator")

-- // GUARDING ANIMATION
local guardAnim = Instance.new("Animation")
guardAnim.AnimationId = "rbxassetid://137451581517346"

local guardTrack = animator:LoadAnimation(guardAnim)
guardTrack.Priority = Enum.AnimationPriority.Idle



-----------------------
-- CONSTANTS / VALUE --
-----------------------
local LOSAngle = 0.5
local FOVDist = 35
local TICK_RATE = 0.5
local MoveToAreaTimeout = 3

-----------------------
--  CONSTANTS / AI   --
-----------------------
local MAX_RETRIES = 5
local RETRY_COOLDOWN = 5
local YIELDING = false
local AGENT_PARAMETERS = {
	AgentCanClimb = true,
	Costs = {
		AgentCanClimb = true,
		Avoid = math.huge,
	}
}

local reachedConnection
local pathBlockedConnection

local currentPos = humanoidRootPart.Position
local facingCFrame = humanoidRootPart.Orientation

local raycastParams = RaycastParams.new()
raycastParams.FilterDescendantsInstances = {model}
raycastParams.FilterType = Enum.RaycastFilterType.Exclude
raycastParams.IgnoreWater = true


local path = PathfindingService:CreatePath()
-----------------------
-- 	   FUNCTIONS 	 --
-----------------------
local function getPositionInRadius(radiusPos: Vector3, radius: number)
	
	-- Generate a random angle and a random distance from the center
	local angle = math.random() * 2 * math.pi
	local distance = math.sqrt(math.random()) * radius

	-- Convert polar coordinates to Cartesian
	local X = math.cos(angle) * distance
	local Z = math.sin(angle) * distance

	local walkPos = radiusPos + Vector3.new(X, 0, Z)

	return walkPos
end

local function playerInFOV()
	local players = game:GetService("Players"):GetPlayers()
	

	for _, player in players do
		if player.Character:WaitForChild("Humanoid").Health > 0 then
			local hrp = player.Character.HumanoidRootPart
			
			local dir = (hrp.Position - humanoidRootPart.Position).Unit
			local rayDir = dir * FOVDist
			
			local angle = dir:Dot(humanoidRootPart.CFrame.LookVector)
			
			local rayResult = workspace:Raycast(humanoidRootPart.Position, rayDir, raycastParams)
			
			if rayResult and angle >= LOSAngle then
				return true, rayResult.Position
			end
		end
	end
end

local function WalkTo(targetPosition: Vector3, Yieldable: boolean)
	local RETRY_NUM = 0
	local success, errorMessage
	
	repeat
		RETRY_NUM += 1
		success, errorMessage = pcall(path.ComputeAsync, path, humanoidRootPart.Position, targetPosition)
		if not success then
			warn("Pathfind compute path error: "..errorMessage)
			task.wait(RETRY_COOLDOWN)
		end
	until success == true or RETRY_NUM > MAX_RETRIES
	
	if success then
		if path.Status == Enum.PathStatus.Success then
			local waypoints = path:GetWaypoints()
			local currentWaypointIndex = 2
			
			if not reachedConnection then
				reachedConnection = humanoid.MoveToFinished:Connect(function(reached)
					if reached and currentWaypointIndex < #waypoints then
						currentWaypointIndex += 1

						humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
						if waypoints[currentWaypointIndex].Action == Enum.PathWaypointAction.Jump then
							humanoid.Jump = true
						end
					else
						reachedConnection:Disconnect()
						pathBlockedConnection:Disconnect()
						reachedConnection = nil
						pathBlockedConnection = nil
						YIELDING = false
					end
				end)
			end
			
			pathBlockedConnection = path.Blocked:Connect(function(waypointNumber)
				if waypointNumber > currentWaypointIndex then
					reachedConnection:Disconnect()
					pathBlockedConnection:Disconnect()
					reachedConnection = nil
					pathBlockedConnection = nil
					WalkTo(workspace.EndGoal.Position, true)
				end
			end)

			humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
			if waypoints[currentWaypointIndex].Action == Enum.PathWaypointAction.Jump then
				humanoid.Jump = true
			end

			if Yieldable then
				YIELDING = true
				repeat 
					task.wait()
				until YIELDING == false
			end
			---------------------------------------
		else
			return
		end
	else
		warn("Pathfind compute retry maxed out, error: "..errorMessage)
		return
	end
end
-----------------------
-- 	    STATES 	     --
-----------------------
local state = {}
local currentState = nil
local seenPos = nil

function state.ReturningToPost()
	WalkTo(currentPos, true)
	humanoidRootPart.Orientation = facingCFrame
	currentState = state.Guard
end

function state.Attack()
	print("!!")
	if playerInFOV() then
		WalkTo(seenPos, false)
	else
		WalkTo(seenPos, false)
		task.wait(1.5)
		print("hm..")
		currentState = state.ReturningToPost
	end		
	
end

function state.CheckLastSeen()
	WalkTo(seenPos, false)
	if playerInFOV() then
		print("!")
		currentState = state.Attack
	else
		guardTrack:Play()
		task.wait(MoveToAreaTimeout)
		guardTrack:Stop()
		currentState = state.ReturningToPost
	end
end

function state.Guard()
	if not guardTrack.IsPlaying and not playerInFOV() then
		guardTrack:Play()
	elseif guardTrack.IsPlaying and playerInFOV() then
		_, seenPos = playerInFOV()
		guardTrack:Stop()
		print("?")
		task.wait(1.5)
		print("...")
		currentState = state.CheckLastSeen

	end
end

-- // DEFAULT STATE
currentState = state.Guard
--------------------

while true do
	currentState()
	task.wait(TICK_RATE)
end

I wanted to ask if there are any mistakes I’m doing or any ways to improve / optimize the code (I already know something might be wrong.)

Here’s the sketch of my FSM too:


Made using draw.io. Pretty useful site.

All and any criticism (constructive and not plain rude), advice and else are accepted. I really want to do something advanced for once.

1 Like

Probably not allowed to bump, but I really need help.

I have tested your script, nothing is wrong and everything is working as it should be.

Thing is - the attack state does not seem to work - the player does not get chased when spotted. I’m not sure how to do targetting and such.