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:
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
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)