Skip to content
This repository has been archived by the owner on Sep 26, 2018. It is now read-only.

Tutorial

Andrew Gresyk edited this page Jun 23, 2018 · 7 revisions

Tutorial

Implementation of a classic streetlight state machine using HFSM

Example code walkthrough

Source code: basic streetlight example

Includes

HFSM and STD's iostream.

#include <hfsm/machine_single.hpp>
#include <iostream>

Context

Context is a data container for storing internal state variables, as well as data to be shared with the external code to FSM. In this case, it holds a single unsigned cycle counter:

// data shared between FSM states and outside code
struct Context {
    unsigned cycleCount;
};

M:: namespace

hfsm::Machine<TContext> is a 'templatized namespace', where all the useful HFSM classes live:

  • Bare (state base)
  • Root (FSM root)
  • Orthogonal and Composite (state regions)
  • etc.

For convenience, typedef it to somehting short, e.g. M:

// convenience typedef
using M = hfsm::Machine<Context>;

State transitions

State transitions are template functions, accepting target state type as a template parameter, e.g.: FSM.changeTo<TState>() Because of this, target states need to be forward declared before the transition:

// forward declared for Red::transition()
struct Off;

Head states

On is the 'head' state of the composite region declared below:

// top-level region in the hierarchy
struct On
    : M::Base // necessary boilerplate!
{

State methods

All state methods (including enter() below) have at least one argument, the reference to Context:

    // called on state entry
    void enter(Context& context) {
        context.cycleCount = 0;
        std::cout << "On" << std::endl;
    }

Sub-states within a region

It is often convenient to define sub-states within their regions, so that the code organization reflects FSM structure:

    // forward declared for Red::transition()
    struct YellowDownwards;

    // sub-states
    struct Red
        : M::Base
    {
        // called on state entry
        void enter(Context& context) {
            ++context.cycleCount;
            std::cout << "  Red" << std::endl;
        }

transitions() methods

transition() method provides API for a state to request transitions:

  • state is free to not request any transitions
  • request one
  • or multiple transitions

Transitions aren't processed immediately, instead they are recorded, and processed at the end of the state machine's update() call. There are no limitations for target states, it is possible to initiate a transition to any state within the hierarchy.

Active state chain

If the target state is a region, then upon its activation, the initial sub-state will also be activated, recursively until the last leaf state in the chain. Additionally, activating a region will also cause all of its parent regions to be activated, also recursively.

It follows that, any point in time, an active FSM has:

  • (implicitly) active root
  • for any active region (including root, since it is treated as a 'special' region):
    • a single active sub-state (for 'composite' regions)
    • all active sub-states (for 'orthogonal' regions)
        // state can initiate transitions to _any_ other state
        void transition(Control& control, Context& context) {
            // multiple transitions can be initiated, can be useful in a hierarchy
            if (context.cycleCount > 3)
                control.changeTo<Off>();
            else
                control.changeTo<YellowDownwards>();
        }
    };

More sub-states

The following 3 sub-states follow similar pattern, they all form a 'cycle':

  • Red
    • Yellow
      • Green
    • Yellow
  • Red

etc.

    // forward declared for transition()
    struct Green;

    struct YellowDownwards
        : M::Base
    {
        void enter(Context&) {
            std::cout << "    Yellow v" << std::endl;
        }

        void transition(Control& control, Context&) {
            control.changeTo<Green>();
        }
    };

    struct YellowUpwards
        : M::Base
    {
        void enter(Context&) {
            std::cout << "    Yellow ^" << std::endl;
        }

        void transition(Control& control, Context&) {
            control.changeTo<Red>();
        }
    };

    struct Green
        : M::Base
    {
        void enter(Context&) {
            std::cout << "      Green" << std::endl;
        }

        void transition(Control& control, Context&) {
            control.changeTo<YellowUpwards>();
        }
    };
};

Arbitrary hierarchy

HFSM allows arbitrary shape of the state hierarchy graph. In this case, Off state is another top-level state without any sub-states of its own:

// another top-level state
struct Off
    : M::Base
{
    void enter(Context&) {
        std::cout << "Off" << std::endl;
    }
};

Driver code

int main() {
    // shared data storage instance
    Context context;

Roots

There are 4 root variants:

  • Generic Root, a special case of 'composite' region, takes 3+ parameters:

    • 'head' state, to be embedded in the root itself
    • initial sub-state
    • remaining sub-states
  • PeerRoot, a shortcut for a 'head-less' composite root, takes 2+ parameters:

    • initial sub-state
    • remaining sub-states
  • OrthogonalRoot, a special case of 'orgthogonal' region, parameters are the same as those of generic Root

  • OrthogonalPeerRoot, a 'head-less' orgthogonal root

    // state machine structure
    M::PeerRoot<
        // sub-machine ..

Regions

Similar to roots, regions also come in 4 variations:

  • 'Generic', taking 3+ parameters:

    • 'head' state, to be embedded in the root itself
    • initial sub-state
    • remaining sub-states
  • 'Head-less' (Peer*), taking 2+ parameters:

    • initial sub-state
    • remaining sub-states
  • 'Composite' regions can have only one active sub-state

  • 'Orthogonal' regions have all sub-states active if they are active themselves

Below is the generic composite region with its head state On, initial sub-state On::Red, and remaining sub-states On::YellowDownwards, On::YellowUpwards and On::Green:

        M::Composite<On,
            // .. with 4 sub-states
            On::Red,
            On::YellowDownwards,
            On::YellowUpwards,
            On::Green
        >,
        Off
    > machine(context);

Checking if state is active

Root::isActive<TState>() method allows to check the activation status of a state:

    while (machine.isActive<Off>() == false)

Core FSM update

All state machine internal logic executes within Root::update() method:

        machine.update();

Transitions from outside of an FSM

It is also possible to initiate a transition from the outside of an FSM using Root::changeTo<TState>() method:

    machine.changeTo<On>();

    return 0;
}

Documentation

Design

  • Core principles
  • Another FSM lib?
  • NoUML compliance
  • Proactive vs. reactive approach
  • Gamedev requirements
  • Alternatives

Basic features

  • Context and M:: 'namespace'
  • Basic state methods
  • Basic transitions
  • Roots and regions
  • Transitions within hierarchy
  • Active chain
  • Quering state activation status

Advanced features

  • Substitutions, aka State guards on steroids
  • State reuse with injections
  • Event handling

Debugging

  • Structure and activity report API
  • Assisted debugging with custom .natvis
  • Logger interface
Clone this wiki locally