Arch - A powerful hierarchical state machine library.

Arch - Hierarchical State Machine Library for Roblox

Documentation | Github Repo | Roblox Model | Wally Install
Feel free to ask questions in reply to this post or in the OSS Server, where I am more active.

Overview

Arch is an open-source hierarchical state machine (HSM) library designed specifically for Roblox developers. Leveraging the principles of hierarchical state machines, Arch provides a structured and efficient framework for managing states within Roblox projects. With a focus on precise control, adaptability, and seamless transitions, Arch empowers developers to model and manage complex game logic efficiently.

Features

  • Hierarchical State Management: Arch enables the creation of a structured system where states can be organized hierarchically, offering a clear and intuitive way to represent complex game behaviors.

  • Seamless Transitions: States seamlessly transition between each other, providing precise control over the flow of game logic. This feature is particularly valuable in scenarios such as combat systems, dynamic interfaces, and diverse game scenarios.

  • Versatile Toolkit: Arch equips developers with a versatile toolkit for creating responsive and engaging gameplay experiences. Whether you’re crafting combat systems, dynamic interfaces, or intricate game scenarios, Arch provides the flexibility needed to meet diverse requirements.

  • Conflict Resolution: Arch includes robust conflict resolution mechanisms, ensuring smooth handling of complex scenarios within your game. This feature enhances the reliability and stability of your project.

Acknowledgments

Arch is inspired by the principles of hierarchical state machines and draws influence from the State Chart XML (SCXML) standard.

17 Likes

Version 0.3.0
Updated the documentation to explain self-transitions and targetless transitions.
Added getMachine function for accessing machines outside of their creation context.
Added options.id for increased flexibility for setting machine ids.
Updated logging api.
Fixed the machine:Stop() function.

1 Like

Is there a way you can check the current state/ what state the HSM is in at a given time? i printed the getmachine() function and it had a bunch of stuff should i perhaps get it from there?
image

Every machine includes a read-only table called configuration, which is a list of all active states. The first index of configuration: machine.configuration[1] is the most recently entered state, aka the most deeply nested state. Keep in mind that this returns the actual state table so to get the name of the state you would do machine.configuration[1].id. In the future I plan to add a machine.value property that holds just the most deeply nested state’s name for ease of access.

1 Like

Ah alright, my bad didnt read this, thanks

2 Likes

Hey after testing around I’ve noticed that you cannot transition from 1 atomic state to another atomic state if both are in different compound states, but it works if you first switch to the compound state and then the atomic state, is it meant to be that way?

I’m not encountering that issue, can you send a repro machine definition?

Version 0.3.1
Added machine.onTransition event
Added machine.value and machine.history
Sift deepCopy definition for module reusability
Changed context.target from state to state.id

Tutorial
I haven’t added these additions to the documentation yet, so I will explain here:

onTransition is a machine-level signal, meaning you can access it through the machine. It returns a data table with three properties: context, value, and history.

Context is explained in the documentation.
value is a list of active atomic states, aka the most deeply nested state for each parallel branch.
history is similar to value, except it is a list of states exited in the most recent microstep.

Also, value and history are accessible at the machine-level, so you can call machine.value or machine.history to get the same variables. I added these properties because of @Ronaldo202076’s post where I recommended using configuration, which is an inconvenient solution.

Here is how you would use onTransition and the new machine properties:

local machine = Arch.getMachine("myMachine")
print(machine.value, machine.history) -- prints the value and last value of the machine

machine.onTransition:Connect(function(data)
  print("A macrostep has been completed, here is the state of the machine", data.context, data.value, data.history)
end)

Is there a way to call a function every frame once a state is entered? I didn’t see any mention of something like this in the documentation, unless i just missed it. Also, is there a way to make a state machine work on the client? When I tried to create a state machine on the client it didn’t work, and since the state machine I’m trying to make deals with player input, having the state machine on the sever would be really inconvenient and would undermine the efficiency of it, especially since the action i want to perform is supposed to be done on the client.

It seems you want activities, which are functions that run continuously while a state is active. This isn’t built-in yet, but you can create the same behavior using RunService and janitors.
Here is an example:

StateA = {
   janitor = true,
   OnEntry = function(context)
      local connection = RunService.Heartbeat:Connect(function()
         --do stuff here every frame
      end)
      context.janitor:Add(connection, "Disconnect") -- automatically disconnects the heartbeat connection when the state is exited
   end,
}

Arch allows you to create state machines on the client, it’s the same as if you were to create one on the server. It would be pretty weak if it couldn’t lol. How are you creating your machine and what is your definition? Are there any errors?

1 Like

So uhhh… when I was testing to see if it worked on the client I think I made a mistake or something like that cause it works just fine now lol. Sorry about the inconvenience. Also, I was wondering if it was possible to have multiple states active at the same time. I’m not talking about how in parallel states, child states are entered and exited at the same time, more like the states are separate, but can be active simultaneously, and even if you exit one of the states, the other one wont exit as well. For example, you can be grounded, but you can also be sliding, running, idle etc. unless that’s the point of a hierarchical state machine and I’m misunderstanding things. I was asking this question mostly because I was wondering if there was a way to check to see if a specific state was active, rather than the most recently active state, but then i realized that asking the question would be pointless if you couldn’t have more than one state active at a time (other than parallel states) in the first place. So, yeah.

Yeah, that’s what makes Arch so powerful - hierarchy. You could have a Grounded state with Sliding, Running, etc. substates. Here is an example definition:

Grounded = {
  initial = "Idle",

  states = {
    Idle = {},
    Walking = {},
    Running = {},
  }
}

In the above definition, a machine could be in the grounded state and one of the substates such as Walking or Running. It is important to note, if you exit the Grounded state, all active descendant states such as Walking will be exited, but exiting a descendant state does not necessarily mean the parent state will be exited. Whether you’re running or walking, you’ll still be grounded. I’ll work on making the documentation better at explaining these things. Here is the documentation page.

1 Like

Sorry for the late reply. I was working on other projects and I completely forgot to check the forums (lol). Anyways, thank you for the info! Much appreciated :slight_smile:.

1 Like

I was wondering if you could add guards to OnEntry or OnExit events. It’s never mentioned in the documentation so I just wanted to make sure. Also, this is just an idea and you by no means have to add this, but I think it would be pretty useful if you added a function that checks to see if a state is active (unless of course you already added this and I’m being an idiot). I’m thinking of something like this:

local function checkIfActive(stateid: string,statemachine: any): boolean
	for _, activestates in pairs(statemachine.configuration) do -- assumes that the confugutation property of a state machine only holds active states and holds all active states
		if activestates.id == stateid then
			return true
		end
	end
	return false
end

You can not add guards to OnEntry or OnExit actions. Rather, you should put guards in transitions. Generally, I recommend putting actions in transitions instead of OnEntry and OnExit when it makes sense which allows for more control over behavior.
No plans to add a built-in way to check if a state is active, but your solution seems to work well.

1 Like

Ok then, thanks for the reply! :+1:

1 Like

Is it possible to access the state machine with in it’s configuration? For example:

local Machine = {
	id = "Example",
	initial = "ExampleState",
	states = {
		ExampleState = {
			OnEntry = function()
				if (condition()) then
					self:Send("ExampleState2")
				end
			end,
		},
		ExampleState2 = {
			-- more stuff goes here
		}
	}
}

Something like this? Thanks :slight_smile:

1 Like

nvm, I figured out a way to do it.

For those of you who want to know how I did it:

local Arch = require(--directory to arch module)
local mystatemachine --declare statemachine variable
local Machine = {
	id = "Example",
	initial = "ExampleState",
	states = {
		ExampleState = {
			OnEntry = function()
				if (condition()) then
					mystatemachine:Send("ExampleState2") --use delcared variable as if equal to statemachine
				end
			end,
		},
		ExampleState2 = {
			-- more stuff goes here
		}
	}
}

--set delcared variable equal to statemachine
mystatemachine = Arch.createMachine(Machine)

Your welcome :slight_smile:

Also, just wondering, Is there way to access the history of states?

The correct way to send events to the machine from within the machine, a vital behavior, is through the use of context. Context is the first parameter passed to every action and guard. It includes the following built-in parameters: .janitor if you have a state’s janitor property set to true, .Send for sending intenral events to the machine, and .event which is the event that triggered the transition to take place.
Here is an example:

OnEntry = function(context)
  context.Send("eventName") --same as machine:Send
end

I will be reworking the documentation to include this and other important information.

1 Like

What are you wanting to access a state’s history for?

1 Like