FSM 設計模式學習

Dream_moon發表於2024-08-21

FSM 設計模式學習

FSM

Struct FSM

定義了狀態機的三個階段:EnterTickExit

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: 定義一個結構體,名稱由 stateStruct 組合而成。例如,如果你傳入 Idle,結構體名為 IdleStruct
  • FSM: state ## Struct 繼承自 FSM,表示這是一個有限狀態機的狀態。
  • 建構函式 (state ## Struct()): 設定Enter、Tick和Exit的 lambda 函式:
    • Enter:進入狀態時呼叫。
    • Tick:狀態啟用期間每幀呼叫一次,delta 表示時間差。
    • Exit:退出狀態時呼叫。
    • 將狀態註冊到 UResourceManagerFSMMap 中,使用 EState::state 作為鍵值(例如 EState::Idle)。

2. 建立例項

state ## Struct state ## Struct;
  • 建立 state ## Struct 的例項,例項名與結構體名相同。如果 stateIdle,則例項名為 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(): 定義一個方法,用於轉換到指定的狀態。如果 stateIdle,方法名為 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 建立的狀態機中的EnterTickExit都分別進行了初始化

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() {
}

相關文章