How Finite State Machines Made My RTS Game Smarter — and My Code Cleaner

Written by sanqri | Published 2025/10/10
Tech Story Tags: software-development | software-engineering | software-architecture | solid | design | rts-game-design | game-engine | game-engine-programming

TLDRReal-time strategy (RTS) games often need predictable yet flexible patterns. Managing that behavior in a clean, scalable way is essential for a maintainable game engine. In this article, I describe how I used finite state machines to structure unit behavior.via the TL;DR App

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.

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

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

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

Visually, this can be represented as a loop:

→ 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.

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

Each transition is triggered by an event or a predicate evaluated continuously or upon state completion. For instance:

  • Transition from WalkingToTreeChopping occurs when the unit is within range of the target tree
  • Transition from ChoppingCarrying happens when the inventory is full
  • Transition from CarryingDepositing when the unit reaches the storage building
  • Transition from DepositingWalkingToTree when the deposit is complete and there are more trees available

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

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

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.

Similarly, the Depositing state might:

  • 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.

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:

  • 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.

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.

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.

This shift from linear scripting to layered composition drastically improved both code reuse and system robustness.

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 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.

For example, a high-level controller might say: “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.

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

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

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:

  1. Walk to the construction site
  2. Start building (a timed process)
  3. 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)

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.

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.

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

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.

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.

👉 Check it out on GitHub


Written by sanqri | I'm a Software Engineer from Odessa, Ukraine. Live in Santa Monica, CA. Like history and philosophy
Published by HackerNoon on 2025/10/10