UE4点选源码分析

在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数组传进来然后刷新一下

但我们的待遇就没这么好了,需要自己手动调一下这个事件,我选择将其添加刚刚抄出来的命名空间下的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键可以显示坐标轴

但是此时拖拽坐标轴还不能改变物体在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

Tags: