AI Development: Finite State Machines

Heya DevForum users, I hope you’re all doing well during this time and enjoying that extra time with your families :D.

Today I’m going to release something I’ve worked on in the past but never really used it much, so I decided to share it with the community!

But first I decided to ensure users are familiar with the concept

For those of you who aren’t Computer Science gurus like myself or aren’t familiar with the concept, a Finite State Machine is defined as:

“A concept used in designing computer programs or digital logic. There are two types of state machines: finite and infinite-state machines. The former is comprised of a finite number of states, transitions, and actions that can be modelled with flow graphs, where the path of logic can be detected when conditions are met. The latter is not practically used.” as stated by an article online.

You can read up more on what a State Machine is here: What is a State Machine?

By now you should be connecting the dots and realizing how this is useful in the world of AI development if not, no worries.

How Are They Useful?

Finite State Machines allow you to outline the states of a machine, or in this case the AI… with that you can then assign behaviours to that particular state.

Example: Le’ts say you have an NPC in an Idle state, within that state that NPC may play an Idle animation, maybe look around etc…

The Anatomy of a State Machine

Let’s take a look at the finite state machine of a turnstile:
image image

All state machines are initiated or start with an initial state, this is the first state every machine starts off as. In the above diagram that is represented by the black dot at the bottom of the Locked state

This machine has two states: Locked and Unlocked
This machine accepts two Events/Triggers/Input, whatever you’d like to call it they’re all acceptable within their context and they’re Push and Coin.

When the machine is given these inputs it may change states, this however depends on the current state the machine is in and the states the triggers transition to.

Here’s a representation of this machine in a simplified code form using if-else statements:

local states = {
 "Locked";
 "Unlocked";
}
-- This indicates the machine's initial state, which is the locked state
local machine_current_state = states[1]

-- This functions transitions to another state given the right conditions
function switchStates(input)
-- If our input/trigger is Push we check our current state,
  if input == "Push" then
   -- If the current state is Locked, then we remain in it
     if machine_current_state == "Locked" then
       machine_current_state = states[1]
   -- If the current state is Unlocked, then we move to the Locked state
    elseif machine_current_state == "Unlocked" then
       machine_current_state = states[1]
    end

-- If our input/trigger is Coin we check our current state,
 elseif input == "Coin" then
   -- If the current state is Locked, then we remain in it
     if machine_current_state == "Locked" then
       machine_current_state = states[2]
   -- If the current state is Unlocked, then we move to the Unlocked state
    elseif machine_current_state == "Unlocked" then
       machine_current_state = states[2]
    end
  end
end

And there you have it a simple state machine system :smiley:

Now, due to the already somewhat large size of this thread, unfortunately, I can’t go any more in-depth on State Machines but I advise you to read up more on it, they’re really interesting and not to mention useful

The Resource

This module allows you to quite easily construct a new state machines with your predefined states and inputs in an array format.

--<USAGE>--
	local stateModule = pathToModule
	
	local my_states = {
		["Walking"] = { 
			["Stop"] = "Idle";
			["Jump"] = "Falling";
		}
		
		["Idle"] = {
			["Walk"] = "Walking";
			["Jump"] = "Falling";
		}
		
		["Falling"] = {
			["Landed"] = "Idle";
		}
	}
	
	local machine = require(stateModule).new("Idle", my_states)
	
	machine:switch("Walk")
	print(machine.current_state) --> "Walking"
	
	machine:switch("Jump")
	print(machine.current_state) --> "Falling"
	
	machine:switch("Landed")
	print(machine.current_state) --> "Idle"
	
	machine.onStateChanged:Connect(function(oldState, newState)
		print("Machine changed state from :"..oldState.." To : "..newState)
	end)

Here is an example with AI applications:

Colors used to visually represent the AI’s current state
Color | State
Green | Idle
Red | Walking
Blue | Jumping
Gray | Landed
White | Falling

Gif:

--This may not be the most efficient way to utilize the machine, but I wrote it on a whim for demonstration purposes
local PetObject = workspace:WaitForChild("Pet")
local Points = workspace:WaitForChild("Points")
local states = {
		["Idling"] = {
			["Walk"] = "Walking";
			["Jump"] = "Jumping";
		};
		
		["Walking"] = { 
			["Idle"] = "Idling";
			["Jump"] = "Jumping";
		};
		
		["Jumping"] = {
			["Idle"] = "Idling";
			["Fall"] = "Falling"
		};
		
		["Falling"] = {
			["Land"] = "Landed"
		};
		
		["Landed"] = {
		 	["Idle"] = "Idling";
			["Jump"] = "Jumping";
			["Walk"] = "Walking"
	    }
}
local state_machine = require(game.ReplicatedStorage.state).new("Idling", states)

-- When called changes the color of the pet
local function changeColor()
	if state_machine.current_state == "Idling" then
		PetObject.BrickColor = BrickColor.Green()
	elseif state_machine.current_state == "Walking" then
		PetObject.BrickColor = BrickColor.Red()
	elseif state_machine.current_state == "Jumping" then
		PetObject.BrickColor = BrickColor.Blue()
	elseif state_machine.current_state == "Falling" then
		PetObject.BrickColor = BrickColor.White()
	elseif state_machine.current_state == "Landed" then
		PetObject.BrickColor = BrickColor.Gray()
	end
end


local initTick = nil
local walkToTarget = nil
local maxJumpHeight = 15
local originPoint = PetObject.CFrame

game["Run Service"].Heartbeat:Connect(function()
	print(state_machine.current_state)
	-- Run behavior for idle state
	if state_machine.current_state == "Idling" then
		-- If the pet is in the idling state then
		-- The pet will idle for 5 seconds and then begin to roam around to random positions on the map
		-- The pet will simply rotate while in the idling state
		if initTick == nil then
			initTick = tick()
		end
		
		-- timer
		if math.ceil(tick() - initTick) == 5 then
			state_machine:switch("Walk", changeColor)
		end
		
		-- if the pet object isn't anchored, anchor it in air
		if not PetObject.Anchored then
			PetObject.Anchored = true
			PetObject.CFrame = PetObject.CFrame * CFrame.new(0, 2, 0)
		end
		
		-- rotate pet object
		PetObject.CFrame = PetObject.CFrame * CFrame.Angles(0, math.rad(10), 0)
	end
	
	-- Run behavior for walking state
	if state_machine.current_state == "Walking" then
		-- if no target exists, we find one
		if not walkToTarget then
			walkToTarget = Points[math.random(1, #Points:GetChildren())]
		end
		print(walkToTarget.Name)
		print(walkToTarget.Position.Magnitude - PetObject.Position.Magnitude)
		-- Walk to the random target if the distance is greater than 1
		if (walkToTarget.Position.Magnitude - PetObject.Position.Magnitude) >= 1 then
			PetObject.CFrame = PetObject.CFrame:Lerp(CFrame.new(walkToTarget.CFrame.Position), 0.05)
			-- We continue to apply the velocity until the state has changed
			-- or the pet is near the object
		else
			-- If the distance from the target and whatnot is less than 1 we switch states
			state_machine:switch("Jump", changeColor)
		end
		
	end
	
	-- Run behavior for jumping state
	if state_machine.current_state == "Jumping" then 
		if PetObject.CFrame.Position.Y < maxJumpHeight then
			PetObject.CFrame = PetObject.CFrame:Lerp(PetObject.CFrame * CFrame.new(0, 1, 0), 0.5)
		else
			state_machine:switch("Fall", changeColor)
		end
	end

	-- Run behavior for falling state
	if state_machine.current_state == "Falling" then 
		if PetObject.CFrame.Position.Y > 1 then
			PetObject.CFrame = PetObject.CFrame:Lerp(PetObject.CFrame * CFrame.new(0, -1, 0), 0.4)
		else
			state_machine:switch("Land", changeColor)
		end
	end
	
	--Run behavior for landed state 
	if state_machine.current_state == "Landed" then 
		state_machine:switch("Idle", changeColor)
	end
end)

state_machine.onStateChanged:Connect(function(oldState, newState)
	-- When the state changes from idle we reset the timer
	-- Optionally of course you could have included the line
	-- before calling the switch function 
	if oldState == "Idling" then
		initTick = nil
	elseif oldState == "Walking" then
		walkToTarget = nil
	end
end)

Here is the place file if anyone wants to experiment: FiniteStateMachine.rbxl (23.4 KB)

33 Likes

Thanks! I am looking for a way to implement animation transition, this is useful!

1 Like