UE4點選原始碼分析

飛翔的子明發表於2021-08-11

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

他的呼叫棧如下圖:

UE4點選原始碼分析

可以看到他是在處理一個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

UE4點選原始碼分析

編輯器可以根據點選操作的不同(雙擊、按住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陣列傳進來然後重新整理一下

但我們的待遇就沒這麼好了,需要自己手動調一下這個事件,我選擇將其新增剛剛抄出來的名稱空間下的ClickActorSelectActor的後面

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鍵可以顯示座標軸

UE4點選原始碼分析

但是此時拖拽座標軸還不能改變物體在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

相關文章