편집기에서 명령들은 Command Pattern 사용한다. 편집기에서 Undo/Redo 기능을 구현하기 위해 사용한 자료구조는 Circular Buffer이다. 사실 Undo/Redo를 구현할 때 대표적으로 사용되는 자료구조는 두 개의 stack을 사용하는 것이다. 하지만 두 개의 stack을 사용하여 현재 이미 실행된 Command 스택과 실행 취소한 Command(또는 Redo 할 수 있는 Command) 스택을 분리하는 것보다 선형 자료구조를 사용하여 구현해보고자 하였다.
void Init(uint32 Capacity);
void Add(UCommand* NewElement);
void Undo();
void Redo();
UCommand* GetCurrentElement() const;
UCommand* operator[](uint32 Index);
const UCommand* operator[](uint32 Index) const;
uint32 Capacity() const;
uint32 GetNextIndex(uint32 CurrentIndex) const;
uint32 GetPreviousIndex(uint32 CurrentIndex) const;
uint32 IndexMask; // 주어진 인덱스가 유효한 인덱스로 변환되기 위한 마스크 값. IndexMask는 버퍼의 길이와 같다.
UPROPERTY(VisibleAnywhere)
int32 Current = -1; // 현재 Command를 가리키는 인덱스. 현재 Command는 실행된 상태이다. 초깃값은 -1이다.
UPROPERTY(VisibleAnywhere)
int32 Head = 0; // 처음 Command를 가리키는 인덱스. 초깃값은 0이고, 0보다 작을 수 없다.
UPROPERTY(VisibleAnywhere)
int32 Tail = 0; // 마지막 Command를 가리키는 인덱스. 초깃값은 0이고, 0보다 작을 수 없다.
/** Holds the buffer's Buffer. */
UPROPERTY(VisibleAnywhere)
TArray<UCommand*> Buffer; // Command를 저장할 배열
버퍼의 크기는 입력 파라미터 Capcity에 의해 결정된다. 함수 내부에서 RoundUpToPowerOfTwo 함수를 호출하는데 이 함수는 Capacity와 가장 가까우면서 큰 2의 승수를 반환한다.
IndexMask는 코드에서 보여지는 것과 같이 Buffer의 크기보다 1 작다. 그 이유는 IndexMask는 제수이고, 결과인 나머지는 [0, 제수-1]의 범위를 갖기 때문이다.
void ACommandManager::Init(uint32 Capacity)
{
checkSlow(Capacity <= 0xffffffffU);
Buffer.Init(nullptr, FMath::RoundUpToPowerOfTwo(Capacity));
IndexMask = Buffer.Num() - 1;
EditorPawn = (Cast<ATransformerPawn>(GetOwner()));
}
// MicrosoftPlatformMath.h
static FORCEINLINE uint32 RoundUpToPowerOfTwo(uint32 Arg)
{
return 1 << CeilLogTwo(Arg);
}
Add 연산은 총 6가지의 경우가 있다.
위 6가지의 경우는 아래 코드와 같이 2가지 경우로 줄일 수 있다.
// Add 함수는 임의의 Command가 입력 되었을 때, Circular Buffer에 해당 Command가 추가된다.
void ACommandManager::Add(UCommand* NewElement)
{
if (NewElement)
{
if (Current == Tail)
{
Current = (Current + 1) & IndexMask;
Tail = (Tail + 1) & IndexMask;
if (Tail == Head)
{
Head = (Head + 1) & IndexMask;
}
}
else // Current != Tail
{
if (Current == -1)
{
Current = Head;
}
else
{
Current = (Current + 1) & IndexMask;
}
Tail = Current;
}
Buffer[Current] = NewElement;
Buffer[Current]->Execute();
}
}