Command解决方案

基本的命令框架,由 CommandCommandHistoryCommandManager 组成;

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 的类型,该类型与具体业务相关;CommandParamsCommandTargets 用于记录 Command 的参数,Params 记录基础类型、Targets 记录相关 UObject 指针;

FCommand:针对特殊的 CommandType : ECommandTypeCommand 特化;

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
// Command

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
// FCommand

UENUM()
enum class ECommandType
{
None = 0,

Test_1,
Test_2,
}

struct FCommand : TCommand<ECommandType>
{
using Super = TCommand<ECommandType>;
using Super::Super;
};

// FCommandWrapper

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 序列集合,ExecuteCommandRecordUndoRedo 等;

FCommandHistoryTCommandHistory 针对 TCommandTOnProcessCommandDelegate 的特化;

通过 OnProcessCommandDelegate<void(const Command&, OperationType)> 来通知与分发 Command 的执行,其中 OperationType 与业务无关,主要有:

  1. Record:记录新的 Command
  2. Undo:回退 Command
  3. Redo:重做 Command
  4. 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
// Operation

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
// TCommandHistory

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;
};

// -----

// TCommandHistory Implementation

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
// TestCommandHistory

DECLARE_DELEGATE_TwoParams(FOnProcessCommandInternal, const FCommand& /*Command*/, ECommandOperationType /*OperationType*/);

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 的注册与分发、提供 RecordUndo 等接口给 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
// CommandManager

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;
}