在UE插件開發中,時常會用到場景預覽窗口的功能,也經常會有點選場景里的物體而同步改變工具界面的需求,網上教程多為講解如何打開一個預覽界面。在最近的一次需求開發中,我粗讀了關卡編輯器和藍圖編輯器的Viewport代碼,從中篩選出了點選的相關邏輯,本文記錄了一個源碼中尋找需要功能的過程。
LevelEditor
點擊Actor
功能:關卡編輯器下的點選Actor
相關的類(主要是LevelEditor模塊):
1、FLevelEditorViewportClient、FEditorViewportClient
2、LevelViewportClickedHandlers
3、SLevelViewport
4、UUnrealEdEngine(依賴模塊UnrealEd)
復現方式:在FLevelEditorViewportClient的InputKey方法下打斷點然后進去看調用棧
virtual bool InputKey(FViewport* Viewport, int32 ControllerId, FKey Key, EInputEvent Event, float AmountDepressed = 1.f, bool bGamepad=false) override;
他的調用棧如下圖:

可以看到他是在處理一個MouseButtonDown的事件,UE的按鍵事件遵循一個職責鏈的設計模式,由上層的SViewport(我猜測是整個UE的Viewport,沒有去考證)先進行處理,然后一路傳遞到下面的子類FLevelEditorViewportClient,調用到InputKey
然后這個InputKey做了很多的事情,包括:計算click的location,檢測有其他的按鍵按下(Alt
Ctrl
),還有一些處理光照和大氣的代碼(這些做移植的時候可以刪掉),比較關鍵的是他中間調用了父類的InputKey
bool bHandled = FEditorViewportClient::InputKey(InViewport,ControllerId,Key,Event,AmountDepressed,bGamepad);
父類做的事情也很多,但最重要的還是他調用ProcessClickInViewport函數,這個函數組出了一個HHitProxy對象,這個函數內部調用到了ProcessClick,ProcessClick又被FLevelEditorViewportClient重寫了,因此點擊代碼核心就是重寫InputKey和重寫ProcessClick函數
一個HHitJProxy對象里包括了點選的是哪個Actor,點選了他的哪個Component

編輯器可以根據點擊操作的不同(雙擊、按住Alt點擊等)去讓界面做對應的變化,比如在藍圖編輯器下就只顯示Comp被選中的輪廓,在關卡編輯器下就是優先選中Actor,如果這個Actor有父Actor那么會優先選中父Actor之類的,這就是ProcessClick函數里應該做的事情。
他首先是判斷選中是一個什么對象(WidgetAxis、Actor、xxxVert等等),我們關注的主要是如果他是一個Actor,那么在移植的時候有些不必要的分支就都可以刪掉了(WidgetAxis不要動,貌似是選中坐標軸的)
在Actor有關的邏輯里他列舉了如果要選中Comp的幾個條件:
// We want to process the click on the component only if:
// 1. The actor clicked is already selected
// 2. The actor selected is the only actor selected
// 3. The actor selected is blueprintable
// 4. No components are already selected and the click was a double click
// 5. OR, a component is already selected and the click was NOT a double click
const bool bActorAlreadySelectedExclusively = GEditor->GetSelectedActors()->IsSelected(ConsideredActor) && (GEditor->GetSelectedActorCount() == 1);
const bool bActorIsBlueprintable = FKismetEditorUtilities::CanCreateBlueprintOfClass(ConsideredActor->GetClass());
const bool bComponentAlreadySelected = GEditor->GetSelectedComponentCount() > 0;
const bool bWasDoubleClick = (Click.GetEvent() == IE_DoubleClick);
const bool bSelectComponent = bActorAlreadySelectedExclusively && bActorIsBlueprintable && (bComponentAlreadySelected != bWasDoubleClick);
if (bSelectComponent)
{
LevelViewportClickHandlers::ClickComponent(this, ActorHitProxy, Click);
}
else
{
LevelViewportClickHandlers::ClickActor(this, ConsideredActor, Click, true);
}
LevelViewportClickHandlers是一個命名空間,這塊代碼有些函數沒有xx_API,表示沒有dll導出,不是對其他模塊公開的,因此可以將其內部的關鍵函數實現抄出來形成自己的版本
其他的error,一般引用頭文件+添加模塊依賴就可以解決
當ActorSelection有變動的時候,一般會做一些事件廣播,可以實現一些原本被選中的物體接到事件取消選中的外輪廓等等的效果,這塊的代碼的位置比較復雜,在LevelEditor中,他享受的待遇很好,直接給寫到了UnrealEdEngine里
void UUnrealEdEngine::UpdateFloatingPropertyWindowsFromActorList(const TArray<UObject*>& ActorList, bool bForceRefresh)
{
FLevelEditorModule& LevelEditor = FModuleManager::LoadModuleChecked<FLevelEditorModule>(TEXT("LevelEditor"));
LevelEditor.BroadcastActorSelectionChanged(ActorList, bForceRefresh);
}
可以看到他其實就是把一個Actor數組傳進來然后刷新一下
但我們的待遇就沒這么好了,需要自己手動調一下這個事件,我選擇將其添加剛剛抄出來的命名空間下的ClickActor的SelectActor的后面
TArray<UObject*> Objects;
Objects.Add(Actor);
FModelShapeEditorModule::BroadcastActorSelectionChanged(Objects);
這里我搞了個靜態函數來做這件事
關卡編輯器他在這個模塊的實現類上注冊了一個OnActorSelectionChanged,用於同步ActorDetail面板的變化(如果沒有可以去掉這段)
void SLevelEditor::OnActorSelectionChanged(const TArray<UObject*>& NewSelection, bool bForceRefresh)
{
for( auto It = AllActorDetailPanels.CreateIterator(); It; ++It )
{
TSharedPtr<SActorDetails> ActorDetails = It->Pin();
if( ActorDetails.IsValid() )
{
ActorDetails->SetObjects(NewSelection, bForceRefresh || bNeedsRefresh);
}
else
{
// remove stray entries here
}
}
bNeedsRefresh = false;
}
在SLevelViewport里他注冊了一個同名函數,然后里面負責修改ViewportClient里的EngineShowFlags,SetSelectionOutline和選中的外輪廓有關,這塊需要修改修改抄過來
void SLevelViewport::OnActorSelectionChanged(const TArray<UObject*>& NewSelection, bool bForceRefresh)
{
// On the first actor selection after entering Game View, enable the selection show flag
if (IsVisible() && IsInGameView() && NewSelection.Num() != 0)
{
if( LevelViewportClient->bAlwaysShowModeWidgetAfterSelectionChanges )
{
LevelViewportClient->EngineShowFlags.SetModeWidgets(true);
}
LevelViewportClient->EngineShowFlags.SetSelection(true);
LevelViewportClient->EngineShowFlags.SetSelectionOutline(GetDefault<ULevelEditorViewportSettings>()->bUseSelectionOutline);
}
bNeedToUpdatePreviews = true;
}
把兩個核心的函數以及委托實現了,基本上就可以選中Actor了,並且可以有外輪廓,按下w
鍵可以顯示坐標軸

但是此時拖拽坐標軸還不能改變物體在world中的位置,還需要重寫很多函數
拖拽Axis
功能:拖拽Axis,改變他在world中的位置
需要重寫或復制的函數
virtual bool InputAxis(FViewport* Viewport, int32 ControllerId, FKey Key, float Delta, float DeltaTime, int32 NumSamples, bool bGamepad) override;
virtual void TrackingStarted( const struct FInputEventState& InInputState, bool bIsDraggingWidget, bool bNudge ) override;
virtual void TrackingStopped() override;
virtual void Tick(float DeltaSeconds) override;
/** Project the specified actors into the world according to the current drag parameters */
void ProjectActorsIntoWorld(const TArray<AActor*>& Actors, FViewport* Viewport, const FVector& Drag, const FRotator& Rot);
virtual bool InputWidgetDelta( FViewport* Viewport, EAxisList::Type CurrentAxis, FVector& Drag, FRotator& Rot, FVector& Scale ) override;
void ApplyDeltaToActor( AActor* InActor, const FVector& InDeltaDrag, const FRotator& InDeltaRot, const FVector& InDeltaScale );
void ApplyDeltaToActors( const FVector& InDrag, const FRotator& InRot, const FVector& InScale );
void ApplyDeltaToComponent(USceneComponent* InComponent, const FVector& InDeltaDrag, const FRotator& InDeltaRot, const FVector& InDeltaScale);
/** Helper functions for ApplyDeltaTo* functions - modifies scale based on grid settings */
void ModifyScale( AActor* InActor, FVector& ScaleDelta, bool bCheckSmallExtent = false ) const;
/**
* Helper function for ApplyDeltaTo* functions - modifies scale based on grid settings.
* Currently public so it can be re-used in FEdModeBlueprint.
*/
void ModifyScale( USceneComponent* InComponent, FVector& ScaleDelta ) const;
void ValidateScale(const FVector& InOriginalPreDragScale, const FVector& CurrentScale, const FVector& BoxExtent,
FVector& ScaleDelta, bool bCheckSmallExtent = false) const;
/** @return Returns true if the delta tracker was used to modify any selected actors or BSP. Must be called before EndTracking(). */
bool HaveSelectedObjectsBeenChanged() const;
還是和之前一樣的思路,但是需要注意,如果有對應的父類函數,一般都調用一下,函數中涉及的變量也要一並摘出自己的版本,在Tracking函數里用到了不少變量
private:
FTrackingTransaction TrackingTransaction;
/** A map of actor locations before a drag operation */
mutable TMap<TWeakObjectPtr<AActor>, FTransform> PreDragActorTransforms;
BlueprintEditor
功能:點擊藍圖類底下的Comp
相關類:
1、SSCSEditorViewport
2、FBlueprintEditor
3、SListView
4、SSCSEditor
這里就不一步一步寫了,因為我也沒有細看
注意的點,粗看是點擊事件先由TreeWidget接受響應,然后把viewport里的渲染響應綁定到了ListView的OnSelectionChange上(指每個UI條目的選中)
總結
抄源碼功能總結就一句話——多退少補。
子明
2021.8.11