分层状态机(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.
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
Airbornethat handles universal air logic (like checking if the character lands, or takes damage mid-air) - Then make
Jump,Fall, andAirAttackchild states ofAirborne—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, andAirAttack, you write it once inAirborne
Games are full of state hierarchies that match HSMs perfectly. Think about a player character:
- They can be in
GroundorAirbornestates (top-level parents) Groundhas children likeIdle,Run,Crouch,AttackAirbornehas children likeJump,Fall,AirDash
Here’s why this matters:
- Less duplicated code: All ground states need to check for jump inputs or damage—put that in the
Groundparent, not every child. - Simpler transitions: If any air state detects a landing, the
Airborneparent can switch directly toGround→Idle—no need to code that transition for every air child. - Easier to extend: Want to add a
Slidestate? Just make it a child ofGround—it automatically inherits all ground logic, and you only write the sliding-specific code. - 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.
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()orParentClass::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
Playerclass handles state switching, so states don’t need to know about each other directly (loose coupling).
内容的提问来源于stack exchange,提问作者Rivasa

