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.