Command解决方案
基本的命令框架,由 Command
、CommandHistory
、CommandManager
组成;
Command
classDiagram
direction LR
class TCommand {
CommandType : TCommandType
CommandParams : FCommonVariantParams
CommandTargets : TArray~TStrongObjectPtr[UObject]~
}
TCommand<|--FCommand
class FCommand
FCommand*--ECommandType
class ECommandType
FCommandWrapper*--FCommand
class FCommandWrapper {
Command : const FCommand *
}
TCommand
:维护 Command
,其中 CommandType
表示该 Command
的类型,该类型与具体业务相关;CommandParams
、CommandTargets
用于记录 Command
的参数,Params
记录基础类型、Targets
记录相关 UObject
指针;
FCommand
:针对特殊的 CommandType : ECommandType
的 Command
特化;
FCommandWrapper
:对 FCommand
的一个封装,可用于打包数据到 lua
;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 template <typename TCommandType>struct TCommand { static_assert (TIsEnumClass<TCommandType>::Value, "TCommandType must be a enum type" ); TCommand () = default ; TCommand (TCommandType Type, const FCommonVariantParams& Params = {}) : CommandCategory (Category), CommandType (Type), CommandParams (Params) {} TCommand (TCommandType Type, TStrongObjectPtr<UObject> Target, const FCommonVariantParams& Params = {}) : CommandCategory (Category), CommandType (Type), CommandParams (Params), CommandTargets ({ Target }) {} TCommand (TCommandType Type, const TArray<TStrongObjectPtr<UObject>>& Targets, const FCommonVariantParams& Params = {}) : CommandCategory (Category), CommandType (Type), CommandParams (Params), CommandTargets (Targets) {} void Clear () ; TCommandType GetType () const { return CommandType; } const FCommonVariantParams& GetParams () const { return CommandParams; } const TArray<TStrongObjectPtr<UObject>>& GetTargets () const { return CommandTargets; } bool IsValid () const { return CommandType != TCommandType::None; } void MarkInvalid () const { CommandType = TCommandType::None; } FString ToString () const ; private : mutable TCommandType CommandType = TCommandType::None; FCommonVariantParams CommandParams {}; TArray<TStrongObjectPtr<UObject>> CommandTargets{}; }; template <typename TCommandType>void TCommand<TCommandType>::Clear (){ CommandType = TCommandType::None; CommandParams = {}; CommandTargets.Empty (); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 UENUM ()enum class ECommandType { None = 0 , Test_1, Test_2, } struct FCommand : TCommand<ECommandType> { using Super = TCommand<ECommandType>; using Super::Super; }; USTRUCT (BlueprintType)struct FCommandWrapper { GENERATED_BODY () void BindCommand (const FCommand* InCommand) { Command = InCommand; } uint32 GetType () { return (uint32)Command->GetType (); } const FCommonVariantParams& GetParams () { return Command->GetParams (); } TArray<UObject*> GetTargets () ; bool IsValid () { return Command->IsValid (); } void MarkInvalid () { Command->MarkInvalid (); } FString ToString () { return Command->ToString (); } private : const FCommand* Command = nullptr ; }; TArray<UObject*> FCommandWrapper::GetTargets () { TArray<UObject*> Res{}; Algo::Transform (Command->GetTargets (), Res, [](const auto & Value) { return Value.Get (); }); return Res; } BEGIN_EXPORT_REFLECTED_CLASS (FCommandWrapper) ADD_FUNCTION (GetType) ADD_FUNCTION (GetParams) ADD_FUNCTION (GetTargets) ADD_FUNCTION (IsValid) ADD_FUNCTION (MarkInvalid) ADD_FUNCTION (ToString) END_EXPORT_CLASS ()IMPLEMENT_EXPORTED_CLASS (FCommandWrapper)
CommandHistory
classDiagram
direction LR
class TCommandHistory {
MaxHistoryCount : int32
CurrIndex : int32
TailIndex : int32
CurrHistoryCount : int32
UndoCommandCount : int32
CommandHistory : TArray~TUniquePtr[TCommand]~
OnProcessCommand : TOnProcessCommandDelegate
ExecuteCommand(Command, Type)
Record(Command)
Undo()
Redo()
}
TCommandHistory<--ECommandOperationType
TCommandHistory<|--FCommandHistory
FCommandHistory*--FOnProcessCommandInternal
FCommandHistory*--FCommand
TCommandHistory
:用于记录 Command
序列集合,ExecuteCommand
的 Record
、Undo
、Redo
等;
FCommandHistory
:TCommandHistory
针对 TCommand
、TOnProcessCommandDelegate
的特化;
通过 OnProcessCommandDelegate<void(const Command&, OperationType)>
来通知与分发 Command
的执行,其中 OperationType
与业务无关,主要有:
Record
:记录新的 Command
;
Undo
:回退 Command
;
Redo
:重做 Command
;
Discard
:在 Truncate
截断 CommandList
(回退后有新 Command
)时,丢弃之前回退的 Command
;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 UENUM ()enum class ECommandOperationType : uint8{ BeforeRecord, Record, BeforeUndo, Undo, AfterUndo, BeforeRedo, Redo, AfterRedo, Discard, Overwrite, Clear, };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 template <typename TCommand, typename TOnProcessCommandDelegate>struct TCommandHistory { static_assert (TIsSame<TOnProcessCommandDelegate, TDelegate<void (const TCommand&, ECommandOperationType)>>::Value, "TOnProcessCommandDelegate must be a TDelegate<void(const TCommand&, ECommandOperationType)>" ); void Init (int32 InMaxHistoryCount) ; void Clear () ; void Destroy () ; void Record (TUniquePtr<TCommand> Command) ; void Undo (int32 StepCount) ; void Redo (int32 StepCount) ; void Truncate () ; TOnProcessCommandDelegate OnProcessCommand; int32 GetCurrHistoryCount () const { return CurrHistoryCount; } int32 GetTotalHistoryCount () const { return CurrHistoryCount + UndoCommandCount; } FString ToString () const ; private : int32 CalcIndex (int32 Val) const ; void ExecuteCommand (int32 Index, ECommandOperationType OperationType) ; void ExecuteCommand (const TCommand* Command, ECommandOperationType OperationType) ; private : int32 MaxHistoryCount = -1 ; TArray<TUniquePtr<TCommand>> CommandHistory{}; int32 CurrIndex = -1 ; int32 TailIndex = -1 ; int32 CurrHistoryCount = 0 ; int32 UndoCommandCount = 0 ; }; template <typename TCommand, typename TOnProcessCommandDelegate>void TCommandHistory<TCommand, TOnProcessCommandDelegate>::Init (int32 InMaxHistoryCount){ MaxHistoryCount = InMaxHistoryCount; } template <typename TCommand, typename TOnProcessCommandDelegate>void TCommandHistory<TCommand, TOnProcessCommandDelegate>::Clear (){ Truncate (); for (auto i = 0 ; i < CurrHistoryCount; i++) { auto Index = CalcIndex (i); if (CommandHistory.IsValidIndex (Index) && CommandHistory[Index]) { ExecuteCommand (Index, ECommandOperationType::Clear); CommandHistory[Index]->Clear (); } } CommandHistory.Empty (); CurrIndex = -1 ; TailIndex = -1 ; CurrHistoryCount = 0 ; UndoCommandCount = 0 ; } template <typename TCommand, typename TOnProcessCommandDelegate>void TCommandHistory<TCommand, TOnProcessCommandDelegate>::Destroy (){ Clear (); OnProcessCommand.Unbind (); } template <typename TCommand, typename TOnProcessCommandDelegate>void TCommandHistory<TCommand, TOnProcessCommandDelegate>::Record (TUniquePtr<TCommand> Command){ if (MaxHistoryCount == 0 ) return ; if (!Command) return ; ExecuteCommand (Command.Get (), ECommandOperationType::BeforeRecord); if (!Command->IsValid ()) { return ; } Truncate (); auto Index = CalcIndex (CurrIndex + 1 ); if (!CommandHistory.IsValidIndex (Index)) { CommandHistory.Add (MoveTemp (Command)); } else { if (CommandHistory[Index] && CommandHistory[Index]->IsValid ()) { ExecuteCommand (Index, ECommandOperationType::Overwrite); } CommandHistory[Index] = MoveTemp (Command); } CurrIndex = TailIndex = Index; if (MaxHistoryCount > 0 ) { CurrHistoryCount = FMath::Min (CurrHistoryCount + 1 , MaxHistoryCount); } else { CurrHistoryCount++; } ExecuteCommand (Index, ECommandOperationType::Record); } template <typename TCommand, typename TOnProcessCommandDelegate>void TCommandHistory<TCommand, TOnProcessCommandDelegate>::Undo (int32 StepCount){ StepCount = FMath::Clamp (StepCount, 0 , CurrHistoryCount); if (StepCount == 0 ) return ; auto LastCurrIndex = CurrIndex; CurrIndex = CalcIndex (LastCurrIndex - StepCount); UndoCommandCount += StepCount; CurrHistoryCount -= StepCount; for (auto i = 0 ; i < StepCount; i++) { auto Index = CalcIndex (LastCurrIndex - i); if (CommandHistory.IsValidIndex (Index) && CommandHistory[Index]) { ExecuteCommand (Index, ECommandOperationType::BeforeUndo); ExecuteCommand (Index, ECommandOperationType::Undo); ExecuteCommand (Index, ECommandOperationType::AfterUndo); } } } template <typename TCommand, typename TOnProcessCommandDelegate>void TCommandHistory<TCommand, TOnProcessCommandDelegate>::Redo (int32 StepCount){ StepCount = FMath::Clamp (StepCount, 0 , UndoCommandCount); if (StepCount == 0 ) return ; auto LastCurrIndex = CurrIndex; CurrIndex = CalcIndex (LastCurrIndex + StepCount); UndoCommandCount -= StepCount; CurrHistoryCount += StepCount; for (auto i = 1 ; i <= StepCount; i++) { auto Index = CalcIndex (LastCurrIndex + i); if (CommandHistory.IsValidIndex (Index) && CommandHistory[Index]) { ExecuteCommand (Index, ECommandOperationType::BeforeRedo); ExecuteCommand (Index, ECommandOperationType::Redo); ExecuteCommand (Index, ECommandOperationType::AfterRedo); } } } template <typename TCommand, typename TOnProcessCommandDelegate>void TCommandHistory<TCommand, TOnProcessCommandDelegate>::Truncate (){ auto Count = UndoCommandCount; if (Count == 0 ) return ; TailIndex = CurrIndex; UndoCommandCount = 0 ; for (auto i = 1 ; i <= Count; i++) { auto Index = CalcIndex (CurrIndex + i); if (CommandHistory.IsValidIndex (Index) && CommandHistory[Index]) { ExecuteCommand (Index, ECommandOperationType::Discard); CommandHistory[Index]->Clear (); } } } template <typename TCommand, typename TOnProcessCommandDelegate>int32 TCommandHistory<TCommand, TOnProcessCommandDelegate>::CalcIndex (int32 Val) const { if (MaxHistoryCount <= 0 ) return Val; auto Res = Val % MaxHistoryCount; if (Res < 0 ) { Res += MaxHistoryCount; } return Res; } template <typename TCommand, typename TOnProcessCommandDelegate>void TCommandHistory<TCommand, TOnProcessCommandDelegate>::ExecuteCommand (int32 Index, ECommandOperationType OperationType){ if (0 <= Index && Index < CommandHistory.Num ()) { OnProcessCommand.ExecuteIfBound (*CommandHistory[Index].Get (), OperationType); } } template <typename TCommand, typename TOnProcessCommandDelegate>void TCommandHistory<TCommand, TOnProcessCommandDelegate>::ExecuteCommand (const TCommand* Command, ECommandOperationType OperationType){ OnProcessCommand.ExecuteIfBound (*Command, OperationType); }
1 2 3 4 5 DECLARE_DELEGATE_TwoParams (FOnProcessCommandInternal, const FCommand& , ECommandOperationType );struct FCommandHistory : TCommandHistory<FCommand, FOnProcessCommandInternal> {};
CommandManager
classDiagram
direction LR
CommandUtils-->CommandManager
class CommandManager {
ProcessCommandEvents : TMap~ECommandType, FOnProcessCommand~
ChangeCurrCommandHistoryType(CommandHistoryType)
RegisterProcessCommandEventListener(CommandType, Object, Callback)
UnregisterProcessCommandEventListener(CommandType, Object)
DispatchProcessCommandEvent(Command, OperationType)
DispatchProcessCommandEventToLua(CommandWrapper, OperationType)
}
CommandManager*--ECommandHistoryType
CommandManager
:
主要是为了维护多个 CommandHistory
(可能有多个业务各自相关的 CommandHistory
),进行相关的初始化;
通过 ChangeCurrCommandHistoryType
来切换当前执行的 Command
所属的 CommandHistoryType
;有些 Command
在多个 CommandHistoryType
的情景下都会有(比如当前切到一个新的 Editor
,但是 PropertyCommand
通用模块在任意一个 Editor
都会使用到),所以不选择执行 Command
时指定 CommandHistoryType
的方案,选择主动切换;业务可以根据自己的情况选择维护数据结构来管理 CommandHistoryType
的切换;
提供对应 Event
的注册与分发、提供 Record
、Undo
等接口给 CommandUtils
调用(期望外部仅调用 CommandUtils
相关方法),同时进行一些 CommonCommandType
的注册以及与 lua
的通信;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 void UCommandManager::InitCommandHistoryMap () { for (auto Type : TEnumRange<ECommandHistoryType>()) { auto CommandHistory = MakeShared<FCommandHistory>(); CommandHistory->Init (MAX_HISTORY_COUNT); CommandHistory->OnProcessCommand.BindUObject (this , &UCommandManager::DispatchProcessCommandEvent); CommandHistoryMap.Add (Type, CommandHistory); } CurrCommandHistoryType = ECommandHistoryType::None; } void UCommandManager::ChangeCurrCommandHistoryType (ECommandHistoryType Type) { CurrCommandHistoryType = Type; } TSharedPtr<FCommandHistory> UCommandManager::GetCurrCommandHistory () const { if (auto CommandHistoryPtr = CommandHistoryMap.Find (CurrCommandHistoryType)) { return *CommandHistoryPtr; } return nullptr ; } void UCommandManager::DispatchProcessCommandEvent (const FCommand& Command, ECommandOperationType OperationType) { auto Type = Command.GetType (); if (auto EventPtr = ProcessCommandEvents.Find (Type)) { EventPtr->Broadcast (Command, OperationType); } FCommandWrapper CommandWrapper{}; CommandWrapper.BindCommand (&Command); DispatchProcessCommandEventToLua (CommandWrapper, OperationType); } template <typename UserClass>bool UCommandManager::RegisterProcessCommandEventListener (ECommandType Type, UserClass* InUserObject, typename TMemFunPtrType<TIsConst<UserClass>::Value, UserClass, FOnProcessCommandCallbackType>::Type InFunc) { if (!ProcessCommandEvents.Contains (Type)) { ProcessCommandEvents.Add (Type, {}); } auto & Event = ProcessCommandEvents[Type]; if (Event.IsBoundToObject (InUserObject)) { return false ; } Event.AddUObject (InUserObject, InFunc); return true ; } template <typename UserClass>bool UCommandManager::UnregisterProcessCommandEventListener (ECommandType Type, UserClass* InUserObject) { if (!ProcessCommandEvents.Contains (Type)) { return false ; } auto & Event = ProcessCommandEvents[Type]; Event.RemoveAll (InUserObject); return true ; }