2013年12月26日 星期四

編輯器的復原和重做功能

UEd裡的許多編輯器有提供常見的復原(Undo)和重做(Redo)功能,例如AnimSet Editor、Matinee、PhAT等,它們其實都是透過交易(Transaction)功能實作出來的。本篇就來介紹一下如何利用交易為自訂的編輯器寫出復原和重做功能。

首先要知道交易功能是利用UObject的序列化(Serialization)實作出來的功能,概念上就是把變更前後的物件狀態儲存起來,然後復原時恢復成變更前的狀態,重做時回到變更後的狀態。所以你想要復原的狀態必需是某個UnrealScript類別的欄位。

基本上復原和重做功能就是在變更物件時順便建立一份交易資料,然後利用它復原或重做。建立交易資料的步驟如下:

  • 如果要復原的物件沒有交易標記,呼叫UObject::SetFlags( RF_Transactional )標記在交易時應處理此物件。
  • 呼叫GEditor->BeginTransaction(session_name),宣告交易開始。
  • 呼叫UObject::Modify()儲存要復原的物件狀態。
  • 變更物件。
  • 呼叫GEditor->EndTransaction(),宣告交易結束。

必須按照上面的順序進行,如果變更完物件才呼叫Modify()就會變成儲存變更後的狀態。而且你必須對每個要復原的物件都呼叫Modify()才會記錄,它並不會自動去找參考到的其他物件。

範例程式碼如下:
MyObject->SetFlags( RF_Transactional );
GEditor->BeginTransaction(TEXT("MySession"));
MyObject->Modify();
// Some changes to MyObject
...
GEditor->EndTransaction();

對於在一個函式之內完成的變更,可以利用FScopedTransaction這個類別來自動宣告交易開始和結束。上面的程式碼可改為:
MyObject->SetFlags( RF_Transactional );
const FScopedTransaction Transaction( TEXT("MySession") );
MyObject->Modify();
// Some changes to MyObject
...

如果是在交易中建立物件的話,就要在建立時傳入RF_Transactional旗標。當然就不需要另外設定交易標記了。

要存取目前的交易資料可直接使用全域變數GUndo。實際上UObject::Modify()是間接呼叫GUndo->SaveObject()來記錄物件狀態的。

許多樹狀編輯器都會使用一個陣列去快取所有的節點,然而因為分支節點大都只記錄一層子節點,這個陣列通常是直接放在編輯器類別的某個欄位。但編輯器類別不繼承UObject,如果想在交易中記錄這個陣列的話,就只好宣告另一個UnrealScript類別來放它。另一種方式是利用TTransArray<T>模板類別,它會自動在增減元素時呼叫GUndo->SaveArray()記錄陣列狀態以供復原。然而可惜的是TTransArray使用時還是需要提供一個持有者(Owner UObject),所以還是免不了要另外多寫一個UnrealScript類別來放它。不過這點似乎並非必要,也許可以修改TTransArray的程式碼拿掉檢查Owner的部份就可以避免。
class MyEditor : public WxTrackableFrame, ...
{
...
    TArray<UMyObject*> MyObjects;
};
例如上面的MyEditor內有個陣列叫MyObjects,如果改用TTransArray的話,首先要定義一個繼承UObject的類別來放陣列:
class UTransArrayHelper : public UObject
{
 DECLARE_CLASS_INTRINSIC(UTransArrayHelper, UObject, CLASS_Transient, MyEditor);

public:

 UTransArrayHelper() : MyObjects(this)  {}

 virtual void Serialize( FArchive& Ar );

 TTransArray<UMyObject*> MyObjects;
};
然後在MyEditor內改用這個類別來存取陣列,當然一樣也要標記RF_Transactional:
class MyEditor
{
...
    UTransArrayHelper* Helper;
};

MyEditor::MyEditor() :
    ...
{
    ...
    Helper = new ( UObject::GetTransientPackage(), NAME_None, RF_Transactional ) UTransArrayHelper();
}

沒有留言:

張貼留言