前言
-
(题外话)nav2系列教材,yolov11部署,系统迁移教程我会放到年后一起更新,最近年末手头事情多,还请大家多多谅解。
-
回顾我们整个学习历程,我们已经学习了很多C++的代码特性,也学习了很多ROS2的跨进程通讯方式,也学会了很多路径规划的种种算法。那么如何将这些学习到的东西整合在一起,合并在工程中,使我们的机器人可以自主进行多任务执行和状态切换呢?本系列教程我们就来看看工程中最常用的几个AI控制结构:
- 行为树
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的主要组成部分包括:
- 状态(State):系统在某一时刻的具体情况。
- 输入(Input/Event):触发状态转换的外部事件或条件。
- 转换(Transition):系统从一个状态到另一个状态的变化,通常是由输入引起的。
- 初始状态(Initial State):FSM开始时的状态。
- 终止状态(Final State)(可选):某些FSM可能有一个或多个终止状态,表示系统的结束。
Initial State Input1 Input2 Final State State1 State2
1-3 工作原理
- FSM的工作可以简述为:
- 系统从初始状态开始。
- 根据输入事件,FSM根据当前状态和输入条件转换到下一个状态。
- 状态转换可能伴随有输出行为。
- FSM根据输入事件不断改变状态,直到满足某些终止条件(如果有)。
1-4 简易问题分析
- 我们通过有限状态机实现一个基础的开关灯的状态切换,具体状态转移图如下:
TURN_ON TURN_OFF OFF ON
OFF
和ON
分别是两个状态。TURN_ON
是从OFF
状态到ON
状态的事件。TURN_OFF
是从ON
状态到OFF
状态的事件。
1-5 简易代码实现
- 我们通过简单的代码实现上述功能
cpp
#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
)。灯光可以在ON
和OFF
两个状态之间切换。通过触发相应的事件(如TURN_ON
和TURN_OFF
),状态机会根据当前状态和事件来决定转换到哪个新状态。 -
其中
cpp
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
存储每个状态-事件对(State
和Event
的组合),并将其映射到对应的目标状态。 transitions
映射了两个状态转换:- 如果当前状态是
OFF
,且事件是TURN_ON
,则转换到ON
。 - 如果当前状态是
ON
,且事件是TURN_OFF
,则转换到OFF
。
- 如果当前状态是
- 通过
transitions.find({state, event})
查找是否有匹配的转换。如果找到匹配项,则返回对应的新状态;如果找不到匹配项,保持当前状态不变。 - 我们可以使用下属表格描述状态转换。
当前状态 (State ) |
事件 (Event ) |
下一个状态 (State ) |
---|---|---|
OFF | TURN_ON | ON |
ON | TURN_OFF | OFF |
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++智能指针,内存管理与设计模式
cpp
#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
) :OffState
和OnState
分别代表开关灯的"OFF"和"ON"状态。每个类都实现了handle_event()
方法,用于根据当前状态处理事件。get_state_name()
方法返回该状态的名称,方便调试。
- 状态机类 (
LightSwitchFSM
) :- 这个类作为上下文类,负责管理当前状态,并在不同状态之间切换。
- 初始时,
current_state
设置为OffState
。 - 每当
handle_event()
被调用时,当前状态会执行相应的行为,之后状态机会根据当前状态切换到另一个状态(从OFF
切换到ON
,或者从ON
切换到OFF
)。
3 FSM进阶前置(一)--实际工程文件架构
3-1 工程文件结构
- 上述代码和上一节的代码我们都写在一个cpp中,但是在实际的工程中,我们往往会把类的实现和类的定义分开。
txt
|- 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
cpp
// 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
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
cpp
// main.cpp
#include "ClassA.hpp"
int main() {
ClassA obj; // 创建对象
obj.display(); // 调用 display() 方法
return 0;
}
CMakeLists.txt
文件:
cmake
# 设置最低版本要求
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 编译执行
- 老规矩我们实现连招
bash
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 游戏中,有限状态机通常应用于以下几个方面:
- 玩家角色的状态管理: 玩家角色通常具有多个不同的行为状态,例如:空闲(Idle)、跑步(Running)、跳跃(Jumping)、攻击(Attacking)、死亡(Dead)等。FSM 可以帮助管理这些状态的转换,并确保每个状态的行为逻辑清晰可控。
- 敌人或 NPC 的行为控制: 敌人或 NPC 也常常有多个行为状态,例如:巡逻(Patrolling)、追击(Chasing)、攻击(Attacking)、逃跑(Fleeing)等。通过 FSM 可以清晰地管理这些行为状态,并定义它们之间的转换条件。
- 游戏场景的管理: 在一些复杂的 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 文件架构
- 考虑到上述类之间出现反复包含关系,我们需要把类的定义和实现分开,并借助前向声明完成代码
- 文件架构如下:
txt
├── 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
cpp
#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
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
类的具体状态(如PlayerIdleState
、PlayerMoveState
等)。
- 这是一个
Initialize()
:- 用于初始化状态机,接收一个
PlayerState
类型的共享指针_startState
,设置状态机的初始状态。初始化时,会调用该状态的Enter()
方法。
- 用于初始化状态机,接收一个
ChangeState()
:- 用于切换当前状态。接收一个新的状态对象
_newState
,先调用当前状态的Exit()
方法,然后将currentState
更新为新状态,并调用新状态的Enter()
方法。
- 用于切换当前状态。接收一个新的状态对象
- 上述代码可以保证:
Enter() 一个状态Update() Exit()
5-5 PlayerState
PlayerState.hpp
cpp
#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
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
)来管理状态的转换。每个具体的状态(如PlayerIdleState
、PlayerRunningState
等)都应该继承自PlayerState
类。 -
下述三个虚函数分别表示每个状态将会进行的
cpp
void PlayerState::Enter() {}
void PlayerState::Update() {}
void PlayerState::Exit() {}
Enter() Update() Exit()
- 且每个状态内都会调用基类的三个函数,这样可以进行更高层次的状态转换,例如
- 假如我们存在一个状态,在所有状态下都可以转移到这个状态
- 比如说当玩家的能量条满了,无论玩家现在处于何种状态,玩家都可以随时切换到魔法攻击,这时候我们就可以把状态切换的逻辑移动到基类(优先级最高)
- 这是因为每个子类都会调用基类的
PlayerState::Update();
(见后面的代码)
cpp
void PlayerState::Update() {
if(/* 角色的能量条满了 且 玩家按下对应按键 */)
this->stateMachine->ChangeState(this->player->MagicAttackState);
}
5-6 具体子类PlayerIdleState
- 这里我们实现几个例子
PlayerIdleState.hpp
cpp
#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
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
进行状态转换。
cpp
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
cpp
#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
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)管理玩家的状态。玩家状态机通过状态类(如PlayerIdleState
和PlayerMoveState
)来控制玩家的不同行为和状态切换。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()
中
- 在这个类中,会集成所有玩家的状态==,以便状态机在进行状态切换的时候进行==
cpp
this->stateMachine->ChangeState(this->player->moveState);
5-8 测试
main.py
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;
}
CMakeList.txt
cmake
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中,如果向参考的可以参考,注意着一个不能直接运行!!!你要拆开!!!!!
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()
方法。具体状态类(如PlayerIdleState
和PlayerMoveState
)继承此类并实现这三个方法。
- 这是所有具体状态的基类,定义了
- 具体状态类 (如
PlayerIdleState
和PlayerMoveState
):- 这些状态类实现了特定的行为。每个状态类会通过
Enter()
、Update()
和Exit()
方法定义该状态进入、更新和退出时的具体行为。 PlayerIdleState
是玩家处于空闲状态时的行为,而PlayerMoveState
处理玩家移动时的行为。
- 这些状态类实现了特定的行为。每个状态类会通过
6 总结
- 本节我们讲解了
FSM有限状态机
,并且合并状态模式
完成了一个2D游戏角色操控的底层逻辑代码。 FSM
通常通过状态和转换来表示系统的行为,适合简单的,线性的行为,但是当有大量状态和转换出现的时候,FSM
就不是很适合了。- 那也就是行为树也解决的辣,那也就是下一节的内容拉!!!!
- 如有错误,欢迎指出!!!
- 感谢大家的支持!!!