当前位置: 首页 > article >正文

【C++决策和状态管理】从状态模式,有限状态机,行为树到决策树(二):从FSM开始的2D游戏角色操控底层源码编写

前言

  • (题外话)nav2系列教材,yolov11部署,系统迁移教程我会放到年后一起更新,最近年末手头事情多,还请大家多多谅解。
  • 回顾我们整个学习历程,我们已经学习了很多C++的代码特性,也学习了很多ROS2的跨进程通讯方式,也学会了很多路径规划的种种算法。那么如何将这些学习到的东西整合在一起,合并在工程中,使我们的机器人可以自主进行多任务执行和状态切换呢?本系列教程我们就来看看工程中最常用的几个AI控制结构:
    • 第一期:# C++决策和状态管理】从状态模式,有限状态机,行为树到决策树(一):从电梯出发的状态模式State Pattern
    • 有限状态机FSM
    • 行为树BTtree
    • 决策树
  • 上一节我们讲述了状态模式,本节我们学习新的FSM有限状态机,并将二者结合实现一个2D游戏角色底层操控脚本
has
1
1
manages
1
1
inherits
abstract
inherits
abstract
inherits
abstract
inherits
abstract
inherits
abstract
inherits
abstract
has
1..*
Player
+std::shared_ptr stateMachine
+std::shared_ptr idleState
+std::shared_ptr moveState
+std::shared_ptr runningState
+std::shared_ptr jumpingState
+std::shared_ptr attackingState
+std::shared_ptr deadState
+void Initialize()
+void Update()
«abstract»
PlayerState
+std::shared_ptr stateMachine
+std::shared_ptr player
+void Enter()
+void Update()
+void Exit()
PlayerIdleState
+void Enter()
+void Update()
+void Exit()
PlayerMoveState
+void Enter()
+void Update()
+void Exit()
PlayerRunningState
+void Enter()
+void Update()
+void Exit()
PlayerJumpingState
+void Enter()
+void Update()
+void Exit()
PlayerAttackingState
+void Enter()
+void Update()
+void Exit()
PlayerDeadState
+void Enter()
+void Update()
+void Exit()
PlayerStateMachine
+std::shared_ptr currentState
+void Initialize(std::shared_ptr _startState)
+void ChangeState(std::shared_ptr _newState)

请添加图片描述


1 有限状态机FSM

1-1 概念
  • 有限状态机(Finite State Machine,FSM)是一种数学模型,用于描述一个系统在一组有限的状态集合中的状态变换。每个状态都代表了系统的一个特定情况,状态之间的转换由某些事件或条件触发。FSM广泛应用于计算机科学、电子工程和自动控制等领域,尤其是在设计具有有限状态、简单规则的系统时非常有效。
1-2 核心组成
  • FSM的主要组成部分包括:
    1. 状态(State):系统在某一时刻的具体情况。
    2. 输入(Input/Event):触发状态转换的外部事件或条件。
    3. 转换(Transition):系统从一个状态到另一个状态的变化,通常是由输入引起的。
    4. 初始状态(Initial State):FSM开始时的状态。
    5. 终止状态(Final State)(可选):某些FSM可能有一个或多个终止状态,表示系统的结束。
Initial State
Input1
Input2
Final State
State1
State2
1-3 工作原理
  • FSM的工作可以简述为:
  1. 系统从初始状态开始。
  2. 根据输入事件,FSM根据当前状态和输入条件转换到下一个状态。
  3. 状态转换可能伴随有输出行为。
  4. FSM根据输入事件不断改变状态,直到满足某些终止条件(如果有)。
1-4 简易问题分析
  • 我们通过有限状态机实现一个基础的开关灯的状态切换,具体状态转移图如下:
TURN_ON
TURN_OFF
OFF
ON
  • OFFON 分别是两个状态。
  • TURN_ON 是从 OFF 状态到 ON 状态的事件。
  • TURN_OFF 是从 ON 状态到 OFF 状态的事件。
1-5 简易代码实现
  • 我们通过简单的代码实现上述功能
#include <iostream>
#include <map>
#include <functional>

class LightSwitchFSM {
public:
    enum class State {
        OFF,
        ON
    };

    enum class Event {
        TURN_ON,
        TURN_OFF
    };

    LightSwitchFSM() : current_state(State::OFF) {}

    void handle_event(Event event) {
        current_state = transition(current_state, event);
        std::cout << "Current State: " << (current_state == State::ON ? "ON" : "OFF") << "\n";
    }

private:
    State current_state;

    State transition(State state, Event event) {
        static std::map<std::pair<State, Event>, State> transitions = {
            {{State::OFF, Event::TURN_ON}, State::ON},
            {{State::ON, Event::TURN_OFF}, State::OFF}
        };

        auto it = transitions.find({state, event});
        if (it != transitions.end()) {
            return it->second;
        }
        return state;  // 如果没有找到匹配的转换,保持当前状态
    }
};

int main() {
    LightSwitchFSM fsm;
    std::cout << "Initial State: OFF\n";
    
    fsm.handle_event(LightSwitchFSM::Event::TURN_ON);
    fsm.handle_event(LightSwitchFSM::Event::TURN_OFF);

    return 0;
}

  • 请添加图片描述

  • 上述代码实现了一个简单的有限状态机(FSM),用于模拟灯光开关(LightSwitchFSM)。灯光可以在 ONOFF 两个状态之间切换。通过触发相应的事件(如 TURN_ONTURN_OFF),状态机会根据当前状态和事件来决定转换到哪个新状态。

  • 其中

State transition(State state, Event event) {
    static std::map<std::pair<State, Event>, State> transitions = {
        {{State::OFF, Event::TURN_ON}, State::ON},
        {{State::ON, Event::TURN_OFF}, State::OFF}
    };

    auto it = transitions.find({state, event});
    if (it != transitions.end()) {
        return it->second; // 返回匹配的转换结果
    }
    return state;  // 如果没有找到匹配的转换,保持当前状态
}

  • 它使用 std::map 存储每个状态-事件对(StateEvent 的组合),并将其映射到对应的目标状态。
  • transitions 映射了两个状态转换:
    • 如果当前状态是 OFF,且事件是 TURN_ON,则转换到 ON
    • 如果当前状态是 ON,且事件是 TURN_OFF,则转换到 OFF
  • 通过 transitions.find({state, event}) 查找是否有匹配的转换。如果找到匹配项,则返回对应的新状态;如果找不到匹配项,保持当前状态不变。
  • 我们可以使用下属表格描述状态转换。
当前状态 (State)事件 (Event)下一个状态 (State)
OFFTURN_ONON
ONTURN_OFFOFF

2 状态模式+FSM的有机结合

  • 通过上述代码聪明的你应该发现了,上述状态的增减有悖于程序的开闭原则(对扩展开放,对修改封闭)。回顾我们上一节所学习的状态模式,我们可以将状态模式封装出去。
2-1 状态模式回顾
"has"
1
*
"implements"
"implements"
"initial state"
"can switch to"
Context
- State currentState
+setState(State state)
+request()
«interface»
State
+handle()
ConcreteStateA
+handle()
ConcreteStateB
+handle()
  • 状态模式将每个状态抽象为一个类,类内部封装了与该状态相关的行为。当系统的状态发生变化时,我们并不直接修改原来的代码,而是动态地切换状态对象。这样可以使得新的状态可以独立添加,而无需修改现有的代码,从而符合开闭原则
  • 状态模式通常会定义一个上下文类(Context),用于切换和管理当前的状态对象。
2-2 状态模式和FSM结合的实现
  • 首先,我们定义一个状态接口,该接口将描述每个状态需要实现的行为。然后,我们为每个具体状态(如“ON”和“OFF”)创建一个类,并在这些类中实现各自的行为。最后,FSM类通过上下文管理状态的切换,并让每个状态类来处理具体的状态转移。
  • 同时我们在上下文使用智能指针std::unique_ptr管理当前类
    • 智能指针的使用可以参考# 【RAII | 设计模式】C++智能指针,内存管理与设计模式
#include <iostream>
#include <memory>

// 状态接口类
class State {
public:
    virtual ~State() = default;
    virtual void handle_event() = 0; // 处理事件
    virtual const char* get_state_name() = 0; // 获取当前状态名
};

// "OFF" 状态类
class OffState : public State {
public:
    void handle_event() override {
        std::cout << "Light is OFF. Turning it ON...\n";
    }

    const char* get_state_name() override {
        return "OFF";
    }
};

// "ON" 状态类
class OnState : public State {
public:
    void handle_event() override {
        std::cout << "Light is ON. Turning it OFF...\n";
    }

    const char* get_state_name() override {
        return "ON";
    }
};

// 状态机类(上下文类)
class LightSwitchFSM {
public:
    LightSwitchFSM() : current_state(std::make_unique<OffState>()) {}

    void handle_event() {
        // 由当前状态处理事件
        current_state->handle_event();
        
        // 根据当前状态切换到下一个状态
        if (current_state->get_state_name() == "OFF") {
            current_state = std::make_unique<OnState>();
        } else {
            current_state = std::make_unique<OffState>();
        }
        std::cout << "Current State: " << current_state->get_state_name() << "\n";
    }

private:
    std::unique_ptr<State> current_state; // 当前状态
};

int main() {
    LightSwitchFSM fsm;
    std::cout << "Initial State: OFF\n";
    
    // 模拟事件处理
    fsm.handle_event();  // 切换到 ON
    fsm.handle_event();  // 切换到 OFF

    return 0;
}

  • 请添加图片描述

  • 状态接口 (State)

    • State 类定义了所有具体状态类必须实现的接口,其中包括:
      • handle_event():处理事件,执行状态的具体行为。
      • get_state_name():返回当前状态的名称,便于调试输出。
  • 具体状态类(OffState, OnState
    • OffStateOnState 分别代表开关灯的“OFF”和“ON”状态。每个类都实现了 handle_event() 方法,用于根据当前状态处理事件。
    • get_state_name() 方法返回该状态的名称,方便调试。
  • 状态机类 (LightSwitchFSM)
    • 这个类作为上下文类,负责管理当前状态,并在不同状态之间切换。
    • 初始时,current_state 设置为 OffState
    • 每当 handle_event() 被调用时,当前状态会执行相应的行为,之后状态机会根据当前状态切换到另一个状态(从 OFF 切换到 ON,或者从 ON 切换到 OFF)。

3 FSM进阶前置(一)–实际工程文件架构

3-1 工程文件结构
  • 上述代码和上一节的代码我们都写在一个cpp中,但是在实际的工程中,我们往往会把类的实现和类的定义分开。
|- build
|- include
	|- ClassA.hpp
|- src
	|- classA.cpp
	|- main.cpp
|- CMakeLists.txt	
  • ClassA.hpp,我们通常会在头文件实现类的定义。
  • classA.cpp,我们通常会在cpp文件实现类的实现。
  • main.cpp:主程序执行的地方
  • CMakeLists.txt:配置cmake
3-2 代码编写
  • ClassA.hpp
// ClassA.hpp
#ifndef CLASS_A_HPP
#define CLASS_A_HPP

class ClassA {
public:
    ClassA();                      // 构造函数
    void display() const;           // 成员函数

private:
    int value;                      // 成员变量
};

#endif // CLASS_A_HPP

  • classA.cpp
// classA.cpp
#include "../include/ClassA.hpp"
#include <iostream>

// 构造函数实现
ClassA::ClassA() : value(0) { 
    // 初始化成员变量
}

// display() 函数实现
void ClassA::display() const {
    std::cout << "Value: " << value << std::endl;
}

  • main.cpp
// main.cpp
#include "ClassA.hpp"

int main() {
    ClassA obj;  // 创建对象
    obj.display(); // 调用 display() 方法
    return 0;
}
  • CMakeLists.txt 文件:
# 设置最低版本要求
cmake_minimum_required(VERSION 3.10)

# 设置项目名
project(MyProject)

# 设置 C++ 标准为 C++17(可以根据需要修改)
set(CMAKE_CXX_STANDARD 17)
# 指定可执行文件的输出目录

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
# 设置包含头文件的目录
include_directories(include)

# 添加源文件
set(SOURCES
    src/classA.cpp
    src/main.cpp
)

# 生成可执行文件
add_executable(MyProject ${SOURCES})

3-3 编译执行
  • 老规矩我们实现连招
    • 下属代码的解释可以参考# 【动态库.so | 头文件.hpp】基于CMake与CMakeList编写C++自定义库
mkdir build && cd build
cmake .. && make -j4
./bin/MyProject 
  • 我们就可以执行编译好的程序:
    ![[Pasted image 20241225084033.png]]

5 FSM进阶–2D玩家角色的状态管理

  • 在2D游戏开发中,有限状态机 (Finite State Machine, FSM) 是一种广泛使用的设计模式,用于处理游戏中对象(如玩家、敌人、NPC等)的状态转换和行为控制。FSM 将对象的行为划分为不同的状态,每个状态有其特定的行为,并定义了状态之间的转换规则。在游戏开发中,FSM 主要用来处理角色行为、动画控制、AI 决策、场景管理等。
5-1 FSM 在 2D 游戏中的应用
  • 在 2D 游戏中,有限状态机通常应用于以下几个方面:
  1. 玩家角色的状态管理: 玩家角色通常具有多个不同的行为状态,例如:空闲(Idle)、跑步(Running)、跳跃(Jumping)、攻击(Attacking)、死亡(Dead)等。FSM 可以帮助管理这些状态的转换,并确保每个状态的行为逻辑清晰可控。
  2. 敌人或 NPC 的行为控制: 敌人或 NPC 也常常有多个行为状态,例如:巡逻(Patrolling)、追击(Chasing)、攻击(Attacking)、逃跑(Fleeing)等。通过 FSM 可以清晰地管理这些行为状态,并定义它们之间的转换条件。
  3. 游戏场景的管理: 在一些复杂的 2D 游戏中,游戏场景的状态(如游戏开始、游戏进行中、游戏暂停、游戏结束等)也可以通过 FSM 来管理。
5-2 玩家角色的状态管理类图
  • 例如下属代码,是本人在unity2D中编写的一个CS脚本,核心就是利用有限状态机判断玩家输入对玩家操控的角色进行状态切换,进行移动,攻击等操作。
has
1
1
manages
1
1
inherits
abstract
inherits
abstract
inherits
abstract
inherits
abstract
inherits
abstract
inherits
abstract
has
1..*
Player
+std::shared_ptr stateMachine
+std::shared_ptr idleState
+std::shared_ptr moveState
+std::shared_ptr runningState
+std::shared_ptr jumpingState
+std::shared_ptr attackingState
+std::shared_ptr deadState
+void Initialize()
+void Update()
«abstract»
PlayerState
+std::shared_ptr stateMachine
+std::shared_ptr player
+void Enter()
+void Update()
+void Exit()
PlayerIdleState
+void Enter()
+void Update()
+void Exit()
PlayerMoveState
+void Enter()
+void Update()
+void Exit()
PlayerRunningState
+void Enter()
+void Update()
+void Exit()
PlayerJumpingState
+void Enter()
+void Update()
+void Exit()
PlayerAttackingState
+void Enter()
+void Update()
+void Exit()
PlayerDeadState
+void Enter()
+void Update()
+void Exit()
PlayerStateMachine
+std::shared_ptr currentState
+void Initialize(std::shared_ptr _startState)
+void ChangeState(std::shared_ptr _newState)
  • Player 类:游戏中玩家对象的实现。它负责管理玩家的状态和行为
  • PlayerState 类是所有具体状态的基类。它为不同的状态提供了统一的接口。
    • PlayerIdleState
    • PlayerMoveState
    • 等等分别继承PlayerState类实现具体状态的功能
  • PlayerStateMachine 类管理玩家的状态,它负责在不同状态之间进行转换。

5-3 文件架构
  • 考虑到上述类之间出现反复包含关系,我们需要把类的定义和实现分开,并借助前向声明完成代码
    • # 【C++多重类循环依赖问题】基于class前向声明与类定义和实现分离的解决方案与分析
  • 文件架构如下:
├── CMakeLists.txt
├── include
│   ├── Player.hpp
│   ├── PlayerIdleState.hpp
│   ├── PlayerMoveState.hpp
│   ├── PlayerState.hpp
│   └── PlayerStateMachine.hpp
├── merge.sh
└── src
    ├── main.cpp
    ├── player.cpp
    ├── playerIdleState.cpp
    ├── playerMoveState.cpp
    ├── playerState.cpp
    └── playerStateMachine.cpp
  • 注意这里我只实现抽象层面的FSM的框架,并演示状态切换如何使用及其进行的。
  • 有了上述代码,后续只需要把这套代码思路移植到任意游戏引擎就可以直接开发游戏了!!!
  • (题外话)本人在unity测试上述框架没有问题,也许以后能看到本人的独立游戏demo发布哈哈哈

5-4 PlayerStateMachine
  • PlayerStateMachine.hpp
#ifndef __PLAYER_STATE_MACHINE_HPP__
#define __PLAYER_STATE_MACHINE_HPP__
#include <memory>
#include <iostream>

class PlayerState;


class PlayerStateMachine
{
public:
    std::shared_ptr<PlayerState> currentState;
    void Initialize(std::shared_ptr<PlayerState> _startState);
    void ChangeState(std::shared_ptr<PlayerState> _newState);
 
    
};

#endif

  • playerStateMachine.cpp
#include "../include/PlayerStateMachine.hpp"
#include "../include/PlayerState.hpp"


void PlayerStateMachine::Initialize(std::shared_ptr<PlayerState> _startState)
{
    currentState = std::move(_startState);
    currentState->Enter();
}

void PlayerStateMachine::ChangeState(std::shared_ptr<PlayerState> _newState)
{
    currentState->Exit();
    currentState = std::move(_newState);
    currentState->Enter();
}

  • PlayerStateMachine 是一个典型的状态机实现,用于管理和切换 Player 类的不同状态(例如:空闲、跑步、跳跃、攻击等)。这个类负责控制当前玩家的状态,并在不同状态之间进行转换。
  • *currentState**:
    • 这是一个 shared_ptr 类型的成员变量,用于存储当前的状态对象。状态机只有一个当前状态,currentState 指向一个继承自 PlayerState 类的具体状态(如 PlayerIdleStatePlayerMoveState 等)。
  • Initialize():
    • 用于初始化状态机,接收一个 PlayerState 类型的共享指针 _startState,设置状态机的初始状态。初始化时,会调用该状态的 Enter() 方法。
  • ChangeState():
    • 用于切换当前状态。接收一个新的状态对象 _newState,先调用当前状态的 Exit() 方法,然后将 currentState 更新为新状态,并调用新状态的 Enter() 方法。
  • 上述代码可以保证:
Enter()
一个状态Update()
Exit()

5-5 PlayerState
  • PlayerState.hpp
#ifndef __PLAYER_STATE_HPP__
#define __PLAYER_STATE_HPP__

#include <memory>  // for std::unique_ptr

class PlayerStateMachine;
class Player;

class PlayerState
{
protected:
    std::shared_ptr<PlayerStateMachine> stateMachine;
    std::shared_ptr<Player> player;  

public:
    PlayerState(std::shared_ptr<Player> _player, std::shared_ptr<PlayerStateMachine> _stateMachine);

    virtual void Enter();
    virtual void Update();
    virtual void Exit();
};

#endif

  • playerState.cpp


#include "../include/Player.hpp"


#include "../include/PlayerStateMachine.hpp"
#include "../include/PlayerStateMachine.hpp"
#include "../include/PlayerState.hpp"


PlayerState::PlayerState(std::shared_ptr<Player> _player, std::shared_ptr<PlayerStateMachine> _stateMachine)
        : player(std::move(_player)), stateMachine(std::move(_stateMachine)) {}


void PlayerState::Enter() {}
void PlayerState::Update() {}
void PlayerState::Exit() {}
  • 这是一个抽象基类,表示玩家的状态(例如空闲、奔跑、攻击等)。状态模式的目标是通过状态机(PlayerStateMachine)来管理状态的转换。每个具体的状态(如 PlayerIdleStatePlayerRunningState 等)都应该继承自 PlayerState 类。

  • 下述三个虚函数分别表示每个状态将会进行的

void PlayerState::Enter() {}
void PlayerState::Update() {} 
void PlayerState::Exit() {}
Enter()
Update()
Exit()
  • 且每个状态内都会调用基类的三个函数,这样可以进行更高层次的状态转换,例如
    • 假如我们存在一个状态,在所有状态下都可以转移到这个状态
    • 比如说当玩家的能量条满了,无论玩家现在处于何种状态,玩家都可以随时切换到魔法攻击,这时候我们就可以把状态切换的逻辑移动到基类(优先级最高)
    • 这是因为每个子类都会调用基类的PlayerState::Update();(见后面的代码)
void PlayerState::Update() {
	if(/* 角色的能量条满了 且 玩家按下对应按键 */)
		this->stateMachine->ChangeState(this->player->MagicAttackState);
} 

5-6 具体子类PlayerIdleState
  • 这里我们实现几个例子
  • PlayerIdleState.hpp
#ifndef __PLAYER_IDLE_STATE_HPP__
#define __PLAYER_IDLE_STATE_HPP__
#include<iostream>
#include<memory>

#include "./PlayerState.hpp"

class PlayerState;
class Player;
class PlayerStateMachine;


class PlayerIdleState : public PlayerState
{
public:
    PlayerIdleState(std::shared_ptr<Player> _player, std::shared_ptr<PlayerStateMachine> _stateMachine);

    void Enter() override;

    void Update() override;

    void Exit() override;
};


#endif

  • playerIdleState.cpp

#include "../include/Player.hpp"
#include "../include/PlayerIdleState.hpp"
#include "../include/PlayerMoveState.hpp"
#include "../include/PlayerStateMachine.hpp"




PlayerIdleState::PlayerIdleState(std::shared_ptr<Player> _player, std::shared_ptr<PlayerStateMachine> _stateMachine)
    : PlayerState(std::move(_player), std::move(_stateMachine)) {}

void PlayerIdleState::Enter() 
{
    PlayerState::Enter();
    std::cout << "Entering Idle State" << std::endl;
}

void PlayerIdleState::Update() 
{
    PlayerState::Update();
    std::cout << "Player remain Idle" << std::endl;
	if (/* 玩家波动遥感 */)
	    this->stateMachine->ChangeState(this->player->moveState);
}

void PlayerIdleState::Exit() 
{
    PlayerState::Exit();
    std::cout << "Exiting Idle State" << std::endl;

  
}


  • 正如上面所属,每个子状态都会继承基类,且分别实现各自子状态的函数,同时执行父类对应的函数(==这时候我们就可以把优先级高的状态切换的逻辑移动到基类())
父类Enter()
子类Enter()
父类Update()
子类Update()
父类Exit()
子类Exit()
  • 在每一个状态我们都可以进行条件判断,判断用户的输入和当前游戏环境的状态,如果满足某个条件,就可以调用状态机的ChangeState进行状态转换。
 this->stateMachine->ChangeState(this->player->moveState);
  • 如此把状态转移判断逻辑具体实现在每个独立的状态中,我们就可以限制状态的转移,相当于在每个具体状态内判断转移条件是否满足,这样就不会担心状态冲突。
5-6-1 无限跳跃bug
  • 比如说我们限制只有玩家在地面状态才能转换到跳跃状态,当玩家切换到跳跃状态以后,立即切换为滞空状态,滞空状态无法切换到跳跃状态,这样玩家就无法进行无限跳跃,除非等玩家回到地面,滞空状态重新切换为地面状态,这时候才能再次相应到跳跃状态。
Start
Jump
Jump -> InAir
Land
Stop
Can't Jump Again
Can't Jump Again
Ground
Jumping
InAir
  • 关于地面状态,我们可以让多个地面状态(如idle,move)继承GroundState,然后让GroundState继承PlayerState即可实现上述的内容
has
1
1
manages
1
1
inherits
abstract
inherits
abstract
inherits
abstract
inherits
abstract
inherits
abstract
inherits
abstract
inherits
abstract
has
1..*
Player
+std::shared_ptr stateMachine
+std::shared_ptr idleState
+std::shared_ptr moveState
+std::shared_ptr runningState
+std::shared_ptr jumpingState
+std::shared_ptr attackingState
+std::shared_ptr deadState
+void Initialize()
+void Update()
«abstract»
PlayerState
+std::shared_ptr stateMachine
+std::shared_ptr player
+void Enter()
+void Update()
+void Exit()
«abstract»
GroundState
+void Enter()
+void Update()
+void Exit()
PlayerIdleState
+void Enter()
+void Update()
+void Exit()
PlayerMoveState
+void Enter()
+void Update()
+void Exit()
PlayerJumpingState
+void Enter()
+void Update()
+void Exit()
PlayerInAirState
+void Enter()
+void Update()
+void Exit()
PlayerAttackingState
+void Enter()
+void Update()
+void Exit()
PlayerDeadState
+void Enter()
+void Update()
+void Exit()
PlayerStateMachine
+std::shared_ptr currentState
+void Initialize(std::shared_ptr _startState)
+void ChangeState(std::shared_ptr _newState)


5-7 Player
  • Player.hpp
#ifndef __PLAYER_HPP__
#define __PLAYER_HPP__
#include <memory>


class PlayerStateMachine;
class PlayerIdleState;
class PlayerStateMachine;
class PlayerMoveState;

class Player:public std::enable_shared_from_this<Player>
{
public:
    Player();
    void Initialize();

    std::shared_ptr<PlayerStateMachine> stateMachine;
    std::shared_ptr<PlayerIdleState> idleState;
    std::shared_ptr<PlayerMoveState> moveState;
   
    void Update();
};

#endif

  • player.cpp


#include "../include/Player.hpp"

#include "../include/PlayerIdleState.hpp"
#include "../include/PlayerMoveState.hpp"
#include "../include/PlayerStateMachine.hpp"


Player::Player()
{
    stateMachine = std::make_shared<PlayerStateMachine>();  
  
}
void Player::Initialize()
{
  
    idleState = std::make_shared<PlayerIdleState>(shared_from_this(), stateMachine);
 
    moveState = std::make_shared<PlayerMoveState>(shared_from_this(), stateMachine);  // 初始化 moveState

    
    stateMachine->Initialize(idleState);
}

void Player::Update()
{
    stateMachine->currentState->Update();
}




  • Player 类通过使用有限状态机(FSM)管理玩家的状态。玩家状态机通过状态类(如 PlayerIdleStatePlayerMoveState)来控制玩家的不同行为和状态切换。
  • Player 类继承了 std::enable_shared_from_this<Player>,这是一个 C++ 标准库模板类,用于允许 Player 类的实例通过 shared_ptr 来创建 shared_from_this() 函数。这使得 Player 对象在其他地方能够安全地获取指向自身的 shared_ptr,比如在状态切换时传递 shared_ptr<Player>
    • # 【RAII | 设计模式】C++智能指针,内存管理与设计模式
    • 这里需要注意shared_from_this()只有在这个类构造函数执行完的时候才能进行因此你不能在构造函数中调用shared_from_this(),会报错!!!!
    • 所以这里选择推迟执行,写在Initialize()
  • 在这个类中,会集成所有玩家的状态==,以便状态机在进行状态切换的时候进行==
this->stateMachine->ChangeState(this->player->moveState);

5-8 测试
  • main.py
#include <iostream>
#include "../include/Player.hpp"
#include <thread>
#include <chrono>  // 引入chrono库以便使用时间延迟功能


int main()
{
    std::shared_ptr<Player> player = std::make_shared<Player>(); // 创建 Player 对象
    player->Initialize();  // 完成初始化
    while(1)
    {
       
        player->Update();  // 执行更新     
        
        // 休眠 16 毫秒,即大约每秒 60 帧
        std::this_thread::sleep_for(std::chrono::milliseconds(16));
        
       
    }
   
    return 0;
}

  • CMakeList.txt
cmake_minimum_required(VERSION 3.10)

# 设置项目名称
project(MyProject)

# 设置 C++ 标准为 C++17
set(CMAKE_CXX_STANDARD 17)

# 查找 ncurses 库
find_package(Curses REQUIRED)

# 设置可执行文件输出目录
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)

# 包含头文件目录
include_directories(${CURSES_INCLUDE_DIR})

# 添加源文件
set(SOURCES
    src/main.cpp
    src/player.cpp
    src/playerState.cpp
    src/playerStateMachine.cpp
    src/playerIdleState.cpp
    src/playerMoveState.cpp
)

# 生成可执行文件
add_executable(MyProject ${SOURCES})

# 链接 ncurses 库
target_link_libraries(MyProject ${CURSES_LIBRARIES})

  • 测试运行,这里我们完成了基础框架,我设置初始状态为idle,进入idle切换为move
    请添加图片描述

5-9 合并代码
  • 我把全部测试代码压缩到一个cpp中,如果向参考的可以参考,注意着一个不能直接运行!!!你要拆开!!!!!
#include <iostream>
#include "../include/Player.hpp"
#include <thread>
#include <chrono>  // 引入chrono库以便使用时间延迟功能


int main()
{
    std::shared_ptr<Player> player = std::make_shared<Player>(); // 创建 Player 对象
    player->Initialize();  // 完成初始化
    while(1)
    {
       
        player->Update();  // 执行更新     
        
        // 休眠 16 毫秒,即大约每秒 60 帧
        std::this_thread::sleep_for(std::chrono::milliseconds(16));
        
       
    }
   
    return 0;
}


#include "../include/Player.hpp"

#include "../include/PlayerIdleState.hpp"
#include "../include/PlayerMoveState.hpp"
#include "../include/PlayerStateMachine.hpp"


Player::Player()
{
    stateMachine = std::make_shared<PlayerStateMachine>();  
  
}
void Player::Initialize()
{
  
    idleState = std::make_shared<PlayerIdleState>(shared_from_this(), stateMachine);
 
    moveState = std::make_shared<PlayerMoveState>(shared_from_this(), stateMachine);  // 初始化 moveState

    
    stateMachine->Initialize(idleState);
}

void Player::Update()
{
    stateMachine->currentState->Update();
}




#include "../include/Player.hpp"
#include "../include/PlayerIdleState.hpp"
#include "../include/PlayerMoveState.hpp"
#include "../include/PlayerStateMachine.hpp"




PlayerIdleState::PlayerIdleState(std::shared_ptr<Player> _player, std::shared_ptr<PlayerStateMachine> _stateMachine)
    : PlayerState(std::move(_player), std::move(_stateMachine)) {}

void PlayerIdleState::Enter() 
{
    PlayerState::Enter();
    std::cout << "Entering Idle State" << std::endl;
}

void PlayerIdleState::Update() 
{
    std::cout << "Player remain Idle" << std::endl;
  
    this->stateMachine->ChangeState(this->player->moveState);
}

void PlayerIdleState::Exit() 
{
    std::cout << "Exiting Idle State" << std::endl;

    PlayerState::Exit();
}






#include "../include/Player.hpp"
#include "../include/PlayerMoveState.hpp"
#include "../include/PlayerIdleState.hpp"
#include "../include/PlayerStateMachine.hpp"


PlayerMoveState::PlayerMoveState(std::shared_ptr<Player> _player, std::shared_ptr<PlayerStateMachine> _stateMachine)
    : PlayerState(std::move(_player), std::move(_stateMachine)) {}

void PlayerMoveState::Enter() 
{
    PlayerState::Enter();
    std::cout << "Entering Move State" << std::endl;
}

void PlayerMoveState::Update() 
{
     
    PlayerState::Update();
    std::cout << "Player is Moving." << std::endl; 


}

void PlayerMoveState::Exit() 
{
    PlayerState::Exit();
    std::cout << "Exiting Move State" << std::endl;
}




#include "../include/Player.hpp"


#include "../include/PlayerStateMachine.hpp"
#include "../include/PlayerStateMachine.hpp"
#include "../include/PlayerState.hpp"


PlayerState::PlayerState(std::shared_ptr<Player> _player, std::shared_ptr<PlayerStateMachine> _stateMachine)
        : player(std::move(_player)), stateMachine(std::move(_stateMachine)) {}


void PlayerState::Enter() {}
void PlayerState::Update() {}
void PlayerState::Exit() {}#include "../include/PlayerStateMachine.hpp"
#include "../include/PlayerState.hpp"


void PlayerStateMachine::Initialize(std::shared_ptr<PlayerState> _startState)
{
    currentState = std::move(_startState);
    currentState->Enter();
}

void PlayerStateMachine::ChangeState(std::shared_ptr<PlayerState> _newState)
{
    currentState->Exit();
    currentState = std::move(_newState);
    currentState->Enter();
}
#ifndef __PLAYER_HPP__
#define __PLAYER_HPP__
#include <memory>


class PlayerStateMachine;
class PlayerIdleState;
class PlayerStateMachine;
class PlayerMoveState;

class Player:public std::enable_shared_from_this<Player>
{
public:
    Player();
    void Initialize();

    std::shared_ptr<PlayerStateMachine> stateMachine;
    std::shared_ptr<PlayerIdleState> idleState;
    std::shared_ptr<PlayerMoveState> moveState;
   
    void Update();
};

#endif
#ifndef __PLAYER_IDLE_STATE_HPP__
#define __PLAYER_IDLE_STATE_HPP__
#include<iostream>
#include<memory>

#include "./PlayerState.hpp"

class PlayerState;
class Player;
class PlayerStateMachine;


class PlayerIdleState : public PlayerState
{
public:
    PlayerIdleState(std::shared_ptr<Player> _player, std::shared_ptr<PlayerStateMachine> _stateMachine);

    void Enter() override;

    void Update() override;

    void Exit() override;
};


#endif
#ifndef __PLAYER_MOVE_STATE_HPP__
#define __PLAYER_MOVE_STATE_HPP__
#include <memory>

#include "./PlayerState.hpp"
class Player;
class PlayerState;
class PlayerStateMachine;
class PlayerMoveState : public PlayerState
{
public:
    PlayerMoveState(std::shared_ptr<Player> _player, std::shared_ptr<PlayerStateMachine> _stateMachine);

    void Enter() override;
    void Update() override;

    void Exit() override;
};


#endif
#ifndef __PLAYER_STATE_HPP__
#define __PLAYER_STATE_HPP__

#include <memory>  // for std::unique_ptr

class PlayerStateMachine;
class Player;

class PlayerState
{
protected:
    std::shared_ptr<PlayerStateMachine> stateMachine;
    std::shared_ptr<Player> player;  

public:
    PlayerState(std::shared_ptr<Player> _player, std::shared_ptr<PlayerStateMachine> _stateMachine);

    virtual void Enter();
    virtual void Update();
    virtual void Exit();
};

#endif
#ifndef __PLAYER_STATE_MACHINE_HPP__
#define __PLAYER_STATE_MACHINE_HPP__
#include <memory>
#include <iostream>

class PlayerState;


class PlayerStateMachine
{
public:
    std::shared_ptr<PlayerState> currentState;

    void Initialize(std::shared_ptr<PlayerState> _startState);

    void ChangeState(std::shared_ptr<PlayerState> _newState);
 
    
};

#endif


5-10 小节
  • 上述框架实现了一个基于 有限状态机(FSM) 的玩家控制系统。
    • 在该系统中,Player 类通过 PlayerStateMachine 管理不同的玩家状态(如 Idle 状态和 Move 状态)。
    • 每个状态通过继承 PlayerState 类来实现,并通过状态机在不同状态之间进行切换。
    • 每个状态类都实现了三个核心函数:Enter()Update()Exit(),用于处理状态的进入、更新和退出行为。
  • 关键点:
    • Player
      • 负责管理玩家的状态机以及具体状态。初始化时,玩家的状态机设为 idleState
      • Update() 中调用当前状态的 Update() 方法,通过状态机控制玩家的行为。
    • PlayerStateMachine
      • 管理当前状态,负责在不同状态之间进行切换。
      • Initialize() 初始化状态机并设置初始状态,ChangeState() 用于状态转换。
    • PlayerState
      • 这是所有具体状态的基类,定义了 Enter()Update()Exit() 方法。具体状态类(如 PlayerIdleStatePlayerMoveState)继承此类并实现这三个方法。
    • 具体状态类(如 PlayerIdleStatePlayerMoveState):
      • 这些状态类实现了特定的行为。每个状态类会通过 Enter()Update()Exit() 方法定义该状态进入、更新和退出时的具体行为。
      • PlayerIdleState 是玩家处于空闲状态时的行为,而 PlayerMoveState 处理玩家移动时的行为。

6 总结

  • 本节我们讲解了FSM有限状态机,并且合并状态模式完成了一个2D游戏角色操控的底层逻辑代码。
  • FSM通常通过状态和转换来表示系统的行为,适合简单的,线性的行为,但是当有大量状态和转换出现的时候,FSM就不是很适合了。
  • 那也就是行为树也解决的辣,那也就是下一节的内容拉!!!!
  • 如有错误,欢迎指出!!!
  • 感谢大家的支持!!!
    请添加图片描述

http://www.kler.cn/a/452477.html

相关文章:

  • 支持向量机入门指南:从原理到实践
  • dockerfile文档编写(1):基础命令
  • 理解并使用 Linux 内核的字符设备
  • YOLOv10目标检测-训练自己的数据
  • 最新的强大的文生视频模型Pyramid Flow 论文阅读及复现
  • 傅里叶变换原理
  • 什么是液体神经网络(LNN)
  • Excel批量设置行高,Excel表格设置自动换行后打印显示不全,Excel表格设置最合适的行高后打印显示不全,完美解决方案!!!
  • 攻防世界 unserialize3
  • 单片机长耗时前后台任务优化
  • 机器学习连载
  • SpringAI人工智能开发框架006---SpringAI多模态接口_编程测试springai多模态接口支持
  • workman服务端开发模式-应用开发-后端api推送工具开发
  • C# OpenCV机器视觉:模板匹配
  • ChatGPT详解
  • 面向微服务的Spring Cloud Gateway的集成解决方案:用户登录认证与访问控制
  • 【UE5 C++课程系列笔记】13——GameInstanceSubsystem的简单使用
  • HTML 画布:创意与技术的融合
  • 【Java】Jackson序列化案例分析
  • Redis生产实践中相关疑问记录
  • 【ollama安装】国内 ubuntu22.04 linux 环境安装ollama教程
  • 学习数量关系
  • 深度学习中的并行策略概述:1 单GPU优化
  • Python调用R语言中的程序包来执行回归树、随机森林、条件推断树和条件推断森林算法
  • NPM老是无法install,timeout?npm install失败
  • Mysql-索引的数据结构