✨ This article is part of a broader discussion in my book Safe by Design: Explorations in Software Architecture and Expressiveness. If you like it, you might enjoy the full read for free on GitHub. This article is part of a broader discussion in my book Safe by Design: Explorations in Software Architecture and Expressiveness Safe by Design: Explorations in Software Architecture and Expressiveness Safe by Design: Explorations in Software Architecture and Expressiveness . If you like it, you might enjoy the full read for free on GitHub. Introduction Introduction When I began working on my real-time strategy (RTS) game project, one of the first challenges I tackled was unit behavior. RTS units often need to follow predictable yet flexible patterns — gathering resources, constructing buildings, patrolling, attacking, and more — and managing that behavior in a clean, scalable way is essential for a maintainable game engine. This post is part of a larger series where I explore architectural decisions and technical solutions I encountered during development. In this article, I’ll describe how I used finite state machines (FSMs) to structure unit behavior, starting from a simple example — chopping down trees. While the example is modest in scope, it illustrates the principles behind a composable, decoupled architecture that can be extended to much more complex behaviors. Motivation Motivation When thinking about where to begin building an RTS engine, I decided to start with something concrete and work toward abstraction — not the other way around. My first practical problem was resource gathering, specifically chopping wood. In most RTS games, the process looks something like this: a worker receives a command to harvest wood, walks toward a tree, spends some time chopping it down, then carries the logs back to a storage building, deposits them, and repeats the cycle. While this behavior may seem simple at first glance, implementing it in a way that is maintainable, extensible, and testable requires a clear separation of concerns — which led me to the use of finite state machines. FSMs allow this seemingly linear behavior to be broken down into discrete, composable states with well-defined transitions. This not only improves clarity and predictability but also makes the logic easier to debug and extend later. A Simple Behavioral Model A Simple Behavioral Model Let’s start by modeling the basic cycle of wood harvesting. Stripped down to essentials, the process consists of four repeating actions: Walking toward the tree Chopping wood Carrying logs to the storage Depositing resources Walking toward the tree Chopping wood Carrying logs to the storage Depositing resources Visually, this can be represented as a loop: → Walk to Tree → Chop → Carry to Storage → Deposit → (repeat) → Walk to Tree → Chop → Carry to Storage → Deposit → (repeat) Each of these actions can be treated as a state, and transitions between them occur when specific conditions are met — for example, when the unit reaches the tree or finishes chopping. Initially, this loop can be described in broad strokes, but for the behavior to become truly modular and reusable, we’ll need to define precise transition rules and encapsulate each step within its own FSM state. state Formalizing States and Transitions Formalizing States and Transitions To turn this intuitive cycle into a proper finite state machine, we need to define: States — atomic modes of behavior, such as WalkingToTree, Chopping, Carrying, Depositing Transitions — conditions under which the unit moves from one state to the next States — atomic modes of behavior, such as WalkingToTree, Chopping, Carrying, Depositing States WalkingToTree Chopping Carrying Depositing Transitions — conditions under which the unit moves from one state to the next Transitions Each transition is triggered by an event or a predicate evaluated continuously or upon state completion. For instance: event predicate Transition from WalkingToTree → Chopping occurs when the unit is within range of the target tree Transition from Chopping → Carrying happens when the inventory is full Transition from Carrying → Depositing when the unit reaches the storage building Transition from Depositing → WalkingToTree when the deposit is complete and there are more trees available Transition from WalkingToTree → Chopping occurs when the unit is within range of the target tree WalkingToTree Chopping Transition from Chopping → Carrying happens when the inventory is full Chopping Carrying Transition from Carrying → Depositing when the unit reaches the storage building Carrying Depositing Transition from Depositing → WalkingToTree when the deposit is complete and there are more trees available Depositing WalkingToTree This model is simple enough to grasp and simulate, but powerful enough to generalize to other unit behaviors. Importantly, each state is implemented independently — it knows nothing about the larger strategy, which makes testing and reusability easier. Per-State Behavior Handling Per-State Behavior Handling Each state in the FSM encapsulates its own internal logic and life cycle. Typically, this includes: OnEnter — code that runs once when the state becomes active OnUpdate — code that runs every frame while the state remains active OnExit — code that runs once when the state is about to change OnEnter — code that runs once when the state becomes active OnEnter OnUpdate — code that runs every frame while the state remains active OnUpdate OnExit — code that runs once when the state is about to change OnExit Let’s take the Chopping state as an example.When the unit enters this state, it might play a chopping animation and start a timer. On each update, it reduces the remaining time until the chop is complete. When done, it adds logs to the inventory and triggers a transition to Carrying. Chopping Carrying Similarly, the Depositing state might: Depositing OnEnter: approach the drop-off point and play an unload animation OnUpdate: wait until the unload completes OnExit: reset the inventory and check if more trees are available OnEnter: approach the drop-off point and play an unload animation OnUpdate: wait until the unload completes OnExit: reset the inventory and check if more trees are available This clear structure makes it easy to reason about what’s happening and why. Each state is a self-contained module with a single responsibility — making bugs easier to isolate and fixing them less likely to cause ripple effects. 📘 If you’re enjoying this so far, there’s a lot more in the book — same tone, just deeper. It’s right here if you want to peek. If you’re enjoying this so far, there’s a lot more in the book — same tone, just deeper. It’s right here here if you want to peek. Nested State Machines and Composition Nested State Machines and Composition At first, states like WalkingToTree may seem atomic — but they’re not. Walking is itself a nontrivial behavior. It may involve: WalkingToTree Pathfinding Obstacle avoidance Recalculating the route if blocked Handling interruptions (e.g., getting attacked mid-route) Pathfinding Obstacle avoidance Recalculating the route if blocked Handling interruptions (e.g., getting attacked mid-route) Instead of bloating the WalkingToTree state with all this logic, I encapsulate walking into its own FSM — effectively a nested state machine. So now WalkingToTree isn’t a primitive action, but a composite that delegates to a walking sub-automaton. WalkingToTree nested state machine WalkingToTree This idea generalizes well. For example, both WalkingToTree and CarryingToStorage involve walking — they can share the same FSM with different targets. Similarly, both Chopping and Building can reuse a timed-action FSM. WalkingToTree CarryingToStorage Chopping Building In other words, I started treating states themselves as strategies, composed from smaller reusable behaviors. FSMs became composable units — I could build higher-order behaviors from a library of well-tested primitives. states themselves as strategies This shift from linear scripting to layered composition drastically improved both code reuse and system robustness. Architectural Separation: Policy vs Behavior Architectural Separation: Policy vs Behavior To keep the system clean and scalable, I established a clear architectural separation between high-level policy and low-level behavior. high-level policy low-level behavior High-level policy decides what the unit wants to do next: gather wood, build, idle, patrol, etc. Low-level behaviors are FSMs that execute the how for each task. High-level policy decides what the unit wants to do next: gather wood, build, idle, patrol, etc. High-level policy what Low-level behaviors are FSMs that execute the how for each task. Low-level behaviors how For example, a high-level controller might say: “The nearest tree has 30 units of wood left. Go gather it.” “The nearest tree has 30 units of wood left. Go gather it.” The gather policy then activates a harvesting FSM composed of states like WalkToTree → Chop → Carry → Deposit. Each of those states may have their own sub-FSMs, like pathfinding or timed actions. WalkToTree → Chop → Carry → Deposit Here’s a simplified sketch of the architecture: High-Level Policy ↓ selects FSM: Harvesting Behavior ├── State: WalkingToTree │ └── Sub-FSM: Pathfinding ├── State: Chopping ├── State: Carrying └── State: Depositing High-Level Policy ↓ selects FSM: Harvesting Behavior ├── State: WalkingToTree │ └── Sub-FSM: Pathfinding ├── State: Chopping ├── State: Carrying └── State: Depositing This separation of concerns allows designers or AI layers to swap policies without rewriting behaviors — and vice versa. It also makes the system easier to test in isolation. By designing with this architectural boundary in mind, I ensured that the code remained modular, composable, and future-proof as game complexity increased. Applying the Same Pattern to Other Behaviors Applying the Same Pattern to Other Behaviors Once the FSM pattern proved effective for woodcutting, I extended it to other types of unit behavior — like building construction. A builder unit follows a similar logic: Walk to the construction site Start building (a timed process) Repeat if the building requires multiple stages or return to idle Walk to the construction site Start building (a timed process) Repeat if the building requires multiple stages or return to idle Again, the behavior breaks down into a composed FSM: → Walk to Site → Build → (Repeat or Idle) → Walk to Site → Build → (Repeat or Idle) Just like with chopping wood, Walk to Site uses the same pathfinding sub-FSM. Build is another timed action — nearly identical in structure to Chop. Walk to Site Build Chop The only real difference is the high-level policy: a harvester seeks out trees and deposits wood, while a builder seeks unfinished buildings and contributes labor. high-level policy Thanks to composable FSMs, I didn’t have to reinvent the architecture — I reused the same behavioral primitives and just changed the sequence and logic that ties them together. This made development faster and safer, with far fewer edge-case bugs and logic duplication. It also allowed me to experiment with new types of units quickly — scouts, soldiers, transporters — each defined by a different orchestration of familiar building blocks. Conclusion: Why This Architecture Matters Conclusion: Why This Architecture Matters Using composable finite state machines turned out to be more than just a neat trick — it became a foundational architecture for my game engine. Here’s what it enabled: Reusability — I could build a library of reliable, modular states (e.g., walking, waiting, acting) and reuse them across different unit types and behaviors. Scalability — Complex actions emerged by composing simple parts, without accumulating spaghetti logic. Testability — Each FSM and state could be tested in isolation, without running the whole game. Debuggability — Because state transitions were explicit, I could easily trace unexpected behaviors. Flexibility — Designers could invent new unit types or tasks just by reordering or combining existing states and policies. Reusability — I could build a library of reliable, modular states (e.g., walking, waiting, acting) and reuse them across different unit types and behaviors. Reusability Scalability — Complex actions emerged by composing simple parts, without accumulating spaghetti logic. Scalability Testability — Each FSM and state could be tested in isolation, without running the whole game. Testability Debuggability — Because state transitions were explicit, I could easily trace unexpected behaviors. Debuggability Flexibility — Designers could invent new unit types or tasks just by reordering or combining existing states and policies. Flexibility This architecture also follows sound engineering principles — single responsibility, composition over inheritance, separation of concerns — and aligns well with how real-world AI and robotics systems are built. While the examples here are focused on RTS units, the pattern applies anywhere behavior must be structured, reactive, and maintainable — from NPCs in RPGs to task execution in simulations or industrial automation. Ultimately, FSMs helped me bridge the gap between gameplay design and clean software architecture. And to me, that’s where real engineering lives. If you enjoyed this article, you might like my book Safe by Design: Explorations in Software Architecture and Expressiveness. It dives deeper into topics like this one — contracts, type safety, architectural clarity, and the philosophy behind better code. Safe by Design: Explorations in Software Architecture and Expressiveness Safe by Design: Explorations in Software Architecture and Expressiveness Safe by Design: Explorations in Software Architecture and Expressiveness 👉 Check it out on GitHub GitHub