(REALEASE) StateMachine

Hey ya’ll. While making my game I realized that managing state isn’t entirely too easy. I have seen many many solutions from other developers that are generally over-engineered (mostly from first year CS students), so I decided to create my own module for this.

You can find the source code here: GitHub - tannnxr/StateMachine: This is a simple state machine for Roblox.

Feel free to contribute to it.

Tutorial

Go to the Github Repo and copy the raw script.

Create a new script in wherever you want the module to be accessed from.

Then require the module and instantiate a new StateMachine like so.

local StateMachine = require(--[[ Path to Module ]])

-- [[ The state machine will always set the first state as the default state ]]
local playerState = StateMachine.newState({"Running", "Walking", "Jumping", "Falling"})

Congratulations! You just created a state machine.

But how do we actually manage state?? Glad you asked. Let me show you how to set states.

-- Obviously create your state machine first

playerState:setState("Jumping")

Congratualtions, the state has now been changed from the first state (default) “Running” to “Jumping”

Keep in mind you can only change the state to a state that you instantiated the StateMachine.

But tanner, how do we get the state from the machine, so far you only showed us how to create it and how to set a state!!!

Glad you asked, because I made this method last minute completely forgetting that you’d need to be able to get the state.

playerState:getCurrentState()

This will return the current state from the machine.

But what if I want to get the state anytime it changes so I can modify things in real time. Don’t worry, I got you. There are BindableEvents for you!

Simply do this:

playerState.stateChanged.Event:Connect(function(oldState, newState)
	print(`State Changed: {oldState} -> {newState}`)
end)

What about error handling?? Well I wanted to leave that up to you (the developer) to decide how to handle errors so I just added in another bindable event for errors.

playerState.error.Event:Connect(function(msg)
	print(msg)
end)

This is fairly versatile, so feel free to do what you want with it!!!

Also please do contribute to make this module better, I genuinely need help im going insane.

Edits

#1 If there are any issues or bugs with the code please do open an issue for myself or another contributer to fix.

6 Likes

That looks like a solid resource! I made a pull request on github (lmk if i did wrong i don’t often use it)

1 Like

This is both overengineered and underengineered imo

Underengineered as in it lacks the cyclical nature that state machines should have (im not too familiar with state machines myself but i know the principles of it)

  • Imo states in a state machine should have exclusive info that lets them branch to specific states, and not others. E.g. Walking to jumping but not jumping to walking as Falling needs to be between them

Overengineered as in it has some excessive design issues in general. On second thought its more like Unpractically engineered if anything

  • Why use string values? Makes no sense
  • The tableContains thing is also unnecessary
  • Why are you setting the values of the metatable after its definition rsther than during its definition
  • Warnings are being used incorrectly. Use ‘error’ instead, as warnings are non-issues

Its just not very good code at all. I can revise it for you later on and help fix these problems


I handwrote some new-and-improved code (out of boredom; im at work)

3 Likes

What do you recommend in place of string values? A normal string?

I considered adding this to the machine, after doing some research. I just have to figure out how to implement the code for this in some way.

This is to check if they are trying to set to a state that doesn’t exist in the stateables, I am unaware of any way to check that without my function that I wrote.

That is just how I learned to create objects in Roblox, so that’s how I did it.

I used an error to prevent the code from breaking if they tried to set a state to the current state, makes no sense to stop the thread with an error for something that already returns early and doesn’t change anything if they try.

Edit: Feel free to contribute to the project, or open an issue in the github with documentation, and sources so I can research further.

1 Like

Yes

table.find(t, v) or t[v]. prior is for arrays latter for dicts with non-(false or nil) values

Theres zero reason to use string values in this case, or almost ever.

heres the code remade

local stateMachine = {}
stateMachine.__index = stateMachine

local stateObject = {}
stateObject.__index = stateObject

function stateMachine.New(states)
	local self = setmetatable({
		states={},
		current=nil,
		StateChanged=Instance.new('BindableEvent'),
	}, stateMachine)
	for _, v in ipairs(states) do
		self:NewState(v[1], v[2])
	end
	return self
end

function stateMachine:NewState(name, branches)
	local newSelf = setmetatable({name=name, master=self, branches=branches or {}}, stateObject)
	self.master.states[name] = newSelf
	
	return newSelf
end

function stateObject:PushBranch(...)
	for _, v in ipairs({...}) do
		self.branches[v] = true
	end
end

function stateObject:PullBranch(...)
	for _, v in ipairs({...}) do
		self.branches[v] = false
	end
end

function stateObject:CanBranch(target)
	return self.branches[target]
end

function stateMachine:Next(target)
	if (self.current) then
		if (not self.current.branches[target]) then
			return
		end
	end
	self.current = self.current.branches[target]
end

function stateMachine:Get(state)
	return self.states[state]
end

function stateObject:Destroy()
	self.master.states[self.name] = nil
	setmetatable(self, nil)
end

function stateMachine:Destroy()
	for _, v in (self.master.states) do
		v:Destroy()
	end
	self.StateChanged:Destroy()
	setmetatable(self, nil)
end

return stateMachine
local sM = stateMachine.New({
        {'Idle', {'Jumping', 'Falling', 'Climbing', 'Running'}},
        {'Running', {'Jumping', 'Falling', 'Climbing', 'Idle'}},
        {'Jumping', {'Falling', 'Climbing'}},
        {'Climbing', {'Jumping', 'Running', 'Idle'}}
        {'Falling', {'Idle', 'Running', 'Climbing'}}
})

sM:Next('Idle')
task.wait(2)
sM:Next('Jumping')
sM:Next('Running') -- impossible, as you're going in an upward direction and cant suddenly start walking

Nah its good i just remade it since the one i wrote at work was badly designed

Alright ran into an issue here

function stateMachine:NewState(name, branches)
	local newSelf = setmetatable({name=name, master=self, branches=branches or {}}, stateObject)
	self.master.states[name] = newSelf

	return newSelf
end

error:

ReplicatedStorage.Lib.StateMachine:21: attempt to index nil with 'states'

I replaced self with newSelf and then that just created another error with another piece of code.

ReplicatedStorage.Lib.StateMachine:48: attempt to index nil with 'branches'
1 Like

That is an intresting approach, however i think the original design of the module (architecture) was pretty solid. I don’t see the reason of this more complex branches/master system.

1 Like

Remove .master
That was my mistake

@Ilucere is right, statemachines need to be more complex to prevent certain states from being changed.

Like they explained, you can’t go from jumping straight to running, because you’d have to go through the “Falling” state first.

The branches works amazingly

1 Like

No it wasnt solid because it had a major component of state machiens missing
Ngl the bad practises he did dont even matter much, its mainly just the design. I was a d1 hater earlier

1 Like

Yea now an issue with this:

function stateMachine:Next(target)
	if (self.current) then
		if (not self.current.branches[target]) then
			return
		end
	end
	self.current = self.current.branches[target]
end
  11:40:40.127  ReplicatedStorage.Lib.StateMachine:48: attempt to index nil with 'branches'  -  Client - StateMachine:48
1 Like

replace
self.current.branches[target]
with
self.states[target]

My mistake

Also i dont think i made the event fire in the code i gave you
Anyhow i declare that its your code now, add some features and make it really your own, and repost the community resource

Thank you I appreciate it, I think I have to fix an issue with it not preventing a state change. Specifically with going from jumping → running; which is marked as impossible by you

1 Like
local stateMachine = {}
stateMachine.__index = stateMachine

local stateObject = {}
stateObject.__index = stateObject

function stateMachine.New(states)
	local self = setmetatable({
		states={},
		current=nil,
		StateChanged=Instance.new('BindableEvent'),
	}, stateMachine)
	for _, v in ipairs(states) do
		self:NewState(v[1], v[2])
	end
	return self
end

function stateMachine:NewState(name, branches)
	local newSelf = setmetatable({name=name, master=self, branches=branches or {}}, stateObject)
	self.states[name] = newSelf

	return newSelf
end

function stateObject:PushBranch(...)
	for _, v in ipairs({...}) do
		local f = table.find(self.branches, v)
		if (not f) then
			table.insert(self.branches, v)
		end
	end
end

function stateObject:PullBranch(...)
	for _, v in ipairs({...}) do
		local f = table.find(self.branches, v)
		if (f) then
			table.remove(self.branches, f)
		end
	end
end

function stateObject:CanBranch(target)
	return self.branches[target]
end

function stateMachine:Next(target)
	if (self.current) then
		if (not table.find(self.current.branches, target)) then
			return
		end
	end
        local last = self.current
	self.current = self.states[target]
        self.StateChanged:Fire(last.name, self.current.name)
end

function stateMachine:Get(state)
	return self.states[state]
end

function stateObject:Destroy()
	self.master.states[self.name] = nil
	setmetatable(self, nil)
end

function stateMachine:Destroy()
	for _, v in (self.master.states) do
		v:Destroy()
	end
	self.StateChanged:Destroy()
	setmetatable(self, nil)
end

return stateMachine

this worked fine for me