Hello and welcome to the next lesson in our journey with Finite State Machines!
In the last lesson, you brought your NPC to life by implementing a Chase state, using PathfindingService to make it intelligently pursue the player. Now, we're going to give your NPC a purpose for that chase.
This lesson addresses the learning outcome: Add an 'Attack' state, triggered when the NPC is within attack range during the 'Chase' state. We will complete the fundamental "Patrol -> Chase -> Attack" behavior loop, which is a cornerstone of AI in many games.

From Chasing to Attacking
Right now, your NPC will chase the player indefinitely until they get too far away. The logical next step is to make the NPC act when it finally catches its target. This means that within the Chase state's logic, we need to constantly check the distance to the player.
We already do this to see if the player is too far away (> CHASE_MAX_DISTANCE). Now, we'll add a second check to see if the player is close enough to attack.
1. The Chase to Attack Transition
Let's modify the chaseState.Update function you built in the last lesson. We need to define an attack range and then check against it.
First, add a new configuration variable at the top of your script. A value between 8 and 15 studs is usually a good starting point for a ranged attack.
-- Add this near your other configuration variables
local ATTACK_RANGE = 10
Now, find your chaseState.Update function. Inside the main logic block, right after you calculate the distance to the target, add a new if statement.
-- Inside chaseState.Update function...
-- ... (code to check if target is lost or too far away) ...
-- Check if target is close enough to attack
if distance <= ATTACK_RANGE then
print("Target in attack range. Transitioning to Attack.")
fsm:transitionTo("Attack", chaseState.currentTarget) -- Pass the target to the Attack state
return -- Exit the Chase state's update
end
-- == CORE CHASE LOGIC ==
-- ... (rest of your pathfinding logic) ...
This logic is simple but powerful. If the player is within ATTACK_RANGE, we command the FSM to transition to a new state, "Attack," and we pass along the target so the Attack state knows who to attack.
2. Building the Attack State
Of course, transitioning to "Attack" won't work until we define what that state does. Let's create it. The attack behavior will have three parts:
- Enter: Stop moving and face the target.
- Update: Continuously fire at the target as long as it's alive and in range. Transition back to
Chaseif the target runs away. - Exit: Clean up when the attack is over.
The following video demonstrates a very robust way to handle an attack, using a separate thread (task.spawn) to manage the firing loop. This prevents the attack action from freezing the rest of our FSM.
NPC Attack Mode using Finite State Machines
This video from SimTek Game Development provides an excellent, in-depth look at implementing an attack state. We will adapt its core concepts for our FSM.
Watch from 34:07 to 48:33. You don't need to copy the code exactly, as it's part of a larger class structure, but focus on these key patterns: (34:07) Checks to ensure the target is valid before attacking. (37:35) Turning to face the target using CFrame.new(position, lookAt). (45:00) The use of task.spawn to create a non-blocking while loop that handles continuous firing. This is the most important concept to grasp. (47:50) The check inside the loop that causes a transition to a Cooldown state when the fight is over.
Inspired by that video, let's build our attackState.
Exercise: Implement the Attack and Cooldown States
Add the following code to your NPC script, alongside your other state definitions.
1. Add New Configuration
First, let's add a variable for how fast the NPC should attack.
-- Add near other config variables
local ATTACK_RATE = 0.5 -- Time in seconds between each shot
2. Create the attackState
This is the most complex part. Read the comments carefully to understand what each part does.
local attackState = {
currentTarget = nil,
isAttacking = false, -- A flag to prevent the attack loop from starting multiple times
Enter = function(target)
print("NPC entered Attack state. Target: " .. target.Name)
attackState.currentTarget = target
attackState.isAttacking = false
-- Stop any movement from the Chase state
humanoid:MoveTo(npc.PrimaryPart.Position)
-- Turn to face the target
local targetRoot = attackState.currentTarget:FindFirstChild("HumanoidRootPart")
if targetRoot then
local newCFrame = CFrame.new(npc.PrimaryPart.Position, targetRoot.Position)
npc.PrimaryPart.CFrame = newCFrame
end
end,
Update = function(dt)
local target = attackState.currentTarget
local targetRoot = target and target:FindFirstChild("HumanoidRootPart")
local targetHumanoid = target and target:FindFirstChild("Humanoid")
-- == Guard Clauses: Conditions to EXIT the state ==
if not targetRoot or not targetHumanoid or targetHumanoid.Health <= 0 then
print("Target is invalid or defeated. Transitioning to Cooldown.")
fsm:transitionTo("Cooldown")
return
end
local distance = (npc.PrimaryPart.Position - targetRoot.Position).Magnitude
if distance > ATTACK_RANGE then
print("Target moved out of attack range. Transitioning back to Chase.")
fsm:transitionTo("Chase", target) -- Pass the target back to the Chase state
return
end
-- Keep facing the target
npc.PrimaryPart.CFrame = CFrame.new(npc.PrimaryPart.Position, targetRoot.Position)
-- == Core Attack Loop ==
-- Start the attack loop only if it's not already running
if not attackState.isAttacking then
attackState.isAttacking = true
-- Use task.spawn to run this in parallel, so it doesn't block Update
task.spawn(function()
while attackState.isAttacking and fsm:getCurrentState() == "Attack" do
-- Re-check target health before firing
if targetHumanoid.Health > 0 then
print("NPC is attacking " .. target.Name)
-- Here you would play an animation, sound, and deal damage
targetHumanoid:TakeDamage(10)
else
-- If target is defeated, break the loop
break
end
task.wait(ATTACK_RATE)
end
end)
end
end,
Exit = function()
print("NPC is leaving Attack state.")
attackState.currentTarget = nil
attackState.isAttacking = false -- This will stop the while loop in the coroutine
end,
}
This Stack Overflow post also shows a clear example of the logic for transitioning between Chase and Attack based on distance, which is what we've just implemented.
Is this a correct way to implement a basic FSM in Lua for my FSM diagram
This Stack Overflow post shows a straightforward implementation of the logic we're building. It's a good reference for the core transition conditions.
Read the code snippets for newZombie.chaseState.Update and newZombie.attackState.Update. Notice how the chaseState transitions to attackState when distanceToTarget is less than MAX_ATTACK_RADIUS, and the attackState transitions back to chaseState when the distance becomes greater.
3. Create the cooldownState
As seen in the SimTek video, it's good practice to have a Cooldown state after a fight. This creates a natural pause before the NPC returns to its normal duties.
local cooldownState = {
Enter = function()
print("NPC entered Cooldown state.")
-- The coroutine will start a timer that transitions after a delay
coroutine.wrap(function()
task.wait(3) -- Wait for 3 seconds
-- Ensure we are still in the Cooldown state before transitioning
if fsm:getCurrentState() == "Cooldown" then
print("Cooldown finished. Transitioning to Patrol.")
fsm:transitionTo("Patrol")
end
end)()
end,
Update = function(dt)
-- This state is purely time-based, so the update function is empty.
-- All logic is handled in the Enter function's coroutine.
end,
Exit = function()
print("NPC is leaving Cooldown state.")
end,
}
4. Update Your FSM Definition
Finally, add your new states to the table where you define all the states for your FSM, so the transitionTo function can find them.
-- Find your fsm states table and add the new states
local states = {
Idle = idleState,
Patrol = patrolState,
Chase = chaseState,
Attack = attackState, -- Add this
Cooldown = cooldownState, -- And this
}
Testing Your Full Combat Loop
It's time to see it all in action!
- Start your game. The NPC should begin its
Idle/Patrolroutine. - Walk into the NPC's detection range. It should transition to
Chaseand start following you. - Let the NPC get close to you (within 10 studs). The console should print "Target in attack range" and the NPC should transition to
Attack. - The NPC should stop moving, turn to face you, and start printing "NPC is attacking..." every 0.5 seconds. Your character should start taking damage.
- Now, run away from the NPC. Once you are more than 10 studs away, it should transition back to
Chaseand start pursuing you again. - Let the NPC defeat your character. Once your health is zero, the NPC should transition to
Cooldown, wait for 3 seconds, and then return to itsPatrolstate.
Conclusion
Excellent work! You have now implemented the core combat loop for an AI agent. Your NPC can now patrol, detect and chase a target, engage in combat, and return to its routine after the fight is over.
Key Takeaways:
- State transitions are driven by conditions. The
Chasestate transitions toAttackbased on a distance check in itsUpdatefunction. - The
Attackstate often requires a self-contained loop. Usingtask.spawnorcoroutine.wrapis the standard way to run this loop without blocking the entire FSM. - States can be simple. The
Cooldownstate shows that a state can be as simple as a timed delay to create more believable pacing in your AI's behavior.
Next Steps
Your NPC is becoming quite formidable! It can patrol, chase, and attack. But what if the player gets the upper hand? In our next lesson, we will implement a 'Flee' state triggered by a low health condition, adding a self-preservation instinct to your AI's repertoire.