|
0. 前言
上次跟着教程搞了下CommonUI,感觉不痛不痒,所以想通过查阅源代码的方式看下CommonUI的工作流程以及熟悉一下Unreal Engine的一系列操作。DebugGame Editor目前的所有操作都是在这个模式下启动的,本人是虚幻小白,所以可能有部分理解不是很充分的地方,望指正。
上篇:
项目资源(注意是Test分支):
1. UCommonGameViewportClient的初始化
为什么要从这个开始呢,因为各个教程都把这个文件的载入作为第一步,每个教程都必须将Project Setting->General Setting->Game Viewport Client设置为CommonUI Client的类,说明这个文件还挺重要的,而我们看他的创建与执行,可以梳理Unreal Engine对多数部件的加载逻辑。
要开始这个部分十分简单,只需要给其父类,也就是UGameViewportClient的构造函数打上断点就可以了,他有两个构造函数,其中我在调试过程中有效的是UGameViewportClient::UGameViewportClient(const FObjectInitializer& ObjectInitializer) ,打上断点,我们就可以看见调用栈了。
Client初始化调用栈
1.1 通过调用栈向上看Tick
以一个新手的视角,从命名上看,最下面的三层栈看起来像一个启动过程,到第四层第五层,看见Tick和Loop有点门道了,第五层的void FEngineLoop::Tick() 应该就是整个Unreal Engine主循环的一部分,这个主循环里面调用了UEngine::Tick(float DeltaSeconds, bool bIdleMode)并且从调用栈中可以看出,我们的GEngine是UEngine子类UUnrealEdEngine实例化,具体引擎内部C++代码如下。
// LaunchEngineLoop.cpp
void FEngineLoop::Tick() {
...
// main game engine tick (world, game objects, etc.)
GEngine->Tick(FApp::GetDeltaTime(), bIdleMode); //-----------------------------------1
...}
// Engine.h
UCLASS(abstract, config=Engine, defaultconfig, transient)
class ENGINE_API UEngine
: public UObject
, public FExec{
...
virtual void Tick( float DeltaSeconds, bool bIdleMode ) PURE_VIRTUAL(UEngine::Tick,);
...}
//--------------------------------------EditorEngine-------------------------------------------------
// EditorEngine.h
UCLASS(config=Engine, transient)
class UNREALED_API UEditorEngine : public UEngine {
...
virtual void Tick(float DeltaSeconds, bool bIdleMode) override;
...}
// EditorEngine.cpp
void UEditorEngine::Tick( float DeltaSeconds, bool bIdleMode ) {
...
// Kick off a Play Session request if one was queued up during the last frame.
// 所以看起来Editor系统和直接进行游戏好像有点不一样,触发PIE的命令在UEditorEngine的Tick中,
// 当我们在引擎Editor中申请说,我要使用PIE了,然后就会申请一个PlaySessionRequest,
// 这样进入StartQueuedPlaySessionRequest函数可以初始化我们PIE的世界。
if (PlaySessionRequest.IsSet()) {
StartQueuedPlaySessionRequest(); //--------------------------------------------------3
}
...}
//-------------------------------------UnrealEdEngine-------------------------------------------------
// UnrealEdEngine.h
class UNREALED_API UUnrealEdEngine : public UEditorEngine, public FNotifyHook
// UnrealEdEngine.cpp
void UUnrealEdEngine::Tick(float DeltaSeconds, bool bIdleMode) {
Super::Tick( DeltaSeconds, bIdleMode ); // Call EditorEngine.cpp-----------------------------2
...}
1.2 Tick之后的触发条件
从上面的代码可以看出来,Tick其实是长时间不断执行的,而唯一改变了的是PlaySessionRequest的Set状态,我们其实很容易想到,这是由于我们按下了PIE,我们设置PlaySessionRequest里面的bIsSet状态作为修改断点(因为修改断点只能打在很小的变量上),再重新使用PIE,调用栈如下
PlaySessionRequest设置成功调用栈
可以看见ToolBarButtonBlock的OnClick 调用了PlayInViewport_Clicked函数,并且其又调用了RequestPlaySession ,这个时候才真正设置好我们在PIE中的请求。
1.3 如何获得类及初始化命令
在DefaultEngine.ini中可以发现,所以我们在Project Setting中应该修改的就是这个文件。
// DefaultEngine.ini
[/Script/Engine.Engine]
+ActiveGameNameRedirects=(OldGameName="TP_FirstPerson",NewGameName="/Script/LearningProject")
+ActiveGameNameRedirects=(OldGameName="/Script/TP_FirstPerson",NewGameName="/Script/LearningProject")
+ActiveClassRedirects=(OldClassName="TP_FirstPersonProjectile",NewClassName="LearningProjectProjectile")
+ActiveClassRedirects=(OldClassName="TP_FirstPersonGameMode",NewClassName="LearningProjectGameMode")
+ActiveClassRedirects=(OldClassName="TP_FirstPersonCharacter",NewClassName="LearningProjectCharacter")
GameViewportClientClassName=/Script/CommonUI.CommonGameViewportClient随后可以在Engine.h中找到GameViewportClientClassName与GameViewportClientClass
UCLASS(abstract, config=Engine, defaultconfig, transient)
class ENGINE_API UEngine
: public UObject
, public FExec{
...
UPROPERTY()
TSubclassOf<class UGameViewportClient> GameViewportClientClass;
/** Sets the class to use for the game viewport client, which can be overridden to change game-specific input and display behavior. */
UPROPERTY(globalconfig, noclear, EditAnywhere, Category=DefaultClasses, meta=(MetaClass=&#34;GameViewportClient&#34;, DisplayName=&#34;Game Viewport Client Class&#34;, ConfigRestartRequired=true))
FSoftClassPath GameViewportClientClassName;
...}
在UEditorEngine::CreateInnerProcessPIEGameInstance中可以获得Engine中的GameViewportClientClass,也就是这里是开始创建CommonUI Client的时候,下面给出Client初始化调用栈的呼叫逻辑。
void UEditorEngine::StartQueuedPlaySessionRequest() //检查,运行调用,调用完毕重置Request
↓
void UEditorEngine::StartQueuedPlaySessionRequestImpl() //检查,检查是否多个运行,分支三种情况运行调用,
↓
void UEditorEngine::CreateNewPlayInEditorInstance(FRequestPlaySessionParams &InRequestParams, const bool bInDedicatedInstance, const EPlayNetMode InNetMode)
↓
void UEditorEngine::OnLoginPIEComplete_Deferred(int32 LocalUserNum, bool bWasSuccessful, FString ErrorString, FPieLoginStruct DataStruct)
↓
UGameInstance* UEditorEngine::CreateInnerProcessPIEGameInstance(FRequestPlaySessionParams& InParams, const FGameInstancePIEParameters& InPIEParameters, int32 InPIEInstanceIndex) {
...
ViewportClient = NewObject<UGameViewportClient>(this, GameViewportClientClass);
ViewportClient->Init(*PieWorldContext, GameInstance);
...
GameViewport = ViewportClient;
GameViewport->bIsPlayInEditorViewport = true;
// Update our World Context to know which Viewport Client is associated.
PieWorldContext->GameViewport = ViewportClient;
...}
在写Lua的时候就可以经常看见我们需要调用Context来获取很多东西,说明Context是我们需要理解的一个大头,在初始化完毕之后,我们的GameViewport也被纳入了Context。
2. UCommonGameViewportClient的运行时
Client运行时肯定需要触发按键输入响应,我们可以分析下Engine怎么触发的响应,Client怎么处理的响应。
2.1 Input响应调用栈
首先是输入进来时的调用栈,这个调用栈中鼠标键盘还有区别,从WindowsApplication.cpp出现差别
int32 FWindowsApplication::ProcessDeferredMessage( const FDeferredWindowsMessage& DeferredMessage )
// 查看了这个函数,发现是一个大型的信息分装机器,这里为分水岭也是在合适不过了。
// 可以发现所有的信息响应也经过了void FEngineLoop::Tick()这个函数,主循环无疑了
鼠标左键输入
键盘T输入
不过他中间的信息传递过程似乎还很复杂,并且是专门给Windows做的,内部还有很多Windows API,但是仔细看了下,应该还行
void FEngineLoop::Tick() {
...
// Don&#39;t pump messages if we&#39;re running embedded as the outer application
// will pass us messages instead.
if (!GUELibraryOverrideSettings.bIsEmbedded) {
GEngine->SetInputSampleLatencyMarker(CurrentFrameCounter);
//QUICK_SCOPE_CYCLE_COUNTER(STAT_PumpMessages);
FPlatformApplicationMisc::PumpMessages(true); // --------------------------------1
}
...}
主循环FEngineLoop::Tick()
void FWindowsPlatformApplicationMisc::PumpMessages(bool bFromMainLoop) {
... // 检查是否从主循环中进来
WinPumpMessages(); // ---------------------------------------------------------------------------2
...
}
static void WinPumpMessages() { // 都是WinAPI,以Message的方式检测输入
TRACE_CPUPROFILER_EVENT_SCOPE(WinPumpMessages); {
MSG Msg;
while( PeekMessage(&Msg,NULL,0,0,PM_REMOVE) ) {
TranslateMessage( &Msg );
DispatchMessage( &Msg );
}
}
}
这个都是WinAPI,接收函数就是FWindowsApplication::AppWndProc
// Defined as a global so that it can be extern&#39;d by UELibrary
LRESULT WindowsApplication_WndProc(HWND hwnd, uint32 msg, WPARAM wParam, LPARAM lParam) {
ensure( IsInGameThread() );
return WindowsApplication->ProcessMessage( hwnd, msg, wParam, lParam ); // ------------------------5
}
/** Windows callback for message processing (forwards messages to the FWindowsApplication instance). */
LRESULT CALLBACK FWindowsApplication::AppWndProc(HWND hwnd, uint32 msg, WPARAM wParam, LPARAM lParam) {
return WindowsApplication_WndProc( hwnd, msg, wParam, lParam ); // --------------------------------4
}
接收Windows传来的函数
int32 FWindowsApplication::ProcessMessage( HWND hwnd, uint32 msg, WPARAM wParam, LPARAM lParam ) {
...
case WM_KEYDOWN:
case WM_SYSKEYUP:
case WM_KEYUP:
case WM_LBUTTONDBLCLK:
case WM_LBUTTONDOWN:
case WM_MBUTTONDBLCLK:
case WM_MBUTTONDOWN:
case WM_RBUTTONDBLCLK:
case WM_RBUTTONDOWN:
case WM_XBUTTONDBLCLK:
case WM_XBUTTONDOWN:
case WM_XBUTTONUP:
case WM_LBUTTONUP:
case WM_MBUTTONUP:
case WM_RBUTTONUP:
case WM_NCMOUSEMOVE:
case WM_MOUSEMOVE:
case WM_MOUSEWHEEL:
case WM_TOUCH: {
// -------------------------------------------------------------------------7
DeferMessage( CurrentNativeEventWindowPtr, hwnd, msg, wParam, lParam );
// Handled
return 0;
}
break;
// Mouse Movement
case WM_INPUT: {
... // 可能有点参考价值?
}
...
}
HWND是Windows的窗口句柄主要是对Windows输入进行分类处理,各个宏的意义都可以在网上查到,还是挺清晰的。
void FWindowsApplication::DeferMessage( TSharedPtr<FWindowsWindow>& NativeWindow, HWND InHWnd, uint32 InMessage, WPARAM InWParam, LPARAM InLParam, int32 MouseX, int32 MouseY, uint32 RawInputFlags ) {
if( GPumpingMessagesOutsideOfMainLoop && bAllowedToDeferMessageProcessing ) {
DeferredMessages.Add( FDeferredWindowsMessage( NativeWindow, InHWnd, InMessage, InWParam, InLParam, MouseX, MouseY, RawInputFlags ) );
}
else {
// When not deferring messages, process them immediately
// -------------------------------------------------------------------------------------------8
ProcessDeferredMessage( FDeferredWindowsMessage( NativeWindow, InHWnd, InMessage, InWParam, InLParam, MouseX, MouseY, RawInputFlags ) );
}
}
这个部分主要是判断下,我们是不是要立即处理这个DeferMessage,如果不延迟,那么就立即执行,如果延迟,那么就把他加入一个TArray<FDeferredWindowsMessage> DeferredMessages
下一个栈就是ProcessDeferredMessage,这个部分其实总体逻辑很清晰,也是我们发出各个输入信号的分水岭,可以看一看,不过不看也不是那么重要了。最后到CommonUI Client会获得一个FInputKeyEventArgs也就是虚幻打包好的一个输入事件,具体什么时候打包的,调用栈会告诉你答案。
2.2 Input处理
从源代码中可以看见CommonUI的Client继承了几个Input函数,并且专门设置了RouterHandle,我们专门从Key分析下他是怎么操作的。逻辑是InputKey是响应函数的最外层,首先从进去查看一下是不是比需要处理的UI按键更加高级的命令IsKeyPriorityAboveUI ,如果不是,那么首先将Result设置为Unhandled,先通过 RerouteInput委托执行,如果委托没有绑定函数,那么去自己定义的HandleRerouteInput执行,如果Result在这里面执行好了,并且设置成为了Handled,那么就返回True,不然就返回False。
最主要的处理过程就是ERouteUIInputResult InputResult = ActionRouter->ProcessInput(Key, EventType);
bool UCommonGameViewportClient::InputKey(const FInputKeyEventArgs& InEventArgs) {
const FInputKeyEventArgs& EventArgs = InEventArgs;
if (IsKeyPriorityAboveUI(EventArgs)) {
return true;
}
// The input is fair game for handling - the UI gets first dibs
#if !UE_BUILD_SHIPPING
if (ViewportConsole && !ViewportConsole->ConsoleState.IsEqual(NAME_Typing) && !ViewportConsole->ConsoleState.IsEqual(NAME_Open))
#endif
{
FReply Result = FReply::Unhandled();
if (!OnRerouteInput().ExecuteIfBound(EventArgs.ControllerId, EventArgs.Key, EventArgs.Event, Result)) {
HandleRerouteInput(EventArgs.ControllerId, EventArgs.Key, EventArgs.Event, Result);
}
if (Result.IsEventHandled()) {
return true;
}
}
return Super::InputKey(EventArgs);
}
bool UCommonGameViewportClient::IsKeyPriorityAboveUI(const FInputKeyEventArgs& EventArgs) {
#if !UE_BUILD_SHIPPING
// First priority goes to the viewport console regardless any state or setting
if (ViewportConsole && ViewportConsole->InputKey(EventArgs.ControllerId, EventArgs.Key, EventArgs.Event, EventArgs.AmountDepressed, EventArgs.IsGamepad())) {
return true;
}
#endif
// We&#39;ll also treat toggling fullscreen as a system-level sort of input that isn&#39;t affected by input filtering
if (TryToggleFullscreenOnInputKey(EventArgs.Key, EventArgs.Event)) {
return true;
}
return false;
}
void UCommonGameViewportClient::HandleRerouteInput(int32 ControllerId, FKey Key, EInputEvent EventType, FReply& Reply) {
ULocalPlayer* LocalPlayer = GameInstance->FindLocalPlayerFromControllerId(ControllerId);
Reply = FReply::Unhandled();
if (LocalPlayer) {
UCommonUIActionRouterBase* ActionRouter = LocalPlayer->GetSubsystem<UCommonUIActionRouterBase>();
if (ensure(ActionRouter)) {
ERouteUIInputResult InputResult = ActionRouter->ProcessInput(Key, EventType);
if (InputResult == ERouteUIInputResult::BlockGameInput) {
// We need to set the reply as handled otherwise the input won&#39;t actually be blocked from reaching the viewport.
Reply = FReply::Handled();
// Notify interested parties that we blocked the input.
OnRerouteBlockedInput().ExecuteIfBound(ControllerId, Key, EventType, Reply);
}
else if (InputResult == ERouteUIInputResult::Handled) {
Reply = FReply::Handled();
}
}
}
}
// GameViewportClient.cpp
bool UGameViewportClient::TryToggleFullscreenOnInputKey(FKey Key, EInputEvent EventType) {
if ((Key == EKeys::Enter && EventType == EInputEvent::IE_Pressed && FSlateApplication::Get().GetModifierKeys().IsAltDown() && GetDefault<UInputSettings>()->bAltEnterTogglesFullscreen)
|| (IsRunningGame() && Key == EKeys::F11 && EventType == EInputEvent::IE_Pressed && GetDefault<UInputSettings>()->bF11TogglesFullscreen && !FSlateApplication::Get().GetModifierKeys().AreModifersDown(EModifierKey::Control | EModifierKey::Alt))) {
HandleToggleFullscreenCommand();
return true;
}
return false;
}
在上一个项目中,我很好奇CommonUI到底是以什么逻辑处理的Input,因为每次到这个部分的时候,其实几个委托都是Unbound的状态,并且怎么也搜不到这几个委托在哪里被绑定了,就很奇怪。如果这几个委托都没有绑定,那么这个类存在的意义是什么呢?然后我尝试将CommonUI Client换回原本的Client,结果发现之前设计的逻辑竟然都能够直接使用!那说明了,如果我们全部输入都Custom的话,那么其实这个大的窗口是不需要换的,不过我也找到了一些需要CommonUI Client触发的操作,其中一个就是Back操作,而要通过Client响应这个Back,我们还需要执行以下的步骤,也就是官方教程中新手完全看不懂的部分
首先右键内容浏览器,在Miscellaneous类别中找到Data Table创建,然后进去之后创建一行,名字任意取,然后按自己需要绑定键盘输入、手柄输入、触控输入(如果需要的话)
我把这一行名字设置为NativeBack,并且设置键盘B键和手柄Face Button Right(也就是XBox的B键)可以触发
然后创建一个CommonUIInputData为基类的蓝图,将我们设置好的按键放在默认Back Action中
测试下Back
然后在Project Setting->Game->Common Input Settings中,将InputData设置为我们刚刚创建的CommonUIInputData,这时候,我们配置已经完成了。打开上次的工程,将HowToPlay和HowToPlayDetail的两个CommonActivatableWidget的IsBackHandler勾选上,至此,我们就可以使用B键对其进行销毁(使用Stack模式),现在我们来查看Back的调用栈,断点打在UCommonActivatableWidget::HandleBackAction()
第一次进入ProcessNormalInput,注意bCanReceiveInput
第二次进入ProcessNormalInput,注意bCanReceiveInput
可以清楚地看见我们触发Back的最上层CommonUI入口的确是CommonGameViewportClient,并且处理函数也是和我们之前说过的是相应的ActionRouter->ProcessInput(Key, EventType);这样想想,似乎ActionRouter才是CommonUI的处理中心,Client只是为部分原生数据提供支持的地方,不过不论怎么样,先把这个部分看完再说。
第一张图可以看到,从Client进去之后,就是UCommonUIActionRouterBase的秀场了,我们的Back信号并没有被其Process函数内的各个特殊判断给吸收,最后被一个Lambda函数吸收了,并且调用到了
ActiveRootNode->ProcessNormalInput(ActiveMode, Key, Event);
...
const auto ProcessNormalInputFunc = [Key, ActiveMode, this](EInputEvent Event) {
bool bHandled = PersistentActions->ProcessNormalInput(ActiveMode, Key, Event);
if (!bHandled && ActiveRootNode && bIsActivatableTreeEnabled) {
bHandled = ActiveRootNode->ProcessNormalInput(ActiveMode, Key, Event);
}
return bHandled;
};
...
// Even if no widget cares about this input, we don&#39;t want to let anything through to the actual game while we&#39;re in menu mode
bHandledInput = ProcessNormalInputFunc(InputEvent);
...
也就到了我们之前截图的调用栈,可以从第一张图得知,我们目前的ActiveRootNode就是我们设置的Container,Container有三个子节点,分别就是三个菜单,这个时候为什么其他的菜单无法对这个进行响应,可以聚焦到各个TreeNode的bCanReceiveInput成员变量,这个成员变量控制了是否能够接收Input信息,这个大概就是压栈出栈会改变的状态(每个Container内部不应该只有一个是Active的吗,那不是直接取就行了?),然后在能够接收到Input的子节点,会进入函数FActionRouterBindingCollection::ProcessNormalInput这里面会将我们之前设置的Action遍历一遍,再把Action里面的键位Mapping遍历一遍,找到对应的Binding、Key、Mapping、Action之后执行Binding,也就是Widget的HandleBackAction(),蓝图可以Override BP_OnHandleBackAction方法,就可以实现自定义Back。
bool FActionRouterBindingCollection::ProcessNormalInput(ECommonInputMode ActiveInputMode, FKey Key, EInputEvent InputEvent) const {
for (FUIActionBindingHandle BindingHandle : ActionBindings) {
if (TSharedPtr<FUIActionBinding> Binding = FUIActionBinding::FindBinding(BindingHandle)) {
if (ActiveInputMode == ECommonInputMode::All || ActiveInputMode == Binding->InputMode) {
for (const FUIActionKeyMapping& KeyMapping : Binding->NormalMappings) {
// A persistent displayed action skips the normal rules for reachability, since it&#39;ll always appear in a bound action bar
const bool bIsDisplayedPersistentAction = Binding->bIsPersistent && Binding->bDisplayInActionBar;
if (KeyMapping.Key == Key && Binding->InputEvent == InputEvent && (bIsDisplayedPersistentAction || IsWidgetReachableForInput(Binding->BoundWidget.Get())))
{
// Just in case this was in the middle of a hold process with a different key, reset now
Binding->CancelHold();
Binding->OnExecuteAction.ExecuteIfBound();
if (Binding->bConsumesInput) {
return true;
}
}
}
}
}
}
return false;
}
不过这功能,感觉上是需要一个System的视角去看可能理解能够更加深刻,我目前对System的理解还不够,所以目前会认为这个功能其实挺鸡肋的。
3. CommonActivatableWidget的初始化
上面的例子已经说明了CommonUI提供的ViewportClient并不是限制我们使用CommonUI一些路由功能的关键,那么具体是谁影响了CommonUI的路由,还可以通过Widget的初始化判断,同时Widget初始化之后可能还会用广播影响RouteTree的行为,这里也需要考虑到,因为上一个项目中,我们在非CommonUI Client模式下也可以获得正确的Gamepad路由。一个CommonActivatableWidget从零到能用到底触发了多少东西,这对整个架构的理解都很重要。
新人提示:在Log界面输入Log LogCommonUI Verbose可以让CommonUI的log显示出来,但发现没什么用。经过调试,我初步判断出了一个CommonActivatableWidget初始化需要经过的步骤,主要与Widget的生命周期有关,因为与Widget有关的创建都是在Lua中执行的,所以在调用栈中会看到很多与Lua相关的栈空间,属正常现象。由于这章是枢纽,所以混杂的类可能会比较多。
3.1 Create/NewObject
首先是创建,UE还是挺方便的,我们可以把断点打在UCommonActivatableWidget的GENERATED_BODY()处,这样就可以查看调用栈,这里主要是初始化类中存在默认定义的变量,这些变量大部分都可以在蓝图中调整这个类对应的初始化方式,其他就是正常的调用父类方法,这部分没有太多特别的。
3.2 AddToViewport/Construct
这部分比较关键,在CommonUIActionRouterBase.cpp中,可以看见构建树的时候需要用到RebuiltWidgetsPendingNodeAssignment,里面存储的是待构建成为树节点的Widget,搜索这个变量可以定位到修改他的地方,把这个函数打上断点,查看调用栈可以发现,这是符合生命周期的,并且我们是从AddToViewport触发的Rebuild时期将Widget加入到RouterBase的待处理中。
AddToViewport调用栈
这里通过调用栈我们可以像上面一样查看下各个时期Unreal Engine的行为,其中主要是在RouterBase初始化时期,注册好了接受OnRebuilding委托函数,并且UCommonActivatableWidget::OnRebuilding设计为静态成员变量,所以只需要在RouterBase这里初始化一次所有的Widget都可以使用。
void UUserWidget::AddToScreen(ULocalPlayer* Player, int32 ZOrder) {
if ( !FullScreenWidget.IsValid() ) {
...
TSharedRef<SWidget> UserSlateWidget = TakeWidget(); // -----------------------------------------1
...
}
...
}
TSharedRef<SWidget> UWidget::TakeWidget() {
LLM_SCOPE(ELLMTag::UI);
return TakeWidget_Private( []( UUserWidget* Widget, TSharedRef<SWidget> Content ) -> TSharedPtr<SObjectWidget> {
return SNew( SObjectWidget, Widget )[ Content ];
} ); // --------------------------------------------------------------------------------------2
}
TSharedRef<SWidget> UWidget::TakeWidget_Private(ConstructMethodType ConstructMethod) {
...
PublicWidget = RebuildWidget(); // ----------------------------------------------------------------------3
...
}
TSharedRef<SWidget> UCommonActivatableWidget::RebuildWidget() {
// Note: the scoped builder guards against design-time so we don&#39;t need to here (as it&#39;d make the scoped lifetime more awkward to leverage)
//FScopedActivatableTreeBuilder ScopedBuilder(*this);
if (!IsDesignTime()) {
OnRebuilding.Broadcast(*this); // ---------------------------------------------------------------4
} ↑
return Super::RebuildWidget(); ↑
} ↑
↑
void UCommonUIActionRouterBase::Initialize(FSubsystemCollectionBase& Collection) { ↑
... ↑
UCommonActivatableWidget::OnRebuilding.AddUObject(this, &UCommonUIActionRouterBase::HandleActivatableWidgetRebuilding);
... ↓
} ↓
↓
void UCommonUIActionRouterBase::HandleActivatableWidgetRebuilding(UCommonActivatableWidget& RebuildingWidget){
if (RebuildingWidget.GetOwningLocalPlayer() == GetLocalPlayerChecked()){
RebuiltWidgetsPendingNodeAssignment.Add(&RebuildingWidget);
}
}
最后AddToViewport这一段生命周期是以Construct构建为结束的,所以可以在Widget的NativeConstruct中打个断点查看一下,这里就可以找到设置了默认Back响应的地方DefaultBackActionHandle = RegisterUIActionBinding(BindArgs);,而AddToViewport到Construct的生命周期似乎都写在了TSharedRef<SWidget> UWidget::TakeWidget_Private 可以Mark一下。
3.3 Activate
按理来说Activate进来之后运行的应该是ActivateWidget-InternalProcessActivation-NativeOnActivated的运行逻辑,但是这里调用栈写的是直接到了NativeOnActivated
Activate调用栈
而加入进树这个步骤,并不是由Activate引起的,而是引擎的Tick启动了Router的Tick,在Tick中判断了我们现在是否存在需要进入树的Widget,如果有,那么就把他们加入到树中,这一段放在上一小节逻辑更合理,但是从运行顺序上来说,启动的Activate比Tick更早执行。
bool UCommonUIActionRouterBase::Tick(float DeltaTime) {
QUICK_SCOPE_CYCLE_COUNTER(STAT_UCommonUIActionRouter_Tick);
if (PendingWidgetRegistrations.Num() > 0 || RebuiltWidgetsPendingNodeAssignment.Num() > 0) {
ProcessRebuiltWidgets();
}
...
}
//这是Tick运行中最主要的函数
void UCommonUIActionRouterBase::ProcessRebuiltWidgets() {
// 把所有的预选节点走一个判断根的流程
// FindOwningActivatable里面是不断的While循环找父亲,直到成为null,消耗可能会比较大?那个地方官方也打了一个TODO哈哈//
// 如果找到了已经在树里面了的父亲,会标记父子关系WidgetsByDirectParent
// Begin by organizing all of the widgets that need nodes according to their direct parent//
// 将所有的根收集起来,给他们建议棵树,所以可以明晰,整个Router维护的是一个森林,这点还蛮令我意外的
// 这应该也是路由的基础了,通过不同的按键可以在森林中Focus跳跃
// FActivatableTreeRoot::Create的时候就会触发Widget的函数绑定,并且把刚才说的执行逻辑立马给补上了
// void FActivatableTreeNode::Init() {
// RepresentedWidget->OnActivated().AddSP(this, &FActivatableTreeNode::HandleWidgetActivated);
// RepresentedWidget->OnDeactivated().AddSP(this, &FActivatableTreeNode::HandleWidgetDeactivated);
// if (RepresentedWidget->IsActivated()) {
// HandleWidgetActivated();
// }
// }
// 同时对于Root还创建了InputSubsystem.OnInputMethodChangedNative.AddSP(this, &FActivatableTreeRoot::HandleInputMethodChanged);
// 从代码逻辑上可以知道,似乎官方并没有给Widget和Node打互通的Activate数据,还需要HandleRootNodeActivated一次
// Widget和TreeNode各玩各的,HandleRootNodeActivated完成了绘制!
// Build a new tree for any new roots
// 处理树中父亲
// Now process any remaining entries - these are widgets that were rebuilt but should be appended to an existing node//
// 树中父亲处理完他还是孤儿状态,就表示出错了
if (WidgetsByDirectParent.Num() > 0)
{
//@todo DanH: Build a string to print all the remaining entries
ensureAlwaysMsgf(false, TEXT(&#34;Somehow we rebuilt a widget that is owned by an activatable, but no node exists for that activatable. This *should* be completely impossible.&#34;));
}
// 非根节点的其他Widget情况
// Now, we account for all the widgets that would like their actions bound
RebuiltWidgetsPendingNodeAssignment.Reset();
PendingWidgetRegistrations.Reset();
}
4. CommonActivatableWidget的运行时
4.1 Activate/Deactivate
我们可以在运行时控制Widget是否激活,首先需要Check的是激活与否的影响,Widget组件有Deactivate函数给我们调用,可以先查看这个部分的调用栈,并且检查Delegate以便我们知道下一步应该怎么做,这个时候发现了有一个Delegate出去的,指向FActivatableTreeNode::HandleWidgetDeactivated()
跳转过来之后发现这里也是一个Delegate,在下图可以看见这个Delegate指向的处理函数,在Router中,接着按照指示跳转到UCommonUIActionRouterBase::HandleRootNodeDeactivated
可以看见Deactivate的作用主要是控制控件树的关系,以及一个可选开关,用Activate和Deactivate控制Visibility(上一篇这里的Activate Visibility和Deactivate Visibility在我最开始写的时候理解错了,其就是是否用Activate控制Visibility,如果控制,切换的时候用什么)。
4.2 Focus
为了知道CommonUI怎么把Active信息与Unreal Engine Focus系统结合起来,这个挺重要,首先从结果方面入手,用之前的命令打开CommonUI的Log之后,手柄的移动就可以打印出来了
在项目中搜索target changed to的位置,就可以找到这个Log从哪里输出的,找到了在CommonAnalogCursor中的Tick函数,其中获得Widget路径的方式如下
void FCommonAnalogCursor::Tick(const float DeltaTime, FSlateApplication& SlateApp, TSharedRef<ICursor> Cursor) {
if (IsUsingGamepad() && IsGameViewportInFocusPathWithoutCapture()) {
const TSharedRef<FSlateUser> SlateUser = SlateApp.GetUser(GetOwnerUserIndex()).ToSharedRef();
...
// By default the cursor target is the focused widget itself, unless we&#39;re working with a list view
TSharedPtr<SWidget> CursorTarget = SlateUser->GetFocusedWidget();
...
}
...
}
// WeakFocusPath中,拿到了从Windows到FocusWidget这条路径上的所有组件,并且最后一级就是FocusWidget
TSharedPtr<SWidget> FSlateUser::GetFocusedWidget() const {
return WeakFocusPath.IsValid() ? WeakFocusPath.GetLastWidget().Pin() : nullptr;
}
现在我们需要找到,CommonUI是怎么找到WeakFocusPath的,其实经过我的Debug发现,并不是CommonUI驱动的寻找这个步骤,而是引擎本身就驱动了这个寻找步骤,CommonUI更关注的可能是让多个可以接受同一个Input的组件在同一个时刻展示,并且用很简单的开发逻辑实现这个过程,我在这里也写一下Unreal Engine是怎么调用的
首先接收到手柄带来的信号,按照这个信号去寻找最符合条件的,并且是Hittable的Widget,这时候我们对于找到的Widget有两种表达方式,一种方式是给出一个屏幕坐标,另一个方式是给出一个Slate层次结构的链
然后,将找到的Widget,经过处理给到SlateUser,这里的WidgetPath就是以链为表示的Widget路径
虽然表达起来两个步骤很简单,但是要能精确的找到也花费了我不少功夫。
5. 总结
从上面的表现来说,CommonUI更加适合处理复杂的Input,他可以用一个树状结构将Input用很少的开发代码送到对的地方,上面的测试确实还没有体现出CommonUI真正的厉害之处,所以我可能后期考虑做一个比较适合表现CommonUI功能性的一个UMG Demo,至于在手机游戏上,这种多层次的结构其实是很讨巧的,问题就在于与之前的兼容,其实感觉要兼容只需要在外面套个皮就可以了。 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|