Finite state machines were the standard way to structure robot behaviour for decades, and ROS 1 robots often ran on SMACH. Today Nav2 is built on behaviour trees, and many teams assume trees have replaced state machines. They haven't: each is better at a different kind of problem. This comparison uses one example robot to show where each shines, and where each one hurts.
The same robot, two ways
Our robot fetches a cup from the kitchen and brings it to the table. It has to stop and charge when its battery runs low.
As a state machine, the robot is always in exactly one state, and events move it between states: GoToKitchen, then FindCup, PickCup and GoToTable, with a transition for each "done" event. That is four transitions, and easy to read. Now add the battery. "Battery low" can happen in any of the four working states, so each needs its own transition to Charge: four more. After charging, the robot should carry on where it stopped, so Charge needs a way back to each of the four states, plus a variable that remembers which one: four more again. Twelve transitions, where the task itself needs four. A second interruption that can happen anywhere, such as a person blocking the way, adds eight more.
As a behaviour tree, the battery check is one ReactiveSequence and a Fallback wrapped around the whole task, as built in our introduction. It is the same four extra nodes whether the task has four steps or forty, and the task itself doesn't change.
To be fair to state machines, the twelve transitions are a problem of flat machines. A hierarchical state machine puts the four working states inside a Working superstate, with one transition from Working to Charge, and a "history" feature lets it return to the sub-state it left. Good state machine libraries support both. The difference that remains is how each one gets back on track.
Remembering versus re-checking
A state machine resumes by remembering: it knows it was in PickCup, so it goes back there. A behaviour tree usually starts again from the top and re-checks the world. For that to work, each step checks whether its job is already done before doing it:
<root BTCPP_format="4">
<BehaviorTree ID="FetchCup">
<Sequence>
<Fallback>
<IsHoldingCup/>
<Sequence>
<GoTo name="go_to_kitchen" target="kitchen"/>
<PickCup/>
</Sequence>
</Fallback>
<GoTo name="go_to_table" target="table"/>
</Sequence>
</BehaviorTree>
</root>
We ran this with BehaviorTree.CPP 4.10. When IsHoldingCup failed, the robot went to the kitchen, picked the cup and went to the table. When it succeeded, as it would after a recharge in the middle of the delivery, the tree skipped straight to the table.
Re-checking is more robust: if someone took the cup from the robot while it was charging, the tree notices and fetches another, while a state machine resuming at GoToTable would deliver nothing. It also costs more design effort. Every step needs a condition that can be checked, and some things, such as "have I already announced my arrival?", are hard to observe from sensors.
Side by side
| State machine | Behaviour tree | |
|---|---|---|
| Basic idea | One active state; events trigger transitions | A tree ticked repeatedly; nodes answer SUCCESS, FAILURE or RUNNING |
| Interruptions | A transition from every state, or a superstate | One reactive node above the task |
| Reuse | States are tied to their transitions, so moving one means rewiring | Subtrees report only success or failure, so they drop into other trees |
| Resuming | Remembers the last state | Re-checks conditions from the top |
| Debugging | "Where am I?" is one state name | The active path through the tree; needs a viewer such as Groot |
| Event-driven protocols | Natural: states and messages map one to one | Awkward: events must be turned into conditions that can be polled |
| Learning curve | Familiar to most engineers | Reactive nodes, halting and the blackboard take time to learn |
When a state machine is the better choice
- Modes. A robot that is Idle, Manual, Autonomous or Emergency stop is in exactly one mode at a time, and the transitions between modes are a safety question you want to see written down.
- Protocols. A docking handshake, a charger negotiation or a device driver waiting for specific replies is a sequence of states driven by messages, which is what state machines describe best.
- Small, stable behaviour. With five states that rarely change, a state machine is shorter and easier for a newcomer to follow.
When a behaviour tree is the better choice
- Tasks with many ways to fail. Retries, fallbacks and recoveries are where trees shine: Nav2's tree tries a costmap clear, then a spin, a wait and a backup, as our Nav2 walkthrough traces step by step.
- Reusable skills. "Pick object" or "dock" written once as a subtree can be used in many missions, by different people.
- Behaviour that changes often. Trees are loaded from XML, so a new mission can be a new file instead of new code.
Using both
Many robots combine them, and that is often the best answer. A small state machine handles the robot's modes, and in Autonomous mode it runs a behaviour tree for the task. Going the other way, the individual actions inside a tree are often small state machines themselves: a BehaviorTree.CPP StatefulActionNode has onStart, onRunning and onHalted callbacks, which are exactly the states of one action.
Libraries for ROS 2
All of these have binary packages for ROS 2 Jazzy:
- Behaviour trees: BehaviorTree.CPP (C++; version 4.10 in Jazzy), used by Nav2 and edited with Groot; py_trees and py_trees_ros (Python).
- State machines: SMACH (Python, ported from ROS 1); YASMIN (Python and C++); FlexBE (Python, with a graphical editor and operator control at run time).
Whichever you choose, draw it. A state diagram or a tree diagram that the whole team can read catches more mistakes than any code review. For trees, paste your XML into the Behaviour Tree Visualizer to see it drawn and checked. For the theory, Colledanchise and Ögren's book Behavior Trees in Robotics and AI: An Introduction compares trees with state machines and other architectures in depth.