// Copyright Epic Games, Inc. All Rights Reserved. #include "PoseSearchDebuggerView.h" #include "Animation/TrajectoryTypes.h" #include "Editor.h" #include "IGameplayProvider.h" #include "Modules/ModuleManager.h" #include "PoseSearch/PoseSearchDatabase.h" #include "PoseSearch/PoseSearchDerivedData.h" #include "PoseSearch/PoseSearchSchema.h" #include "PoseSearchDatabaseEditor.h" #include "PoseSearchDebugger.h" #include "PoseSearchDebuggerDatabaseRowData.h" #include "PoseSearchDebuggerDatabaseView.h" #include "PoseSearchDebuggerReflection.h" #include "PoseSearchDebuggerViewModel.h" #include "PoseSearchEditor.h" #include "PropertyEditorModule.h" #include "Subsystems/AssetEditorSubsystem.h" #include "Trace/PoseSearchTraceProvider.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SNumericEntryBox.h" #include "Widgets/Layout/SWidgetSwitcher.h" #include "Widgets/SBoxPanel.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Views/SListView.h" #define LOCTEXT_NAMESPACE "PoseSearchDebugger" namespace UE::PoseSearch { FText GenerateSearchName(const FTraceMotionMatchingStateMessage& MotionMatchingState, const IGameplayProvider* GameplayProvider) { TStringBuilder<256> StringBuilder; if (GameplayProvider) { bool bAddDatabasesSeparator = false; for (const FTraceMotionMatchingStateDatabaseEntry& DbEntry : MotionMatchingState.DatabaseEntries) { if (bAddDatabasesSeparator) { StringBuilder.Append(" - "); } const FObjectInfo& DatabaseObjectInfo = GameplayProvider->GetObjectInfo(DbEntry.DatabaseId); StringBuilder.Append(DatabaseObjectInfo.Name); bAddDatabasesSeparator = true; } if (MotionMatchingState.Roles.Num() > 1) { if (MotionMatchingState.Roles.Num() != MotionMatchingState.SkeletalMeshComponentIds.Num()) { StringBuilder.Append("Error!"); } else { StringBuilder.Append(" ["); bool bAddRolesSeparator = false; for (int32 RoleIndex = 0; RoleIndex < MotionMatchingState.Roles.Num(); ++RoleIndex) { if (bAddRolesSeparator) { StringBuilder.Append(" - "); } StringBuilder.Append(MotionMatchingState.Roles[RoleIndex].ToString()); StringBuilder.Append(": "); const FObjectInfo& SkeletalMeshComponentObjectInfo = GameplayProvider->GetObjectInfo(MotionMatchingState.SkeletalMeshComponentIds[RoleIndex]); const FObjectInfo& SkeletalMeshComponentOuterObjectInfo = GameplayProvider->GetObjectInfo(SkeletalMeshComponentObjectInfo.GetOuterId()); StringBuilder.Append(SkeletalMeshComponentOuterObjectInfo.Name); bAddRolesSeparator = true; } StringBuilder.Append("]"); } } } else { StringBuilder.Append("Error!"); } return FText::FromString(StringBuilder.ToString()); } class SDebuggerMessageBox : public SCompoundWidget { SLATE_BEGIN_ARGS(SDebuggerMessageBox) {} SLATE_END_ARGS() void Construct(const FArguments& InArgs, const FString& Message) { ChildSlot [ SNew(SVerticalBox) + SVerticalBox::Slot() .HAlign(HAlign_Center) .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(FText::FromString(Message)) .Font(FAppStyle::Get().GetFontStyle("DetailsView.CategoryFontStyle")) ] ]; } }; void SDebuggerDetailsView::Construct(const FArguments& InArgs) { ParentDebuggerViewPtr = InArgs._Parent; // Add property editor (detail view) UObject to world root so that it persists when PIE is stopped Reflection = NewObject(); Reflection->AddToRoot(); check(IsValid(Reflection)); // @TODO: Convert this to a custom builder instead of of a standard details view // Load property module and create details view with our reflection UObject FPropertyEditorModule& PropPlugin = FModuleManager::LoadModuleChecked("PropertyEditor"); FDetailsViewArgs DetailsViewArgs; DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea; DetailsViewArgs.DefaultsOnlyVisibility = EEditDefaultsOnlyNodeVisibility::Hide; Details = PropPlugin.CreateDetailView(DetailsViewArgs); Details->SetObject(Reflection); ChildSlot [ Details.ToSharedRef() ]; } void SDebuggerDetailsView::Update(const FTraceMotionMatchingStateMessage& State) const { UpdateReflection(State); } SDebuggerDetailsView::~SDebuggerDetailsView() { // Our previously instantiated object attached to root may be cleaned up at this point if (UObjectInitialized()) { Reflection->RemoveFromRoot(); } } void SDebuggerDetailsView::UpdateReflection(const FTraceMotionMatchingStateMessage& State) const { check(Reflection); Reflection->InterruptMode = State.InterruptMode; Reflection->ElapsedPoseSearchTime = State.ElapsedPoseSearchTime; Reflection->AssetPlayerTime = State.AssetPlayerTime; Reflection->LastDeltaTime = State.DeltaTime; Reflection->SimLinearVelocity = State.SimLinearVelocity; Reflection->SimAngularVelocity = State.SimAngularVelocity; Reflection->AnimLinearVelocity = State.AnimLinearVelocity; Reflection->AnimAngularVelocity = State.AnimAngularVelocity; Reflection->Playrate = State.Playrate; Reflection->AnimLinearVelocityNoTimescale = State.AnimLinearVelocityNoTimescale; Reflection->AnimAngularVelocityNoTimescale = State.AnimAngularVelocityNoTimescale; } void SDebuggerView::Construct(const FArguments& InArgs, uint64 InAnimInstanceId, int32 InWantedSearchId) { ViewModel = InArgs._ViewModel; OnViewClosed = InArgs._OnViewClosed; // Validate the existence of the passed getters check(ViewModel.IsBound()) check(OnViewClosed.IsBound()); AnimInstanceId = InAnimInstanceId; WantedSearchId = InWantedSearchId; SelectedSearchId = InWantedSearchId; ChildSlot [ SAssignNew(DebuggerView, SVerticalBox) + SVerticalBox::Slot() .FillHeight(1.0f) .VAlign(VAlign_Fill) .HAlign(HAlign_Fill) [ SAssignNew(Switcher, SWidgetSwitcher) .WidgetIndex(this, &SDebuggerView::SelectView) // [0] Selection view before node selection is made + SWidgetSwitcher::Slot() .Padding(40.0f) .HAlign(HAlign_Fill) .VAlign(VAlign_Fill) [ SAssignNew(SelectionView, SVerticalBox) ] // [1] Node selected; node debugger view + SWidgetSwitcher::Slot() [ GenerateNodeDebuggerView() ] // [2] Occluding message box when stopped (no recording) + SWidgetSwitcher::Slot() [ SNew(SDebuggerMessageBox, "Record gameplay to begin debugging") ] // [3] Occluding message box when recording + SWidgetSwitcher::Slot() [ SNew(SDebuggerMessageBox, "Recording...") ] // [4] Occluding message box when there is no data for the selected MM node + SWidgetSwitcher::Slot() [ GenerateNoDataMessageView() ] ] ]; } void SDebuggerView::SetTimeMarker(double InTimeMarker) { if (FDebugger::IsPIESimulating()) { return; } TimeMarker = InTimeMarker; } void SDebuggerView::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { if (FDebugger::IsPIESimulating()) { return; } const UWorld* DebuggerWorld = FDebugger::GetWorld(); check(DebuggerWorld); const bool bSameTime = FMath::Abs(TimeMarker - PreviousTimeMarker) < DOUBLE_SMALL_NUMBER; PreviousTimeMarker = TimeMarker; TSharedPtr Model = ViewModel.Get(); check(Model.IsValid()); // We haven't reached the update point yet if (CurrentConsecutiveFrames < ConsecutiveFramesUpdateThreshold) { // If we're on the same time marker, it is consecutive if (bSameTime) { ++CurrentConsecutiveFrames; } } else { // New frame after having updated, reset consecutive frames count and start counting again if (!bSameTime) { CurrentConsecutiveFrames = 0; bUpdated = false; } // Haven't updated since passing through frame gate, update once else if (!bUpdated) { Model->OnUpdate(); if (UpdateNodeSelection()) { Model->OnUpdateSearchSelection(SelectedSearchId); UpdateViews(); } bUpdated = true; } } // Draw features if (const FTraceMotionMatchingStateMessage* State = Model->GetMotionMatchingState()) { FRoleToIndex RoleToIndex; TArray AnimContextsData; TArray AnimContexts; TArray PoseHistories; UWorld* World = nullptr; const int32 NumRoles = State->Roles.Num(); RoleToIndex.Reserve(NumRoles); AnimContextsData.SetNum(NumRoles); AnimContexts.SetNum(NumRoles); PoseHistories.SetNum(NumRoles); for (int32 RoleIndex = 0; RoleIndex < NumRoles; ++RoleIndex) { const uint64 ActorSkeletalMeshComponentId = State->SkeletalMeshComponentIds[RoleIndex]; if (const TWeakObjectPtr* ActorPtr = Model->GetDebugDrawActors().Find(ActorSkeletalMeshComponentId)) { if (ActorPtr->IsValid()) { const FRole& Role = State->Roles[RoleIndex]; for (UActorComponent* ActorComponent : (*ActorPtr)->GetInstanceComponents()) { if (UPoseSearchMeshComponent* PoseSearchMeshComponent = Cast(ActorComponent)) { World = PoseSearchMeshComponent->GetWorld(); RoleToIndex.Add(Role) = RoleIndex; AnimContextsData[RoleIndex].AddObjectParam(PoseSearchMeshComponent); AnimContexts[RoleIndex] = &AnimContextsData[RoleIndex]; PoseHistories[RoleIndex] = &State->PoseHistories[RoleIndex]; break; } } } } } // checking if all roles have been resolved properly if (RoleToIndex.Num() != NumRoles) { return; } // Draw world space trajectory #if ENABLE_ANIM_DEBUG const bool bDrawTrajectory = Model->GetDrawTrajectory(); const bool bDrawHistory = Model->GetDrawHistory(); if (bDrawTrajectory || bDrawHistory) { for (int32 RoleIndex = 0; RoleIndex < NumRoles; ++RoleIndex) { const FTransformTrajectory& Trajectory = State->PoseHistories[RoleIndex].Trajectory; if (bDrawTrajectory) { UTransformTrajectoryBlueprintLibrary::DebugDrawTrajectory(Trajectory, World); } if (bDrawHistory) { State->PoseHistories[RoleIndex].DebugDraw(World, FColor::Red); } } } #endif // Draw query vector if (Model->GetDrawQuery()) { if (const UPoseSearchDatabase* CurrentDatabase = Model->GetCurrentDatabase()) { for (const FTraceMotionMatchingStateDatabaseEntry& DbEntry : State->DatabaseEntries) { const UPoseSearchDatabase* Database = FTraceMotionMatchingStateMessage::GetObjectFromId(DbEntry.DatabaseId); if (Database && Database == CurrentDatabase && EAsyncBuildIndexResult::Success == FAsyncPoseSearchDatabasesManagement::RequestAsyncBuildIndex(CurrentDatabase, ERequestAsyncBuildFlag::ContinueRequest) && DbEntry.QueryVector.Num() == Database->Schema->SchemaCardinality) { FDebugDrawParams DrawParams(AnimContexts, PoseHistories, RoleToIndex, CurrentDatabase); DrawParams.DrawFeatureVector(DbEntry.QueryVector); break; } } } } // Draw selected poses const TSharedPtr>>& DatabaseRows = DatabaseView->GetDatabaseRows(); TArray> SelectedRows = DatabaseRows->GetSelectedItems(); // Draw any selected database vectors constexpr int32 MaxRowsToDraw = 250; const int32 NumRowsToDraw = FMath::Min(MaxRowsToDraw, SelectedRows.Num()); for (int32 RowIdx = 0; RowIdx < NumRowsToDraw; ++RowIdx) { const TSharedRef& Row = SelectedRows[RowIdx]; const UPoseSearchDatabase* RowDatabase = Row->SharedData->SourceDatabase.Get(); if (EAsyncBuildIndexResult::Success == FAsyncPoseSearchDatabasesManagement::RequestAsyncBuildIndex(RowDatabase, ERequestAsyncBuildFlag::ContinueRequest)) { FDebugDrawParams DrawParams(AnimContexts, PoseHistories, RoleToIndex, RowDatabase); DrawParams.DrawFeatureVector(Row->PoseIdx); } } // Draw active pose TArray> ActiveRows = DatabaseView->GetActiveRow()->GetSelectedItems(); // Active row should only have 0 or 1 check(ActiveRows.Num() < 2); if (!ActiveRows.IsEmpty()) { const UPoseSearchDatabase* Database = ActiveRows[0]->SharedData->SourceDatabase.Get(); if (EAsyncBuildIndexResult::Success == FAsyncPoseSearchDatabasesManagement::RequestAsyncBuildIndex(Database, ERequestAsyncBuildFlag::ContinueRequest)) { // Use the motion-matching state's pose idx, as the active row may be update-throttled at this point FDebugDrawParams DrawParams(AnimContexts, PoseHistories, RoleToIndex, Database); DrawParams.DrawFeatureVector(ActiveRows[0]->PoseIdx); } } // Draw continuing pose TArray> ContinuingRows = DatabaseView->GetContinuingPoseRow()->GetSelectedItems(); // ContinuingPose row should only have 0 or 1 check(ContinuingRows.Num() < 2); if (!ContinuingRows.IsEmpty()) { const UPoseSearchDatabase* Database = ContinuingRows[0]->SharedData->SourceDatabase.Get(); if (EAsyncBuildIndexResult::Success == FAsyncPoseSearchDatabasesManagement::RequestAsyncBuildIndex(Database, ERequestAsyncBuildFlag::ContinueRequest)) { FDebugDrawParams DrawParams(AnimContexts, PoseHistories, RoleToIndex, Database); DrawParams.DrawFeatureVector(ContinuingRows[0]->PoseIdx); } } } // synchronizing the model DrawQuery state with all the open PoseSearchDatabaseEditor(s) const bool bDrawQuery = Model->GetDrawQuery(); if (UAssetEditorSubsystem* AssetEditorSS = GEditor->GetEditorSubsystem()) { TArray EditedAssets = AssetEditorSS->GetAllEditedAssets(); for (UObject* EditedAsset : EditedAssets) { if (Cast(EditedAsset)) { if (IAssetEditorInstance* Editor = AssetEditorSS->FindEditorForAsset(EditedAsset, false)) { if (Editor->GetEditorName() == FName("PoseSearchDatabaseEditor")) { FDatabaseEditor* DatabaseEditor = static_cast(Editor); DatabaseEditor->SetDrawQueryVector(bDrawQuery); } } } } } } bool SDebuggerView::UpdateNodeSelection() { TSharedPtr Model = ViewModel.Get(); check(Model.IsValid()); const TArray& MotionMatchingStates = Model->GetMotionMatchingStates(); // Update selection view if no node selected if (SelectedSearchId != InvalidSearchId) { if (!MotionMatchingStates.IsEmpty()) { // making sure SelectedSearchId is still valid. if not, let's pick the first available MotionMatchingStates for (const FTraceMotionMatchingStateMessage& MotionMatchingState : MotionMatchingStates) { if (MotionMatchingState.GetSearchId() == SelectedSearchId) { return true; } } if (WantedSearchId == InvalidSearchId) { // SelectedSearchId is not valid, and since there's no WantedSearchId, by specifically double clicking the // search track instead of selecting "Pose Search" track, we reassign SelectedSearchId to the first valid SearchId SelectedSearchId = MotionMatchingStates[0].GetSearchId(); } } return true; } // Only one active state, bypass selection view if (MotionMatchingStates.Num() == 1) { SelectedSearchId = MotionMatchingStates[0].GetSearchId(); return true; } // Create selection view with buttons for each node, displaying the database name SelectionView->ClearChildren(); if (!MotionMatchingStates.IsEmpty()) { IRewindDebugger* RewindDebugger = IRewindDebugger::Instance(); const TraceServices::IAnalysisSession* AnalysisSession = RewindDebugger->GetAnalysisSession(); check(AnalysisSession); const IGameplayProvider* GameplayProvider = AnalysisSession->ReadProvider("GameplayProvider"); TraceServices::FAnalysisSessionReadScope SessionReadScope(*AnalysisSession); for (const FTraceMotionMatchingStateMessage& MotionMatchingState : MotionMatchingStates) { Model->OnUpdateSearchSelection(MotionMatchingState.GetSearchId()); SelectionView->AddSlot() .HAlign(HAlign_Fill) .VAlign(VAlign_Center) .Padding(10.0f) [ SNew(SButton) .Text(GenerateSearchName(MotionMatchingState, GameplayProvider)) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .ContentPadding(10.0f) .OnClicked(this, &SDebuggerView::OnUpdateSearchSelection, MotionMatchingState.GetSearchId()) ]; } } return false; } void SDebuggerView::UpdateViews() const { if (const FTraceMotionMatchingStateMessage* State = ViewModel.Get()->GetMotionMatchingState()) { DatabaseView->Update(*State); DetailsView->Update(*State); } } TArray> SDebuggerView::GetSelectedDatabaseRows() const { return DatabaseView->GetDatabaseRows()->GetSelectedItems(); } int32 SDebuggerView::SelectView() const { // Currently recording if (FDebugger::IsPIESimulating() && FDebugger::IsRecording()) { return RecordingMsg; } // Data has not been recorded yet if (FDebugger::GetRecordingDuration() < DOUBLE_SMALL_NUMBER) { return StoppedMsg; } const TSharedPtr Model = ViewModel.Get(); check(Model.IsValid()); const bool bNoActiveNodes = Model->GetNodesNum() == 0; const bool bNodeSelectedWithoutData = SelectedSearchId != InvalidSearchId && Model->GetMotionMatchingState() == nullptr; // No active nodes, or node selected has no data if (bNoActiveNodes || bNodeSelectedWithoutData) { return NoDataMsg; } // Node not selected yet, showcase selection view if (SelectedSearchId == InvalidSearchId) { return Selection; } // Standard debugger view return Debugger; } void SDebuggerView::OnPoseSelectionChanged(const UPoseSearchDatabase* Database, int32 DbPoseIdx, float Time) { const TSharedPtr Model = ViewModel.Get(); check(Model.IsValid()); if (const FTraceMotionMatchingStateMessage* State = Model->GetMotionMatchingState()) { DetailsView->Update(*State); } } FReply SDebuggerView::OnUpdateSearchSelection(int32 InSelectedSearchId) { // InvalidSearchId will backtrack to selection view SelectedSearchId = InSelectedSearchId; bUpdated = false; return FReply::Handled(); } TSharedRef SDebuggerView::GenerateNoDataMessageView() { TSharedRef ReturnButtonView = GenerateReturnButtonView(); ReturnButtonView->SetVisibility(TAttribute::CreateLambda([this]() -> EVisibility { // Hide the return button for the no data message if we have no nodes at all return ViewModel.Get()->GetNodesNum() > 0 ? EVisibility::Visible : EVisibility::Hidden; })); return SNew(SVerticalBox) + SVerticalBox::Slot() [ SNew(SDebuggerMessageBox, "No recorded data available for the selected frame") ] + SVerticalBox::Slot() .AutoHeight() .Padding(20.0f) .HAlign(HAlign_Center) .VAlign(VAlign_Center) [ ReturnButtonView ]; } TSharedRef SDebuggerView::GenerateReturnButtonView() { return SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(10, 5, 0, 0) .AutoWidth() [ SNew(SButton) .VAlign(VAlign_Center) .Visibility_Lambda([this] { return ViewModel.Get()->GetNodesNum() > 1 ? EVisibility::Visible : EVisibility::Hidden; }) .ButtonStyle(FAppStyle::Get(), "SimpleButton") .ContentPadding( FMargin(1, 0) ) .OnClicked(this, &SDebuggerView::OnUpdateSearchSelection, InvalidSearchId) // Contents of button, icon then text [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .VAlign(VAlign_Center) [ SNew(SImage) .Image(FAppStyle::Get().GetBrush("Icons.CircleArrowLeft")) .ColorAndOpacity(FSlateColor::UseForeground()) ] + SHorizontalBox::Slot() .AutoWidth() .Padding(FMargin(8.0f, 0.0f, 0.0f, 0.0f)) .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(FText::FromString("Return to Search Selection")) .Justification(ETextJustify::Center) ] ] ] +SHorizontalBox::Slot() .VAlign(VAlign_Top) .HAlign(HAlign_Left) .Padding(64, 5, 0, 0) .AutoWidth() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .Padding(0, 5, 0, 0) [ SNew(SCheckBox) .IsChecked_Lambda([this] { return ViewModel.Get()->GetDrawQuery() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; }) .OnCheckStateChanged_Lambda([this](ECheckBoxState State) { ViewModel.Get()->SetDrawQuery(State == ECheckBoxState::Checked); }) [ SNew(STextBlock) .Text(LOCTEXT("PoseSearchDebuggerDrawQuery", "Draw Query")) ] ] ] +SHorizontalBox::Slot() .VAlign(VAlign_Top) .HAlign(HAlign_Left) .Padding(64, 5, 0, 0) .AutoWidth() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .Padding(0, 5, 0, 0) [ SNew(SCheckBox) .IsChecked_Lambda([this] { return ViewModel.Get()->GetDrawTrajectory() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; }) .OnCheckStateChanged_Lambda([this](ECheckBoxState State) { ViewModel.Get()->SetDrawTrajectory(State == ECheckBoxState::Checked); }) [ SNew(STextBlock) .Text(LOCTEXT("PoseSearchDebuggerDrawTrajectory", "Draw Trajectory")) ] ] ] +SHorizontalBox::Slot() .VAlign(VAlign_Top) .HAlign(HAlign_Left) .Padding(64, 5, 0, 0) .AutoWidth() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .Padding(0, 5, 0, 0) [ SNew(SCheckBox) .IsChecked_Lambda([this] { return ViewModel.Get()->GetDrawHistory() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; }) .OnCheckStateChanged_Lambda([this](ECheckBoxState State) { ViewModel.Get()->SetDrawHistory(State == ECheckBoxState::Checked); }) [ SNew(STextBlock) .Text(LOCTEXT("PoseSearchDebuggerDrawHistory", "Draw History")) ] ] ] + SHorizontalBox::Slot() .VAlign(VAlign_Top) .HAlign(HAlign_Left) .Padding(64, 5, 0, 0) .AutoWidth() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .Padding(0, 5, 0, 0) [ SNew(SCheckBox) .IsChecked_Lambda([this] { return ViewModel.Get()->IsVerbose() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; }) .OnCheckStateChanged_Lambda([this](ECheckBoxState State) { ViewModel.Get()->SetVerbose(State == ECheckBoxState::Checked); UpdateViews(); }) [ SNew(STextBlock) .Text(LOCTEXT("PoseSearchDebuggerShowVerbose", "Channels Breakdown")) ] ] ]; } TSharedRef SDebuggerView::GenerateNodeDebuggerView() { return SNew(SSplitter) .Orientation(Orient_Vertical) .ResizeMode(ESplitterResizeMode::Fill) // Database view + SSplitter::Slot() .Value(0.8f) [ SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() [ GenerateReturnButtonView() ] + SVerticalBox::Slot() [ SAssignNew(DatabaseView, SDebuggerDatabaseView) .Parent(SharedThis(this)) .OnPoseSelectionChanged(this, &SDebuggerView::OnPoseSelectionChanged) ] ] // Details panel view + SSplitter::Slot() .Value(0.2f) [ SAssignNew(DetailsView, SDebuggerDetailsView) .Parent(SharedThis(this)) ]; } FName SDebuggerView::GetName() const { static const FName DebuggerName("PoseSearchDebugger"); return DebuggerName; } uint64 SDebuggerView::GetObjectId() const { return AnimInstanceId; } SDebuggerView::~SDebuggerView() { OnViewClosed.Execute(AnimInstanceId); } } // namespace UE::PoseSearch #undef LOCTEXT_NAMESPACE