2012年12月22日 星期六

自訂樹狀結構編輯器

許多Unreal資源都以樹狀或圖(一種以相連的結點構成的數學模型)表示,例如AnimTree和Kismet。為了方便撰寫這類編輯器,UEd提供一組以LinkedObj為名的類別,在此翻譯為鏈結物件。

鏈結物件以方形繪出,左右下三邊可能會有一個方塊或三角形代表接點。鏈結物件有四種接點:輸入、輸出、變數和事件。輸入在左,輸出在右,變數在左下,事件在右下。請參考下圖:
以下列出鏈結物件相關的類別:
  • FLinkedObjViewportClient:鏈結物件視埠。要使用鏈結物件功能的編輯器必須使用此類別作為視埠。此類別繼承自編輯器視埠類別FEditorLevelViewportClient。
  • FLinkedObjEdNotifyInterface:鏈結物件視埠事件界面。鏈結物件視埠會把原本編輯器視埠的輸入和繪圖事件處理後傳遞給界面物件,因此自訂編輯器可繼承此界面實作視埠的輸入和繪圖功能。
  • FLinkedObjVCHolder:用來放鏈結物件視埠的視窗類別。建立時會自動產生鏈結物件視埠,因此編輯器不需自行建立視埠。
  • FLinkedObjDrawUtils:包含許多用來繪製鏈結物件的成員函式。由於這些函式都是靜態函式,此類別比較像是一個命名空間。視埠通常會使用這些函式來繪製節點和連線。
  • FLinkedObjDrawInfo:鏈結物件的繪圖資訊。作為FLinkedObjDrawUtils的繪圖函式參數。
  • FLinkedObjConnInfo:鏈結物件的接點資訊。在FLinkedObjDrawInfo類別裡有四個FLinkedObjConnInfo陣列分別代表四種接點。

FLinkedObjDrawUtils


以下列出常用的繪圖函式:
  • DrawLinkedObj(Canvas, LinkedObjDrawInfo, Name, ...):繪製鏈結物件和它的接點。
  • DrawSpline(Canvas, StartPoint, StartDir, EndPoint, EndDir, Color, bArrowhead, ...):繪製脊線。可用來繪製兩個接點間的連線。

FLinkedObjDrawInfo


在呼叫FLinkedObjDrawUtils的繪圖函式前,需要先設定好FLinkedObjDrawInfo裡一些輸入用欄位:
  • ObjObject [UObject*]:開發者可以指定一個關聯物件,選取接點時會傳回這個物件。
  • Inputs [TArray<FLinkedObjConnInfo>]:輸入接點資訊。
  • Outputs [TArray<FLinkedObjConnInfo>]:輸出接點資訊。
  • Variables [TArray<FLinkedObjConnInfo>]:變數接點資訊。
  • Events [TArray<FLinkedObjConnInfo>]:事件接點資訊。

在呼叫完FLinkedObjDrawUtils的繪圖函式後,FLinkedObjDrawInfo裡一些繪圖位置欄位會被更新,編輯器需要記錄這些欄位來計算接點位置:
  • DrawWidth [INT]:方形寬度。
  • DrawHeight [INT]:方形高度。
  • InputY [TArray<INT>]:輸入接點的垂直座標。
  • OutputY [TArray<INT>]:輸出接點的垂直座標。
  • VariableX [TArray<INT>]:變數接點的水平座標。
  • EventX [TArray<INT>]:事件接點的水平座標。

FLinkedObjEdNotifyInterface


輸入


以下列出常用的輸入函式:
  • EdHandleKeyInput(Viewport, Key, Event):覆載此函式可取得按鍵事件。
  • DoubleClickedObject(Object):覆載此函式可取得雙擊物件事件。 
  • OpenNewObjectMenu():覆載此函式可取得右擊空地事件。
  • OpenObjectOptionsMenu():覆載此函式可取得右擊物件事件。
  • OpenConnectorOptionsMenu():覆載此函式可取得右擊接點事件。

繪圖


鏈結物件視埠只會清除背景,因此需要自行繪製物件和連線。以下列出常用的繪圖函式:
  • DrawObjects(Viewport, Canvas):覆載此函式可自行繪製視埠。需要使用FLinkedObjDrawUtils提供的繪圖函式才能正常支援選取和連結功能。

選取


鏈結物件視埠提供點選和框選的功能,但不會記錄選取的物件,編輯器需要自行記錄。鏈結物件視埠會繪製選取方框。以下列出常用的選取函式:
  • AddToSelection(Object):覆載此函式可取得選取物件事件。
  • RemvoeFromSelection(Object):覆載此函式可取得取消選取事件。
  • EmptySelection():覆載此函式可取得清除選取事件。
  • IsInSelection(Object):傳回指定物件是否選取。使用選取功能時必須實作此函式。
  • GetNumSelected():傳回選取物件的數量。使用選取功能時必須實作此函式。
  • MoveSelectedObjected( DeltaX, DeltaY ):移動選取的物件。使用選取功能時必須實作此函式。

連結


當點選一個接點然後拖曳,鏈結物件視埠會從接點到游標之間繪製一條曲線。一旦取消拖曳,曲線也會隨之消失。鏈結物件視埠並不會記錄選取的接點,編輯器需要自行記錄。以下列出常用的連結函式:
  • SetSelectedConnector( LinkedObjectConnector ):覆載此函式可取得選取接點事件。編輯器需要記錄選取的接點以得知正在嘗試連結的起始接點。
  • GetSelectedConnLocation( Canvas ):傳回連結位置。編輯器需要記錄FLinkedObjDrawInfo傳回的位置以計算連結位置。使用連結功能時必須實作此函式。
  • GetSelectedConnectorType():傳回連結類型為輸入、輸出、變數還是事件。編輯器需要記錄從SetSelectedConnector()函式取得的選取接點類型,然後在這個函式傳回。使用連結功能時必須實作此函式。
  • GetMarkingLinkColor():傳回連線顏色。在接點上拖曳出連線時會是這個顏色。
  • MakeConnectionToConnector( LinkedObjectConnector ):覆載此函式可取得連結事件。從一個接點拉線到另一個時會發送此事件。可覆載此函式設定連結相關屬性。

範例


以下程式碼展示一個典型的鏈結物件編輯器。為了方便解說,先定義資源類別:
class MyGraph extends Object
    native;

var array<MyGraphNode> Nodes;
展示用編輯器類別定義:
class WxMyGraphEditor :
    public WxTrackableFrame,
    public FLinkedObjEdNotifyInterface,
    public FDockingParent,
    public FNotifyHook,
    public FSerializableObject
{
public:

    WxMyGraphEditor( wxWindow* Parent, wxWindowID ID, UMyGraph* MyGraph );
    ~WxMyGraphEditor();
    
    // FLinkedObjEdNotifyInterface interfaces
    virtual void EdHandleKeyInput(FViewport* Viewport, FName Key, EInputEvent Event);
    
    virtual void DrawObjecs(FViewport* Viewport, FCanvas* Canvas);
    
    virtual void EmptySelection();
    virtual void AddToSelection( UObject* Obj );
    virtual void RemoveFromSelection( UObject* Obj );
    virtual UBOOL IsInSelection( UObject* Obj ) const;
    virtual INT GetNumSelected() const;
    virtual void MoveSelectedObjects( INT DeltaX, INT DeltaY );
    
    virtual void SetSelectedConnector( FLinkedObjectConnector& Connector );
    virtual FIntPoint GetSelectedConnLocation( FCanvas* Canvas );
    virtual INT GetSelectedConnType();
    virtual FColor GetMakingLinkColor();
    virtual void MakeConnectionToConnector( FLinkedObjectConnector& Connector );

    // FSerializableObject interface
    virtual void Serialize( FArchive& Archive );
    
    // FDockingParent interfaces
    virtual const TCHAR* GetDockingParentName() const  { return TEXT("MyGraphEditor"); }
    virtual const TCHAR* GetDockingParentVersion() const  { return 0; }        
    
private:

    UMyGraph* m_MyGraph;

    FLinkedObjViewportClient*   m_LinkedObjVC;
    WxPropertyWindowHost*       m_PropertyWindowHost;
    
    TArray<UMyGraphNode*>       m_SelNodes;
    FLinkedobjectConnector      m_SelConnector;    
};     
編輯器類別建構子:
WxMyAssetEditor::WxMyGraphEditor( wxWindow* Parent, wxWindowID ID, UMyAsset* MyGraph ) :
    WxTrackableFrame( Parent, ID, TEXT(""), wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_sTYLE | wxFRAME_FLOAT_ON_PARENT | wxFRAME_NO_TASKBAR ),
    FDockingParent(this),
    m_MyGraph(MyGraph),
    m_PropertyWindowHost(NULL),
    m_LinkedObjVC(NULL),
    m_SelConnector( NULL, LOC_INPUT, INDEX_NONE )    
{
    SetTitle( *FString::Printf(LocalizeSecure(LocalizeUnreaEd("MyGraphEditor_F"), *m_MyGraph-&gt;GetPathName())) );

    SetSize(800, 600);
    FWindowUtil::LoadPosSize( TEXT("MyGraphEditor"), this, 256, 256, 800, 600);
    
    WxLinkedObjVCHolder* LinkedObjVCHolder = new WxLinkedObjVCHolder( this, -1, this );
    m_LinkedObjVC = LinkedObjVCHolder->LinkedObjVC;
    
    m_LinkedObjVC->Origin2D.X = 150;
    m_LinkedObjVC->Origin2D.Y = 300;
    
    m_PropertyWindowHost = new WxPropertyWindowHost;
    m_PropertyWindowHost->Create( this, this );
    m_PropertyWindowHost->SetObject( m_MyGraph, EPropertyWindowFlags::ExpandCategories );    
    
    AddDockingWindow( LinkedObjVCHolder, FDockingParent::DH_None, NULL );
    AddDockingWindow( m_PropertyWindowHost, FDockingParent::DH_Bottom, *FString::Printf(LocalizeSecure(LocalizeUnreaEd("PropertiesCaption_F"), *m_MyGraph-&gt;GetPathName())), *LocalizeUnreadEd("Properties") );
    
    LoadDockingLayout();
    
    ...
}

輸入


在此簡單示範如何實作Insert鍵新增節點:
void WxMyGraphEditor::EdHandleKeyInput(FViewport* Viewport, FName Key, EInputEvent Event)
{
    if( Key == KEY_Insert && Event == IE_Pressed )
    {
        INT X = (m_LinkedObjVC->NewX - m_LinkedObjVC->Origin2D.X) / m_LinkedObjVC->Zoom2D;
        INT Y = (m_LinkedObjVC->NewY - m_LinkedObjVC->Origin2D.Y) / m_LinkedObjVC->Zoom2D;
        
        UMyGraphNode* Node = ConstructObject<UMyGraphNode>( UMyGraphNode::StaticClass(), m_MyGraph, NAME_None, RF_Transactional );
        m_MyGraph->Nodes.AddItem(Node);
        
        m_LinkedObjVC->Invalidate(); // Force it to redraw.
    }
}

繪圖


覆載DrawObjects()函式繪製節點和連線:
void WxMyGraphEditor::DrawObjecs(FViewport* Viewport, FCanvas* Canvas)
{
    for(INT i=0; i<m_MyGraph->Nodes.Num(); i++)
    {
        UMyGraphNode* Node = m_MyGraph->Nodes(i);
        if( Node )
        {
            Node->DrawNode( Canvas, IsInSelection(Node) );
        }    
    }
    
    // To ensure links uncovered, draw them in another pass.   
    for(INT i=0; i<m_MyGraph->Nodes.Num(); i++)
    {
        UMyGraphNode* Node = m_MyGraph->Nodes(i);
        if( Node )
        {
            Node->DrawLinks( Canvas, m_SelNodes );
        }    
    }        
}
節點類別定義:
class MyGraphNode extends Object
    native;
    
var() name NodeName;

var editoronly IntPoint DrawPos;

var editoronly int DrawWidth, DrawHeight;

struct native MyInputLink
{
    var string Name;
};

var export array<MyInputLink> InputLinks;

struct native MyOutputLink
{
    var string Name;
    var MyGraphNode LinkedNode;
    var int LinkedNodeInputIndex;
};

var export array<MyOutputLink> OutputLinks;

var editoronly array<int> InputY;
var editoronly array<int> OutputY;


cpptext
{
    virtual void DrawNode(FCanvas* Canvas, UBOOL bSelected);
    virtual void DrawLinks(FCanvas* Canvas, const TArray<UMyGraphNode*>& SelectedNodes);
    virtual FIntPoint GetConnectionLocation(INT ConnType, INT ConnIndex);
}


defaultproperties
{
    InputLinks(0)=(Name="In")
    OutputLinks(0)=(Name="Out")
}
節點類別實作:
void UMyGraphNode::DrawNode(FCanvas* Canvas, UBOOL bSelected)
{
    const FColor& BorderColor = bSelected ? FColor(200, 200, 0) : FColor(100, 100, 100);
    const FColor& FillColor = FColor(50, 50, 50);
    const FColor& InputColor = FColor(128, 0, 0);
    const FColor& OutputColor = FColor(0, 128, 0);
    
    FLinkedObjDrawInfo ObjInfo;      
    ObjInfo.ObjObject = this;
    
    for(INT i=0; i<InputLinks.Num(); ++i)
    {
        ObjInfo.Inputs.AddItem(FLinkedObjConnInfo( *InputLinks(i).Name, InputColor ));
    } 
    
    for(INT i=0; i<OutputLinks.Num(); ++i)
    {
        ObjInfo.Outputs.AddItem(FLinkedObjConnInfo( *OutputLinks(i).Name, OutputColor ));
    }
    
    FLinkedObjDrawUtils::DrawLinkedObj( Canvas, ObjInfo, (NodeName==NAME_None) ? TEXT("") : *NodeName.ToString(), NULL, BorderColor, FillColor, DrawPos );
    
    // Remember draw info to calculate connector locations
    DrawWidth = ObjInfo.DrawWidth;
    DrawHeight = ObjInfo.DrawHeight;
    InputY = ObjInfo.InputY;        
    OutputY = ObjInfo.OutputY;        
}

void UMyGraphNode::DrawLinks(FCanvas* Canvas, const TArray<UMyGraphNode*>& SelectedNodes)
{
    UBOOL bSelfSelected = SelectedNodes.ContainsItem(this);
    
    for(INT i=0; i<OutputLinks.Num(); ++i)
    {
        FMyOutputLink& Link = OutputLinks(i);
        if( Link.LinkedNode )
        {
            const FColor& LinkColor = (bSelfSelected || SelectedNodes.ContainsItem(Link.LinkedNode)) ? FColor(255, 255, 0) : FColor(40, 40, 40);
            
            FIntPoint Start = GetConnectionLocation( LOC_OUTPUT, i );
            FIntPoint End   = Link.LinkedNode->GetConnectionLocation( LOC_INPUT, Link.LinkedNodeInputIndex );
            const FLOAT Tension = Abs<INT>(Start.X - End.X);
            
            FLinkedObjDrawUtils::DrawSpline( Canvas, Start, Tension*FVector2D(1,0), End, Tension*FVector2D(1,0), LinkColor, TRUE );
        }
    } 
}

FIntPoint UMyGraphNode::GetConnectionLocation(INT ConnType, INT ConnIndex)
{
    switch( ConnType )
    {
    case LOC_INPUT:
        return FIntPoint( DrawPos.X - LO_CONNECTOR_lENGTH, InputY(ConnIndex) );
    case LOC_OUTPUT:
        return FIntPoint( DrawPos.X + DrawWidth + LO_CONNECTOR_lENGTH, OutputY(ConnIndex) );
    }
    
    return FIntPoint(0, 0);
}

選取

void WxMyGraphEditor::EmptySelection()
{
    m_SelNodes.Empty();
    
    m_PropertyWindowHost->SetObject( m_MyGraph, EPropertyWindowFlags::ExpandCategories );    
}

void WxMyGraphEditor::AddToSelection( UObject* Obj )
{
    m_SelNodes.AddItem( (UMyGraphNode*)Obj );
    
    m_PropertyWindowHost->SetObjectArray( m_SelNodes, EPropertyWindowFlags::ExpandCategories );    
}

void WxMyGraphEditor::RemoveFromSelection( UObject* Obj )
{
    m_SelNodes.RemoveItem( (UMyGraphNode*)Obj );
    
    m_PropertyWindowHost->SetObjectArray( m_SelNodes, EPropertyWindowFlags::ExpandCategories );    
}

UBOOL WxMyGraphEditor::IsInSelection( UObject* Obj ) const
{
    return m_SelNodes.ContainsItem( (UMyGraphNode*)Obj );
}

INT WxMyGraphEditor::GetNumSelected() const
{
    return m_SelNodes.Num();
}

void WxMyGraphEditor::MoveSelectedObjects( INT DeltaX, INT DeltaY )
{
    for(INT i=0; i<m_SelNodes.Num(); i++)
    {
        UMyGraphNode* Node = m_SelNodes(i);
        if( Node )
        {
            Node->DrawPos.X += DeltaX;
            Node->DrawPos.Y += DeltaY;
        }        
    }
}

連結

void WxMyAssetEditor::SetSelectedConnector( FLinkedObjectConnector& Connector )
{
    m_SelConnector = Connector;
}

FIntPoint WxMyAssetEditor::GetSelectedConnLocation( FCanvas* Canvas )
{
    UMyGraphNode* Node = Cast<UMyGraphNode>( m_SelConnector.ConnObj );
    if( Node )
    {
        return Node->GetConnectionLocation( m_SelConnector.ConnType, m_SelConnector.ConnIndex ); 
    }
    return FIntPoint(0,0);
}

INT WxMyAssetEditor::GetSelectedConnType()
{
    return m_SelConnector.ConnType;
}

FColor WxMyAssetEditor::GetMakingLinkColor()
{
    return FColor(255, 128, 0);
}

void WxMyAssetEditor::MakeConnectionToConnector( FLinkedObjectConnector& Connector )
{
    UMyGraphNode* StartNode = Cast<UMyGraphNode>( m_SelConnector.ConnObj );
    UMyGraphNode* EndNode   = Cast<UMyGraphNode>( Connector.ConnObj );
    
    if( StartNode && EndNode && StartNode != EndNode )
    {
        if( m_SelConnector.ConnType == LOC_INPUT && Connector.ConnType == LOC_OUTPUT )
        {
            FMyOutputLink& Output = EndNode->OutputLinks(Connector.ConnIndex);
            Output.LinkedNode           = StartNode;
            Output.LinkedNodeInputIndex = m_SelConnector.ConnIndex;
            
            m_MyGraph->MakePackageDirty(); 
        }
        else if( m_SelConnector.ConnType == LOC_OUTPUT && Connector.ConnType == LOC_INPUT )
        {
            FMyOutputLink& Output = StartNode->OutputLinks(m_SelConnector.ConnIndex);
            Output.LinkedNode           = EndNode;
            Output.LinkedNodeInputIndex = Connector.ConnIndex;
            
            m_MyGraph->MakePackageDirty(); 
        }        
    }
}

沒有留言:

張貼留言