FSM 設計模式學習
FSM
Struct FSM
定義了狀態機的三個階段:Enter
、Tick
、Exit
struct FSM {
public:
FSM() {
}
TUniqueFunction<void()> Enter;
TUniqueFunction<void(float)> Tick;
TUniqueFunction<void()> Exit;
};
enum EState
列舉了不同的狀態
UENUM(BlueprintType)
enum EState {
Idle UMETA(DisplayName = "Idle"),
Placement UMETA(DisplayName = "Placement"),
Press UMETA(DisplayName = "Press"),
Drag UMETA(DisplayName = "Drag"),
MoveAxis UMETA(DisplayName = "MoveAxis"),
};
FSMMap
TMap<EState, FSM*> FSMMap;
RegisterFSM
這個宏用於在 Unreal Engine 中定義和註冊有限狀態機的狀態。它自動建立一個狀態的結構體,設定進入、更新和退出狀態的處理函式,並提供狀態轉換的方法。使用這個宏可以減少重複程式碼,確保狀態定義和註冊的一致性。
1. 定義結構體
struct state ## Struct : FSM {
public:
state ## Struct() {
Enter = [=]() {
getController()->Enter ##state## State();
};
Tick = [=](float delta) {
getController()->Tick ##state## State(delta);
};
Exit = [=]() {
getController()->Exit ##state## State();
};
UResourceManager::Get()->FSMMap.Add(EState::state, this);
}
};
state ## Struct
: 定義一個結構體,名稱由state
和Struct
組合而成。例如,如果你傳入Idle
,結構體名為IdleStruct
。FSM
:state ## Struct
繼承自FSM
,表示這是一個有限狀態機的狀態。- 建構函式 (
state ## Struct()
): 設定Enter、Tick和Exit的 lambda 函式:Enter
:進入狀態時呼叫。Tick
:狀態啟用期間每幀呼叫一次,delta
表示時間差。Exit
:退出狀態時呼叫。- 將狀態註冊到
UResourceManager
的FSMMap
中,使用EState::state
作為鍵值(例如EState::Idle
)。
2. 建立例項
state ## Struct state ## Struct;
- 建立
state ## Struct
的例項,例項名與結構體名相同。如果state
是Idle
,則例項名為IdleStruct
。
3. 定義狀態轉換函式
void Enter ##state## State();
UFUNCTION()
void Tick ##state## State(float* delta) {
Tick ##state## State(*delta);
}
void Tick ##state## State(float delta);
void Exit ##state## State();
Enter ##state## State()
: 宣告一個方法,用於處理狀態進入的邏輯。Tick ##state## State(float\* delta)
: 宣告一個方法,用於處理狀態的每幀更新,引數是delta
的指標。UFUNCTION
宏使得這個方法可以透過 Unreal 的反射系統呼叫。Tick ##state## State(float delta)
: 宣告一個方法,用於處理狀態的每幀更新,引數是delta
的值。Exit ##state## State()
: 宣告一個方法,用於處理狀態退出的邏輯。
4. 定義狀態轉換方法
void ChangeStateTo ## state() {
ChangeStateTo(EState::state);
}
ChangeStateTo ## state()
: 定義一個方法,用於轉換到指定的狀態。如果state
是Idle
,方法名為ChangeStateToIdle()
,並呼叫ChangeStateTo(EState::Idle)
。
完整程式碼
#define RegisterFSM(state) \
struct state ## Struct : FSM{ \
public: \
state ## Struct(){ \
Enter = [=](){ \
getController()->Enter ##state## State(); \
}; \
Tick = [=](float delta){ \
getController()->Tick ##state## State(delta); \
}; \
Exit = [=](){ \
getController()->Exit ##state## State(); \
}; \
UResourceManager::Get()->FSMMap.Add(EState::state, this); \
} \
}; \
state ## Struct state ## Struct; \
void Enter ##state## State(); \
UFUNCTION() \
void Tick ##state## State(float* delta) { \
Tick ##state## State(*delta); \
} \
void Tick ##state## State(float delta); \
void Exit ##state## State();\
void ChangeStateTo ## state(){\
ChangeStateTo(EState::state); \
}
ChangeStateTo
EState State = EState::Idle;
void AMyPlayerController::ChangeStateTo(EState state) {
if (State == state)
return;
if (UResourceManager::Get()->FSMMap.Contains(State) && UResourceManager::Get()->FSMMap[State] != nullptr)
UResourceManager::Get()->FSMMap[State]->Exit();
State = state;
if (UResourceManager::Get()->FSMMap.Contains(State) && UResourceManager::Get()->FSMMap[State] != nullptr)
UResourceManager::Get()->FSMMap[State]->Enter();
}
Tick
控制器的 Tick
函式中,如果當前狀態存在,則狀態依照進入 Tick
函式
void AMyPlayerController::Tick(float delta) {
if (UResourceManager::Get()->FSMMap.Contains(State) && UResourceManager::Get()->FSMMap[State] != nullptr)
UResourceManager::Get()->FSMMap[State]->Tick(delta);
}
實際運用
註冊各個狀態下的狀態機
不同狀態機還附帶了需要傳遞的資訊
RegisterFSM(Idle);
RegisterFSM2(Placement, AActor*, HitActor, UStaticMeshComponent*, HitComponent);
RegisterFSM3(Drag, AActor*, HitActor, UStaticMeshComponent*, HitComponent, FTransform, StartTransform);
RegisterFSM2(MoveAxis, FTouchParam, TouchParam, FTransform, StartTransform);
RegisterFSM4(Press, AActor*, HitActor, UMeshComponent*, HitComponent, FTouchParam, TouchParam, FPressParam, PressParam);
以下是轉化不同狀態的實際運用場景,可以發現,除了一開始定義的 ChangeStateTo()
函式,透過 RegisterFSM
建立的狀態機中的Enter
、Tick
、Exit
都分別進行了初始化
void AMyPlayerController::OnLeftMousePressed() {
if (!IsPressActor()) {
ChangeStateToPress(nullptr, nullptr);
}
}
bool AMyPlayerController::IsPressActor() {
if (IsPressAxisActor()) {
ChangeStateTo(EState::MoveAxis);
return true;
}
}
if (hit.GetActor()->IsA<AMeshActor>()) {
UMeshComponent* meshComponent = Cast<AMeshActor>(hit.GetActor())->MeshComponent;
ChangeStateToPress(hit.GetActor(), meshComponent);
}
if (UResourceManager::Get()->GetTag(hit.GetActor(), "soft").Equals("true")) {
ChangeStateToPress(hit.GetActor(), Cast<UStaticMeshComponent>(hit.GetComponent()));
return true;
}
void AMyPlayerController::OnLeftMouseReleased() {
UWidgetManager::Get()->SetMouseOverUI(false);
ChangeStateToIdle();
}
AActor* AMyPlayerController::PlacementActor(const FString& id)
{
AActor* actor = UResourceManager::Get()->SpawnActor(id, FVector(0, 0, -5000), FRotator::ZeroRotator);
if (actor != nullptr) {
ChangeStateToPlacement(actor, nullptr);
}
return actor;
}
void AMyPlayerController::EnterIdleState() {
}
void AMyPlayerController::TickIdleState(float delta) {
}
void AMyPlayerController::ExitIdleState() {
}