You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

分层状态机(HSM)原理、实现及与FSM的差异与C++游戏开发咨询

Hey there! Let's break this down nice and simple—no fancy jargon that makes your eyes glaze over. I’ve been building game state systems in C++ for years, so I’ll walk you through everything you need to know.

First: HSM vs. Regular FSM—What’s the Difference?

Let’s start with the basics you already know: a regular Finite State Machine (FSM) is flat. Every state is on the same level—think of your character’s Idle, Run, Jump, and Attack states as separate boxes. To switch between them, you have to write logic for every possible transition (e.g., "If in Jump and landed, go to Idle; if in Run and pressed attack, go to Attack"). When you have 10+ states, this turns into a messy web of if-else or switch statements that’s impossible to maintain.

A Hierarchical State Machine (HSM) fixes this by organizing states into a tree (hierarchy). States have parent states, and child states inherit behavior from their parents. For example:

  • Create a parent state Airborne that handles universal air logic (like checking if the character lands, or takes damage mid-air)
  • Then make Jump, Fall, and AirAttack child states of Airborne—they only need to handle their unique logic (like applying jump force, or playing the air attack animation)

Here’s a quick comparison to drive it home:

  • Structure: Regular FSM = flat list; HSM = nested tree
  • Code Reuse: HSM lets you write logic once in a parent state (e.g., landing checks) instead of copying it into every air-related child state
  • Transition Complexity: HSM reduces transition logic—instead of writing "land" checks for Jump, Fall, and AirAttack, you write it once in Airborne
Why HSM Is Perfect for Game Development

Games are full of state hierarchies that match HSMs perfectly. Think about a player character:

  • They can be in Ground or Airborne states (top-level parents)
  • Ground has children like Idle, Run, Crouch, Attack
  • Airborne has children like Jump, Fall, AirDash

Here’s why this matters:

  1. Less duplicated code: All ground states need to check for jump inputs or damage—put that in the Ground parent, not every child.
  2. Simpler transitions: If any air state detects a landing, the Airborne parent can switch directly to Ground → Idle—no need to code that transition for every air child.
  3. Easier to extend: Want to add a Slide state? Just make it a child of Ground—it automatically inherits all ground logic, and you only write the sliding-specific code.
  4. Matches game logic intuitively: Game states naturally have "contains" relationships (e.g., "attacking while on ground" is a subset of "being on ground")—HSMs mirror this instead of forcing flat states.
Implementing a Basic HSM in C++ Using the State Pattern

The state pattern is the perfect foundation for HSMs. We’ll create a base State class with parent support, a Player class (the "context" that holds the current state), and example parent/child states.

Step 1: Define the Base State Class

This class handles core state behavior, plus the parent state pointer for hierarchy:

#include <iostream>
#include <string>

// Forward declare our context (Player)
class Player;

class State {
protected:
    Player* m_player;
    State* m_parent; // Pointer to parent state for hierarchy

public:
    State(Player* player, State* parent = nullptr) 
        : m_player(player), m_parent(parent) {}
    virtual ~State() = default;

    // Lifecycle methods
    virtual void Enter() {}
    virtual void Update(float dt) {}
    virtual void Exit() {}

    // Event handling: return true if we handled the event, false to pass to parent
    virtual bool HandleEvent(const std::string& event) {
        if (m_parent) {
            return m_parent->HandleEvent(event);
        }
        return false; // No parent, event unhandled
    }

    // For debugging/logging
    virtual std::string GetName() const = 0;
};

Step 2: Create the Context (Player Class)

This class manages the current state, handles state transitions, and passes updates/events to the active state:

class Player {
private:
    State* m_currentState;

public:
    Player() : m_currentState(nullptr) {}
    ~Player() {
        // Clean up current state (use smart pointers in production to avoid leaks!)
        if (m_currentState) {
            m_currentState->Exit();
            delete m_currentState;
        }
    }

    // Switch to a new state
    void ChangeState(State* newState) {
        if (m_currentState) {
            m_currentState->Exit();
            delete m_currentState;
        }
        m_currentState = newState;
        if (m_currentState) {
            m_currentState->Enter();
        }
    }

    // Pass frame updates to the current state
    void Update(float dt) {
        if (m_currentState) {
            m_currentState->Update(dt);
        }
    }

    // Pass events to the current state
    void HandleEvent(const std::string& event) {
        if (m_currentState) {
            m_currentState->HandleEvent(event);
        }
    }

    // Helper for logging state behavior
    void LogStatus(const std::string& message) {
        std::cout << "[Player] " << message << "\n";
    }
};

Step 3: Build Parent and Child States

Let’s make a GroundState (parent) and two child states: IdleState and RunState.

Parent State: GroundState

Handles universal ground logic (like jump input):

class GroundState : public State {
public:
    GroundState(Player* player) : State(player) {}

    std::string GetName() const override { return "GroundState"; }

    void Update(float dt) override {
        // Universal ground logic: e.g., check for damage, stamina regen
        m_player->LogStatus("GroundState: Running universal ground checks");
    }

    bool HandleEvent(const std::string& event) override {
        if (event == "Jump") {
            m_player->LogStatus("GroundState: Handling jump → switching to Airborne");
            // In a real game, you'd create an AirborneState here
            // m_player->ChangeState(new AirborneState(m_player));
            return true;
        }
        // Pass unhandled events to parent (none here, since GroundState is top-level)
        return State::HandleEvent(event);
    }
};

Child State: IdleState

Inherits from GroundState—only handles idle-specific logic:

class IdleState : public GroundState {
public:
    IdleState(Player* player) : GroundState(player) {}

    std::string GetName() const override { return "IdleState"; }

    void Enter() override {
        m_player->LogStatus("Entering Idle state");
    }

    void Update(float dt) override {
        // First run parent's update to get universal ground logic
        GroundState::Update(dt);
        // Idle-specific logic: play idle animation, check for input
        m_player->LogStatus("IdleState: Playing idle animation");
    }

    void Exit() override {
        m_player->LogStatus("Exiting Idle state");
    }

    bool HandleEvent(const std::string& event) override {
        if (event == "Move") {
            m_player->LogStatus("IdleState: Handling move → switching to Run");
            m_player->ChangeState(new RunState(m_player));
            return true;
        }
        // Pass unhandled events to parent (e.g., Jump)
        return GroundState::HandleEvent(event);
    }
};

Child State: RunState

Another child of GroundState:

class RunState : public GroundState {
public:
    RunState(Player* player) : GroundState(player) {}

    std::string GetName() const override { return "RunState"; }

    void Enter() override {
        m_player->LogStatus("Entering Run state");
    }

    void Update(float dt) override {
        GroundState::Update(dt);
        // Run-specific logic: move player, play run animation
        m_player->LogStatus("RunState: Moving player forward");
    }

    void Exit() override {
        m_player->LogStatus("Exiting Run state");
    }

    bool HandleEvent(const std::string& event) override {
        if (event == "StopMove") {
            m_player->LogStatus("RunState: Handling stop → switching to Idle");
            m_player->ChangeState(new IdleState(m_player));
            return true;
        }
        // Pass unhandled events to parent (e.g., Jump)
        return GroundState::HandleEvent(event);
    }
};

Step 4: Test the HSM

Let’s see it in action:

int main() {
    Player player;
    // Start in Idle state
    player.ChangeState(new IdleState(&player));

    std::cout << "\n--- Frame 1 Update ---\n";
    player.Update(0.016f); // 60fps delta time

    std::cout << "\n--- Handling 'Move' Event ---\n";
    player.HandleEvent("Move");

    std::cout << "\n--- Frame 2 Update ---\n";
    player.Update(0.016f);

    std::cout << "\n--- Handling 'Jump' Event ---\n";
    player.HandleEvent("Jump");

    std::cout << "\n--- Handling 'StopMove' Event (Back to Ground) ---\n";
    player.ChangeState(new RunState(&player));
    player.HandleEvent("StopMove");

    return 0;
}

Key Takeaways from This Implementation

  • Hierarchy via Inheritance: Child states inherit parent state logic by calling ParentClass::Update() or ParentClass::HandleEvent().
  • Event Bubbling: If a child state doesn’t handle an event, it passes it up to the parent—just like DOM events in web dev, but for game states.
  • Clean Transitions: The Player class handles state switching, so states don’t need to know about each other directly (loose coupling).

内容的提问来源于stack exchange,提问作者Rivasa

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 11:14:38