If you’ve ever tried building an NPC that does more than just stand still, you’ve probably run into a familiar problem: your code becomes a tangled mess of if-statements and flags. One moment your guard is chasing a player, the next they should patrol, but wait—they were in the middle of an attack animation! Sound familiar?
This is where state machines come in. They’re one of the most important patterns in game AI, and once you understand them, you’ll wonder how you ever lived without them. Let’s dive in and build one together!
What Is a State Machine?
A state machine is a way of organizing behavior where your NPC can only be in one state at a time. Think about it like this: a guard NPC might have these states:
- Idle – Standing around, looking for trouble
- Patrol – Walking along a set path
- Chase – Running after a player they’ve spotted
- Attack – Fighting the player
- Return – Walking back to their post after losing sight of the player
The beauty of this approach is that each state has its own clear logic, and you define exactly which states can transition to which other states. No more spaghetti code!
Why Use a State Machine?
Before we start coding, let’s talk about why this pattern is so powerful:
- Organization: Each state’s code is separate and focused. When your attack behavior has a bug, you know exactly where to look.
- Predictability: You control exactly when and how states change. No more “how did my NPC get into this weird situation?” moments.
- Scalability: Adding new behaviors means adding new states, not untangling existing code.
- Debugging: You can print the current state and instantly understand what your NPC thinks it’s doing.
Building Our State Machine Class
Let’s start by creating a reusable StateMachine class. We’ll keep it simple but powerful:
local StateMachine = {}
StateMachine.__index = StateMachine
function StateMachine.new(npc)
local self = setmetatable({}, StateMachine)
self.npc = npc
self.states = {}
self.currentState = nil
self.currentStateName = nil
return self
end
function StateMachine:AddState(name, stateTable)
self.states[name] = stateTable
end
function StateMachine:ChangeState(newStateName)
-- Exit the current state
if self.currentState and self.currentState.OnExit then
self.currentState.OnExit(self.npc)
end
-- Change to new state
self.currentStateName = newStateName
self.currentState = self.states[newStateName]
-- Enter the new state
if self.currentState and self.currentState.OnEnter then
self.currentState.OnEnter(self.npc)
end
end
function StateMachine:Update(deltaTime)
if self.currentState and self.currentState.OnUpdate then
self.currentState.OnUpdate(self.npc, deltaTime, self)
end
end
return StateMachine
This class does three important things:
- OnEnter – Runs once when entering a state (perfect for playing animations or setting initial values)
- OnUpdate – Runs every frame while in that state (this is where the state’s main logic lives)
- OnExit – Runs once when leaving a state (great for cleanup)
Creating States for a Guard NPC
Now let’s build actual states for a guard NPC. We’ll create each state as a table with those three functions:
The Idle State
local IdleState = {}
function IdleState.OnEnter(npc)
local humanoid = npc:FindFirstChild("Humanoid")
if humanoid then
humanoid.WalkSpeed = 0
end
print(npc.Name .. " is now idle")
end
function IdleState.OnUpdate(npc, deltaTime, stateMachine)
-- Look for nearby players
local nearestPlayer = findNearestPlayer(npc, 30)
if nearestPlayer then
stateMachine:ChangeState("Chase")
else
-- After 3 seconds of idle, start patrolling
wait(3)
stateMachine:ChangeState("Patrol")
end
end
function IdleState.OnExit(npc)
-- Nothing special needed when leaving idle
end
The Chase State
local ChaseState = {}
function ChaseState.OnEnter(npc)
local humanoid = npc:FindFirstChild("Humanoid")
if humanoid then
humanoid.WalkSpeed = 24 -- Run fast!
end
print(npc.Name .. " is chasing!")
end
function ChaseState.OnUpdate(npc, deltaTime, stateMachine)
local humanoid = npc:FindFirstChild("Humanoid")
local nearestPlayer = findNearestPlayer(npc, 50)
if not nearestPlayer then
-- Lost sight of player
stateMachine:ChangeState("Return")
return
end
local distance = (npc.HumanoidRootPart.Position - nearestPlayer.Character.HumanoidRootPart.Position).Magnitude
if distance < 5 then
-- Close enough to attack
stateMachine:ChangeState("Attack")
else
-- Keep chasing
humanoid:MoveTo(nearestPlayer.Character.HumanoidRootPart.Position)
end
end
function ChaseState.OnExit(npc)
local humanoid = npc:FindFirstChild("Humanoid")
if humanoid then
humanoid.WalkSpeed = 16 -- Return to normal speed
end
end
The Attack State
local AttackState = {}
function AttackState.OnEnter(npc)
npc.HumanoidRootPart.Anchored = true
print(npc.Name .. " is attacking!")
-- Play attack animation here
end
function AttackState.OnUpdate(npc, deltaTime, stateMachine)
local nearestPlayer = findNearestPlayer(npc, 10)
if not nearestPlayer then
stateMachine:ChangeState("Return")
return
end
local distance = (npc.HumanoidRootPart.Position - nearestPlayer.Character.HumanoidRootPart.Position).Magnitude
if distance > 7 then
-- Player moved away, chase again
stateMachine:ChangeState("Chase")
else
-- Deal damage (simplified)
wait(1) -- Attack cooldown
local playerHumanoid = nearestPlayer.Character:FindFirstChild("Humanoid")
if playerHumanoid then
playerHumanoid:TakeDamage(10)
end
end
end
function AttackState.OnExit(npc)
npc.HumanoidRootPart.Anchored = false
end
Helper Functions
Our states need some helper functions. Here's a simple one to find nearby players:
local function findNearestPlayer(npc, maxDistance)
local nearestPlayer = nil
local shortestDistance = maxDistance
for _, player in pairs(game.Players:GetPlayers()) do
if player.Character and player.Character:FindFirstChild("HumanoidRootPart") then
local distance = (npc.HumanoidRootPart.Position - player.Character.HumanoidRootPart.Position).Magnitude
if distance < shortestDistance then
shortestDistance = distance
nearestPlayer = player
end
end
end
return nearestPlayer
end
Putting It All Together
Now let's create an NPC and give it our state machine:
local StateMachine = require(script.StateMachine)
local npc = workspace.GuardNPC -- Your NPC model
-- Create the state machine
local ai = StateMachine.new(npc)
-- Add all our states
ai:AddState("Idle", IdleState)
ai:AddState("Chase", ChaseState)
ai:AddState("Attack", AttackState)
-- Add Patrol and Return states here too
-- Start in the Idle state
ai:ChangeState("Idle")
-- Main update loop
local RunService = game:GetService("RunService")
RunService.Heartbeat:Connect(function(deltaTime)
ai:Update(deltaTime)
end)
Tips and Best Practices
Here are some things I've learned building state machines:
- Keep states focused: Each state should do one thing well. If a state is getting complicated, consider splitting it into multiple states.
- Use OnEnter and OnExit: These are perfect for setting up and cleaning up. Don't scatter initialization code throughout OnUpdate.
- Debug with prints: Add print statements to your OnEnter functions. You'll immediately see if your NPC is switching states too quickly or getting stuck.
- Consider using enums: Instead of strings like "Idle", create a table of state names to avoid typos.
- Don't overuse wait(): In production code, use tick() or os.clock() to track time instead of yielding with wait().
Recap and Next Steps
Congratulations! You've just built a flexible, maintainable state machine for NPC AI. You now understand how to organize complex behaviors into clear, separate states, and how to control transitions between them.
Your state machine handles entering states, updating them each frame, and exiting them cleanly. This pattern will serve you well in countless game development scenarios—not just NPCs, but also game modes, UI systems, and more.
Ready to level up? Try these next steps:
- Add more states: Implement Patrol and Return states to complete the guard behavior
- Create state transitions: Build a system that validates whether a state transition is allowed
- Learn behavior trees: These are the next evolution of AI systems, perfect for even more complex NPCs
- Explore path finding: Combine your state machine with Roblox's PathfindingService for smarter navigation
Happy scripting, and may your NPCs always know what state they're in!